@bobfrankston/rmfmail 1.1.36 → 1.1.38
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.bundle.js +205 -35
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +42 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +41 -0
- package/client/components/message-list.js +58 -35
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +55 -34
- package/client/components/tabs.js +155 -0
- package/client/components/tabs.js.map +1 -0
- package/client/components/tabs.ts +163 -0
- package/client/index.html +4 -0
- package/client/styles/components.css +58 -0
- package/client/styles/layout.css +15 -9
- package/docs/multi-view.md +81 -0
- package/package.json +3 -2
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-45456 → node_modules.npmglobalize-stash-49720}/.package-lock.json +0 -0
package/client/app.bundle.js
CHANGED
|
@@ -2298,7 +2298,7 @@ function pickFolder(accountId, opts) {
|
|
|
2298
2298
|
}
|
|
2299
2299
|
const excluded = new Set(opts?.excludeFolderIds || []);
|
|
2300
2300
|
const targets = folders.filter((f) => !excluded.has(f.id)).filter((f) => f.specialUse !== "outbox").sort((a, b) => a.path.localeCompare(b.path));
|
|
2301
|
-
function
|
|
2301
|
+
function render2(filter) {
|
|
2302
2302
|
listEl.innerHTML = "";
|
|
2303
2303
|
const lc = filter.toLowerCase().trim();
|
|
2304
2304
|
let firstMatchSet = false;
|
|
@@ -2331,8 +2331,8 @@ function pickFolder(accountId, opts) {
|
|
|
2331
2331
|
listEl.appendChild(row);
|
|
2332
2332
|
}
|
|
2333
2333
|
}
|
|
2334
|
-
|
|
2335
|
-
search.addEventListener("input", () =>
|
|
2334
|
+
render2("");
|
|
2335
|
+
search.addEventListener("input", () => render2(search.value));
|
|
2336
2336
|
setTimeout(() => search.focus(), 0);
|
|
2337
2337
|
});
|
|
2338
2338
|
}
|
|
@@ -2400,24 +2400,27 @@ function rememberPosition() {
|
|
|
2400
2400
|
const uid = Number(sel.dataset.uid);
|
|
2401
2401
|
if (!Number.isFinite(uid))
|
|
2402
2402
|
return;
|
|
2403
|
-
positionMemory.set(key, { uid, scroll: body.scrollTop });
|
|
2403
|
+
positionMemory.set(key, { uid, uuid: sel.dataset.uuid || "", scroll: body.scrollTop });
|
|
2404
2404
|
persistPositions();
|
|
2405
2405
|
}
|
|
2406
2406
|
function pickRestoreUid(items, saved) {
|
|
2407
2407
|
if (!items.length)
|
|
2408
2408
|
return null;
|
|
2409
|
-
if (
|
|
2410
|
-
|
|
2411
|
-
|
|
2409
|
+
if (saved.uuid) {
|
|
2410
|
+
const exact = items.find((m) => m.uuid && m.uuid === saved.uuid);
|
|
2411
|
+
if (exact?.uuid)
|
|
2412
|
+
return exact.uuid;
|
|
2413
|
+
}
|
|
2414
|
+
let best = null;
|
|
2412
2415
|
for (const m of items) {
|
|
2413
|
-
if (typeof m.uid !== "number")
|
|
2416
|
+
if (typeof m.uid !== "number" || !m.uuid)
|
|
2414
2417
|
continue;
|
|
2415
|
-
if (m.uid < saved && m.uid > best)
|
|
2416
|
-
best = m
|
|
2418
|
+
if (m.uid < saved.uid && (!best || m.uid > best.uid))
|
|
2419
|
+
best = m;
|
|
2417
2420
|
}
|
|
2418
|
-
if (best
|
|
2419
|
-
return best;
|
|
2420
|
-
return
|
|
2421
|
+
if (best)
|
|
2422
|
+
return best.uuid;
|
|
2423
|
+
return items[0]?.uuid || null;
|
|
2421
2424
|
}
|
|
2422
2425
|
function listResultsEqual(a, b) {
|
|
2423
2426
|
if (!a || a.length !== b.length)
|
|
@@ -2748,10 +2751,10 @@ async function loadUnifiedInbox(autoSelect = true) {
|
|
|
2748
2751
|
totalMessages = cached.total;
|
|
2749
2752
|
setMessages(cached.items);
|
|
2750
2753
|
renderMessages(body, "", cached.items);
|
|
2751
|
-
const
|
|
2752
|
-
if (
|
|
2754
|
+
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
|
|
2755
|
+
if (targetUuid) {
|
|
2753
2756
|
body.scrollTop = savedScroll;
|
|
2754
|
-
restoreSelection(body,
|
|
2757
|
+
restoreSelection(body, targetUuid);
|
|
2755
2758
|
} else if (autoSelect) {
|
|
2756
2759
|
selectFirst(body);
|
|
2757
2760
|
}
|
|
@@ -2773,10 +2776,10 @@ async function loadUnifiedInbox(autoSelect = true) {
|
|
|
2773
2776
|
}
|
|
2774
2777
|
setMessages(result.items);
|
|
2775
2778
|
renderMessages(body, "", result.items);
|
|
2776
|
-
const
|
|
2777
|
-
if (
|
|
2779
|
+
const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
|
|
2780
|
+
if (targetUuid) {
|
|
2778
2781
|
body.scrollTop = savedScroll;
|
|
2779
|
-
restoreSelection(body,
|
|
2782
|
+
restoreSelection(body, targetUuid);
|
|
2780
2783
|
} else if (autoSelect) {
|
|
2781
2784
|
selectFirst(body);
|
|
2782
2785
|
}
|
|
@@ -2887,11 +2890,11 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
|
|
|
2887
2890
|
totalMessages = cached.total;
|
|
2888
2891
|
setMessages(cached.items);
|
|
2889
2892
|
renderMessages(body, accountId, cached.items);
|
|
2890
|
-
const
|
|
2891
|
-
if (
|
|
2893
|
+
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
|
|
2894
|
+
if (targetUuid) {
|
|
2892
2895
|
requestAnimationFrame(() => {
|
|
2893
2896
|
body.scrollTop = savedScroll;
|
|
2894
|
-
restoreSelection(body,
|
|
2897
|
+
restoreSelection(body, targetUuid);
|
|
2895
2898
|
});
|
|
2896
2899
|
} else if (autoSelect) {
|
|
2897
2900
|
selectFirst(body);
|
|
@@ -2915,13 +2918,13 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
|
|
|
2915
2918
|
}
|
|
2916
2919
|
setMessages(result.items);
|
|
2917
2920
|
renderMessages(body, accountId, result.items);
|
|
2918
|
-
const
|
|
2919
|
-
if (
|
|
2921
|
+
const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
|
|
2922
|
+
if (targetUuid) {
|
|
2920
2923
|
requestAnimationFrame(() => {
|
|
2921
2924
|
if (myGen !== loadGen)
|
|
2922
2925
|
return;
|
|
2923
2926
|
body.scrollTop = savedScroll;
|
|
2924
|
-
restoreSelection(body,
|
|
2927
|
+
restoreSelection(body, targetUuid);
|
|
2925
2928
|
});
|
|
2926
2929
|
} else if (autoSelect) {
|
|
2927
2930
|
selectFirst(body);
|
|
@@ -2967,12 +2970,16 @@ function selectFirst(body) {
|
|
|
2967
2970
|
if (firstRow)
|
|
2968
2971
|
firstRow.click();
|
|
2969
2972
|
}
|
|
2970
|
-
function restoreSelection(body,
|
|
2971
|
-
if (!
|
|
2973
|
+
function restoreSelection(body, savedUuid) {
|
|
2974
|
+
if (!savedUuid)
|
|
2975
|
+
return;
|
|
2976
|
+
const row = body.querySelector(`.ml-row[data-uuid="${CSS.escape(savedUuid)}"]`);
|
|
2977
|
+
if (!row)
|
|
2972
2978
|
return;
|
|
2973
|
-
const accountId =
|
|
2974
|
-
|
|
2975
|
-
|
|
2979
|
+
const accountId = row.dataset.accountId;
|
|
2980
|
+
const uid = Number(row.dataset.uid);
|
|
2981
|
+
if (accountId && Number.isFinite(uid)) {
|
|
2982
|
+
focusByIdentity(accountId, uid, { scroll: false });
|
|
2976
2983
|
}
|
|
2977
2984
|
}
|
|
2978
2985
|
async function showThreadPopup(pillEl, headMsg) {
|
|
@@ -3173,6 +3180,8 @@ var init_message_list = __esm({
|
|
|
3173
3180
|
row.dataset.uid = String(msg.uid);
|
|
3174
3181
|
row.dataset.accountId = accountId;
|
|
3175
3182
|
row.dataset.folderId = String(msg.folderId);
|
|
3183
|
+
if (msg.uuid)
|
|
3184
|
+
row.dataset.uuid = msg.uuid;
|
|
3176
3185
|
if (msg.threadId)
|
|
3177
3186
|
row.dataset.threadId = msg.threadId;
|
|
3178
3187
|
if (threadHead)
|
|
@@ -4466,7 +4475,7 @@ async function openAddressBook() {
|
|
|
4466
4475
|
const listEl = panel.querySelector("#ab-list");
|
|
4467
4476
|
const countEl = panel.querySelector("#ab-count");
|
|
4468
4477
|
let editingEmail = null;
|
|
4469
|
-
const
|
|
4478
|
+
const render2 = (items, total) => {
|
|
4470
4479
|
countEl.textContent = total === items.length ? `${total} contact${total === 1 ? "" : "s"}` : `Showing ${items.length} of ${total}`;
|
|
4471
4480
|
if (items.length === 0) {
|
|
4472
4481
|
listEl.innerHTML = `<div class="ab-empty">No contacts match.</div>`;
|
|
@@ -4576,7 +4585,7 @@ async function openAddressBook() {
|
|
|
4576
4585
|
const reload = async () => {
|
|
4577
4586
|
try {
|
|
4578
4587
|
const r = await listContacts(searchInput2.value, 1, 200);
|
|
4579
|
-
|
|
4588
|
+
render2(r.items, r.total);
|
|
4580
4589
|
} catch (e) {
|
|
4581
4590
|
listEl.innerHTML = `<div class="ab-empty">Load failed: ${escapeHtml5(e?.message || String(e))}</div>`;
|
|
4582
4591
|
}
|
|
@@ -6090,6 +6099,135 @@ async function updateFolderCounts() {
|
|
|
6090
6099
|
// client/app.ts
|
|
6091
6100
|
init_message_list();
|
|
6092
6101
|
init_mailx_types();
|
|
6102
|
+
|
|
6103
|
+
// client/components/tabs.js
|
|
6104
|
+
var STORAGE_KEY = "mailx-view-tabs";
|
|
6105
|
+
var tabs = [];
|
|
6106
|
+
var activeId = "";
|
|
6107
|
+
var stripEl = null;
|
|
6108
|
+
var applyView = () => {
|
|
6109
|
+
};
|
|
6110
|
+
var _nextId = 1;
|
|
6111
|
+
function newId() {
|
|
6112
|
+
return `t${_nextId++}`;
|
|
6113
|
+
}
|
|
6114
|
+
function persist() {
|
|
6115
|
+
try {
|
|
6116
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ tabs, activeId }));
|
|
6117
|
+
} catch {
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
function render() {
|
|
6121
|
+
if (!stripEl)
|
|
6122
|
+
return;
|
|
6123
|
+
stripEl.innerHTML = "";
|
|
6124
|
+
for (const tab of tabs) {
|
|
6125
|
+
const chip = document.createElement("div");
|
|
6126
|
+
chip.className = "view-tab" + (tab.id === activeId ? " active" : "");
|
|
6127
|
+
chip.dataset.tabId = tab.id;
|
|
6128
|
+
const label = document.createElement("span");
|
|
6129
|
+
label.className = "view-tab-label";
|
|
6130
|
+
label.textContent = tab.title || "(view)";
|
|
6131
|
+
chip.appendChild(label);
|
|
6132
|
+
if (tabs.length > 1) {
|
|
6133
|
+
const close = document.createElement("button");
|
|
6134
|
+
close.className = "view-tab-close";
|
|
6135
|
+
close.textContent = "\xD7";
|
|
6136
|
+
close.title = "Close tab";
|
|
6137
|
+
close.addEventListener("click", (e) => {
|
|
6138
|
+
e.stopPropagation();
|
|
6139
|
+
closeTab(tab.id);
|
|
6140
|
+
});
|
|
6141
|
+
chip.appendChild(close);
|
|
6142
|
+
}
|
|
6143
|
+
chip.addEventListener("click", () => activate(tab.id));
|
|
6144
|
+
stripEl.appendChild(chip);
|
|
6145
|
+
}
|
|
6146
|
+
const plus = document.createElement("button");
|
|
6147
|
+
plus.className = "view-tab-new";
|
|
6148
|
+
plus.textContent = "+";
|
|
6149
|
+
plus.title = "New tab (All Inboxes)";
|
|
6150
|
+
plus.addEventListener("click", () => openTab({ kind: "unified" }, "All Inboxes", true));
|
|
6151
|
+
stripEl.appendChild(plus);
|
|
6152
|
+
stripEl.hidden = tabs.length < 1;
|
|
6153
|
+
}
|
|
6154
|
+
function initTabs(strip, apply) {
|
|
6155
|
+
stripEl = strip;
|
|
6156
|
+
applyView = apply;
|
|
6157
|
+
try {
|
|
6158
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
6159
|
+
if (raw) {
|
|
6160
|
+
const parsed = JSON.parse(raw);
|
|
6161
|
+
if (Array.isArray(parsed?.tabs) && parsed.tabs.length) {
|
|
6162
|
+
tabs = parsed.tabs;
|
|
6163
|
+
activeId = parsed.activeId || tabs[0].id;
|
|
6164
|
+
for (const t of tabs) {
|
|
6165
|
+
const n = Number(String(t.id).replace(/^t/, ""));
|
|
6166
|
+
if (Number.isFinite(n) && n >= _nextId)
|
|
6167
|
+
_nextId = n + 1;
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
} catch {
|
|
6172
|
+
}
|
|
6173
|
+
render();
|
|
6174
|
+
}
|
|
6175
|
+
function activeTab() {
|
|
6176
|
+
return tabs.find((t) => t.id === activeId) || null;
|
|
6177
|
+
}
|
|
6178
|
+
function openTab(view, title, activateIt = true) {
|
|
6179
|
+
const tab = { id: newId(), title, view };
|
|
6180
|
+
tabs.push(tab);
|
|
6181
|
+
if (activateIt)
|
|
6182
|
+
activeId = tab.id;
|
|
6183
|
+
persist();
|
|
6184
|
+
render();
|
|
6185
|
+
if (activateIt)
|
|
6186
|
+
applyView(tab);
|
|
6187
|
+
}
|
|
6188
|
+
function activate(id) {
|
|
6189
|
+
const tab = tabs.find((t) => t.id === id);
|
|
6190
|
+
if (!tab || id === activeId)
|
|
6191
|
+
return;
|
|
6192
|
+
activeId = id;
|
|
6193
|
+
persist();
|
|
6194
|
+
render();
|
|
6195
|
+
applyView(tab);
|
|
6196
|
+
}
|
|
6197
|
+
function closeTab(id) {
|
|
6198
|
+
if (tabs.length < 2)
|
|
6199
|
+
return;
|
|
6200
|
+
const idx = tabs.findIndex((t) => t.id === id);
|
|
6201
|
+
if (idx < 0)
|
|
6202
|
+
return;
|
|
6203
|
+
const wasActive = id === activeId;
|
|
6204
|
+
tabs.splice(idx, 1);
|
|
6205
|
+
if (wasActive) {
|
|
6206
|
+
const next = tabs[Math.max(0, idx - 1)];
|
|
6207
|
+
activeId = next.id;
|
|
6208
|
+
persist();
|
|
6209
|
+
render();
|
|
6210
|
+
applyView(next);
|
|
6211
|
+
} else {
|
|
6212
|
+
persist();
|
|
6213
|
+
render();
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
function setActiveView(view, title) {
|
|
6217
|
+
let tab = activeTab();
|
|
6218
|
+
if (!tab) {
|
|
6219
|
+
tab = { id: newId(), title, view };
|
|
6220
|
+
tabs.push(tab);
|
|
6221
|
+
activeId = tab.id;
|
|
6222
|
+
} else {
|
|
6223
|
+
tab.view = view;
|
|
6224
|
+
tab.title = title;
|
|
6225
|
+
}
|
|
6226
|
+
persist();
|
|
6227
|
+
render();
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
// client/app.ts
|
|
6093
6231
|
init_message_viewer();
|
|
6094
6232
|
init_api_client();
|
|
6095
6233
|
init_message_state();
|
|
@@ -6383,6 +6521,7 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
6383
6521
|
loadMessages(accountId, folderId, 1, specialUse);
|
|
6384
6522
|
setTitle(`${APP_NAME} - ${folderName}`);
|
|
6385
6523
|
setNarrowFolderTitle(folderName);
|
|
6524
|
+
setActiveView({ kind: "folder", accountId, folderId, specialUse }, folderName);
|
|
6386
6525
|
document.dispatchEvent(new CustomEvent("mailx-folder-changed", { detail: { accountId, folderId } }));
|
|
6387
6526
|
}, () => {
|
|
6388
6527
|
currentFolderSpecialUse = "inbox";
|
|
@@ -6392,7 +6531,34 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
6392
6531
|
loadUnifiedInbox();
|
|
6393
6532
|
setTitle(`${APP_NAME} - All Inboxes`);
|
|
6394
6533
|
setNarrowFolderTitle("All Inboxes");
|
|
6534
|
+
setActiveView({ kind: "unified" }, "All Inboxes");
|
|
6395
6535
|
});
|
|
6536
|
+
function applyTabView(tab) {
|
|
6537
|
+
if (searchInput) searchInput.value = "";
|
|
6538
|
+
clearSearchMode();
|
|
6539
|
+
releaseFocus();
|
|
6540
|
+
const v = tab.view;
|
|
6541
|
+
if (v.kind === "unified") {
|
|
6542
|
+
currentFolderSpecialUse = "inbox";
|
|
6543
|
+
loadUnifiedInbox();
|
|
6544
|
+
setTitle(`${APP_NAME} - All Inboxes`);
|
|
6545
|
+
setNarrowFolderTitle("All Inboxes");
|
|
6546
|
+
} else if (v.kind === "folder") {
|
|
6547
|
+
currentFolderSpecialUse = v.specialUse;
|
|
6548
|
+
currentAccountId3 = v.accountId;
|
|
6549
|
+
currentFolderId2 = v.folderId;
|
|
6550
|
+
loadMessages(v.accountId, v.folderId, 1, v.specialUse);
|
|
6551
|
+
setTitle(`${APP_NAME} - ${tab.title}`);
|
|
6552
|
+
setNarrowFolderTitle(tab.title);
|
|
6553
|
+
} else {
|
|
6554
|
+
if (searchInput) searchInput.value = v.query;
|
|
6555
|
+
loadSearchResults(v.query, v.scope, v.accountId, v.folderId, v.includeTrash);
|
|
6556
|
+
setTitle(`${APP_NAME} - Search`);
|
|
6557
|
+
setNarrowFolderTitle(`Search: ${v.query}`);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
var tabStripEl = document.getElementById("view-tab-strip");
|
|
6561
|
+
if (tabStripEl) initTabs(tabStripEl, applyTabView);
|
|
6396
6562
|
initMessageList((_accountId, _uid, _folderId) => {
|
|
6397
6563
|
if (window.innerWidth <= 768) {
|
|
6398
6564
|
document.getElementById("message-viewer")?.classList.add("narrow-active");
|
|
@@ -7400,6 +7566,10 @@ function doSearch(immediate = false) {
|
|
|
7400
7566
|
}
|
|
7401
7567
|
loadSearchResults(query, effectiveScope, currentAccountId3, currentFolderId2, includeTrash);
|
|
7402
7568
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
7569
|
+
setActiveView(
|
|
7570
|
+
{ kind: "search", query, scope: effectiveScope, accountId: currentAccountId3, folderId: currentFolderId2, includeTrash },
|
|
7571
|
+
`Search: ${query}`
|
|
7572
|
+
);
|
|
7403
7573
|
if (immediate) recordSearchHistory(query);
|
|
7404
7574
|
}
|
|
7405
7575
|
var currentAccountId3 = "";
|
|
@@ -8239,7 +8409,7 @@ function openShortcutsDialog() {
|
|
|
8239
8409
|
const panel = document.createElement("div");
|
|
8240
8410
|
panel.className = "mailx-modal";
|
|
8241
8411
|
panel.style.maxWidth = "640px";
|
|
8242
|
-
const
|
|
8412
|
+
const tabs2 = [
|
|
8243
8413
|
{
|
|
8244
8414
|
id: "app",
|
|
8245
8415
|
label: "Application",
|
|
@@ -8298,10 +8468,10 @@ function openShortcutsDialog() {
|
|
|
8298
8468
|
<button type="button" class="mailx-modal-close" id="sc-x" title="Close (Esc)" aria-label="Close">×</button>
|
|
8299
8469
|
</div>
|
|
8300
8470
|
<div class="mailx-shortcuts-tabs" role="tablist" style="display:flex;gap:0;border-bottom:1px solid var(--color-border, #ddd);margin:0 -16px 12px -16px;padding:0 16px;">
|
|
8301
|
-
${
|
|
8471
|
+
${tabs2.map((t, i) => `<button type="button" role="tab" class="mailx-shortcuts-tab" data-tab="${t.id}" aria-selected="${i === 0}" style="background:none;border:0;padding:8px 14px;cursor:pointer;font:inherit;border-bottom:2px solid ${i === 0 ? "var(--color-accent, #3b82f6)" : "transparent"};color:${i === 0 ? "var(--color-text)" : "var(--color-text-muted, #888)"};">${t.label}</button>`).join("")}
|
|
8302
8472
|
</div>
|
|
8303
8473
|
<div class="mailx-about">
|
|
8304
|
-
${
|
|
8474
|
+
${tabs2.map((t, i) => `<div class="mailx-shortcuts-pane" data-pane="${t.id}" ${i === 0 ? "" : "hidden"}>
|
|
8305
8475
|
<dl class="mailx-about-dl">
|
|
8306
8476
|
${t.rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("")}
|
|
8307
8477
|
</dl>
|