@bobfrankston/rmfmail 1.0.659 → 1.0.661
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/TODO.md +1 -0
- package/client/app.js +134 -5
- package/client/app.js.map +1 -1
- package/client/app.ts +127 -5
- package/client/compose/compose.js +59 -9
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +61 -8
- package/client/lib/rmf-tiny.js +77 -2
- package/package.json +3 -3
- package/packages/mailx-compose/index.js +7 -2
- package/packages/mailx-compose/index.js.map +1 -1
- package/packages/mailx-compose/index.ts +8 -2
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +44 -23
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +38 -18
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +11 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +12 -0
- package/packages/mailx-store/package.json +1 -1
package/TODO.md
CHANGED
|
@@ -236,6 +236,7 @@ Previously shown as showstoppers; moved here because they haven't recurred on re
|
|
|
236
236
|
|
|
237
237
|
Small, self-contained items. Pick them up between higher-priority blocks without asking. Bump version per fix.
|
|
238
238
|
|
|
239
|
+
- **Q134 — Real popout: compose + preview into independent msger windows.** Current popout button (title-bar icon) toggles the floating compose overlay to fill the host window — fast win but not a true OS window. Bob 2026-05-10: "popout should pop out into an independent window. This should also be true for the preview." Requires daemon refactor: single `handle = showService(...)` in `bin/mailx.ts` (~40 `handle.send` call sites) becomes a `windows` registry — `broadcast(msg)` fans events to all open windows; per-window onRequest closures route request acks back to the originator. New jsonrpc actions `openPopout({ mode, init })` (spawns a fresh `showService` with `contentDir: <client>` pointed at `compose/compose.html` or a `preview/preview.html` for viewer popout) and `getPopoutInit({ token })` (init data delivery — see option below). Client wires the popout button to call `api.openPopout(...)` then closes the floating overlay. Preview gets a matching button in `message-viewer.ts`. Init data delivery: URL-hash token + first-load fetch from daemon cache; popout has no state until it asks. Estimate: half-day. Pre-req for any window-pair workflow (browse main + read popped-out message, write compose while triaging inbox).
|
|
239
240
|
- **Q133 — TinyMCE: pre-warm CDN fetch / progressive editor swap.** Bob 2026-05-10: "if fetching tinymce takes time it should be done in background and only activate when it is available." Today, picking TinyMCE in Settings means the FIRST compose-open after that setting blocks for ~1s while the CDN bundle downloads. Better: when user toggles to TinyMCE in Settings, fire a fetch (or a `<link rel="modulepreload">`) so the bundle is in browser cache by the time they hit Compose. Even better: open compose with Quill instantly, swap the editor to TinyMCE in place once the bundle resolves. Touches `compose.ts` editor init + `app.ts` Settings toggle handler.
|
|
240
241
|
- **Q132 — importgen bugs blocking "always use importgen" rule for rmfmail.** Three issues, fix in `y:\dev\utils\importgen`: (1) reads CWD's `package.json`, so running from `client/` reads `client/package.json` (declares only quill); needs a `-p` flag or to walk up. (2) `resolveEntryPoint` blows up on packages with conditional `exports` (e.g. `".": { "import": { "node": "...", "browser": "..." } }`) — `dotExport.import` becomes an object, then `entryPoint.startsWith` throws. (3) Doesn't follow dynamic `await import()` calls if it scans imports rather than package.json deps (still need to verify which path it actually takes). Until fixed, rmfmail's import map is hand-maintained.
|
|
241
242
|
- **Q131 — Show holidays in calendar sidebar.** Thunderbird-style "Show holidays" checkbox. When checked, daemon's `getCalendarEvents` ALSO fetches `en.usa#holiday@group.v.calendar.google.com` (Google's standard US holidays) and merges into results, marking each with `isHoliday: true`. UI renders holiday rows with a distinct style (muted color, no edit/delete actions). Concrete edits: (a) `mailx-service/index.ts` line 1075ish `getCalendarEvents` — also call `gsync.listCalendarEvents(tp, fromMs, toMs, "en.usa#holiday@group.v.calendar.google.com")` when settings.calendar.showHolidays is true, store with `is_holiday=1` flag; (b) `mailx-store/db.ts` `calendar_events` schema — add `is_holiday` column; (c) `client/components/calendar-sidebar.ts` — render rows with `data-holiday="1"` styled distinctly; (d) settings UI radio/checkbox in the calendar sidebar's options popover. ~2 hours.
|
package/client/app.js
CHANGED
|
@@ -794,12 +794,68 @@ function showComposeOverlay(title = "Compose") {
|
|
|
794
794
|
wrapper.style.cssText = "position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:#fff;";
|
|
795
795
|
}
|
|
796
796
|
else {
|
|
797
|
-
|
|
797
|
+
// CSS `resize:both` only gives a single lower-right grip. To allow
|
|
798
|
+
// dragging any edge or corner, we omit it here and attach eight
|
|
799
|
+
// manual handles via addComposeResizeHandles() below.
|
|
800
|
+
wrapper.style.cssText = "position:fixed;bottom:0;right:16px;width:min(900px,55vw);height:min(700px,70vh);z-index:1000;border-radius:8px 8px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;overflow:hidden;";
|
|
798
801
|
}
|
|
799
|
-
// Title bar — drag to move, close
|
|
802
|
+
// Title bar — drag to move; right-side cluster holds discard, popout, close.
|
|
800
803
|
const titleBar = document.createElement("div");
|
|
801
804
|
titleBar.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:#e8ecf0;border-radius:8px 8px 0 0;cursor:move;user-select:none;flex-shrink:0;";
|
|
802
|
-
|
|
805
|
+
const titleText = document.createElement("span");
|
|
806
|
+
titleText.textContent = title;
|
|
807
|
+
titleBar.appendChild(titleText);
|
|
808
|
+
const btnCluster = document.createElement("div");
|
|
809
|
+
btnCluster.style.cssText = "display:flex;align-items:center;gap:2px;";
|
|
810
|
+
titleBar.appendChild(btnCluster);
|
|
811
|
+
// Outline SVG icons matched to the rest of the toolbar — 16px, 1.6 stroke,
|
|
812
|
+
// currentColor so hover styles work via plain CSS.
|
|
813
|
+
const SVG_ATTRS = `width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"`;
|
|
814
|
+
const TRASH_SVG = `<svg ${SVG_ATTRS}><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
|
|
815
|
+
const POPOUT_SVG = `<svg ${SVG_ATTRS}><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>`;
|
|
816
|
+
const RESTORE_SVG = `<svg ${SVG_ATTRS}><path d="M21 9v-6h-6"/><path d="M3 15v6h6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>`;
|
|
817
|
+
const makeIconBtn = (svg, titleStr, hoverColor) => {
|
|
818
|
+
const b = document.createElement("button");
|
|
819
|
+
b.title = titleStr;
|
|
820
|
+
b.style.cssText = "background:none;border:none;cursor:pointer;color:#666;padding:4px 6px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;line-height:0;";
|
|
821
|
+
b.innerHTML = svg;
|
|
822
|
+
b.addEventListener("mouseenter", () => b.style.color = hoverColor);
|
|
823
|
+
b.addEventListener("mouseleave", () => b.style.color = "#666");
|
|
824
|
+
return b;
|
|
825
|
+
};
|
|
826
|
+
const discardBtn = makeIconBtn(TRASH_SVG, "Discard draft", "#c00");
|
|
827
|
+
discardBtn.addEventListener("click", () => {
|
|
828
|
+
try {
|
|
829
|
+
const win = frame.contentWindow;
|
|
830
|
+
if (win)
|
|
831
|
+
win.dispatchEvent(new Event("compose-discard"));
|
|
832
|
+
}
|
|
833
|
+
catch { /* */ }
|
|
834
|
+
});
|
|
835
|
+
btnCluster.appendChild(discardBtn);
|
|
836
|
+
// Popout = toggle between the floating overlay and a host-window-filling
|
|
837
|
+
// layout. The compose iframe can't open in a real OS window (msger custom
|
|
838
|
+
// protocol doesn't propagate to child windows), so "popout" here means
|
|
839
|
+
// "fill the host window" — second click restores the floating geometry.
|
|
840
|
+
const popoutBtn = makeIconBtn(POPOUT_SVG, "Maximize", "#000");
|
|
841
|
+
let maximized = false;
|
|
842
|
+
let savedCss = "";
|
|
843
|
+
popoutBtn.addEventListener("click", () => {
|
|
844
|
+
if (!maximized) {
|
|
845
|
+
savedCss = wrapper.style.cssText;
|
|
846
|
+
wrapper.style.cssText = "position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:#fff;overflow:hidden;";
|
|
847
|
+
popoutBtn.innerHTML = RESTORE_SVG;
|
|
848
|
+
popoutBtn.title = "Restore";
|
|
849
|
+
maximized = true;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
wrapper.style.cssText = savedCss;
|
|
853
|
+
popoutBtn.innerHTML = POPOUT_SVG;
|
|
854
|
+
popoutBtn.title = "Maximize";
|
|
855
|
+
maximized = false;
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
btnCluster.appendChild(popoutBtn);
|
|
803
859
|
const closeBtn = document.createElement("button");
|
|
804
860
|
closeBtn.textContent = "✕";
|
|
805
861
|
closeBtn.title = "Save draft and close";
|
|
@@ -818,7 +874,7 @@ function showComposeOverlay(title = "Compose") {
|
|
|
818
874
|
}
|
|
819
875
|
catch { /* */ }
|
|
820
876
|
});
|
|
821
|
-
|
|
877
|
+
btnCluster.appendChild(closeBtn);
|
|
822
878
|
// Drag to move. While dragging we set pointer-events:none on the iframe
|
|
823
879
|
// so mouse events don't get swallowed by the inner document the moment
|
|
824
880
|
// the cursor crosses into the iframe region. Without that, drag only
|
|
@@ -876,13 +932,86 @@ function showComposeOverlay(title = "Compose") {
|
|
|
876
932
|
});
|
|
877
933
|
wrapper.appendChild(titleBar);
|
|
878
934
|
wrapper.appendChild(frame);
|
|
935
|
+
if (!isSmall)
|
|
936
|
+
addComposeResizeHandles(wrapper, frame);
|
|
879
937
|
document.body.appendChild(wrapper);
|
|
880
938
|
}
|
|
939
|
+
/** Attach eight resize grippers (4 edges + 4 corners) to a positioned wrapper.
|
|
940
|
+
* CSS `resize:both` only supports a single lower-right corner; replacing it
|
|
941
|
+
* with manual handles is the only way to make any side / corner draggable.
|
|
942
|
+
* Each handle captures the starting geometry on mousedown, then mousemove
|
|
943
|
+
* adjusts width/height/left/top in the directions implied by which edge. */
|
|
944
|
+
function addComposeResizeHandles(wrapper, frame) {
|
|
945
|
+
const MIN_W = 320;
|
|
946
|
+
const MIN_H = 200;
|
|
947
|
+
const dirs = {
|
|
948
|
+
n: { cursor: "ns-resize", style: "top:-3px;left:12px;right:12px;height:6px;" },
|
|
949
|
+
s: { cursor: "ns-resize", style: "bottom:-3px;left:12px;right:12px;height:6px;" },
|
|
950
|
+
e: { cursor: "ew-resize", style: "right:-3px;top:12px;bottom:12px;width:6px;" },
|
|
951
|
+
w: { cursor: "ew-resize", style: "left:-3px;top:12px;bottom:12px;width:6px;" },
|
|
952
|
+
ne: { cursor: "nesw-resize", style: "top:-3px;right:-3px;width:14px;height:14px;" },
|
|
953
|
+
nw: { cursor: "nwse-resize", style: "top:-3px;left:-3px;width:14px;height:14px;" },
|
|
954
|
+
se: { cursor: "nwse-resize", style: "bottom:-3px;right:-3px;width:14px;height:14px;" },
|
|
955
|
+
sw: { cursor: "nesw-resize", style: "bottom:-3px;left:-3px;width:14px;height:14px;" },
|
|
956
|
+
};
|
|
957
|
+
for (const [dir, conf] of Object.entries(dirs)) {
|
|
958
|
+
const h = document.createElement("div");
|
|
959
|
+
h.style.cssText = `position:absolute;z-index:2;background:transparent;cursor:${conf.cursor};${conf.style}`;
|
|
960
|
+
h.addEventListener("mousedown", (e) => {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
e.stopPropagation();
|
|
963
|
+
const rect = wrapper.getBoundingClientRect();
|
|
964
|
+
const startX = e.clientX, startY = e.clientY;
|
|
965
|
+
const startL = rect.left, startT = rect.top;
|
|
966
|
+
const startW = rect.width, startH = rect.height;
|
|
967
|
+
// Pin to top-left so left/top can be adjusted to grow upward/leftward.
|
|
968
|
+
wrapper.style.left = `${startL}px`;
|
|
969
|
+
wrapper.style.top = `${startT}px`;
|
|
970
|
+
wrapper.style.right = "auto";
|
|
971
|
+
wrapper.style.bottom = "auto";
|
|
972
|
+
frame.style.pointerEvents = "none";
|
|
973
|
+
document.body.style.userSelect = "none";
|
|
974
|
+
const onMove = (ev) => {
|
|
975
|
+
const dx = ev.clientX - startX;
|
|
976
|
+
const dy = ev.clientY - startY;
|
|
977
|
+
let newW = startW, newH = startH, newL = startL, newT = startT;
|
|
978
|
+
if (dir.includes("e"))
|
|
979
|
+
newW = Math.max(MIN_W, startW + dx);
|
|
980
|
+
if (dir.includes("w")) {
|
|
981
|
+
newW = Math.max(MIN_W, startW - dx);
|
|
982
|
+
newL = startL + (startW - newW);
|
|
983
|
+
}
|
|
984
|
+
if (dir.includes("s"))
|
|
985
|
+
newH = Math.max(MIN_H, startH + dy);
|
|
986
|
+
if (dir.includes("n")) {
|
|
987
|
+
newH = Math.max(MIN_H, startH - dy);
|
|
988
|
+
newT = startT + (startH - newH);
|
|
989
|
+
}
|
|
990
|
+
wrapper.style.width = `${newW}px`;
|
|
991
|
+
wrapper.style.height = `${newH}px`;
|
|
992
|
+
wrapper.style.left = `${newL}px`;
|
|
993
|
+
wrapper.style.top = `${newT}px`;
|
|
994
|
+
};
|
|
995
|
+
const onUp = () => {
|
|
996
|
+
frame.style.pointerEvents = "";
|
|
997
|
+
document.body.style.userSelect = "";
|
|
998
|
+
document.removeEventListener("mousemove", onMove);
|
|
999
|
+
document.removeEventListener("mouseup", onUp);
|
|
1000
|
+
};
|
|
1001
|
+
document.addEventListener("mousemove", onMove);
|
|
1002
|
+
document.addEventListener("mouseup", onUp);
|
|
1003
|
+
});
|
|
1004
|
+
wrapper.appendChild(h);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
881
1007
|
// Marketing-email layout tables (deeply nested, fixed widths) collapse to
|
|
882
1008
|
// 30-40px columns inside a phone-width compose pane and wrap text
|
|
883
1009
|
// character-by-character. Strip styles + flatten tables before quoting.
|
|
884
1010
|
function sanitizeQuotedBody(msg) {
|
|
885
|
-
|
|
1011
|
+
// `white-space:pre-wrap` preserves the original line breaks but lets long
|
|
1012
|
+
// lines wrap to the compose width. `<pre>` would have suppressed wrapping
|
|
1013
|
+
// entirely, producing a horizontal-scrolling quote inside the editor.
|
|
1014
|
+
let body = msg.bodyHtml || `<div style="white-space:pre-wrap;font-family:inherit;margin:0">${msg.bodyText || ""}</div>`;
|
|
886
1015
|
body = body.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
887
1016
|
body = body.replace(/\s+style="[^"]*"/gi, "");
|
|
888
1017
|
body = body.replace(/\s+class="[^"]*"/gi, "");
|