@bobfrankston/rmfmail 1.0.607 → 1.0.619
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/TODO.md +10 -0
- package/bin/mailx.js +24 -8
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +32 -8
- package/client/app.js +70 -30
- package/client/app.js.map +1 -1
- package/client/app.ts +66 -28
- package/client/components/alarms.js +91 -173
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +85 -158
- package/client/components/folder-tree.js +1 -1
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +1 -1
- package/client/components/message-list.js +21 -6
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +23 -6
- package/client/components/message-viewer.js +37 -6
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +37 -6
- package/client/index.html +8 -5
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +3 -0
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +3 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +36 -0
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +29 -0
- package/packages/mailx-store/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -9,8 +9,36 @@ import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate,
|
|
|
9
9
|
import * as messageState from "./lib/message-state.js";
|
|
10
10
|
// ── New message badge (favicon + title) ──
|
|
11
11
|
/** The user-visible app name. Single point of change for the rename;
|
|
12
|
-
* every UI surface that shows "rmfmail" reads from here.
|
|
12
|
+
* every UI surface that shows "rmfmail" reads from here. Static HTML
|
|
13
|
+
* uses placeholder text that `propagateAppName` (called immediately
|
|
14
|
+
* below) overwrites — so the constant is the only place the literal
|
|
15
|
+
* string lives. */
|
|
13
16
|
export const APP_NAME = "rmfmail";
|
|
17
|
+
/** Stamp APP_NAME into every static HTML element that should show it.
|
|
18
|
+
* Runs synchronously at module load — before paint when imports are
|
|
19
|
+
* fast, with a sub-frame flash of the placeholder otherwise. Add new
|
|
20
|
+
* surfaces here, never inline literals. */
|
|
21
|
+
function propagateAppName() {
|
|
22
|
+
const set = (id, fn) => {
|
|
23
|
+
const el = document.getElementById(id);
|
|
24
|
+
if (el)
|
|
25
|
+
el.textContent = fn(APP_NAME);
|
|
26
|
+
};
|
|
27
|
+
document.title = APP_NAME;
|
|
28
|
+
set("startup-status", n => `Starting ${n}…`);
|
|
29
|
+
set("status-version", n => n);
|
|
30
|
+
set("app-version", n => n);
|
|
31
|
+
// About-button label / restart-button hover-titles live in HTML
|
|
32
|
+
// attributes; rewrite the ones that include the name.
|
|
33
|
+
const aboutBtn = document.getElementById("btn-about");
|
|
34
|
+
if (aboutBtn)
|
|
35
|
+
aboutBtn.title = `Show version and build info`;
|
|
36
|
+
const aboutText = aboutBtn?.textContent || "";
|
|
37
|
+
if (aboutBtn && aboutText.toLowerCase().includes("about")) {
|
|
38
|
+
aboutBtn.textContent = `About ${APP_NAME}...`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
propagateAppName();
|
|
14
42
|
let baseTitle = APP_NAME;
|
|
15
43
|
let lastSeenCount = 0;
|
|
16
44
|
let badgeCount = 0;
|
|
@@ -894,10 +922,36 @@ async function deleteSelectedMessages() {
|
|
|
894
922
|
selected.push({ accountId: current.accountId, uid: current.message.uid, folderId: current.message.folderId });
|
|
895
923
|
}
|
|
896
924
|
const statusSync = document.getElementById("status-sync");
|
|
925
|
+
// Optimistic UI: remove from list IMMEDIATELY, then queue the IPC.
|
|
926
|
+
// Old order awaited the daemon round-trip (IPC + DB updates) before
|
|
927
|
+
// the rows disappeared, which felt sluggish on bigger selections or
|
|
928
|
+
// when the IPC was congested. Spam button already worked this way;
|
|
929
|
+
// trash now matches. If the IPC fails, the next folder reload
|
|
930
|
+
// re-populates the row and the catch block surfaces the error.
|
|
931
|
+
const snapshot = [...selected];
|
|
932
|
+
removeMessagesAndReconcile(selected);
|
|
933
|
+
// Undo support set immediately too — Ctrl+Z works the moment rows
|
|
934
|
+
// disappear from the list, not only after the daemon ACKs.
|
|
935
|
+
if (snapshot.length === 1) {
|
|
936
|
+
lastDeleted = { ...snapshot[0], subject: "" };
|
|
937
|
+
if (statusSync)
|
|
938
|
+
statusSync.textContent = `Trashed 1 message (syncing) — Ctrl+Z to undo`;
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
lastDeleted = null;
|
|
942
|
+
if (statusSync)
|
|
943
|
+
statusSync.textContent = `Trashed ${snapshot.length} messages (syncing)`;
|
|
944
|
+
}
|
|
945
|
+
if (undoTimeout)
|
|
946
|
+
clearTimeout(undoTimeout);
|
|
947
|
+
undoTimeout = setTimeout(() => {
|
|
948
|
+
lastDeleted = null;
|
|
949
|
+
if (statusSync?.textContent?.includes("undo"))
|
|
950
|
+
statusSync.textContent = "";
|
|
951
|
+
}, 30000);
|
|
897
952
|
try {
|
|
898
|
-
// Delete on server — group by account for bulk operations
|
|
899
953
|
const byAccount = new Map();
|
|
900
|
-
for (const msg of
|
|
954
|
+
for (const msg of snapshot) {
|
|
901
955
|
const uids = byAccount.get(msg.accountId) || [];
|
|
902
956
|
uids.push(msg.uid);
|
|
903
957
|
byAccount.set(msg.accountId, uids);
|
|
@@ -905,32 +959,11 @@ async function deleteSelectedMessages() {
|
|
|
905
959
|
for (const [accountId, uids] of byAccount) {
|
|
906
960
|
await deleteMessages(accountId, uids);
|
|
907
961
|
}
|
|
908
|
-
// Undo supports the last batch
|
|
909
|
-
if (selected.length === 1) {
|
|
910
|
-
lastDeleted = { ...selected[0], subject: "" };
|
|
911
|
-
if (statusSync)
|
|
912
|
-
statusSync.textContent = `Trashed 1 message (syncing) — Ctrl+Z to undo`;
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
lastDeleted = null;
|
|
916
|
-
if (statusSync)
|
|
917
|
-
statusSync.textContent = `Trashed ${selected.length} messages (syncing)`;
|
|
918
|
-
}
|
|
919
|
-
if (undoTimeout)
|
|
920
|
-
clearTimeout(undoTimeout);
|
|
921
|
-
undoTimeout = setTimeout(() => {
|
|
922
|
-
lastDeleted = null;
|
|
923
|
-
if (statusSync?.textContent?.includes("undo"))
|
|
924
|
-
statusSync.textContent = "";
|
|
925
|
-
}, 30000);
|
|
926
|
-
// Remove via the list controller — single transaction filters
|
|
927
|
-
// state, deletes DOM rows, hands focus to a survivor (or clears
|
|
928
|
-
// the viewer). Replaces the old "state.removeMessages broadcasts
|
|
929
|
-
// a removed event, list+viewer subscribe independently" model.
|
|
930
|
-
removeMessagesAndReconcile(selected);
|
|
931
962
|
}
|
|
932
963
|
catch (e) {
|
|
933
964
|
console.error(`Delete failed: ${e.message}`);
|
|
965
|
+
if (statusSync)
|
|
966
|
+
statusSync.textContent = `Delete failed: ${e?.message || e}`;
|
|
934
967
|
}
|
|
935
968
|
}
|
|
936
969
|
async function undoDelete() {
|
|
@@ -1790,7 +1823,7 @@ window.addEventListener("message", (e) => {
|
|
|
1790
1823
|
// instantly and lingers when the user moves to do anything else,
|
|
1791
1824
|
// including punching through the compose overlay (which sits at
|
|
1792
1825
|
// z-index 1000 — popover was at 10000, hence the bug in the
|
|
1793
|
-
// screenshot). Now:
|
|
1826
|
+
// screenshot). Now: 1500ms hover delay; suppressed entirely when
|
|
1794
1827
|
// any overlay (compose, modal) is open; auto-dismissed on click,
|
|
1795
1828
|
// scroll, blur, or any keypress.
|
|
1796
1829
|
const w = window;
|
|
@@ -1815,7 +1848,7 @@ window.addEventListener("message", (e) => {
|
|
|
1815
1848
|
const source = e.source;
|
|
1816
1849
|
w._linkHoverShowTimer = setTimeout(() => {
|
|
1817
1850
|
// Re-check overlay state at fire time — overlay may have appeared
|
|
1818
|
-
// during the
|
|
1851
|
+
// during the 1500ms wait.
|
|
1819
1852
|
if (document.querySelector(".compose-overlay, .mailx-modal-backdrop"))
|
|
1820
1853
|
return;
|
|
1821
1854
|
if (!pop) {
|
|
@@ -1850,7 +1883,7 @@ window.addEventListener("message", (e) => {
|
|
|
1850
1883
|
pop.style.left = x + "px";
|
|
1851
1884
|
pop.style.top = y + "px";
|
|
1852
1885
|
}
|
|
1853
|
-
},
|
|
1886
|
+
}, 1500);
|
|
1854
1887
|
}
|
|
1855
1888
|
if (e.data?.type === "previewKey" && typeof e.data.key === "string") {
|
|
1856
1889
|
// Re-dispatch as a real keydown on document so the hotkey handler
|
|
@@ -2429,11 +2462,18 @@ document.addEventListener("keydown", (e) => {
|
|
|
2429
2462
|
// Ctrl+D or Delete = Delete selected messages.
|
|
2430
2463
|
// P15: don't hijack Delete inside text inputs / textareas / contenteditable
|
|
2431
2464
|
// — JSONC editor's Delete key was being eaten because we always preventDefault'd.
|
|
2465
|
+
// 2026-05-09 fix: if the LIST has multiple selected rows, the user
|
|
2466
|
+
// clearly means "delete those" regardless of where focus is — search
|
|
2467
|
+
// input retaining focus after filtering shouldn't make the key dead.
|
|
2468
|
+
// Only defer to native behavior when there's no list selection AND
|
|
2469
|
+
// focus is in a text-editing surface.
|
|
2432
2470
|
if ((e.ctrlKey && e.key === "d") || e.key === "Delete") {
|
|
2433
2471
|
const t = e.target;
|
|
2434
2472
|
const tag = t?.tagName;
|
|
2435
2473
|
const editable = t?.isContentEditable;
|
|
2436
|
-
|
|
2474
|
+
const inEditable = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || editable;
|
|
2475
|
+
const listSelectionCount = document.querySelectorAll("#ml-body .ml-row.selected").length;
|
|
2476
|
+
if (inEditable && listSelectionCount === 0)
|
|
2437
2477
|
return;
|
|
2438
2478
|
e.preventDefault();
|
|
2439
2479
|
deleteSelection();
|