mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-02 21:16:07 +02:00
notifications v2 table
This commit is contained in:
@@ -42,7 +42,9 @@
|
||||
'group.queueReady',
|
||||
'moderation.warning.group',
|
||||
'moderation.report.closed',
|
||||
'instance.closed'
|
||||
'moderation.contentrestriction',
|
||||
'instance.closed',
|
||||
'economy.alert'
|
||||
]"
|
||||
:key="type"
|
||||
:value="type">
|
||||
@@ -89,7 +91,6 @@
|
||||
import { RefreshCw } from 'lucide-vue-next';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
@@ -97,10 +98,8 @@
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useGalleryStore,
|
||||
useGroupStore,
|
||||
useInviteStore,
|
||||
useNotificationStore,
|
||||
useUserStore,
|
||||
useVrcxStore
|
||||
} from '../../stores';
|
||||
import { DataTableLayout } from '../../components/ui/data-table';
|
||||
@@ -113,8 +112,6 @@
|
||||
import SendInviteResponseDialog from './dialogs/SendInviteResponseDialog.vue';
|
||||
import configRepository from '../../service/config';
|
||||
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { showGroupDialog } = useGroupStore();
|
||||
const { refreshInviteMessageTableData } = useInviteStore();
|
||||
const { clearInviteImageUpload } = useGalleryStore();
|
||||
const { notificationTable, isNotificationsLoading } = storeToRefs(useNotificationStore());
|
||||
@@ -126,7 +123,8 @@
|
||||
acceptRequestInvite,
|
||||
sendNotificationResponse,
|
||||
deleteNotificationLog,
|
||||
deleteNotificationLogPrompt
|
||||
deleteNotificationLogPrompt,
|
||||
openNotificationLink
|
||||
} = useNotificationStore();
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
@@ -294,38 +292,6 @@
|
||||
saveTableFilters();
|
||||
}
|
||||
|
||||
function openNotificationLink(link) {
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
const data = link.split(':');
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case 'group':
|
||||
showGroupDialog(data[1]);
|
||||
break;
|
||||
case 'user':
|
||||
showUserDialog(data[1]);
|
||||
break;
|
||||
case 'event':
|
||||
const ids = data[1].split(',');
|
||||
if (ids.length < 2) {
|
||||
console.error('Invalid event notification link:', data[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
showGroupDialog(ids[0]);
|
||||
// ids[1] cal_ is the event id
|
||||
break;
|
||||
case 'openNotificationLink':
|
||||
default:
|
||||
toast.error('Unsupported notification link type');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getSmallThumbnailUrl(url) {
|
||||
return convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
useLocationStore,
|
||||
useUiStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
useWorldStore,
|
||||
useNotificationStore
|
||||
} from '../../stores';
|
||||
|
||||
import Emoji from '../../components/Emoji.vue';
|
||||
@@ -61,6 +62,7 @@ export const createColumns = ({
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { isNotificationExpired } = useNotificationStore();
|
||||
|
||||
const canInvite = () => {
|
||||
const location = lastLocation.value?.location;
|
||||
@@ -385,7 +387,8 @@ export const createColumns = ({
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
if (original.type === 'boop') {
|
||||
const imageUrl = original.details?.imageUrl;
|
||||
const imageUrl =
|
||||
original.details?.imageUrl || original.imageUrl;
|
||||
if (!imageUrl || imageUrl.startsWith('default_')) {
|
||||
return null;
|
||||
}
|
||||
@@ -455,7 +458,28 @@ export const createColumns = ({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{original.message && original.title ? (
|
||||
<TooltipWrapper
|
||||
content={`${original.title}, ${original.message}`}
|
||||
delayDuration={500}
|
||||
>
|
||||
<span class="block w-full min-w-0 truncate">
|
||||
{`${original.title}, ${original.message}`}
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
) : null}
|
||||
{!original.message && original.title ? (
|
||||
<TooltipWrapper
|
||||
content={original.title}
|
||||
delayDuration={500}
|
||||
>
|
||||
<span class="block w-full min-w-0 truncate">
|
||||
{original.title}
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
) : null}
|
||||
{original.message &&
|
||||
!original.title &&
|
||||
original.message !==
|
||||
`This is a generated invite to ${original.details?.worldName}` ? (
|
||||
<TooltipWrapper
|
||||
@@ -529,16 +553,12 @@ export const createColumns = ({
|
||||
!original.link?.startsWith('economy.');
|
||||
const showDeleteLog =
|
||||
original.type !== 'friendRequest' &&
|
||||
original.type !== 'ignoredFriendRequest' &&
|
||||
!original.type?.includes('group.') &&
|
||||
!original.type?.includes('moderation.') &&
|
||||
!original.type?.includes('instance.') &&
|
||||
!original.link?.startsWith('economy.');
|
||||
original.type !== 'ignoredFriendRequest';
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
{original.senderUserId !== currentUser.value?.id &&
|
||||
!original.$isExpired ? (
|
||||
!isNotificationExpired(original) ? (
|
||||
<span class="inline-flex items-center gap-2">
|
||||
{original.type === 'friendRequest' ? (
|
||||
<Tooltip>
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
const activeTab = ref('friend');
|
||||
|
||||
const activeCount = computed(() => ({
|
||||
friend: friendNotifications.value.filter((n) => !n.$isExpired).length,
|
||||
group: groupNotifications.value.filter((n) => !n.$isExpired).length,
|
||||
other: otherNotifications.value.filter((n) => !n.$isExpired).length
|
||||
friend: friendNotifications.value.length,
|
||||
group: groupNotifications.value.length,
|
||||
other: otherNotifications.value.length
|
||||
}));
|
||||
|
||||
// Dialog state
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<Item
|
||||
size="sm"
|
||||
:variant="notification.$isExpired ? 'default' : 'muted'"
|
||||
:class="[{ 'opacity-50': notification.$isExpired }, 'mb-1.5']">
|
||||
<Item size="sm" variant="muted" class="mb-1.5">
|
||||
<ItemMedia variant="image" class="cursor-pointer" @click.stop="openSender">
|
||||
<Avatar class="size-full">
|
||||
<AvatarImage v-if="avatarUrl" :src="avatarUrl" />
|
||||
@@ -18,7 +15,7 @@
|
||||
{{ typeLabel }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="!notification.$isExpired && isUnseen"
|
||||
v-if="!isNotificationExpired(notification) && !isSeen"
|
||||
class="ml-auto size-2 shrink-0 rounded-full bg-blue-500" />
|
||||
</ItemTitle>
|
||||
<TooltipWrapper v-if="displayMessage" side="top" :content="displayMessage" :delay-duration="600">
|
||||
@@ -33,7 +30,7 @@
|
||||
{{ relativeTime }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<template v-if="!notification.$isExpired">
|
||||
<template v-if="!isNotificationExpired(notification)">
|
||||
<TooltipWrapper
|
||||
v-if="notification.type === 'friendRequest'"
|
||||
side="top"
|
||||
@@ -134,9 +131,10 @@
|
||||
} from 'lucide-vue-next';
|
||||
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TooltipWrapper } from '@/components/ui/tooltip';
|
||||
import { computed } from 'vue';
|
||||
import { notificationRequest } from '@/api';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -158,6 +156,7 @@
|
||||
const notificationStore = useNotificationStore();
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { openNotificationLink, isNotificationExpired, handleNotificationV2Hide } = useNotificationStore();
|
||||
|
||||
const senderName = computed(() => {
|
||||
const n = props.notification;
|
||||
@@ -224,6 +223,7 @@
|
||||
|
||||
const showDecline = computed(() => {
|
||||
const type = props.notification.type;
|
||||
const link = props.notification.link;
|
||||
return (
|
||||
type !== 'requestInviteResponse' &&
|
||||
type !== 'inviteResponse' &&
|
||||
@@ -232,7 +232,8 @@
|
||||
type !== 'groupChange' &&
|
||||
!type?.includes('group.') &&
|
||||
!type?.includes('moderation.') &&
|
||||
!type?.includes('instance.')
|
||||
!type?.includes('instance.') &&
|
||||
!link?.startsWith('economy.')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -242,13 +243,18 @@
|
||||
const n = props.notification;
|
||||
const type = n.type;
|
||||
if (type === 'friendRequest' || type === 'ignoredFriendRequest') return false;
|
||||
if (type?.includes('group.') || type?.includes('moderation.') || type?.includes('instance.')) return false;
|
||||
if (n.link?.startsWith('economy.')) return false;
|
||||
// For active notifications, group.queueReady is handled separately
|
||||
if (!n.$isExpired && type === 'group.queueReady') return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const isSeen = computed(() => {
|
||||
const n = props.notification;
|
||||
if (typeof n.seen === 'boolean') {
|
||||
return n.seen;
|
||||
}
|
||||
// Fallback for v1 notifications without seen property
|
||||
return !props.isUnseen;
|
||||
});
|
||||
|
||||
const canInvite = computed(() => {
|
||||
const location = lastLocation.value?.location;
|
||||
return Boolean(location) && isGameRunning.value && checkCanInvite(location);
|
||||
@@ -284,27 +290,6 @@
|
||||
notificationStore.sendNotificationResponse(props.notification.id, props.notification.responses, response.type);
|
||||
}
|
||||
|
||||
function openNotificationLink(link) {
|
||||
if (!link) return;
|
||||
const data = link.split(':');
|
||||
if (!data.length) return;
|
||||
switch (data[0]) {
|
||||
case 'group':
|
||||
groupStore.showGroupDialog(data[1]);
|
||||
break;
|
||||
case 'user':
|
||||
userStore.showUserDialog(data[1]);
|
||||
break;
|
||||
case 'event': {
|
||||
const ids = data[1].split(',');
|
||||
if (ids.length >= 2) {
|
||||
groupStore.showGroupDialog(ids[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openSender() {
|
||||
const userId = props.notification.senderUserId;
|
||||
if (!userId) return;
|
||||
@@ -314,4 +299,36 @@
|
||||
userStore.showUserDialog(userId);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Mark as seen
|
||||
if (isNotificationExpired(props.notification) || isSeen.value) {
|
||||
return;
|
||||
}
|
||||
const params = { notificationId: props.notification.id };
|
||||
if (!props.notification.version || props.notification.version < 2) {
|
||||
notificationRequest.seeNotification({ notificationId: props.notification.id }).then((args) => {
|
||||
console.log('Marked notification-v1 as seen:', args.json);
|
||||
notificationStore.handleNotificationSee(props.notification.id);
|
||||
});
|
||||
return;
|
||||
}
|
||||
notificationRequest
|
||||
.seeNotificationV2(params)
|
||||
.then((args) => {
|
||||
console.log('Marked notification-v2 as seen:', args.json);
|
||||
const newArgs = {
|
||||
params,
|
||||
json: {
|
||||
...args.json,
|
||||
seen: true
|
||||
}
|
||||
};
|
||||
notificationStore.handleNotificationV2Update(newArgs);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to mark notification-v2 as seen:', err);
|
||||
handleNotificationV2Hide(props.notification.id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@show-invite-request-response="$emit('show-invite-request-response', $event)" />
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center p-8 text-sm text-muted-foreground">
|
||||
{{ t('side_panel.notification_center.no_notifications') }}
|
||||
{{ t('side_panel.notification_center.no_new_notifications') }}
|
||||
</div>
|
||||
|
||||
<template v-if="expiredNotifications.length">
|
||||
@@ -73,11 +73,13 @@
|
||||
|
||||
const sortedNotifications = computed(() => [...props.notifications].sort((a, b) => getTs(b) - getTs(a)));
|
||||
|
||||
const activeNotifications = computed(() => sortedNotifications.value.filter((n) => !n.$isExpired));
|
||||
const activeNotifications = computed(() =>
|
||||
sortedNotifications.value.filter((n) => getTs(n) > dayjs().subtract(1, 'week').valueOf())
|
||||
);
|
||||
|
||||
const MAX_EXPIRED = 20;
|
||||
|
||||
const expiredNotifications = computed(() =>
|
||||
sortedNotifications.value.filter((n) => n.$isExpired).slice(0, MAX_EXPIRED)
|
||||
sortedNotifications.value.filter((n) => getTs(n) <= dayjs().subtract(1, 'week').valueOf()).slice(0, MAX_EXPIRED)
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user