diff --git a/apps/staged/src/lib/features/projects/ProjectSection.svelte b/apps/staged/src/lib/features/projects/ProjectSection.svelte index f0ed5d49..5ba110a2 100644 --- a/apps/staged/src/lib/features/projects/ProjectSection.svelte +++ b/apps/staged/src/lib/features/projects/ProjectSection.svelte @@ -42,6 +42,7 @@ import { subscribeDragDrop } from '../branches/dragDrop'; import { isImageFile } from '../branches/branchCardHelpers'; import { createImage, createImageFromData, deleteImage, getImageData } from '../../api/commands'; + import { formatRelativeTime, minuteNow } from '../../shared/relativeTime.svelte'; interface Props { project: Project; @@ -347,21 +348,6 @@ let openNote = $state<{ title: string; content: string; sessionId?: string } | null>(null); let openSessionId = $state(null); - function formatRelativeTime(timestampMs: number): string { - const date = new Date(timestampMs); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); - } - // ── Lifecycle ────────────────────────────────────────────────────────── onMount(() => { @@ -563,6 +549,7 @@ {#if projectNotes.length > 0} + {@const nowMs = minuteNow.now()}
@@ -580,7 +567,9 @@ : isFailed ? 'Session finished — no note created' : note.title || 'Untitled note'} - secondaryMeta={isRunning || isFailed ? undefined : formatRelativeTime(note.createdAt)} + secondaryMeta={isRunning || isFailed + ? undefined + : formatRelativeTime(note.createdAt, nowMs)} deleting={deletingNoteIds.has(note.id)} isLast={index === timelineNotes.length - 1} sessionId={note.sessionId ?? undefined} diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 8f48824a..03f8c711 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -13,6 +13,11 @@ import type { BranchTimeline as BranchTimelineData } from '../../types'; import TimelineRow from './TimelineRow.svelte'; import type { TimelineItemType, TimelineBadge } from './TimelineRow.svelte'; + import { + formatRelativeTime, + formatRelativeTimeSeconds, + minuteNow, + } from '../../shared/relativeTime.svelte'; import { collectRunningSessionIds, createLiveSessionHints, @@ -222,6 +227,7 @@ // Merge commits, notes, and reviews into a single sorted list let items = $derived.by(() => { + const nowMs = minuteNow.now(); const all: DisplayItem[] = []; const deletingCommitIds = new Set( deletingItems.filter((item) => item.type === 'commit').map((item) => item.id) @@ -260,7 +266,7 @@ secondaryMeta = liveHint ?? 'Generating commit'; } else { type = 'commit'; - secondaryMeta = formatRelativeTime(commit.timestamp); + secondaryMeta = formatRelativeTimeSeconds(commit.timestamp, nowMs); } all.push({ @@ -300,7 +306,7 @@ secondaryMeta = liveHint ?? 'Generating note'; } else { type = 'note'; - secondaryMeta = formatRelativeTimeMs(note.createdAt); + secondaryMeta = formatRelativeTime(note.createdAt, nowMs); } all.push({ @@ -357,7 +363,7 @@ meta = liveHint ?? 'Generating review'; } else { type = 'review'; - meta = formatRelativeTimeMs(review.createdAt); + meta = formatRelativeTime(review.createdAt, nowMs); } all.push({ @@ -382,7 +388,7 @@ key: `image-${image.id}`, type: 'image' as TimelineItemType, title: image.filename, - secondaryMeta: isDeleting ? 'Deleting...' : formatRelativeTimeMs(image.createdAt), + secondaryMeta: isDeleting ? 'Deleting...' : formatRelativeTime(image.createdAt, nowMs), deleting: isDeleting, timestamp: Math.floor(image.createdAt / 1000), order: 0, @@ -496,25 +502,6 @@ onDeleteImage(item.imageId); } } - - function formatRelativeTime(timestamp: number): string { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); - } - - function formatRelativeTimeMs(timestamp: number): string { - return formatRelativeTime(Math.floor(timestamp / 1000)); - } {#if items.length === 0 && !onNewNote && !onNewCommit && !onNewReview && pendingDropNotes.length === 0 && pendingItems.length === 0} diff --git a/apps/staged/src/lib/shared/relativeTime.svelte.ts b/apps/staged/src/lib/shared/relativeTime.svelte.ts new file mode 100644 index 00000000..56923340 --- /dev/null +++ b/apps/staged/src/lib/shared/relativeTime.svelte.ts @@ -0,0 +1,48 @@ +class MinuteNowStore { + private value = $state(Date.now()); + + constructor() { + if (typeof window !== 'undefined') { + this.start(); + } + } + + now(): number { + return this.value; + } + + private start(): void { + this.value = Date.now(); + + const msUntilNextMinute = 60000 - (this.value % 60000); + setTimeout(() => { + this.value = Date.now(); + setInterval(() => { + this.value = Date.now(); + }, 60000); + }, msUntilNextMinute); + } +} + +export const minuteNow = new MinuteNowStore(); + +export function formatRelativeTime(timestampMs: number, nowMs = minuteNow.now()): string { + const date = new Date(timestampMs); + const diffMs = nowMs - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +export function formatRelativeTimeSeconds( + timestampSeconds: number, + nowMs = minuteNow.now() +): string { + return formatRelativeTime(timestampSeconds * 1000, nowMs); +}