@bobfrankston/rmfmail 1.2.182 → 1.2.184

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/bin/mailx.ts CHANGED
@@ -2131,19 +2131,33 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2131
2131
  }
2132
2132
  }, 5000);
2133
2133
  }
2134
+ // Open reminder popups by occurrence key, so a dismiss/snooze made on
2135
+ // ANOTHER machine (seen via the reminders.jsonc state sync) can retract
2136
+ // a popup that's already on this machine's screen. `popupKey` is
2137
+ // supplied by alarms.ts and stripped before the opts reach msger.
2138
+ const reminderPopups = new Map<string, { close: () => void }>();
2134
2139
  svc.setPopupFn(async (opts: any) => {
2135
2140
  // Own WebView2 profile so a reminder popup can never share (and
2136
2141
  // crash) the main window's browser process — see S67 note on the
2137
2142
  // popout windows below. Service-supplied opts may override.
2138
- const h = showMessageBoxEx({ profile: "popout", ...opts });
2143
+ const { popupKey, ...boxOpts } = opts || {};
2144
+ const h = showMessageBoxEx({ profile: "popout", ...boxOpts });
2139
2145
  if (typeof h.pid === "number") addChildPid(h.pid);
2146
+ if (popupKey) reminderPopups.set(popupKey, { close: () => { try { h.close(); } catch { /* already gone */ } } });
2140
2147
  try {
2141
2148
  const r = await h.result;
2142
2149
  return { button: r.button, closed: r.closed, dismissed: r.dismissed, form: (r as any).form };
2143
2150
  } finally {
2144
2151
  if (typeof h.pid === "number") removeChildPid(h.pid);
2152
+ if (popupKey) reminderPopups.delete(popupKey);
2145
2153
  }
2146
2154
  });
2155
+ svc.setPopupCloserFn((key: string) => {
2156
+ const p = reminderPopups.get(key);
2157
+ if (!p) return false;
2158
+ p.close();
2159
+ return true;
2160
+ });
2147
2161
 
2148
2162
  // Optional debug HTTP server ALONGSIDE the GUI. `--server` mode replaces
2149
2163
  // the WebView with Express; `--debug-server` instead runs a loopback
@@ -29,6 +29,7 @@ __export(api_client_exports, {
29
29
  cancelServerSearch: () => cancelServerSearch,
30
30
  checkDraftNewer: () => checkDraftNewer,
31
31
  closeComposePopout: () => closeComposePopout,
32
+ closeReminderPopup: () => closeReminderPopup,
32
33
  closeWordEdit: () => closeWordEdit,
33
34
  connectEvents: () => connectEvents,
34
35
  connectWebSocket: () => connectWebSocket,
@@ -621,6 +622,9 @@ function closeWordEdit(editId) {
621
622
  function showReminderPopup(opts) {
622
623
  return ipc().showReminderPopup?.(opts) ?? Promise.resolve({ button: "", reason: "no host" });
623
624
  }
625
+ function closeReminderPopup(key) {
626
+ return ipc().closeReminderPopup?.(key) ?? Promise.resolve({ ok: false });
627
+ }
624
628
  function mergeReminderState(patch) {
625
629
  return ipc().mergeReminderState?.(patch) ?? Promise.resolve(null);
626
630
  }
@@ -1595,6 +1599,19 @@ function clearViewer() {
1595
1599
  headerEl.hidden = true;
1596
1600
  if (attEl)
1597
1601
  attEl.hidden = true;
1602
+ setTimeout(() => {
1603
+ try {
1604
+ const stillPlaceholder = !!document.querySelector("#mv-body .mv-empty");
1605
+ if (!stillPlaceholder)
1606
+ return;
1607
+ const sel = document.querySelectorAll(".ml-row.selected");
1608
+ if (sel.length === 0)
1609
+ return;
1610
+ const uids = Array.from(sel).slice(0, 5).map((r) => `${r.dataset.accountId}/${r.dataset.uid}`);
1611
+ window.mailxapi?.logClientEvent?.("[fate-share] viewer empty but rows still highlighted", { rows: uids });
1612
+ } catch {
1613
+ }
1614
+ }, 1e3);
1598
1615
  }
1599
1616
  function confirmDeleteAttachments(msg, accountId, uid, ids, question) {
1600
1617
  const backdrop = document.createElement("div");
@@ -3109,8 +3126,23 @@ var init_message_viewer = __esm({
3109
3126
  return;
3110
3127
  if (!currentMessage)
3111
3128
  return;
3129
+ const evUuid = ev.msgUuid || ev.uuid;
3130
+ if (evUuid && currentMessage.uuid) {
3131
+ if (ev.accountId === currentAccountId && evUuid === currentMessage.uuid) {
3132
+ try {
3133
+ window.mailxapi?.logClientEvent?.("[fate-share] viewed message removed (uuid) \u2014 clearing viewer", { uuid: evUuid });
3134
+ } catch {
3135
+ }
3136
+ clearViewer();
3137
+ }
3138
+ return;
3139
+ }
3112
3140
  const folderMatches = ev.folderId == null || currentMessage.folderId == null || ev.folderId === currentMessage.folderId;
3113
3141
  if (ev.accountId === currentAccountId && ev.uid === currentMessage.uid && folderMatches) {
3142
+ try {
3143
+ window.mailxapi?.logClientEvent?.("[fate-share] viewed message removed (uid) \u2014 clearing viewer", { uid: ev.uid, folderId: ev.folderId ?? null });
3144
+ } catch {
3145
+ }
3114
3146
  clearViewer();
3115
3147
  }
3116
3148
  });
@@ -3499,10 +3531,17 @@ function focusRow(row, opts = {}) {
3499
3531
  if (opts.scroll !== false) {
3500
3532
  row.el.scrollIntoView({ block: "nearest" });
3501
3533
  }
3502
- showMessage(row.accountId, row.msg.uid, row.msg.folderId, currentSpecialUse || void 0, false, row.msg);
3503
- onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
3504
- document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
3505
3534
  rememberPosition();
3535
+ try {
3536
+ showMessage(row.accountId, row.msg.uid, row.msg.folderId, currentSpecialUse || void 0, false, row.msg);
3537
+ onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
3538
+ document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
3539
+ } catch (e) {
3540
+ try {
3541
+ logClientEvent("[fate-share] focusRow viewer drive threw", { uid: row.msg?.uid, accountId: row.accountId, err: e?.message || String(e), stack: e?.stack || null });
3542
+ } catch {
3543
+ }
3544
+ }
3506
3545
  }
3507
3546
  function getCurrentFocused() {
3508
3547
  return focusedRow ? focusedRow.msg : null;
@@ -4212,8 +4251,13 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
4212
4251
  setMessages(result.items);
4213
4252
  withScrollAnchor(body, () => renderMessages(body, accountId, result.items, { background: !autoSelect }));
4214
4253
  if (autoSelect && focusedRow) {
4215
- const stillThere = result.items.some((m) => m.accountId === focusedRow.accountId && m.uid === focusedRow.msg.uid);
4254
+ const fUuid = focusedRow.msg?.uuid;
4255
+ const stillThere = result.items.some((m) => fUuid && m.uuid ? m.uuid === fUuid : m.accountId === focusedRow.accountId && m.uid === focusedRow.msg.uid);
4216
4256
  if (!stillThere) {
4257
+ try {
4258
+ logClientEvent("[fate-share] focused row gone after foreground load \u2014 clearing viewer", { uid: focusedRow.msg?.uid, uuid: fUuid || null, accountId: focusedRow.accountId });
4259
+ } catch {
4260
+ }
4217
4261
  clearViewer();
4218
4262
  focusedRow = null;
4219
4263
  }
@@ -4309,8 +4353,13 @@ function renderMessages(body, accountId, items, opts = {}) {
4309
4353
  if (focusedRow && !opts.background) {
4310
4354
  const fAcct = focusedRow.accountId;
4311
4355
  const fUid = focusedRow.msg?.uid;
4312
- const stillThere = items.some((m) => (m.accountId || accountId) === fAcct && m.uid === fUid);
4356
+ const fUuid = focusedRow.msg?.uuid;
4357
+ const stillThere = items.some((m) => fUuid && m.uuid ? m.uuid === fUuid : (m.accountId || accountId) === fAcct && m.uid === fUid);
4313
4358
  if (!stillThere) {
4359
+ try {
4360
+ logClientEvent("[fate-share] focused row gone after foreground render \u2014 clearing viewer", { uid: fUid, uuid: fUuid || null, accountId: fAcct });
4361
+ } catch {
4362
+ }
4314
4363
  clearViewer();
4315
4364
  focusedRow = null;
4316
4365
  }
@@ -4335,6 +4384,12 @@ function restoreSelection(body, savedUuid) {
4335
4384
  const uid = Number(row.dataset.uid);
4336
4385
  if (accountId && Number.isFinite(uid)) {
4337
4386
  const switching = focusedRow?.msg?.uuid !== savedUuid;
4387
+ if (switching) {
4388
+ try {
4389
+ logClientEvent("[fate-share] restoreSelection switching viewer", { toUuid: savedUuid, toUid: uid, fromUuid: focusedRow?.msg?.uuid || null });
4390
+ } catch {
4391
+ }
4392
+ }
4338
4393
  focusByIdentity(accountId, uid, { scroll: switching });
4339
4394
  }
4340
4395
  }
@@ -6153,6 +6208,7 @@ async function syncReminderState(force = false) {
6153
6208
  sn[k] = v;
6154
6209
  }
6155
6210
  saveSnoozed(sn);
6211
+ retractSuppressedPopups();
6156
6212
  return true;
6157
6213
  } catch (e) {
6158
6214
  alog("state sync failed", { err: e?.message || String(e) });
@@ -6249,10 +6305,28 @@ function formatWhen(ms) {
6249
6305
  const d = new Date(ms);
6250
6306
  return d.toLocaleString(void 0, { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
6251
6307
  }
6308
+ function retractSuppressedPopups() {
6309
+ if (openPopups.size === 0)
6310
+ return;
6311
+ const now = Date.now();
6312
+ const dismissed = loadDismissed();
6313
+ const snoozed = loadSnoozed();
6314
+ for (const [key, entry] of openPopups) {
6315
+ if (entry.retracted)
6316
+ continue;
6317
+ if (dismissed[key] || (snoozed[key] || 0) > now) {
6318
+ alog("retracting popup \u2014 resolved on another machine", { key, dismissed: !!dismissed[key], snoozedUntil: snoozed[key] || 0 });
6319
+ try {
6320
+ entry.retract();
6321
+ } catch {
6322
+ }
6323
+ }
6324
+ }
6325
+ }
6252
6326
  function escapeHtml6(s) {
6253
6327
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6254
6328
  }
6255
- function showInWebViewPopup(opts) {
6329
+ function showInWebViewPopup(opts, onRegisterClose) {
6256
6330
  return new Promise((resolve) => {
6257
6331
  const overlay = document.createElement("div");
6258
6332
  overlay.className = "alarm-overlay";
@@ -6294,12 +6368,15 @@ function showInWebViewPopup(opts) {
6294
6368
  panel.querySelectorAll("[data-button]").forEach((btn) => {
6295
6369
  btn.addEventListener("click", () => cleanup(btn.dataset.button || ""));
6296
6370
  });
6371
+ onRegisterClose?.(cleanup);
6297
6372
  });
6298
6373
  }
6299
6374
  async function firePopupForItem(item) {
6300
6375
  if (openPopups.has(item.uuid))
6301
6376
  return;
6302
- openPopups.add(item.uuid);
6377
+ const popupEntry = { retracted: false, retract: () => {
6378
+ } };
6379
+ openPopups.set(item.uuid, popupEntry);
6303
6380
  const openUrl = item.kind === "calendar" ? item.htmlLink || "https://calendar.google.com/" : "https://tasks.google.com/";
6304
6381
  const buttons = ["snooze", "Dismiss", "Open"];
6305
6382
  if (item.kind === "calendar")
@@ -6401,17 +6478,28 @@ async function firePopupForItem(item) {
6401
6478
  let r;
6402
6479
  try {
6403
6480
  if (hasHost) {
6481
+ popupEntry.retract = () => {
6482
+ popupEntry.retracted = true;
6483
+ closeReminderPopup(item.uuid).catch(() => {
6484
+ });
6485
+ };
6404
6486
  r = await showReminderPopup({
6405
6487
  title,
6406
6488
  html,
6407
6489
  buttons,
6490
+ popupKey: item.uuid,
6408
6491
  // Sized for the new snooze form (chips row + custom input
6409
6492
  // row + action buttons) plus title/when/kind. Width fits
6410
6493
  // "5m 15m 30m 1h 2h 1d" on one line in Segoe UI.
6411
6494
  size: { width: 560, height: 300 }
6412
6495
  });
6413
6496
  } else {
6414
- r = await showInWebViewPopup({ title, html, buttons });
6497
+ r = await showInWebViewPopup({ title, html, buttons }, (close) => {
6498
+ popupEntry.retract = () => {
6499
+ popupEntry.retracted = true;
6500
+ close("retracted");
6501
+ };
6502
+ });
6415
6503
  }
6416
6504
  } catch (e) {
6417
6505
  alog("popup IPC failed \u2014 treating as Dismiss", { key: item.uuid, err: e?.message || String(e) });
@@ -6419,6 +6507,11 @@ async function firePopupForItem(item) {
6419
6507
  } finally {
6420
6508
  openPopups.delete(item.uuid);
6421
6509
  }
6510
+ if (popupEntry.retracted) {
6511
+ alog("popup closed by retraction \u2014 skipping result handling", { key: item.uuid });
6512
+ firedThisSession.delete(item.uuid);
6513
+ return;
6514
+ }
6422
6515
  if (!r || typeof r !== "object" || typeof r.button !== "string") {
6423
6516
  alog("popup returned empty / malformed \u2014 treating as Dismiss", { key: item.uuid, raw: r });
6424
6517
  r = { button: "Dismiss" };
@@ -6570,7 +6663,7 @@ var init_alarms = __esm({
6570
6663
  _lastStateSync = 0;
6571
6664
  STATE_SYNC_MIN_MS = 6e4;
6572
6665
  firedThisSession = /* @__PURE__ */ new Set();
6573
- openPopups = /* @__PURE__ */ new Set();
6666
+ openPopups = /* @__PURE__ */ new Map();
6574
6667
  }
6575
6668
  });
6576
6669