@bobfrankston/rmfmail 1.2.87 → 1.2.95
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/chk-search-tmp.mjs +14 -0
- package/cleanup-sent-tmp.mjs +43 -0
- package/client/android-bootstrap.bundle.js +65 -15
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +361 -282
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +23 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +20 -1
- package/client/components/address-book.js +5 -1
- package/client/components/address-book.js.map +1 -1
- package/client/components/address-book.ts +5 -1
- package/client/components/message-list.js +164 -54
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +159 -51
- package/client/components/message-viewer.js +7 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +7 -0
- package/client/compose/compose.bundle.js +4 -4
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/index.html +4 -0
- package/client/lib/api-client.js +4 -4
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -4
- package/client/lib/mailxapi.js +4 -4
- package/client/styles/components.css +20 -0
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts +17 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +86 -4
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +85 -4
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +2 -2
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +6 -4
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +7 -5
- package/packages/mailx-service/jsonrpc.js +2 -2
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -2
- package/packages/mailx-service/sync-queue.d.ts +3 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -1
- package/packages/mailx-service/sync-queue.js +30 -11
- package/packages/mailx-service/sync-queue.js.map +1 -1
- package/packages/mailx-service/sync-queue.ts +34 -9
- package/packages/mailx-service/sync-worker.js +51 -0
- package/packages/mailx-service/sync-worker.js.map +1 -1
- package/packages/mailx-service/sync-worker.ts +45 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +10 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +68 -7
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +70 -8
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts +1 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +2 -2
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +2 -2
- package/packages/mailx-store-web/imap-web-provider.d.ts +7 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.js +12 -0
- package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.ts +15 -0
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
- package/packages/mailx-store-web/sync-manager.js +12 -0
- package/packages/mailx-store-web/sync-manager.js.map +1 -1
- package/packages/mailx-store-web/sync-manager.ts +12 -0
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +53 -19
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +60 -20
- package/packages/mailx-types/index.d.ts +5 -1
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +5 -1
- package/packages/mailx-types/package.json +1 -1
package/client/app.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced, setOutboxTotal } from "./components/folder-tree.js";
|
|
7
|
-
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, setLiveFilter, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex, revealMessage, selectAllVisible, exitMultiSelect } from "./components/message-list.js";
|
|
7
|
+
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, setLiveFilter, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex, revealMessage, selectAllVisible, exitMultiSelect, getDateBasis, setDateBasis } from "./components/message-list.js";
|
|
8
8
|
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
9
9
|
import { initTabs, setActiveView as setActiveTabView, openTab, type ViewTab } from "./components/tabs.js";
|
|
10
10
|
import { showMessage, getCurrentMessage, initViewer, popOutCurrentMessage, popOutToWindow, printCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu, wrapHtmlBody } from "./components/message-viewer.js";
|
|
@@ -2290,6 +2290,12 @@ document.addEventListener("mailx-share-intent", ((e: CustomEvent) => {
|
|
|
2290
2290
|
if (detail.subject) init.subject = detail.subject;
|
|
2291
2291
|
if (detail.text) init.bodyHtml = `<p>${escapeHtml(String(detail.text)).replace(/\n/g, "<br>")}</p>`;
|
|
2292
2292
|
}
|
|
2293
|
+
// Shared files (screenshots, PDFs, .eml, …) arrive base64-encoded from the
|
|
2294
|
+
// Android bridge. Seed them as compose attachments — compose.ts consumes
|
|
2295
|
+
// init.attachments[{filename, mimeType, dataBase64}] and builds the chips.
|
|
2296
|
+
if (Array.isArray(detail.attachments) && detail.attachments.length) {
|
|
2297
|
+
init.attachments = detail.attachments.filter((a: any) => a?.filename && a?.dataBase64);
|
|
2298
|
+
}
|
|
2293
2299
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
2294
2300
|
showComposeOverlay(init.subject || "Compose");
|
|
2295
2301
|
}) as EventListener);
|
|
@@ -4927,6 +4933,19 @@ optThreaded?.addEventListener("change", () => {
|
|
|
4927
4933
|
reloadCurrentFolder();
|
|
4928
4934
|
});
|
|
4929
4935
|
|
|
4936
|
+
// Date basis (Sent / Received). message-list owns the state + persistence +
|
|
4937
|
+
// reload; app.ts just reflects the current value into the radios and forwards
|
|
4938
|
+
// changes. Initialize the radio to match the stored basis.
|
|
4939
|
+
const optDateSent = document.getElementById("opt-date-sent") as HTMLInputElement | null;
|
|
4940
|
+
const optDateReceived = document.getElementById("opt-date-received") as HTMLInputElement | null;
|
|
4941
|
+
(function initDateBasisRadios() {
|
|
4942
|
+
const basis = getDateBasis();
|
|
4943
|
+
if (optDateSent) optDateSent.checked = basis === "sent";
|
|
4944
|
+
if (optDateReceived) optDateReceived.checked = basis === "received";
|
|
4945
|
+
})();
|
|
4946
|
+
optDateSent?.addEventListener("change", () => { if (optDateSent.checked) setDateBasis("sent"); });
|
|
4947
|
+
optDateReceived?.addEventListener("change", () => { if (optDateReceived.checked) setDateBasis("received"); });
|
|
4948
|
+
|
|
4930
4949
|
// Flagged-only filter — keeps the CSS-level hiding for instant feedback on
|
|
4931
4950
|
// the current page AND re-queries the folder so flagged messages that live
|
|
4932
4951
|
// outside the currently-loaded page show up.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { listContacts, upsertContact, deleteContact } from "../lib/api-client.js";
|
|
7
7
|
let isOpen = false;
|
|
8
|
-
export async function openAddressBook() {
|
|
8
|
+
export async function openAddressBook(prefillSearch) {
|
|
9
9
|
if (isOpen)
|
|
10
10
|
return;
|
|
11
11
|
isOpen = true;
|
|
@@ -171,6 +171,10 @@ export async function openAddressBook() {
|
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
searchInput.addEventListener("input", scheduleReload);
|
|
174
|
+
// "Open in contacts" from the viewer's address right-click lands here with
|
|
175
|
+
// the address prefilled, so the list opens already filtered to that person.
|
|
176
|
+
if (prefillSearch)
|
|
177
|
+
searchInput.value = prefillSearch;
|
|
174
178
|
const close = () => {
|
|
175
179
|
if (reloadDebounce)
|
|
176
180
|
window.clearTimeout(reloadDebounce);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAUlF,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB,MAAM,CAAC,KAAK,UAAU,eAAe;
|
|
1
|
+
{"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@ interface Contact {
|
|
|
15
15
|
|
|
16
16
|
let isOpen = false;
|
|
17
17
|
|
|
18
|
-
export async function openAddressBook(): Promise<void> {
|
|
18
|
+
export async function openAddressBook(prefillSearch?: string): Promise<void> {
|
|
19
19
|
if (isOpen) return;
|
|
20
20
|
isOpen = true;
|
|
21
21
|
|
|
@@ -181,6 +181,10 @@ export async function openAddressBook(): Promise<void> {
|
|
|
181
181
|
});
|
|
182
182
|
searchInput.addEventListener("input", scheduleReload);
|
|
183
183
|
|
|
184
|
+
// "Open in contacts" from the viewer's address right-click lands here with
|
|
185
|
+
// the address prefilled, so the list opens already filtered to that person.
|
|
186
|
+
if (prefillSearch) searchInput.value = prefillSearch;
|
|
187
|
+
|
|
184
188
|
const close = () => {
|
|
185
189
|
if (reloadDebounce) window.clearTimeout(reloadDebounce);
|
|
186
190
|
backdrop.remove();
|
|
@@ -87,6 +87,33 @@ if (typeof document !== "undefined") {
|
|
|
87
87
|
// (text columns default asc, date defaults desc).
|
|
88
88
|
let currentSort = "date";
|
|
89
89
|
let currentSortDir = "desc";
|
|
90
|
+
// Date basis for the "date" sort + the date column: "sent" (Date: header,
|
|
91
|
+
// default — identical mail clusters, matches other clients) or "received"
|
|
92
|
+
// (per-account arrival time). Persisted so the View-menu toggle survives
|
|
93
|
+
// reloads. Bob 2026-07-01.
|
|
94
|
+
let dateBasis = localStorage.getItem("mailx-date-basis") || "sent";
|
|
95
|
+
/** The epoch-ms to DISPLAY for a row, honoring the current basis. sentDate is
|
|
96
|
+
* always populated by the store (backfilled to date), but guard anyway. */
|
|
97
|
+
function effectiveDate(msg) {
|
|
98
|
+
return dateBasis === "received" ? msg.date : (msg.sentDate ?? msg.date);
|
|
99
|
+
}
|
|
100
|
+
/** Current date basis — read by app.ts to check the View-menu radio state. */
|
|
101
|
+
export function getDateBasis() { return dateBasis; }
|
|
102
|
+
/** Switch the date basis, persist it, and reload the current view so the new
|
|
103
|
+
* sort + column take effect. Called by the View menu. */
|
|
104
|
+
export function setDateBasis(basis) {
|
|
105
|
+
if (basis === dateBasis)
|
|
106
|
+
return;
|
|
107
|
+
dateBasis = basis;
|
|
108
|
+
try {
|
|
109
|
+
localStorage.setItem("mailx-date-basis", basis);
|
|
110
|
+
}
|
|
111
|
+
catch { /* private mode */ }
|
|
112
|
+
// Cached snapshots were ordered by the old basis — clear so the reload
|
|
113
|
+
// re-sorts from the DB rather than repainting the stale order.
|
|
114
|
+
listCache.clear();
|
|
115
|
+
reloadCurrentFolder();
|
|
116
|
+
}
|
|
90
117
|
/** Generation counter — incremented on every load* call (loadMessages,
|
|
91
118
|
* loadUnifiedInbox, loadSearchResults). Each load captures the current
|
|
92
119
|
* value at the top, then checks before rendering — if the captured gen
|
|
@@ -165,6 +192,7 @@ function withScrollAnchor(body, doRender) {
|
|
|
165
192
|
// must not go off screen"). The old `scrollTop < 4 → no anchor` shortcut
|
|
166
193
|
// was exactly the bug: at the top, new rows shoved the read letter off the
|
|
167
194
|
// bottom.
|
|
195
|
+
let anchoredOnSelected = false;
|
|
168
196
|
const selected = body.querySelector(".ml-row.selected");
|
|
169
197
|
if (selected) {
|
|
170
198
|
const vt = selected.offsetTop - scrollTop;
|
|
@@ -172,6 +200,7 @@ function withScrollAnchor(body, doRender) {
|
|
|
172
200
|
if (vt > -selected.offsetHeight && vt < viewportH) {
|
|
173
201
|
anchorUuid = selected.dataset.uuid || "";
|
|
174
202
|
anchorOffsetWithinViewport = vt;
|
|
203
|
+
anchoredOnSelected = true;
|
|
175
204
|
}
|
|
176
205
|
}
|
|
177
206
|
if (!anchorUuid) {
|
|
@@ -203,7 +232,14 @@ function withScrollAnchor(body, doRender) {
|
|
|
203
232
|
// bottom do we fall back to pinning it visible. This satisfies both
|
|
204
233
|
// "show new mail at the top" and the 2026-06-04 rule "a letter in view
|
|
205
234
|
// must stay in view (it may scroll down but must not go off screen)".
|
|
206
|
-
|
|
235
|
+
//
|
|
236
|
+
// EXCEPTION (Bob 2026-06-30: "I was looking at a message and suddenly
|
|
237
|
+
// my position changed and I was looking at a new message"): when the
|
|
238
|
+
// anchor IS the open/selected message, the user is actively reading —
|
|
239
|
+
// never reposition them. Pin the selected row exactly where it sat.
|
|
240
|
+
// The scroll-to-0 reveal only applies when nothing is open and the user
|
|
241
|
+
// is merely parked at the top of the list browsing subjects.
|
|
242
|
+
if (wasAtTop && !anchoredOnSelected && after.offsetTop < viewportH - after.offsetHeight) {
|
|
207
243
|
body.scrollTop = 0;
|
|
208
244
|
}
|
|
209
245
|
else {
|
|
@@ -235,8 +271,20 @@ function rememberPosition() {
|
|
|
235
271
|
persistPositions();
|
|
236
272
|
}
|
|
237
273
|
/** Choose the row to focus when re-entering a view with saved position.
|
|
238
|
-
* Returns the uid to focus, or null to fall back to selectFirst.
|
|
239
|
-
|
|
274
|
+
* Returns the uid to focus, or null to fall back to selectFirst.
|
|
275
|
+
*
|
|
276
|
+
* `allowNeighbor` gates the deleted-message fallback. On a FOREGROUND
|
|
277
|
+
* re-entry (user navigating INTO a view) the saved message being absent
|
|
278
|
+
* means it was deleted, so we hand focus to the nearest survivor. On a
|
|
279
|
+
* BACKGROUND reload (sync/new-mail rebuild of the current view) absent
|
|
280
|
+
* does NOT mean deleted — the message the user is READING has merely been
|
|
281
|
+
* pushed out of the loaded top-50 page by new arrivals. Switching to a
|
|
282
|
+
* neighbor there yanks the viewer onto the new arrival while the list
|
|
283
|
+
* stays put (Bob 2026-07-01: "I was looking at a letter and suddenly found
|
|
284
|
+
* myself looking at a newly arrived message; the summary did not scroll").
|
|
285
|
+
* So background callers pass allowNeighbor=false → return null → keep the
|
|
286
|
+
* current viewer untouched. */
|
|
287
|
+
function pickRestoreUid(items, saved, allowNeighbor = true) {
|
|
240
288
|
if (!items.length)
|
|
241
289
|
return null;
|
|
242
290
|
// Exact restore by stable uuid — globally unique, so this can never
|
|
@@ -247,6 +295,8 @@ function pickRestoreUid(items, saved) {
|
|
|
247
295
|
if (exact?.uuid)
|
|
248
296
|
return exact.uuid;
|
|
249
297
|
}
|
|
298
|
+
if (!allowNeighbor)
|
|
299
|
+
return null;
|
|
250
300
|
// Saved message is gone (deleted). Pick the next-older entry by uid —
|
|
251
301
|
// uid is roughly monotonic with arrival on IMAP/Gmail — and return ITS
|
|
252
302
|
// uuid so the restore stays uuid-keyed end to end.
|
|
@@ -718,10 +768,32 @@ export function exitMultiSelect() {
|
|
|
718
768
|
updateBulkBar();
|
|
719
769
|
}
|
|
720
770
|
/** Bulk-actions bar retired 2026-04-24 — trash + spam live on the main
|
|
721
|
-
* toolbar now
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
|
|
771
|
+
* toolbar. This now renders just the selection COUNT (Bob 2026-07-01:
|
|
772
|
+
* "multiselect should show the count somewhere") as a floating pill at the
|
|
773
|
+
* bottom of the list panel, shown only while a multi-selection of ≥2 is
|
|
774
|
+
* active. Every selection-changing site already calls this. */
|
|
775
|
+
function updateBulkBar() {
|
|
776
|
+
const panel = document.getElementById("message-list");
|
|
777
|
+
if (!panel)
|
|
778
|
+
return;
|
|
779
|
+
const body = document.getElementById("ml-body");
|
|
780
|
+
const count = body?.classList.contains("multi-select-on")
|
|
781
|
+
? body.querySelectorAll(".ml-row.selected").length : 0;
|
|
782
|
+
let pill = document.getElementById("ml-select-count");
|
|
783
|
+
if (count >= 2) {
|
|
784
|
+
if (!pill) {
|
|
785
|
+
pill = document.createElement("div");
|
|
786
|
+
pill.id = "ml-select-count";
|
|
787
|
+
pill.className = "ml-select-count";
|
|
788
|
+
panel.appendChild(pill);
|
|
789
|
+
}
|
|
790
|
+
pill.textContent = `${count} selected`;
|
|
791
|
+
pill.style.display = "";
|
|
792
|
+
}
|
|
793
|
+
else if (pill) {
|
|
794
|
+
pill.style.display = "none";
|
|
795
|
+
}
|
|
796
|
+
}
|
|
725
797
|
/** Select every shown (not filter-hidden) row AND enter multi-select mode.
|
|
726
798
|
* Shared by the selection menu's "Select all shown" and the Ctrl+A shortcut.
|
|
727
799
|
* CRITICAL: it sets `multi-select-on` — without that flag a subsequent ★ click
|
|
@@ -846,37 +918,7 @@ export function initMessageList(handler) {
|
|
|
846
918
|
touchWasScroll = true;
|
|
847
919
|
}
|
|
848
920
|
}, { passive: true });
|
|
849
|
-
body.addEventListener("scroll", () => {
|
|
850
|
-
if (loading)
|
|
851
|
-
return;
|
|
852
|
-
// Prefetch the next page well BEFORE the bottom (was 200px). The next
|
|
853
|
-
// 50 rows are a DB query + render; triggering it only 200px out meant
|
|
854
|
-
// the user scrolled INTO the not-yet-loaded gap and saw a pause (Bob
|
|
855
|
-
// 2026-06-18 "scroll the summary, there is a delay"). 1000px ≈ a couple
|
|
856
|
-
// screens of lead time so the page is usually ready by the time it's
|
|
857
|
-
// scrolled to. `loading` guard still prevents overlapping fetches.
|
|
858
|
-
const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 1000;
|
|
859
|
-
if (nearBottom) {
|
|
860
|
-
if (currentPage * 50 < totalMessages) {
|
|
861
|
-
loadMoreMessages();
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
864
|
-
// Diagnostic: at bottom but not loading more. Log why
|
|
865
|
-
// so the user can see whether totalMessages is wrong,
|
|
866
|
-
// a filter is on, or we've truly reached the end.
|
|
867
|
-
// Only log once per "stuck at bottom" episode.
|
|
868
|
-
if (!body._mlScrollEndLogged) {
|
|
869
|
-
body._mlScrollEndLogged = true;
|
|
870
|
-
const rows = body.querySelectorAll(".ml-row").length;
|
|
871
|
-
console.log(` [ml-scroll] reached bottom — currentPage=${currentPage} pageSize=50 loadedRows=${rows} totalMessages=${totalMessages} searchMode=${searchMode} unifiedMode=${unifiedMode} flaggedOnly=${body.classList.contains("flagged-only")}`);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
// Reset the once-flag so next time we hit bottom we log again.
|
|
877
|
-
body._mlScrollEndLogged = false;
|
|
878
|
-
}
|
|
879
|
-
});
|
|
921
|
+
body.addEventListener("scroll", () => { maybeLoadMore(body); });
|
|
880
922
|
}
|
|
881
923
|
// Viewer signals "this row is gone server-side" via mailx-remove-stale.
|
|
882
924
|
// The list owns row lifecycle, so it runs the removal here — filtering
|
|
@@ -1110,7 +1152,7 @@ export async function loadUnifiedInbox(autoSelect = true) {
|
|
|
1110
1152
|
withScrollAnchor(body, renderCached);
|
|
1111
1153
|
else
|
|
1112
1154
|
renderCached();
|
|
1113
|
-
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
|
|
1155
|
+
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered, autoSelect) : null;
|
|
1114
1156
|
if (targetUuid) {
|
|
1115
1157
|
if (autoSelect)
|
|
1116
1158
|
body.scrollTop = savedScroll;
|
|
@@ -1124,7 +1166,7 @@ export async function loadUnifiedInbox(autoSelect = true) {
|
|
|
1124
1166
|
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
1125
1167
|
}
|
|
1126
1168
|
try {
|
|
1127
|
-
const result = await apiGetUnifiedInbox(1, 50, flaggedOnly);
|
|
1169
|
+
const result = await apiGetUnifiedInbox(1, 50, flaggedOnly, dateBasis);
|
|
1128
1170
|
if (myGen !== loadGen)
|
|
1129
1171
|
return; // user moved on; drop stale response
|
|
1130
1172
|
totalMessages = result.total;
|
|
@@ -1147,7 +1189,7 @@ export async function loadUnifiedInbox(autoSelect = true) {
|
|
|
1147
1189
|
// yank them. If they're at the very top, no anchor — new rows
|
|
1148
1190
|
// appear above as usual.
|
|
1149
1191
|
withScrollAnchor(body, () => renderMessages(body, "", result.items, { background: !autoSelect }));
|
|
1150
|
-
const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
|
|
1192
|
+
const targetUuid = remembered ? pickRestoreUid(result.items, remembered, autoSelect) : null;
|
|
1151
1193
|
if (targetUuid) {
|
|
1152
1194
|
// Only restore the saved scrollTop on a first paint (cache miss).
|
|
1153
1195
|
// After a sync-driven re-render, withScrollAnchor has already
|
|
@@ -1228,8 +1270,8 @@ export async function loadSearchResults(query, scope = "all", accountId = "", fo
|
|
|
1228
1270
|
return;
|
|
1229
1271
|
}
|
|
1230
1272
|
const source = scope === "current" && accountId
|
|
1231
|
-
? await apiGetMessages(accountId, folderId, 1, 10000)
|
|
1232
|
-
: await apiGetUnifiedInbox(1, 10000);
|
|
1273
|
+
? await apiGetMessages(accountId, folderId, 1, 10000, false, currentSort, currentSortDir, dateBasis)
|
|
1274
|
+
: await apiGetUnifiedInbox(1, 10000, false, dateBasis);
|
|
1233
1275
|
if (myGen !== loadGen)
|
|
1234
1276
|
return;
|
|
1235
1277
|
const matches = source.items.filter((m) => regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || ""));
|
|
@@ -1338,7 +1380,7 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
1338
1380
|
withScrollAnchor(body, renderCached);
|
|
1339
1381
|
else
|
|
1340
1382
|
renderCached();
|
|
1341
|
-
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
|
|
1383
|
+
const targetUuid = remembered ? pickRestoreUid(cached.items, remembered, autoSelect) : null;
|
|
1342
1384
|
if (focusUid != null) {
|
|
1343
1385
|
// Search-as-find: jump to the specific message the user selected.
|
|
1344
1386
|
requestAnimationFrame(() => {
|
|
@@ -1363,7 +1405,7 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
1363
1405
|
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
1364
1406
|
}
|
|
1365
1407
|
try {
|
|
1366
|
-
const result = await apiGetMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir);
|
|
1408
|
+
const result = await apiGetMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
|
|
1367
1409
|
// Stale-response guard: a newer load* fired while we were
|
|
1368
1410
|
// awaiting; the new view already painted. Drop this result
|
|
1369
1411
|
// silently rather than overwriting.
|
|
@@ -1402,7 +1444,7 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
1402
1444
|
}
|
|
1403
1445
|
}
|
|
1404
1446
|
// Prefer saved position; otherwise default by autoSelect.
|
|
1405
|
-
const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
|
|
1447
|
+
const targetUuid = remembered ? pickRestoreUid(result.items, remembered, autoSelect) : null;
|
|
1406
1448
|
if (focusUid != null) {
|
|
1407
1449
|
// Search-as-find: select the message the user picked from results.
|
|
1408
1450
|
requestAnimationFrame(() => {
|
|
@@ -1437,6 +1479,44 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
1437
1479
|
body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
|
|
1438
1480
|
}
|
|
1439
1481
|
}
|
|
1482
|
+
/** Decide whether the next page should load, and load it. Centralized so the
|
|
1483
|
+
* scroll handler and the post-load re-check share one code path.
|
|
1484
|
+
*
|
|
1485
|
+
* Why a re-check exists (Bob 2026-06-30: "scroll down near the bottom does
|
|
1486
|
+
* nothing till an upward scroll then I can scroll down again"): the browser
|
|
1487
|
+
* fires `scroll` events only while scrollTop actually changes. When the user
|
|
1488
|
+
* reaches the absolute bottom of the loaded rows, scrollTop is pinned at max,
|
|
1489
|
+
* so NO further downward scroll events fire. If the event that crossed
|
|
1490
|
+
* nearBottom arrived while a previous load was still in flight (the old
|
|
1491
|
+
* `if (loading) return` dropped it), the next page was never requested — the
|
|
1492
|
+
* list looked frozen until an upward scroll re-armed the handler. Now every
|
|
1493
|
+
* finished load re-evaluates geometry and chains the next page if the user is
|
|
1494
|
+
* still sitting near the bottom, so reaching the end is self-driving. */
|
|
1495
|
+
function maybeLoadMore(body) {
|
|
1496
|
+
if (loading)
|
|
1497
|
+
return;
|
|
1498
|
+
const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 1000;
|
|
1499
|
+
if (!nearBottom) {
|
|
1500
|
+
// Reset the once-flag so next time we hit bottom we log again.
|
|
1501
|
+
body._mlScrollEndLogged = false;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (currentPage * 50 < totalMessages) {
|
|
1505
|
+
// Prefetch the next page well BEFORE the bottom (1000px lead). The next
|
|
1506
|
+
// 50 rows are a DB query + render; triggering only 200px out meant the
|
|
1507
|
+
// user scrolled INTO the not-yet-loaded gap and saw a pause (Bob
|
|
1508
|
+
// 2026-06-18 "scroll the summary, there is a delay").
|
|
1509
|
+
loadMoreMessages();
|
|
1510
|
+
}
|
|
1511
|
+
else if (!body._mlScrollEndLogged) {
|
|
1512
|
+
// Diagnostic: at bottom but not loading more. Log why so we can see
|
|
1513
|
+
// whether totalMessages is wrong, a filter is on, or we've truly
|
|
1514
|
+
// reached the end. Once per "stuck at bottom" episode.
|
|
1515
|
+
body._mlScrollEndLogged = true;
|
|
1516
|
+
const rows = body.querySelectorAll(".ml-row").length;
|
|
1517
|
+
console.log(` [ml-scroll] reached bottom — currentPage=${currentPage} pageSize=50 loadedRows=${rows} totalMessages=${totalMessages} searchMode=${searchMode} unifiedMode=${unifiedMode} flaggedOnly=${body.classList.contains("flagged-only")}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1440
1520
|
async function loadMoreMessages() {
|
|
1441
1521
|
const body = document.getElementById("ml-body");
|
|
1442
1522
|
if (!body)
|
|
@@ -1457,8 +1537,8 @@ async function loadMoreMessages() {
|
|
|
1457
1537
|
const result = searchMode
|
|
1458
1538
|
? await searchMessages(currentSearchQuery, currentPage)
|
|
1459
1539
|
: unifiedMode
|
|
1460
|
-
? await apiGetUnifiedInbox(currentPage, 50, flaggedOnly)
|
|
1461
|
-
: await apiGetMessages(currentAccountId, currentFolderId, currentPage, 50, flaggedOnly);
|
|
1540
|
+
? await apiGetUnifiedInbox(currentPage, 50, flaggedOnly, dateBasis)
|
|
1541
|
+
: await apiGetMessages(currentAccountId, currentFolderId, currentPage, 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
|
|
1462
1542
|
// Append to state
|
|
1463
1543
|
const current = state.getMessages();
|
|
1464
1544
|
state.setMessages([...current, ...result.items]);
|
|
@@ -1475,6 +1555,13 @@ async function loadMoreMessages() {
|
|
|
1475
1555
|
finally {
|
|
1476
1556
|
loading = false;
|
|
1477
1557
|
}
|
|
1558
|
+
// Re-evaluate: if the user is STILL near the bottom (they kept their finger
|
|
1559
|
+
// pinned at the end, or the page that loaded was shorter than the viewport),
|
|
1560
|
+
// chain the next page. Geometry has settled after the synchronous append
|
|
1561
|
+
// above, so this reads true heights. Terminates when nearBottom goes false
|
|
1562
|
+
// or currentPage*50 >= totalMessages. requestAnimationFrame defers one frame
|
|
1563
|
+
// so layout flushes and we don't recurse on stale heights.
|
|
1564
|
+
requestAnimationFrame(() => maybeLoadMore(body));
|
|
1478
1565
|
}
|
|
1479
1566
|
/** Replace body contents with rendered rows */
|
|
1480
1567
|
function renderMessages(body, accountId, items, opts = {}) {
|
|
@@ -1501,6 +1588,8 @@ function renderMessages(body, accountId, items, opts = {}) {
|
|
|
1501
1588
|
for (const uuid of preserveSel) {
|
|
1502
1589
|
body.querySelector(`.ml-row[data-uuid="${CSS.escape(uuid)}"]`)?.classList.add("selected");
|
|
1503
1590
|
}
|
|
1591
|
+
// Keep the "N selected" pill accurate after a background rebuild.
|
|
1592
|
+
updateBulkBar();
|
|
1504
1593
|
}
|
|
1505
1594
|
// A filter (flagged-only / priority-only) that hides EVERY row makes the
|
|
1506
1595
|
// list look broken — identical to an empty/failed load. Bob 2026-05-27
|
|
@@ -1571,10 +1660,16 @@ function restoreSelection(body, savedUuid) {
|
|
|
1571
1660
|
const accountId = row.dataset.accountId;
|
|
1572
1661
|
const uid = Number(row.dataset.uid);
|
|
1573
1662
|
if (accountId && Number.isFinite(uid)) {
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
|
|
1663
|
+
// FATE-SHARING SAFETY NET (Bob 2026-07-01: "even if there is an
|
|
1664
|
+
// inappropriate switch it must automatically make it visible in the
|
|
1665
|
+
// summary"). If this restore re-binds the SAME message already in the
|
|
1666
|
+
// viewer — the normal case after a sync rebuild — keep the user's
|
|
1667
|
+
// scroll exactly where it is; they may be reading rows above/below and
|
|
1668
|
+
// must not be yanked. But if it targets a DIFFERENT message than the
|
|
1669
|
+
// one currently shown, that IS a real viewer switch: scroll the list to
|
|
1670
|
+
// that row so the summary and the viewer can never silently disagree.
|
|
1671
|
+
const switching = focusedRow?.msg?.uuid !== savedUuid;
|
|
1672
|
+
focusByIdentity(accountId, uid, { scroll: switching });
|
|
1578
1673
|
}
|
|
1579
1674
|
}
|
|
1580
1675
|
/** Show a floating list of all messages in a thread when the pill is clicked.
|
|
@@ -1603,7 +1698,7 @@ export async function showThreadPopup(pillEl, headMsg) {
|
|
|
1603
1698
|
from.textContent = msg.from?.name || msg.from?.address || "?";
|
|
1604
1699
|
const date = document.createElement("span");
|
|
1605
1700
|
date.className = "ml-thread-popup-date";
|
|
1606
|
-
date.textContent = formatDate(msg
|
|
1701
|
+
date.textContent = formatDate(effectiveDate(msg));
|
|
1607
1702
|
const subject = document.createElement("span");
|
|
1608
1703
|
subject.className = "ml-thread-popup-subject";
|
|
1609
1704
|
subject.textContent = msg.subject || "(no subject)";
|
|
@@ -1796,7 +1891,8 @@ class MessageRow {
|
|
|
1796
1891
|
renderStatusIcons(icons, msg);
|
|
1797
1892
|
const dateText = document.createElement("span");
|
|
1798
1893
|
dateText.className = "ml-date-text";
|
|
1799
|
-
dateText.textContent = formatDate(msg
|
|
1894
|
+
dateText.textContent = formatDate(effectiveDate(msg));
|
|
1895
|
+
dateText.title = dateBasis === "received" ? "Received date" : "Sent date";
|
|
1800
1896
|
date.appendChild(icons);
|
|
1801
1897
|
date.appendChild(dateText);
|
|
1802
1898
|
row.appendChild(avatar);
|
|
@@ -2034,6 +2130,20 @@ class MessageRow {
|
|
|
2034
2130
|
if (e.ctrlKey || e.metaKey) {
|
|
2035
2131
|
this.setSelected(!this.isSelected);
|
|
2036
2132
|
lastClickedRow = this.el;
|
|
2133
|
+
// Ctrl/Cmd-click builds a multi-selection — it MUST set
|
|
2134
|
+
// `multi-select-on`, or a background re-render (new mail arriving)
|
|
2135
|
+
// won't preserve the highlights and restoreSelection collapses the
|
|
2136
|
+
// whole selection down to the single focused row (Bob 2026-07-01:
|
|
2137
|
+
// "suddenly deselect all the messages and leave one"). Same root as
|
|
2138
|
+
// the old Ctrl+A gap. Drop the flag again if the toggle emptied the
|
|
2139
|
+
// selection so a later plain click behaves as single-select.
|
|
2140
|
+
if (body) {
|
|
2141
|
+
const n = body.querySelectorAll(".ml-row.selected").length;
|
|
2142
|
+
if (n >= 1)
|
|
2143
|
+
body.classList.add("multi-select-on");
|
|
2144
|
+
else
|
|
2145
|
+
body.classList.remove("multi-select-on");
|
|
2146
|
+
}
|
|
2037
2147
|
}
|
|
2038
2148
|
else {
|
|
2039
2149
|
clearSelection();
|