@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.
|
|
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.
|
|
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
|
|
42
|
+
const dbAccounts = this.db.getAccounts();
|
|
43
43
|
const settings = loadSettings();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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) {
|