@bobfrankston/rmfmail 1.0.467
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +429 -0
- package/bin/build-icon-ico.js +283 -0
- package/bin/lean-accounts.js +172 -0
- package/bin/mailx.js +1580 -0
- package/bin/mailx.js.map +1 -0
- package/bin/mailx.ts +1549 -0
- package/bin/postinstall.js +39 -0
- package/bin/postinstall.js.map +1 -0
- package/bin/postinstall.ts +41 -0
- package/bin/tsconfig.json +10 -0
- package/client/.gitattributes +10 -0
- package/client/.msger-window.json +1 -0
- package/client/android.html +226 -0
- package/client/app.js +3400 -0
- package/client/app.js.map +1 -0
- package/client/app.ts +3190 -0
- package/client/components/address-book.js +199 -0
- package/client/components/address-book.js.map +1 -0
- package/client/components/address-book.ts +204 -0
- package/client/components/alarms.js +299 -0
- package/client/components/alarms.js.map +1 -0
- package/client/components/alarms.ts +276 -0
- package/client/components/calendar-sidebar.js +508 -0
- package/client/components/calendar-sidebar.js.map +1 -0
- package/client/components/calendar-sidebar.ts +474 -0
- package/client/components/calendar.js +217 -0
- package/client/components/calendar.js.map +1 -0
- package/client/components/calendar.ts +211 -0
- package/client/components/context-menu.js +80 -0
- package/client/components/context-menu.js.map +1 -0
- package/client/components/context-menu.ts +95 -0
- package/client/components/folder-picker.js +119 -0
- package/client/components/folder-picker.js.map +1 -0
- package/client/components/folder-picker.ts +127 -0
- package/client/components/folder-tree.js +1093 -0
- package/client/components/folder-tree.js.map +1 -0
- package/client/components/folder-tree.ts +1069 -0
- package/client/components/message-list.js +1140 -0
- package/client/components/message-list.js.map +1 -0
- package/client/components/message-list.ts +1129 -0
- package/client/components/message-viewer.js +1291 -0
- package/client/components/message-viewer.js.map +1 -0
- package/client/components/message-viewer.ts +1257 -0
- package/client/components/outbox-view.js +104 -0
- package/client/components/outbox-view.js.map +1 -0
- package/client/components/outbox-view.ts +102 -0
- package/client/components/tasks.js +261 -0
- package/client/components/tasks.js.map +1 -0
- package/client/components/tasks.ts +234 -0
- package/client/compose/compose.css +560 -0
- package/client/compose/compose.html +52 -0
- package/client/compose/compose.js +1289 -0
- package/client/compose/compose.js.map +1 -0
- package/client/compose/compose.ts +1231 -0
- package/client/compose/editor.js +616 -0
- package/client/compose/editor.js.map +1 -0
- package/client/compose/editor.ts +599 -0
- package/client/compose/ghost-text.js +122 -0
- package/client/compose/ghost-text.js.map +1 -0
- package/client/compose/ghost-text.ts +140 -0
- package/client/favicon.svg +11 -0
- package/client/icon.ico +0 -0
- package/client/icon.png +0 -0
- package/client/index.html +230 -0
- package/client/lib/android-bootstrap.js +9 -0
- package/client/lib/android-bootstrap.js.map +1 -0
- package/client/lib/android-bootstrap.ts +9 -0
- package/client/lib/api-client.js +378 -0
- package/client/lib/api-client.js.map +1 -0
- package/client/lib/api-client.ts +439 -0
- package/client/lib/local-service.js +603 -0
- package/client/lib/local-service.js.map +1 -0
- package/client/lib/local-service.ts +646 -0
- package/client/lib/local-store.js +214 -0
- package/client/lib/local-store.js.map +1 -0
- package/client/lib/local-store.ts +283 -0
- package/client/lib/mailxapi.js +304 -0
- package/client/lib/message-state.js +132 -0
- package/client/lib/message-state.js.map +1 -0
- package/client/lib/message-state.ts +160 -0
- package/client/package.json +16 -0
- package/client/styles/components.css +2049 -0
- package/client/styles/layout.css +332 -0
- package/client/styles/variables.css +85 -0
- package/client/tsconfig.json +19 -0
- package/package.json +124 -0
- package/packages/mailx-api/.gitattributes +10 -0
- package/packages/mailx-api/index.d.ts +10 -0
- package/packages/mailx-api/index.d.ts.map +1 -0
- package/packages/mailx-api/index.js +322 -0
- package/packages/mailx-api/index.js.map +1 -0
- package/packages/mailx-api/index.ts +283 -0
- package/packages/mailx-api/package.json +27 -0
- package/packages/mailx-api/tsconfig.json +9 -0
- package/packages/mailx-api/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-compose/.gitattributes +10 -0
- package/packages/mailx-compose/index.d.ts +21 -0
- package/packages/mailx-compose/index.d.ts.map +1 -0
- package/packages/mailx-compose/index.js +61 -0
- package/packages/mailx-compose/index.js.map +1 -0
- package/packages/mailx-compose/index.ts +85 -0
- package/packages/mailx-compose/package.json +18 -0
- package/packages/mailx-compose/tsconfig.json +9 -0
- package/packages/mailx-core/index.d.ts +139 -0
- package/packages/mailx-core/index.d.ts.map +1 -0
- package/packages/mailx-core/index.js +403 -0
- package/packages/mailx-core/index.js.map +1 -0
- package/packages/mailx-core/index.ts +424 -0
- package/packages/mailx-core/ipc.d.ts +13 -0
- package/packages/mailx-core/ipc.d.ts.map +1 -0
- package/packages/mailx-core/ipc.js +56 -0
- package/packages/mailx-core/ipc.js.map +1 -0
- package/packages/mailx-core/ipc.ts +62 -0
- package/packages/mailx-core/package.json +18 -0
- package/packages/mailx-core/tsconfig.json +9 -0
- package/packages/mailx-host/.gitattributes +10 -0
- package/packages/mailx-host/index.d.ts +21 -0
- package/packages/mailx-host/index.d.ts.map +1 -0
- package/packages/mailx-host/index.js +29 -0
- package/packages/mailx-host/index.js.map +1 -0
- package/packages/mailx-host/index.ts +38 -0
- package/packages/mailx-host/package.json +23 -0
- package/packages/mailx-host/tsconfig.json +9 -0
- package/packages/mailx-host/types-shim.d.ts +14 -0
- package/packages/mailx-imap/.gitattributes +10 -0
- package/packages/mailx-imap/index.d.ts +442 -0
- package/packages/mailx-imap/index.d.ts.map +1 -0
- package/packages/mailx-imap/index.js +3684 -0
- package/packages/mailx-imap/index.js.map +1 -0
- package/packages/mailx-imap/index.ts +3652 -0
- package/packages/mailx-imap/package-lock.json +131 -0
- package/packages/mailx-imap/package.json +28 -0
- package/packages/mailx-imap/providers/gmail-api.d.ts +8 -0
- package/packages/mailx-imap/providers/gmail-api.d.ts.map +1 -0
- package/packages/mailx-imap/providers/gmail-api.js +8 -0
- package/packages/mailx-imap/providers/gmail-api.js.map +1 -0
- package/packages/mailx-imap/providers/gmail-api.ts +8 -0
- package/packages/mailx-imap/providers/outlook-api.ts +7 -0
- package/packages/mailx-imap/providers/types.d.ts +9 -0
- package/packages/mailx-imap/providers/types.d.ts.map +1 -0
- package/packages/mailx-imap/providers/types.js +9 -0
- package/packages/mailx-imap/providers/types.js.map +1 -0
- package/packages/mailx-imap/providers/types.ts +9 -0
- package/packages/mailx-imap/tsconfig.json +9 -0
- package/packages/mailx-imap/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-send/.gitattributes +10 -0
- package/packages/mailx-send/README.md +217 -0
- package/packages/mailx-send/cli-queue.d.ts +11 -0
- package/packages/mailx-send/cli-queue.d.ts.map +1 -0
- package/packages/mailx-send/cli-queue.js +59 -0
- package/packages/mailx-send/cli-queue.js.map +1 -0
- package/packages/mailx-send/cli-queue.ts +62 -0
- package/packages/mailx-send/cli-send.d.ts +17 -0
- package/packages/mailx-send/cli-send.d.ts.map +1 -0
- package/packages/mailx-send/cli-send.js +75 -0
- package/packages/mailx-send/cli-send.js.map +1 -0
- package/packages/mailx-send/cli-send.ts +83 -0
- package/packages/mailx-send/cli.d.ts +22 -0
- package/packages/mailx-send/cli.d.ts.map +1 -0
- package/packages/mailx-send/cli.js +115 -0
- package/packages/mailx-send/cli.js.map +1 -0
- package/packages/mailx-send/cli.ts +126 -0
- package/packages/mailx-send/index.d.ts +77 -0
- package/packages/mailx-send/index.d.ts.map +1 -0
- package/packages/mailx-send/index.js +264 -0
- package/packages/mailx-send/index.js.map +1 -0
- package/packages/mailx-send/index.ts +333 -0
- package/packages/mailx-send/mailsend/README.md +133 -0
- package/packages/mailx-send/mailsend/cli.d.ts +12 -0
- package/packages/mailx-send/mailsend/cli.d.ts.map +1 -0
- package/packages/mailx-send/mailsend/cli.js +70 -0
- package/packages/mailx-send/mailsend/cli.js.map +1 -0
- package/packages/mailx-send/mailsend/cli.ts +81 -0
- package/packages/mailx-send/mailsend/index.d.ts +77 -0
- package/packages/mailx-send/mailsend/index.d.ts.map +1 -0
- package/packages/mailx-send/mailsend/index.js +264 -0
- package/packages/mailx-send/mailsend/index.js.map +1 -0
- package/packages/mailx-send/mailsend/index.ts +333 -0
- package/packages/mailx-send/mailsend/package-lock.json +65 -0
- package/packages/mailx-send/mailsend/package.json +26 -0
- package/packages/mailx-send/mailsend/tsconfig.json +21 -0
- package/packages/mailx-send/package-lock.json +65 -0
- package/packages/mailx-send/package.json +37 -0
- package/packages/mailx-send/tsconfig.json +21 -0
- package/packages/mailx-server/.gitattributes +10 -0
- package/packages/mailx-server/index.d.ts +8 -0
- package/packages/mailx-server/index.d.ts.map +1 -0
- package/packages/mailx-server/index.js +397 -0
- package/packages/mailx-server/index.js.map +1 -0
- package/packages/mailx-server/index.ts +429 -0
- package/packages/mailx-server/package.json +28 -0
- package/packages/mailx-server/tsconfig.json +9 -0
- package/packages/mailx-server/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-service/google-sync.d.ts +96 -0
- package/packages/mailx-service/google-sync.d.ts.map +1 -0
- package/packages/mailx-service/google-sync.js +153 -0
- package/packages/mailx-service/google-sync.js.map +1 -0
- package/packages/mailx-service/google-sync.ts +238 -0
- package/packages/mailx-service/index.d.ts +366 -0
- package/packages/mailx-service/index.d.ts.map +1 -0
- package/packages/mailx-service/index.js +2589 -0
- package/packages/mailx-service/index.js.map +1 -0
- package/packages/mailx-service/index.ts +2512 -0
- package/packages/mailx-service/jsonrpc.d.ts +25 -0
- package/packages/mailx-service/jsonrpc.d.ts.map +1 -0
- package/packages/mailx-service/jsonrpc.js +238 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -0
- package/packages/mailx-service/jsonrpc.ts +268 -0
- package/packages/mailx-service/package.json +22 -0
- package/packages/mailx-service/tsconfig.json +9 -0
- package/packages/mailx-settings/.gitattributes +10 -0
- package/packages/mailx-settings/cloud.d.ts +47 -0
- package/packages/mailx-settings/cloud.d.ts.map +1 -0
- package/packages/mailx-settings/cloud.js +408 -0
- package/packages/mailx-settings/cloud.js.map +1 -0
- package/packages/mailx-settings/cloud.ts +393 -0
- package/packages/mailx-settings/index.d.ts +140 -0
- package/packages/mailx-settings/index.d.ts.map +1 -0
- package/packages/mailx-settings/index.js +872 -0
- package/packages/mailx-settings/index.js.map +1 -0
- package/packages/mailx-settings/index.ts +908 -0
- package/packages/mailx-settings/package.json +20 -0
- package/packages/mailx-settings/tsconfig.json +9 -0
- package/packages/mailx-settings/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-store/.gitattributes +10 -0
- package/packages/mailx-store/db.d.ts +346 -0
- package/packages/mailx-store/db.d.ts.map +1 -0
- package/packages/mailx-store/db.js +1796 -0
- package/packages/mailx-store/db.js.map +1 -0
- package/packages/mailx-store/db.ts +2007 -0
- package/packages/mailx-store/file-store.d.ts +36 -0
- package/packages/mailx-store/file-store.d.ts.map +1 -0
- package/packages/mailx-store/file-store.js +80 -0
- package/packages/mailx-store/file-store.js.map +1 -0
- package/packages/mailx-store/file-store.ts +82 -0
- package/packages/mailx-store/index.d.ts +7 -0
- package/packages/mailx-store/index.d.ts.map +1 -0
- package/packages/mailx-store/index.js +7 -0
- package/packages/mailx-store/index.js.map +1 -0
- package/packages/mailx-store/index.ts +7 -0
- package/packages/mailx-store/package.json +19 -0
- package/packages/mailx-store/tsconfig.json +9 -0
- package/packages/mailx-store/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-store-web/android-bootstrap.d.ts +16 -0
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -0
- package/packages/mailx-store-web/android-bootstrap.js +1247 -0
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -0
- package/packages/mailx-store-web/android-bootstrap.ts +1262 -0
- package/packages/mailx-store-web/db.d.ts +132 -0
- package/packages/mailx-store-web/db.d.ts.map +1 -0
- package/packages/mailx-store-web/db.js +642 -0
- package/packages/mailx-store-web/db.js.map +1 -0
- package/packages/mailx-store-web/db.ts +756 -0
- package/packages/mailx-store-web/gmail-api-web.d.ts +11 -0
- package/packages/mailx-store-web/gmail-api-web.d.ts.map +1 -0
- package/packages/mailx-store-web/gmail-api-web.js +11 -0
- package/packages/mailx-store-web/gmail-api-web.js.map +1 -0
- package/packages/mailx-store-web/gmail-api-web.ts +11 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts +33 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -0
- package/packages/mailx-store-web/imap-web-provider.js +140 -0
- package/packages/mailx-store-web/imap-web-provider.js.map +1 -0
- package/packages/mailx-store-web/imap-web-provider.ts +156 -0
- package/packages/mailx-store-web/index.d.ts +10 -0
- package/packages/mailx-store-web/index.d.ts.map +1 -0
- package/packages/mailx-store-web/index.js +10 -0
- package/packages/mailx-store-web/index.js.map +1 -0
- package/packages/mailx-store-web/index.ts +10 -0
- package/packages/mailx-store-web/main-thread-host.d.ts +15 -0
- package/packages/mailx-store-web/main-thread-host.d.ts.map +1 -0
- package/packages/mailx-store-web/main-thread-host.js +292 -0
- package/packages/mailx-store-web/main-thread-host.js.map +1 -0
- package/packages/mailx-store-web/main-thread-host.ts +322 -0
- package/packages/mailx-store-web/package.json +23 -0
- package/packages/mailx-store-web/provider-types.d.ts +7 -0
- package/packages/mailx-store-web/provider-types.d.ts.map +1 -0
- package/packages/mailx-store-web/provider-types.js +7 -0
- package/packages/mailx-store-web/provider-types.js.map +1 -0
- package/packages/mailx-store-web/provider-types.ts +7 -0
- package/packages/mailx-store-web/sql-wasm-esm.js +10 -0
- package/packages/mailx-store-web/sql.js.d.ts +29 -0
- package/packages/mailx-store-web/sync-manager.d.ts +68 -0
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -0
- package/packages/mailx-store-web/sync-manager.js +506 -0
- package/packages/mailx-store-web/sync-manager.js.map +1 -0
- package/packages/mailx-store-web/sync-manager.ts +508 -0
- package/packages/mailx-store-web/tsconfig.json +10 -0
- package/packages/mailx-store-web/web-jsonrpc.d.ts +20 -0
- package/packages/mailx-store-web/web-jsonrpc.d.ts.map +1 -0
- package/packages/mailx-store-web/web-jsonrpc.js +102 -0
- package/packages/mailx-store-web/web-jsonrpc.js.map +1 -0
- package/packages/mailx-store-web/web-jsonrpc.ts +116 -0
- package/packages/mailx-store-web/web-message-store.d.ts +16 -0
- package/packages/mailx-store-web/web-message-store.d.ts.map +1 -0
- package/packages/mailx-store-web/web-message-store.js +89 -0
- package/packages/mailx-store-web/web-message-store.js.map +1 -0
- package/packages/mailx-store-web/web-message-store.ts +97 -0
- package/packages/mailx-store-web/web-service.d.ts +111 -0
- package/packages/mailx-store-web/web-service.d.ts.map +1 -0
- package/packages/mailx-store-web/web-service.js +542 -0
- package/packages/mailx-store-web/web-service.js.map +1 -0
- package/packages/mailx-store-web/web-service.ts +616 -0
- package/packages/mailx-store-web/web-settings.d.ts +83 -0
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -0
- package/packages/mailx-store-web/web-settings.js +491 -0
- package/packages/mailx-store-web/web-settings.js.map +1 -0
- package/packages/mailx-store-web/web-settings.ts +522 -0
- package/packages/mailx-store-web/worker-bundle.js +6838 -0
- package/packages/mailx-store-web/worker-entry.d.ts +8 -0
- package/packages/mailx-store-web/worker-entry.d.ts.map +1 -0
- package/packages/mailx-store-web/worker-entry.js +187 -0
- package/packages/mailx-store-web/worker-entry.js.map +1 -0
- package/packages/mailx-store-web/worker-entry.ts +215 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts +28 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts.map +1 -0
- package/packages/mailx-store-web/worker-tcp-transport.js +98 -0
- package/packages/mailx-store-web/worker-tcp-transport.js.map +1 -0
- package/packages/mailx-store-web/worker-tcp-transport.ts +101 -0
- package/packages/mailx-types/.gitattributes +10 -0
- package/packages/mailx-types/index.d.ts +319 -0
- package/packages/mailx-types/index.d.ts.map +1 -0
- package/packages/mailx-types/index.js +243 -0
- package/packages/mailx-types/index.js.map +1 -0
- package/packages/mailx-types/index.ts +498 -0
- package/packages/mailx-types/package.json +16 -0
- package/packages/mailx-types/tsconfig.json +9 -0
- package/packages/mailx-types/tsconfig.tsbuildinfo +1 -0
- package/todo.json +13 -0
- package/tsconfig.base.json +31 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alarm subsystem β Thunderbird/Outlook-style reminder popups.
|
|
3
|
+
*
|
|
4
|
+
* Design decisions (2026-04-24):
|
|
5
|
+
* - One shared subsystem for calendar events + tasks (+ future mail reminders).
|
|
6
|
+
* - Popup shows item title, scheduled time, Snooze (5 / 15 / 30 min / 1 hr /
|
|
7
|
+
* custom), Dismiss. Snooze delays the alarm by the chosen interval; Dismiss
|
|
8
|
+
* suppresses it permanently.
|
|
9
|
+
* - Dismissed / snoozed state lives in localStorage (per-device). Google
|
|
10
|
+
* Calendar's own reminders aren't mutated by this β mailx's popup is a
|
|
11
|
+
* local convenience, not a reminder-authority replacement.
|
|
12
|
+
* - Default lead time: 10 min before calendar event start; at task due time
|
|
13
|
+
* for tasks. Per-event overrides from Google Calendar's `reminders.overrides`
|
|
14
|
+
* are a follow-up (we don't currently fetch that field).
|
|
15
|
+
*
|
|
16
|
+
* Check cadence: every 30 s while the tab/window is focused. No check when
|
|
17
|
+
* hidden β alarm won't fire until user returns, which matches Thunderbird's
|
|
18
|
+
* behavior (focus-gated). OS-level notifications are a separate follow-up.
|
|
19
|
+
*/
|
|
20
|
+
import { getCalendarEvents, getTasks } from "../lib/api-client.js";
|
|
21
|
+
const DISMISSED_KEY = "mailx-alarm-dismissed"; // { uuid: true }
|
|
22
|
+
const SNOOZED_KEY = "mailx-alarm-snoozed"; // { uuid: epoch-ms-end }
|
|
23
|
+
const CAL_LEAD_MS = 10 * 60 * 1000; // 10 min before cal event start
|
|
24
|
+
const LOOKBACK_MS = 60 * 60 * 1000; // fire past-due alarms from the last 60 min
|
|
25
|
+
const LOOKAHEAD_MS = 2 * 60 * 60 * 1000; // poll window: upcoming 2 hr
|
|
26
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
27
|
+
function loadDismissed() {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(localStorage.getItem(DISMISSED_KEY) || "{}");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function saveDismissed(map) {
|
|
36
|
+
try {
|
|
37
|
+
localStorage.setItem(DISMISSED_KEY, JSON.stringify(map));
|
|
38
|
+
}
|
|
39
|
+
catch { /* private mode */ }
|
|
40
|
+
}
|
|
41
|
+
function loadSnoozed() {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(localStorage.getItem(SNOOZED_KEY) || "{}");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveSnoozed(map) {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.setItem(SNOOZED_KEY, JSON.stringify(map));
|
|
52
|
+
}
|
|
53
|
+
catch { /* */ }
|
|
54
|
+
}
|
|
55
|
+
/** Prune expired entries so the maps don't grow forever. */
|
|
56
|
+
function pruneState(now) {
|
|
57
|
+
const snoozed = loadSnoozed();
|
|
58
|
+
let changed = false;
|
|
59
|
+
for (const [uuid, until] of Object.entries(snoozed)) {
|
|
60
|
+
// Snoozed entries older than 7 days have been fired already (or the
|
|
61
|
+
// event is long past); drop them.
|
|
62
|
+
if (until < now - 7 * 86400_000) {
|
|
63
|
+
delete snoozed[uuid];
|
|
64
|
+
changed = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (changed)
|
|
68
|
+
saveSnoozed(snoozed);
|
|
69
|
+
// Dismissed is sparse β don't bother pruning aggressively.
|
|
70
|
+
}
|
|
71
|
+
async function collectDueAlarms(now) {
|
|
72
|
+
const dismissed = loadDismissed();
|
|
73
|
+
const snoozed = loadSnoozed();
|
|
74
|
+
const items = [];
|
|
75
|
+
try {
|
|
76
|
+
const events = await getCalendarEvents(now - LOOKBACK_MS, now + LOOKAHEAD_MS);
|
|
77
|
+
for (const ev of events) {
|
|
78
|
+
if (!ev.uuid)
|
|
79
|
+
continue;
|
|
80
|
+
if (dismissed[ev.uuid])
|
|
81
|
+
continue;
|
|
82
|
+
const alarm = (ev.start || 0) - CAL_LEAD_MS;
|
|
83
|
+
const effective = snoozed[ev.uuid] || alarm;
|
|
84
|
+
if (effective <= now && effective > now - LOOKBACK_MS) {
|
|
85
|
+
items.push({
|
|
86
|
+
uuid: ev.uuid, kind: "calendar",
|
|
87
|
+
title: ev.title || "(no title)",
|
|
88
|
+
alarmMs: effective, whenMs: ev.start || 0,
|
|
89
|
+
htmlLink: ev.htmlLink,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* sidebar will surface API errors; alarm path stays quiet */ }
|
|
95
|
+
try {
|
|
96
|
+
const tasks = await getTasks(false);
|
|
97
|
+
for (const t of tasks) {
|
|
98
|
+
if (!t.uuid || !t.dueMs)
|
|
99
|
+
continue;
|
|
100
|
+
if (dismissed[t.uuid])
|
|
101
|
+
continue;
|
|
102
|
+
const effective = snoozed[t.uuid] || t.dueMs;
|
|
103
|
+
if (effective <= now && effective > now - LOOKBACK_MS) {
|
|
104
|
+
items.push({
|
|
105
|
+
uuid: t.uuid, kind: "task",
|
|
106
|
+
title: t.title || "(no title)",
|
|
107
|
+
alarmMs: effective, whenMs: t.dueMs,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch { /* */ }
|
|
113
|
+
return items;
|
|
114
|
+
}
|
|
115
|
+
function formatWhen(ms) {
|
|
116
|
+
if (!ms)
|
|
117
|
+
return "";
|
|
118
|
+
const d = new Date(ms);
|
|
119
|
+
return d.toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
120
|
+
}
|
|
121
|
+
let currentPopup = null;
|
|
122
|
+
let firedThisSession = new Set();
|
|
123
|
+
function showPopup(items) {
|
|
124
|
+
if (currentPopup)
|
|
125
|
+
return; // another popup already up
|
|
126
|
+
if (items.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const overlay = document.createElement("div");
|
|
129
|
+
overlay.className = "alarm-overlay";
|
|
130
|
+
const panel = document.createElement("div");
|
|
131
|
+
panel.className = "alarm-panel";
|
|
132
|
+
panel.innerHTML = `
|
|
133
|
+
<div class="alarm-head">
|
|
134
|
+
<span class="alarm-icon">β°</span>
|
|
135
|
+
<span class="alarm-title">${items.length} reminder${items.length > 1 ? "s" : ""}</span>
|
|
136
|
+
<button type="button" class="alarm-close" aria-label="Close">×</button>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="alarm-list"></div>
|
|
139
|
+
<div class="alarm-foot">
|
|
140
|
+
<label class="alarm-snooze-label">Snooze for
|
|
141
|
+
<select class="alarm-snooze-sel">
|
|
142
|
+
<option value="5">5 minutes</option>
|
|
143
|
+
<option value="15" selected>15 minutes</option>
|
|
144
|
+
<option value="30">30 minutes</option>
|
|
145
|
+
<option value="60">1 hour</option>
|
|
146
|
+
<option value="240">4 hours</option>
|
|
147
|
+
<option value="1440">1 day</option>
|
|
148
|
+
<option value="custom">customβ¦</option>
|
|
149
|
+
</select>
|
|
150
|
+
</label>
|
|
151
|
+
<span style="flex:1"></span>
|
|
152
|
+
<button type="button" class="alarm-btn alarm-btn-snooze">Snooze all</button>
|
|
153
|
+
<button type="button" class="alarm-btn alarm-btn-primary alarm-btn-dismiss">Dismiss all</button>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
const list = panel.querySelector(".alarm-list");
|
|
157
|
+
for (const it of items) {
|
|
158
|
+
const row = document.createElement("div");
|
|
159
|
+
row.className = "alarm-row";
|
|
160
|
+
row.dataset.uuid = it.uuid;
|
|
161
|
+
row.innerHTML = `
|
|
162
|
+
<div class="alarm-row-main">
|
|
163
|
+
<span class="alarm-row-kind">${it.kind === "calendar" ? "π
" : "β"}</span>
|
|
164
|
+
<span class="alarm-row-title"></span>
|
|
165
|
+
<span class="alarm-row-when"></span>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="alarm-row-actions">
|
|
168
|
+
${it.htmlLink ? `<button type="button" class="alarm-row-link" data-link="${it.htmlLink}" title="View in browser">β</button>` : ""}
|
|
169
|
+
<button type="button" class="alarm-row-dismiss" title="Dismiss this one">β</button>
|
|
170
|
+
</div>
|
|
171
|
+
`;
|
|
172
|
+
row.querySelector(".alarm-row-title").textContent = it.title;
|
|
173
|
+
row.querySelector(".alarm-row-when").textContent = formatWhen(it.whenMs);
|
|
174
|
+
list.appendChild(row);
|
|
175
|
+
}
|
|
176
|
+
overlay.appendChild(panel);
|
|
177
|
+
document.body.appendChild(overlay);
|
|
178
|
+
currentPopup = overlay;
|
|
179
|
+
// Pin the mailx window above other apps while a reminder is showing β
|
|
180
|
+
// matches Thunderbird's reminder dialog behavior. Cleared on close so
|
|
181
|
+
// the window doesn't stay always-on-top once the user has acknowledged.
|
|
182
|
+
// No-op in browser fallback (msgapi only present in WebView2).
|
|
183
|
+
const msgapi = window.msgapi;
|
|
184
|
+
try {
|
|
185
|
+
msgapi?.setAlwaysOnTop?.(true);
|
|
186
|
+
}
|
|
187
|
+
catch { /* */ }
|
|
188
|
+
const snoozeMinutes = () => {
|
|
189
|
+
const sel = panel.querySelector(".alarm-snooze-sel");
|
|
190
|
+
if (sel.value === "custom") {
|
|
191
|
+
const v = prompt("Snooze for how many minutes?", "30");
|
|
192
|
+
const n = parseInt(v || "", 10);
|
|
193
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
194
|
+
}
|
|
195
|
+
return parseInt(sel.value, 10);
|
|
196
|
+
};
|
|
197
|
+
const snoozeAll = (minutes) => {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const map = loadSnoozed();
|
|
200
|
+
for (const it of items)
|
|
201
|
+
map[it.uuid] = now + minutes * 60_000;
|
|
202
|
+
saveSnoozed(map);
|
|
203
|
+
for (const it of items)
|
|
204
|
+
firedThisSession.delete(it.uuid);
|
|
205
|
+
close();
|
|
206
|
+
};
|
|
207
|
+
const dismissAll = () => {
|
|
208
|
+
const map = loadDismissed();
|
|
209
|
+
for (const it of items)
|
|
210
|
+
map[it.uuid] = true;
|
|
211
|
+
saveDismissed(map);
|
|
212
|
+
close();
|
|
213
|
+
};
|
|
214
|
+
const close = () => {
|
|
215
|
+
overlay.remove();
|
|
216
|
+
currentPopup = null;
|
|
217
|
+
try {
|
|
218
|
+
msgapi?.setAlwaysOnTop?.(false);
|
|
219
|
+
}
|
|
220
|
+
catch { /* */ }
|
|
221
|
+
};
|
|
222
|
+
panel.querySelector(".alarm-close")?.addEventListener("click", close);
|
|
223
|
+
panel.querySelector(".alarm-btn-snooze")?.addEventListener("click", () => {
|
|
224
|
+
const n = snoozeMinutes();
|
|
225
|
+
if (n)
|
|
226
|
+
snoozeAll(n);
|
|
227
|
+
});
|
|
228
|
+
panel.querySelector(".alarm-btn-dismiss")?.addEventListener("click", dismissAll);
|
|
229
|
+
panel.querySelectorAll(".alarm-row-dismiss").forEach(btn => {
|
|
230
|
+
btn.addEventListener("click", () => {
|
|
231
|
+
const row = btn.closest(".alarm-row");
|
|
232
|
+
const uuid = row?.dataset.uuid;
|
|
233
|
+
if (!uuid)
|
|
234
|
+
return;
|
|
235
|
+
const map = loadDismissed();
|
|
236
|
+
map[uuid] = true;
|
|
237
|
+
saveDismissed(map);
|
|
238
|
+
row.remove();
|
|
239
|
+
if (panel.querySelector(".alarm-list")?.childElementCount === 0)
|
|
240
|
+
close();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
panel.querySelectorAll(".alarm-row-link").forEach(btn => {
|
|
244
|
+
btn.addEventListener("click", () => {
|
|
245
|
+
const url = btn.dataset.link;
|
|
246
|
+
if (!url)
|
|
247
|
+
return;
|
|
248
|
+
const api = window.mailxapi;
|
|
249
|
+
if (api?.openExternal)
|
|
250
|
+
api.openExternal(url);
|
|
251
|
+
else
|
|
252
|
+
window.open(url, "_blank");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
// Escape closes without action (same as X) β user can re-open with next
|
|
256
|
+
// poll tick by not acting on it.
|
|
257
|
+
const onKey = (e) => {
|
|
258
|
+
if (e.key === "Escape") {
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
close();
|
|
261
|
+
document.removeEventListener("keydown", onKey, true);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
document.addEventListener("keydown", onKey, true);
|
|
265
|
+
}
|
|
266
|
+
async function pollAlarms() {
|
|
267
|
+
if (document.hidden)
|
|
268
|
+
return; // don't fire while tab hidden
|
|
269
|
+
if (currentPopup)
|
|
270
|
+
return; // user hasn't acknowledged last one
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
pruneState(now);
|
|
273
|
+
const due = await collectDueAlarms(now);
|
|
274
|
+
// Only show items we haven't already fired this session (prevents the
|
|
275
|
+
// popup from immediately re-appearing if the user closes it without
|
|
276
|
+
// dismissing β the item still matches the "due" filter next tick).
|
|
277
|
+
const fresh = due.filter(d => !firedThisSession.has(d.uuid));
|
|
278
|
+
if (fresh.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
for (const d of fresh)
|
|
281
|
+
firedThisSession.add(d.uuid);
|
|
282
|
+
showPopup(fresh);
|
|
283
|
+
}
|
|
284
|
+
/** Start the alarm poller. Called from app.ts on startup. */
|
|
285
|
+
export function startAlarmPoller() {
|
|
286
|
+
if (window.__mailxAlarmPollerRunning)
|
|
287
|
+
return;
|
|
288
|
+
window.__mailxAlarmPollerRunning = true;
|
|
289
|
+
// First check after 5 s so startup isn't jittered by a modal.
|
|
290
|
+
setTimeout(() => { pollAlarms().catch(() => { }); }, 5000);
|
|
291
|
+
setInterval(() => { pollAlarms().catch(() => { }); }, POLL_INTERVAL_MS);
|
|
292
|
+
// Re-check on visibility change β if the user comes back to the tab
|
|
293
|
+
// after an hour away, they should see anything they missed immediately.
|
|
294
|
+
document.addEventListener("visibilitychange", () => {
|
|
295
|
+
if (!document.hidden)
|
|
296
|
+
pollAlarms().catch(() => { });
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=alarms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alarms.js","sourceRoot":"","sources":["alarms.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alarm subsystem β Thunderbird/Outlook-style reminder popups.
|
|
3
|
+
*
|
|
4
|
+
* Design decisions (2026-04-24):
|
|
5
|
+
* - One shared subsystem for calendar events + tasks (+ future mail reminders).
|
|
6
|
+
* - Popup shows item title, scheduled time, Snooze (5 / 15 / 30 min / 1 hr /
|
|
7
|
+
* custom), Dismiss. Snooze delays the alarm by the chosen interval; Dismiss
|
|
8
|
+
* suppresses it permanently.
|
|
9
|
+
* - Dismissed / snoozed state lives in localStorage (per-device). Google
|
|
10
|
+
* Calendar's own reminders aren't mutated by this β mailx's popup is a
|
|
11
|
+
* local convenience, not a reminder-authority replacement.
|
|
12
|
+
* - Default lead time: 10 min before calendar event start; at task due time
|
|
13
|
+
* for tasks. Per-event overrides from Google Calendar's `reminders.overrides`
|
|
14
|
+
* are a follow-up (we don't currently fetch that field).
|
|
15
|
+
*
|
|
16
|
+
* Check cadence: every 30 s while the tab/window is focused. No check when
|
|
17
|
+
* hidden β alarm won't fire until user returns, which matches Thunderbird's
|
|
18
|
+
* behavior (focus-gated). OS-level notifications are a separate follow-up.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getCalendarEvents, getTasks } from "../lib/api-client.js";
|
|
22
|
+
|
|
23
|
+
const DISMISSED_KEY = "mailx-alarm-dismissed"; // { uuid: true }
|
|
24
|
+
const SNOOZED_KEY = "mailx-alarm-snoozed"; // { uuid: epoch-ms-end }
|
|
25
|
+
const CAL_LEAD_MS = 10 * 60 * 1000; // 10 min before cal event start
|
|
26
|
+
const LOOKBACK_MS = 60 * 60 * 1000; // fire past-due alarms from the last 60 min
|
|
27
|
+
const LOOKAHEAD_MS = 2 * 60 * 60 * 1000; // poll window: upcoming 2 hr
|
|
28
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
29
|
+
|
|
30
|
+
interface AlarmItem {
|
|
31
|
+
uuid: string;
|
|
32
|
+
kind: "calendar" | "task";
|
|
33
|
+
title: string;
|
|
34
|
+
/** When the alarm should fire (epoch ms). */
|
|
35
|
+
alarmMs: number;
|
|
36
|
+
/** When the source event actually starts / is due (epoch ms) β shown in popup. */
|
|
37
|
+
whenMs: number;
|
|
38
|
+
/** Optional web link for "view in browser". */
|
|
39
|
+
htmlLink?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadDismissed(): Record<string, boolean> {
|
|
43
|
+
try { return JSON.parse(localStorage.getItem(DISMISSED_KEY) || "{}"); } catch { return {}; }
|
|
44
|
+
}
|
|
45
|
+
function saveDismissed(map: Record<string, boolean>): void {
|
|
46
|
+
try { localStorage.setItem(DISMISSED_KEY, JSON.stringify(map)); } catch { /* private mode */ }
|
|
47
|
+
}
|
|
48
|
+
function loadSnoozed(): Record<string, number> {
|
|
49
|
+
try { return JSON.parse(localStorage.getItem(SNOOZED_KEY) || "{}"); } catch { return {}; }
|
|
50
|
+
}
|
|
51
|
+
function saveSnoozed(map: Record<string, number>): void {
|
|
52
|
+
try { localStorage.setItem(SNOOZED_KEY, JSON.stringify(map)); } catch { /* */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Prune expired entries so the maps don't grow forever. */
|
|
56
|
+
function pruneState(now: number): void {
|
|
57
|
+
const snoozed = loadSnoozed();
|
|
58
|
+
let changed = false;
|
|
59
|
+
for (const [uuid, until] of Object.entries(snoozed)) {
|
|
60
|
+
// Snoozed entries older than 7 days have been fired already (or the
|
|
61
|
+
// event is long past); drop them.
|
|
62
|
+
if (until < now - 7 * 86400_000) { delete snoozed[uuid]; changed = true; }
|
|
63
|
+
}
|
|
64
|
+
if (changed) saveSnoozed(snoozed);
|
|
65
|
+
// Dismissed is sparse β don't bother pruning aggressively.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function collectDueAlarms(now: number): Promise<AlarmItem[]> {
|
|
69
|
+
const dismissed = loadDismissed();
|
|
70
|
+
const snoozed = loadSnoozed();
|
|
71
|
+
const items: AlarmItem[] = [];
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const events = await getCalendarEvents(now - LOOKBACK_MS, now + LOOKAHEAD_MS);
|
|
75
|
+
for (const ev of events) {
|
|
76
|
+
if (!ev.uuid) continue;
|
|
77
|
+
if (dismissed[ev.uuid]) continue;
|
|
78
|
+
const alarm = (ev.start || 0) - CAL_LEAD_MS;
|
|
79
|
+
const effective = snoozed[ev.uuid] || alarm;
|
|
80
|
+
if (effective <= now && effective > now - LOOKBACK_MS) {
|
|
81
|
+
items.push({
|
|
82
|
+
uuid: ev.uuid, kind: "calendar",
|
|
83
|
+
title: ev.title || "(no title)",
|
|
84
|
+
alarmMs: effective, whenMs: ev.start || 0,
|
|
85
|
+
htmlLink: ev.htmlLink,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch { /* sidebar will surface API errors; alarm path stays quiet */ }
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const tasks = await getTasks(false);
|
|
93
|
+
for (const t of tasks) {
|
|
94
|
+
if (!t.uuid || !t.dueMs) continue;
|
|
95
|
+
if (dismissed[t.uuid]) continue;
|
|
96
|
+
const effective = snoozed[t.uuid] || t.dueMs;
|
|
97
|
+
if (effective <= now && effective > now - LOOKBACK_MS) {
|
|
98
|
+
items.push({
|
|
99
|
+
uuid: t.uuid, kind: "task",
|
|
100
|
+
title: t.title || "(no title)",
|
|
101
|
+
alarmMs: effective, whenMs: t.dueMs,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch { /* */ }
|
|
106
|
+
|
|
107
|
+
return items;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatWhen(ms: number): string {
|
|
111
|
+
if (!ms) return "";
|
|
112
|
+
const d = new Date(ms);
|
|
113
|
+
return d.toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let currentPopup: HTMLElement | null = null;
|
|
117
|
+
let firedThisSession = new Set<string>();
|
|
118
|
+
|
|
119
|
+
function showPopup(items: AlarmItem[]): void {
|
|
120
|
+
if (currentPopup) return; // another popup already up
|
|
121
|
+
if (items.length === 0) return;
|
|
122
|
+
const overlay = document.createElement("div");
|
|
123
|
+
overlay.className = "alarm-overlay";
|
|
124
|
+
const panel = document.createElement("div");
|
|
125
|
+
panel.className = "alarm-panel";
|
|
126
|
+
panel.innerHTML = `
|
|
127
|
+
<div class="alarm-head">
|
|
128
|
+
<span class="alarm-icon">β°</span>
|
|
129
|
+
<span class="alarm-title">${items.length} reminder${items.length > 1 ? "s" : ""}</span>
|
|
130
|
+
<button type="button" class="alarm-close" aria-label="Close">×</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="alarm-list"></div>
|
|
133
|
+
<div class="alarm-foot">
|
|
134
|
+
<label class="alarm-snooze-label">Snooze for
|
|
135
|
+
<select class="alarm-snooze-sel">
|
|
136
|
+
<option value="5">5 minutes</option>
|
|
137
|
+
<option value="15" selected>15 minutes</option>
|
|
138
|
+
<option value="30">30 minutes</option>
|
|
139
|
+
<option value="60">1 hour</option>
|
|
140
|
+
<option value="240">4 hours</option>
|
|
141
|
+
<option value="1440">1 day</option>
|
|
142
|
+
<option value="custom">customβ¦</option>
|
|
143
|
+
</select>
|
|
144
|
+
</label>
|
|
145
|
+
<span style="flex:1"></span>
|
|
146
|
+
<button type="button" class="alarm-btn alarm-btn-snooze">Snooze all</button>
|
|
147
|
+
<button type="button" class="alarm-btn alarm-btn-primary alarm-btn-dismiss">Dismiss all</button>
|
|
148
|
+
</div>
|
|
149
|
+
`;
|
|
150
|
+
const list = panel.querySelector(".alarm-list")!;
|
|
151
|
+
for (const it of items) {
|
|
152
|
+
const row = document.createElement("div");
|
|
153
|
+
row.className = "alarm-row";
|
|
154
|
+
row.dataset.uuid = it.uuid;
|
|
155
|
+
row.innerHTML = `
|
|
156
|
+
<div class="alarm-row-main">
|
|
157
|
+
<span class="alarm-row-kind">${it.kind === "calendar" ? "π
" : "β"}</span>
|
|
158
|
+
<span class="alarm-row-title"></span>
|
|
159
|
+
<span class="alarm-row-when"></span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="alarm-row-actions">
|
|
162
|
+
${it.htmlLink ? `<button type="button" class="alarm-row-link" data-link="${it.htmlLink}" title="View in browser">β</button>` : ""}
|
|
163
|
+
<button type="button" class="alarm-row-dismiss" title="Dismiss this one">β</button>
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
(row.querySelector(".alarm-row-title") as HTMLElement).textContent = it.title;
|
|
167
|
+
(row.querySelector(".alarm-row-when") as HTMLElement).textContent = formatWhen(it.whenMs);
|
|
168
|
+
list.appendChild(row);
|
|
169
|
+
}
|
|
170
|
+
overlay.appendChild(panel);
|
|
171
|
+
document.body.appendChild(overlay);
|
|
172
|
+
currentPopup = overlay;
|
|
173
|
+
|
|
174
|
+
// Pin the mailx window above other apps while a reminder is showing β
|
|
175
|
+
// matches Thunderbird's reminder dialog behavior. Cleared on close so
|
|
176
|
+
// the window doesn't stay always-on-top once the user has acknowledged.
|
|
177
|
+
// No-op in browser fallback (msgapi only present in WebView2).
|
|
178
|
+
const msgapi: any = (window as any).msgapi;
|
|
179
|
+
try { msgapi?.setAlwaysOnTop?.(true); } catch { /* */ }
|
|
180
|
+
|
|
181
|
+
const snoozeMinutes = (): number | null => {
|
|
182
|
+
const sel = panel.querySelector<HTMLSelectElement>(".alarm-snooze-sel")!;
|
|
183
|
+
if (sel.value === "custom") {
|
|
184
|
+
const v = prompt("Snooze for how many minutes?", "30");
|
|
185
|
+
const n = parseInt(v || "", 10);
|
|
186
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
187
|
+
}
|
|
188
|
+
return parseInt(sel.value, 10);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const snoozeAll = (minutes: number): void => {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const map = loadSnoozed();
|
|
194
|
+
for (const it of items) map[it.uuid] = now + minutes * 60_000;
|
|
195
|
+
saveSnoozed(map);
|
|
196
|
+
for (const it of items) firedThisSession.delete(it.uuid);
|
|
197
|
+
close();
|
|
198
|
+
};
|
|
199
|
+
const dismissAll = (): void => {
|
|
200
|
+
const map = loadDismissed();
|
|
201
|
+
for (const it of items) map[it.uuid] = true;
|
|
202
|
+
saveDismissed(map);
|
|
203
|
+
close();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const close = () => {
|
|
207
|
+
overlay.remove();
|
|
208
|
+
currentPopup = null;
|
|
209
|
+
try { msgapi?.setAlwaysOnTop?.(false); } catch { /* */ }
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
panel.querySelector(".alarm-close")?.addEventListener("click", close);
|
|
213
|
+
panel.querySelector(".alarm-btn-snooze")?.addEventListener("click", () => {
|
|
214
|
+
const n = snoozeMinutes();
|
|
215
|
+
if (n) snoozeAll(n);
|
|
216
|
+
});
|
|
217
|
+
panel.querySelector(".alarm-btn-dismiss")?.addEventListener("click", dismissAll);
|
|
218
|
+
|
|
219
|
+
panel.querySelectorAll<HTMLButtonElement>(".alarm-row-dismiss").forEach(btn => {
|
|
220
|
+
btn.addEventListener("click", () => {
|
|
221
|
+
const row = btn.closest<HTMLElement>(".alarm-row");
|
|
222
|
+
const uuid = row?.dataset.uuid;
|
|
223
|
+
if (!uuid) return;
|
|
224
|
+
const map = loadDismissed();
|
|
225
|
+
map[uuid] = true;
|
|
226
|
+
saveDismissed(map);
|
|
227
|
+
row!.remove();
|
|
228
|
+
if (panel.querySelector(".alarm-list")?.childElementCount === 0) close();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
panel.querySelectorAll<HTMLButtonElement>(".alarm-row-link").forEach(btn => {
|
|
232
|
+
btn.addEventListener("click", () => {
|
|
233
|
+
const url = btn.dataset.link;
|
|
234
|
+
if (!url) return;
|
|
235
|
+
const api = (window as any).mailxapi;
|
|
236
|
+
if (api?.openExternal) api.openExternal(url);
|
|
237
|
+
else window.open(url, "_blank");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Escape closes without action (same as X) β user can re-open with next
|
|
242
|
+
// poll tick by not acting on it.
|
|
243
|
+
const onKey = (e: KeyboardEvent) => {
|
|
244
|
+
if (e.key === "Escape") { e.stopPropagation(); close(); document.removeEventListener("keydown", onKey, true); }
|
|
245
|
+
};
|
|
246
|
+
document.addEventListener("keydown", onKey, true);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function pollAlarms(): Promise<void> {
|
|
250
|
+
if (document.hidden) return; // don't fire while tab hidden
|
|
251
|
+
if (currentPopup) return; // user hasn't acknowledged last one
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
pruneState(now);
|
|
254
|
+
const due = await collectDueAlarms(now);
|
|
255
|
+
// Only show items we haven't already fired this session (prevents the
|
|
256
|
+
// popup from immediately re-appearing if the user closes it without
|
|
257
|
+
// dismissing β the item still matches the "due" filter next tick).
|
|
258
|
+
const fresh = due.filter(d => !firedThisSession.has(d.uuid));
|
|
259
|
+
if (fresh.length === 0) return;
|
|
260
|
+
for (const d of fresh) firedThisSession.add(d.uuid);
|
|
261
|
+
showPopup(fresh);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Start the alarm poller. Called from app.ts on startup. */
|
|
265
|
+
export function startAlarmPoller(): void {
|
|
266
|
+
if ((window as any).__mailxAlarmPollerRunning) return;
|
|
267
|
+
(window as any).__mailxAlarmPollerRunning = true;
|
|
268
|
+
// First check after 5 s so startup isn't jittered by a modal.
|
|
269
|
+
setTimeout(() => { pollAlarms().catch(() => { /* */ }); }, 5000);
|
|
270
|
+
setInterval(() => { pollAlarms().catch(() => { /* */ }); }, POLL_INTERVAL_MS);
|
|
271
|
+
// Re-check on visibility change β if the user comes back to the tab
|
|
272
|
+
// after an hour away, they should see anything they missed immediately.
|
|
273
|
+
document.addEventListener("visibilitychange", () => {
|
|
274
|
+
if (!document.hidden) pollAlarms().catch(() => { /* */ });
|
|
275
|
+
});
|
|
276
|
+
}
|