mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
add tanstack/vue-virtual
This commit is contained in:
@@ -145,8 +145,8 @@
|
||||
</div>
|
||||
</Item>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" :side-offset="8" class="w-80 p-3">
|
||||
<!-- Group notifications -->
|
||||
<HoverCardContent side="left" :side-offset="8" class="w-100 p-3">
|
||||
<!-- Group -->
|
||||
<template v-if="isGroupType">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Avatar class="size-8 shrink-0 rounded">
|
||||
@@ -182,15 +182,19 @@
|
||||
<p class="text-xs text-muted-foreground">{{ typeLabel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="notification.details?.worldName" class="text-xs mb-1">
|
||||
<span class="text-muted-foreground">World: </span>{{ notification.details.worldName }}
|
||||
</p>
|
||||
<div v-if="notification.details?.worldId" class="text-xs mb-1">
|
||||
<Location
|
||||
:location="notification.details.worldId"
|
||||
:hint="notification.details.worldName || ''"
|
||||
:grouphint="notification.details.groupName || ''"
|
||||
link />
|
||||
</div>
|
||||
<p v-if="friendMessage" class="text-xs text-muted-foreground warp-break-words leading-relaxed">
|
||||
{{ friendMessage }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- Other notifications -->
|
||||
<!-- Other -->
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Avatar class="size-8 shrink-0">
|
||||
@@ -212,7 +216,6 @@
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- Time (always shown) -->
|
||||
<Separator v-if="absoluteTime" class="my-2" />
|
||||
<div v-if="absoluteTime" class="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||
<CalendarDays />{{ absoluteTime }}
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-if="sortedUnseenNotifications.length" class="flex flex-col gap-0.5 p-2">
|
||||
<NotificationItem
|
||||
v-for="n in sortedUnseenNotifications"
|
||||
:key="n.id + n.created_at"
|
||||
:notification="n"
|
||||
:is-unseen="true"
|
||||
@show-invite-response="$emit('show-invite-response', $event)"
|
||||
@show-invite-request-response="$emit('show-invite-request-response', $event)" />
|
||||
<div ref="scrollViewportRef" class="flex-1 overflow-y-auto">
|
||||
<div v-if="allRows.length" class="relative w-full px-1.5 py-2" :style="virtualContainerStyle">
|
||||
<template v-for="item in virtualItems" :key="item.virtualItem.key">
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full px-0.5 box-border"
|
||||
:data-index="item.virtualItem.index"
|
||||
:ref="virtualizer.measureElement"
|
||||
:style="{ transform: `translateY(${item.virtualItem.start}px)` }">
|
||||
<!-- Section header -->
|
||||
<div v-if="item.row.type === 'section-header'" class="flex items-center gap-2 px-2.5 py-2">
|
||||
<Separator class="flex-1" />
|
||||
<span class="shrink-0 text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
{{ item.row.label }}
|
||||
</span>
|
||||
<Separator class="flex-1" />
|
||||
</div>
|
||||
|
||||
<!-- Notification item -->
|
||||
<NotificationItem
|
||||
v-else-if="item.row.type === 'notification'"
|
||||
:notification="item.row.notification"
|
||||
:is-unseen="item.row.isUnseen"
|
||||
@show-invite-response="$emit('show-invite-response', $event)"
|
||||
@show-invite-request-response="$emit('show-invite-request-response', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="sortedRecentNotifications.length">
|
||||
<div class="flex items-center gap-2 px-4 py-2">
|
||||
<Separator class="flex-1" />
|
||||
<span class="shrink-0 text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
{{ t('side_panel.notification_center.past_notifications') }}
|
||||
</span>
|
||||
<Separator class="flex-1" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 px-2 pb-2">
|
||||
<NotificationItem
|
||||
v-for="n in sortedRecentNotifications"
|
||||
:key="n.id + n.created_at"
|
||||
:notification="n"
|
||||
:is-unseen="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!sortedUnseenNotifications.length && !sortedRecentNotifications.length"
|
||||
class="flex items-center justify-center p-8 text-sm text-muted-foreground">
|
||||
<div v-if="!allRows.length" class="flex items-center justify-center p-8 text-sm text-muted-foreground">
|
||||
{{ t('side_panel.notification_center.no_new_notifications') }}
|
||||
</div>
|
||||
|
||||
@@ -48,10 +46,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -65,6 +64,7 @@
|
||||
defineEmits(['show-invite-response', 'show-invite-request-response', 'navigate-to-table']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const scrollViewportRef = ref(null);
|
||||
|
||||
function getTs(n) {
|
||||
const raw = n?.created_at ?? n?.createdAt;
|
||||
@@ -77,4 +77,64 @@
|
||||
const sortedRecentNotifications = computed(() =>
|
||||
[...props.recentNotifications].sort((a, b) => getTs(b) - getTs(a))
|
||||
);
|
||||
|
||||
const allRows = computed(() => {
|
||||
const rows = [];
|
||||
for (const n of sortedUnseenNotifications.value) {
|
||||
rows.push({
|
||||
type: 'notification',
|
||||
key: `unseen:${n.id}`,
|
||||
notification: n,
|
||||
isUnseen: true
|
||||
});
|
||||
}
|
||||
if (sortedRecentNotifications.value.length) {
|
||||
rows.push({
|
||||
type: 'section-header',
|
||||
key: 'header:recent',
|
||||
label: t('side_panel.notification_center.past_notifications')
|
||||
});
|
||||
for (const n of sortedRecentNotifications.value) {
|
||||
rows.push({
|
||||
type: 'notification',
|
||||
key: `recent:${n.id}`,
|
||||
notification: n,
|
||||
isUnseen: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
const virtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: allRows.value.length,
|
||||
getScrollElement: () => scrollViewportRef.value,
|
||||
estimateSize: (index) => {
|
||||
const row = allRows.value[index];
|
||||
return row?.type === 'section-header' ? 32 : 56;
|
||||
},
|
||||
getItemKey: (index) => allRows.value[index]?.key ?? index,
|
||||
overscan: 8
|
||||
}))
|
||||
);
|
||||
|
||||
const virtualItems = computed(() => {
|
||||
const items = virtualizer.value?.getVirtualItems?.() ?? [];
|
||||
return items.map((virtualItem) => ({
|
||||
virtualItem,
|
||||
row: allRows.value[virtualItem.index]
|
||||
}));
|
||||
});
|
||||
|
||||
const virtualContainerStyle = computed(() => ({
|
||||
height: `${virtualizer.value?.getTotalSize?.() ?? 0}px`,
|
||||
width: '100%'
|
||||
}));
|
||||
|
||||
watch(allRows, () => {
|
||||
nextTick(() => {
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user