@bobfrankston/rmfmail 1.0.622 → 1.0.627
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/TODO.md +4 -0
- package/client/app.js +57 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +47 -0
- package/client/components/message-list.js +19 -0
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +17 -0
- package/client/components/message-viewer.js +22 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +17 -0
- package/client/index.html +34 -3
- package/package.json +1 -1
- /package/client/{android.html → android.html.deleted} +0 -0
package/TODO.md
CHANGED
|
@@ -787,6 +787,10 @@ Was part of S1 "local-first reconciliation refactor" until 2026-04-23 when S1's
|
|
|
787
787
|
|
|
788
788
|
| Item | Version |
|
|
789
789
|
|---|---|
|
|
790
|
+
| **WebView timing forwarded to daemon log + cleanups** — Daemon log shows boot is **~204 ms to showService** (2026-05-09 14:23 run). The "long startup" delay is entirely WebView2 init + JS module cascade + first paint after the daemon hands off. The `[wv N ms]` / `[preview N ms]` / `[search N ms]` marks were `console.log`-only — invisible to the daemon log. Now forwarded via `mailxapi.logClientEvent` so they land alongside `[boot N ms]` for full timeline. Plus: `android.html` removed (renamed `.deleted`); `silent=true` on logit URLs (Android + WebView error handlers) so verbose marks don't spam jserv UI; build clean. Run mailx, check the log — actual breakdown of where the time goes is now visible. | v1.0.626 |
|
|
791
|
+
| **Preview-path timing checkpoints** — User report 2026-05-09: "still seeing very long delays on previews that should be instantaneous? It finally displayed but why?" The boot timing answers cold-start; this answers click-to-preview. New `[preview N ms] showMessage entry / parsedCache HIT|MISS / IPC done (cached, N bytes html) / iframe srcdoc set / iframe load (rendered)` marks. Slow paths now have stage-by-stage breakdown — IPC vs parser vs iframe-paint becomes visible without speculation. | v1.0.625 |
|
|
792
|
+
| **Search history dropdown (recent queries)** — User request 2026-05-09: "the search bar should be a dropdown offering me recent searches so I don't need to type them again. Just recent dropdowns is fine for now without editing and remove from history. Editing is implicit in bringing up a search and being able to edit it." Native HTML5 `<datalist id="search-history">` bound to `#search-input` via `list=` attribute — browser picker handles dropdown UX with no custom code. Backed by localStorage `mailx-search-history` (cap 25, move-to-front dedup). Recorded only when `doSearch(immediate=true)` fires (Enter / scope change) so debounced keystrokes don't pollute the list. Picking a past entry brings it into the input where the user can edit and re-Enter — the edited version becomes the new top entry. | v1.0.624 |
|
|
793
|
+
| **List/search/startup polish + measurement** — User stack 2026-05-09: (a) "long startup — would be nice to see a countup time in addition to the spinner." Live elapsed-seconds counter (ticks 100 ms) appended to `#startup-status`; auto-stops when overlay is removed. (b) "deleted entries reappeared after switching windows." `removeMessagesAndReconcile` now `listCache.clear()`s — pre-fix the per-folder snapshot cache repainted the deleted rows on next visit. (c) "switched to All Inboxes but search is still showing — being ignored?" Both folder handlers (regular + unified) now call `clearSearchMode()` AND clear the input — previously only the input was cleared, leaving `searchMode = true` so subsequent loads were rerouted. (d) "search is suddenly slow." Added `[search N ms] start / IPC begin / IPC done (n/total)` console marks so the actual cost is visible. Pair with `[boot N ms]` and `[wv N ms]` for a full timing picture. | v1.0.623 |
|
|
790
794
|
| **PWA convergence: one HTML, runtime bootstrap detect, MAUI points at index.html** — User insight 2026-05-09: "If I run on a Pixel tablet it should look like the desktop... You're missing the entire point of the PWA approach!" Two HTML files (`index.html` desktop, `android.html` MAUI) had been forking — different toolbars, different button labels, different search controls, different reset wording. CSS responsive layout already drives wide-vs-narrow; the HTML fork was forcing phone-style on tablets regardless of viewport. Convergence: `index.html` now carries the union of platform-runtime concerns (Android import map for in-WebView module resolution, logit error handlers, single boot router that branches on `!window.mailxapi` — present on desktop because msger pre-injects via initScript, absent on Android until `initAndroid()` runs). MAUI's `MainPage.xaml.cs` flipped `webView.Source` to `client/index.html`. `android.html` left in place as a safety net for now; delete after a successful tablet/phone smoke test. Pixel tablet → desktop layout (CSS wide breakpoint). Phone → narrow layout. Browser → desktop layout. Same code path. Plus per-stage boot timestamps `[wv N ms]` from html-parse-start through first rAF, paired with the daemon's `[boot N ms]` lines from v1.0.611 — actual measurement, not theorizing. | v1.0.621 |
|
|
791
795
|
| **Header encoded-words: uniform decode at the storage seam** — User reports 2026-05-09: ".eml file with strange header" (chase / spam .eml had `From: "=?UTF-8?B?...?=" <s@x>` — encoded-word inside quoted-string, RFC-strictly forbidden but common in the wild). Bob: "you aren't parsing encoded header fields correctly. It's not subject that is encoded. You should uniformly process header escapes early in the parsing." Right answer: not per-callsite patches. Added `decodeHeaderWords` helper at the top of `mailx-store/db.ts` using `libmime.decodeWords`; called once in `upsertMessage` for `subject`, `from.name`, `to[].name`, `cc[].name`. Every storage path (IMAP fetch, Gmail API, manual append, eml replay) now decodes uniformly. Safe on already-decoded text — no `=?` markers → no-op. Old rows in DB keep their bad names until re-fetched; new fetches and any sync action that re-upserts the row pick up the fix. | v1.0.618 |
|
|
792
796
|
| **List selection: clicks during in-flight IPC no longer bounce** — User report 2026-05-09: "I just selected the 10:03 message and it showed and then bounced back to the 15:49. I have to be quick to capture the 10:03 before it bounces away." Root cause: `loadUnifiedInbox` and `loadMessages` captured `savedUid` at function start, then awaited the IPC. If you clicked a different row during the await, the new selection painted briefly, then `renderMessages` (post-IPC) cleared it and `restoreSelection` restored the *captured* old uid — your click was overwritten. Fix: read the selected uid LIVE — once before each `restoreSelection` call (just before `renderMessages` clears the DOM). The user's mid-flight click survives the re-render. | v1.0.617 |
|
package/client/app.js
CHANGED
|
@@ -337,8 +337,11 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
337
337
|
currentFolderSpecialUse = specialUse;
|
|
338
338
|
currentAccountId = accountId;
|
|
339
339
|
currentFolderId = folderId;
|
|
340
|
+
// Drop search state on folder switch — input alone wasn't enough,
|
|
341
|
+
// searchMode stayed true and the next loadMessages was rerouted.
|
|
340
342
|
if (searchInput)
|
|
341
343
|
searchInput.value = "";
|
|
344
|
+
clearSearchMode();
|
|
342
345
|
markAsSeen();
|
|
343
346
|
releaseFocus();
|
|
344
347
|
loadMessages(accountId, folderId, 1, specialUse);
|
|
@@ -348,6 +351,14 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
348
351
|
}, () => {
|
|
349
352
|
// Unified inbox handler
|
|
350
353
|
currentFolderSpecialUse = "inbox";
|
|
354
|
+
// Clear search state — switching folders should drop any active filter.
|
|
355
|
+
// Pre-fix: the search input retained its old value AND search mode
|
|
356
|
+
// stayed active, so "All Inboxes" appeared to be ignoring its own
|
|
357
|
+
// contents (Bob 2026-05-09: "I just switched to all inboxes but the
|
|
358
|
+
// search is still showing even though all the entries are there.").
|
|
359
|
+
if (searchInput)
|
|
360
|
+
searchInput.value = "";
|
|
361
|
+
clearSearchMode();
|
|
351
362
|
releaseFocus();
|
|
352
363
|
loadUnifiedInbox();
|
|
353
364
|
setTitle(`${APP_NAME} - All Inboxes`);
|
|
@@ -1479,6 +1490,48 @@ document.addEventListener("mailx-share-intent", ((e) => {
|
|
|
1479
1490
|
let searchTimeout;
|
|
1480
1491
|
const searchInput = document.getElementById("search-input");
|
|
1481
1492
|
const searchScope = document.getElementById("search-scope");
|
|
1493
|
+
// Recent-searches dropdown — backed by localStorage, populated into the
|
|
1494
|
+
// existing <datalist id="search-history"> so the browser's native picker
|
|
1495
|
+
// shows on focus/click. No edit / delete UI; editing is implicit (pick a
|
|
1496
|
+
// past query, modify, hit Enter — the modified version is recorded).
|
|
1497
|
+
// Cap at 25 to keep the list usable.
|
|
1498
|
+
const SEARCH_HISTORY_KEY = "mailx-search-history";
|
|
1499
|
+
const SEARCH_HISTORY_MAX = 25;
|
|
1500
|
+
function loadSearchHistory() {
|
|
1501
|
+
try {
|
|
1502
|
+
return JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || "[]");
|
|
1503
|
+
}
|
|
1504
|
+
catch {
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function saveSearchHistory(list) {
|
|
1509
|
+
try {
|
|
1510
|
+
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(list));
|
|
1511
|
+
}
|
|
1512
|
+
catch { /* private mode */ }
|
|
1513
|
+
}
|
|
1514
|
+
function refreshSearchHistoryDatalist() {
|
|
1515
|
+
const dl = document.getElementById("search-history");
|
|
1516
|
+
if (!dl)
|
|
1517
|
+
return;
|
|
1518
|
+
const items = loadSearchHistory();
|
|
1519
|
+
dl.innerHTML = items.map(q => `<option value="${q.replace(/"/g, """)}"></option>`).join("");
|
|
1520
|
+
}
|
|
1521
|
+
function recordSearchHistory(query) {
|
|
1522
|
+
const trimmed = query.trim();
|
|
1523
|
+
if (!trimmed)
|
|
1524
|
+
return;
|
|
1525
|
+
const cur = loadSearchHistory();
|
|
1526
|
+
// Move-to-front dedup: if already there, pull it; then prepend.
|
|
1527
|
+
const filtered = cur.filter(q => q !== trimmed);
|
|
1528
|
+
filtered.unshift(trimmed);
|
|
1529
|
+
if (filtered.length > SEARCH_HISTORY_MAX)
|
|
1530
|
+
filtered.length = SEARCH_HISTORY_MAX;
|
|
1531
|
+
saveSearchHistory(filtered);
|
|
1532
|
+
refreshSearchHistoryDatalist();
|
|
1533
|
+
}
|
|
1534
|
+
refreshSearchHistoryDatalist();
|
|
1482
1535
|
function doSearch(immediate = false) {
|
|
1483
1536
|
const query = searchInput.value.trim();
|
|
1484
1537
|
if (query.length === 0) {
|
|
@@ -1510,6 +1563,10 @@ function doSearch(immediate = false) {
|
|
|
1510
1563
|
}
|
|
1511
1564
|
loadSearchResults(query, effectiveScope, currentAccountId, currentFolderId, includeTrash);
|
|
1512
1565
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
1566
|
+
// Only record on `immediate=true` (Enter / scope change) — debounced
|
|
1567
|
+
// typing would otherwise add every keystroke as its own entry.
|
|
1568
|
+
if (immediate)
|
|
1569
|
+
recordSearchHistory(query);
|
|
1513
1570
|
}
|
|
1514
1571
|
// Track current folder for scoped search
|
|
1515
1572
|
let currentAccountId = "";
|