Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/SnoozePresetsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ function SnoozePresetList(props: {
return { tone: "muted", text: "Duplicate of an option above" } as const;
}
if (seenWakes.has(resolved.snoozedUntil)) {
return { tone: "muted", text: "Same time as an option above" } as const;
return { tone: "muted", text: "Combined with an option above" } as const;
}
seenIds.add(resolved.id);
seenWakes.add(resolved.snoozedUntil);
Expand Down
34 changes: 18 additions & 16 deletions packages/client-runtime/src/state/threadSettled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,14 @@ function snoozePresetDaysAhead(
return (WEEKDAY_INDEX[setting.day] - now.getDay() + 7) % 7 || 7;
}

/** Labels follow the actual day gap, so a Monday option on a Sunday reads "Tomorrow". */
/** Keep named weekdays recognizable even when they fall tomorrow. */
function snoozePresetLabel(setting: SnoozePresetSetting, daysAhead: number): string {
if (setting.kind === "after") return `In ${formatSnoozeDuration(setting.minutes)}`;
if (daysAhead === 0) {
const hour = Number(setting.time.slice(0, 2));
return hour < 12 ? "This morning" : hour < 17 ? "This afternoon" : "This evening";
}
if (daysAhead === 1) return "Tomorrow";
if (daysAhead === 1) return capitalize(setting.day);
if (setting.day === "monday") return "Next week";
return daysAhead === 7 ? `Next ${capitalize(setting.day)}` : capitalize(setting.day);
}
Expand Down Expand Up @@ -311,21 +311,23 @@ export function resolveSnoozePresets(
now: Date,
options: SnoozePresetOptions = {},
): ReadonlyArray<SnoozePreset> {
// By minute, so a duration (which keeps the clock's seconds) and a set time in the same
// minute tie, and the stable sort keeps whichever comes first in settings.
// Durations keep the clock's seconds; ties within a minute keep the first setting's wake.
const wakeMinute = (preset: SnoozePreset) => Math.floor(Date.parse(preset.snoozedUntil) / 60_000);
const seen = new Set<number>();
return (
(options.presets ?? DEFAULT_SNOOZE_PRESETS)
.flatMap((setting) => resolveSnoozePreset(setting, now, options.formatTime) ?? [])
// flatMap returned a new array; Hermes has no `toSorted`.
.sort((a, b) => wakeMinute(a) - wakeMinute(b))
.filter((preset) => {
if (seen.has(wakeMinute(preset))) return false;
seen.add(wakeMinute(preset));
return true;
})
);
const presets = (options.presets ?? DEFAULT_SNOOZE_PRESETS)
.flatMap((setting) => resolveSnoozePreset(setting, now, options.formatTime) ?? [])
// flatMap returned a new array; Hermes has no `toSorted`.
.sort((a, b) => wakeMinute(a) - wakeMinute(b));
const groups = new Map<number, SnoozePreset[]>();
for (const preset of presets) {
const minute = wakeMinute(preset);
const group = groups.get(minute);
if (group) group.push(preset);
else groups.set(minute, [preset]);
}
return [...groups.values()].map((group) => ({
...group[0]!,
label: [...new Set(group.map((preset) => preset.label))].join(" · "),
}));
}

/**
Expand Down
34 changes: 30 additions & 4 deletions packages/client-runtime/src/state/threadSnoozed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,15 @@ describe("resolveSnoozePresets", () => {
expect(new Date(presets[0]!.snoozedUntil)).toEqual(localDate(2026, 4, 20, 9));
});

it("names the real day gap: a Monday option on Sunday is Tomorrow, a weekday a week out is Next", () => {
it("keeps named weekdays visible and names a weekday a week out as Next", () => {
const theo: ReadonlyArray<SnoozePresetSetting> = [
{ kind: "at", day: "tomorrow", time: "21:00" },
{ kind: "at", day: "monday", time: "18:00" },
{ kind: "at", day: "friday", time: "17:00" },
];
// Sunday 14:00.
expect(menu(localDate(2026, 8, 30, 14), theo)).toEqual([
"Tomorrow 18:00",
"Monday 18:00",
"Tomorrow 21:00",
"Friday 17:00",
]);
Expand All @@ -446,7 +446,33 @@ describe("resolveSnoozePresets", () => {
"In 1 hour 8:01",
"In 3 hours 10:01",
"This evening 18:00",
"Tomorrow 9:00",
"Tomorrow · Monday 9:00",
]);
});

it("keeps every configured option visible when Tomorrow and Saturday coincide", () => {
const presets: ReadonlyArray<SnoozePresetSetting> = [
{ kind: "after", minutes: 60 },
{ kind: "after", minutes: 180 },
{ kind: "at", day: "today", time: "18:00" },
{ kind: "at", day: "tomorrow", time: "09:00" },
{ kind: "at", day: "saturday", time: "09:00" },
{ kind: "at", day: "monday", time: "09:00" },
];
const now = localDate(2026, 11, 6, 12, 2);
expect(menu(now, presets)).toEqual([
"In 1 hour 13:02",
"In 3 hours 15:02",
"This evening 18:00",
"Tomorrow · Saturday 9:00",
"Next week Mon 9:00",
]);
const merged = resolveSnoozePresets(now, { presets })[3]!;
expect(merged.id).toBe("at:tomorrow:09:00");
expect(merged.snoozedUntil).toBe(localDate(2026, 11, 7, 9).toISOString());
expect(menu(now, [...presets].reverse())[3]).toBe("Saturday · Tomorrow 9:00");
expect(menu(now, [presets[3]!, presets[3]!, presets[4]!])).toEqual([
"Tomorrow · Saturday 9:00",
]);
});

Expand All @@ -458,7 +484,7 @@ describe("resolveSnoozePresets", () => {
{ kind: "after", minutes: 180 },
{ kind: "at", day: "today", time: "18:00" },
]),
).toEqual(["In 3 hours 18:00"]);
).toEqual(["In 3 hours · This evening 18:00"]);
});
});

Expand Down
Loading