@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.js
CHANGED
|
@@ -7,7 +7,7 @@ import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, rel
|
|
|
7
7
|
import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
|
|
8
8
|
import { initTabs, setActiveView as setActiveTabView, openTab } from "./components/tabs.js";
|
|
9
9
|
import { getCurrentMessage, initViewer, popOutCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu, wrapHtmlBody } from "./components/message-viewer.js";
|
|
10
|
-
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";
|
|
10
|
+
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";
|
|
11
11
|
import * as messageState from "./lib/message-state.js";
|
|
12
12
|
// ── New message badge (favicon + title) ──
|
|
13
13
|
/** The user-visible app name. Single point of change for the rename;
|
|
@@ -1282,16 +1282,19 @@ function quoteBody(msg) {
|
|
|
1282
1282
|
const date = new Date(msg.date).toLocaleString();
|
|
1283
1283
|
const from = msg.from.name ? `${msg.from.name} <${msg.from.address}>` : msg.from.address;
|
|
1284
1284
|
const body = sanitizeQuotedBody(msg);
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1285
|
+
// Lead with an empty paragraph so every editor has a real block for the
|
|
1286
|
+
// caret to land in and the user's reply to flow into — bare <br>s aren't
|
|
1287
|
+
// a block container, so TinyMCE's caret fell through to the quote (Bob
|
|
1288
|
+
// 2026-05-21). The blank-line spacing lives in <br>s AFTER the </p>, not
|
|
1289
|
+
// inside it.
|
|
1290
|
+
return `<p></p><br><br><div class="reply"><p>On ${date}, ${from} wrote:</p><blockquote>${body}</blockquote></div>`;
|
|
1288
1291
|
}
|
|
1289
1292
|
function forwardBody(msg) {
|
|
1290
1293
|
const date = new Date(msg.date).toLocaleString();
|
|
1291
1294
|
const from = msg.from.name ? `${msg.from.name} <${msg.from.address}>` : msg.from.address;
|
|
1292
1295
|
const to = msg.to.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
1293
1296
|
const body = sanitizeQuotedBody(msg);
|
|
1294
|
-
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>`;
|
|
1297
|
+
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>`;
|
|
1295
1298
|
}
|
|
1296
1299
|
let lastDeleted = null;
|
|
1297
1300
|
let lastMoved = null;
|
|
@@ -2013,6 +2016,14 @@ searchInput?.addEventListener("scroll", () => {
|
|
|
2013
2016
|
});
|
|
2014
2017
|
searchInput?.addEventListener("change", () => updateSearchHighlight());
|
|
2015
2018
|
updateSearchHighlight();
|
|
2019
|
+
// Server search is slow (per-folder IMAP SEARCH across ~90 folders). It must
|
|
2020
|
+
// never gate the local result display, and it should only fire once typing
|
|
2021
|
+
// has settled. Local search runs immediately on every doSearch; the server
|
|
2022
|
+
// pass is scheduled `SERVER_SEARCH_DALLY_MS` later and any new keystroke
|
|
2023
|
+
// clears+reschedules it. message-list's loadGen counter discards a stale
|
|
2024
|
+
// server pass's results, so a restart is effectively an abort at the UI.
|
|
2025
|
+
const SERVER_SEARCH_DALLY_MS = 700;
|
|
2026
|
+
let serverSearchTimer = null;
|
|
2016
2027
|
function doSearch(immediate = false) {
|
|
2017
2028
|
const query = searchInput.value.trim();
|
|
2018
2029
|
if (query.length === 0) {
|
|
@@ -2021,17 +2032,28 @@ function doSearch(immediate = false) {
|
|
|
2021
2032
|
}
|
|
2022
2033
|
if (query.length < 2 && !immediate)
|
|
2023
2034
|
return;
|
|
2024
|
-
// P20: orthogonal "Server" checkbox. When checked,
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2035
|
+
// P20: orthogonal "Server" checkbox. When checked, the search ALSO spans
|
|
2036
|
+
// every folder on the server — but as a deferred second phase, never a
|
|
2037
|
+
// replacement for the instant local pass.
|
|
2027
2038
|
const serverCheck = document.getElementById("search-server-too");
|
|
2028
2039
|
const trashCheck = document.getElementById("search-include-trash");
|
|
2029
2040
|
const localScope = searchScope?.value || "all";
|
|
2030
|
-
const effectiveScope = serverCheck?.checked ? "server" : localScope;
|
|
2031
2041
|
const includeTrash = !!trashCheck?.checked;
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2042
|
+
const serverOn = !!serverCheck?.checked;
|
|
2043
|
+
// Any re-run aborts a pending server pass — it'll be rescheduled below
|
|
2044
|
+
// if still wanted. This is the "editing the search aborts + restarts"
|
|
2045
|
+
// behavior: each keystroke cancels the prior server search. clearTimeout
|
|
2046
|
+
// kills a not-yet-fired pass; cancelServerSearch() aborts one that's
|
|
2047
|
+
// already mid-sweep on the daemon (generation bump → loop bails).
|
|
2048
|
+
if (serverSearchTimer) {
|
|
2049
|
+
clearTimeout(serverSearchTimer);
|
|
2050
|
+
serverSearchTimer = null;
|
|
2051
|
+
}
|
|
2052
|
+
cancelServerSearch();
|
|
2053
|
+
// "This folder" scope: instant client-side filter of the visible rows.
|
|
2054
|
+
// Only when the server checkbox is OFF — with it on we want the real
|
|
2055
|
+
// local+server search, not a row filter.
|
|
2056
|
+
if (localScope === "current" && !serverOn && !immediate) {
|
|
2035
2057
|
const body = document.getElementById("ml-body");
|
|
2036
2058
|
if (body) {
|
|
2037
2059
|
const lower = query.toLowerCase();
|
|
@@ -2042,14 +2064,28 @@ function doSearch(immediate = false) {
|
|
|
2042
2064
|
}
|
|
2043
2065
|
return;
|
|
2044
2066
|
}
|
|
2045
|
-
|
|
2067
|
+
// ── Phase 1: LOCAL search — always, immediately ──
|
|
2068
|
+
// The local SQLite store is fast; results paint as the user types.
|
|
2069
|
+
const localScopeEff = localScope === "current" ? "current" : "all";
|
|
2070
|
+
loadSearchResults(query, localScopeEff, currentAccountId, currentFolderId, includeTrash);
|
|
2046
2071
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
2047
|
-
setActiveTabView({ kind: "search", query, scope:
|
|
2072
|
+
setActiveTabView({ kind: "search", query, scope: serverOn ? "server" : localScopeEff, accountId: currentAccountId, folderId: currentFolderId, includeTrash }, `Search: ${query}`);
|
|
2048
2073
|
// Record every executed search — Enter AND a settled debounced search
|
|
2049
2074
|
// alike. The prefix-pruning in recordSearchHistory() collapses the
|
|
2050
2075
|
// keystroke progression, so a search the user typed without pressing
|
|
2051
2076
|
// Enter ("Hoddie") still shows up in history.
|
|
2052
2077
|
recordSearchHistory(query);
|
|
2078
|
+
// ── Phase 2: SERVER search — deferred, augments phase 1 ──
|
|
2079
|
+
// Fires after the dally so a burst of keystrokes only triggers one IMAP
|
|
2080
|
+
// sweep. The local rows from phase 1 stay on screen the whole time
|
|
2081
|
+
// (loadSearchResults won't blank a list that already has rows), so the
|
|
2082
|
+
// server pass never makes the user stare at an empty list.
|
|
2083
|
+
if (serverOn) {
|
|
2084
|
+
serverSearchTimer = setTimeout(() => {
|
|
2085
|
+
serverSearchTimer = null;
|
|
2086
|
+
loadSearchResults(query, "server", currentAccountId, currentFolderId, includeTrash);
|
|
2087
|
+
}, SERVER_SEARCH_DALLY_MS);
|
|
2088
|
+
}
|
|
2053
2089
|
}
|
|
2054
2090
|
// Track current folder for scoped search
|
|
2055
2091
|
let currentAccountId = "";
|
|
@@ -2057,6 +2093,11 @@ let currentFolderId = 0;
|
|
|
2057
2093
|
let reloadDebounceTimer = null;
|
|
2058
2094
|
searchInput?.addEventListener("input", () => {
|
|
2059
2095
|
clearTimeout(searchTimeout);
|
|
2096
|
+
if (serverSearchTimer) {
|
|
2097
|
+
clearTimeout(serverSearchTimer);
|
|
2098
|
+
serverSearchTimer = null;
|
|
2099
|
+
cancelServerSearch();
|
|
2100
|
+
}
|
|
2060
2101
|
updateSearchHighlight();
|
|
2061
2102
|
if (searchInput.value.trim() === "") {
|
|
2062
2103
|
// Cleared — reset immediately, no debounce. Must exit search mode
|
|
@@ -2070,7 +2111,10 @@ searchInput?.addEventListener("input", () => {
|
|
|
2070
2111
|
setTitle(APP_NAME);
|
|
2071
2112
|
}
|
|
2072
2113
|
else {
|
|
2073
|
-
|
|
2114
|
+
// Short debounce — just enough to coalesce a keystroke burst. The
|
|
2115
|
+
// local pass is cheap; the server pass has its own longer dally
|
|
2116
|
+
// inside doSearch, so this stays snappy.
|
|
2117
|
+
searchTimeout = setTimeout(() => doSearch(false), 180);
|
|
2074
2118
|
}
|
|
2075
2119
|
});
|
|
2076
2120
|
searchInput?.addEventListener("keydown", (e) => {
|
|
@@ -2080,6 +2124,11 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
2080
2124
|
}
|
|
2081
2125
|
if (e.key === "Escape") {
|
|
2082
2126
|
searchInput.value = "";
|
|
2127
|
+
if (serverSearchTimer) {
|
|
2128
|
+
clearTimeout(serverSearchTimer);
|
|
2129
|
+
serverSearchTimer = null;
|
|
2130
|
+
}
|
|
2131
|
+
cancelServerSearch();
|
|
2083
2132
|
updateSearchHighlight();
|
|
2084
2133
|
clearSearchMode();
|
|
2085
2134
|
// Clear any client-side filters
|
|
@@ -4481,6 +4530,17 @@ getSettings().then((s) => {
|
|
|
4481
4530
|
}).catch(() => { });
|
|
4482
4531
|
// Save editor choice to server settings
|
|
4483
4532
|
function saveEditorSetting(editor) {
|
|
4533
|
+
// Update the localStorage cache SYNCHRONOUSLY. compose.ts reads
|
|
4534
|
+
// `mailx-editor-type` from localStorage at module-load to pick the
|
|
4535
|
+
// editor — its async getSettings() refresh only runs when a compose
|
|
4536
|
+
// window opens, and reads localStorage FIRST. Without this write the
|
|
4537
|
+
// cache stays stale and the next compose keeps the old editor until a
|
|
4538
|
+
// full app restart (Bob 2026-05-21: "changed to quill but got tinymce
|
|
4539
|
+
// until I restarted"). With it, the very next compose-open is correct.
|
|
4540
|
+
try {
|
|
4541
|
+
localStorage.setItem("mailx-editor-type", editor);
|
|
4542
|
+
}
|
|
4543
|
+
catch { /* private mode */ }
|
|
4484
4544
|
getSettings().then((settings) => {
|
|
4485
4545
|
settings.ui = { ...settings.ui, editor };
|
|
4486
4546
|
saveSettings(settings);
|