authorgravatar for Noojuno@users.noreply.github.comJono Kemball <Noojuno@users.noreply.github.com> 2026-08-11 10:03:26+12:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-11 00:03:26+02:00
log9afef94a61466422128da9c3b723b633d4c7ed1d
tree053a20dd99e24d55ce7a733a2dd59f8a2d4de058
parenta7b0366cbe1e9eabc9e37eb079a38f6b6691f999
signature Signed by PGP key B5690EEEBB952194

feat: add three-hour snooze option (#5914)


3 files changed, 22 insertions(+), 6 deletions(-)

apps/web/src/components/Sidebar.snooze.test.ts+6-3
......@@ -8,15 +8,18 @@ function localDate(year: number, month: number, day: number, hour: number, minut
88}
99
1010describe("resolveSnoozePresets", () => {
11 it("offers hour, evening, tomorrow, next week in the morning", () => {
11 it("offers one hour, three hours, evening, tomorrow, and next week in the morning", () => {
1212 // Wednesday 2026-04-08 10:00 local.
1313 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale");
1414 expect(presets.map((preset) => preset.id)).toEqual([
1515 "hour",
16 "three-hours",
1617 "evening",
1718 "tomorrow",
1819 "next-week",
1920 ]);
21 const threeHours = presets.find((preset) => preset.id === "three-hours");
22 expect(new Date(threeHours!.snoozedUntil).getHours()).toBe(13);
2023 const evening = presets.find((preset) => preset.id === "evening");
2124 expect(new Date(evening!.snoozedUntil).getHours()).toBe(18);
2225 const tomorrow = presets.find((preset) => preset.id === "tomorrow");
......@@ -45,10 +48,10 @@ describe("resolveSnoozePresets", () => {
4548 it("drops the evening preset once evening is near or past", () => {
4649 expect(
4750 resolveSnoozePresets(localDate(2026, 4, 8, 17, 30), "locale").map((preset) => preset.id),
48 ).toEqual(["hour", "tomorrow", "next-week"]);
51 ).toEqual(["hour", "three-hours", "tomorrow", "next-week"]);
4952 expect(
5053 resolveSnoozePresets(localDate(2026, 4, 8, 21), "locale").map((preset) => preset.id),
51 ).toEqual(["hour", "tomorrow", "next-week"]);
54 ).toEqual(["hour", "three-hours", "tomorrow", "next-week"]);
5255 });
5356
5457 it("puts next week a full week out when today is Monday", () => {
packages/client-runtime/src/state/threadSettled.ts+10-3
......@@ -284,7 +284,7 @@ const HOUR_MS = 60 * 60 * 1_000;
284284const EVENING_HOUR = 18;
285285const MORNING_HOUR = 9;
286286
287export type SnoozePresetId = "hour" | "evening" | "tomorrow" | "next-week";
287export type SnoozePresetId = "hour" | "three-hours" | "evening" | "tomorrow" | "next-week";
288288
289289export interface SnoozePreset {
290290 readonly id: SnoozePresetId;
......@@ -317,11 +317,12 @@ function addSnoozeDays(base: Date, days: number): Date {
317317
318318/**
319319 * Shared "snooze until" choices for every client. "This evening" only
320 * appears while it is meaningfully before evening; after that the list
321 * starts at "Tomorrow".
320 * appears while it is meaningfully before evening; after that the calendar
321 * choices start at "Tomorrow".
322322 */
323323export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
324324 const inAnHour = new Date(now.getTime() + HOUR_MS);
325 const inThreeHours = new Date(now.getTime() + 3 * HOUR_MS);
325326 const presets: SnoozePreset[] = [
326327 {
327328 id: "hour",
......@@ -329,6 +330,12 @@ export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
329330 whenLabel: snoozeTimeOfDayLabel(inAnHour),
330331 snoozedUntil: inAnHour.toISOString(),
331332 },
333 {
334 id: "three-hours",
335 label: "In 3 hours",
336 whenLabel: snoozeTimeOfDayLabel(inThreeHours),
337 snoozedUntil: inThreeHours.toISOString(),
338 },
332339 ];
333340
334341 const evening = snoozeAtHour(now, EVENING_HOUR);
packages/client-runtime/src/state/threadSnoozed.test.ts+6
......@@ -265,10 +265,15 @@ describe("resolveSnoozePresets", () => {
265265 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10));
266266 expect(presets.map((preset) => preset.id)).toEqual([
267267 "hour",
268 "three-hours",
268269 "evening",
269270 "tomorrow",
270271 "next-week",
271272 ]);
273 expect(presets.find((preset) => preset.id === "three-hours")?.snoozedUntil).toBe(
274 localDate(2026, 4, 8, 13).toISOString(),
275 );
276 expect(presets.find((preset) => preset.id === "three-hours")?.label).toBe("In 3 hours");
272277 expect(presets.find((preset) => preset.id === "evening")?.label).toBe("This evening");
273278 expect(
274279 new Date(presets.find((preset) => preset.id === "tomorrow")!.snoozedUntil).getHours(),
......@@ -278,6 +283,7 @@ describe("resolveSnoozePresets", () => {
278283 it("drops the evening choice once evening is near or past", () => {
279284 expect(resolveSnoozePresets(localDate(2026, 4, 8, 17, 30)).map((preset) => preset.id)).toEqual([
280285 "hour",
286 "three-hours",
281287 "tomorrow",
282288 "next-week",
283289 ]);