@bobfrankston/mailx 1.0.297 → 1.0.298

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/bin/mailx.js CHANGED
@@ -835,6 +835,17 @@ async function main() {
835
835
  const clientDir = path.join(import.meta.dirname, "..", "client");
836
836
  const mailxapiPath = path.join(clientDir, "lib", "mailxapi.js");
837
837
  const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
838
+ // Restore saved window geometry (position + size) from previous session
839
+ const windowJsonPath = path.join(getConfigDir(), "window.json");
840
+ let savedGeometry = null;
841
+ try {
842
+ const raw = JSON.parse(fs.readFileSync(windowJsonPath, "utf-8"));
843
+ if (typeof raw.width === "number" && raw.width > 200 &&
844
+ typeof raw.height === "number" && raw.height > 200) {
845
+ savedGeometry = raw;
846
+ }
847
+ }
848
+ catch { /* no saved geometry — use defaults */ }
838
849
  const rootPkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version;
839
850
  const handle = showService({
840
851
  title: `mailx v${rootPkgVersion}`,
@@ -843,7 +854,12 @@ async function main() {
843
854
  initScript: mailxapiScript,
844
855
  icon: path.join(clientDir, "icon.png"),
845
856
  aumid: "com.frankston.mailx",
846
- size: { width: 1400, height: 900 },
857
+ size: savedGeometry
858
+ ? { width: savedGeometry.width, height: savedGeometry.height }
859
+ : { width: 1400, height: 900 },
860
+ pos: savedGeometry
861
+ ? { x: savedGeometry.x, y: savedGeometry.y }
862
+ : undefined,
847
863
  escapeCloses: false,
848
864
  });
849
865
  // Register ourselves as the live instance so subsequent `mailx` invocations
@@ -868,6 +884,23 @@ async function main() {
868
884
  return;
869
885
  }
870
886
  console.log(`[ipc] ← ${req._action} (${req._cbid})`);
887
+ // Save window position+size for next launch (handled here, not in MailxService)
888
+ if (req._action === "saveWindowGeometry") {
889
+ try {
890
+ const geom = {
891
+ x: Number(req.x) || 0,
892
+ y: Number(req.y) || 0,
893
+ width: Math.max(400, Number(req.width) || 1400),
894
+ height: Math.max(300, Number(req.height) || 900),
895
+ };
896
+ fs.writeFileSync(windowJsonPath, JSON.stringify(geom, null, 2));
897
+ }
898
+ catch (e) {
899
+ console.error(`[window] Failed to save geometry: ${e.message}`);
900
+ }
901
+ handle.send({ _cbid: req._cbid, result: { ok: true } });
902
+ return;
903
+ }
871
904
  // Auto-update action: run npm install then restart
872
905
  if (req._action === "performUpdate") {
873
906
  handle.send({ _cbid: req._cbid, ok: true, status: "updating" });
package/client/app.js CHANGED
@@ -691,6 +691,32 @@ document.addEventListener("mailx-moved", (e) => {
691
691
  undoTimeout = setTimeout(() => { lastMoved = null; }, 60000);
692
692
  });
693
693
  document.getElementById("btn-delete")?.addEventListener("click", deleteSelectedMessages);
694
+ // ── Flag toggle ──
695
+ document.getElementById("btn-flag")?.addEventListener("click", async () => {
696
+ const sel = messageState.getSelected();
697
+ if (!sel)
698
+ return;
699
+ const isFlagged = sel.flags.includes("\\Flagged");
700
+ const newFlags = isFlagged
701
+ ? sel.flags.filter((f) => f !== "\\Flagged")
702
+ : [...sel.flags, "\\Flagged"];
703
+ try {
704
+ await updateFlags(sel.accountId, sel.uid, newFlags);
705
+ sel.flags = newFlags;
706
+ messageState.updateMessageFlags(sel.accountId, sel.uid, newFlags);
707
+ // Update the message-list row's flag indicator
708
+ const row = document.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`);
709
+ if (row) {
710
+ row.classList.toggle("flagged", newFlags.includes("\\Flagged"));
711
+ const flagEl = row.querySelector(".ml-flag");
712
+ if (flagEl)
713
+ flagEl.textContent = newFlags.includes("\\Flagged") ? "\u2605" : "\u2606";
714
+ }
715
+ }
716
+ catch (e) {
717
+ console.error(`Flag toggle failed: ${e.message}`);
718
+ }
719
+ });
694
720
  async function spamSelectedMessages() {
695
721
  const selected = getSelectedMessages();
696
722
  if (selected.length === 0) {
@@ -1660,4 +1686,22 @@ versionPromise.then((d) => {
1660
1686
  else if (d.theme === "light")
1661
1687
  document.documentElement.classList.add("theme-light");
1662
1688
  }).catch(() => { });
1689
+ // ── Save window geometry on close (IPC mode only) ──
1690
+ // Sends window position and size so the next launch restores them.
1691
+ if (isApp) {
1692
+ const ipcApi = window.mailxapi;
1693
+ function sendGeometry() {
1694
+ if (!ipcApi?.saveWindowGeometry)
1695
+ return;
1696
+ ipcApi.saveWindowGeometry({
1697
+ x: window.screenX,
1698
+ y: window.screenY,
1699
+ width: window.outerWidth,
1700
+ height: window.outerHeight,
1701
+ }).catch(() => { });
1702
+ }
1703
+ // Save on unload (window close) and periodically as a safety net
1704
+ window.addEventListener("beforeunload", sendGeometry);
1705
+ setInterval(sendGeometry, 60_000);
1706
+ }
1663
1707
  //# sourceMappingURL=app.js.map
@@ -3,12 +3,22 @@
3
3
  * Shows a menu at a given position with clickable items.
4
4
  */
5
5
  let activeMenu = null;
6
- /** Close any open context menu */
6
+ let dismissListener = null;
7
+ let escapeListener = null;
8
+ /** Close any open context menu and remove dismiss listeners */
7
9
  export function closeContextMenu() {
8
10
  if (activeMenu) {
9
11
  activeMenu.remove();
10
12
  activeMenu = null;
11
13
  }
14
+ if (dismissListener) {
15
+ document.removeEventListener("pointerdown", dismissListener, true);
16
+ dismissListener = null;
17
+ }
18
+ if (escapeListener) {
19
+ document.removeEventListener("keydown", escapeListener, true);
20
+ escapeListener = null;
21
+ }
12
22
  }
13
23
  /** Show a context menu at the given position */
14
24
  export function showContextMenu(x, y, items) {
@@ -43,11 +53,28 @@ export function showContextMenu(x, y, items) {
43
53
  if (rect.bottom > window.innerHeight)
44
54
  menu.style.top = `${y - rect.height}px`;
45
55
  activeMenu = menu;
56
+ // Dismiss on click/tap outside the menu. Uses pointerdown in capture phase
57
+ // so it fires before any child handler and catches both left- and right-clicks.
58
+ // Deferred by one frame so the opening pointerdown doesn't immediately close it.
59
+ requestAnimationFrame(() => {
60
+ dismissListener = (e) => {
61
+ if (activeMenu && !activeMenu.contains(e.target)) {
62
+ closeContextMenu();
63
+ }
64
+ };
65
+ document.addEventListener("pointerdown", dismissListener, true);
66
+ escapeListener = (e) => {
67
+ if (e.key === "Escape") {
68
+ e.preventDefault();
69
+ e.stopPropagation();
70
+ closeContextMenu();
71
+ }
72
+ };
73
+ document.addEventListener("keydown", escapeListener, true);
74
+ });
46
75
  }
47
- // Close on any interaction outside the menu
48
- document.addEventListener("click", closeContextMenu);
49
- document.addEventListener("keydown", (e) => { if (e.key === "Escape")
50
- closeContextMenu(); });
76
+ // Scroll anywhere closes the menu (capture phase so nested scrollers trigger it)
51
77
  document.addEventListener("scroll", closeContextMenu, true);
78
+ // A new right-click that opens a different menu goes through showContextMenu→closeContextMenu
52
79
  document.addEventListener("contextmenu", () => { });
53
80
  //# sourceMappingURL=context-menu.js.map
@@ -172,6 +172,11 @@
172
172
  },
173
173
  repairAccounts: function() { return callNode("repairAccounts"); },
174
174
 
175
+ // Window geometry
176
+ saveWindowGeometry: function(geom) {
177
+ return callNode("saveWindowGeometry", geom);
178
+ },
179
+
175
180
  // Events
176
181
  onEvent: function(handler) { _eventHandlers.push(handler); },
177
182
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.297",
3
+ "version": "1.0.298",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",