@bobfrankston/rmfmail 1.0.487 → 1.0.489
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 +91 -32
- package/client/app.js.map +1 -1
- package/client/app.ts +87 -32
- package/client/components/message-viewer.js +46 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +46 -0
- package/client/index.html +5 -5
- package/client/styles/layout.css +19 -0
- package/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced, setOutboxTotal } from "./components/folder-tree.js";
|
|
6
6
|
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView } from "./components/message-list.js";
|
|
7
|
-
import { showMessage, getCurrentMessage, initViewer, popOutCurrentMessage } from "./components/message-viewer.js";
|
|
7
|
+
import { showMessage, getCurrentMessage, initViewer, popOutCurrentMessage, toggleFullscreenPreview } 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";
|
|
10
10
|
// ── New message badge (favicon + title) ──
|
|
@@ -1194,44 +1194,91 @@ function openMenuFromRail(dropdownId, anchor) {
|
|
|
1194
1194
|
if (dd.parentElement?.classList.contains("tb-menu")) {
|
|
1195
1195
|
document.body.appendChild(dd);
|
|
1196
1196
|
}
|
|
1197
|
-
|
|
1197
|
+
// Reset all positional inline styles each open so a previous configuration
|
|
1198
|
+
// (e.g. wide-tier anchor → narrow-tier modal) doesn't leak through.
|
|
1199
|
+
dd.style.cssText = "";
|
|
1198
1200
|
dd.style.position = "fixed";
|
|
1199
|
-
// Anchor to the right of the rail icon. Clamp horizontally so the menu
|
|
1200
|
-
// doesn't fall off the right edge on narrow screens.
|
|
1201
|
-
const minWidth = 220;
|
|
1202
|
-
const left = Math.min(rect.right + 6, window.innerWidth - minWidth - 8);
|
|
1203
|
-
dd.style.left = `${Math.max(8, left)}px`;
|
|
1204
|
-
// Reset any prior anchoring so measurement is clean.
|
|
1205
|
-
dd.style.top = "";
|
|
1206
|
-
dd.style.bottom = "";
|
|
1207
|
-
dd.style.maxHeight = "";
|
|
1208
|
-
dd.style.overflowY = "";
|
|
1209
1201
|
dd.style.zIndex = "10000";
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
dd.style.
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1202
|
+
if (window.innerWidth <= 768) {
|
|
1203
|
+
// Narrow tier: render as a centered modal-style overlay. Anchor-based
|
|
1204
|
+
// positioning was unreliable on phones because bottom-rail icons sit
|
|
1205
|
+
// at the viewport floor — the menu either spilled off-screen or
|
|
1206
|
+
// landed where the rail itself overlapped it. A centered card is
|
|
1207
|
+
// always visible, always reachable, and matches the keyboard-shortcut
|
|
1208
|
+
// dialog pattern (click-outside / Esc to close).
|
|
1209
|
+
dd.style.top = "50%";
|
|
1210
|
+
dd.style.left = "50%";
|
|
1211
|
+
dd.style.transform = "translate(-50%, -50%)";
|
|
1212
|
+
dd.style.maxHeight = "85vh";
|
|
1213
|
+
dd.style.maxWidth = "92vw";
|
|
1214
|
+
dd.style.overflowY = "auto";
|
|
1215
|
+
dd.style.boxShadow = "0 8px 32px rgba(0,0,0,0.4)";
|
|
1216
|
+
dd.hidden = false;
|
|
1217
|
+
ensureRailMenuBackdrop();
|
|
1226
1218
|
}
|
|
1227
1219
|
else {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1220
|
+
// Wide tier: anchor to the right of the rail icon, growing whichever
|
|
1221
|
+
// way has more room. (Bottom-rail icons sit near the viewport floor —
|
|
1222
|
+
// anchoring `top: rect.top` makes the menu spill off the bottom.)
|
|
1223
|
+
const rect = anchor.getBoundingClientRect();
|
|
1224
|
+
const minWidth = 220;
|
|
1225
|
+
const left = Math.min(rect.right + 6, window.innerWidth - minWidth - 8);
|
|
1226
|
+
dd.style.left = `${Math.max(8, left)}px`;
|
|
1227
|
+
dd.hidden = false;
|
|
1228
|
+
const ddHeight = dd.getBoundingClientRect().height;
|
|
1229
|
+
const spaceBelow = window.innerHeight - rect.top - 8;
|
|
1230
|
+
const spaceAbove = rect.bottom - 8;
|
|
1231
|
+
if (ddHeight > spaceBelow && spaceAbove > spaceBelow) {
|
|
1232
|
+
dd.style.bottom = `${Math.max(8, window.innerHeight - rect.bottom)}px`;
|
|
1233
|
+
if (ddHeight > spaceAbove) {
|
|
1234
|
+
dd.style.maxHeight = `${spaceAbove}px`;
|
|
1235
|
+
dd.style.overflowY = "auto";
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
dd.style.top = `${Math.max(8, rect.top)}px`;
|
|
1240
|
+
if (ddHeight > spaceBelow) {
|
|
1241
|
+
dd.style.maxHeight = `${spaceBelow}px`;
|
|
1242
|
+
dd.style.overflowY = "auto";
|
|
1243
|
+
}
|
|
1232
1244
|
}
|
|
1233
1245
|
}
|
|
1234
1246
|
}
|
|
1247
|
+
/** Narrow-tier rail menus need a tap-anywhere-to-close backdrop because the
|
|
1248
|
+
* rail itself stays open under the modal. The document outside-click handler
|
|
1249
|
+
* hides the dropdown but doesn't visually convey "modal" — the backdrop
|
|
1250
|
+
* dims the page, makes the modal look like one, and gives a single
|
|
1251
|
+
* unambiguous click target for dismissal. Auto-removes when the dropdown
|
|
1252
|
+
* hides (we observe the [hidden] flip via MutationObserver). */
|
|
1253
|
+
function ensureRailMenuBackdrop() {
|
|
1254
|
+
if (document.getElementById("rail-menu-backdrop"))
|
|
1255
|
+
return;
|
|
1256
|
+
const bd = document.createElement("div");
|
|
1257
|
+
bd.id = "rail-menu-backdrop";
|
|
1258
|
+
bd.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9999;";
|
|
1259
|
+
bd.addEventListener("click", () => {
|
|
1260
|
+
for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
|
|
1261
|
+
const dd = document.getElementById(id);
|
|
1262
|
+
if (dd && !dd.hidden)
|
|
1263
|
+
dd.hidden = true;
|
|
1264
|
+
}
|
|
1265
|
+
bd.remove();
|
|
1266
|
+
});
|
|
1267
|
+
document.body.appendChild(bd);
|
|
1268
|
+
// Self-clean when no rail menu is open
|
|
1269
|
+
const cleanup = () => {
|
|
1270
|
+
const anyOpen = ["settings-dropdown", "view-dropdown", "restart-dropdown"]
|
|
1271
|
+
.some(id => { const el = document.getElementById(id); return el && !el.hidden; });
|
|
1272
|
+
if (!anyOpen)
|
|
1273
|
+
bd.remove();
|
|
1274
|
+
};
|
|
1275
|
+
const obs = new MutationObserver(cleanup);
|
|
1276
|
+
for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
|
|
1277
|
+
const el = document.getElementById(id);
|
|
1278
|
+
if (el)
|
|
1279
|
+
obs.observe(el, { attributes: true, attributeFilter: ["hidden"] });
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1235
1282
|
document.getElementById("rail-settings")?.addEventListener("click", (e) => {
|
|
1236
1283
|
e.stopPropagation();
|
|
1237
1284
|
openMenuFromRail("settings-dropdown", e.currentTarget);
|
|
@@ -2209,6 +2256,18 @@ document.addEventListener("keydown", (e) => {
|
|
|
2209
2256
|
}
|
|
2210
2257
|
}
|
|
2211
2258
|
});
|
|
2259
|
+
// ── Double-click viewer chrome → fullscreen preview ──
|
|
2260
|
+
// The iframe's own contentDocument has its own dblclick handler (in
|
|
2261
|
+
// message-viewer.ts). This catches dblclicks on the headers / from row /
|
|
2262
|
+
// reply-to row of the viewer that aren't inside the iframe.
|
|
2263
|
+
document.getElementById("message-viewer")?.addEventListener("dblclick", (e) => {
|
|
2264
|
+
const target = e.target;
|
|
2265
|
+
// Don't toggle when double-clicking interactive controls (chips, buttons,
|
|
2266
|
+
// links, address widgets) — only on the chrome / empty space.
|
|
2267
|
+
if (target?.closest("a, button, input, select, textarea, [contenteditable]"))
|
|
2268
|
+
return;
|
|
2269
|
+
toggleFullscreenPreview();
|
|
2270
|
+
});
|
|
2212
2271
|
// ── F6 pane cycling ──
|
|
2213
2272
|
function cyclePaneFocus(reverse) {
|
|
2214
2273
|
// Major panes in tab order. Skip ones not currently visible (folder
|