From 2ae8c89990d12c2daae99a5bc645ed7cb61c82d0 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:50:40 +0200
Subject: [PATCH] fix: show the scheduled time rounded to the minute and with
its date
---
components/email/email-viewer.tsx | 3 ++-
components/email/thread-list-item.tsx | 7 +++---
lib/__tests__/scheduled-send-label.test.ts | 25 ++++++++++++++++++++++
lib/scheduled-send-label.ts | 23 ++++++++++++++++++++
4 files changed, 54 insertions(+), 4 deletions(-)
create mode 100644 lib/__tests__/scheduled-send-label.test.ts
create mode 100644 lib/scheduled-send-label.ts
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' });
+}