@bobfrankston/mailx 1.0.132 → 1.0.133
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 +14 -2
- package/client/components/message-list.js +18 -5
- package/package.json +1 -1
- package/packages/mailx-imap/index.js +14 -2
package/client/app.js
CHANGED
|
@@ -428,6 +428,7 @@ function doSearch(immediate = false) {
|
|
|
428
428
|
// Track current folder for scoped search
|
|
429
429
|
let currentAccountId = "";
|
|
430
430
|
let currentFolderId = 0;
|
|
431
|
+
let reloadDebounceTimer = null;
|
|
431
432
|
searchInput?.addEventListener("input", () => {
|
|
432
433
|
clearTimeout(searchTimeout);
|
|
433
434
|
searchTimeout = setTimeout(() => doSearch(false), 300);
|
|
@@ -585,8 +586,19 @@ onWsEvent((event) => {
|
|
|
585
586
|
// Incremental count update — no DOM rebuild, no jitter
|
|
586
587
|
updateFolderCounts();
|
|
587
588
|
updateNewMessageCount();
|
|
588
|
-
//
|
|
589
|
-
|
|
589
|
+
// Only reload message list if the synced account is the one we're viewing
|
|
590
|
+
// (or unified inbox which shows all accounts). Debounce to avoid rapid reloads
|
|
591
|
+
// during first sync which emits per-batch.
|
|
592
|
+
const syncedAccount = event.accountId;
|
|
593
|
+
const viewingThis = !currentAccountId || currentAccountId === syncedAccount;
|
|
594
|
+
if (viewingThis) {
|
|
595
|
+
if (reloadDebounceTimer)
|
|
596
|
+
clearTimeout(reloadDebounceTimer);
|
|
597
|
+
reloadDebounceTimer = setTimeout(() => {
|
|
598
|
+
reloadDebounceTimer = null;
|
|
599
|
+
reloadCurrentFolder();
|
|
600
|
+
}, 500);
|
|
601
|
+
}
|
|
590
602
|
// Sync finished — re-enable sync button
|
|
591
603
|
const syncBtn = document.getElementById("btn-sync");
|
|
592
604
|
if (syncBtn) {
|
|
@@ -94,7 +94,10 @@ export async function loadUnifiedInbox(autoSelect = true) {
|
|
|
94
94
|
return;
|
|
95
95
|
const savedScroll = !autoSelect ? body.scrollTop : 0;
|
|
96
96
|
const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
|
|
97
|
-
|
|
97
|
+
// Only show loading indicator on fresh navigation, not reloads
|
|
98
|
+
if (autoSelect) {
|
|
99
|
+
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
100
|
+
}
|
|
98
101
|
try {
|
|
99
102
|
const result = await getUnifiedInbox(1);
|
|
100
103
|
totalMessages = result.total;
|
|
@@ -103,8 +106,13 @@ export async function loadUnifiedInbox(autoSelect = true) {
|
|
|
103
106
|
clearViewer();
|
|
104
107
|
return;
|
|
105
108
|
}
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
// Build new rows into a fragment, then swap atomically (no flash)
|
|
110
|
+
const fragment = document.createDocumentFragment();
|
|
111
|
+
const tempDiv = document.createElement("div");
|
|
112
|
+
appendMessages(tempDiv, "", result.items);
|
|
113
|
+
while (tempDiv.firstChild)
|
|
114
|
+
fragment.appendChild(tempDiv.firstChild);
|
|
115
|
+
body.replaceChildren(fragment);
|
|
108
116
|
if (autoSelect) {
|
|
109
117
|
const firstRow = body.querySelector(".ml-row");
|
|
110
118
|
if (firstRow)
|
|
@@ -209,8 +217,13 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
209
217
|
clearViewer();
|
|
210
218
|
return;
|
|
211
219
|
}
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
// Build new rows into a fragment, then swap atomically (no flash)
|
|
221
|
+
const fragment = document.createDocumentFragment();
|
|
222
|
+
const tempDiv = document.createElement("div");
|
|
223
|
+
appendMessages(tempDiv, accountId, result.items);
|
|
224
|
+
while (tempDiv.firstChild)
|
|
225
|
+
fragment.appendChild(tempDiv.firstChild);
|
|
226
|
+
body.replaceChildren(fragment);
|
|
214
227
|
if (autoSelect) {
|
|
215
228
|
// Explicit folder navigation — select first message
|
|
216
229
|
const firstRow = body.querySelector(".ml-row");
|
package/package.json
CHANGED
|
@@ -866,7 +866,11 @@ export class ImapManager extends EventEmitter {
|
|
|
866
866
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
867
867
|
try {
|
|
868
868
|
const client = this.getFetchClient(accountId);
|
|
869
|
-
|
|
869
|
+
// 30s timeout — prevents hanging on stale connections
|
|
870
|
+
const msg = await Promise.race([
|
|
871
|
+
client.fetchMessageByUid(folder.path, uid, { source: true }),
|
|
872
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Body fetch timeout (30s)")), 30000))
|
|
873
|
+
]);
|
|
870
874
|
if (!msg?.source)
|
|
871
875
|
return null;
|
|
872
876
|
const raw = Buffer.from(msg.source, "utf-8");
|
|
@@ -876,8 +880,16 @@ export class ImapManager extends EventEmitter {
|
|
|
876
880
|
return raw;
|
|
877
881
|
}
|
|
878
882
|
catch (e) {
|
|
879
|
-
console.error(` Body fetch error (${accountId}/${uid}): ${e.message}`);
|
|
883
|
+
console.error(` Body fetch error (${accountId}/${uid} attempt ${attempt + 1}): ${e.message}`);
|
|
884
|
+
// Kill stale client so retry creates a fresh connection
|
|
885
|
+
const stale = this.fetchClients.get(accountId);
|
|
880
886
|
this.fetchClients.delete(accountId);
|
|
887
|
+
if (stale) {
|
|
888
|
+
try {
|
|
889
|
+
await stale.logout();
|
|
890
|
+
}
|
|
891
|
+
catch { /* ignore */ }
|
|
892
|
+
}
|
|
881
893
|
if (attempt === 1)
|
|
882
894
|
return null;
|
|
883
895
|
// Retry with fresh client
|