@bobfrankston/rmfmail 1.1.106 → 1.1.107
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.bundle.js +28 -7
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +58 -14
- package/client/app.js.map +1 -1
- package/client/app.ts +52 -14
- package/client/compose/compose.bundle.js +25 -9
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/rmf-tiny.js +40 -11
- package/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-77444 → node_modules.npmglobalize-stash-61168}/.package-lock.json +0 -0
package/client/app.ts
CHANGED
|
@@ -1237,9 +1237,12 @@ function quoteBody(msg: any): string {
|
|
|
1237
1237
|
const date = new Date(msg.date).toLocaleString();
|
|
1238
1238
|
const from = msg.from.name ? `${msg.from.name} <${msg.from.address}>` : msg.from.address;
|
|
1239
1239
|
const body = sanitizeQuotedBody(msg);
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1240
|
+
// Lead with a real empty paragraph (not bare <br><br>) so every editor
|
|
1241
|
+
// has a proper block for the caret to land in and for the user's reply
|
|
1242
|
+
// to flow into. Bare <br>s aren't a block container — TinyMCE's caret
|
|
1243
|
+
// fell through to the quote and typing landed unpredictably (Bob
|
|
1244
|
+
// 2026-05-21). `<p><br></p>` is the standard "empty editable paragraph".
|
|
1245
|
+
return `<p><br></p><div class="reply"><p>On ${date}, ${from} wrote:</p><blockquote>${body}</blockquote></div>`;
|
|
1243
1246
|
}
|
|
1244
1247
|
|
|
1245
1248
|
function forwardBody(msg: any): string {
|
|
@@ -1247,7 +1250,7 @@ function forwardBody(msg: any): string {
|
|
|
1247
1250
|
const from = msg.from.name ? `${msg.from.name} <${msg.from.address}>` : msg.from.address;
|
|
1248
1251
|
const to = msg.to.map((a: any) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
1249
1252
|
const body = sanitizeQuotedBody(msg);
|
|
1250
|
-
return `<
|
|
1253
|
+
return `<p><br></p><div class="reply"><p>---------- Forwarded message ----------<br>From: ${from}<br>Date: ${date}<br>Subject: ${msg.subject}<br>To: ${to}</p>${body}</div>`;
|
|
1251
1254
|
}
|
|
1252
1255
|
|
|
1253
1256
|
// ── Delete with undo ──
|
|
@@ -1922,23 +1925,38 @@ searchInput?.addEventListener("scroll", () => {
|
|
|
1922
1925
|
searchInput?.addEventListener("change", () => updateSearchHighlight());
|
|
1923
1926
|
updateSearchHighlight();
|
|
1924
1927
|
|
|
1928
|
+
// Server search is slow (per-folder IMAP SEARCH across ~90 folders). It must
|
|
1929
|
+
// never gate the local result display, and it should only fire once typing
|
|
1930
|
+
// has settled. Local search runs immediately on every doSearch; the server
|
|
1931
|
+
// pass is scheduled `SERVER_SEARCH_DALLY_MS` later and any new keystroke
|
|
1932
|
+
// clears+reschedules it. message-list's loadGen counter discards a stale
|
|
1933
|
+
// server pass's results, so a restart is effectively an abort at the UI.
|
|
1934
|
+
const SERVER_SEARCH_DALLY_MS = 700;
|
|
1935
|
+
let serverSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1936
|
+
|
|
1925
1937
|
function doSearch(immediate = false): void {
|
|
1926
1938
|
const query = searchInput.value.trim();
|
|
1927
1939
|
if (query.length === 0) { reloadCurrentFolder(); return; }
|
|
1928
1940
|
if (query.length < 2 && !immediate) return;
|
|
1929
1941
|
|
|
1930
|
-
// P20: orthogonal "Server" checkbox. When checked,
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1942
|
+
// P20: orthogonal "Server" checkbox. When checked, the search ALSO spans
|
|
1943
|
+
// every folder on the server — but as a deferred second phase, never a
|
|
1944
|
+
// replacement for the instant local pass.
|
|
1933
1945
|
const serverCheck = document.getElementById("search-server-too") as HTMLInputElement | null;
|
|
1934
1946
|
const trashCheck = document.getElementById("search-include-trash") as HTMLInputElement | null;
|
|
1935
1947
|
const localScope = searchScope?.value || "all";
|
|
1936
|
-
const effectiveScope = serverCheck?.checked ? "server" : localScope;
|
|
1937
1948
|
const includeTrash = !!trashCheck?.checked;
|
|
1949
|
+
const serverOn = !!serverCheck?.checked;
|
|
1950
|
+
|
|
1951
|
+
// Any re-run aborts a pending server pass — it'll be rescheduled below
|
|
1952
|
+
// if still wanted. This is the "editing the search aborts + restarts"
|
|
1953
|
+
// behavior: each keystroke cancels the prior server search.
|
|
1954
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; }
|
|
1938
1955
|
|
|
1939
|
-
// "This folder" scope: instant client-side filter
|
|
1940
|
-
|
|
1941
|
-
|
|
1956
|
+
// "This folder" scope: instant client-side filter of the visible rows.
|
|
1957
|
+
// Only when the server checkbox is OFF — with it on we want the real
|
|
1958
|
+
// local+server search, not a row filter.
|
|
1959
|
+
if (localScope === "current" && !serverOn && !immediate) {
|
|
1942
1960
|
const body = document.getElementById("ml-body");
|
|
1943
1961
|
if (body) {
|
|
1944
1962
|
const lower = query.toLowerCase();
|
|
@@ -1950,10 +1968,13 @@ function doSearch(immediate = false): void {
|
|
|
1950
1968
|
return;
|
|
1951
1969
|
}
|
|
1952
1970
|
|
|
1953
|
-
|
|
1971
|
+
// ── Phase 1: LOCAL search — always, immediately ──
|
|
1972
|
+
// The local SQLite store is fast; results paint as the user types.
|
|
1973
|
+
const localScopeEff = localScope === "current" ? "current" : "all";
|
|
1974
|
+
loadSearchResults(query, localScopeEff, currentAccountId, currentFolderId, includeTrash);
|
|
1954
1975
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
1955
1976
|
setActiveTabView(
|
|
1956
|
-
{ kind: "search", query, scope:
|
|
1977
|
+
{ kind: "search", query, scope: serverOn ? "server" : localScopeEff, accountId: currentAccountId, folderId: currentFolderId, includeTrash },
|
|
1957
1978
|
`Search: ${query}`,
|
|
1958
1979
|
);
|
|
1959
1980
|
// Record every executed search — Enter AND a settled debounced search
|
|
@@ -1961,6 +1982,18 @@ function doSearch(immediate = false): void {
|
|
|
1961
1982
|
// keystroke progression, so a search the user typed without pressing
|
|
1962
1983
|
// Enter ("Hoddie") still shows up in history.
|
|
1963
1984
|
recordSearchHistory(query);
|
|
1985
|
+
|
|
1986
|
+
// ── Phase 2: SERVER search — deferred, augments phase 1 ──
|
|
1987
|
+
// Fires after the dally so a burst of keystrokes only triggers one IMAP
|
|
1988
|
+
// sweep. The local rows from phase 1 stay on screen the whole time
|
|
1989
|
+
// (loadSearchResults won't blank a list that already has rows), so the
|
|
1990
|
+
// server pass never makes the user stare at an empty list.
|
|
1991
|
+
if (serverOn) {
|
|
1992
|
+
serverSearchTimer = setTimeout(() => {
|
|
1993
|
+
serverSearchTimer = null;
|
|
1994
|
+
loadSearchResults(query, "server", currentAccountId, currentFolderId, includeTrash);
|
|
1995
|
+
}, SERVER_SEARCH_DALLY_MS);
|
|
1996
|
+
}
|
|
1964
1997
|
}
|
|
1965
1998
|
|
|
1966
1999
|
// Track current folder for scoped search
|
|
@@ -1970,6 +2003,7 @@ let reloadDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
1970
2003
|
|
|
1971
2004
|
searchInput?.addEventListener("input", () => {
|
|
1972
2005
|
clearTimeout(searchTimeout);
|
|
2006
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; }
|
|
1973
2007
|
updateSearchHighlight();
|
|
1974
2008
|
if (searchInput.value.trim() === "") {
|
|
1975
2009
|
// Cleared — reset immediately, no debounce. Must exit search mode
|
|
@@ -1981,7 +2015,10 @@ searchInput?.addEventListener("input", () => {
|
|
|
1981
2015
|
reloadCurrentFolder();
|
|
1982
2016
|
setTitle(APP_NAME);
|
|
1983
2017
|
} else {
|
|
1984
|
-
|
|
2018
|
+
// Short debounce — just enough to coalesce a keystroke burst. The
|
|
2019
|
+
// local pass is cheap; the server pass has its own longer dally
|
|
2020
|
+
// inside doSearch, so this stays snappy.
|
|
2021
|
+
searchTimeout = setTimeout(() => doSearch(false), 180);
|
|
1985
2022
|
}
|
|
1986
2023
|
});
|
|
1987
2024
|
searchInput?.addEventListener("keydown", (e) => {
|
|
@@ -1991,6 +2028,7 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
1991
2028
|
}
|
|
1992
2029
|
if (e.key === "Escape") {
|
|
1993
2030
|
searchInput.value = "";
|
|
2031
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; }
|
|
1994
2032
|
updateSearchHighlight();
|
|
1995
2033
|
clearSearchMode();
|
|
1996
2034
|
// Clear any client-side filters
|
|
@@ -824,17 +824,33 @@ async function createTinyMceEditor(container2, opts = {}) {
|
|
|
824
824
|
editor2.focus();
|
|
825
825
|
},
|
|
826
826
|
setCursor(pos) {
|
|
827
|
-
|
|
828
|
-
editor2.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
827
|
+
const place = () => {
|
|
828
|
+
const body = editor2.getBody();
|
|
829
|
+
if (pos === 0) {
|
|
830
|
+
const first = body.firstChild;
|
|
831
|
+
if (first && first.nodeType === 1) {
|
|
832
|
+
editor2.selection.setCursorLocation(first, 0);
|
|
833
|
+
} else {
|
|
834
|
+
editor2.selection.select(body, true);
|
|
835
|
+
editor2.selection.collapse(true);
|
|
836
|
+
}
|
|
837
|
+
editor2.focus();
|
|
835
838
|
editor2.getWin()?.scrollTo(0, 0);
|
|
836
|
-
else
|
|
839
|
+
} else {
|
|
840
|
+
editor2.selection.select(body, true);
|
|
841
|
+
editor2.selection.collapse(false);
|
|
842
|
+
editor2.focus();
|
|
837
843
|
editor2.selection.scrollIntoView();
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
try {
|
|
847
|
+
place();
|
|
848
|
+
editor2.getWin()?.requestAnimationFrame?.(() => {
|
|
849
|
+
try {
|
|
850
|
+
place();
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
});
|
|
838
854
|
} catch {
|
|
839
855
|
}
|
|
840
856
|
},
|