diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 6146b8f05..f8897b8ab 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -16,6 +16,7 @@ import { useCopyLink } from "@/hooks/use-copy-link"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; import { formatFileSize, cn, MailboxNode, formatDate, formatDateTime, generateUUID } from "@/lib/utils"; +import { formatScheduledSendAt } from "@/lib/scheduled-send-label"; import { buildMoveTargets, resolveMoveOwnerAccountId } from "@/lib/move-targets"; import { emailDisplayDate } from "@/lib/email-date"; import { TagBadge } from "./tag-badge"; @@ -4722,7 +4723,7 @@ export function EmailViewer({
- {t('scheduled_banner', { date: email.scheduledSendAt ? formatDateTime(email.scheduledSendAt, timeFormat) : '' })} + {t('scheduled_banner', { date: email.scheduledSendAt ? formatScheduledSendAt(email.scheduledSendAt, timeFormat) : '' })}
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index d2a769d81..7d5f27873 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -1,7 +1,8 @@ "use client"; import React, { useCallback } from "react"; -import { cleanPreview, formatDate, formatDateTime } from "@/lib/utils"; +import { cleanPreview, formatDate } from "@/lib/utils"; +import { formatScheduledSendAt } from "@/lib/scheduled-send-label"; import { Email, ThreadGroup } from "@/lib/jmap/types"; import { cn } from "@/lib/utils"; import { ListAttachmentChips, useListHasAttachment } from "./attachment-chips"; @@ -324,7 +325,7 @@ const SingleEmailItem = React.forwardRef( const subjectSnippet = email.searchSnippet?.subject ?? null; const previewSnippet = showPreview ? (email.searchSnippet?.preview ?? null) : null; const scheduledSendLabel = email.isScheduled && email.scheduledSendAt - ? formatDateTime(email.scheduledSendAt, timeFormat) + ? formatScheduledSendAt(email.scheduledSendAt, timeFormat) : null; const tagIds = sortTagIds(getEmailTagIds(email.keywords)); @@ -825,7 +826,7 @@ export const ThreadListItem = React.forwardRef new Date(y, mo - 1, d, h, mi, s); + +describe('formatScheduledSendAt', () => { + const today = at(2026, 10, 11, 18, 0); + + it('shows 08:00 for a release one second before eight', () => { + const label = formatScheduledSendAt(at(2026, 10, 11, 19, 59, 59).toISOString(), '24h', today); + expect(label).toContain('20:00'); + expect(label).not.toContain('19:59'); + }); + + it('shows only the time for a release later today', () => { + expect(formatScheduledSendAt(at(2026, 10, 11, 21, 30).toISOString(), '24h', today)).toBe('21:30'); + }); + + it('includes the date for a release on another day', () => { + const label = formatScheduledSendAt(at(2026, 10, 12, 7, 59, 59).toISOString(), '24h', today); + expect(label).toContain('08:00'); + expect(label).toMatch(/12/); + }); +}); diff --git a/lib/scheduled-send-label.ts b/lib/scheduled-send-label.ts new file mode 100644 index 000000000..77296aa18 --- /dev/null +++ b/lib/scheduled-send-label.ts @@ -0,0 +1,23 @@ +import { formatDateTime } from '@/lib/utils'; + +/** + * The release time of a scheduled message, for lists and banners. + * + * The server can land a second before the minute the user picked (08:00 + * showed as 07:59), so the time is rounded to the nearest minute. A message + * that is not due today also shows its date; a bare "07:59" for tomorrow + * morning read like a time that had already passed. + */ +export function formatScheduledSendAt( + sendAt: string, + timeFormat: '12h' | '24h', + now: Date = new Date(), +): string { + const ms = Date.parse(sendAt); + if (!Number.isFinite(ms)) return sendAt; + const rounded = new Date(Math.round(ms / 60_000) * 60_000); + const sameDay = rounded.toDateString() === now.toDateString(); + return sameDay + ? formatDateTime(rounded, timeFormat) + : formatDateTime(rounded, timeFormat, { weekday: 'short', month: 'short', day: 'numeric' }); +}