refactor(notifications): extract NotificationItem type and relativeTime to shared utility
Extracted from NotificationBell.svelte into $lib/utils/notifications.ts so the history page can reuse them. relativeTime() now accepts an optional `now` param for deterministic unit testing. Added parseNotificationEvent() for SSE payload shape validation (NullX Finding 3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,17 +2,11 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
type NotificationItem = {
|
||||
id: string;
|
||||
type: 'REPLY' | 'MENTION';
|
||||
documentId: string;
|
||||
referenceId: string;
|
||||
annotationId: string | null;
|
||||
read: boolean;
|
||||
createdAt: string;
|
||||
actorName: string;
|
||||
};
|
||||
import {
|
||||
type NotificationItem,
|
||||
relativeTime,
|
||||
parseNotificationEvent
|
||||
} from '$lib/utils/notifications';
|
||||
|
||||
let notifications: NotificationItem[] = $state([]);
|
||||
let unreadCount: number = $state(0);
|
||||
@@ -131,23 +125,12 @@ function attachClickOutside(node: HTMLElement) {
|
||||
};
|
||||
}
|
||||
|
||||
function relativeTime(isoString: string): string {
|
||||
const diffMs = Date.now() - new Date(isoString).getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
||||
if (diffMin < 1) return rtf.format(0, 'minute');
|
||||
if (diffMin < 60) return rtf.format(-diffMin, 'minute');
|
||||
const diffH = Math.floor(diffMin / 60);
|
||||
if (diffH < 24) return rtf.format(-diffH, 'hour');
|
||||
const diffD = Math.floor(diffH / 24);
|
||||
return rtf.format(-diffD, 'day');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchUnreadCount();
|
||||
eventSource = new EventSource('/api/notifications/stream');
|
||||
eventSource.addEventListener('notification', (e) => {
|
||||
const notification = JSON.parse(e.data) as NotificationItem;
|
||||
const notification = parseNotificationEvent(e.data);
|
||||
if (!notification) return;
|
||||
notifications = [notification, ...notifications];
|
||||
if (!notification.read) unreadCount += 1;
|
||||
});
|
||||
@@ -317,6 +300,16 @@ onDestroy(() => {
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="border-t border-line px-4 py-2">
|
||||
<a
|
||||
href="/notifications"
|
||||
onclick={closeDropdown}
|
||||
class="text-xs font-medium text-ink-2 transition-colors hover:text-ink"
|
||||
>
|
||||
{m.notification_view_all()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user