@bobfrankston/rmfmail 1.0.484 → 1.0.486
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/app.js +135 -22
- package/client/app.js.map +1 -1
- package/client/app.ts +125 -19
- package/client/components/message-list.js +19 -0
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +19 -0
- package/client/index.html +1 -0
- package/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 } from "./components/message-list.js";
|
|
6
|
+
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView } from "./components/message-list.js";
|
|
7
7
|
import { showMessage, getCurrentMessage, initViewer, popOutCurrentMessage } from "./components/message-viewer.js";
|
|
8
8
|
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage } from "./lib/api-client.js";
|
|
9
9
|
import * as messageState from "./lib/message-state.js";
|
|
@@ -479,19 +479,10 @@ document.addEventListener("pointerdown", (e) => {
|
|
|
479
479
|
if (window.innerWidth <= 768)
|
|
480
480
|
rail.classList.remove("open");
|
|
481
481
|
}, true);
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// rail
|
|
486
|
-
document.querySelectorAll(".icon-rail .rail-btn").forEach(btn => {
|
|
487
|
-
btn.addEventListener("click", () => {
|
|
488
|
-
if (window.innerWidth > 768)
|
|
489
|
-
return;
|
|
490
|
-
if (btn.id === "rail-settings" || btn.id === "rail-view")
|
|
491
|
-
return;
|
|
492
|
-
document.querySelector(".icon-rail")?.classList.remove("open");
|
|
493
|
-
});
|
|
494
|
-
});
|
|
482
|
+
// The rail stays open until the user clicks outside it — handled by the
|
|
483
|
+
// document-level outside-click handler above. No per-button close: chaining
|
|
484
|
+
// rail actions (toggle theme, then check About, then change a setting)
|
|
485
|
+
// without having to re-open the rail each time was the explicit ask.
|
|
495
486
|
// ── Toolbar actions ──
|
|
496
487
|
document.getElementById("btn-sync")?.addEventListener("click", async () => {
|
|
497
488
|
const btn = document.getElementById("btn-sync");
|
|
@@ -1000,14 +991,9 @@ document.getElementById("btn-flag")?.addEventListener("click", async () => {
|
|
|
1000
991
|
await updateFlags(sel.accountId, sel.uid, newFlags);
|
|
1001
992
|
sel.flags = newFlags;
|
|
1002
993
|
messageState.updateMessageFlags(sel.accountId, sel.uid, newFlags);
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
row.classList.toggle("flagged", newFlags.includes("\\Flagged"));
|
|
1007
|
-
const flagEl = row.querySelector(".ml-flag");
|
|
1008
|
-
if (flagEl)
|
|
1009
|
-
flagEl.textContent = newFlags.includes("\\Flagged") ? "\u2605" : "\u2606";
|
|
1010
|
-
}
|
|
994
|
+
// Row owns its own DOM \u2014 go through the row object so class + star
|
|
995
|
+
// update atomically and the list/preview stay in sync.
|
|
996
|
+
setRowFlagged(sel.accountId, sel.uid, newFlags.includes("\\Flagged"));
|
|
1011
997
|
}
|
|
1012
998
|
catch (e) {
|
|
1013
999
|
console.error(`Flag toggle failed: ${e.message}`);
|
|
@@ -2160,6 +2146,36 @@ document.addEventListener("keydown", (e) => {
|
|
|
2160
2146
|
row.classList.toggle("unread", !newFlags.includes("\\Seen"));
|
|
2161
2147
|
}).catch(() => { });
|
|
2162
2148
|
}
|
|
2149
|
+
// Z = locate the focused row in the list (scroll-to-selected). After
|
|
2150
|
+
// scrolling the list out of sync with the preview, this snaps back.
|
|
2151
|
+
if (e.key.toLowerCase() === "z" && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
2152
|
+
const active = document.activeElement;
|
|
2153
|
+
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.tagName === "SELECT"))
|
|
2154
|
+
return;
|
|
2155
|
+
const editable = active?.isContentEditable;
|
|
2156
|
+
if (editable)
|
|
2157
|
+
return;
|
|
2158
|
+
e.preventDefault();
|
|
2159
|
+
scrollFocusedIntoView();
|
|
2160
|
+
}
|
|
2161
|
+
// F6 / Shift+F6 — standard pane-switch shortcut. Cycles focus among
|
|
2162
|
+
// folder tree → message list → message viewer.
|
|
2163
|
+
if (e.key === "F6") {
|
|
2164
|
+
e.preventDefault();
|
|
2165
|
+
cyclePaneFocus(e.shiftKey);
|
|
2166
|
+
}
|
|
2167
|
+
// ? = show keyboard shortcuts help. Skip when typing in inputs so a
|
|
2168
|
+
// literal "?" in search/compose doesn't pop the dialog.
|
|
2169
|
+
if (e.key === "?" && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
2170
|
+
const active = document.activeElement;
|
|
2171
|
+
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.tagName === "SELECT"))
|
|
2172
|
+
return;
|
|
2173
|
+
const editable = active?.isContentEditable;
|
|
2174
|
+
if (editable)
|
|
2175
|
+
return;
|
|
2176
|
+
e.preventDefault();
|
|
2177
|
+
openShortcutsDialog();
|
|
2178
|
+
}
|
|
2163
2179
|
// Arrow keys + Home/End/PgUp/PgDn — navigate message list (Q58).
|
|
2164
2180
|
if (["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"].includes(e.key)) {
|
|
2165
2181
|
const active = document.activeElement;
|
|
@@ -2193,6 +2209,96 @@ document.addEventListener("keydown", (e) => {
|
|
|
2193
2209
|
}
|
|
2194
2210
|
}
|
|
2195
2211
|
});
|
|
2212
|
+
// ── F6 pane cycling ──
|
|
2213
|
+
function cyclePaneFocus(reverse) {
|
|
2214
|
+
// Major panes in tab order. Skip ones not currently visible (folder
|
|
2215
|
+
// tree is hidden in narrow tier when the rail/drawer is closed; message
|
|
2216
|
+
// viewer is only meaningful with a message open).
|
|
2217
|
+
const panes = [
|
|
2218
|
+
{ id: "folder-tree", el: document.getElementById("folder-tree") },
|
|
2219
|
+
{ id: "ml-body", el: document.getElementById("ml-body") },
|
|
2220
|
+
{ id: "message-viewer", el: document.getElementById("message-viewer") },
|
|
2221
|
+
].filter(p => {
|
|
2222
|
+
if (!p.el)
|
|
2223
|
+
return false;
|
|
2224
|
+
const r = p.el.getBoundingClientRect();
|
|
2225
|
+
return r.width > 0 && r.height > 0;
|
|
2226
|
+
});
|
|
2227
|
+
if (panes.length === 0)
|
|
2228
|
+
return;
|
|
2229
|
+
const active = document.activeElement;
|
|
2230
|
+
const currentIdx = panes.findIndex(p => p.el && (p.el === active || p.el.contains(active)));
|
|
2231
|
+
const step = reverse ? -1 : 1;
|
|
2232
|
+
const nextIdx = currentIdx < 0 ? 0 : (currentIdx + step + panes.length) % panes.length;
|
|
2233
|
+
const next = panes[nextIdx];
|
|
2234
|
+
if (!next.el)
|
|
2235
|
+
return;
|
|
2236
|
+
// Make the pane focusable if it isn't already; tabindex=-1 lets us
|
|
2237
|
+
// focus it programmatically without inserting it into Tab order.
|
|
2238
|
+
if (!next.el.hasAttribute("tabindex"))
|
|
2239
|
+
next.el.setAttribute("tabindex", "-1");
|
|
2240
|
+
next.el.focus();
|
|
2241
|
+
// Visual hint — brief outline so the user can see which pane took focus.
|
|
2242
|
+
next.el.style.outline = "2px solid var(--color-accent, #3b82f6)";
|
|
2243
|
+
next.el.style.outlineOffset = "-2px";
|
|
2244
|
+
setTimeout(() => { next.el.style.outline = ""; next.el.style.outlineOffset = ""; }, 600);
|
|
2245
|
+
}
|
|
2246
|
+
// ── Keyboard shortcuts help dialog ──
|
|
2247
|
+
function openShortcutsDialog() {
|
|
2248
|
+
if (document.querySelector(".mailx-shortcuts-modal"))
|
|
2249
|
+
return;
|
|
2250
|
+
const backdrop = document.createElement("div");
|
|
2251
|
+
backdrop.className = "mailx-modal-backdrop mailx-shortcuts-modal";
|
|
2252
|
+
const panel = document.createElement("div");
|
|
2253
|
+
panel.className = "mailx-modal";
|
|
2254
|
+
panel.style.maxWidth = "560px";
|
|
2255
|
+
panel.innerHTML = `
|
|
2256
|
+
<div class="mailx-modal-title">
|
|
2257
|
+
<span class="mailx-modal-title-text">Keyboard shortcuts</span>
|
|
2258
|
+
<button type="button" class="mailx-modal-close" id="sc-x" title="Close (Esc)" aria-label="Close">×</button>
|
|
2259
|
+
</div>
|
|
2260
|
+
<div class="mailx-about">
|
|
2261
|
+
<dl class="mailx-about-dl">
|
|
2262
|
+
<dt>Compose</dt><dd>Ctrl+N</dd>
|
|
2263
|
+
<dt>Reply</dt><dd>Ctrl+R</dd>
|
|
2264
|
+
<dt>Reply all</dt><dd>Ctrl+Shift+R</dd>
|
|
2265
|
+
<dt>Forward</dt><dd>Ctrl+F</dd>
|
|
2266
|
+
<dt>Send (in compose)</dt><dd>Ctrl+Enter</dd>
|
|
2267
|
+
<dt>Sync</dt><dd>F5</dd>
|
|
2268
|
+
<dt>Delete selected</dt><dd>Del or Ctrl+D</dd>
|
|
2269
|
+
<dt>Undo last delete/move</dt><dd>Ctrl+Z</dd>
|
|
2270
|
+
<dt>Toggle read/unread</dt><dd>R</dd>
|
|
2271
|
+
<dt>Toggle flag</dt><dd>(Flag button in viewer)</dd>
|
|
2272
|
+
<dt>Select all visible</dt><dd>Ctrl+A</dd>
|
|
2273
|
+
<dt>Navigate list</dt><dd>↑ ↓ Home End PgUp PgDn</dd>
|
|
2274
|
+
<dt>Scroll-to-focused row</dt><dd>Z</dd>
|
|
2275
|
+
<dt>Switch pane</dt><dd>F6 / Shift+F6</dd>
|
|
2276
|
+
<dt>Find / search</dt><dd>(focus the search box)</dd>
|
|
2277
|
+
<dt>Close dialog</dt><dd>Esc</dd>
|
|
2278
|
+
<dt>Show this help</dt><dd>?</dd>
|
|
2279
|
+
</dl>
|
|
2280
|
+
<div class="mailx-about-foot">On Android most shortcuts need a Bluetooth/USB keyboard. Touch equivalents: tap a row to view, tap the rail icons for navigation/settings/theme/help, long-press a row for the context menu.</div>
|
|
2281
|
+
</div>
|
|
2282
|
+
<div class="mailx-modal-buttons">
|
|
2283
|
+
<span class="mailx-modal-spacer"></span>
|
|
2284
|
+
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="close">Close</button>
|
|
2285
|
+
</div>`;
|
|
2286
|
+
backdrop.appendChild(panel);
|
|
2287
|
+
document.body.appendChild(backdrop);
|
|
2288
|
+
const close = () => { backdrop.remove(); document.removeEventListener("keydown", onKey, true); };
|
|
2289
|
+
const onKey = (e) => {
|
|
2290
|
+
if (e.key === "Escape") {
|
|
2291
|
+
e.stopPropagation();
|
|
2292
|
+
e.preventDefault();
|
|
2293
|
+
close();
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
document.addEventListener("keydown", onKey, true);
|
|
2297
|
+
panel.querySelector('[data-action="close"]').addEventListener("click", close);
|
|
2298
|
+
panel.querySelector("#sc-x").addEventListener("click", close);
|
|
2299
|
+
backdrop.addEventListener("mousedown", (e) => { if (e.target === backdrop)
|
|
2300
|
+
close(); });
|
|
2301
|
+
}
|
|
2196
2302
|
// ── View menu ──
|
|
2197
2303
|
const viewBtn = document.getElementById("btn-view");
|
|
2198
2304
|
const viewDropdown = document.getElementById("view-dropdown");
|
|
@@ -2817,6 +2923,13 @@ function renderMarkdown(md) {
|
|
|
2817
2923
|
closeList();
|
|
2818
2924
|
return out.join("\n");
|
|
2819
2925
|
}
|
|
2926
|
+
// ── Keyboard shortcuts (Settings menu item) ──
|
|
2927
|
+
document.getElementById("btn-shortcuts")?.addEventListener("click", () => {
|
|
2928
|
+
const settingsDropdown = document.getElementById("settings-dropdown");
|
|
2929
|
+
if (settingsDropdown)
|
|
2930
|
+
settingsDropdown.hidden = true;
|
|
2931
|
+
openShortcutsDialog();
|
|
2932
|
+
});
|
|
2820
2933
|
// ── About dialog ──
|
|
2821
2934
|
document.getElementById("btn-about")?.addEventListener("click", () => {
|
|
2822
2935
|
const settingsDropdown = document.getElementById("settings-dropdown");
|