Close stale notifications once the item is dealt with
This commit is contained in:
@@ -31,9 +31,6 @@
|
|||||||
|
|
||||||
# M7 — Inbox triage
|
# M7 — Inbox triage
|
||||||
|
|
||||||
- Close stale notifications once the item is dealt with
|
|
||||||
`Notifications.swift` removes each delivered notification by its identifier when `inbox.updated` leaves the item no longer open, or `summary.read` covers its `turn-<seq>`, so a question answered on another Mac or by the lead leaves Notification Center. Factory clears notifications when the session is focused.
|
|
||||||
|
|
||||||
# M8 — macOS client
|
# M8 — macOS client
|
||||||
|
|
||||||
- Phone app
|
- Phone app
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ The running macOS app posts a notification for each new inbox item, raised by th
|
|||||||
|
|
||||||
- **Preferences.** `users.notification_prefs` holds which item kinds notify the user, returned as `notification_prefs` on `GET /api/me` and set with `PUT /api/me/notifications`: `questions` (on by default; it also covers `client_run` items, whose body reads "Run on your Mac: <why>"), `reports` (task turns that end with `summary: true` on `turn.ended`, labelled "Turn summaries"), `errors` (`session_error`) and `rate_limits`. The app checks them against each item or `turn.ended` event as it arrives.
|
- **Preferences.** `users.notification_prefs` holds which item kinds notify the user, returned as `notification_prefs` on `GET /api/me` and set with `PUT /api/me/notifications`: `questions` (on by default; it also covers `client_run` items, whose body reads "Run on your Mac: <why>"), `reports` (task turns that end with `summary: true` on `turn.ended`, labelled "Turn summaries"), `errors` (`session_error`) and `rate_limits`. The app checks them against each item or `turn.ended` event as it arrives.
|
||||||
- **Content.** The title is `byre · <task title>` and the body the question (or the turn summary's first line, the error, or "Rate limited"), cut at 200 characters. The notification's identifier is the item's ID, or `turn-<seq>` for a turn summary, so a repeat replaces the earlier one.
|
- **Content.** The title is `byre · <task title>` and the body the question (or the turn summary's first line, the error, or "Rate limited"), cut at 200 characters. The notification's identifier is the item's ID, or `turn-<seq>` for a turn summary, so a repeat replaces the earlier one.
|
||||||
|
- **Clearing.** A delivered notification leaves Notification Center once it is dealt with anywhere, on this Mac, another one or by the lead: an `inbox.updated` event whose item is no longer `open` removes the item's notification (`clearInbox`), and a `summary.read` event removes that task's `turn-<seq>` notifications up to its `seq` (`clearTurns`).
|
||||||
- **Clicking** a notification activates the app, shows its window and points the right pane at the item's task.
|
- **Clicking** a notification activates the app, shows its window and points the right pane at the item's task.
|
||||||
|
|
||||||
The left pane's **Settings** tab asks macOS for notification permission and edits the preferences.
|
The left pane's **Settings** tab asks macOS for notification permission and edits the preferences.
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ private func itemText(_ item: InboxItem) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private var queue: Task<Void, Never>?
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func enqueue(_ op: @escaping @MainActor () async -> Void) {
|
||||||
|
let prev = queue
|
||||||
|
queue = Task {
|
||||||
|
await prev?.value
|
||||||
|
await op()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: String) {
|
private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: String) {
|
||||||
let line = trimmed(text).split(separator: "\n", omittingEmptySubsequences: false).first.map(String.init) ?? ""
|
let line = trimmed(text).split(separator: "\n", omittingEmptySubsequences: false).first.map(String.init) ?? ""
|
||||||
@@ -68,7 +80,7 @@ private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: Strin
|
|||||||
content.sound = .default
|
content.sound = .default
|
||||||
content.userInfo = ["task_id": taskId]
|
content.userInfo = ["task_id": taskId]
|
||||||
let request = UNNotificationRequest(identifier: tag, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: tag, content: content, trigger: nil)
|
||||||
Task {
|
enqueue {
|
||||||
let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
||||||
if status != .authorized && status != .provisional {
|
if status != .authorized && status != .provisional {
|
||||||
return
|
return
|
||||||
@@ -89,6 +101,29 @@ func notifyInbox(_ store: Store, _ item: InboxItem) {
|
|||||||
show(store, item.taskId, item.id, itemText(item))
|
show(store, item.taskId, item.id, itemText(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func clearInbox(_ item: InboxItem) {
|
||||||
|
if item.state == .open {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enqueue {
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [item.id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func clearTurns(_ taskId: String, through seq: Int64) {
|
||||||
|
enqueue {
|
||||||
|
let ids = await UNUserNotificationCenter.current().deliveredNotifications().map(\.request).filter { r in
|
||||||
|
guard r.content.userInfo["task_id"] as? String == taskId, r.identifier.hasPrefix("turn-"), let s = Int64(r.identifier.dropFirst(5)) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s <= seq
|
||||||
|
}.map(\.identifier)
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func notifyTurn(_ store: Store, _ ev: ByreEvent) {
|
func notifyTurn(_ store: Store, _ ev: ByreEvent) {
|
||||||
guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty else {
|
guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty else {
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ final class Store {
|
|||||||
try await self.loadInbox()
|
try await self.loadInbox()
|
||||||
}
|
}
|
||||||
case "inbox.updated":
|
case "inbox.updated":
|
||||||
|
clearInbox(try! decodePayload(ev.payload, as: InboxItem.self))
|
||||||
spawn("load inbox") {
|
spawn("load inbox") {
|
||||||
try await self.loadInbox()
|
try await self.loadInbox()
|
||||||
}
|
}
|
||||||
@@ -566,6 +567,7 @@ final class Store {
|
|||||||
case "summary.read":
|
case "summary.read":
|
||||||
if let taskId = ev.payload.value["task_id"] as? String, let seq = ev.payload.value["seq"] as? Int {
|
if let taskId = ev.payload.value["task_id"] as? String, let seq = ev.payload.value["seq"] as? Int {
|
||||||
_ = noteSummaryRead(taskId, Int64(seq))
|
_ = noteSummaryRead(taskId, Int64(seq))
|
||||||
|
clearTurns(taskId, through: Int64(seq))
|
||||||
}
|
}
|
||||||
case "focus.updated":
|
case "focus.updated":
|
||||||
if ev.actor != me?.login {
|
if ev.actor != me?.login {
|
||||||
|
|||||||
Reference in New Issue
Block a user