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