Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions apps/staged/src/lib/features/projects/ProjectSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -347,21 +348,6 @@
let openNote = $state<{ title: string; content: string; sessionId?: string } | null>(null);
let openSessionId = $state<string | null>(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(() => {
Expand Down Expand Up @@ -563,6 +549,7 @@

<!-- Project notes -->
{#if projectNotes.length > 0}
{@const nowMs = minuteNow.now()}
<div class="project-notes">
<div class="notes-header">
<FileText size={13} />
Expand All @@ -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}
Expand Down
33 changes: 10 additions & 23 deletions apps/staged/src/lib/features/timeline/BranchTimeline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -260,7 +266,7 @@
secondaryMeta = liveHint ?? 'Generating commit';
} else {
type = 'commit';
secondaryMeta = formatRelativeTime(commit.timestamp);
secondaryMeta = formatRelativeTimeSeconds(commit.timestamp, nowMs);
}

all.push({
Expand Down Expand Up @@ -300,7 +306,7 @@
secondaryMeta = liveHint ?? 'Generating note';
} else {
type = 'note';
secondaryMeta = formatRelativeTimeMs(note.createdAt);
secondaryMeta = formatRelativeTime(note.createdAt, nowMs);
}

all.push({
Expand Down Expand Up @@ -357,7 +363,7 @@
meta = liveHint ?? 'Generating review';
} else {
type = 'review';
meta = formatRelativeTimeMs(review.createdAt);
meta = formatRelativeTime(review.createdAt, nowMs);
}

all.push({
Expand All @@ -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,
Expand Down Expand Up @@ -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));
}
</script>

{#if items.length === 0 && !onNewNote && !onNewCommit && !onNewReview && pendingDropNotes.length === 0 && pendingItems.length === 0}
Expand Down
48 changes: 48 additions & 0 deletions apps/staged/src/lib/shared/relativeTime.svelte.ts
Original file line number Diff line number Diff line change
@@ -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);
}