Skip to content

Commit 57e68e3

Browse files
Add notification auto-stop countdown
Show the 30-second auto-stop countdown in the compact notification, animate the Stop button progress, and stop listening when the notification times out.
1 parent 361b36a commit 57e68e3

8 files changed

Lines changed: 97 additions & 21 deletions

File tree

apps/desktop/src/services/event-listeners.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ function shouldAutoStartNotificationSession(
6969
}
7070

7171
function handleAutoStopEndedNotification(
72-
type: "notification_confirm" | "notification_accept",
72+
type: "notification_confirm" | "notification_accept" | "notification_timeout",
7373
key: string,
7474
): boolean {
7575
const sessionId = parseAutoStopEndedNotificationKey(key);
7676
if (!sessionId) {
7777
return false;
7878
}
7979

80-
if (type !== "notification_accept") {
80+
if (type === "notification_confirm") {
8181
return true;
8282
}
8383

@@ -362,12 +362,17 @@ function useNotificationEvents() {
362362
.listen(({ payload }) => {
363363
if (
364364
payload.type === "notification_confirm" ||
365-
payload.type === "notification_accept"
365+
payload.type === "notification_accept" ||
366+
payload.type === "notification_timeout"
366367
) {
367368
if (handleAutoStopEndedNotification(payload.type, payload.key)) {
368369
return;
369370
}
370371

372+
if (payload.type === "notification_timeout") {
373+
return;
374+
}
375+
371376
const eventId =
372377
payload.source?.type === "calendar_event"
373378
? payload.source.event_id

apps/desktop/src/stt/auto-stop-notification.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const AUTO_STOP_ENDED_NOTIFICATION_KEY_PREFIX =
22
"auto-stop-ended:" as const;
3+
export const AUTO_STOP_CONFIRM_TIMEOUT_SECONDS = 30;
34

45
const AUTO_STOP_ENDED_NOTIFICATION_KEY_NONCE_SEPARATOR = ":prompt:";
56

apps/desktop/src/stt/contexts.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -887,8 +887,8 @@ describe("ListenerProvider detect events", () => {
887887
expect(notification).toEqual({
888888
key: expect.stringContaining("auto-stop-ended:session-1"),
889889
title: "Did your meeting end?",
890-
message: `${browser.name} stopped using the microphone before the scheduled end time.`,
891-
timeout: { secs: 60, nanos: 0 },
890+
message: "Anarlog will stop listening in 30 seconds.",
891+
timeout: { secs: 30, nanos: 0 },
892892
source: null,
893893
start_time: null,
894894
participants: null,

apps/desktop/src/stt/contexts.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
type NotificationIcon,
1212
} from "@hypr/plugin-notification";
1313

14-
import { createAutoStopEndedNotificationKey } from "./auto-stop-notification";
14+
import {
15+
AUTO_STOP_CONFIRM_TIMEOUT_SECONDS,
16+
createAutoStopEndedNotificationKey,
17+
} from "./auto-stop-notification";
1518

1619
import { getSessionEventById } from "~/session/utils";
1720
import * as main from "~/store/tinybase/store/main";
@@ -261,11 +264,6 @@ function getAutoStopActiveCheckAppIds(
261264
return [...new Set([...candidateAppIds, ...unreliableTriggerAppIds])];
262265
}
263266

264-
function getStoppedAppLabel(app: MicApp | null) {
265-
const name = app ? getNotificationAppName(app).trim() : "";
266-
return name || "The meeting app";
267-
}
268-
269267
function showMeetingEndedPrompt({
270268
sessionId,
271269
stoppedTriggerAppIds,
@@ -280,8 +278,8 @@ function showMeetingEndedPrompt({
280278
void notificationCommands.showNotification({
281279
key: createAutoStopEndedNotificationKey(sessionId),
282280
title: "Did your meeting end?",
283-
message: `${getStoppedAppLabel(app)} stopped using the microphone before the scheduled end time.`,
284-
timeout: { secs: 60, nanos: 0 },
281+
message: `Anarlog will stop listening in ${AUTO_STOP_CONFIRM_TIMEOUT_SECONDS} seconds.`,
282+
timeout: { secs: AUTO_STOP_CONFIRM_TIMEOUT_SECONDS, nanos: 0 },
285283
source: null,
286284
start_time: null,
287285
participants: null,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ struct NotificationPayload: Codable {
230230
return actionVariant == .destructive
231231
}
232232

233+
var showsStopCountdown: Bool {
234+
return isDestructiveAction && actionLabel == "Stop" && timeoutSeconds > 0
235+
}
236+
233237
var hasOptions: Bool {
234238
guard let options = options else { return false }
235239
return !options.isEmpty

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ class CompactActionButton: ActionButton {
215215
}
216216
}
217217
var onProgressComplete: (() -> Void)?
218+
private var progressBaseBackgroundColor = Colors.compactActionButtonElapsedBg
219+
private var progressFillColor = Colors.compactActionButtonRemainingBg
218220

219221
override init(frame frameRect: NSRect) {
220222
super.init(frame: frameRect)
@@ -237,7 +239,7 @@ class CompactActionButton: ActionButton {
237239
layer?.cornerCurve = .continuous
238240
}
239241

240-
progressLayer.backgroundColor = Colors.compactActionButtonRemainingBg
242+
progressLayer.backgroundColor = progressFillColor
241243
progressLayer.anchorPoint = CGPoint(x: 0, y: 0.5)
242244
progressLayer.isHidden = true
243245
layer?.addSublayer(progressLayer)
@@ -258,6 +260,12 @@ class CompactActionButton: ActionButton {
258260
}
259261
}
260262

263+
func configureDestructiveCountdownStyle() {
264+
progressBaseBackgroundColor = Colors.actionButtonDestructivePressedBg
265+
progressFillColor = Colors.actionButtonDestructiveBg
266+
progressLayer.backgroundColor = progressFillColor
267+
}
268+
261269
private func syncProgressLayerFrame() {
262270
let width = isPaused ? progressLayerFullWidth * progressRatio : progressLayerFullWidth
263271
progressLayer.frame = CGRect(x: 0, y: 0, width: width, height: bounds.height)
@@ -317,7 +325,8 @@ class CompactActionButton: ActionButton {
317325
isCountdownActive = true
318326
progressRatio = 1.0
319327

320-
layer?.backgroundColor = Colors.compactActionButtonElapsedBg
328+
layer?.backgroundColor = progressBaseBackgroundColor
329+
progressLayer.backgroundColor = progressFillColor
321330
progressLayer.isHidden = false
322331
progressLayer.removeAllAnimations()
323332
syncProgressLayerFrame()
@@ -363,7 +372,7 @@ class CompactActionButton: ActionButton {
363372
clearProgressState()
364373

365374
if showsProgress {
366-
layer?.backgroundColor = Colors.buttonNormalBg
375+
layer?.backgroundColor = progressBaseBackgroundColor
367376
}
368377

369378
CATransaction.begin()

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class NotificationInstance {
2323
var meetingStartTime: Date?
2424
weak var timerLabel: NSTextField?
2525
weak var compactMessageLabel: NSTextField?
26+
weak var stopCountdownLabel: NSTextField?
27+
var stopCountdownTimer: Timer?
2628

2729
init(
2830
payload: NotificationPayload, panel: NSPanel, clickableView: ClickableView, creationIndex: Int
@@ -60,6 +62,11 @@ class NotificationInstance {
6062
timerLabel = nil
6163
}
6264

65+
func bindStopCountdownLabel(_ label: NSTextField) {
66+
stopCountdownLabel = label
67+
updateStopCountdownLabel(remainingSeconds: payload.timeoutSeconds)
68+
}
69+
6370
func startScheduleUpdates() {
6471
guard let meetingStartTime else { return }
6572
updateScheduleLabels()
@@ -75,8 +82,11 @@ class NotificationInstance {
7582
func stopScheduleUpdates() {
7683
countdownTimer?.invalidate()
7784
countdownTimer = nil
85+
stopCountdownTimer?.invalidate()
86+
stopCountdownTimer = nil
7887
timerLabel = nil
7988
compactMessageLabel = nil
89+
stopCountdownLabel = nil
8090
}
8191

8292
private func updateScheduleLabels() {
@@ -110,6 +120,7 @@ class NotificationInstance {
110120
remainingDismissSeconds = timeoutSeconds
111121
dismissStartTime = Date()
112122
scheduleDismissTimer(after: timeoutSeconds)
123+
startStopCountdownUpdates()
113124

114125
if let compactActionButton {
115126
compactActionButton.startProgress(duration: timeoutSeconds)
@@ -125,6 +136,9 @@ class NotificationInstance {
125136
}
126137
dismissTimer?.invalidate()
127138
dismissTimer = nil
139+
stopCountdownTimer?.invalidate()
140+
stopCountdownTimer = nil
141+
updateStopCountdownLabel(remainingSeconds: remainingDismissSeconds)
128142

129143
if let compactActionButton {
130144
compactActionButton.pauseProgress()
@@ -135,6 +149,7 @@ class NotificationInstance {
135149
guard timeoutSeconds > 0, remainingDismissSeconds > 0 else { return }
136150
dismissStartTime = Date()
137151
scheduleDismissTimer(after: remainingDismissSeconds)
152+
startStopCountdownUpdates()
138153

139154
if let compactActionButton {
140155
compactActionButton.resumeProgress()
@@ -148,6 +163,7 @@ class NotificationInstance {
148163
remainingDismissSeconds = timeoutSeconds
149164
dismissStartTime = Date()
150165
scheduleDismissTimer(after: timeoutSeconds)
166+
startStopCountdownUpdates()
151167

152168
if let compactActionButton {
153169
compactActionButton.startProgress(duration: timeoutSeconds)
@@ -157,6 +173,8 @@ class NotificationInstance {
157173
func dismiss() {
158174
dismissTimer?.invalidate()
159175
dismissTimer = nil
176+
stopCountdownTimer?.invalidate()
177+
stopCountdownTimer = nil
160178
dismissStartTime = nil
161179
remainingDismissSeconds = 0
162180
compactActionButton?.resetProgress()
@@ -191,8 +209,40 @@ class NotificationInstance {
191209
}
192210
}
193211

212+
func stopCountdownText(_ remainingSeconds: Double) -> String {
213+
let seconds = max(0, Int(ceil(remainingSeconds)))
214+
return "Anarlog will stop listening in \(seconds) seconds."
215+
}
216+
217+
private func startStopCountdownUpdates() {
218+
guard stopCountdownLabel != nil else { return }
219+
updateStopCountdownLabel()
220+
221+
stopCountdownTimer?.invalidate()
222+
stopCountdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {
223+
[weak self] _ in
224+
self?.updateStopCountdownLabel()
225+
}
226+
}
227+
228+
private func updateStopCountdownLabel(remainingSeconds: Double? = nil) {
229+
guard stopCountdownLabel != nil else { return }
230+
231+
let remaining: Double
232+
if let remainingSeconds {
233+
remaining = remainingSeconds
234+
} else if let dismissStartTime {
235+
remaining = max(0, remainingDismissSeconds - Date().timeIntervalSince(dismissStartTime))
236+
} else {
237+
remaining = remainingDismissSeconds
238+
}
239+
240+
stopCountdownLabel?.stringValue = stopCountdownText(remaining)
241+
}
242+
194243
deinit {
195244
countdownTimer?.invalidate()
196245
dismissTimer?.invalidate()
246+
stopCountdownTimer?.invalidate()
197247
}
198248
}

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ extension NotificationManager {
1414
let actionLabel = notification.payload.actionLabel ?? "Open Anarlog"
1515
if notification.payload.isDestructiveAction {
1616
actionButton.configureDestructiveAction(label: actionLabel)
17-
actionButton.showsProgress = false
17+
actionButton.showsProgress = notification.payload.showsStopCountdown
18+
if notification.payload.showsStopCountdown {
19+
actionButton.configureDestructiveCountdownStyle()
20+
}
1821
} else {
1922
actionButton.title = actionLabel
2023
}
@@ -64,10 +67,14 @@ extension NotificationManager {
6467

6568
textStack.addArrangedSubview(titleLabel)
6669

67-
let compactMessage =
68-
notification.meetingStartTime != nil
69-
? "Starting soon"
70-
: notification.payload.message
70+
let compactMessage: String
71+
if notification.meetingStartTime != nil {
72+
compactMessage = "Starting soon"
73+
} else if notification.payload.showsStopCountdown {
74+
compactMessage = notification.stopCountdownText(notification.payload.timeoutSeconds)
75+
} else {
76+
compactMessage = notification.payload.message
77+
}
7178
if !compactMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
7279
let bodyLabel = NSTextField(labelWithString: compactMessage)
7380
bodyLabel.font = NSFont.systemFont(ofSize: Fonts.bodySize, weight: Fonts.bodyWeight)
@@ -82,6 +89,8 @@ extension NotificationManager {
8289

8390
if notification.meetingStartTime != nil {
8491
notification.bindCompactMessageLabel(bodyLabel)
92+
} else if notification.payload.showsStopCountdown {
93+
notification.bindStopCountdownLabel(bodyLabel)
8594
}
8695
}
8796

0 commit comments

Comments
 (0)