@bobfrankston/rmfmail 1.2.215 → 1.2.217
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.bundle.js +26 -11
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +49 -13
- package/client/app.js.map +1 -1
- package/client/app.ts +49 -11
- package/client/compose/compose.bundle.js +2 -1
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/index.html +1 -1
- package/client/lib/api-client.js +7 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +7 -1
- package/package.json +5 -5
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-60680 → node_modules.npmglobalize-stash-36516}/.package-lock.json +0 -0
package/client/app.js
CHANGED
|
@@ -1790,11 +1790,24 @@ async function deleteSelectedMessages() {
|
|
|
1790
1790
|
});
|
|
1791
1791
|
}
|
|
1792
1792
|
}
|
|
1793
|
+
/** Serialize Ctrl+Z presses. Each undo is several awaited IPCs plus a
|
|
1794
|
+
* revealMessage + folder reload; firing them concurrently (hold Ctrl and tap Z
|
|
1795
|
+
* three times to walk back three deletions) had them interleaving — two
|
|
1796
|
+
* reveals fighting over the list position, reloads landing out of order. The
|
|
1797
|
+
* stack is LIFO and each queued call pops its own entry when its turn comes,
|
|
1798
|
+
* so N presses undo the N most recent actions, in order. */
|
|
1799
|
+
let undoChain = Promise.resolve();
|
|
1800
|
+
function queueUndo() {
|
|
1801
|
+
undoChain = undoChain.then(() => performUndo()).catch(() => { });
|
|
1802
|
+
}
|
|
1793
1803
|
async function performUndo() {
|
|
1794
1804
|
const op = popUndo();
|
|
1795
1805
|
if (!op)
|
|
1796
1806
|
return;
|
|
1797
1807
|
const statusSync = document.getElementById("status-sync");
|
|
1808
|
+
// "3 more to undo" — without it, a stack of undone deletions is invisible
|
|
1809
|
+
// and the user can't tell whether pressing Ctrl+Z again will do anything.
|
|
1810
|
+
const remaining = undoStack.length ? ` — ${undoStack.length} more to undo` : "";
|
|
1798
1811
|
try {
|
|
1799
1812
|
if (op.kind === "delete") {
|
|
1800
1813
|
// payload is the full batch (was a single object pre-2026-06-12;
|
|
@@ -1805,9 +1818,9 @@ async function performUndo() {
|
|
|
1805
1818
|
await undeleteMessage(m.accountId, m.uid, m.folderId);
|
|
1806
1819
|
}
|
|
1807
1820
|
if (statusSync)
|
|
1808
|
-
statusSync.textContent = msgs.length === 1
|
|
1821
|
+
statusSync.textContent = (msgs.length === 1
|
|
1809
1822
|
? "Message restored"
|
|
1810
|
-
: `Restored ${msgs.length} messages
|
|
1823
|
+
: `Restored ${msgs.length} messages`) + remaining;
|
|
1811
1824
|
// Position at what was just restored — an undo whose effect lands
|
|
1812
1825
|
// off-screen (or in a folder you've since left) reads as a no-op.
|
|
1813
1826
|
// Reveal the first of the batch; the rest are in the same view.
|
|
@@ -1820,7 +1833,7 @@ async function performUndo() {
|
|
|
1820
1833
|
}
|
|
1821
1834
|
}
|
|
1822
1835
|
else if (op.kind === "move") {
|
|
1823
|
-
const { messages } = op.payload;
|
|
1836
|
+
const { messages, targetFolderId } = op.payload;
|
|
1824
1837
|
const byDest = new Map();
|
|
1825
1838
|
for (const m of messages) {
|
|
1826
1839
|
const key = `${m.accountId}:${m.sourceFolderId}`;
|
|
@@ -1828,15 +1841,18 @@ async function performUndo() {
|
|
|
1828
1841
|
byDest.set(key, { accountId: m.accountId, folderId: m.sourceFolderId, uids: [] });
|
|
1829
1842
|
byDest.get(key).uids.push(m.uid);
|
|
1830
1843
|
}
|
|
1831
|
-
const { moveMessages
|
|
1844
|
+
const { moveMessages } = await import("./lib/api-client.js");
|
|
1832
1845
|
for (const group of byDest.values()) {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1846
|
+
// The messages are sitting in targetFolderId right now, so that
|
|
1847
|
+
// is their SOURCE for the reverse move — pass it explicitly.
|
|
1848
|
+
// Without it the service resolves each row by (account, uid)
|
|
1849
|
+
// alone and a same-numbered uid in another folder can be moved
|
|
1850
|
+
// instead (the flag-clear / spam-move wrong-folder class).
|
|
1851
|
+
const currentFolderIds = targetFolderId ? group.uids.map(() => targetFolderId) : undefined;
|
|
1852
|
+
await moveMessages(group.accountId, group.uids, group.folderId, currentFolderIds);
|
|
1837
1853
|
}
|
|
1838
1854
|
if (statusSync)
|
|
1839
|
-
statusSync.textContent = `Undid move of ${messages.length} message${messages.length !== 1 ? "s" : ""}`;
|
|
1855
|
+
statusSync.textContent = `Undid move of ${messages.length} message${messages.length !== 1 ? "s" : ""}${remaining}`;
|
|
1840
1856
|
// Same positioning rule as undo-delete: show the message back in
|
|
1841
1857
|
// its original folder.
|
|
1842
1858
|
const firstMoved = messages[0];
|
|
@@ -1861,7 +1877,7 @@ async function performUndo() {
|
|
|
1861
1877
|
messageState.updateMessageFlags(entry.accountId, entry.uid, flagsAfter);
|
|
1862
1878
|
}
|
|
1863
1879
|
if (statusSync)
|
|
1864
|
-
statusSync.textContent = `Undid flag change on ${op.payload.length} message${op.payload.length !== 1 ? "s" : ""}`;
|
|
1880
|
+
statusSync.textContent = `Undid flag change on ${op.payload.length} message${op.payload.length !== 1 ? "s" : ""}${remaining}`;
|
|
1865
1881
|
}
|
|
1866
1882
|
reloadCurrentFolder();
|
|
1867
1883
|
}
|
|
@@ -2170,12 +2186,32 @@ async function spamSelectedMessages() {
|
|
|
2170
2186
|
g.folderIds.push(msg.folderId);
|
|
2171
2187
|
byAccount.set(msg.accountId, g);
|
|
2172
2188
|
}
|
|
2189
|
+
// Marking spam is just a move to the \Junk folder (service-side
|
|
2190
|
+
// markAsSpamMessages), so its inverse is the move back — the same undo op
|
|
2191
|
+
// a drag-to-folder move records. Pushed BEFORE the IPC, not in its `.then`:
|
|
2192
|
+
// a misfired spam click is exactly the case where the user reaches for
|
|
2193
|
+
// Ctrl+Z immediately, and the local move has already happened by then
|
|
2194
|
+
// (Bob 2026-08-03: "^z should undo send-to-spam"). The move branch of
|
|
2195
|
+
// performUndo restores each message to its own sourceFolderId, so a
|
|
2196
|
+
// selection spanning folders comes back correctly; targetFolderId is only
|
|
2197
|
+
// known once the service answers and the undo doesn't need it.
|
|
2198
|
+
const spamUndo = {
|
|
2199
|
+
messages: snapshot.map(m => ({ accountId: m.accountId, uid: m.uid, sourceFolderId: m.folderId })),
|
|
2200
|
+
targetAccountId: snapshot[0].accountId,
|
|
2201
|
+
targetFolderId: 0, // filled in below — only the service knows which folder is \Junk
|
|
2202
|
+
};
|
|
2203
|
+
pushUndo({ kind: "move", at: Date.now(), payload: spamUndo });
|
|
2173
2204
|
if (statusSync)
|
|
2174
|
-
statusSync.textContent = `Spam: ${snapshot.length} queued —
|
|
2205
|
+
statusSync.textContent = `Spam: ${snapshot.length} queued — Ctrl+Z to undo`;
|
|
2175
2206
|
for (const [accountId, { uids, folderIds }] of byAccount) {
|
|
2176
2207
|
markAsSpamMessages(accountId, uids, folderIds)
|
|
2177
2208
|
.then(result => {
|
|
2178
2209
|
console.log(`[spam] ${accountId}: moved ${result?.moved ?? uids.length} to folderId=${result?.targetFolderId}`);
|
|
2210
|
+
// Now we know where they landed. The undo op is still on the
|
|
2211
|
+
// stack (same object) — record it so the reverse move names the
|
|
2212
|
+
// messages' current folder instead of guessing by uid alone.
|
|
2213
|
+
if (result?.targetFolderId)
|
|
2214
|
+
spamUndo.targetFolderId = result.targetFolderId;
|
|
2179
2215
|
})
|
|
2180
2216
|
.catch(e => {
|
|
2181
2217
|
console.error(`[spam] ${accountId} failed:`, e);
|
|
@@ -4094,7 +4130,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
4094
4130
|
const composeOpen = !!document.querySelector(".compose-overlay");
|
|
4095
4131
|
if (!inEditable && !composeOpen && undoStack.length > 0) {
|
|
4096
4132
|
e.preventDefault();
|
|
4097
|
-
|
|
4133
|
+
queueUndo();
|
|
4098
4134
|
}
|
|
4099
4135
|
}
|
|
4100
4136
|
// F5 = Sync
|
|
@@ -4333,7 +4369,7 @@ function openShortcutsDialog() {
|
|
|
4333
4369
|
["Forward", "Ctrl+F"],
|
|
4334
4370
|
["Sync", "F5"],
|
|
4335
4371
|
["Delete selected", "Del or Ctrl+D"],
|
|
4336
|
-
["Undo
|
|
4372
|
+
["Undo delete / move / spam / flag (repeat to go back further)", "Ctrl+Z"],
|
|
4337
4373
|
["Toggle read/unread", "R"],
|
|
4338
4374
|
["Toggle flag (★)", "(⚑ button in viewer)"],
|
|
4339
4375
|
["Select all visible", "Ctrl+A"],
|