@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.
@@ -236,6 +236,60 @@ export function showPreviewBodyMenu(absX: number, absY: number, selectedText: st
236
236
  showContextMenu(absX, absY, items);
237
237
  }
238
238
 
239
+ /** How long the viewer waits for a body before it stops counting and
240
+ * explains itself. Long enough that a slow Dovecot read or a cold Gmail
241
+ * batch still lands inside it; short enough that nobody watches a counter
242
+ * tick past a minute wondering whether the app is alive. */
243
+ const WAIT_GIVE_UP_SECS = 45;
244
+
245
+ /**
246
+ * Replace the elapsed-time counter with something that tells the user what
247
+ * happened and what they can still do.
248
+ *
249
+ * The counter existed to distinguish "slow" from "wedged" (v1.0.x, Bob
250
+ * 2026-05-24) and it does — but only for the first half-minute. Past that it
251
+ * is a progress bar for a process that is not running: nothing retries, no
252
+ * error was reported, and the number just grows. This says so.
253
+ */
254
+ function renderBodyFetchGaveUp(
255
+ bodyEl: HTMLElement,
256
+ accountId: string,
257
+ uid: number,
258
+ folderId: number | undefined,
259
+ secs: number,
260
+ previewText: string,
261
+ specialUse?: string,
262
+ envelope?: any,
263
+ ): void {
264
+ // A Gmail account addresses bodies by a uid derived from the Gmail
265
+ // message id — a ~10^14 number. A small sequential uid on such an
266
+ // account means this row came from the IMAP side (server search, an
267
+ // older sync), and the Gmail API fetcher has no way to name it. That is
268
+ // not a slow network and retrying will not help, so say which it is
269
+ // instead of offering false hope.
270
+ const looksImapUidOnApiAccount = uid > 0 && uid < 1_000_000_000;
271
+ const why = looksImapUidOnApiAccount
272
+ ? `This message was indexed over IMAP, but its account fetches bodies through the Gmail API, which addresses messages by a different id. The body can't be requested for this row.`
273
+ : `The server accepted the request but never returned the body.`;
274
+ const snippet = previewText
275
+ ? `<div class="mv-preview-placeholder">${escapeHtml(previewText)}<div class="mv-tear-line"><span>✂ snippet — this is all that arrived ✂</span></div></div>`
276
+ : "";
277
+ bodyEl.innerHTML = `${snippet}<div class="mv-system-message mv-system-error">
278
+ <div class="mv-system-tag">rmfmail</div>
279
+ <div class="mv-system-title">Body didn't arrive after ${secs}s — not still loading</div>
280
+ <div class="mv-system-body">${escapeHtml(why)}
281
+ <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
282
+ <button id="mv-body-retry" class="tb-btn">Try again</button>
283
+ </div></div></div>`;
284
+ bodyEl.querySelector("#mv-body-retry")?.addEventListener("click", () => {
285
+ invalidateParsedCache(accountId, folderId, uid);
286
+ showMessage(accountId, uid, folderId, specialUse, true, envelope).catch(() => { /* reported by the next render */ });
287
+ });
288
+ // No "View source" button here — the viewer toolbar already has Source,
289
+ // and a second control that dispatches an event nothing listens for is
290
+ // the silent-no-op pattern this session spent the day removing.
291
+ }
292
+
239
293
  /** Toggle preview-fullscreen mode. CSS rule on body.fullscreen-preview hides
240
294
  * toolbar / rail / folder tree / list / statusbar and expands the viewer to
241
295
  * the full viewport. Exits via second toggle, Esc, or the floating ✕. */
@@ -777,12 +831,30 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
777
831
  // After 30 s a still-counting timer is suspicious — surface
778
832
  // a hint so the user knows the daemon should be looked at.
779
833
  if (secs === 30) span.classList.add("mv-wait-slow");
834
+ // …and at 45 s, stop counting and SAY something. A counter
835
+ // that runs forever is not information: the fetch is not
836
+ // "still working", it has failed in a way nothing reports —
837
+ // e.g. a Gmail row indexed over IMAP (small sequential uid)
838
+ // whose body the Gmail API path cannot address at all,
839
+ // because it only knows uids derived from Gmail message ids
840
+ // (Bob 2026-08-09: "definitely need an appropriate message
841
+ // rather than an infinitely counting timer"). Whatever the
842
+ // cause, at this point the honest thing is to say the body
843
+ // did not arrive, keep the snippet visible, and offer the
844
+ // paths that DO work.
845
+ if (secs >= WAIT_GIVE_UP_SECS) {
846
+ clearInterval(tick);
847
+ off?.();
848
+ renderBodyFetchGaveUp(bodyEl, accountId, uid, folderId, secs, previewText, specialUse, envelope || cached || undefined);
849
+ }
780
850
  }, 1000);
781
851
  // Subscribe via Store bus (the post-refactor surface). The
782
852
  // reconciler publishes `bodyAvailable` on `message:<uuid>` (or
783
853
  // `folder:<id>` when uuid isn't available); the wildcard catches
784
854
  // both. Match by account+uid because the viewer keys off those.
785
- const off = subscribeStore("*", (ev: any) => {
855
+ // eslint-disable-next-line prefer-const assigned below, referenced by the tick above
856
+ let off: (() => void) | undefined;
857
+ off = subscribeStore("*", (ev: any) => {
786
858
  if (ev.kind !== "bodyAvailable") return;
787
859
  if (ev.accountId !== accountId || ev.uid !== uid) return;
788
860
  if (captureGen !== showMessageGeneration) { off(); clearInterval(tick); return; }
@@ -2645,6 +2645,20 @@ async function createTinyMceEditor(container2, opts = {}) {
2645
2645
  // 2026-07-28: pasted/inserted text needs a one-click quote.
2646
2646
  "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help"
2647
2647
  ].join(" | "),
2648
+ // Wrap the toolbar onto as many rows as it needs instead of hiding
2649
+ // the overflow behind a "…" that opens a FLOATING popover over the
2650
+ // message body (TinyMCE default is toolbar_mode:"floating").
2651
+ //
2652
+ // Two things were wrong with the popover: it covers the text you
2653
+ // are writing, and it does not dismiss on its own (Bob 2026-08-08:
2654
+ // "you temporarily extend the menu as a popover but that does not
2655
+ // go away on its own. It makes more sense to just wrap the buttons
2656
+ // onto a separate line. That works because the width is stable so
2657
+ // there is not any fibrillation"). Wrapping grows the header, which
2658
+ // is the honest use of the space — the compose window width does
2659
+ // not change while you type, so the row count is stable and there
2660
+ // is no reflow churn.
2661
+ toolbar_mode: "wrap",
2648
2662
  // Include "tools" so wordcount and searchreplace are reachable.
2649
2663
  menubar: "file edit view insert format tools",
2650
2664
  // View menu — append zoom commands. fullscreen omitted: this editor