@bobfrankston/rmfmail 1.1.248 → 1.1.249
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 +129 -1
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +155 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +139 -0
- package/client/components/context-menu.js +3 -1
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +5 -1
- package/client/compose/compose.bundle.js +7 -1
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +6 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +7 -0
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +24 -0
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +25 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +7 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +15 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +15 -0
- package/packages/mailx-service/jsonrpc.js +3 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +3 -0
- package/packages/mailx-service/sync-queue.d.ts +5 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -1
- package/packages/mailx-service/sync-queue.js +8 -0
- package/packages/mailx-service/sync-queue.js.map +1 -1
- package/packages/mailx-service/sync-queue.ts +9 -0
package/client/app.js
CHANGED
|
@@ -1628,6 +1628,161 @@ async function performUndo() {
|
|
|
1628
1628
|
document.addEventListener("mailx-moved", (e) => {
|
|
1629
1629
|
pushUndo({ kind: "move", at: Date.now(), payload: e.detail });
|
|
1630
1630
|
});
|
|
1631
|
+
// ── Right-button drag → Move / Copy menu ───────────────────────────────
|
|
1632
|
+
// Browser drag-and-drop is left-button only, so a RIGHT-button drag is a
|
|
1633
|
+
// custom mouse-tracked gesture: right-press a message row, drag onto a folder
|
|
1634
|
+
// in the tree, release → a menu offers Move (default, bold) or Copy. A plain
|
|
1635
|
+
// left drag still moves directly; a plain right-CLICK (no movement) still opens
|
|
1636
|
+
// the normal message menu — only a right-press-AND-drag triggers this.
|
|
1637
|
+
(() => {
|
|
1638
|
+
const SLOP = 6;
|
|
1639
|
+
let cand = null;
|
|
1640
|
+
let active = false;
|
|
1641
|
+
let ghost = null;
|
|
1642
|
+
let suppressContext = false;
|
|
1643
|
+
// Cursor + ghost + drop-highlight styles (self-contained).
|
|
1644
|
+
const style = document.createElement("style");
|
|
1645
|
+
style.textContent =
|
|
1646
|
+
"body.rmf-rdrag, body.rmf-rdrag * { cursor: grabbing !important; }" +
|
|
1647
|
+
".rmf-rdrag-ghost { position: fixed; z-index: 100000; pointer-events: none;" +
|
|
1648
|
+
" background: var(--color-accent, #1a6dd4); color: #fff; padding: 2px 9px;" +
|
|
1649
|
+
" border-radius: 4px; font: 12px system-ui; box-shadow: 0 2px 8px rgba(0,0,0,.3); }" +
|
|
1650
|
+
".ft-folder.rmf-rdrag-over { outline: 2px solid var(--color-accent, #1a6dd4); outline-offset: -2px; border-radius: 4px; }";
|
|
1651
|
+
document.head.appendChild(style);
|
|
1652
|
+
const folderAt = (x, y) => document.elementFromPoint(x, y)?.closest?.(".ft-folder") || null;
|
|
1653
|
+
const clearOver = () => document.querySelectorAll(".ft-folder.rmf-rdrag-over").forEach(el => el.classList.remove("rmf-rdrag-over"));
|
|
1654
|
+
const cleanup = () => {
|
|
1655
|
+
active = false;
|
|
1656
|
+
cand = null;
|
|
1657
|
+
document.body.classList.remove("rmf-rdrag");
|
|
1658
|
+
ghost?.remove();
|
|
1659
|
+
ghost = null;
|
|
1660
|
+
clearOver();
|
|
1661
|
+
};
|
|
1662
|
+
document.addEventListener("mousedown", (e) => {
|
|
1663
|
+
if (e.button !== 2)
|
|
1664
|
+
return;
|
|
1665
|
+
const row = e.target?.closest?.(".ml-row");
|
|
1666
|
+
if (!row)
|
|
1667
|
+
return;
|
|
1668
|
+
const pressed = {
|
|
1669
|
+
accountId: row.dataset.accountId || "",
|
|
1670
|
+
uid: Number(row.dataset.uid),
|
|
1671
|
+
folderId: Number(row.dataset.folderId),
|
|
1672
|
+
};
|
|
1673
|
+
if (!pressed.uid)
|
|
1674
|
+
return;
|
|
1675
|
+
// Drag the current multi-selection if the pressed row is in it;
|
|
1676
|
+
// otherwise just the pressed row.
|
|
1677
|
+
let msgs = getSelectedMessages();
|
|
1678
|
+
if (!msgs.some(m => m.accountId === pressed.accountId && m.uid === pressed.uid))
|
|
1679
|
+
msgs = [pressed];
|
|
1680
|
+
cand = { msgs, x: e.clientX, y: e.clientY };
|
|
1681
|
+
active = false;
|
|
1682
|
+
}, true);
|
|
1683
|
+
document.addEventListener("mousemove", (e) => {
|
|
1684
|
+
if (!cand)
|
|
1685
|
+
return;
|
|
1686
|
+
if (!active) {
|
|
1687
|
+
if (Math.abs(e.clientX - cand.x) < SLOP && Math.abs(e.clientY - cand.y) < SLOP)
|
|
1688
|
+
return;
|
|
1689
|
+
active = true;
|
|
1690
|
+
document.body.classList.add("rmf-rdrag");
|
|
1691
|
+
ghost = document.createElement("div");
|
|
1692
|
+
ghost.className = "rmf-rdrag-ghost";
|
|
1693
|
+
ghost.textContent = cand.msgs.length === 1 ? "1 message" : `${cand.msgs.length} messages`;
|
|
1694
|
+
document.body.appendChild(ghost);
|
|
1695
|
+
}
|
|
1696
|
+
if (ghost) {
|
|
1697
|
+
ghost.style.left = `${e.clientX + 14}px`;
|
|
1698
|
+
ghost.style.top = `${e.clientY + 8}px`;
|
|
1699
|
+
}
|
|
1700
|
+
clearOver();
|
|
1701
|
+
folderAt(e.clientX, e.clientY)?.classList.add("rmf-rdrag-over");
|
|
1702
|
+
}, true);
|
|
1703
|
+
document.addEventListener("mouseup", (e) => {
|
|
1704
|
+
if (!cand)
|
|
1705
|
+
return;
|
|
1706
|
+
const wasActive = active;
|
|
1707
|
+
const msgs = cand.msgs;
|
|
1708
|
+
const folder = wasActive ? folderAt(e.clientX, e.clientY) : null;
|
|
1709
|
+
const mx = e.clientX, my = e.clientY;
|
|
1710
|
+
cleanup();
|
|
1711
|
+
if (!wasActive)
|
|
1712
|
+
return; // a right-click, not a drag
|
|
1713
|
+
suppressContext = true; // eat the contextmenu that fires next
|
|
1714
|
+
if (!folder)
|
|
1715
|
+
return; // released off any folder
|
|
1716
|
+
e.preventDefault();
|
|
1717
|
+
e.stopPropagation();
|
|
1718
|
+
const targetAccount = folder.dataset.accountId || "";
|
|
1719
|
+
const targetFolderId = Number(folder.dataset.folderId);
|
|
1720
|
+
const targetName = folder.querySelector(".ft-folder-name")?.textContent?.trim() || "folder";
|
|
1721
|
+
void showRightDragMenu(mx, my, msgs, targetAccount, targetFolderId, targetName);
|
|
1722
|
+
}, true);
|
|
1723
|
+
// Suppress the folder/message context menu that would otherwise pop on the
|
|
1724
|
+
// right-button release after a drag.
|
|
1725
|
+
document.addEventListener("contextmenu", (e) => {
|
|
1726
|
+
if (suppressContext) {
|
|
1727
|
+
e.preventDefault();
|
|
1728
|
+
e.stopPropagation();
|
|
1729
|
+
suppressContext = false;
|
|
1730
|
+
}
|
|
1731
|
+
}, true);
|
|
1732
|
+
async function showRightDragMenu(x, y, msgs, targetAccount, targetFolderId, targetName) {
|
|
1733
|
+
const { showContextMenu } = await import("./components/context-menu.js");
|
|
1734
|
+
const n = msgs.length;
|
|
1735
|
+
const label = n === 1 ? "message" : `${n} messages`;
|
|
1736
|
+
showContextMenu(x, y, [
|
|
1737
|
+
{ label: `Move ${label} here`, emphasized: true, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "move") },
|
|
1738
|
+
{ label: `Copy ${label} here`, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "copy") },
|
|
1739
|
+
]);
|
|
1740
|
+
}
|
|
1741
|
+
async function runRightDragOp(msgs, targetAccount, targetFolderId, targetName, op) {
|
|
1742
|
+
const status = document.getElementById("status-sync");
|
|
1743
|
+
const api = await import("./lib/api-client.js");
|
|
1744
|
+
const byAccount = new Map();
|
|
1745
|
+
for (const m of msgs) {
|
|
1746
|
+
const g = byAccount.get(m.accountId) || { uids: [], folderIds: [] };
|
|
1747
|
+
g.uids.push(m.uid);
|
|
1748
|
+
g.folderIds.push(m.folderId);
|
|
1749
|
+
byAccount.set(m.accountId, g);
|
|
1750
|
+
}
|
|
1751
|
+
if (op === "move") {
|
|
1752
|
+
// Optimistic remove + undo, mirroring the left-drag drop handler.
|
|
1753
|
+
removeMessagesAndReconcile(msgs);
|
|
1754
|
+
document.dispatchEvent(new CustomEvent("mailx-moved", {
|
|
1755
|
+
detail: { messages: msgs.map(m => ({ accountId: m.accountId, uid: m.uid, sourceFolderId: m.folderId })) },
|
|
1756
|
+
}));
|
|
1757
|
+
for (const [src, g] of byAccount) {
|
|
1758
|
+
const targetAcct = src === targetAccount ? undefined : targetAccount;
|
|
1759
|
+
api.moveMessages(src, g.uids, targetFolderId, targetAcct)?.catch?.((err) => {
|
|
1760
|
+
if (status)
|
|
1761
|
+
status.textContent = `Move failed: ${err?.message || err}`;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (status)
|
|
1765
|
+
status.textContent = `Moved ${msgs.length} to ${targetName} — Ctrl+Z to undo`;
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
let skippedXacct = false;
|
|
1769
|
+
for (const [src, g] of byAccount) {
|
|
1770
|
+
if (src !== targetAccount) {
|
|
1771
|
+
skippedXacct = true;
|
|
1772
|
+
continue;
|
|
1773
|
+
} // copy is same-account for now
|
|
1774
|
+
api.copyMessages(src, g.uids, g.folderIds, targetFolderId)?.catch?.((err) => {
|
|
1775
|
+
if (status)
|
|
1776
|
+
status.textContent = `Copy failed: ${err?.message || err}`;
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (status)
|
|
1780
|
+
status.textContent = skippedXacct
|
|
1781
|
+
? `Copy across accounts isn't supported yet`
|
|
1782
|
+
: `Copying ${msgs.length} to ${targetName}…`;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
})();
|
|
1631
1786
|
document.getElementById("btn-delete")?.addEventListener("click", deleteSelection);
|
|
1632
1787
|
// Same handlers also bound to the top-toolbar icons so delete/spam work
|
|
1633
1788
|
// regardless of whether a message is open in the viewer. Useful for quick
|