@bobfrankston/rmfmail 1.2.221 → 1.2.223

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.ts CHANGED
@@ -183,6 +183,32 @@ const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
183
183
  }
184
184
  }
185
185
 
186
+ /** App entries appended to the NATIVE right-click menu inside compose. msger
187
+ * keeps the WebView2 default menu (that is the only place spellcheck
188
+ * suggestions live) and gates these to editable targets, so they never show
189
+ * on the read-only list or message preview.
190
+ *
191
+ * Deliberately NO Cut/Copy/Paste here: WebView2's own menu already carries
192
+ * them (with the right enabled/disabled state and the Ctrl+X/C/V hints), so
193
+ * app copies would just be duplicates a few rows further down. The menu that
194
+ * genuinely lacks them is mailx's spell menu, which preventDefaults the
195
+ * native one away — that gap is filled in compose/edit-commands.ts, at the
196
+ * menu that actually has the hole (Bob 2026-08-07).
197
+ *
198
+ * Ids are dispatched to compose.ts via window.__msgerContextCommand — one
199
+ * list, both windows (in-window compose overlay + popout compose), because
200
+ * drift between the two is what produced "right mouse isn't showing any
201
+ * formatting options" in the first place (Bob 2026-07-18). */
202
+ const COMPOSE_CONTEXT_MENU_ITEMS = [
203
+ { id: "bold", label: "Bold" },
204
+ { id: "italic", label: "Italic" },
205
+ { id: "underline", label: "Underline" },
206
+ { id: "strike", label: "Strikethrough" },
207
+ { id: "link", label: "Link…" },
208
+ { id: "clear", label: "Clear formatting" },
209
+ { id: "source", label: "View HTML source…" },
210
+ ];
211
+
186
212
  /** Windows mailto: registration — registry keys via reg.exe. Shared by the
187
213
  * `-register-mailto` CLI flag and the Settings → "Make default email app"
188
214
  * menu action (injected into MailxService; the registry work stays HERE in
@@ -2420,15 +2446,7 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2420
2446
  // App entries on the NATIVE right-click menu (native stays
2421
2447
  // so spellcheck suggestions survive). compose.ts maps ids
2422
2448
  // to editor commands via window.__msgerContextCommand.
2423
- contextMenuItems: [
2424
- { id: "bold", label: "Bold" },
2425
- { id: "italic", label: "Italic" },
2426
- { id: "underline", label: "Underline" },
2427
- { id: "strike", label: "Strikethrough" },
2428
- { id: "link", label: "Link…" },
2429
- { id: "clear", label: "Clear formatting" },
2430
- { id: "source", label: "View HTML source…" },
2431
- ],
2449
+ contextMenuItems: COMPOSE_CONTEXT_MENU_ITEMS,
2432
2450
  });
2433
2451
  popoutWindows.set(key, h);
2434
2452
  if (typeof h.pid === "number") addChildPid(h.pid);
@@ -2552,15 +2570,7 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2552
2570
  // "right mouse isn't showing any formatting options let alone source").
2553
2571
  // msger gates these to editable targets, so they DON'T appear on
2554
2572
  // right-clicks in the read-only list / message preview.
2555
- contextMenuItems: [
2556
- { id: "bold", label: "Bold" },
2557
- { id: "italic", label: "Italic" },
2558
- { id: "underline", label: "Underline" },
2559
- { id: "strike", label: "Strikethrough" },
2560
- { id: "link", label: "Link…" },
2561
- { id: "clear", label: "Clear formatting" },
2562
- { id: "source", label: "View HTML source…" },
2563
- ],
2573
+ contextMenuItems: COMPOSE_CONTEXT_MENU_ITEMS,
2564
2574
  });
2565
2575
  // Late-bind the popout-action forwarder now that the main window's
2566
2576
  // service channel exists.
@@ -723,7 +723,14 @@ __export(context_menu_exports, {
723
723
  closeContextMenu: () => closeContextMenu,
724
724
  showContextMenu: () => showContextMenu
725
725
  });
726
+ function closeSubmenu() {
727
+ if (activeSubmenu) {
728
+ activeSubmenu.remove();
729
+ activeSubmenu = null;
730
+ }
731
+ }
726
732
  function closeContextMenu() {
733
+ closeSubmenu();
727
734
  if (activeMenu) {
728
735
  activeMenu.remove();
729
736
  activeMenu = null;
@@ -737,6 +744,45 @@ function closeContextMenu() {
737
744
  escapeListener = null;
738
745
  }
739
746
  }
747
+ function openSubmenu(parentRow, items) {
748
+ closeSubmenu();
749
+ const sub = document.createElement("div");
750
+ sub.className = "ctx-menu";
751
+ for (const item of items) {
752
+ if (item.separator) {
753
+ const sep = document.createElement("div");
754
+ sep.className = "ctx-sep";
755
+ sub.appendChild(sep);
756
+ continue;
757
+ }
758
+ const el = document.createElement("div");
759
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
760
+ el.textContent = item.label;
761
+ if (item.tooltip)
762
+ el.title = item.tooltip;
763
+ if (!item.disabled) {
764
+ el.addEventListener("click", () => {
765
+ closeContextMenu();
766
+ item.action();
767
+ });
768
+ }
769
+ sub.appendChild(el);
770
+ }
771
+ document.body.appendChild(sub);
772
+ const anchor = parentRow.getBoundingClientRect();
773
+ const vw = window.visualViewport?.width ?? window.innerWidth;
774
+ const vh = window.visualViewport?.height ?? window.innerHeight;
775
+ const rect = sub.getBoundingClientRect();
776
+ let left = anchor.right - 2;
777
+ if (left + rect.width > vw)
778
+ left = anchor.left - rect.width + 2;
779
+ let top = anchor.top;
780
+ if (top + rect.height > vh)
781
+ top = vh - rect.height - 4;
782
+ sub.style.left = `${Math.max(4, left)}px`;
783
+ sub.style.top = `${Math.max(4, top)}px`;
784
+ activeSubmenu = sub;
785
+ }
740
786
  function showContextMenu(x, y, items) {
741
787
  closeContextMenu();
742
788
  const menu = document.createElement("div");
@@ -755,6 +801,24 @@ function showContextMenu(x, y, items) {
755
801
  el.textContent = item.label;
756
802
  if (item.tooltip)
757
803
  el.title = item.tooltip;
804
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
805
+ el.style.display = "flex";
806
+ el.style.justifyContent = "space-between";
807
+ el.style.gap = "12px";
808
+ const caret = document.createElement("span");
809
+ caret.textContent = "\u25B8";
810
+ caret.style.opacity = "0.6";
811
+ el.appendChild(caret);
812
+ const open = () => openSubmenu(el, item.submenu);
813
+ el.addEventListener("mouseenter", open);
814
+ el.addEventListener("click", (e) => {
815
+ e.stopPropagation();
816
+ open();
817
+ });
818
+ menu.appendChild(el);
819
+ continue;
820
+ }
821
+ el.addEventListener("mouseenter", closeSubmenu);
758
822
  if (!item.disabled) {
759
823
  el.addEventListener("click", () => {
760
824
  closeContextMenu();
@@ -786,6 +850,8 @@ function showContextMenu(x, y, items) {
786
850
  activeMenu = menu;
787
851
  requestAnimationFrame(() => {
788
852
  dismissListener = (e) => {
853
+ if (activeSubmenu?.contains(e.target))
854
+ return;
789
855
  if (activeMenu && !activeMenu.contains(e.target)) {
790
856
  closeContextMenu();
791
857
  }
@@ -801,13 +867,14 @@ function showContextMenu(x, y, items) {
801
867
  document.addEventListener("keydown", escapeListener, true);
802
868
  });
803
869
  }
804
- var activeMenu, dismissListener, escapeListener;
870
+ var activeMenu, dismissListener, escapeListener, activeSubmenu;
805
871
  var init_context_menu = __esm({
806
872
  "client/components/context-menu.js"() {
807
873
  "use strict";
808
874
  activeMenu = null;
809
875
  dismissListener = null;
810
876
  escapeListener = null;
877
+ activeSubmenu = null;
811
878
  document.addEventListener("scroll", closeContextMenu, true);
812
879
  document.addEventListener("contextmenu", () => {
813
880
  });
@@ -9117,6 +9184,17 @@ function showComposeOverlay(title = "Compose") {
9117
9184
  document.body.appendChild(wrapper);
9118
9185
  return frame;
9119
9186
  }
9187
+ window.__msgerContextCommand = (id) => {
9188
+ const frames = [...document.querySelectorAll(".compose-overlay iframe")];
9189
+ const target = frames.sort((a, b) => {
9190
+ const z = (el) => Number(el.closest(".compose-overlay")?.style.zIndex || 0);
9191
+ return z(b) - z(a);
9192
+ })[0];
9193
+ try {
9194
+ target?.contentWindow?.__msgerContextCommand?.(id);
9195
+ } catch {
9196
+ }
9197
+ };
9120
9198
  function installDragShield(cursor) {
9121
9199
  const shield = document.createElement("div");
9122
9200
  shield.style.cssText = `position:fixed;inset:0;z-index:9999;background:transparent;cursor:${cursor};user-select:none;`;