@bobfrankston/mailx 1.0.395 → 1.0.405
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/client/android.html +12 -1
- package/client/app.js +44 -4
- package/client/components/alarms.js +286 -0
- package/client/components/calendar-sidebar.js +43 -7
- package/client/components/message-list.js +215 -16
- package/client/components/message-viewer.js +120 -18
- package/client/compose/compose.js +137 -41
- package/client/index.html +12 -1
- package/client/lib/api-client.js +3 -0
- package/client/lib/mailxapi.js +1 -0
- package/client/styles/components.css +251 -6
- package/package.json +1 -1
- package/packages/mailx-imap/index.js +18 -2
- package/packages/mailx-server/index.js +29 -0
- package/packages/mailx-service/index.d.ts +4 -0
- package/packages/mailx-service/index.js +6 -0
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-store/db.d.ts +8 -0
- package/packages/mailx-store/db.js +34 -1
- package/packages/mailx-store-web/android-bootstrap.js +193 -93
- package/packages/mailx-store-web/db.d.ts +4 -0
- package/packages/mailx-store-web/db.js +25 -0
- package/packages/mailx-store-web/sync-manager.d.ts +7 -0
- package/packages/mailx-store-web/sync-manager.js +55 -0
- package/packages/mailx-store-web/web-service.d.ts +4 -0
- package/packages/mailx-store-web/web-service.js +7 -0
- package/tdview.cmd +1 -0
- package/unwedge.cmd +1 -0
package/client/android.html
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
<label class="tb-menu-item"><input type="radio" name="opt-editor" value="quill" id="opt-editor-quill" checked> Quill</label>
|
|
84
84
|
<label class="tb-menu-item"><input type="radio" name="opt-editor" value="tiptap" id="opt-editor-tiptap"> tiptap</label>
|
|
85
85
|
<hr class="tb-menu-sep">
|
|
86
|
-
<label class="tb-menu-item"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
|
|
86
|
+
<label class="tb-menu-item" title="Ghost-text completions while composing — Ollama / Claude / OpenAI back-end, off by default"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
|
|
87
87
|
</div>
|
|
88
88
|
</div>
|
|
89
89
|
<span id="app-version" class="app-version">mailx</span>
|
|
@@ -145,11 +145,22 @@
|
|
|
145
145
|
<input type="search" id="search-input" placeholder="Search..." autocomplete="off" title="Search messages">
|
|
146
146
|
</search>
|
|
147
147
|
<div class="ml-header">
|
|
148
|
+
<span class="ml-col ml-col-avatar"></span>
|
|
148
149
|
<span class="ml-col ml-col-flag"></span>
|
|
149
150
|
<span class="ml-col ml-col-from" data-sort="from">From</span>
|
|
150
151
|
<span class="ml-col ml-col-date" data-sort="date">Date</span>
|
|
151
152
|
<span class="ml-col ml-col-subject">Subject</span>
|
|
152
153
|
</div>
|
|
154
|
+
<div class="ml-bulkbar" id="ml-bulkbar" hidden>
|
|
155
|
+
<button type="button" class="ml-bulk-cancel" id="ml-bulk-cancel" title="Exit multi-select">✕</button>
|
|
156
|
+
<span class="ml-bulk-count" id="ml-bulk-count">0 selected</span>
|
|
157
|
+
<span style="flex:1"></span>
|
|
158
|
+
<button type="button" class="ml-bulk-btn" data-bulk="markread" title="Mark read">◉</button>
|
|
159
|
+
<button type="button" class="ml-bulk-btn" data-bulk="flag" title="Flag">⚑</button>
|
|
160
|
+
<button type="button" class="ml-bulk-btn" data-bulk="move" title="Move to folder…">➜</button>
|
|
161
|
+
<button type="button" class="ml-bulk-btn" data-bulk="spam" title="Mark as spam">⚠</button>
|
|
162
|
+
<button type="button" class="ml-bulk-btn ml-bulk-danger" data-bulk="delete" title="Delete">🗑</button>
|
|
163
|
+
</div>
|
|
153
164
|
<div class="ml-body" id="ml-body">
|
|
154
165
|
<div class="ml-empty">Select a folder to view messages</div>
|
|
155
166
|
</div>
|
package/client/app.js
CHANGED
|
@@ -1056,14 +1056,27 @@ document.getElementById("rail-contacts")?.addEventListener("click", async () =>
|
|
|
1056
1056
|
openAddressBook();
|
|
1057
1057
|
setRailActive("rail-contacts");
|
|
1058
1058
|
});
|
|
1059
|
+
// Q114 decided 2026-04-24: full-screen calendar/tasks modals are
|
|
1060
|
+
// temporarily retired — the right-docked sidebar (calendar-sidebar.ts)
|
|
1061
|
+
// owns both views. Rail buttons now just reveal the sidebar. Files kept
|
|
1062
|
+
// (`calendar.ts`, `tasks.ts`) for potential revival; not imported.
|
|
1059
1063
|
document.getElementById("rail-calendar")?.addEventListener("click", async () => {
|
|
1060
|
-
const {
|
|
1061
|
-
|
|
1064
|
+
const { showCalendarSidebar } = await import("./components/calendar-sidebar.js");
|
|
1065
|
+
await showCalendarSidebar();
|
|
1066
|
+
// Flip the View-menu checkbox so the on-state stays coherent across paths.
|
|
1067
|
+
const optSidebar = document.getElementById("opt-calendar-sidebar");
|
|
1068
|
+
if (optSidebar)
|
|
1069
|
+
optSidebar.checked = true;
|
|
1062
1070
|
setRailActive("rail-calendar");
|
|
1063
1071
|
});
|
|
1064
1072
|
document.getElementById("rail-tasks")?.addEventListener("click", async () => {
|
|
1065
|
-
const {
|
|
1066
|
-
|
|
1073
|
+
const { showCalendarSidebar } = await import("./components/calendar-sidebar.js");
|
|
1074
|
+
await showCalendarSidebar();
|
|
1075
|
+
// Scroll the sidebar to the tasks section if possible.
|
|
1076
|
+
document.getElementById("cal-side-tasks")?.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
1077
|
+
const optSidebar = document.getElementById("opt-calendar-sidebar");
|
|
1078
|
+
if (optSidebar)
|
|
1079
|
+
optSidebar.checked = true;
|
|
1067
1080
|
setRailActive("rail-tasks");
|
|
1068
1081
|
});
|
|
1069
1082
|
document.getElementById("rail-settings")?.addEventListener("click", () => {
|
|
@@ -1253,6 +1266,21 @@ window.addEventListener("message", (e) => {
|
|
|
1253
1266
|
// with no failures), so we do the IPC from here and post the result back
|
|
1254
1267
|
// to the iframe via its source. `e.source` is the iframe's window; use it
|
|
1255
1268
|
// so targeting works even if the iframe moves in the DOM.
|
|
1269
|
+
// S61 2026-04-24: parent-relay compose close. On Android the
|
|
1270
|
+
// window.close() override applied in `frame.onload` doesn't always fire
|
|
1271
|
+
// (WebView2 / MAUI WebView dispatches close to the shell in some cases),
|
|
1272
|
+
// leaving the compose overlay stuck after Send. postMessage is reliable;
|
|
1273
|
+
// compose.ts's closeCompose() posts this, and we find-and-remove the
|
|
1274
|
+
// overlay whose iframe window matches e.source.
|
|
1275
|
+
if (e.data?.type === "mailx-compose-close") {
|
|
1276
|
+
const src = e.source;
|
|
1277
|
+
document.querySelectorAll(".compose-overlay").forEach(el => {
|
|
1278
|
+
const iframe = el.querySelector("iframe");
|
|
1279
|
+
if (!src || iframe?.contentWindow === src)
|
|
1280
|
+
el.remove();
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1256
1284
|
if (e.data?.type === "mailx-compose-send" && e.data.id && e.data.body) {
|
|
1257
1285
|
const src = e.source;
|
|
1258
1286
|
const id = e.data.id;
|
|
@@ -2043,6 +2071,18 @@ function applyThreadFilter() {
|
|
|
2043
2071
|
hideCalendarSidebar();
|
|
2044
2072
|
});
|
|
2045
2073
|
})();
|
|
2074
|
+
// P17 / Q104: alarm subsystem — Thunderbird/Outlook-style popup with
|
|
2075
|
+
// snooze + dismiss. Covers calendar events + tasks today; mail reminders
|
|
2076
|
+
// will slot in here when the mail-reminder feature lands.
|
|
2077
|
+
(async () => {
|
|
2078
|
+
try {
|
|
2079
|
+
const { startAlarmPoller } = await import("./components/alarms.js");
|
|
2080
|
+
startAlarmPoller();
|
|
2081
|
+
}
|
|
2082
|
+
catch (e) {
|
|
2083
|
+
console.error("alarm poller init failed:", e?.message || e);
|
|
2084
|
+
}
|
|
2085
|
+
})();
|
|
2046
2086
|
// Two-line toggle
|
|
2047
2087
|
optTwoLine?.addEventListener("change", () => {
|
|
2048
2088
|
const list = document.getElementById("message-list");
|
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
const snoozeMinutes = () => {
|
|
180
|
+
const sel = panel.querySelector(".alarm-snooze-sel");
|
|
181
|
+
if (sel.value === "custom") {
|
|
182
|
+
const v = prompt("Snooze for how many minutes?", "30");
|
|
183
|
+
const n = parseInt(v || "", 10);
|
|
184
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
185
|
+
}
|
|
186
|
+
return parseInt(sel.value, 10);
|
|
187
|
+
};
|
|
188
|
+
const snoozeAll = (minutes) => {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const map = loadSnoozed();
|
|
191
|
+
for (const it of items)
|
|
192
|
+
map[it.uuid] = now + minutes * 60_000;
|
|
193
|
+
saveSnoozed(map);
|
|
194
|
+
for (const it of items)
|
|
195
|
+
firedThisSession.delete(it.uuid);
|
|
196
|
+
close();
|
|
197
|
+
};
|
|
198
|
+
const dismissAll = () => {
|
|
199
|
+
const map = loadDismissed();
|
|
200
|
+
for (const it of items)
|
|
201
|
+
map[it.uuid] = true;
|
|
202
|
+
saveDismissed(map);
|
|
203
|
+
close();
|
|
204
|
+
};
|
|
205
|
+
const close = () => {
|
|
206
|
+
overlay.remove();
|
|
207
|
+
currentPopup = null;
|
|
208
|
+
};
|
|
209
|
+
panel.querySelector(".alarm-close")?.addEventListener("click", close);
|
|
210
|
+
panel.querySelector(".alarm-btn-snooze")?.addEventListener("click", () => {
|
|
211
|
+
const n = snoozeMinutes();
|
|
212
|
+
if (n)
|
|
213
|
+
snoozeAll(n);
|
|
214
|
+
});
|
|
215
|
+
panel.querySelector(".alarm-btn-dismiss")?.addEventListener("click", dismissAll);
|
|
216
|
+
panel.querySelectorAll(".alarm-row-dismiss").forEach(btn => {
|
|
217
|
+
btn.addEventListener("click", () => {
|
|
218
|
+
const row = btn.closest(".alarm-row");
|
|
219
|
+
const uuid = row?.dataset.uuid;
|
|
220
|
+
if (!uuid)
|
|
221
|
+
return;
|
|
222
|
+
const map = loadDismissed();
|
|
223
|
+
map[uuid] = true;
|
|
224
|
+
saveDismissed(map);
|
|
225
|
+
row.remove();
|
|
226
|
+
if (panel.querySelector(".alarm-list")?.childElementCount === 0)
|
|
227
|
+
close();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
panel.querySelectorAll(".alarm-row-link").forEach(btn => {
|
|
231
|
+
btn.addEventListener("click", () => {
|
|
232
|
+
const url = btn.dataset.link;
|
|
233
|
+
if (!url)
|
|
234
|
+
return;
|
|
235
|
+
const api = window.mailxapi;
|
|
236
|
+
if (api?.openExternal)
|
|
237
|
+
api.openExternal(url);
|
|
238
|
+
else
|
|
239
|
+
window.open(url, "_blank");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
// Escape closes without action (same as X) — user can re-open with next
|
|
243
|
+
// poll tick by not acting on it.
|
|
244
|
+
const onKey = (e) => {
|
|
245
|
+
if (e.key === "Escape") {
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
close();
|
|
248
|
+
document.removeEventListener("keydown", onKey, true);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
document.addEventListener("keydown", onKey, true);
|
|
252
|
+
}
|
|
253
|
+
async function pollAlarms() {
|
|
254
|
+
if (document.hidden)
|
|
255
|
+
return; // don't fire while tab hidden
|
|
256
|
+
if (currentPopup)
|
|
257
|
+
return; // user hasn't acknowledged last one
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
pruneState(now);
|
|
260
|
+
const due = await collectDueAlarms(now);
|
|
261
|
+
// Only show items we haven't already fired this session (prevents the
|
|
262
|
+
// popup from immediately re-appearing if the user closes it without
|
|
263
|
+
// dismissing — the item still matches the "due" filter next tick).
|
|
264
|
+
const fresh = due.filter(d => !firedThisSession.has(d.uuid));
|
|
265
|
+
if (fresh.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
for (const d of fresh)
|
|
268
|
+
firedThisSession.add(d.uuid);
|
|
269
|
+
showPopup(fresh);
|
|
270
|
+
}
|
|
271
|
+
/** Start the alarm poller. Called from app.ts on startup. */
|
|
272
|
+
export function startAlarmPoller() {
|
|
273
|
+
if (window.__mailxAlarmPollerRunning)
|
|
274
|
+
return;
|
|
275
|
+
window.__mailxAlarmPollerRunning = true;
|
|
276
|
+
// First check after 5 s so startup isn't jittered by a modal.
|
|
277
|
+
setTimeout(() => { pollAlarms().catch(() => { }); }, 5000);
|
|
278
|
+
setInterval(() => { pollAlarms().catch(() => { }); }, POLL_INTERVAL_MS);
|
|
279
|
+
// Re-check on visibility change — if the user comes back to the tab
|
|
280
|
+
// after an hour away, they should see anything they missed immediately.
|
|
281
|
+
document.addEventListener("visibilitychange", () => {
|
|
282
|
+
if (!document.hidden)
|
|
283
|
+
pollAlarms().catch(() => { });
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=alarms.js.map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* and tasks tables); this file does not use localStorage for data.
|
|
15
15
|
*/
|
|
16
16
|
import { getCalendarEvents, createCalendarEvent, getTasks, createTask, updateTask, deleteTask, reauthGoogleScopes, } from "../lib/api-client.js";
|
|
17
|
+
import { showContextMenu } from "./context-menu.js";
|
|
17
18
|
const SIDEBAR_PREF = "mailx-calendar-sidebar-on";
|
|
18
19
|
const SHOW_RECURRING_PREF = "mailx-cal-show-recurring";
|
|
19
20
|
const SHOW_DONE_PREF = "mailx-task-show-done";
|
|
@@ -118,16 +119,35 @@ function renderEvents(events) {
|
|
|
118
119
|
body.innerHTML = html;
|
|
119
120
|
// Click-to-open — interim per user 2026-04-23: route to Google Calendar's
|
|
120
121
|
// web UI via openExternal until we build an in-app event editor.
|
|
122
|
+
// Right-click gives a context menu with "View in browser" explicit.
|
|
123
|
+
const openInBrowser = (url) => {
|
|
124
|
+
const api = window.mailxapi;
|
|
125
|
+
if (api?.openExternal)
|
|
126
|
+
api.openExternal(url);
|
|
127
|
+
else
|
|
128
|
+
window.open(url, "_blank");
|
|
129
|
+
};
|
|
121
130
|
body.querySelectorAll(".cal-side-event").forEach(el => {
|
|
122
131
|
el.addEventListener("click", () => {
|
|
123
132
|
const link = el.dataset.link;
|
|
124
|
-
if (link)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
if (link)
|
|
134
|
+
openInBrowser(link);
|
|
135
|
+
});
|
|
136
|
+
el.addEventListener("contextmenu", (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
const link = el.dataset.link;
|
|
139
|
+
const items = [];
|
|
140
|
+
if (link)
|
|
141
|
+
items.push({
|
|
142
|
+
label: "View in browser",
|
|
143
|
+
action: () => openInBrowser(link),
|
|
144
|
+
});
|
|
145
|
+
items.push({
|
|
146
|
+
label: "Open Google Calendar",
|
|
147
|
+
action: () => openInBrowser("https://calendar.google.com/"),
|
|
148
|
+
});
|
|
149
|
+
if (items.length > 0)
|
|
150
|
+
showContextMenu(e.clientX, e.clientY, items);
|
|
131
151
|
});
|
|
132
152
|
});
|
|
133
153
|
}
|
|
@@ -152,6 +172,13 @@ async function renderTasks() {
|
|
|
152
172
|
</div>`;
|
|
153
173
|
}
|
|
154
174
|
host.innerHTML = html;
|
|
175
|
+
const openInBrowser = (url) => {
|
|
176
|
+
const api = window.mailxapi;
|
|
177
|
+
if (api?.openExternal)
|
|
178
|
+
api.openExternal(url);
|
|
179
|
+
else
|
|
180
|
+
window.open(url, "_blank");
|
|
181
|
+
};
|
|
155
182
|
host.querySelectorAll(".cal-side-task").forEach(row => {
|
|
156
183
|
const uuid = row.dataset.uuid;
|
|
157
184
|
row.querySelector(".cal-side-task-check")?.addEventListener("change", async (e) => {
|
|
@@ -163,6 +190,15 @@ async function renderTasks() {
|
|
|
163
190
|
await deleteTask(uuid);
|
|
164
191
|
renderTasks();
|
|
165
192
|
});
|
|
193
|
+
row.addEventListener("contextmenu", (e) => {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
// Google Tasks doesn't have a per-task web URL; open the list view.
|
|
196
|
+
showContextMenu(e.clientX, e.clientY, [
|
|
197
|
+
{ label: "View in Google Tasks", action: () => openInBrowser("https://tasks.google.com/") },
|
|
198
|
+
{ label: "", action: () => { }, separator: true },
|
|
199
|
+
{ label: "Delete task", action: async () => { await deleteTask(uuid); renderTasks(); } },
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
166
202
|
});
|
|
167
203
|
}
|
|
168
204
|
async function refresh() {
|