|
4 | 4 | // Activates only on the Console — where the in-app inbox lives — and only |
5 | 5 | // inside Tauri. It polls the in-app inbox and, when the window is in the |
6 | 6 | // background, surfaces each NEW message as a native OS notification (macOS |
7 | | -// Notification Center / Windows toast / Linux libnotify), badges the dock with |
8 | | -// the count missed while away, and focuses the app (deep-linking to the record |
9 | | -// when the plugin reports the click). |
| 7 | +// Notification Center / Windows toast / Linux libnotify) and badges the dock |
| 8 | +// with the count missed while away (cleared on focus). |
10 | 9 | // |
11 | 10 | // Source of truth is `sys_inbox_message` (ADR-0030 L5) — the same table the |
12 | | -// Console bell reads. This only mirrors new rows to the OS so they reach the |
13 | | -// user when the window isn't focused — the point of a desktop client. The |
14 | | -// dedicated `/api/v1/notifications` route isn't mounted in this runtime, so we |
15 | | -// read the canonical inbox object directly via the data API. |
| 11 | +// Console bell reads. The dedicated `/api/v1/notifications` route isn't mounted |
| 12 | +// in this runtime, so we read the canonical inbox object via the data API. |
| 13 | +// |
| 14 | +// All native work goes through Rust commands (`notify_native`, `set_badge`, |
| 15 | +// `notif_request_permission`) via `core.invoke` — matching this app's pattern. |
| 16 | +// `withGlobalTauri` only exposes `core`/`event`, NOT the plugin/window JS APIs, |
| 17 | +// so we must not call `__TAURI__.notification`/`.window`/`.app` directly. |
16 | 18 | (function () { |
17 | 19 | if (window.__objectosNotifyBridge) return; |
18 | 20 | // Only on the Console (its login/app routes live under /_console), and only |
19 | 21 | // when the Tauri bridge is present (no-op in a plain browser). |
20 | 22 | if (!/\/_console(\/|$)/.test(location.pathname)) return; |
21 | | - if (!window.__TAURI__) return; |
| 23 | + if (!window.__TAURI__ || !window.__TAURI__.core) return; |
22 | 24 | window.__objectosNotifyBridge = true; |
23 | 25 |
|
24 | | - var T = window.__TAURI__; |
| 26 | + var invoke = window.__TAURI__.core.invoke; |
25 | 27 | var INBOX_URL = '/api/v1/data/sys_inbox_message?sort=-created_at&limit=25'; |
26 | 28 | var POLL_MS = 20000; |
27 | 29 | var SEEN_KEY = '__objectos_notif_seen_v1'; |
|
30 | 32 | var baselined = false; // first successful poll adopts the backlog silently |
31 | 33 | var inFlight = false; |
32 | 34 | var fatal = false; // inbox object absent → stop |
33 | | - var permission = false; |
34 | 35 | var unseen = 0; // count surfaced while backgrounded, cleared on focus |
35 | | - var pendingUrl = {}; // notification id → action_url, for click routing |
36 | 36 |
|
37 | 37 | function loadSeen() { |
38 | 38 | try { |
|
55 | 55 | return document.visibilityState === 'hidden' || !document.hasFocus(); |
56 | 56 | } |
57 | 57 |
|
58 | | - function currentWindow() { |
59 | | - try { |
60 | | - var w = T.window; |
61 | | - if (!w) return null; |
62 | | - if (w.getCurrentWindow) return w.getCurrentWindow(); |
63 | | - if (w.getCurrent) return w.getCurrent(); |
64 | | - } catch (_) {} |
65 | | - return null; |
66 | | - } |
67 | | - |
68 | | - async function ensurePermission() { |
69 | | - try { |
70 | | - var n = T.notification; |
71 | | - if (!n) return false; |
72 | | - if (await n.isPermissionGranted()) return true; |
73 | | - return (await n.requestPermission()) === 'granted'; |
74 | | - } catch (_) { |
75 | | - return false; |
76 | | - } |
77 | | - } |
78 | | - |
79 | | - async function notify(id, title, body) { |
80 | | - var opts = { title: title || 'Notification', body: body || '', tag: id }; |
81 | | - try { |
82 | | - if (T.notification && T.notification.sendNotification) { |
83 | | - T.notification.sendNotification(opts); |
84 | | - return; |
85 | | - } |
86 | | - } catch (_) {} |
87 | | - // Fall back to the raw plugin command if the JS guest isn't on the global. |
| 58 | + function notify(title, body) { |
88 | 59 | try { |
89 | | - await T.core.invoke('plugin:notification|notify', { options: opts }); |
| 60 | + return invoke('notify_native', { title: title || 'Notification', body: body || '' }); |
90 | 61 | } catch (_) {} |
91 | 62 | } |
92 | | - |
93 | | - async function setBadge(count) { |
94 | | - var n = count > 0 ? count : null; |
95 | | - try { |
96 | | - if (T.app && T.app.setBadgeCount) { |
97 | | - await T.app.setBadgeCount(n); |
98 | | - return; |
99 | | - } |
100 | | - } catch (_) {} |
| 63 | + function setBadge(count) { |
101 | 64 | try { |
102 | | - var w = currentWindow(); |
103 | | - if (w && w.setBadgeCount) await w.setBadgeCount(n); |
| 65 | + return invoke('set_badge', { count: count > 0 ? count : null }); |
104 | 66 | } catch (_) {} |
105 | 67 | } |
106 | 68 |
|
107 | | - async function focusAndOpen(actionUrl) { |
108 | | - var w = currentWindow(); |
109 | | - if (w) { |
110 | | - try { await w.show(); } catch (_) {} |
111 | | - try { await w.unminimize(); } catch (_) {} |
112 | | - try { await w.setFocus(); } catch (_) {} |
113 | | - } |
114 | | - if (actionUrl) { |
115 | | - try { location.assign(actionUrl); } catch (_) {} |
116 | | - } |
117 | | - } |
118 | | - |
119 | | - // Best-effort click routing: if this build exposes notification actions, |
120 | | - // clicking a toast focuses the app and deep-links to the record. When it |
121 | | - // doesn't, the OS default (focus the app) still applies; only the deep-link |
122 | | - // is lost. |
123 | | - (async function wireActions() { |
124 | | - try { |
125 | | - if (T.notification && T.notification.onAction) { |
126 | | - await T.notification.onAction(function (n) { |
127 | | - var tag = n && n.tag; |
128 | | - focusAndOpen(tag ? pendingUrl[tag] : null); |
129 | | - }); |
130 | | - } |
131 | | - } catch (_) {} |
132 | | - })(); |
133 | | - |
134 | 69 | // sys_inbox_message → the minimal shape we need for a toast. |
135 | 70 | function view(row) { |
136 | 71 | return { |
137 | 72 | id: row && row.id, |
138 | 73 | title: (row && row.title) || (row && row.topic) || 'Notification', |
139 | 74 | body: (row && (row.body_md || row.body)) || '', |
140 | | - actionUrl: (row && row.action_url) || null, |
141 | 75 | createdAt: (row && row.created_at) || '', |
142 | 76 | }; |
143 | 77 | } |
|
172 | 106 | fresh.forEach(function (v) { seen.add(v.id); }); |
173 | 107 | baselined = true; |
174 | 108 | } else { |
175 | | - var allowed = backgrounded() && permission; |
| 109 | + var allowed = backgrounded(); |
176 | 110 | fresh.forEach(function (v) { |
177 | 111 | seen.add(v.id); |
178 | 112 | if (allowed) { |
179 | | - pendingUrl[v.id] = v.actionUrl; |
180 | | - notify(v.id, v.title, String(v.body).slice(0, 240)); |
| 113 | + notify(v.title, String(v.body).slice(0, 240)); |
181 | 114 | unseen += 1; |
182 | 115 | } |
183 | 116 | }); |
|
198 | 131 | poll(); |
199 | 132 | } |
200 | 133 |
|
201 | | - // Ask once up front, while the window is in the foreground, so the OS prompt |
202 | | - // isn't sprung from the background. |
203 | | - ensurePermission().then(function (g) { permission = g; }); |
| 134 | + // Ask once up front, in the foreground, so the OS prompt isn't sprung from |
| 135 | + // the background. |
| 136 | + try { invoke('notif_request_permission'); } catch (_) {} |
204 | 137 |
|
205 | 138 | poll(); |
206 | 139 | setInterval(poll, POLL_MS); |
|
0 commit comments