@bobfrankston/rmfmail 1.0.680 → 1.0.686
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/build-quill.js +35 -0
- package/bin/lean-accounts.js +0 -1
- package/client/app.bundle.js +172 -55
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +73 -27
- package/client/app.js.map +1 -1
- package/client/app.ts +73 -29
- package/client/components/context-menu.js +2 -0
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +6 -0
- package/client/components/folder-tree.js +26 -4
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +21 -4
- package/client/components/message-list.js +108 -40
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +103 -38
- package/client/compose/compose.bundle.js +189 -17
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/compose/compose.js +51 -3
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +47 -3
- package/client/compose/spellcheck.js +178 -12
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +168 -8
- package/client/lib/api-client.js +3 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -0
- package/client/lib/mailxapi.js +3 -0
- package/client/lib/quill/quill.js +3 -0
- package/client/lib/quill/quill.snow.css +10 -0
- package/client/lib/rmf-tiny.js +25 -6
- package/docs/accounts.md +7 -2
- package/package.json +8 -8
- package/packages/mailx-core/index.d.ts.map +1 -1
- package/packages/mailx-core/index.js +2 -12
- package/packages/mailx-core/index.js.map +1 -1
- package/packages/mailx-core/index.ts +2 -12
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +31 -6
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +32 -6
- package/packages/mailx-imap/node_modules.npmglobalize-stash-11884/.package-lock.json +116 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +22 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +134 -6
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +128 -11
- package/packages/mailx-service/jsonrpc.js +3 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +3 -0
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +15 -12
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +15 -12
- package/packages/mailx-settings/docs/accounts.md +14 -1
- package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
- package/packages/mailx-settings/docs/prod-android.md +88 -0
- package/packages/mailx-settings/docs/prod.md +224 -0
- package/packages/mailx-settings/docs/push-relay.md +141 -0
- package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
- package/packages/mailx-settings/index.d.ts +2 -2
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +13 -10
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +13 -9
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +44 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +47 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +4 -1
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
- package/packages/mailx-store-web/web-settings.js +0 -1
- package/packages/mailx-store-web/web-settings.js.map +1 -1
- package/packages/mailx-store-web/web-settings.ts +0 -1
- package/packages/mailx-types/index.d.ts +1 -2
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +1 -2
- package/packages/mailx-types/package.json +1 -1
|
@@ -70,6 +70,73 @@ function cacheKey(mode: "folder" | "search", a?: string, f?: number, flagged?: b
|
|
|
70
70
|
return `search:${q}`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Per-view position memory — remembered selected UID + scroll position
|
|
74
|
+
* per folder / unified inbox / saved search. Keyed by the same string
|
|
75
|
+
* `cacheKey()` uses so each view has its own slot.
|
|
76
|
+
*
|
|
77
|
+
* Restore rule (Bob 2026-05-12): on return to a view, focus the saved
|
|
78
|
+
* uid if it still exists; otherwise focus the first row whose uid is
|
|
79
|
+
* less than the saved uid (next-older entry in a date-desc list — uid
|
|
80
|
+
* is roughly monotonic with arrival time on most IMAP servers and on
|
|
81
|
+
* Gmail's hash-derived ids). If no smaller uid exists either, fall
|
|
82
|
+
* back to whatever row sits at the same numeric index. Survives a
|
|
83
|
+
* session reload via sessionStorage. */
|
|
84
|
+
interface ViewPosition { uid: number; scroll: number; }
|
|
85
|
+
const positionMemory = new Map<string, ViewPosition>();
|
|
86
|
+
const POSITION_STORAGE_KEY = "mailx-list-positions";
|
|
87
|
+
try {
|
|
88
|
+
const raw = sessionStorage.getItem(POSITION_STORAGE_KEY);
|
|
89
|
+
if (raw) {
|
|
90
|
+
const parsed = JSON.parse(raw) as Record<string, ViewPosition>;
|
|
91
|
+
for (const [k, v] of Object.entries(parsed || {})) {
|
|
92
|
+
if (typeof v?.uid === "number") positionMemory.set(k, v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* */ }
|
|
96
|
+
function persistPositions(): void {
|
|
97
|
+
try {
|
|
98
|
+
const obj: Record<string, ViewPosition> = {};
|
|
99
|
+
for (const [k, v] of positionMemory) obj[k] = v;
|
|
100
|
+
sessionStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify(obj));
|
|
101
|
+
} catch { /* */ }
|
|
102
|
+
}
|
|
103
|
+
function currentViewKey(): string | null {
|
|
104
|
+
if (searchMode) return cacheKey("search", undefined, undefined, undefined, currentSearchQuery);
|
|
105
|
+
if (unifiedMode) return CACHE_KEY_UNIFIED;
|
|
106
|
+
if (!currentAccountId || currentFolderId == null) return null;
|
|
107
|
+
const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
|
|
108
|
+
return cacheKey("folder", currentAccountId, currentFolderId, flaggedOnly);
|
|
109
|
+
}
|
|
110
|
+
function rememberPosition(): void {
|
|
111
|
+
const key = currentViewKey();
|
|
112
|
+
if (!key) return;
|
|
113
|
+
const body = document.getElementById("ml-body");
|
|
114
|
+
if (!body) return;
|
|
115
|
+
const sel = body.querySelector(".ml-row.selected") as HTMLElement | null;
|
|
116
|
+
if (!sel) return;
|
|
117
|
+
const uid = Number(sel.dataset.uid);
|
|
118
|
+
if (!Number.isFinite(uid)) return;
|
|
119
|
+
positionMemory.set(key, { uid, scroll: body.scrollTop });
|
|
120
|
+
persistPositions();
|
|
121
|
+
}
|
|
122
|
+
/** Choose the row to focus when re-entering a view with saved position.
|
|
123
|
+
* Returns the uid to focus, or null to fall back to selectFirst. */
|
|
124
|
+
function pickRestoreUid(items: any[], saved: number): number | null {
|
|
125
|
+
if (!items.length) return null;
|
|
126
|
+
if (items.some(m => m.uid === saved)) return saved;
|
|
127
|
+
// Next-older entry: largest remaining uid that is < saved. Stable
|
|
128
|
+
// even if list is unsorted by uid (loops through all rows).
|
|
129
|
+
let best = -1;
|
|
130
|
+
for (const m of items) {
|
|
131
|
+
if (typeof m.uid !== "number") continue;
|
|
132
|
+
if (m.uid < saved && m.uid > best) best = m.uid;
|
|
133
|
+
}
|
|
134
|
+
if (best >= 0) return best;
|
|
135
|
+
// No older entry — saved was the bottom of the list. Snap to the
|
|
136
|
+
// first row that sorts "after" via date-desc tie-break (top of list).
|
|
137
|
+
return typeof items[0]?.uid === "number" ? items[0].uid : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
73
140
|
/** Quick equality check — same UID set, same flag pattern, same total.
|
|
74
141
|
* Skip re-render when this returns true to avoid DOM churn / scroll-
|
|
75
142
|
* jump on a refresh that didn't change anything. */
|
|
@@ -148,6 +215,8 @@ function focusRow(row: MessageRow): void {
|
|
|
148
215
|
viewerShow(row.accountId, row.msg.uid, row.msg.folderId, undefined, false, row.msg);
|
|
149
216
|
onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
|
|
150
217
|
document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
|
|
218
|
+
// Persist position so returning to this view re-focuses this row.
|
|
219
|
+
rememberPosition();
|
|
151
220
|
}
|
|
152
221
|
|
|
153
222
|
/** Read the currently-focused message envelope. Used by app-level
|
|
@@ -552,29 +621,26 @@ export async function loadUnifiedInbox(autoSelect = true): Promise<void> {
|
|
|
552
621
|
const fromHeader = document.querySelector(".ml-col-from");
|
|
553
622
|
if (fromHeader) fromHeader.textContent = "From";
|
|
554
623
|
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
// "I just selected the 10:03 message and it showed and then bounced
|
|
560
|
-
// back to the 15:49"). Live read picks up whatever the user clicked
|
|
561
|
-
// most recently, even during the await.
|
|
562
|
-
const currentSelectedUid = (): string | null =>
|
|
563
|
-
!autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") || null : null;
|
|
624
|
+
// Saved per-view position wins over current DOM selection — same
|
|
625
|
+
// rationale as loadMessages.
|
|
626
|
+
const remembered = positionMemory.get(CACHE_KEY_UNIFIED);
|
|
627
|
+
const savedScroll = remembered?.scroll ?? (!autoSelect ? body.scrollTop : 0);
|
|
564
628
|
|
|
565
629
|
// Instant first paint from cache. The IPC fires in background below
|
|
566
630
|
// and only triggers a re-render if the result diverges from the
|
|
567
|
-
// cached snapshot.
|
|
568
|
-
// re-render covers staleness. C126 part 1 (the part that doesn't
|
|
569
|
-
// need cross-restart persistence; cold-start pre-render is part 2).
|
|
631
|
+
// cached snapshot.
|
|
570
632
|
const cached = listCache.get(CACHE_KEY_UNIFIED);
|
|
571
633
|
if (cached) {
|
|
572
|
-
const preCacheUid = currentSelectedUid();
|
|
573
634
|
totalMessages = cached.total;
|
|
574
635
|
state.setMessages(cached.items);
|
|
575
636
|
renderMessages(body, "", cached.items);
|
|
576
|
-
|
|
577
|
-
|
|
637
|
+
const targetUid = remembered ? pickRestoreUid(cached.items, remembered.uid) : null;
|
|
638
|
+
if (targetUid != null) {
|
|
639
|
+
body.scrollTop = savedScroll;
|
|
640
|
+
restoreSelection(body, String(targetUid));
|
|
641
|
+
} else if (autoSelect) {
|
|
642
|
+
selectFirst(body);
|
|
643
|
+
}
|
|
578
644
|
} else if (autoSelect) {
|
|
579
645
|
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
580
646
|
}
|
|
@@ -596,17 +662,15 @@ export async function loadUnifiedInbox(autoSelect = true): Promise<void> {
|
|
|
596
662
|
return;
|
|
597
663
|
}
|
|
598
664
|
|
|
599
|
-
// Capture selection immediately before renderMessages clears it,
|
|
600
|
-
// not earlier — preserves any click made during the IPC await.
|
|
601
|
-
const preRenderUid = currentSelectedUid();
|
|
602
665
|
state.setMessages(result.items);
|
|
603
666
|
renderMessages(body, "", result.items);
|
|
604
667
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
} else {
|
|
668
|
+
const targetUid = remembered ? pickRestoreUid(result.items, remembered.uid) : null;
|
|
669
|
+
if (targetUid != null) {
|
|
608
670
|
body.scrollTop = savedScroll;
|
|
609
|
-
restoreSelection(body,
|
|
671
|
+
restoreSelection(body, String(targetUid));
|
|
672
|
+
} else if (autoSelect) {
|
|
673
|
+
selectFirst(body);
|
|
610
674
|
}
|
|
611
675
|
} catch (e: any) {
|
|
612
676
|
if (e.name === "AbortError") return;
|
|
@@ -735,26 +799,27 @@ export async function loadMessages(accountId: string, folderId: number, page = 1
|
|
|
735
799
|
const fromHeader = document.querySelector(".ml-col-from");
|
|
736
800
|
if (fromHeader) fromHeader.textContent = showToInsteadOfFrom ? "To" : "From";
|
|
737
801
|
|
|
738
|
-
const savedScroll = !autoSelect ? body.scrollTop : 0;
|
|
739
|
-
// Live read — see loadUnifiedInbox above for why this can't be
|
|
740
|
-
// captured at function start.
|
|
741
|
-
const currentSelectedUid = (): string | null =>
|
|
742
|
-
!autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") || null : null;
|
|
743
|
-
|
|
744
802
|
const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
|
|
745
803
|
const cKey = cacheKey("folder", accountId, folderId, flaggedOnly);
|
|
804
|
+
// Saved per-view position takes priority over the DOM's current
|
|
805
|
+
// selection — when switching from folder A to B and back, the DOM
|
|
806
|
+
// shows B's selected row but we want to land on whatever was last
|
|
807
|
+
// focused in A.
|
|
808
|
+
const remembered = positionMemory.get(cKey);
|
|
809
|
+
const savedScroll = remembered?.scroll ?? (!autoSelect ? body.scrollTop : 0);
|
|
746
810
|
const cached = listCache.get(cKey);
|
|
747
811
|
if (cached) {
|
|
748
|
-
const preCacheUid = currentSelectedUid();
|
|
749
812
|
totalMessages = cached.total;
|
|
750
813
|
state.setMessages(cached.items);
|
|
751
814
|
renderMessages(body, accountId, cached.items);
|
|
752
|
-
|
|
753
|
-
|
|
815
|
+
const targetUid = remembered ? pickRestoreUid(cached.items, remembered.uid) : null;
|
|
816
|
+
if (targetUid != null) {
|
|
754
817
|
requestAnimationFrame(() => {
|
|
755
818
|
body.scrollTop = savedScroll;
|
|
756
|
-
restoreSelection(body,
|
|
819
|
+
restoreSelection(body, String(targetUid));
|
|
757
820
|
});
|
|
821
|
+
} else if (autoSelect) {
|
|
822
|
+
selectFirst(body);
|
|
758
823
|
}
|
|
759
824
|
} else if (autoSelect) {
|
|
760
825
|
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
@@ -778,19 +843,19 @@ export async function loadMessages(accountId: string, folderId: number, page = 1
|
|
|
778
843
|
return;
|
|
779
844
|
}
|
|
780
845
|
|
|
781
|
-
// Capture selection right before renderMessages clears it.
|
|
782
|
-
const preRenderUid = currentSelectedUid();
|
|
783
846
|
state.setMessages(result.items);
|
|
784
847
|
renderMessages(body, accountId, result.items);
|
|
785
848
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
849
|
+
// Prefer saved position; otherwise default by autoSelect.
|
|
850
|
+
const targetUid = remembered ? pickRestoreUid(result.items, remembered.uid) : null;
|
|
851
|
+
if (targetUid != null) {
|
|
789
852
|
requestAnimationFrame(() => {
|
|
790
853
|
if (myGen !== loadGen) return;
|
|
791
854
|
body.scrollTop = savedScroll;
|
|
792
|
-
restoreSelection(body,
|
|
855
|
+
restoreSelection(body, String(targetUid));
|
|
793
856
|
});
|
|
857
|
+
} else if (autoSelect) {
|
|
858
|
+
selectFirst(body);
|
|
794
859
|
}
|
|
795
860
|
} catch (e: any) {
|
|
796
861
|
if (e.name === "AbortError") return;
|
|
@@ -85,6 +85,7 @@ __export(api_client_exports, {
|
|
|
85
85
|
logClientEvent: () => logClientEvent,
|
|
86
86
|
markAsSpamMessages: () => markAsSpamMessages,
|
|
87
87
|
markFolderRead: () => markFolderRead,
|
|
88
|
+
moveFolderToTrash: () => moveFolderToTrash,
|
|
88
89
|
moveMessage: () => moveMessage,
|
|
89
90
|
moveMessages: () => moveMessages,
|
|
90
91
|
onEvent: () => onEvent,
|
|
@@ -357,6 +358,9 @@ function renameFolder(accountId, folderId, newName) {
|
|
|
357
358
|
function deleteFolder(accountId, folderId) {
|
|
358
359
|
return ipc().deleteFolder?.(accountId, folderId);
|
|
359
360
|
}
|
|
361
|
+
function moveFolderToTrash(accountId, folderId) {
|
|
362
|
+
return ipc().moveFolderToTrash?.(accountId, folderId);
|
|
363
|
+
}
|
|
360
364
|
function emptyFolder(accountId, folderId) {
|
|
361
365
|
return ipc().emptyFolder?.(accountId, folderId);
|
|
362
366
|
}
|
|
@@ -617,7 +621,23 @@ async function createTinyMceEditor(container2, opts = {}) {
|
|
|
617
621
|
return;
|
|
618
622
|
args.content = args.content.replace(/(^|[\s(\[])((?:https?|ftp):\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]])/gi, (_m, lead, url) => `${lead}<a href="${url}">${url}</a>`);
|
|
619
623
|
},
|
|
620
|
-
|
|
624
|
+
// Body font + quoted-reply styling. Without explicit rules for
|
|
625
|
+
// <blockquote> and div.reply the editor iframe renders the
|
|
626
|
+
// quoted block with no visual distinction from the user's own
|
|
627
|
+
// text — pasted reply quotes vanish into a wall of unformatted
|
|
628
|
+
// paragraphs (Bob 2026-05-12: "the body losing all formatting").
|
|
629
|
+
// The styles below match Thunderbird/Outlook conventions: a
|
|
630
|
+
// muted left-bar + indent on blockquotes, slight color shift on
|
|
631
|
+
// the "On … wrote:" attribution, untouched typography for
|
|
632
|
+
// everything else so genuine HTML formatting (bold / italic /
|
|
633
|
+
// tables / inline color) still comes through verbatim.
|
|
634
|
+
content_style: [
|
|
635
|
+
"body { font-family: system-ui, sans-serif; font-size: 14px; }",
|
|
636
|
+
"blockquote { border-left: 3px solid #c0c8d0; margin: 0 0 0 4px; padding: 2px 0 2px 10px; color: #555; }",
|
|
637
|
+
"div.reply { margin-top: 0.5em; }",
|
|
638
|
+
"div.reply > p:first-child { color: #666; font-size: 0.95em; margin: 0 0 4px 0; }",
|
|
639
|
+
"pre, code { font-family: ui-monospace, Consolas, Menlo, monospace; }"
|
|
640
|
+
].join(" "),
|
|
621
641
|
init_instance_callback: (ed) => resolve(ed),
|
|
622
642
|
setup: (ed) => {
|
|
623
643
|
if (opts.initialHtml)
|
|
@@ -714,10 +734,14 @@ async function createTinyMceEditor(container2, opts = {}) {
|
|
|
714
734
|
focus() {
|
|
715
735
|
editor2.focus();
|
|
716
736
|
},
|
|
717
|
-
setCursor(
|
|
737
|
+
setCursor(pos) {
|
|
718
738
|
try {
|
|
719
739
|
editor2.selection.select(editor2.getBody(), true);
|
|
720
|
-
editor2.selection.collapse(
|
|
740
|
+
editor2.selection.collapse(
|
|
741
|
+
pos === 0
|
|
742
|
+
/* true = start */
|
|
743
|
+
);
|
|
744
|
+
editor2.focus();
|
|
721
745
|
} catch {
|
|
722
746
|
}
|
|
723
747
|
},
|
|
@@ -1766,7 +1790,7 @@ function decorate(editor2, sp) {
|
|
|
1766
1790
|
p = p.parentNode;
|
|
1767
1791
|
}
|
|
1768
1792
|
}
|
|
1769
|
-
const
|
|
1793
|
+
const savedAbs = caretAbsOffsetFromBody(body);
|
|
1770
1794
|
try {
|
|
1771
1795
|
editor2.undoManager?.ignore?.(() => {
|
|
1772
1796
|
const old = body.querySelectorAll(`span[${MARKER_ATTR}]`);
|
|
@@ -1791,6 +1815,16 @@ function decorate(editor2, sp) {
|
|
|
1791
1815
|
return NodeFilter.FILTER_ACCEPT;
|
|
1792
1816
|
}
|
|
1793
1817
|
});
|
|
1818
|
+
let caretNode = null;
|
|
1819
|
+
let caretOffset = 0;
|
|
1820
|
+
const liveSel = doc.getSelection();
|
|
1821
|
+
if (liveSel && liveSel.rangeCount > 0) {
|
|
1822
|
+
const f = liveSel.focusNode;
|
|
1823
|
+
if (f && f.nodeType === Node.TEXT_NODE) {
|
|
1824
|
+
caretNode = f;
|
|
1825
|
+
caretOffset = liveSel.focusOffset;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1794
1828
|
const hits = [];
|
|
1795
1829
|
let n = walker.nextNode();
|
|
1796
1830
|
const WORD_RE = /[\p{L}][\p{L}'’\-]*/gu;
|
|
@@ -1803,6 +1837,9 @@ function decorate(editor2, sp) {
|
|
|
1803
1837
|
const word = m[0];
|
|
1804
1838
|
if (word.length < MIN_WORD_LEN)
|
|
1805
1839
|
continue;
|
|
1840
|
+
if (caretNode === tn && caretOffset >= m.index && caretOffset <= m.index + word.length) {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1806
1843
|
if (sp.correct(word))
|
|
1807
1844
|
continue;
|
|
1808
1845
|
hits.push({ node: tn, start: m.index, end: m.index + word.length });
|
|
@@ -1823,11 +1860,72 @@ function decorate(editor2, sp) {
|
|
|
1823
1860
|
}
|
|
1824
1861
|
});
|
|
1825
1862
|
} finally {
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1863
|
+
if (savedAbs != null)
|
|
1864
|
+
restoreCaretFromAbsOffset(body, savedAbs);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
function caretAbsOffsetFromBody(body) {
|
|
1868
|
+
const doc = body.ownerDocument;
|
|
1869
|
+
const sel = doc.getSelection();
|
|
1870
|
+
if (!sel || sel.rangeCount === 0)
|
|
1871
|
+
return null;
|
|
1872
|
+
const focusNode = sel.focusNode;
|
|
1873
|
+
const focusOffset = sel.focusOffset;
|
|
1874
|
+
if (!focusNode)
|
|
1875
|
+
return null;
|
|
1876
|
+
if (focusNode.nodeType !== Node.TEXT_NODE) {
|
|
1877
|
+
let abs2 = 0;
|
|
1878
|
+
const walker2 = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
1879
|
+
let n2 = walker2.nextNode();
|
|
1880
|
+
while (n2) {
|
|
1881
|
+
if (focusNode.contains(n2))
|
|
1882
|
+
break;
|
|
1883
|
+
const cmp = focusNode.compareDocumentPosition(n2);
|
|
1884
|
+
if (cmp & Node.DOCUMENT_POSITION_PRECEDING)
|
|
1885
|
+
abs2 += n2.data.length;
|
|
1886
|
+
n2 = walker2.nextNode();
|
|
1887
|
+
}
|
|
1888
|
+
return abs2;
|
|
1889
|
+
}
|
|
1890
|
+
let abs = 0;
|
|
1891
|
+
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
1892
|
+
let n = walker.nextNode();
|
|
1893
|
+
while (n) {
|
|
1894
|
+
if (n === focusNode)
|
|
1895
|
+
return abs + focusOffset;
|
|
1896
|
+
abs += n.data.length;
|
|
1897
|
+
n = walker.nextNode();
|
|
1898
|
+
}
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
function restoreCaretFromAbsOffset(body, abs) {
|
|
1902
|
+
const doc = body.ownerDocument;
|
|
1903
|
+
const sel = doc.getSelection();
|
|
1904
|
+
if (!sel)
|
|
1905
|
+
return;
|
|
1906
|
+
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
1907
|
+
let acc = 0;
|
|
1908
|
+
let n = walker.nextNode();
|
|
1909
|
+
while (n) {
|
|
1910
|
+
const len = n.data.length;
|
|
1911
|
+
if (acc + len >= abs) {
|
|
1912
|
+
const range = doc.createRange();
|
|
1913
|
+
range.setStart(n, Math.max(0, abs - acc));
|
|
1914
|
+
range.collapse(true);
|
|
1915
|
+
sel.removeAllRanges();
|
|
1916
|
+
sel.addRange(range);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
acc += len;
|
|
1920
|
+
n = walker.nextNode();
|
|
1921
|
+
}
|
|
1922
|
+
const last = walker.previousNode();
|
|
1923
|
+
if (last) {
|
|
1924
|
+
const range = doc.createRange();
|
|
1925
|
+
range.setStart(last, last.data.length);
|
|
1926
|
+
range.collapse(true);
|
|
1927
|
+
sel.removeAllRanges();
|
|
1928
|
+
sel.addRange(range);
|
|
1831
1929
|
}
|
|
1832
1930
|
}
|
|
1833
1931
|
function installDecorationStyle(editor2) {
|
|
@@ -1920,18 +2018,37 @@ function showSuggestionsMenu(parentDoc, x, y, items) {
|
|
|
1920
2018
|
menu.style.left = `${Math.max(8, window.innerWidth - r.width - 8)}px`;
|
|
1921
2019
|
if (r.bottom > window.innerHeight)
|
|
1922
2020
|
menu.style.top = `${Math.max(8, window.innerHeight - r.height - 8)}px`;
|
|
2021
|
+
const docs = [parentDoc];
|
|
2022
|
+
try {
|
|
2023
|
+
const composeWin = parentDoc.defaultView;
|
|
2024
|
+
if (composeWin?.frameElement && composeWin.parent?.document && composeWin.parent.document !== parentDoc) {
|
|
2025
|
+
docs.push(composeWin.parent.document);
|
|
2026
|
+
}
|
|
2027
|
+
} catch {
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
const editorIframe = parentDoc.querySelector("iframe.tox-edit-area__iframe") || parentDoc.querySelector("iframe");
|
|
2031
|
+
const editorDoc = editorIframe?.contentDocument;
|
|
2032
|
+
if (editorDoc && editorDoc !== parentDoc)
|
|
2033
|
+
docs.push(editorDoc);
|
|
2034
|
+
} catch {
|
|
2035
|
+
}
|
|
1923
2036
|
const dismiss2 = (e) => {
|
|
1924
2037
|
if (e.type === "keydown" && e.key !== "Escape")
|
|
1925
2038
|
return;
|
|
1926
2039
|
if (e.type === "mousedown" && menu.contains(e.target))
|
|
1927
2040
|
return;
|
|
1928
2041
|
menu.remove();
|
|
1929
|
-
|
|
1930
|
-
|
|
2042
|
+
for (const d of docs) {
|
|
2043
|
+
d.removeEventListener("mousedown", dismiss2, true);
|
|
2044
|
+
d.removeEventListener("keydown", dismiss2, true);
|
|
2045
|
+
}
|
|
1931
2046
|
};
|
|
1932
2047
|
setTimeout(() => {
|
|
1933
|
-
|
|
1934
|
-
|
|
2048
|
+
for (const d of docs) {
|
|
2049
|
+
d.addEventListener("mousedown", dismiss2, true);
|
|
2050
|
+
d.addEventListener("keydown", dismiss2, true);
|
|
2051
|
+
}
|
|
1935
2052
|
}, 0);
|
|
1936
2053
|
}
|
|
1937
2054
|
function replaceMarker(editor2, marker, replacement) {
|
|
@@ -1993,7 +2110,21 @@ function wireSpellcheck(editor2) {
|
|
|
1993
2110
|
return;
|
|
1994
2111
|
e.preventDefault();
|
|
1995
2112
|
e.stopPropagation();
|
|
1996
|
-
const
|
|
2113
|
+
const transposed = [];
|
|
2114
|
+
for (let i = 0; i < word.length - 1; i++) {
|
|
2115
|
+
const swapped = word.slice(0, i) + word[i + 1] + word[i] + word.slice(i + 2);
|
|
2116
|
+
if (swapped !== word && sp.correct(swapped) && !transposed.includes(swapped)) {
|
|
2117
|
+
transposed.push(swapped);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
const nspellSugs = sp.suggest(word);
|
|
2121
|
+
const sugs = [];
|
|
2122
|
+
for (const s of [...transposed, ...nspellSugs]) {
|
|
2123
|
+
if (!sugs.includes(s))
|
|
2124
|
+
sugs.push(s);
|
|
2125
|
+
if (sugs.length >= 7)
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
1997
2128
|
const iframeEl = editor2.iframeElement;
|
|
1998
2129
|
const iframeRect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1999
2130
|
const items = [];
|
|
@@ -2040,7 +2171,7 @@ var init_spellcheck = __esm({
|
|
|
2040
2171
|
import_nspell = __toESM(require_lib(), 1);
|
|
2041
2172
|
USER_DICT_KEY = "mailx-user-dict";
|
|
2042
2173
|
MARKER_ATTR = "data-mailx-spellerror";
|
|
2043
|
-
DECORATE_DEBOUNCE_MS =
|
|
2174
|
+
DECORATE_DEBOUNCE_MS = 1200;
|
|
2044
2175
|
MIN_WORD_LEN = 3;
|
|
2045
2176
|
SKIP_TAGS = /* @__PURE__ */ new Set(["BLOCKQUOTE", "CODE", "PRE", "A", "SCRIPT", "STYLE", "KBD", "SAMP", "VAR"]);
|
|
2046
2177
|
spellPromise = null;
|
|
@@ -2922,6 +3053,8 @@ function showContextMenu(x, y, items) {
|
|
|
2922
3053
|
const el = document.createElement("div");
|
|
2923
3054
|
el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
|
|
2924
3055
|
el.textContent = item.label;
|
|
3056
|
+
if (item.tooltip)
|
|
3057
|
+
el.title = item.tooltip;
|
|
2925
3058
|
if (!item.disabled) {
|
|
2926
3059
|
el.addEventListener("click", () => {
|
|
2927
3060
|
closeContextMenu();
|
|
@@ -2962,6 +3095,15 @@ document.addEventListener("contextmenu", () => {
|
|
|
2962
3095
|
|
|
2963
3096
|
// client/compose/compose.ts
|
|
2964
3097
|
logClientEvent("compose-module-loaded", { href: location.href, version: window.mailxVersion || "?" });
|
|
3098
|
+
var _composeT0 = performance.now();
|
|
3099
|
+
function _ctick(label) {
|
|
3100
|
+
const ms = (performance.now() - _composeT0).toFixed(0).padStart(5);
|
|
3101
|
+
try {
|
|
3102
|
+
logClientEvent(`compose-tick ${ms}ms ${label}`);
|
|
3103
|
+
} catch {
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
_ctick("module body executing");
|
|
2965
3107
|
function closeCompose() {
|
|
2966
3108
|
logClientEvent("compose-close");
|
|
2967
3109
|
try {
|
|
@@ -3000,8 +3142,8 @@ async function loadEditorAssets(type) {
|
|
|
3000
3142
|
loadScript(`${cdn}/@tiptap/extension-placeholder@2/dist/index.umd.js`)
|
|
3001
3143
|
]);
|
|
3002
3144
|
} else {
|
|
3003
|
-
loadCSS("
|
|
3004
|
-
await loadScript("
|
|
3145
|
+
loadCSS("../lib/quill/quill.snow.css");
|
|
3146
|
+
await loadScript("../lib/quill/quill.js");
|
|
3005
3147
|
}
|
|
3006
3148
|
}
|
|
3007
3149
|
var editorType = "quill";
|
|
@@ -3083,7 +3225,9 @@ async function tryEditor(type) {
|
|
|
3083
3225
|
}
|
|
3084
3226
|
}
|
|
3085
3227
|
var activeEditorType = editorType;
|
|
3228
|
+
_ctick(`editor load start (${editorType})`);
|
|
3086
3229
|
editor = await tryEditor(editorType);
|
|
3230
|
+
_ctick(`editor load end (${editorType}, ok=${!!editor})`);
|
|
3087
3231
|
if (!editor) {
|
|
3088
3232
|
const fallbackType = editorType === "quill" ? "tiptap" : "quill";
|
|
3089
3233
|
logClientEvent("compose-editor-fallback-other", { from: editorType, to: fallbackType });
|
|
@@ -3668,13 +3812,41 @@ function scheduleDraftSave() {
|
|
|
3668
3812
|
});
|
|
3669
3813
|
}, DRAFT_INPUT_DEBOUNCE_MS);
|
|
3670
3814
|
}
|
|
3815
|
+
var _parentInitReady = !!sessionStorage.getItem("composeInit");
|
|
3816
|
+
var _parentInitListeners = [];
|
|
3817
|
+
window.addEventListener("message", (e) => {
|
|
3818
|
+
if (e.data?.type !== "compose-init-ready") return;
|
|
3819
|
+
_parentInitReady = true;
|
|
3820
|
+
for (const fn of _parentInitListeners.splice(0)) fn();
|
|
3821
|
+
});
|
|
3822
|
+
function waitForParentInit(maxMs) {
|
|
3823
|
+
if (_parentInitReady) return Promise.resolve();
|
|
3824
|
+
return new Promise((resolve) => {
|
|
3825
|
+
const timer = setTimeout(() => {
|
|
3826
|
+
_parentInitReady = true;
|
|
3827
|
+
resolve();
|
|
3828
|
+
}, maxMs);
|
|
3829
|
+
_parentInitListeners.push(() => {
|
|
3830
|
+
clearTimeout(timer);
|
|
3831
|
+
resolve();
|
|
3832
|
+
});
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3671
3835
|
(async () => {
|
|
3836
|
+
_ctick("init IIFE start");
|
|
3837
|
+
if (!sessionStorage.getItem("composeInit")) {
|
|
3838
|
+
_ctick("waiting for parent init");
|
|
3839
|
+
await waitForParentInit(1500);
|
|
3840
|
+
_ctick("parent init received");
|
|
3841
|
+
}
|
|
3672
3842
|
const stored = sessionStorage.getItem("composeInit");
|
|
3673
3843
|
if (stored) {
|
|
3674
3844
|
sessionStorage.removeItem("composeInit");
|
|
3675
3845
|
const init = JSON.parse(stored);
|
|
3846
|
+
_ctick(`init parsed (mode=${init.mode}, bodyHtml=${init.bodyHtml?.length || 0} bytes)`);
|
|
3676
3847
|
if (init.accounts && init.accounts.length > 0) {
|
|
3677
3848
|
applyInit(init);
|
|
3849
|
+
_ctick("applyInit done \u2014 compose visible");
|
|
3678
3850
|
getAccounts().then((fresh) => {
|
|
3679
3851
|
if (Array.isArray(fresh) && fresh.length > 0) {
|
|
3680
3852
|
init.accounts = fresh;
|