@bobfrankston/rmfmail 1.1.106 → 1.1.108
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 +39 -7
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +75 -15
- package/client/app.js.map +1 -1
- package/client/app.ts +66 -16
- package/client/compose/compose.bundle.js +29 -9
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +5 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +6 -0
- package/client/lib/rmf-tiny.js +40 -11
- package/package.json +3 -3
- package/packages/mailx-service/index.d.ts +8 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +26 -1
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +28 -0
- 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-imap/{node_modules.npmglobalize-stash-77444 → node_modules.npmglobalize-stash-72068}/.package-lock.json +0 -0
package/client/app.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, rel
|
|
|
8
8
|
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
9
9
|
import { initTabs, setActiveView as setActiveTabView, openTab, type ViewTab } from "./components/tabs.js";
|
|
10
10
|
import { showMessage, getCurrentMessage, initViewer, popOutCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu, wrapHtmlBody } from "./components/message-viewer.js";
|
|
11
|
-
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessage, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage, subscribeStore } from "./lib/api-client.js";
|
|
11
|
+
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessage, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage, subscribeStore, cancelServerSearch } from "./lib/api-client.js";
|
|
12
12
|
import * as messageState from "./lib/message-state.js";
|
|
13
13
|
|
|
14
14
|
// ── New message badge (favicon + title) ──
|
|
@@ -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 an empty paragraph so every editor has a real block for the
|
|
1241
|
+
// caret to land in and the user's reply to flow into — bare <br>s aren't
|
|
1242
|
+
// a block container, so TinyMCE's caret fell through to the quote (Bob
|
|
1243
|
+
// 2026-05-21). The blank-line spacing lives in <br>s AFTER the </p>, not
|
|
1244
|
+
// inside it.
|
|
1245
|
+
return `<p></p><br><br><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 `<br><br><div class="reply"><p>---------- Forwarded message ----------<br>From: ${from}<br>Date: ${date}<br>Subject: ${msg.subject}<br>To: ${to}</p>${body}</div>`;
|
|
1253
|
+
return `<p></p><br><br><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,41 @@ 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;
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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. clearTimeout
|
|
1954
|
+
// kills a not-yet-fired pass; cancelServerSearch() aborts one that's
|
|
1955
|
+
// already mid-sweep on the daemon (generation bump → loop bails).
|
|
1956
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; }
|
|
1957
|
+
cancelServerSearch();
|
|
1958
|
+
|
|
1959
|
+
// "This folder" scope: instant client-side filter of the visible rows.
|
|
1960
|
+
// Only when the server checkbox is OFF — with it on we want the real
|
|
1961
|
+
// local+server search, not a row filter.
|
|
1962
|
+
if (localScope === "current" && !serverOn && !immediate) {
|
|
1942
1963
|
const body = document.getElementById("ml-body");
|
|
1943
1964
|
if (body) {
|
|
1944
1965
|
const lower = query.toLowerCase();
|
|
@@ -1950,10 +1971,13 @@ function doSearch(immediate = false): void {
|
|
|
1950
1971
|
return;
|
|
1951
1972
|
}
|
|
1952
1973
|
|
|
1953
|
-
|
|
1974
|
+
// ── Phase 1: LOCAL search — always, immediately ──
|
|
1975
|
+
// The local SQLite store is fast; results paint as the user types.
|
|
1976
|
+
const localScopeEff = localScope === "current" ? "current" : "all";
|
|
1977
|
+
loadSearchResults(query, localScopeEff, currentAccountId, currentFolderId, includeTrash);
|
|
1954
1978
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
1955
1979
|
setActiveTabView(
|
|
1956
|
-
{ kind: "search", query, scope:
|
|
1980
|
+
{ kind: "search", query, scope: serverOn ? "server" : localScopeEff, accountId: currentAccountId, folderId: currentFolderId, includeTrash },
|
|
1957
1981
|
`Search: ${query}`,
|
|
1958
1982
|
);
|
|
1959
1983
|
// Record every executed search — Enter AND a settled debounced search
|
|
@@ -1961,6 +1985,18 @@ function doSearch(immediate = false): void {
|
|
|
1961
1985
|
// keystroke progression, so a search the user typed without pressing
|
|
1962
1986
|
// Enter ("Hoddie") still shows up in history.
|
|
1963
1987
|
recordSearchHistory(query);
|
|
1988
|
+
|
|
1989
|
+
// ── Phase 2: SERVER search — deferred, augments phase 1 ──
|
|
1990
|
+
// Fires after the dally so a burst of keystrokes only triggers one IMAP
|
|
1991
|
+
// sweep. The local rows from phase 1 stay on screen the whole time
|
|
1992
|
+
// (loadSearchResults won't blank a list that already has rows), so the
|
|
1993
|
+
// server pass never makes the user stare at an empty list.
|
|
1994
|
+
if (serverOn) {
|
|
1995
|
+
serverSearchTimer = setTimeout(() => {
|
|
1996
|
+
serverSearchTimer = null;
|
|
1997
|
+
loadSearchResults(query, "server", currentAccountId, currentFolderId, includeTrash);
|
|
1998
|
+
}, SERVER_SEARCH_DALLY_MS);
|
|
1999
|
+
}
|
|
1964
2000
|
}
|
|
1965
2001
|
|
|
1966
2002
|
// Track current folder for scoped search
|
|
@@ -1970,6 +2006,7 @@ let reloadDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
1970
2006
|
|
|
1971
2007
|
searchInput?.addEventListener("input", () => {
|
|
1972
2008
|
clearTimeout(searchTimeout);
|
|
2009
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; cancelServerSearch(); }
|
|
1973
2010
|
updateSearchHighlight();
|
|
1974
2011
|
if (searchInput.value.trim() === "") {
|
|
1975
2012
|
// Cleared — reset immediately, no debounce. Must exit search mode
|
|
@@ -1981,7 +2018,10 @@ searchInput?.addEventListener("input", () => {
|
|
|
1981
2018
|
reloadCurrentFolder();
|
|
1982
2019
|
setTitle(APP_NAME);
|
|
1983
2020
|
} else {
|
|
1984
|
-
|
|
2021
|
+
// Short debounce — just enough to coalesce a keystroke burst. The
|
|
2022
|
+
// local pass is cheap; the server pass has its own longer dally
|
|
2023
|
+
// inside doSearch, so this stays snappy.
|
|
2024
|
+
searchTimeout = setTimeout(() => doSearch(false), 180);
|
|
1985
2025
|
}
|
|
1986
2026
|
});
|
|
1987
2027
|
searchInput?.addEventListener("keydown", (e) => {
|
|
@@ -1991,6 +2031,8 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
1991
2031
|
}
|
|
1992
2032
|
if (e.key === "Escape") {
|
|
1993
2033
|
searchInput.value = "";
|
|
2034
|
+
if (serverSearchTimer) { clearTimeout(serverSearchTimer); serverSearchTimer = null; }
|
|
2035
|
+
cancelServerSearch();
|
|
1994
2036
|
updateSearchHighlight();
|
|
1995
2037
|
clearSearchMode();
|
|
1996
2038
|
// Clear any client-side filters
|
|
@@ -4181,6 +4223,14 @@ getSettings().then((s: any) => {
|
|
|
4181
4223
|
|
|
4182
4224
|
// Save editor choice to server settings
|
|
4183
4225
|
function saveEditorSetting(editor: string): void {
|
|
4226
|
+
// Update the localStorage cache SYNCHRONOUSLY. compose.ts reads
|
|
4227
|
+
// `mailx-editor-type` from localStorage at module-load to pick the
|
|
4228
|
+
// editor — its async getSettings() refresh only runs when a compose
|
|
4229
|
+
// window opens, and reads localStorage FIRST. Without this write the
|
|
4230
|
+
// cache stays stale and the next compose keeps the old editor until a
|
|
4231
|
+
// full app restart (Bob 2026-05-21: "changed to quill but got tinymce
|
|
4232
|
+
// until I restarted"). With it, the very next compose-open is correct.
|
|
4233
|
+
try { localStorage.setItem("mailx-editor-type", editor); } catch { /* private mode */ }
|
|
4184
4234
|
getSettings().then((settings: any) => {
|
|
4185
4235
|
settings.ui = { ...settings.ui, editor };
|
|
4186
4236
|
saveSettings(settings);
|
|
@@ -44,6 +44,7 @@ __export(api_client_exports, {
|
|
|
44
44
|
allowRemoteContent: () => allowRemoteContent,
|
|
45
45
|
autocomplete: () => autocomplete,
|
|
46
46
|
cancelQueuedOutgoing: () => cancelQueuedOutgoing,
|
|
47
|
+
cancelServerSearch: () => cancelServerSearch,
|
|
47
48
|
closeWordEdit: () => closeWordEdit,
|
|
48
49
|
connectEvents: () => connectEvents,
|
|
49
50
|
connectWebSocket: () => connectWebSocket,
|
|
@@ -217,6 +218,9 @@ function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
|
217
218
|
function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0, includeTrashSpam = false) {
|
|
218
219
|
return ipc().searchMessages(query, page, pageSize, scope, accountId, folderId, includeTrashSpam);
|
|
219
220
|
}
|
|
221
|
+
function cancelServerSearch() {
|
|
222
|
+
return ipc().cancelServerSearch?.();
|
|
223
|
+
}
|
|
220
224
|
function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
221
225
|
return ipc().getMessage(accountId, uid, allowRemote, folderId);
|
|
222
226
|
}
|
|
@@ -824,17 +828,33 @@ async function createTinyMceEditor(container2, opts = {}) {
|
|
|
824
828
|
editor2.focus();
|
|
825
829
|
},
|
|
826
830
|
setCursor(pos) {
|
|
827
|
-
|
|
828
|
-
editor2.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
831
|
+
const place = () => {
|
|
832
|
+
const body = editor2.getBody();
|
|
833
|
+
if (pos === 0) {
|
|
834
|
+
const first = body.firstChild;
|
|
835
|
+
if (first && first.nodeType === 1) {
|
|
836
|
+
editor2.selection.setCursorLocation(first, 0);
|
|
837
|
+
} else {
|
|
838
|
+
editor2.selection.select(body, true);
|
|
839
|
+
editor2.selection.collapse(true);
|
|
840
|
+
}
|
|
841
|
+
editor2.focus();
|
|
835
842
|
editor2.getWin()?.scrollTo(0, 0);
|
|
836
|
-
else
|
|
843
|
+
} else {
|
|
844
|
+
editor2.selection.select(body, true);
|
|
845
|
+
editor2.selection.collapse(false);
|
|
846
|
+
editor2.focus();
|
|
837
847
|
editor2.selection.scrollIntoView();
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
try {
|
|
851
|
+
place();
|
|
852
|
+
editor2.getWin()?.requestAnimationFrame?.(() => {
|
|
853
|
+
try {
|
|
854
|
+
place();
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
});
|
|
838
858
|
} catch {
|
|
839
859
|
}
|
|
840
860
|
},
|