@bobfrankston/mailx 1.0.119 → 1.0.120

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
@@ -541,12 +541,26 @@ onWsEvent((event) => {
541
541
  startupStatus.textContent = "Loading accounts...";
542
542
  // Don't refresh folder tree on connect — it's already loaded by initFolderTree
543
543
  break;
544
- case "syncProgress":
544
+ case "syncProgress": {
545
545
  if (statusSync)
546
- statusSync.textContent = `Syncing ${event.accountId}: ${event.phase} ${event.progress}%`;
546
+ statusSync.textContent = `Syncing ${event.accountId}: ${event.phase} ${event.progress || 0}%`;
547
547
  if (startupStatus)
548
548
  startupStatus.textContent = `Syncing ${event.accountId}: ${event.phase}`;
549
+ // Mark syncing folder in tree
550
+ const syncPath = event.phase?.startsWith("sync:") ? event.phase.slice(5) : null;
551
+ // Clear previous syncing markers for this account
552
+ document.querySelectorAll(`.ft-folder.ft-syncing[data-account-id="${event.accountId}"]`).forEach(el => el.classList.remove("ft-syncing"));
553
+ if (syncPath) {
554
+ const folderEl = document.querySelector(`.ft-folder[data-account-id="${event.accountId}"][data-folder-path="${CSS.escape(syncPath)}"]`);
555
+ if (folderEl) {
556
+ if (event.progress < 100)
557
+ folderEl.classList.add("ft-syncing");
558
+ else
559
+ folderEl.classList.remove("ft-syncing");
560
+ }
561
+ }
549
562
  break;
563
+ }
550
564
  case "folderCountsChanged": {
551
565
  refreshFolderTree();
552
566
  updateNewMessageCount();
@@ -703,6 +717,7 @@ const optTwoLine = document.getElementById("opt-two-line");
703
717
  const optPreview = document.getElementById("opt-preview");
704
718
  const optSnippet = document.getElementById("opt-snippet");
705
719
  const optFlagged = document.getElementById("opt-flagged");
720
+ const optFolderCounts = document.getElementById("opt-folder-counts");
706
721
  // Toggle dropdown
707
722
  viewBtn?.addEventListener("click", (e) => {
708
723
  e.stopPropagation();
@@ -720,6 +735,7 @@ const savedTwoLine = localStorage.getItem("mailx-two-line") === "true";
720
735
  const savedPreview = localStorage.getItem("mailx-preview") !== "false"; // default true
721
736
  const savedSnippet = localStorage.getItem("mailx-snippet") !== "false"; // default true
722
737
  const savedFlagged = localStorage.getItem("mailx-flagged") === "true";
738
+ const savedFolderCounts = localStorage.getItem("mailx-folder-counts") === "true";
723
739
  if (optTwoLine)
724
740
  optTwoLine.checked = savedTwoLine;
725
741
  if (optPreview)
@@ -728,6 +744,8 @@ if (optSnippet)
728
744
  optSnippet.checked = savedSnippet;
729
745
  if (optFlagged)
730
746
  optFlagged.checked = savedFlagged;
747
+ if (optFolderCounts)
748
+ optFolderCounts.checked = savedFolderCounts;
731
749
  if (savedTwoLine)
732
750
  document.getElementById("message-list")?.classList.add("two-line");
733
751
  if (!savedPreview)
@@ -736,6 +754,8 @@ if (!savedSnippet)
736
754
  document.getElementById("message-list")?.classList.add("no-snippets");
737
755
  if (savedFlagged)
738
756
  document.getElementById("ml-body")?.classList.add("flagged-only");
757
+ if (savedFolderCounts)
758
+ document.getElementById("folder-tree")?.classList.add("show-folder-counts");
739
759
  // Two-line toggle
740
760
  optTwoLine?.addEventListener("change", () => {
741
761
  const list = document.getElementById("message-list");
@@ -780,6 +800,17 @@ optFlagged?.addEventListener("change", () => {
780
800
  }
781
801
  localStorage.setItem("mailx-flagged", String(optFlagged.checked));
782
802
  });
803
+ // Folder counts toggle
804
+ optFolderCounts?.addEventListener("change", () => {
805
+ const tree = document.getElementById("folder-tree");
806
+ if (optFolderCounts.checked) {
807
+ tree?.classList.add("show-folder-counts");
808
+ }
809
+ else {
810
+ tree?.classList.remove("show-folder-counts");
811
+ }
812
+ localStorage.setItem("mailx-folder-counts", String(optFolderCounts.checked));
813
+ });
783
814
  // ── Settings menu ──
784
815
  const settingsBtn = document.getElementById("btn-settings");
785
816
  const settingsDropdown = document.getElementById("settings-dropdown");
@@ -100,6 +100,7 @@ function renderNode(node, container, depth) {
100
100
  folderEl.className = "ft-folder";
101
101
  folderEl.dataset.accountId = node.accountId;
102
102
  folderEl.dataset.folderId = String(node.id);
103
+ folderEl.dataset.folderPath = node.path;
103
104
  folderEl.style.paddingLeft = `${depth * 16 + 8}px`;
104
105
  // Expand/collapse toggle
105
106
  const toggle = document.createElement("span");
@@ -139,6 +140,13 @@ function renderNode(node, container, depth) {
139
140
  badge.textContent = String(node.unreadCount);
140
141
  folderEl.appendChild(badge);
141
142
  }
143
+ // Total count (shown when View > Folder counts is checked)
144
+ if (node.totalCount > 0) {
145
+ const total = document.createElement("span");
146
+ total.className = "ft-total-count";
147
+ total.textContent = String(node.totalCount);
148
+ folderEl.appendChild(total);
149
+ }
142
150
  folderEl.addEventListener("click", () => {
143
151
  if (node.id === -1) {
144
152
  // Virtual parent — toggle expand instead of selecting
package/client/index.html CHANGED
@@ -26,6 +26,7 @@
26
26
  <label class="tb-menu-item"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
27
27
  <label class="tb-menu-item"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
28
28
  <label class="tb-menu-item"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
29
+ <label class="tb-menu-item"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
29
30
  </div>
30
31
  </div>
31
32
  <div class="tb-menu" id="settings-menu">
@@ -259,6 +259,31 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
259
259
  outline-offset: -2px;
260
260
  }
261
261
 
262
+ .ft-folder.ft-syncing .ft-folder-name::after {
263
+ content: " \21BB"; /* ↻ clockwise arrow */
264
+ color: var(--color-accent);
265
+ animation: ft-spin 1s linear infinite;
266
+ display: inline-block;
267
+ }
268
+
269
+ @keyframes ft-spin {
270
+ from { transform: rotate(0deg); }
271
+ to { transform: rotate(360deg); }
272
+ }
273
+
274
+ .ft-total-count {
275
+ display: none;
276
+ margin-left: auto;
277
+ padding: 0 6px;
278
+ font-size: 0.75rem;
279
+ color: var(--color-text-muted);
280
+ opacity: 0.7;
281
+ }
282
+
283
+ .show-folder-counts .ft-total-count {
284
+ display: inline;
285
+ }
286
+
262
287
  .ml-row.dragging {
263
288
  opacity: 0.5;
264
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.119",
3
+ "version": "1.0.120",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,7 +20,7 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.47",
23
+ "@bobfrankston/iflow": "^1.0.48",
24
24
  "@bobfrankston/miscinfo": "^1.0.7",
25
25
  "@bobfrankston/oauthsupport": "^1.0.20",
26
26
  "@bobfrankston/rust-builder": "^0.1.3",
@@ -688,6 +688,7 @@ export class ImapManager extends EventEmitter {
688
688
  client = this.createClient(accountId);
689
689
  const count = await client.getMessagesCount("INBOX");
690
690
  await client.logout();
691
+ this.trackLogout(accountId);
691
692
  client = null;
692
693
  const prev = this.lastInboxCounts.get(accountId) ?? count;
693
694
  this.lastInboxCounts.set(accountId, count);
@@ -696,6 +697,7 @@ export class ImapManager extends EventEmitter {
696
697
  client = this.createClient(accountId);
697
698
  await this.syncFolder(accountId, inbox.id, client);
698
699
  await client.logout();
700
+ this.trackLogout(accountId);
699
701
  client = null;
700
702
  }
701
703
  }
@@ -703,11 +705,13 @@ export class ImapManager extends EventEmitter {
703
705
  // Lightweight check — silently ignore errors
704
706
  }
705
707
  finally {
706
- if (client)
708
+ if (client) {
707
709
  try {
708
710
  await client.logout();
709
711
  }
710
712
  catch { /* ignore */ }
713
+ this.trackLogout(accountId);
714
+ }
711
715
  }
712
716
  }
713
717
  }
@@ -39,12 +39,21 @@ export class MailxService {
39
39
  }
40
40
  // ── Accounts ──
41
41
  getAccounts() {
42
- const accounts = this.db.getAccounts();
42
+ const dbAccounts = this.db.getAccounts();
43
43
  const settings = loadSettings();
44
- return accounts.map(a => {
45
- const cfg = settings.accounts.find(s => s.id === a.id);
46
- return { ...a, label: cfg?.label, defaultSend: cfg?.defaultSend || false };
47
- });
44
+ // Order by settings (accounts.jsonc is the source of truth for order)
45
+ const ordered = [];
46
+ for (const cfg of settings.accounts) {
47
+ const a = dbAccounts.find(d => d.id === cfg.id);
48
+ if (a)
49
+ ordered.push({ ...a, label: cfg.label, defaultSend: cfg.defaultSend || false });
50
+ }
51
+ // Append any DB accounts not in settings
52
+ for (const a of dbAccounts) {
53
+ if (!ordered.find((o) => o.id === a.id))
54
+ ordered.push(a);
55
+ }
56
+ return ordered;
48
57
  }
49
58
  // ── Folders ──
50
59
  getFolders(accountId) {