127 lines · 4.4 KB · TypeScript
RawDownload
/** * Snooze preset resolution for the sidebar snooze menu. Pure functions so * the preset math (evening/tomorrow/next-week boundaries) is unit-testable * without a DOM. * * Presets deliberately skew short: agent-thread rhythms are hours (a CI * run, a teammate review, the next work session), not days. */import { parseTimestampDate } from "../timestampFormat"; type SnoozePresetId = "hour" | "evening" | "tomorrow" | "next-week"; export interface SnoozePreset {  readonly id: SnoozePresetId;  readonly label: string;  /** Menu-row time column. Complements the label instead of repeating it:      "Tomorrow" pairs with "9:00 AM", not "tomorrow 9:00 AM". */  readonly whenLabel: string;  /** ISO wake time. */  readonly snoozedUntil: string;} function timeOfDayLabel(date: Date): string {  return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });} const EVENING_HOUR = 18;const MORNING_HOUR = 9;const HOUR_MS = 60 * 60 * 1_000;const DAY_MS = 24 * HOUR_MS; function atHour(base: Date, hour: number): Date {  const next = new Date(base);  next.setHours(hour, 0, 0, 0);  return next;} // Calendar-day advance instead of adding DAY_MS: fixed millisecond offsets// land on the wrong local day across DST transitions (a spring-forward day// is 23 hours, so 23:30 + 24h skips the whole next day).function addDays(base: Date, days: number): Date {  const next = new Date(base);  next.setDate(next.getDate() + days);  return next;} /** * Presets for "snooze until", computed against local time. "This evening" * only appears while it is still meaningfully before evening; after that * the list starts at "Tomorrow". */export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {  const inAnHour = new Date(now.getTime() + HOUR_MS);  const presets: SnoozePreset[] = [    {      id: "hour",      label: "In 1 hour",      whenLabel: timeOfDayLabel(inAnHour),      snoozedUntil: inAnHour.toISOString(),    },  ];   const evening = atHour(now, EVENING_HOUR);  // Suppress the evening preset once it is within an hour (or past): it  // would duplicate "In 1 hour" or point at the past.  if (evening.getTime() - now.getTime() > HOUR_MS) {    presets.push({      id: "evening",      label: "This evening",      whenLabel: timeOfDayLabel(evening),      snoozedUntil: evening.toISOString(),    });  }   const tomorrow = atHour(addDays(now, 1), MORNING_HOUR);  presets.push({    id: "tomorrow",    label: "Tomorrow",    whenLabel: timeOfDayLabel(tomorrow),    snoozedUntil: tomorrow.toISOString(),  });   // Next Monday 9:00 (a week out when today is Monday).  const daysUntilMonday = (1 - now.getDay() + 7) % 7 || 7;  const nextWeek = atHour(addDays(now, daysUntilMonday), MORNING_HOUR);  presets.push({    id: "next-week",    label: "Next week",    whenLabel: `${nextWeek.toLocaleDateString(undefined, { weekday: "short" })} ${timeOfDayLabel(nextWeek)}`,    snoozedUntil: nextWeek.toISOString(),  });   return presets;} /** * Compact "wakes in" label for snoozed rows: "2h", "18h", "3d". Minutes * round up so a snooze never reads "0m" while still hidden. */export function snoozeWakeLabel(snoozedUntil: string, now: Date): string {  const wake = parseTimestampDate(snoozedUntil);  if (wake === null) return "now";  const remainingMs = wake.getTime() - now.getTime();  if (remainingMs <= 0) return "now";  if (remainingMs < HOUR_MS) return `${Math.max(1, Math.ceil(remainingMs / 60_000))}m`;  if (remainingMs < DAY_MS) return `${Math.ceil(remainingMs / HOUR_MS)}h`;  return `${Math.ceil(remainingMs / DAY_MS)}d`;} /** * Human wake time for menus and toasts: "tomorrow 9:00", "Mon 9:00", * "17:30" (today). */export function snoozeWakeDescription(snoozedUntil: string, now: Date): string {  const wake = parseTimestampDate(snoozedUntil);  if (wake === null) return "";  const time = wake.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });  const startOfToday = new Date(now);  startOfToday.setHours(0, 0, 0, 0);  const dayDelta = Math.floor((wake.getTime() - startOfToday.getTime()) / DAY_MS);  if (dayDelta === 0) return time;  if (dayDelta === 1) return `tomorrow ${time}`;  const weekday = wake.toLocaleDateString(undefined, { weekday: "short" });  if (dayDelta < 7) return `${weekday} ${time}`;  const date = wake.toLocaleDateString(undefined, { month: "short", day: "numeric" });  return `${date}, ${time}`;}