@bobfrankston/rmfmail 1.2.241 → 1.2.242

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.
@@ -275,6 +275,17 @@ body {
275
275
  read a large message being replied to (Bob 2026-06-23). Forcing min-height:0
276
276
  bounds each wrapper so the edit-area iframe gets a fixed height and TinyMCE's
277
277
  own internal scrollbar engages. */
278
+ /* The toolbar wraps onto extra rows (toolbar_mode:"wrap" in rmf-tiny) rather
279
+ than hiding buttons behind a "…" popover that covers the message body. For
280
+ that to be an ENLARGED bar rather than a clipped one, the header must not be
281
+ a shrink target: it keeps its natural height and the edit area below gives up
282
+ the space (Bob 2026-08-09: "better to just make the bar bigger if it cannot
283
+ be wider"). Without this the header inherits flex-shrink:1 and a narrow
284
+ compose window silently crops the second row. */
285
+ .editor-tinymce .tox-editor-header {
286
+ flex: 0 0 auto !important;
287
+ }
288
+
278
289
  .editor-tinymce .tox-editor-container,
279
290
  .editor-tinymce .tox-sidebar-wrap,
280
291
  .editor-tinymce .tox-edit-area {
@@ -135,6 +135,20 @@ export async function createTinyMceEditor(container, opts = {}) {
135
135
  // 2026-07-28: pasted/inserted text needs a one-click quote.
136
136
  "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help",
137
137
  ].join(" | "),
138
+ // Wrap the toolbar onto as many rows as it needs instead of hiding
139
+ // the overflow behind a "…" that opens a FLOATING popover over the
140
+ // message body (TinyMCE default is toolbar_mode:"floating").
141
+ //
142
+ // Two things were wrong with the popover: it covers the text you
143
+ // are writing, and it does not dismiss on its own (Bob 2026-08-08:
144
+ // "you temporarily extend the menu as a popover but that does not
145
+ // go away on its own. It makes more sense to just wrap the buttons
146
+ // onto a separate line. That works because the width is stable so
147
+ // there is not any fibrillation"). Wrapping grows the header, which
148
+ // is the honest use of the space — the compose window width does
149
+ // not change while you type, so the row count is stable and there
150
+ // is no reflow churn.
151
+ toolbar_mode: "wrap",
138
152
  // Include "tools" so wordcount and searchreplace are reachable.
139
153
  menubar: "file edit view insert format tools",
140
154
  // View menu — append zoom commands. fullscreen omitted: this editor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.241",
3
+ "version": "1.2.242",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -38,7 +38,7 @@
38
38
  "@bobfrankston/mailx-store-web": "^0.1.76",
39
39
  "@bobfrankston/mailx-sync": "^0.1.29",
40
40
  "@bobfrankston/miscinfo": "^1.0.16",
41
- "@bobfrankston/msger": "^0.1.422",
41
+ "@bobfrankston/msger": "^0.1.424",
42
42
  "@bobfrankston/node-tcp-transport": "^0.1.10",
43
43
  "@bobfrankston/oauthsupport": "^1.0.34",
44
44
  "@bobfrankston/rmf-tiny": "^0.1.45",
@@ -118,7 +118,7 @@
118
118
  "@bobfrankston/mailx-store-web": "^0.1.76",
119
119
  "@bobfrankston/mailx-sync": "^0.1.29",
120
120
  "@bobfrankston/miscinfo": "^1.0.16",
121
- "@bobfrankston/msger": "^0.1.422",
121
+ "@bobfrankston/msger": "^0.1.424",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
124
  "@bobfrankston/rmf-tiny": "^0.1.45",
@@ -2038,6 +2038,11 @@ export class MailxService implements MailxApi {
2038
2038
  let foldersSearched = 0;
2039
2039
  let foldersFailed = 0;
2040
2040
  const failedFolders: string[] = [];
2041
+ // Folders the server says are gone: counted apart from transient
2042
+ // failures so the UI never tells the user to retry something that
2043
+ // cannot succeed.
2044
+ let foldersGone = 0;
2045
+ const goneFolders: string[] = [];
2041
2046
  let droppedHits = 0;
2042
2047
 
2043
2048
  // Bounded concurrency. The per-account ops queue already
@@ -2071,9 +2076,28 @@ export class MailxService implements MailxApi {
2071
2076
  for (let j = 0; j < results.length; j++) {
2072
2077
  const r = results[j];
2073
2078
  if (r.status !== "fulfilled") {
2079
+ const why = String(r.reason?.message || r.reason || "");
2080
+ // A mailbox the server says does not exist is not a
2081
+ // timeout and will never succeed — every search pays
2082
+ // for it again and the user is told to "retry to
2083
+ // search them again", which cannot work. Gmail
2084
+ // answers `[NONEXISTENT] Unknown Mailbox` for the
2085
+ // `[Imap]/…` rows mailx keeps locally; Dovecot says
2086
+ // "doesn't exist". Drop the local folder row (the
2087
+ // IMAP sync path already does this on its own
2088
+ // wording) and count it separately.
2089
+ if (/\[NONEXISTENT\]|Unknown Mailbox|doesn't exist|does not exist/i.test(why)) {
2090
+ foldersGone++;
2091
+ goneFolders.push(`${acct.id}/${batch[j].path}`);
2092
+ console.log(` [server-search] ${acct.id}/${batch[j].path}: mailbox not on server — removing the stale local folder row`);
2093
+ try { this.db.deleteFolder(batch[j].id); } catch (e: any) {
2094
+ console.error(` [server-search] could not remove stale folder ${batch[j].path}: ${e.message}`);
2095
+ }
2096
+ continue;
2097
+ }
2074
2098
  foldersFailed++;
2075
2099
  failedFolders.push(`${acct.id}/${batch[j].path}`);
2076
- console.error(` [server-search] ${acct.id}/${batch[j].path}: ${r.reason?.message || r.reason}`);
2100
+ console.error(` [server-search] ${acct.id}/${batch[j].path}: ${why}`);
2077
2101
  continue;
2078
2102
  }
2079
2103
  foldersSearched++;
@@ -2101,14 +2125,20 @@ export class MailxService implements MailxApi {
2101
2125
  // Newest first, then paginate.
2102
2126
  items.sort((a: any, b: any) => (b.date?.getTime?.() || 0) - (a.date?.getTime?.() || 0));
2103
2127
  const sliced = items.slice((page - 1) * pageSize, page * pageSize);
2104
- if (foldersFailed > 0 || droppedHits > 0) {
2105
- console.log(` [server-search] q="${q}" — ${total} hits across ${foldersSearched} folders; ${foldersFailed} folders failed, ${droppedHits} hits dropped`);
2128
+ if (foldersFailed > 0 || droppedHits > 0 || foldersGone > 0) {
2129
+ console.log(` [server-search] q="${q}" — ${total} hits across ${foldersSearched} folders; ${foldersFailed} failed, ${foldersGone} stale folder(s) removed, ${droppedHits} hits dropped${aborted ? " (superseded)" : ""}`);
2106
2130
  }
2107
2131
  return {
2108
2132
  items: sliced, total, page, pageSize,
2109
2133
  aborted,
2110
- partial: foldersFailed > 0 || droppedHits > 0,
2134
+ // A superseded search is NOT a partial result — the user typed
2135
+ // another character and this generation was abandoned on
2136
+ // purpose. Reporting its untouched folders as failures is what
2137
+ // produced "99 could not be searched (timeout / connection)"
2138
+ // for a search nothing had gone wrong with (Bob 2026-08-09).
2139
+ partial: !aborted && (foldersFailed > 0 || droppedHits > 0),
2111
2140
  foldersSearched, foldersFailed, failedFolders, droppedHits,
2141
+ foldersGone, goneFolders,
2112
2142
  };
2113
2143
  } else if (scope === "current" && accountId && folderId) {
2114
2144
  // Per-folder search — folder scope is the user's explicit