@bobfrankston/rmfmail 1.2.303 → 1.2.305

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/.commitmsg CHANGED
@@ -1,47 +1,40 @@
1
- The phone's star filter was never asked for, and the chip announcing it landed in a 28px column
1
+ One click on a task disabled Delete for the whole app
2
2
 
3
- "it then says no flagged messages. but there are message flagged" (Bob
4
- 2026-09-04, on Android, with the screenshot). There are 260 — 257 in bobma's
5
- INBOX alone, and the desktop's own SQL finds every one of them.
3
+ "I press del and the message is not deleted whether I'm in the letter or
4
+ summary and trashcan is not working" (Bob 2026-09-05).
6
5
 
7
- `WebMailxService.getUnifiedInbox` took two parameters. api-client has always
8
- called it with four: `(page, pageSize, flaggedOnly, dateBasis)`. `getMessages`
9
- took seven and is called with nine, `flaggedOnly` at position 8. The extras
10
- fell off the end and JavaScript said nothing and neither did TypeScript,
11
- because an implementation with FEWER parameters satisfies an interface
12
- declaring more. Both dropped arguments are on `MessageQuery`, documented, and
13
- honoured by the desktop store.
6
+ `deleteSelection()` the one route for the Delete key, Ctrl+D, and both trash
7
+ buttons gave tasks an unconditional priority: if `getSelectedTaskUuids()` was
8
+ non-empty it deleted tasks and returned, whatever the user was actually looking
9
+ at. A task selection is set by a single click on a task row in the sidebar, is
10
+ never cleared by anything done in the mail panes, and has no indicator outside
11
+ the sidebar. So one stray click on a task turned off Delete and both trash
12
+ buttons for messages, indefinitely, and the only symptom was nothing happening.
13
+ Invisible modal state deciding what a destructive gesture means — the same
14
+ fault as a filter that reads as a selection.
14
15
 
15
- What was left on the phone was the client's `.flagged-only` CSS rule, which
16
- hides unflagged rows in the loaded page. That is precisely the arrangement the
17
- desktop comment has warned about since 2026-06-05: a paginated list whose
18
- newest 50 messages happen to carry no star shows nothing at all, and every
19
- starred message deeper in the mailbox is unreachable. The overlay then counted
20
- the rows it was hiding and reported the truth from where it stood — "A 'flagged
21
- only' filter is hiding 50 messages" — while the filter that could have found
22
- the other 260 had never run.
16
+ The daemon log shows it exactly: at the moment of the attempt there is a
17
+ getMessage and an updateFlags and no trashMessages at all. Nothing was slow or
18
+ failing; the gesture never left the client.
23
19
 
24
- Both parameters are now threaded through, and the `\Flagged` predicate lives in
25
- one named constant matching desktop's character for character: two backslashes
26
- in the SQL literal, because `flags_json` is JSON and the flag is stored
27
- escaped. `dateBasis` was dropped by the same omission on the same line, so the
28
- phone has been ordering by arrival time while the desktop ordered by the Date:
29
- header; it now honours what it is handed. That is a visible change to list
30
- order on the phone, and it is the order the client has been asking for all
31
- along say so if you want it left as it was.
20
+ Routing now follows the pane the user last acted in, tracked on capture-phase
21
+ pointerdown so it lands before any handler that stops propagation and before
22
+ focus moves. The toolbar buttons sit in neither pane, so clicking one leaves
23
+ that reading as the user's last real interaction set it which is the point,
24
+ since the button is how you act on what you were just doing. Select tasks and
25
+ press Delete and the tasks still go, which is what the priority rule was
26
+ written for; the part that was never intended is gone. A delete routed to
27
+ messages also clears any leftover task selection, so the sidebar stops showing
28
+ rows as chosen after the gesture has moved on.
32
29
 
33
- Three tests assert the threading, which is where the bug was. They use a spy
34
- db, since the failure was an argument that never arrived, not SQL that got it
35
- wrong.
30
+ I made this worse yesterday and should say so. Before v1.2.304 the swallowed
31
+ delete at least put up the native confirm misplaced and late, but visible, so
32
+ "my delete went to the tasks" was discoverable. Moving that question to a menu
33
+ anchored over the task pane made the misrouting completely silent whenever the
34
+ sidebar was collapsed or scrolled away. The routing bug was already there; my
35
+ change removed the last evidence of it.
36
36
 
37
- **The chip, separately.** `#ml-filter-chip` said `display: block; width: 100%`
38
- and never said `grid-column: 1 / -1`. `.message-list` is a five-column grid,
39
- every other full-width child declares the span, and this one auto-placed into
40
- column 1 the 28px avatar track — so "Showing flagged (★) messages only —
41
- click to show everything" came out one word per line down the left edge of the
42
- header. `width: 100%` cannot help there; inside a grid it means 100% of a 28px
43
- track.
44
-
45
- Not changed: the ★ button sits in the search row, next to Server and
46
- Trash/Spam, which is why reaching for search found a list filter. That is a
47
- placement judgement, not a bug, so it is yours to call.
37
+ Also guarded, from the same cause: the batch confirm measured its anchor with
38
+ getBoundingClientRect on a possibly-hidden sidebar, which is 0x0 at 0,0 it
39
+ now falls back to the middle of the window rather than asking in a corner
40
+ nobody is looking at.
@@ -1015,6 +1015,7 @@ var init_edit_menu = __esm({
1015
1015
  var context_menu_exports = {};
1016
1016
  __export(context_menu_exports, {
1017
1017
  closeContextMenu: () => closeContextMenu,
1018
+ confirmAt: () => confirmAt,
1018
1019
  showContextMenu: () => showContextMenu
1019
1020
  });
1020
1021
  function closeSubmenu() {
@@ -1029,6 +1030,11 @@ function closeContextMenu() {
1029
1030
  activeMenu.remove();
1030
1031
  activeMenu = null;
1031
1032
  }
1033
+ if (closeHandler) {
1034
+ const fn = closeHandler;
1035
+ closeHandler = null;
1036
+ fn();
1037
+ }
1032
1038
  if (dismissListener) {
1033
1039
  document.removeEventListener("pointerdown", dismissListener, true);
1034
1040
  dismissListener = null;
@@ -1079,6 +1085,7 @@ function openSubmenu(parentRow, items) {
1079
1085
  }
1080
1086
  function showContextMenu(x, y, items, opts = {}) {
1081
1087
  closeContextMenu();
1088
+ closeHandler = opts.onClose ?? null;
1082
1089
  if (opts.editTarget !== void 0 || opts.editEngine) {
1083
1090
  const edit = editMenuItems({ target: opts.editTarget, engine: opts.editEngine }, { onError: opts.onEditError });
1084
1091
  if (edit.length > 0) {
@@ -1168,7 +1175,29 @@ function showContextMenu(x, y, items, opts = {}) {
1168
1175
  document.addEventListener("keydown", escapeListener, true);
1169
1176
  });
1170
1177
  }
1171
- var activeMenu, dismissListener, escapeListener, activeSubmenu;
1178
+ function confirmAt(x, y, confirmLabel, opts = {}) {
1179
+ return new Promise((resolve) => {
1180
+ let settled = false;
1181
+ const done = (v) => {
1182
+ if (!settled) {
1183
+ settled = true;
1184
+ resolve(v);
1185
+ }
1186
+ };
1187
+ showContextMenu(x, y, [
1188
+ { label: confirmLabel, action: () => done(true), emphasized: true, tooltip: opts.tooltip },
1189
+ { label: "Cancel", action: () => done(false) }
1190
+ ], {
1191
+ // An item's click calls closeContextMenu() BEFORE item.action(), so
1192
+ // onClose always runs first. Defer the "cancelled" answer by one
1193
+ // microtask: the action is synchronous and finishes within this
1194
+ // task, so by the time the microtask drains, a chosen item has
1195
+ // already settled the promise and this call is a no-op.
1196
+ onClose: () => queueMicrotask(() => done(false))
1197
+ });
1198
+ });
1199
+ }
1200
+ var activeMenu, dismissListener, escapeListener, activeSubmenu, closeHandler;
1172
1201
  var init_context_menu = __esm({
1173
1202
  "client/components/context-menu.js"() {
1174
1203
  "use strict";
@@ -1177,6 +1206,7 @@ var init_context_menu = __esm({
1177
1206
  dismissListener = null;
1178
1207
  escapeListener = null;
1179
1208
  activeSubmenu = null;
1209
+ closeHandler = null;
1180
1210
  document.addEventListener("scroll", closeContextMenu, true);
1181
1211
  document.addEventListener("contextmenu", () => {
1182
1212
  });
@@ -6512,6 +6542,7 @@ var init_outbox_view = __esm({
6512
6542
  // client/components/calendar-sidebar.js
6513
6543
  var calendar_sidebar_exports = {};
6514
6544
  __export(calendar_sidebar_exports, {
6545
+ clearTaskSelection: () => clearTaskSelection,
6515
6546
  deleteSelectedTasks: () => deleteSelectedTasks,
6516
6547
  getSelectedTaskUuids: () => getSelectedTaskUuids,
6517
6548
  hideCalendarSidebar: () => hideCalendarSidebar,
@@ -6619,6 +6650,12 @@ async function renderCalendarList() {
6619
6650
  if (lastEvents.length > 0)
6620
6651
  renderEvents(lastEvents);
6621
6652
  }
6653
+ function clearTaskSelection() {
6654
+ if (selectedTaskUuids.size === 0)
6655
+ return;
6656
+ selectedTaskUuids.clear();
6657
+ void renderTasks();
6658
+ }
6622
6659
  function getSelectedTaskUuids() {
6623
6660
  return [...selectedTaskUuids];
6624
6661
  }
@@ -6626,7 +6663,11 @@ async function deleteSelectedTasks() {
6626
6663
  const uuids = [...selectedTaskUuids];
6627
6664
  if (uuids.length === 0)
6628
6665
  return;
6629
- if (!confirm(`Delete ${uuids.length} task${uuids.length === 1 ? "" : "s"}? This removes ${uuids.length === 1 ? "it" : "them"} from Google Tasks \u2014 to mark done, use the checkbox instead.`))
6666
+ const anchorEl = document.querySelector(".cal-side-task.selected") || document.getElementById("cal-side-tasks");
6667
+ const rect = anchorEl?.getBoundingClientRect();
6668
+ const r = rect && rect.width > 0 && rect.height > 0 ? rect : { left: window.innerWidth / 2 - 60, bottom: window.innerHeight / 3 };
6669
+ const label = uuids.length === 1 ? `Delete "${document.querySelector(".cal-side-task.selected .cal-side-task-title")?.textContent || "this task"}"` : `Delete ${uuids.length} tasks`;
6670
+ if (!await confirmAt(r.left + 24, r.bottom, label, { tooltip: TASK_DELETE_NOTE }))
6630
6671
  return;
6631
6672
  selectedTaskUuids.clear();
6632
6673
  for (const uuid of uuids) {
@@ -6914,7 +6955,7 @@ async function renderTasks(prefetched) {
6914
6955
  row.querySelector(".cal-side-task-delete")?.addEventListener("click", async (e) => {
6915
6956
  e.stopPropagation();
6916
6957
  const title = tasks.find((t) => t.uuid === uuid)?.title || "this task";
6917
- if (!confirm(`Delete task "${title}"? This removes it from Google Tasks \u2014 to mark done, use the checkbox instead.`))
6958
+ if (!await confirmAt(e.clientX, e.clientY, `Delete "${title}"`, { tooltip: TASK_DELETE_NOTE }))
6918
6959
  return;
6919
6960
  await deleteTask(uuid);
6920
6961
  selectedTaskUuids.delete(uuid);
@@ -6941,9 +6982,9 @@ async function renderTasks(prefetched) {
6941
6982
  { label: "View in Google Tasks", action: () => openInBrowser("https://tasks.google.com/") },
6942
6983
  { label: "", action: () => {
6943
6984
  }, separator: true },
6944
- { label: "Delete task", action: async () => {
6985
+ { label: "Delete task", tooltip: TASK_DELETE_NOTE, action: async () => {
6945
6986
  const title = tasks.find((t) => t.uuid === uuid)?.title || "this task";
6946
- if (!confirm(`Delete task "${title}"? This removes it from Google Tasks \u2014 to mark done, use the checkbox instead.`))
6987
+ if (!await confirmAt(e.clientX, e.clientY, `Delete "${title}"`, { tooltip: TASK_DELETE_NOTE }))
6947
6988
  return;
6948
6989
  await deleteTask(uuid);
6949
6990
  renderTasks();
@@ -7296,7 +7337,7 @@ function initCalendarSidebar() {
7296
7337
  }
7297
7338
  }
7298
7339
  }
7299
- var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, lastOverdueTasks, calById, calendarList, hiddenCalendars, selectedTaskUuids;
7340
+ var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, lastOverdueTasks, calById, calendarList, hiddenCalendars, selectedTaskUuids, TASK_DELETE_NOTE;
7300
7341
  var init_calendar_sidebar = __esm({
7301
7342
  "client/components/calendar-sidebar.js"() {
7302
7343
  "use strict";
@@ -7317,6 +7358,7 @@ var init_calendar_sidebar = __esm({
7317
7358
  calendarList = [];
7318
7359
  hiddenCalendars = /* @__PURE__ */ new Set();
7319
7360
  selectedTaskUuids = /* @__PURE__ */ new Set();
7361
+ TASK_DELETE_NOTE = "Removes it from Google Tasks. To mark it done instead, use the checkbox.";
7320
7362
  }
7321
7363
  });
7322
7364
 
@@ -10554,13 +10596,21 @@ function popUndo() {
10554
10596
  }
10555
10597
  return null;
10556
10598
  }
10599
+ var lastActionPane = "messages";
10600
+ document.addEventListener("pointerdown", (e) => {
10601
+ const t = e.target;
10602
+ if (!t?.closest) return;
10603
+ if (t.closest("#cal-side-tasks")) lastActionPane = "tasks";
10604
+ else if (t.closest(".message-list, .message-viewer, #ml-body, #message-viewer")) lastActionPane = "messages";
10605
+ }, true);
10557
10606
  async function deleteSelection() {
10558
10607
  try {
10559
10608
  const sidebar = await Promise.resolve().then(() => (init_calendar_sidebar(), calendar_sidebar_exports));
10560
- if (sidebar.getSelectedTaskUuids().length > 0) {
10609
+ if (lastActionPane === "tasks" && sidebar.getSelectedTaskUuids().length > 0) {
10561
10610
  await sidebar.deleteSelectedTasks();
10562
10611
  return;
10563
10612
  }
10613
+ if (sidebar.getSelectedTaskUuids().length > 0) sidebar.clearTaskSelection();
10564
10614
  } catch {
10565
10615
  }
10566
10616
  await deleteSelectedMessages();