@bobfrankston/rmfmail 1.2.221 → 1.2.222

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.ts CHANGED
@@ -1449,6 +1449,27 @@ function showComposeOverlay(title = "Compose"): HTMLIFrameElement {
1449
1449
  return frame;
1450
1450
  }
1451
1451
 
1452
+ // Native context-menu commands land in the TOP frame: msger dispatches them
1453
+ // with CoreWebView2.ExecuteScript, which always runs against the top-level
1454
+ // document. But the in-window compose is an IFRAME, and the handler that owns
1455
+ // the semantics (__msgerContextCommand) is defined inside it — so every
1456
+ // right-click item in the in-window compose hit msger's `window.__msgerContext
1457
+ // Command && …` guard, found nothing, and silently did nothing. The items were
1458
+ // registered for this window in v1.2.155; the dispatcher never was. Forward to
1459
+ // the frontmost compose overlay (same origin, so reaching into it is legal).
1460
+ (window as any).__msgerContextCommand = (id: string): void => {
1461
+ const frames = [...document.querySelectorAll<HTMLIFrameElement>(".compose-overlay iframe")];
1462
+ // Frontmost = highest z-index on the wrapper; mousedown maintains it, so
1463
+ // the one the user just right-clicked is the one on top.
1464
+ const target = frames.sort((a, b) => {
1465
+ const z = (el: HTMLIFrameElement) => Number((el.closest(".compose-overlay") as HTMLElement)?.style.zIndex || 0);
1466
+ return z(b) - z(a);
1467
+ })[0];
1468
+ try {
1469
+ (target?.contentWindow as any)?.__msgerContextCommand?.(id);
1470
+ } catch { /* frame torn down mid-click */ }
1471
+ };
1472
+
1452
1473
  /** Drop a transparent full-viewport shield in front of every other element
1453
1474
  * so mousemove events stay in the document during a drag. Setting
1454
1475
  * pointer-events:none on the compose iframe alone wasn't enough — the
@@ -2080,7 +2080,13 @@ async function createTinyMceEditor(container2, opts = {}) {
2080
2080
  // 2026-06-01). The paste_* OPTIONS below remain valid (core).
2081
2081
  plugins: "lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
2082
2082
  toolbar: [
2083
- "undo redo | bold italic underline strikethrough | forecolor backcolor",
2083
+ // fontsize on the main bar (Bob 2026-08-07: "there should also
2084
+ // be font sizing on the tool bar"). `fontsize` is the v6+ name
2085
+ // for v5's `fontsizeselect`; it renders the size dropdown and
2086
+ // reflects the size at the caret. The Format ▸ Font sizes
2087
+ // submenu had it all along, which is two clicks deeper than a
2088
+ // size control has any business being.
2089
+ "undo redo | fontsize | bold italic underline strikethrough | forecolor backcolor",
2084
2090
  // blockquote on the main bar (was quickbar-only) — Bob
2085
2091
  // 2026-07-28: pasted/inserted text needs a one-click quote.
2086
2092
  "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help"
@@ -6094,6 +6100,60 @@ async function popoutHandoff() {
6094
6100
  window.addEventListener("compose-popout", () => {
6095
6101
  void popoutHandoff();
6096
6102
  });
6103
+ async function runClipboardCommand(id) {
6104
+ const ne = editor?.nativeEditor;
6105
+ const isTiny = !!ne && typeof ne.execCommand === "function" && !!ne.selection;
6106
+ const label = id === "cut" ? "Cut" : id === "copy" ? "Copy" : "Paste";
6107
+ const shortcut = id === "cut" ? "Ctrl+X" : id === "copy" ? "Ctrl+C" : "Ctrl+V";
6108
+ try {
6109
+ if (id === "paste") {
6110
+ let html2 = "";
6111
+ let text2 = "";
6112
+ if (navigator.clipboard?.read) {
6113
+ for (const item of await navigator.clipboard.read()) {
6114
+ if (item.types.includes("text/html")) html2 = await (await item.getType("text/html")).text();
6115
+ if (item.types.includes("text/plain")) text2 = await (await item.getType("text/plain")).text();
6116
+ }
6117
+ } else {
6118
+ text2 = await navigator.clipboard.readText();
6119
+ }
6120
+ const content = html2 || (text2 ? text2.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]).replace(/\r?\n/g, "<br>") : "");
6121
+ if (!content) return;
6122
+ if (isTiny) ne.execCommand("mceInsertContent", false, content);
6123
+ else if (ne && typeof ne.getSelection === "function") {
6124
+ const sel = ne.getSelection(true);
6125
+ ne.clipboard?.dangerouslyPasteHTML?.(sel?.index ?? 0, content);
6126
+ } else throw new Error("no editor");
6127
+ return;
6128
+ }
6129
+ if (isTiny) {
6130
+ try {
6131
+ if (ne.selection.isCollapsed() && typeof ne.selection.expand === "function") ne.selection.expand({ type: "word" });
6132
+ } catch {
6133
+ }
6134
+ }
6135
+ const html = isTiny ? ne.selection.getContent({ format: "html" }) : "";
6136
+ const text = isTiny ? ne.selection.getContent({ format: "text" }) : window.getSelection()?.toString() || "";
6137
+ if (!text && !html) return;
6138
+ if (html && typeof ClipboardItem === "function" && navigator.clipboard?.write) {
6139
+ await navigator.clipboard.write([new ClipboardItem({
6140
+ "text/html": new Blob([html], { type: "text/html" }),
6141
+ "text/plain": new Blob([text], { type: "text/plain" })
6142
+ })]);
6143
+ } else {
6144
+ await navigator.clipboard.writeText(text);
6145
+ }
6146
+ if (id === "cut") {
6147
+ if (isTiny) ne.execCommand("Delete");
6148
+ else if (ne && typeof ne.deleteText === "function") {
6149
+ const sel = ne.getSelection();
6150
+ if (sel?.length) ne.deleteText(sel.index, sel.length);
6151
+ }
6152
+ }
6153
+ } catch (e) {
6154
+ showDraftStatus(`${label} failed: ${e?.message || e}. ${shortcut} still works.`, true);
6155
+ }
6156
+ }
6097
6157
  window.__msgerContextCommand = (id) => {
6098
6158
  if (id === "source") {
6099
6159
  try {
@@ -6102,6 +6162,10 @@ window.__msgerContextCommand = (id) => {
6102
6162
  }
6103
6163
  return;
6104
6164
  }
6165
+ if (id === "cut" || id === "copy" || id === "paste") {
6166
+ void runClipboardCommand(id);
6167
+ return;
6168
+ }
6105
6169
  const ne = editor?.nativeEditor;
6106
6170
  if (!ne) return;
6107
6171
  const isTiny = typeof ne.execCommand === "function" && ne.selection;