@bobfrankston/mailx 1.0.289 → 1.0.291

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/bin/mailx.js CHANGED
@@ -939,6 +939,9 @@ async function main() {
939
939
  imapManager.on("syncComplete", (accountId) => {
940
940
  handle.send({ _event: "syncComplete", type: "syncComplete", accountId });
941
941
  });
942
+ imapManager.on("syncActionFailed", (accountId, action, uid, error) => {
943
+ handle.send({ _event: "syncActionFailed", type: "syncActionFailed", accountId, action, uid, error });
944
+ });
942
945
  // Cloud-write/read failures from mailx-settings → push to UI as a banner so
943
946
  // silent fall-back-to-local can no longer swallow Drive errors.
944
947
  const { onCloudError } = await import("@bobfrankston/mailx-settings");
package/client/app.js CHANGED
@@ -608,12 +608,12 @@ async function deleteSelectedMessages() {
608
608
  if (selected.length === 1) {
609
609
  lastDeleted = { ...selected[0], subject: "" };
610
610
  if (statusSync)
611
- statusSync.textContent = `Deleted 1 message — Ctrl+Z to undo`;
611
+ statusSync.textContent = `Trashed 1 message (syncing) — Ctrl+Z to undo`;
612
612
  }
613
613
  else {
614
614
  lastDeleted = null;
615
615
  if (statusSync)
616
- statusSync.textContent = `Deleted ${selected.length} messages`;
616
+ statusSync.textContent = `Trashed ${selected.length} messages (syncing)`;
617
617
  }
618
618
  if (undoTimeout)
619
619
  clearTimeout(undoTimeout);
@@ -711,7 +711,7 @@ async function spamSelectedMessages() {
711
711
  await markAsSpamMessages(accountId, uids);
712
712
  }
713
713
  if (statusSync)
714
- statusSync.textContent = `Marked ${selected.length} as spam`;
714
+ statusSync.textContent = `Spam: ${selected.length} queued — pending server sync`;
715
715
  messageState.removeMessages(selected);
716
716
  }
717
717
  catch (e) {
@@ -1019,7 +1019,11 @@ onWsEvent((event) => {
1019
1019
  }
1020
1020
  break;
1021
1021
  case "folderCountsChanged": {
1022
- // Update folder badges + silently refresh message list (preserves selection and viewer)
1022
+ // Update folder badges + full tree refresh (not just counts) so
1023
+ // newly-synced folders appear. Without this, the first sync after
1024
+ // setup creates DB folders but the tree never picks them up because
1025
+ // syncComplete fires during page reload and gets lost.
1026
+ refreshFolderTree();
1023
1027
  updateFolderCounts();
1024
1028
  updateNewMessageCount();
1025
1029
  // Debounced silent reload — preserves scroll position, selection, and viewer
@@ -1040,6 +1044,14 @@ onWsEvent((event) => {
1040
1044
  statusSync.textContent = `Synced ${new Date().toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false })}`;
1041
1045
  break;
1042
1046
  }
1047
+ case "syncActionFailed": {
1048
+ // Surface sync failures (move/delete/flag not applied on server)
1049
+ // so the user knows local-first actions haven't propagated yet.
1050
+ const action = event.action === "move" ? "Move" : event.action === "delete" ? "Delete" : event.action;
1051
+ if (statusSync)
1052
+ statusSync.textContent = `Sync failed: ${action} — ${event.error}`;
1053
+ break;
1054
+ }
1043
1055
  case "reload":
1044
1056
  location.reload();
1045
1057
  break;
@@ -482,22 +482,34 @@ async function loadFolderTree(container) {
482
482
  "aol.com": "Use an app password: AOL Settings → Account Security → Generate app password",
483
483
  "icloud.com": "Use an app-specific password: appleid.apple.com → Sign-In and Security → App-Specific Passwords",
484
484
  };
485
+ let oauthAutoFired = false;
485
486
  emailInput?.addEventListener("input", () => {
486
487
  const email = emailInput.value.trim();
487
488
  const domain = email.split("@")[1]?.toLowerCase() || "";
488
489
  const hasAt = email.includes("@") && domain.length > 0;
489
490
  const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
490
- // Progressive reveal: email first, then name + submit,
491
- // password only for non-OAuth providers.
491
+ const isGmailLike = ["gmail.com", "googlemail.com"].includes(domain);
492
+ // OAuth providers: auto-fire setup immediately once domain
493
+ // is recognized — don't show name/password (name is auto-
494
+ // detected from Google profile, no password needed). This
495
+ // eliminates the "form flash" where fields briefly appear
496
+ // before the page reloads.
497
+ if (hasAt && isOAuth && !oauthAutoFired && !setupTriggered) {
498
+ oauthAutoFired = true;
499
+ statusEl.textContent = `Connecting to ${isGmailLike ? "Gmail" : "Outlook"}...`;
500
+ trySetup();
501
+ return;
502
+ }
503
+ // Non-OAuth: progressive reveal of name + password + submit.
492
504
  const nameRow = document.getElementById("setup-name-row");
493
505
  const pwRow = document.getElementById("setup-password-row");
494
506
  const submitBtn = document.getElementById("setup-submit");
495
507
  if (nameRow)
496
- nameRow.style.display = hasAt ? "block" : "none";
508
+ nameRow.style.display = hasAt && !isOAuth ? "block" : "none";
497
509
  if (pwRow)
498
510
  pwRow.style.display = hasAt && !isOAuth ? "block" : "none";
499
511
  if (submitBtn)
500
- submitBtn.style.display = hasAt ? "block" : "none";
512
+ submitBtn.style.display = hasAt && !isOAuth ? "block" : "none";
501
513
  const helpEl = document.getElementById("setup-app-password-help");
502
514
  if (helpEl) {
503
515
  const help = APP_PASSWORD_HELP[domain];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.289",
3
+ "version": "1.0.291",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -22,6 +22,7 @@ export interface ImapManagerEvents {
22
22
  /** Fired after a message body has been written to the local store — lets
23
23
  * the UI flip a row's "not-downloaded" indicator without re-rendering. */
24
24
  bodyCached: (accountId: string, uid: number) => void;
25
+ syncActionFailed: (accountId: string, action: string, uid: number, error: string) => void;
25
26
  }
26
27
  export declare class ImapManager extends EventEmitter {
27
28
  private configs;
@@ -2078,9 +2078,11 @@ export class ImapManager extends EventEmitter {
2078
2078
  catch (e) {
2079
2079
  console.error(` [sync] Failed action ${action.action} UID ${action.uid}: ${e.message}`);
2080
2080
  this.db.failSyncAction(action.id, e.message);
2081
+ this.emit("syncActionFailed", accountId, action.action, action.uid, e.message);
2081
2082
  if (action.attempts >= 5) {
2082
2083
  console.error(` [sync] Giving up on action ${action.id} after 5 attempts`);
2083
2084
  this.db.completeSyncAction(action.id);
2085
+ this.emit("syncActionFailed", accountId, action.action, action.uid, `Gave up after 5 attempts: ${e.message}`);
2084
2086
  }
2085
2087
  }
2086
2088
  }