@bobfrankston/mailx 1.0.408 → 1.0.411

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 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, getSelectedMessages, markBodiesCached } from "./components/message-list.js";
6
+ import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached } from "./components/message-list.js";
7
7
  import { showMessage, getCurrentMessage, initViewer } 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";
@@ -1184,11 +1184,15 @@ let reloadDebounceTimer = null;
1184
1184
  searchInput?.addEventListener("input", () => {
1185
1185
  clearTimeout(searchTimeout);
1186
1186
  if (searchInput.value.trim() === "") {
1187
- // Cleared — reset immediately, no debounce
1187
+ // Cleared — reset immediately, no debounce. Must exit search mode
1188
+ // first; otherwise reloadCurrentFolder() sees searchMode=true and
1189
+ // re-runs the stale query (user-reported regression 2026-04-24).
1190
+ clearSearchMode();
1188
1191
  const body = document.getElementById("ml-body");
1189
1192
  if (body)
1190
1193
  body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
1191
1194
  reloadCurrentFolder();
1195
+ setTitle("mailx");
1192
1196
  }
1193
1197
  else {
1194
1198
  searchTimeout = setTimeout(() => doSearch(false), 300);
@@ -1201,11 +1205,13 @@ searchInput?.addEventListener("keydown", (e) => {
1201
1205
  }
1202
1206
  if (e.key === "Escape") {
1203
1207
  searchInput.value = "";
1208
+ clearSearchMode();
1204
1209
  // Clear any client-side filters
1205
1210
  const body = document.getElementById("ml-body");
1206
1211
  if (body)
1207
1212
  body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
1208
1213
  reloadCurrentFolder();
1214
+ setTitle("mailx");
1209
1215
  }
1210
1216
  });
1211
1217
  // Message state handles move/delete — no manual event listener needed
@@ -330,6 +330,13 @@ export function reloadCurrentFolder() {
330
330
  loadMessages(currentAccountId, currentFolderId, 1, "", false);
331
331
  }
332
332
  }
333
+ /** Exit search mode without triggering a reload — caller decides what to load
334
+ * next. Used by the search-input "cleared to empty" handler so the next
335
+ * reloadCurrentFolder() call doesn't re-run the stale search query. */
336
+ export function clearSearchMode() {
337
+ searchMode = false;
338
+ currentSearchQuery = "";
339
+ }
333
340
  /** Load unified inbox (all accounts) */
334
341
  export async function loadUnifiedInbox(autoSelect = true) {
335
342
  unifiedMode = true;
@@ -373,8 +373,8 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
373
373
  if (anyUrl) {
374
374
  unsubBtn.hidden = false;
375
375
  unsubBtn.textContent = oneClick && httpUrl ? "Unsubscribe (1-click)" : "Unsubscribe";
376
- unsubBtn.title = anyUrl;
377
- unsubBtn.href = "#";
376
+ unsubBtn.removeAttribute("title");
377
+ unsubBtn.href = httpUrl || mailUrl || "#";
378
378
  unsubBtn.onclick = async (e) => {
379
379
  e.preventDefault();
380
380
  const status = document.getElementById("status-sync");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.408",
3
+ "version": "1.0.411",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -278,11 +278,12 @@ export class MailxService {
278
278
  parseListUnsubscribe(parsed2.headers));
279
279
  listUnsubscribe = listUnsubscribeHttp || listUnsubscribeMail;
280
280
  }
281
- // EML path: read the real on-disk path from `envelope.bodyPath` (DB
282
- // is authoritative since v1.0.361 files are opaque UUIDs, not the
283
- // old {folderId}/{uid}.eml layout). Synthesizing the legacy path
284
- // here showed users a path that doesn't exist.
285
- const emlPath = envelope.bodyPath || "";
281
+ // EML path: re-read the row after the fetch `fetchMessageBody`
282
+ // writes the body to disk and updates `body_path` on success, but the
283
+ // `envelope` snapshot above pre-dates that write, so trusting it
284
+ // hides the Source button on every just-opened message.
285
+ const refreshed = this.db.getMessageByUid(accountId, uid, folderId);
286
+ const emlPath = refreshed?.bodyPath || envelope.bodyPath || "";
286
287
  return {
287
288
  ...envelope, bodyHtml, bodyText, hasRemoteContent, remoteAllowed: allowRemote,
288
289
  attachments, emlPath, deliveredTo, returnPath,