From 655a2003cb54ee592473f3a17611b32dbec8c24f Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 13:02:49 +0200 Subject: [PATCH] refactor(time): extract relativeTime into shared time.ts utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move relativeTime from notifications.ts (Intl.RelativeTimeFormat) to a new time.ts that uses the Paraglide comment_time_* message keys — the same logic that was already in CommentThread's timeAgo(). Remove the duplicate timeAgo() from CommentThread and re-export relativeTime from notifications.ts for backwards compatibility. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/CommentThread.svelte | 16 ++---- frontend/src/lib/utils/notifications.spec.ts | 53 +------------------ frontend/src/lib/utils/notifications.ts | 13 +---- frontend/src/lib/utils/time.spec.ts | 52 ++++++++++++++++++ frontend/src/lib/utils/time.ts | 12 +++++ 5 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 frontend/src/lib/utils/time.spec.ts create mode 100644 frontend/src/lib/utils/time.ts diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index 740825b3..6796cc3f 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js'; import type { Comment } from '$lib/types'; import MentionEditor from '$lib/components/MentionEditor.svelte'; import { renderBody, extractContent } from '$lib/utils/mention'; +import { relativeTime } from '$lib/utils/time'; import type { MentionDTO } from '$lib/types'; type Props = { @@ -67,17 +68,6 @@ $effect(() => { } }); -function timeAgo(iso: string): string { - const diff = Date.now() - new Date(iso).getTime(); - const minutes = Math.floor(diff / 60000); - if (minutes < 1) return m.comment_time_just_now(); - if (minutes < 60) return m.comment_time_minutes({ count: minutes }); - const hours = Math.floor(minutes / 60); - if (hours < 24) return m.comment_time_hours({ count: hours }); - const days = Math.floor(hours / 24); - return m.comment_time_days({ count: days }); -} - function wasEdited(c: { createdAt: string; updatedAt: string }): boolean { return c.updatedAt > c.createdAt; } @@ -235,10 +225,10 @@ onMount(() => { {msg.authorName} {#if wasEdited(msg)} {timeAgo(msg.updatedAt)} {m.comment_edited_label()}{relativeTime(msg.updatedAt)} {m.comment_edited_label()} {:else} - {timeAgo(msg.createdAt)} + {relativeTime(msg.createdAt)} {/if} {#if parsed.quote} diff --git a/frontend/src/lib/utils/notifications.spec.ts b/frontend/src/lib/utils/notifications.spec.ts index e1333d5a..294ea0ac 100644 --- a/frontend/src/lib/utils/notifications.spec.ts +++ b/frontend/src/lib/utils/notifications.spec.ts @@ -1,56 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { relativeTime, parseNotificationEvent } from '$lib/utils/notifications'; - -const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }); - -function msAgo(ms: number, now: Date): string { - return new Date(now.getTime() - ms).toISOString(); -} - -describe('relativeTime', () => { - const now = new Date('2024-06-15T12:00:00.000Z'); - - it('should use minute bucket for timestamps under 60 seconds ago', () => { - const ts = msAgo(30_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(0, 'minute')); - }); - - it('should use minute bucket for exactly 59 minutes ago', () => { - const ts = msAgo(59 * 60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-59, 'minute')); - }); - - it('should use minute bucket for exactly 1 minute ago', () => { - const ts = msAgo(60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-1, 'minute')); - }); - - it('should use hour bucket for exactly 1 hour ago', () => { - const ts = msAgo(60 * 60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-1, 'hour')); - }); - - it('should use hour bucket for 23 hours ago', () => { - const ts = msAgo(23 * 60 * 60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-23, 'hour')); - }); - - it('should use day bucket for exactly 24 hours ago', () => { - const ts = msAgo(24 * 60 * 60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-1, 'day')); - }); - - it('should use day bucket for 6 days ago', () => { - const ts = msAgo(6 * 24 * 60 * 60_000, now); - expect(relativeTime(ts, now)).toBe(rtf.format(-6, 'day')); - }); - - it('should default now to current time when omitted', () => { - // Just verify it returns a non-empty string — exact value depends on runtime clock - const ts = new Date(Date.now() - 5 * 60_000).toISOString(); - expect(relativeTime(ts)).toBeTruthy(); - }); -}); +import { parseNotificationEvent } from '$lib/utils/notifications'; describe('parseNotificationEvent', () => { const valid = { diff --git a/frontend/src/lib/utils/notifications.ts b/frontend/src/lib/utils/notifications.ts index a58f1b11..a4feb005 100644 --- a/frontend/src/lib/utils/notifications.ts +++ b/frontend/src/lib/utils/notifications.ts @@ -10,18 +10,7 @@ export type NotificationItem = { documentTitle: string | null; }; -const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }); - -export function relativeTime(isoString: string, now: Date = new Date()): string { - const diffMs = now.getTime() - new Date(isoString).getTime(); - const diffMin = Math.floor(diffMs / 60_000); - 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'); -} +export { relativeTime } from '$lib/utils/time'; export function parseNotificationEvent(raw: string): NotificationItem | null { try { diff --git a/frontend/src/lib/utils/time.spec.ts b/frontend/src/lib/utils/time.spec.ts new file mode 100644 index 00000000..f3d99a5a --- /dev/null +++ b/frontend/src/lib/utils/time.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { m } from '$lib/paraglide/messages.js'; + +const { relativeTime } = await import('./time'); + +function msAgo(ms: number, now: Date): string { + return new Date(now.getTime() - ms).toISOString(); +} + +describe('relativeTime', () => { + const now = new Date('2024-06-15T12:00:00.000Z'); + + it('returns "just now" for timestamps under 60 seconds ago', () => { + const ts = msAgo(30_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_just_now()); + }); + + it('returns 1-minute label for exactly 1 minute ago', () => { + const ts = msAgo(60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_minutes({ count: 1 })); + }); + + it('returns 59-minute label for exactly 59 minutes ago', () => { + const ts = msAgo(59 * 60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_minutes({ count: 59 })); + }); + + it('returns 1-hour label for exactly 1 hour ago', () => { + const ts = msAgo(60 * 60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_hours({ count: 1 })); + }); + + it('returns 23-hour label for 23 hours ago', () => { + const ts = msAgo(23 * 60 * 60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_hours({ count: 23 })); + }); + + it('returns 1-day label for exactly 24 hours ago', () => { + const ts = msAgo(24 * 60 * 60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_days({ count: 1 })); + }); + + it('returns 6-day label for 6 days ago', () => { + const ts = msAgo(6 * 24 * 60 * 60_000, now); + expect(relativeTime(ts, now)).toBe(m.comment_time_days({ count: 6 })); + }); + + it('defaults now to current time when omitted', () => { + const ts = new Date(Date.now() - 5 * 60_000).toISOString(); + expect(relativeTime(ts)).toBeTruthy(); + }); +}); diff --git a/frontend/src/lib/utils/time.ts b/frontend/src/lib/utils/time.ts new file mode 100644 index 00000000..bbe47ee6 --- /dev/null +++ b/frontend/src/lib/utils/time.ts @@ -0,0 +1,12 @@ +import { m } from '$lib/paraglide/messages.js'; + +export function relativeTime(isoString: string, now: Date = new Date()): string { + const diff = now.getTime() - new Date(isoString).getTime(); + const minutes = Math.floor(diff / 60_000); + if (minutes < 1) return m.comment_time_just_now(); + if (minutes < 60) return m.comment_time_minutes({ count: minutes }); + const hours = Math.floor(minutes / 60); + if (hours < 24) return m.comment_time_hours({ count: hours }); + const days = Math.floor(hours / 24); + return m.comment_time_days({ count: days }); +}