@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.
Files changed (33) hide show
  1. package/client/app.bundle.js +129 -1
  2. package/client/app.bundle.js.map +3 -3
  3. package/client/app.js +155 -0
  4. package/client/app.js.map +1 -1
  5. package/client/app.ts +139 -0
  6. package/client/components/context-menu.js +3 -1
  7. package/client/components/context-menu.js.map +1 -1
  8. package/client/components/context-menu.ts +5 -1
  9. package/client/compose/compose.bundle.js +7 -1
  10. package/client/compose/compose.bundle.js.map +2 -2
  11. package/client/lib/api-client.js +6 -0
  12. package/client/lib/api-client.js.map +1 -1
  13. package/client/lib/api-client.ts +7 -0
  14. package/package.json +1 -1
  15. package/packages/mailx-imap/index.d.ts.map +1 -1
  16. package/packages/mailx-imap/index.js +24 -0
  17. package/packages/mailx-imap/index.js.map +1 -1
  18. package/packages/mailx-imap/index.ts +25 -0
  19. package/packages/mailx-imap/package-lock.json +2 -2
  20. package/packages/mailx-imap/package.json +1 -1
  21. package/packages/mailx-service/index.d.ts +7 -0
  22. package/packages/mailx-service/index.d.ts.map +1 -1
  23. package/packages/mailx-service/index.js +15 -0
  24. package/packages/mailx-service/index.js.map +1 -1
  25. package/packages/mailx-service/index.ts +15 -0
  26. package/packages/mailx-service/jsonrpc.js +3 -0
  27. package/packages/mailx-service/jsonrpc.js.map +1 -1
  28. package/packages/mailx-service/jsonrpc.ts +3 -0
  29. package/packages/mailx-service/sync-queue.d.ts +5 -0
  30. package/packages/mailx-service/sync-queue.d.ts.map +1 -1
  31. package/packages/mailx-service/sync-queue.js +8 -0
  32. package/packages/mailx-service/sync-queue.js.map +1 -1
  33. 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