Skip to content

Commit fc32ff3

Browse files
fix: persist event reminder notifications
Remove the auto-dismiss timeout from calendar event reminders and add a regression test for persistent notifications.
1 parent d1681eb commit fc32ff3

File tree

6 files changed

+118
-16
lines changed

6 files changed

+118
-16
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createMergeableStore } from "tinybase/with-schemas";
2+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3+
4+
import { SCHEMA } from "@hypr/store";
5+
6+
const pluginNotification = vi.hoisted(() => ({
7+
showNotification: vi.fn().mockResolvedValue({ status: "ok", data: null }),
8+
}));
9+
10+
vi.mock("@hypr/plugin-notification", async () => {
11+
const actual = await vi.importActual<
12+
typeof import("@hypr/plugin-notification")
13+
>("@hypr/plugin-notification");
14+
15+
return {
16+
...actual,
17+
commands: {
18+
...actual.commands,
19+
showNotification: pluginNotification.showNotification,
20+
},
21+
};
22+
});
23+
24+
import { checkEventNotifications } from "./index";
25+
26+
import { SCHEMA as SETTINGS_SCHEMA } from "~/store/tinybase/store/settings";
27+
28+
function createMainStore() {
29+
return createMergeableStore()
30+
.setTablesSchema(SCHEMA.table)
31+
.setValuesSchema(SCHEMA.value);
32+
}
33+
34+
function createSettingsStore() {
35+
return createMergeableStore()
36+
.setTablesSchema(SETTINGS_SCHEMA.table)
37+
.setValuesSchema(SETTINGS_SCHEMA.value);
38+
}
39+
40+
describe("checkEventNotifications", () => {
41+
beforeEach(() => {
42+
vi.useFakeTimers();
43+
vi.setSystemTime(new Date("2026-03-30T16:00:00.000Z"));
44+
pluginNotification.showNotification.mockClear();
45+
});
46+
47+
afterEach(() => {
48+
vi.useRealTimers();
49+
});
50+
51+
test("shows persistent notifications for upcoming events", () => {
52+
const store = createMainStore();
53+
const settingsStore = createSettingsStore();
54+
const startTime = "2026-03-30T16:02:00.000Z";
55+
56+
settingsStore.setValue("notification_event", true);
57+
store.setRow("events", "event-1", {
58+
title: "Design review",
59+
started_at: startTime,
60+
});
61+
62+
checkEventNotifications(store, settingsStore, new Map());
63+
64+
expect(pluginNotification.showNotification).toHaveBeenCalledWith(
65+
expect.objectContaining({
66+
key: `event-event-1-${new Date(startTime).getTime()}`,
67+
title: "Design review",
68+
message: "Starting in 2 minutes",
69+
timeout: null,
70+
source: { type: "calendar_event", event_id: "event-1" },
71+
action_label: "Start listening",
72+
}),
73+
);
74+
});
75+
});

apps/desktop/src/services/event-notification/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export function checkEventNotifications(
132132
key: notificationKey,
133133
title: title,
134134
message: `Starting in ${minutesUntil} minute${minutesUntil !== 1 ? "s" : ""}`,
135-
timeout: { secs: 30, nanos: 0 },
135+
timeout: null,
136136
source: { type: "calendar_event", event_id: eventId },
137137
start_time: Math.floor(startTime.getTime() / 1000),
138138
participants: participants,

crates/notification-macos/swift-lib/src/NotificationInstance.swift

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class NotificationInstance {
1616

1717
var countdownTimer: Timer?
1818
var meetingStartTime: Date?
19-
weak var timerLabel: NSTextField?
19+
var compactTimerLabel: NSTextField?
20+
var expandedTimerLabel: NSTextField?
2021
weak var progressBar: NotificationBackgroundView? {
2122
didSet {
2223
progressBar?.onProgressComplete = { [weak self] in
@@ -45,28 +46,46 @@ class NotificationInstance {
4546
NotificationManager.shared.animateExpansion(notification: self, isExpanded: isExpanded)
4647
}
4748

48-
func startCountdown(label: NSTextField) {
49-
timerLabel = label
49+
func stopCountdown() {
50+
countdownTimer?.invalidate()
51+
countdownTimer = nil
52+
}
53+
54+
func setCompactCountdownLabel(_ label: NSTextField) {
55+
compactTimerLabel = label
56+
startCountdownIfNeeded()
5057
updateCountdown()
58+
}
59+
60+
func setExpandedCountdownLabel(_ label: NSTextField) {
61+
expandedTimerLabel = label
62+
startCountdownIfNeeded()
63+
updateCountdown()
64+
}
65+
66+
func clearExpandedCountdownLabel() {
67+
expandedTimerLabel = nil
68+
}
69+
70+
private func startCountdownIfNeeded() {
71+
guard meetingStartTime != nil else {
72+
stopCountdown()
73+
return
74+
}
75+
guard countdownTimer == nil else { return }
5176

52-
countdownTimer?.invalidate()
5377
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
5478
self?.updateCountdown()
5579
}
5680
}
5781

58-
func stopCountdown() {
59-
countdownTimer?.invalidate()
60-
countdownTimer = nil
61-
timerLabel = nil
62-
}
63-
6482
private func updateCountdown() {
65-
guard let startTime = meetingStartTime, let label = timerLabel else { return }
83+
guard let startTime = meetingStartTime else { return }
6684
let remaining = startTime.timeIntervalSinceNow
6785

6886
if remaining <= 0 {
69-
label.stringValue = "Started"
87+
compactTimerLabel?.stringValue = "Started"
88+
expandedTimerLabel?.stringValue = "Started"
7089
countdownTimer?.invalidate()
7190
countdownTimer = nil
7291

@@ -77,7 +96,9 @@ class NotificationInstance {
7796
} else {
7897
let minutes = Int(remaining) / 60
7998
let seconds = Int(remaining) % 60
80-
label.stringValue = "Begins in \(minutes):\(String(format: "%02d", seconds))"
99+
let countdownText = "Begins in \(minutes):\(String(format: "%02d", seconds))"
100+
compactTimerLabel?.stringValue = countdownText
101+
expandedTimerLabel?.stringValue = countdownText
81102
}
82103
}
83104

@@ -103,6 +124,8 @@ class NotificationInstance {
103124
progressBar?.onProgressComplete = nil
104125
progressBar?.resetProgress()
105126
stopCountdown()
127+
compactTimerLabel = nil
128+
expandedTimerLabel = nil
106129

107130
NSAnimationContext.runAnimationGroup({ context in
108131
context.duration = Timing.dismiss

crates/notification-macos/swift-lib/src/NotificationManager+Animation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension NotificationManager {
8484
}
8585

8686
private func animateToCompact(notification: NotificationInstance, frame: NSRect) {
87-
notification.stopCountdown()
87+
notification.clearExpandedCountdownLabel()
8888
notification.expandedContentView?.removeFromSuperview()
8989
notification.expandedContentView = nil
9090
notification.compactContentView?.alphaValue = 0

crates/notification-macos/swift-lib/src/NotificationManager+CompactView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ extension NotificationManager {
5757
textStack.addArrangedSubview(titleLabel)
5858
textStack.addArrangedSubview(bodyLabel)
5959

60+
if notification.meetingStartTime != nil {
61+
notification.setCompactCountdownLabel(bodyLabel)
62+
}
63+
6064
container.addArrangedSubview(iconContainer)
6165
container.addArrangedSubview(textStack)
6266

crates/notification-macos/swift-lib/src/NotificationManager+ExpandedView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extension NotificationManager {
5555
container.addArrangedSubview(actionStack)
5656
actionStack.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
5757

58-
notification.startCountdown(label: timerLabel)
58+
notification.setExpandedCountdownLabel(timerLabel)
5959

6060
return container
6161
}

0 commit comments

Comments
 (0)