@bobfrankston/rmfmail 1.2.214 → 1.2.216
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/android-bootstrap.bundle.js +3 -1
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +21 -6
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +38 -7
- package/client/app.js.map +1 -1
- package/client/app.ts +39 -7
- package/client/index.html +1 -1
- package/package.json +5 -5
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-37064 → node_modules.npmglobalize-stash-25568}/.package-lock.json +0 -0
package/client/app.ts
CHANGED
|
@@ -1758,10 +1758,24 @@ async function deleteSelectedMessages(): Promise<void> {
|
|
|
1758
1758
|
}
|
|
1759
1759
|
}
|
|
1760
1760
|
|
|
1761
|
+
/** Serialize Ctrl+Z presses. Each undo is several awaited IPCs plus a
|
|
1762
|
+
* revealMessage + folder reload; firing them concurrently (hold Ctrl and tap Z
|
|
1763
|
+
* three times to walk back three deletions) had them interleaving — two
|
|
1764
|
+
* reveals fighting over the list position, reloads landing out of order. The
|
|
1765
|
+
* stack is LIFO and each queued call pops its own entry when its turn comes,
|
|
1766
|
+
* so N presses undo the N most recent actions, in order. */
|
|
1767
|
+
let undoChain: Promise<void> = Promise.resolve();
|
|
1768
|
+
function queueUndo(): void {
|
|
1769
|
+
undoChain = undoChain.then(() => performUndo()).catch(() => { /* performUndo reports its own failures */ });
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1761
1772
|
async function performUndo(): Promise<void> {
|
|
1762
1773
|
const op = popUndo();
|
|
1763
1774
|
if (!op) return;
|
|
1764
1775
|
const statusSync = document.getElementById("status-sync");
|
|
1776
|
+
// "3 more to undo" — without it, a stack of undone deletions is invisible
|
|
1777
|
+
// and the user can't tell whether pressing Ctrl+Z again will do anything.
|
|
1778
|
+
const remaining = undoStack.length ? ` — ${undoStack.length} more to undo` : "";
|
|
1765
1779
|
try {
|
|
1766
1780
|
if (op.kind === "delete") {
|
|
1767
1781
|
// payload is the full batch (was a single object pre-2026-06-12;
|
|
@@ -1771,9 +1785,9 @@ async function performUndo(): Promise<void> {
|
|
|
1771
1785
|
for (const m of msgs) {
|
|
1772
1786
|
await undeleteMessage(m.accountId, m.uid, m.folderId);
|
|
1773
1787
|
}
|
|
1774
|
-
if (statusSync) statusSync.textContent = msgs.length === 1
|
|
1788
|
+
if (statusSync) statusSync.textContent = (msgs.length === 1
|
|
1775
1789
|
? "Message restored"
|
|
1776
|
-
: `Restored ${msgs.length} messages
|
|
1790
|
+
: `Restored ${msgs.length} messages`) + remaining;
|
|
1777
1791
|
// Position at what was just restored — an undo whose effect lands
|
|
1778
1792
|
// off-screen (or in a folder you've since left) reads as a no-op.
|
|
1779
1793
|
// Reveal the first of the batch; the rest are in the same view.
|
|
@@ -1797,7 +1811,7 @@ async function performUndo(): Promise<void> {
|
|
|
1797
1811
|
if (group.uids.length === 1) await moveMessage(group.accountId, group.uids[0], group.folderId);
|
|
1798
1812
|
else await moveMessages(group.accountId, group.uids, group.folderId);
|
|
1799
1813
|
}
|
|
1800
|
-
if (statusSync) statusSync.textContent = `Undid move of ${messages.length} message${messages.length !== 1 ? "s" : ""}`;
|
|
1814
|
+
if (statusSync) statusSync.textContent = `Undid move of ${messages.length} message${messages.length !== 1 ? "s" : ""}${remaining}`;
|
|
1801
1815
|
// Same positioning rule as undo-delete: show the message back in
|
|
1802
1816
|
// its original folder.
|
|
1803
1817
|
const firstMoved = messages[0];
|
|
@@ -1820,7 +1834,7 @@ async function performUndo(): Promise<void> {
|
|
|
1820
1834
|
setRowFlagged(entry.accountId, entry.uid, entry.prevFlagged);
|
|
1821
1835
|
messageState.updateMessageFlags(entry.accountId, entry.uid, flagsAfter);
|
|
1822
1836
|
}
|
|
1823
|
-
if (statusSync) statusSync.textContent = `Undid flag change on ${op.payload.length} message${op.payload.length !== 1 ? "s" : ""}`;
|
|
1837
|
+
if (statusSync) statusSync.textContent = `Undid flag change on ${op.payload.length} message${op.payload.length !== 1 ? "s" : ""}${remaining}`;
|
|
1824
1838
|
}
|
|
1825
1839
|
reloadCurrentFolder();
|
|
1826
1840
|
} catch (e: any) {
|
|
@@ -2107,7 +2121,25 @@ async function spamSelectedMessages(): Promise<void> {
|
|
|
2107
2121
|
g.folderIds.push(msg.folderId);
|
|
2108
2122
|
byAccount.set(msg.accountId, g);
|
|
2109
2123
|
}
|
|
2110
|
-
|
|
2124
|
+
// Marking spam is just a move to the \Junk folder (service-side
|
|
2125
|
+
// markAsSpamMessages), so its inverse is the move back — the same undo op
|
|
2126
|
+
// a drag-to-folder move records. Pushed BEFORE the IPC, not in its `.then`:
|
|
2127
|
+
// a misfired spam click is exactly the case where the user reaches for
|
|
2128
|
+
// Ctrl+Z immediately, and the local move has already happened by then
|
|
2129
|
+
// (Bob 2026-08-03: "^z should undo send-to-spam"). The move branch of
|
|
2130
|
+
// performUndo restores each message to its own sourceFolderId, so a
|
|
2131
|
+
// selection spanning folders comes back correctly; targetFolderId is only
|
|
2132
|
+
// known once the service answers and the undo doesn't need it.
|
|
2133
|
+
pushUndo({
|
|
2134
|
+
kind: "move",
|
|
2135
|
+
at: Date.now(),
|
|
2136
|
+
payload: {
|
|
2137
|
+
messages: snapshot.map(m => ({ accountId: m.accountId, uid: m.uid, sourceFolderId: m.folderId })),
|
|
2138
|
+
targetAccountId: snapshot[0].accountId,
|
|
2139
|
+
targetFolderId: 0,
|
|
2140
|
+
},
|
|
2141
|
+
});
|
|
2142
|
+
if (statusSync) statusSync.textContent = `Spam: ${snapshot.length} queued — Ctrl+Z to undo`;
|
|
2111
2143
|
for (const [accountId, { uids, folderIds }] of byAccount) {
|
|
2112
2144
|
markAsSpamMessages(accountId, uids, folderIds)
|
|
2113
2145
|
.then(result => {
|
|
@@ -3899,7 +3931,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
3899
3931
|
const composeOpen = !!document.querySelector(".compose-overlay");
|
|
3900
3932
|
if (!inEditable && !composeOpen && undoStack.length > 0) {
|
|
3901
3933
|
e.preventDefault();
|
|
3902
|
-
|
|
3934
|
+
queueUndo();
|
|
3903
3935
|
}
|
|
3904
3936
|
}
|
|
3905
3937
|
// F5 = Sync
|
|
@@ -4113,7 +4145,7 @@ function openShortcutsDialog(): void {
|
|
|
4113
4145
|
["Forward", "Ctrl+F"],
|
|
4114
4146
|
["Sync", "F5"],
|
|
4115
4147
|
["Delete selected", "Del or Ctrl+D"],
|
|
4116
|
-
["Undo
|
|
4148
|
+
["Undo delete / move / spam / flag (repeat to go back further)", "Ctrl+Z"],
|
|
4117
4149
|
["Toggle read/unread", "R"],
|
|
4118
4150
|
["Toggle flag (★)", "(⚑ button in viewer)"],
|
|
4119
4151
|
["Select all visible", "Ctrl+A"],
|
package/client/index.html
CHANGED
|
@@ -779,7 +779,7 @@
|
|
|
779
779
|
</div>
|
|
780
780
|
</div>
|
|
781
781
|
<button class="tb-btn" id="btn-tb-delete" title="Delete selected message (Del)">🗑</button>
|
|
782
|
-
<button class="tb-btn" id="btn-tb-spam" title="Mark as spam — move to the configured Junk folder">⚠</button>
|
|
782
|
+
<button class="tb-btn" id="btn-tb-spam" title="Mark as spam — move to the configured Junk folder (Ctrl+Z to undo)">⚠</button>
|
|
783
783
|
<button class="tb-btn" id="btn-tb-flag" title="Flag / unflag selected message(s) — Ctrl+Z to undo">★</button>
|
|
784
784
|
<span id="app-version" class="app-version"></span>
|
|
785
785
|
<span class="tb-hint" title="Cycle pane focus: F6 = forward (folders → list → viewer), Shift+F6 = back. From the preview, ⇧F6 lands on the message-list row that drove it.">⇧F6 ← list</span>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/rmfmail",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.216",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"postinstall": "node bin/postinstall.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
37
|
+
"@bobfrankston/iflow-direct": "^0.1.65",
|
|
38
38
|
"@bobfrankston/mailx-host": "^0.1.14",
|
|
39
39
|
"@bobfrankston/mailx-imap": "^0.1.140",
|
|
40
40
|
"@bobfrankston/mailx-store-web": "^0.1.69",
|
|
41
41
|
"@bobfrankston/mailx-sync": "^0.1.29",
|
|
42
42
|
"@bobfrankston/miscinfo": "^1.0.15",
|
|
43
|
-
"@bobfrankston/msger": "^0.1.
|
|
43
|
+
"@bobfrankston/msger": "^0.1.421",
|
|
44
44
|
"@bobfrankston/node-tcp-transport": "^0.1.10",
|
|
45
45
|
"@bobfrankston/oauthsupport": "^1.0.34",
|
|
46
46
|
"@bobfrankston/rmf-tiny": "^0.1.43",
|
|
@@ -114,13 +114,13 @@
|
|
|
114
114
|
},
|
|
115
115
|
".transformedSnapshot": {
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
117
|
+
"@bobfrankston/iflow-direct": "^0.1.65",
|
|
118
118
|
"@bobfrankston/mailx-host": "^0.1.14",
|
|
119
119
|
"@bobfrankston/mailx-imap": "^0.1.140",
|
|
120
120
|
"@bobfrankston/mailx-store-web": "^0.1.69",
|
|
121
121
|
"@bobfrankston/mailx-sync": "^0.1.29",
|
|
122
122
|
"@bobfrankston/miscinfo": "^1.0.15",
|
|
123
|
-
"@bobfrankston/msger": "^0.1.
|
|
123
|
+
"@bobfrankston/msger": "^0.1.421",
|
|
124
124
|
"@bobfrankston/node-tcp-transport": "^0.1.10",
|
|
125
125
|
"@bobfrankston/oauthsupport": "^1.0.34",
|
|
126
126
|
"@bobfrankston/rmf-tiny": "^0.1.43",
|