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...@@ -8,15 +8,18 @@ function localDate(year: number, month: number, day: number, hour: number, minut
8}8}
99
10describe("resolveSnoozePresets", () => {10describe("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", () => {
12 // Wednesday 2026-04-08 10:00 local.12 // Wednesday 2026-04-08 10:00 local.
13 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale");13 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale");
14 expect(presets.map((preset) => preset.id)).toEqual([14 expect(presets.map((preset) => preset.id)).toEqual([
15 "hour",15 "hour",
16 "three-hours",
16 "evening",17 "evening",
17 "tomorrow",18 "tomorrow",
18 "next-week",19 "next-week",
19 ]);20 ]);
21 const threeHours = presets.find((preset) => preset.id === "three-hours");
22 expect(new Date(threeHours!.snoozedUntil).getHours()).toBe(13);
20 const evening = presets.find((preset) => preset.id === "evening");23 const evening = presets.find((preset) => preset.id === "evening");
21 expect(new Date(evening!.snoozedUntil).getHours()).toBe(18);24 expect(new Date(evening!.snoozedUntil).getHours()).toBe(18);
22 const tomorrow = presets.find((preset) => preset.id === "tomorrow");25 const tomorrow = presets.find((preset) => preset.id === "tomorrow");
...@@ -45,10 +48,10 @@ describe("resolveSnoozePresets", () => {...@@ -45,10 +48,10 @@ describe("resolveSnoozePresets", () => {
45 it("drops the evening preset once evening is near or past", () => {48 it("drops the evening preset once evening is near or past", () => {
46 expect(49 expect(
47 resolveSnoozePresets(localDate(2026, 4, 8, 17, 30), "locale").map((preset) => preset.id),50 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"]);
49 expect(52 expect(
50 resolveSnoozePresets(localDate(2026, 4, 8, 21), "locale").map((preset) => preset.id),53 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"]);
52 });55 });
5356
54 it("puts next week a full week out when today is Monday", () => {57 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;...@@ -284,7 +284,7 @@ const HOUR_MS = 60 * 60 * 1_000;
284const EVENING_HOUR = 18;284const EVENING_HOUR = 18;
285const MORNING_HOUR = 9;285const MORNING_HOUR = 9;
286286
287export type SnoozePresetId = "hour" | "evening" | "tomorrow" | "next-week";287export type SnoozePresetId = "hour" | "three-hours" | "evening" | "tomorrow" | "next-week";
288288
289export interface SnoozePreset {289export interface SnoozePreset {
290 readonly id: SnoozePresetId;290 readonly id: SnoozePresetId;
...@@ -317,11 +317,12 @@ function addSnoozeDays(base: Date, days: number): Date {...@@ -317,11 +317,12 @@ function addSnoozeDays(base: Date, days: number): Date {
317317
318/**318/**
319 * Shared "snooze until" choices for every client. "This evening" only319 * Shared "snooze until" choices for every client. "This evening" only
320 * appears while it is meaningfully before evening; after that the list320 * appears while it is meaningfully before evening; after that the calendar
321 * starts at "Tomorrow".321 * choices start at "Tomorrow".
322 */322 */
323export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {323export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
324 const inAnHour = new Date(now.getTime() + HOUR_MS);324 const inAnHour = new Date(now.getTime() + HOUR_MS);
325 const inThreeHours = new Date(now.getTime() + 3 * HOUR_MS);
325 const presets: SnoozePreset[] = [326 const presets: SnoozePreset[] = [
326 {327 {
327 id: "hour",328 id: "hour",
...@@ -329,6 +330,12 @@ export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {...@@ -329,6 +330,12 @@ export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
329 whenLabel: snoozeTimeOfDayLabel(inAnHour),330 whenLabel: snoozeTimeOfDayLabel(inAnHour),
330 snoozedUntil: inAnHour.toISOString(),331 snoozedUntil: inAnHour.toISOString(),
331 },332 },
333 {
334 id: "three-hours",
335 label: "In 3 hours",
336 whenLabel: snoozeTimeOfDayLabel(inThreeHours),
337 snoozedUntil: inThreeHours.toISOString(),
338 },
332 ];339 ];
333340
334 const evening = snoozeAtHour(now, EVENING_HOUR);341 const evening = snoozeAtHour(now, EVENING_HOUR);
packages/client-runtime/src/state/threadSnoozed.test.ts+6
...@@ -265,10 +265,15 @@ describe("resolveSnoozePresets", () => {...@@ -265,10 +265,15 @@ describe("resolveSnoozePresets", () => {
265 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10));265 const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10));
266 expect(presets.map((preset) => preset.id)).toEqual([266 expect(presets.map((preset) => preset.id)).toEqual([
267 "hour",267 "hour",
268 "three-hours",
268 "evening",269 "evening",
269 "tomorrow",270 "tomorrow",
270 "next-week",271 "next-week",
271 ]);272 ]);
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");
272 expect(presets.find((preset) => preset.id === "evening")?.label).toBe("This evening");277 expect(presets.find((preset) => preset.id === "evening")?.label).toBe("This evening");
273 expect(278 expect(
274 new Date(presets.find((preset) => preset.id === "tomorrow")!.snoozedUntil).getHours(),279 new Date(presets.find((preset) => preset.id === "tomorrow")!.snoozedUntil).getHours(),
...@@ -278,6 +283,7 @@ describe("resolveSnoozePresets", () => {...@@ -278,6 +283,7 @@ describe("resolveSnoozePresets", () => {
278 it("drops the evening choice once evening is near or past", () => {283 it("drops the evening choice once evening is near or past", () => {
279 expect(resolveSnoozePresets(localDate(2026, 4, 8, 17, 30)).map((preset) => preset.id)).toEqual([284 expect(resolveSnoozePresets(localDate(2026, 4, 8, 17, 30)).map((preset) => preset.id)).toEqual([
280 "hour",285 "hour",
286 "three-hours",
281 "tomorrow",287 "tomorrow",
282 "next-week",288 "next-week",
283 ]);289 ]);