feat(reminders): view and cancel a message's existing reminders
There was no way to see or cancel a reminder once set (removeReminder was only called by the fire-and-forget monitor), and addReminder didn't dedupe, so a message could silently accumulate duplicate reminders. The Remind Me dialog now lists the reminders already set on that message (soonest first) each with a cancel button. - New shared, tested formatFriendlyDateTime(ts, now?) in utils/datetimeInput.ts (Today/Tomorrow/date + time). - Per-row cancel busy-guard; inline "Could not cancel" on failure. Also applies two nits from the custom-time review: focus the date input when the custom picker is revealed, and clear the error when editing date/time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { toLocalDate, toLocalTime, parseLocalDateTime } from './datetimeInput';
|
||||
import {
|
||||
toLocalDate,
|
||||
toLocalTime,
|
||||
parseLocalDateTime,
|
||||
formatFriendlyDateTime,
|
||||
} from './datetimeInput';
|
||||
|
||||
// All helpers work in the LOCAL timezone. We construct Dates with the local
|
||||
// component constructor so the assertions hold regardless of where tests run.
|
||||
@@ -43,3 +48,23 @@ test('parseLocalDateTime: valid input yields the local wall-clock time', () => {
|
||||
assert.equal(parsed.getHours(), 9);
|
||||
assert.equal(parsed.getMinutes(), 3);
|
||||
});
|
||||
|
||||
test('formatFriendlyDateTime: uses Today/Tomorrow/date prefixes', () => {
|
||||
const now = new Date(2026, 0, 5, 10, 0).getTime();
|
||||
const laterToday = new Date(2026, 0, 5, 15, 30).getTime();
|
||||
const tomorrow = new Date(2026, 0, 6, 9, 0).getTime();
|
||||
const nextWeek = new Date(2026, 0, 12, 9, 0).getTime();
|
||||
|
||||
assert.ok(formatFriendlyDateTime(laterToday, now).startsWith('Today at '));
|
||||
assert.ok(formatFriendlyDateTime(tomorrow, now).startsWith('Tomorrow at '));
|
||||
const other = formatFriendlyDateTime(nextWeek, now);
|
||||
assert.ok(!other.startsWith('Today'));
|
||||
assert.ok(!other.startsWith('Tomorrow'));
|
||||
assert.ok(other.includes(' at '));
|
||||
});
|
||||
|
||||
test('formatFriendlyDateTime: Tomorrow rolls over month/year boundaries', () => {
|
||||
const nye = new Date(2026, 11, 31, 23, 0).getTime();
|
||||
const jan1 = new Date(2027, 0, 1, 9, 0).getTime();
|
||||
assert.ok(formatFriendlyDateTime(jan1, nye).startsWith('Tomorrow at '));
|
||||
});
|
||||
|
||||
@@ -23,6 +23,23 @@ export function parseLocalDateTime(dateValue: string, timeValue: string): Date |
|
||||
return Number.isNaN(dt.getTime()) ? null : dt;
|
||||
}
|
||||
|
||||
// Human-friendly absolute time: "Today at 3:00 PM", "Tomorrow at 9:00 AM", or
|
||||
// "1/5/2026 at 3:00 PM". `now` is injectable so the relative-day logic is testable.
|
||||
export function formatFriendlyDateTime(ts: number, now: number = Date.now()): string {
|
||||
const date = new Date(ts);
|
||||
const nowDate = new Date(now);
|
||||
const sameDay = (a: Date, b: Date): boolean =>
|
||||
a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate();
|
||||
const tomorrow = new Date(nowDate);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
if (sameDay(date, nowDate)) return `Today at ${timeStr}`;
|
||||
if (sameDay(date, tomorrow)) return `Tomorrow at ${timeStr}`;
|
||||
return `${date.toLocaleDateString()} at ${timeStr}`;
|
||||
}
|
||||
|
||||
// Shared style for date/time <input>s — matches the app's surface tokens and
|
||||
// hints a dark-mode calendar/clock popup via colorScheme.
|
||||
export function pickerInputStyle(
|
||||
|
||||
Reference in New Issue
Block a user