@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.js
CHANGED
|
@@ -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 a real empty paragraph (not bare <br><br>) so every editor
|
|
1286
|
+
// has a proper block for the caret to land in and for the user's reply
|
|
1287
|
+
// to flow into. Bare <br>s aren't a block container — TinyMCE's caret
|
|
1288
|
+
// fell through to the quote and typing landed unpredictably (Bob
|
|
1289
|
+
// 2026-05-21). `<p><br></p>` is the standard "empty editable paragraph".
|
|
1290
|
+
return `<p><br></p><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 `<
|
|
1297
|
+
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>`;
|
|
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,25 @@ 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.
|
|
2046
|
+
if (serverSearchTimer) {
|
|
2047
|
+
clearTimeout(serverSearchTimer);
|
|
2048
|
+
serverSearchTimer = null;
|
|
2049
|
+
}
|
|
2050
|
+
// "This folder" scope: instant client-side filter of the visible rows.
|
|
2051
|
+
// Only when the server checkbox is OFF — with it on we want the real
|
|
2052
|
+
// local+server search, not a row filter.
|
|
2053
|
+
if (localScope === "current" && !serverOn && !immediate) {
|
|
2035
2054
|
const body = document.getElementById("ml-body");
|
|
2036
2055
|
if (body) {
|
|
2037
2056
|
const lower = query.toLowerCase();
|
|
@@ -2042,14 +2061,28 @@ function doSearch(immediate = false) {
|
|
|
2042
2061
|
}
|
|
2043
2062
|
return;
|
|
2044
2063
|
}
|
|
2045
|
-
|
|
2064
|
+
// ── Phase 1: LOCAL search — always, immediately ──
|
|
2065
|
+
// The local SQLite store is fast; results paint as the user types.
|
|
2066
|
+
const localScopeEff = localScope === "current" ? "current" : "all";
|
|
2067
|
+
loadSearchResults(query, localScopeEff, currentAccountId, currentFolderId, includeTrash);
|
|
2046
2068
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
2047
|
-
setActiveTabView({ kind: "search", query, scope:
|
|
2069
|
+
setActiveTabView({ kind: "search", query, scope: serverOn ? "server" : localScopeEff, accountId: currentAccountId, folderId: currentFolderId, includeTrash }, `Search: ${query}`);
|
|
2048
2070
|
// Record every executed search — Enter AND a settled debounced search
|
|
2049
2071
|
// alike. The prefix-pruning in recordSearchHistory() collapses the
|
|
2050
2072
|
// keystroke progression, so a search the user typed without pressing
|
|
2051
2073
|
// Enter ("Hoddie") still shows up in history.
|
|
2052
2074
|
recordSearchHistory(query);
|
|
2075
|
+
// ── Phase 2: SERVER search — deferred, augments phase 1 ──
|
|
2076
|
+
// Fires after the dally so a burst of keystrokes only triggers one IMAP
|
|
2077
|
+
// sweep. The local rows from phase 1 stay on screen the whole time
|
|
2078
|
+
// (loadSearchResults won't blank a list that already has rows), so the
|
|
2079
|
+
// server pass never makes the user stare at an empty list.
|
|
2080
|
+
if (serverOn) {
|
|
2081
|
+
serverSearchTimer = setTimeout(() => {
|
|
2082
|
+
serverSearchTimer = null;
|
|
2083
|
+
loadSearchResults(query, "server", currentAccountId, currentFolderId, includeTrash);
|
|
2084
|
+
}, SERVER_SEARCH_DALLY_MS);
|
|
2085
|
+
}
|
|
2053
2086
|
}
|
|
2054
2087
|
// Track current folder for scoped search
|
|
2055
2088
|
let currentAccountId = "";
|
|
@@ -2057,6 +2090,10 @@ let currentFolderId = 0;
|
|
|
2057
2090
|
let reloadDebounceTimer = null;
|
|
2058
2091
|
searchInput?.addEventListener("input", () => {
|
|
2059
2092
|
clearTimeout(searchTimeout);
|
|
2093
|
+
if (serverSearchTimer) {
|
|
2094
|
+
clearTimeout(serverSearchTimer);
|
|
2095
|
+
serverSearchTimer = null;
|
|
2096
|
+
}
|
|
2060
2097
|
updateSearchHighlight();
|
|
2061
2098
|
if (searchInput.value.trim() === "") {
|
|
2062
2099
|
// Cleared — reset immediately, no debounce. Must exit search mode
|
|
@@ -2070,7 +2107,10 @@ searchInput?.addEventListener("input", () => {
|
|
|
2070
2107
|
setTitle(APP_NAME);
|
|
2071
2108
|
}
|
|
2072
2109
|
else {
|
|
2073
|
-
|
|
2110
|
+
// Short debounce — just enough to coalesce a keystroke burst. The
|
|
2111
|
+
// local pass is cheap; the server pass has its own longer dally
|
|
2112
|
+
// inside doSearch, so this stays snappy.
|
|
2113
|
+
searchTimeout = setTimeout(() => doSearch(false), 180);
|
|
2074
2114
|
}
|
|
2075
2115
|
});
|
|
2076
2116
|
searchInput?.addEventListener("keydown", (e) => {
|
|
@@ -2080,6 +2120,10 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
2080
2120
|
}
|
|
2081
2121
|
if (e.key === "Escape") {
|
|
2082
2122
|
searchInput.value = "";
|
|
2123
|
+
if (serverSearchTimer) {
|
|
2124
|
+
clearTimeout(serverSearchTimer);
|
|
2125
|
+
serverSearchTimer = null;
|
|
2126
|
+
}
|
|
2083
2127
|
updateSearchHighlight();
|
|
2084
2128
|
clearSearchMode();
|
|
2085
2129
|
// Clear any client-side filters
|