@bobfrankston/rmfmail 1.2.50 → 1.2.65
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-bootstrap.bundle.js +63 -2
- package/client/android-bootstrap.bundle.js.map +4 -4
- package/client/app.bundle.js +417 -63
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +182 -24
- package/client/app.js.map +1 -1
- package/client/app.ts +166 -21
- package/client/components/calendar-sidebar.js +5 -5
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +5 -5
- package/client/components/folder-tree.js +81 -0
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +70 -0
- package/client/components/message-list.js +282 -38
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +264 -36
- package/client/compose/compose.bundle.js +70 -14
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.css +28 -7
- package/client/compose/compose.js +64 -4
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +66 -4
- package/client/compose/editor.js +23 -7
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +23 -7
- package/client/help/search-help.js +4 -3
- package/client/help/search-help.js.map +1 -1
- package/client/help/search-help.ts +4 -3
- package/client/index.html +5 -1
- package/client/lib/api-client.js +4 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -2
- package/client/lib/mailxapi.js +2 -2
- package/client/lib/rmf-tiny.js +39 -6
- package/client/styles/components.css +6 -0
- package/docs/azure.md +58 -0
- package/docs/config-help.md +131 -0
- package/docs/edit-in-word-docx.md +98 -0
- package/docs/host-abstraction-plan.md +169 -0
- package/docs/local-first-plan.md +303 -0
- package/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/docs/outlook.md +80 -18
- package/docs/outlook.txt +35 -0
- package/docs/rules-design.md +172 -0
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts +48 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +320 -68
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +311 -65
- package/packages/mailx-imap/microsoft-credentials.json +8 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-imap/providers/outlook-api.d.ts +7 -0
- package/packages/mailx-imap/providers/outlook-api.d.ts.map +1 -0
- package/packages/mailx-imap/providers/outlook-api.js +7 -0
- package/packages/mailx-imap/providers/outlook-api.js.map +1 -0
- package/packages/mailx-service/index.d.ts +7 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +8 -8
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +8 -7
- package/packages/mailx-service/jsonrpc.js +1 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +1 -1
- package/packages/mailx-settings/docs/azure.md +58 -0
- package/packages/mailx-settings/docs/config-help.md +131 -0
- package/packages/mailx-settings/docs/edit-in-word-docx.md +98 -0
- package/packages/mailx-settings/docs/host-abstraction-plan.md +169 -0
- package/packages/mailx-settings/docs/local-first-plan.md +303 -0
- package/packages/mailx-settings/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/packages/mailx-settings/docs/outlook.md +80 -18
- package/packages/mailx-settings/docs/rules-design.md +172 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +9 -8
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +9 -8
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +36 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +100 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +100 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-types/contact-rules.d.ts +1 -1
- package/packages/mailx-types/contact-rules.js +1 -1
- package/packages/mailx-types/contact-rules.js.map +1 -1
- package/packages/mailx-types/contact-rules.jsonc +1 -1
- package/packages/mailx-types/contact-rules.ts +1 -1
- package/packages/mailx-types/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -3,17 +3,41 @@
|
|
|
3
3
|
* Wires together all UI components and WebSocket connection.
|
|
4
4
|
*/
|
|
5
5
|
import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced, setOutboxTotal } from "./components/folder-tree.js";
|
|
6
|
-
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex } from "./components/message-list.js";
|
|
6
|
+
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex, showSelectionMenu, revealMessage } from "./components/message-list.js";
|
|
7
7
|
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
8
8
|
import { initTabs, setActiveView as setActiveTabView, openTab } from "./components/tabs.js";
|
|
9
9
|
import { getCurrentMessage, initViewer, popOutCurrentMessage, printCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu, wrapHtmlBody } from "./components/message-viewer.js";
|
|
10
|
-
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessage, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage, subscribeStore, cancelServerSearch, installConsoleCapture } from "./lib/api-client.js";
|
|
10
|
+
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessage, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage, subscribeStore, cancelServerSearch, installConsoleCapture, getAttachment } from "./lib/api-client.js";
|
|
11
11
|
import * as messageState from "./lib/message-state.js";
|
|
12
12
|
// FIRST THING. Captures every console.log/warn/error + window errors +
|
|
13
13
|
// unhandled promise rejections to the daemon log via logClientEvent, so
|
|
14
14
|
// debugging client-side bugs never requires opening devtools — search
|
|
15
15
|
// rmfmail-YYYY-MM-DD.log for "console.error" or "window.error" instead.
|
|
16
16
|
installConsoleCapture();
|
|
17
|
+
// ── Main-thread stall watchdog ──
|
|
18
|
+
// Bob 2026-06-22: "trying to scroll does nothing, as if the UI thread is
|
|
19
|
+
// locked"; the UI must always be responsive. A blocked event loop is
|
|
20
|
+
// otherwise invisible — the symptom shows up in the UI but never names its
|
|
21
|
+
// cause. This watchdog re-arms a 1s timer and reports the slip whenever the
|
|
22
|
+
// callback fires late by more than a threshold: a 1s timer that fires at
|
|
23
|
+
// 1.0s is healthy; one that fires at 4.2s means the main thread was wedged
|
|
24
|
+
// ~3.2s (long IPC on the loop, giant synchronous render, parse on main).
|
|
25
|
+
// The slip duration + wall-clock pinpoints which interaction blew out.
|
|
26
|
+
(function installStallWatchdog() {
|
|
27
|
+
const EXPECTED_MS = 1000;
|
|
28
|
+
const STALL_MS = 250; // only report slips that a user would feel
|
|
29
|
+
let last = performance.now();
|
|
30
|
+
const tick = () => {
|
|
31
|
+
const now = performance.now();
|
|
32
|
+
const slip = now - last - EXPECTED_MS;
|
|
33
|
+
if (slip > STALL_MS) {
|
|
34
|
+
console.warn(` [stall] main thread blocked ~${slip.toFixed(0)}ms (event loop frozen — UI was unresponsive this long)`);
|
|
35
|
+
}
|
|
36
|
+
last = now;
|
|
37
|
+
setTimeout(tick, EXPECTED_MS);
|
|
38
|
+
};
|
|
39
|
+
setTimeout(tick, EXPECTED_MS);
|
|
40
|
+
})();
|
|
17
41
|
// ── New message badge (favicon + title) ──
|
|
18
42
|
/** The user-visible app name. Single point of change for the rename;
|
|
19
43
|
* every UI surface that shows "rmfmail" reads from here. Static HTML
|
|
@@ -881,6 +905,17 @@ document.getElementById("btn-factory-reset")?.addEventListener("click", async ()
|
|
|
881
905
|
});
|
|
882
906
|
async function openCompose(mode, overrideMsg, overrideAccountId) {
|
|
883
907
|
logClientEvent("openCompose-entry", { mode });
|
|
908
|
+
// Opening a compose (Ctrl+N, the Compose button, the rail) must dismiss any
|
|
909
|
+
// open toolbar menu. Click-outside already closes them via the overlay
|
|
910
|
+
// shield, but a keyboard Ctrl+N over an open Settings/View menu left it
|
|
911
|
+
// stuck open behind the new compose (Bob 2026-06-21). Close them here so
|
|
912
|
+
// every compose-open path behaves the same.
|
|
913
|
+
for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
|
|
914
|
+
const dd = document.getElementById(id);
|
|
915
|
+
if (dd && !dd.hidden)
|
|
916
|
+
hideDropdownHard(dd);
|
|
917
|
+
}
|
|
918
|
+
refreshToolbarOverlayShield();
|
|
884
919
|
// `overrideMsg` lets a detached surface (the message pop-out) compose a
|
|
885
920
|
// reply/forward for ITS message rather than whatever the main viewer has
|
|
886
921
|
// selected. Same `{ message, accountId }` shape getCurrentMessage returns.
|
|
@@ -920,7 +955,16 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
|
|
|
920
955
|
// Now finish initialisation off the critical path — editor bootstrap
|
|
921
956
|
// inside the iframe runs concurrently with this await.
|
|
922
957
|
const accounts = await accountsP;
|
|
923
|
-
|
|
958
|
+
// For a brand-new compose, default the From to the account whose folder
|
|
959
|
+
// you're currently viewing — pressing Ctrl+N while reading AOL should
|
|
960
|
+
// compose AS aol, not the first/defaultSend account. In the unified "All
|
|
961
|
+
// Inboxes" view there's no single account (currentAccountId === ""), so
|
|
962
|
+
// fall back to the selected message's account, then the first account.
|
|
963
|
+
// Reply/forward keep using the message's own account (you answer from the
|
|
964
|
+
// address that received it; detectReplyFrom refines further).
|
|
965
|
+
const accountId = mode === "new"
|
|
966
|
+
? (currentAccountId || current?.accountId || accounts[0]?.id || "")
|
|
967
|
+
: (current?.accountId || currentAccountId || accounts[0]?.id || "");
|
|
924
968
|
const rePrefix = /^(re|fwd?):\s*/i;
|
|
925
969
|
const cleanSubject = msg ? msg.subject.replace(rePrefix, "") : "";
|
|
926
970
|
const init = {
|
|
@@ -1077,6 +1121,32 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
|
|
|
1077
1121
|
init.subject = msg.subject || "";
|
|
1078
1122
|
init.bodyHtml = editAsNewBody(msg);
|
|
1079
1123
|
}
|
|
1124
|
+
// Carry the original message's attachments into forward / edit-as-new so
|
|
1125
|
+
// reopening a sent message to edit (or forwarding one) keeps its files —
|
|
1126
|
+
// they were being silently dropped (Bob 2026-06-21). Fetch each via
|
|
1127
|
+
// getAttachment (the same path the viewer's download uses) and hand compose
|
|
1128
|
+
// the base64 it expects. Reply/replyAll intentionally omit attachments.
|
|
1129
|
+
if (msg && (mode === "editAsNew" || mode === "forward") && Array.isArray(msg.attachments) && msg.attachments.length) {
|
|
1130
|
+
const msgAccountId = current?.accountId || accountId;
|
|
1131
|
+
const loaded = [];
|
|
1132
|
+
for (const att of msg.attachments) {
|
|
1133
|
+
try {
|
|
1134
|
+
const data = await getAttachment(msgAccountId, msg.uid, att.id, msg.folderId);
|
|
1135
|
+
if (data?.content) {
|
|
1136
|
+
loaded.push({
|
|
1137
|
+
filename: data.filename || att.filename || "attachment",
|
|
1138
|
+
mimeType: data.contentType || att.contentType || "application/octet-stream",
|
|
1139
|
+
dataBase64: data.content,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
catch (e) {
|
|
1144
|
+
console.error(`[compose] ${mode}: failed to load attachment ${att.id}: ${e?.message || e}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (loaded.length)
|
|
1148
|
+
init.attachments = loaded;
|
|
1149
|
+
}
|
|
1080
1150
|
// Store init data for compose window to pick up. Two parallel paths
|
|
1081
1151
|
// because sessionStorage propagation to a freshly-loading iframe has
|
|
1082
1152
|
// intermittently failed under WebView2's custom-protocol host (Bob
|
|
@@ -1632,6 +1702,14 @@ async function performUndo() {
|
|
|
1632
1702
|
document.addEventListener("mailx-moved", (e) => {
|
|
1633
1703
|
pushUndo({ kind: "move", at: Date.now(), payload: e.detail });
|
|
1634
1704
|
});
|
|
1705
|
+
// Bulk flag/unflag (Filtered menu, row context menu) emits "mailx-flagged-batch"
|
|
1706
|
+
// with the per-row prior state so Ctrl+Z restores the whole batch at once.
|
|
1707
|
+
document.addEventListener("mailx-flagged-batch", (e) => {
|
|
1708
|
+
const payload = e.detail;
|
|
1709
|
+
if (Array.isArray(payload) && payload.length) {
|
|
1710
|
+
pushUndo({ kind: "flag", at: Date.now(), payload });
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1635
1713
|
// ── Right-button drag → Move / Copy menu ───────────────────────────────
|
|
1636
1714
|
// Browser drag-and-drop is left-button only, so a RIGHT-button drag is a
|
|
1637
1715
|
// custom mouse-tracked gesture: right-press a message row, drag onto a folder
|
|
@@ -1980,6 +2058,14 @@ document.getElementById("btn-spam-report")?.addEventListener("click", async () =
|
|
|
1980
2058
|
}
|
|
1981
2059
|
});
|
|
1982
2060
|
document.getElementById("btn-compose")?.addEventListener("click", () => openCompose("new"));
|
|
2061
|
+
// Toolbar "Selection" button — opens the bulk-actions menu anchored under the
|
|
2062
|
+
// button (flag/read/move/delete + select-all/flagged/invert/clear). Rendered
|
|
2063
|
+
// via the shared context-menu component so it self-dismisses on outside click.
|
|
2064
|
+
document.getElementById("btn-selection")?.addEventListener("click", (e) => {
|
|
2065
|
+
e.stopPropagation();
|
|
2066
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
2067
|
+
void showSelectionMenu(r.left, r.bottom + 2);
|
|
2068
|
+
});
|
|
1983
2069
|
document.getElementById("btn-mark-unread")?.addEventListener("click", () => {
|
|
1984
2070
|
// Toggle \Seen on the currently-focused message. Mirrors the R
|
|
1985
2071
|
// keyboard shortcut and the right-click "Mark unread" menu item, but
|
|
@@ -2507,6 +2593,28 @@ let currentAccountId = "";
|
|
|
2507
2593
|
let currentFolderId = 0;
|
|
2508
2594
|
let reloadDebounceTimer = null;
|
|
2509
2595
|
let lastListReloadAt = 0;
|
|
2596
|
+
// Search-as-find: closing search returns the user to the message they had
|
|
2597
|
+
// selected among the results, in its own folder — not back to the pre-search
|
|
2598
|
+
// view scrolled "who knows where" (Bob 2026-06-25). Capture the focused result
|
|
2599
|
+
// BEFORE clearing search mode, then navigate to that message's folder and
|
|
2600
|
+
// reselect it. With no selected result (or no folderId) fall back to the old
|
|
2601
|
+
// reloadCurrentFolder behavior.
|
|
2602
|
+
function closeSearchAndReveal() {
|
|
2603
|
+
const focused = getCurrentFocused();
|
|
2604
|
+
clearSearchMode();
|
|
2605
|
+
const body = document.getElementById("ml-body");
|
|
2606
|
+
if (body)
|
|
2607
|
+
body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
|
|
2608
|
+
if (focused && focused.folderId) {
|
|
2609
|
+
currentAccountId = focused.accountId;
|
|
2610
|
+
currentFolderId = focused.folderId;
|
|
2611
|
+
revealMessage(focused.accountId, focused.folderId, focused.uid);
|
|
2612
|
+
}
|
|
2613
|
+
else {
|
|
2614
|
+
reloadCurrentFolder();
|
|
2615
|
+
}
|
|
2616
|
+
setTitle(APP_NAME);
|
|
2617
|
+
}
|
|
2510
2618
|
searchInput?.addEventListener("input", () => {
|
|
2511
2619
|
clearTimeout(searchTimeout);
|
|
2512
2620
|
if (serverSearchTimer) {
|
|
@@ -2519,12 +2627,7 @@ searchInput?.addEventListener("input", () => {
|
|
|
2519
2627
|
// Cleared — reset immediately, no debounce. Must exit search mode
|
|
2520
2628
|
// first; otherwise reloadCurrentFolder() sees searchMode=true and
|
|
2521
2629
|
// re-runs the stale query (user-reported regression 2026-04-24).
|
|
2522
|
-
|
|
2523
|
-
const body = document.getElementById("ml-body");
|
|
2524
|
-
if (body)
|
|
2525
|
-
body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
|
|
2526
|
-
reloadCurrentFolder();
|
|
2527
|
-
setTitle(APP_NAME);
|
|
2630
|
+
closeSearchAndReveal();
|
|
2528
2631
|
}
|
|
2529
2632
|
else {
|
|
2530
2633
|
// Short debounce — just enough to coalesce a keystroke burst. The
|
|
@@ -2546,13 +2649,7 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
2546
2649
|
}
|
|
2547
2650
|
cancelServerSearch();
|
|
2548
2651
|
updateSearchHighlight();
|
|
2549
|
-
|
|
2550
|
-
// Clear any client-side filters
|
|
2551
|
-
const body = document.getElementById("ml-body");
|
|
2552
|
-
if (body)
|
|
2553
|
-
body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
|
|
2554
|
-
reloadCurrentFolder();
|
|
2555
|
-
setTitle(APP_NAME);
|
|
2652
|
+
closeSearchAndReveal();
|
|
2556
2653
|
}
|
|
2557
2654
|
});
|
|
2558
2655
|
// Re-run the active search when the scope dropdown or "server too" checkbox
|
|
@@ -3952,6 +4049,7 @@ const viewDropdown = document.getElementById("view-dropdown");
|
|
|
3952
4049
|
const optTwoLine = document.getElementById("opt-two-line");
|
|
3953
4050
|
const optPreview = document.getElementById("opt-preview");
|
|
3954
4051
|
const optSnippet = document.getElementById("opt-snippet");
|
|
4052
|
+
const optFlagStars = document.getElementById("opt-flag-stars");
|
|
3955
4053
|
const optThreaded = document.getElementById("opt-threaded");
|
|
3956
4054
|
const optFlagged = document.getElementById("opt-flagged");
|
|
3957
4055
|
const optFolderCounts = document.getElementById("opt-folder-counts");
|
|
@@ -4031,6 +4129,16 @@ function hideDropdownHard(dd) {
|
|
|
4031
4129
|
// while any toolbar dropdown is open — its pointerdown lands here in the
|
|
4032
4130
|
// parent doc, triggering the same outside-click close. The shield sits
|
|
4033
4131
|
// above compose (z 1600) but below the dropdown itself (z 2000).
|
|
4132
|
+
// Cleanup for the in-iframe Escape forwarder (see below). Null when no
|
|
4133
|
+
// forwarder is attached.
|
|
4134
|
+
let tbEscapeCleanup = null;
|
|
4135
|
+
function closeToolbarDropdowns() {
|
|
4136
|
+
for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
|
|
4137
|
+
const dd = document.getElementById(id);
|
|
4138
|
+
if (dd && !dd.hidden)
|
|
4139
|
+
hideDropdownHard(dd);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4034
4142
|
function refreshToolbarOverlayShield() {
|
|
4035
4143
|
const dropdownOpen = ["settings-dropdown", "view-dropdown", "restart-dropdown"]
|
|
4036
4144
|
.some(id => {
|
|
@@ -4047,18 +4155,57 @@ function refreshToolbarOverlayShield() {
|
|
|
4047
4155
|
shield.addEventListener("pointerdown", (ev) => {
|
|
4048
4156
|
ev.preventDefault();
|
|
4049
4157
|
ev.stopPropagation();
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
if (dd)
|
|
4053
|
-
dd.hidden = true;
|
|
4054
|
-
}
|
|
4055
|
-
shield?.remove();
|
|
4158
|
+
closeToolbarDropdowns();
|
|
4159
|
+
refreshToolbarOverlayShield();
|
|
4056
4160
|
});
|
|
4057
4161
|
document.body.appendChild(shield);
|
|
4058
4162
|
}
|
|
4163
|
+
// Escape with the cursor inside the compose editor never reaches the
|
|
4164
|
+
// parent's Escape handler — the same-origin iframe captures the
|
|
4165
|
+
// keystroke. While a toolbar menu is open over compose, attach a
|
|
4166
|
+
// capture-phase keydown to each compose/popout iframe document that
|
|
4167
|
+
// closes the menu and swallows the event, so compose's OWN Escape
|
|
4168
|
+
// (the save/discard prompt) doesn't also fire. Only active while a
|
|
4169
|
+
// menu is open; detached when it closes (Bob 2026-06-21 "fix esc").
|
|
4170
|
+
if (!tbEscapeCleanup) {
|
|
4171
|
+
const docs = [];
|
|
4172
|
+
document.querySelectorAll(".compose-overlay iframe, .popout-overlay iframe")
|
|
4173
|
+
.forEach(ifr => { try {
|
|
4174
|
+
if (ifr.contentDocument)
|
|
4175
|
+
docs.push(ifr.contentDocument);
|
|
4176
|
+
}
|
|
4177
|
+
catch { /* cross-origin */ } });
|
|
4178
|
+
if (docs.length) {
|
|
4179
|
+
const onEsc = (e) => {
|
|
4180
|
+
if (e.key !== "Escape")
|
|
4181
|
+
return;
|
|
4182
|
+
if (!["settings-dropdown", "view-dropdown", "restart-dropdown"]
|
|
4183
|
+
.some(id => { const el = document.getElementById(id); return el && !el.hidden; }))
|
|
4184
|
+
return;
|
|
4185
|
+
e.preventDefault();
|
|
4186
|
+
e.stopPropagation();
|
|
4187
|
+
closeToolbarDropdowns();
|
|
4188
|
+
refreshToolbarOverlayShield();
|
|
4189
|
+
};
|
|
4190
|
+
for (const d of docs)
|
|
4191
|
+
d.addEventListener("keydown", onEsc, true);
|
|
4192
|
+
tbEscapeCleanup = () => {
|
|
4193
|
+
for (const d of docs) {
|
|
4194
|
+
try {
|
|
4195
|
+
d.removeEventListener("keydown", onEsc, true);
|
|
4196
|
+
}
|
|
4197
|
+
catch { /* */ }
|
|
4198
|
+
}
|
|
4199
|
+
tbEscapeCleanup = null;
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4059
4203
|
}
|
|
4060
|
-
else
|
|
4061
|
-
shield
|
|
4204
|
+
else {
|
|
4205
|
+
if (shield)
|
|
4206
|
+
shield.remove();
|
|
4207
|
+
if (tbEscapeCleanup)
|
|
4208
|
+
tbEscapeCleanup();
|
|
4062
4209
|
}
|
|
4063
4210
|
}
|
|
4064
4211
|
// Escape always closes any open dropdown/backdrop, regardless of how it was
|
|
@@ -4094,6 +4241,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
4094
4241
|
const savedTwoLine = localStorage.getItem("mailx-two-line") === "true";
|
|
4095
4242
|
const savedPreview = localStorage.getItem("mailx-preview") !== "false"; // default true
|
|
4096
4243
|
const savedSnippet = localStorage.getItem("mailx-snippet") !== "false"; // default true
|
|
4244
|
+
const savedFlagStars = localStorage.getItem("mailx-flag-stars") !== "false"; // default true (show)
|
|
4097
4245
|
const savedThreaded = localStorage.getItem("mailx-threaded") === "true";
|
|
4098
4246
|
const savedFlagged = localStorage.getItem("mailx-flagged") === "true";
|
|
4099
4247
|
const savedFolderCounts = localStorage.getItem("mailx-folder-counts") === "true";
|
|
@@ -4103,6 +4251,8 @@ if (optPreview)
|
|
|
4103
4251
|
optPreview.checked = savedPreview;
|
|
4104
4252
|
if (optSnippet)
|
|
4105
4253
|
optSnippet.checked = savedSnippet;
|
|
4254
|
+
if (optFlagStars)
|
|
4255
|
+
optFlagStars.checked = savedFlagStars;
|
|
4106
4256
|
if (optThreaded)
|
|
4107
4257
|
optThreaded.checked = savedThreaded;
|
|
4108
4258
|
if (optFlagged)
|
|
@@ -4115,6 +4265,8 @@ if (!savedPreview)
|
|
|
4115
4265
|
document.querySelector(".main-area")?.classList.add("no-preview");
|
|
4116
4266
|
if (!savedSnippet)
|
|
4117
4267
|
document.getElementById("message-list")?.classList.add("no-snippets");
|
|
4268
|
+
if (!savedFlagStars)
|
|
4269
|
+
document.getElementById("message-list")?.classList.add("no-empty-stars");
|
|
4118
4270
|
if (savedThreaded)
|
|
4119
4271
|
document.getElementById("ml-body")?.classList.add("threaded");
|
|
4120
4272
|
if (savedFlagged) {
|
|
@@ -4349,6 +4501,12 @@ optSnippet?.addEventListener("change", () => {
|
|
|
4349
4501
|
}
|
|
4350
4502
|
localStorage.setItem("mailx-snippet", String(optSnippet.checked));
|
|
4351
4503
|
});
|
|
4504
|
+
optFlagStars?.addEventListener("change", () => {
|
|
4505
|
+
const list = document.getElementById("message-list");
|
|
4506
|
+
// checked = show empty stars (no class); unchecked = hide them.
|
|
4507
|
+
list?.classList.toggle("no-empty-stars", !optFlagStars.checked);
|
|
4508
|
+
localStorage.setItem("mailx-flag-stars", String(optFlagStars.checked));
|
|
4509
|
+
});
|
|
4352
4510
|
// ── Search help button (?) ──
|
|
4353
4511
|
// Toggles a NON-modal panel with the search-syntax reference so it stays
|
|
4354
4512
|
// visible while the user types into the search box — build a query
|