@bobfrankston/rmfmail 1.2.302 → 1.2.304

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,49 +1,49 @@
1
- Opening a PDF on the phone: a stub, and bytes laundered through UTF-8
2
-
3
- "I tried opening a pdf attachment on Android and it said 'opening' but nothing
4
- happened" (Bob 2026-09-04). In the Android log, verbatim:
5
-
6
- Couldn't open "260901-THE-AI-COMPUTE-RACE-…-PRIVATE-DRAFT.pdf":
7
- Not implemented on Android: getAttachment
8
-
9
- Two independent bugs, and only the first was reachable.
10
-
11
- **getAttachment was a `notImpl` stub.** It sat one line above a comment that
12
- read "Android opens attachments via the native bridge getAttachment + base64
13
- hand-off covers it": a correct description of the design, immediately below the
14
- throw that made the design impossible. The viewer's Android branch does exactly
15
- what the comment says fetch the bytes, hand them to
16
- `_nativeBridge.openAttachment` so the throw landed in the chip's catch, which
17
- posts a banner and restores the label after 600ms. From the phone that is
18
- "Opening…", and then the chip goes back to normal. It is now implemented: read
19
- the body through the sync manager, parse, index the attachment the same way
20
- getMessage numbered the chips, return base64 in the shape the desktop jsonrpc
21
- returns.
22
-
23
- **The parser was destroying the bytes anyway.** The attachment branch called
24
- `decodeBody()` which decodes the transfer encoding to bytes and then decodes
25
- THOSE as UTF-8 text and re-encoded the result with `TextEncoder`. Every byte
26
- sequence that is not valid UTF-8 became U+FFFD and came back as the three bytes
27
- EF BF BD. A PDF, an image or a zip would have arrived corrupt and LONGER than
28
- it started, with `size` wrong to match, the moment the stub was filled in. The
29
- transfer-encoding step is now `decodeBodyBytes()` and stops at bytes;
30
- `decodeBody` is that plus the charset step, so the encodings still have one
31
- implementation. Attachments take the bytes. Text parts are unaffected.
32
-
33
- Verified against the actual message: the web parser and mailparser now produce
34
- byte-identical output for that PDF — 3,307,996 bytes, `%PDF-` to `%%EOF`.
35
- tests/attachment-bytes.test.ts covers it with a fixture built from bytes no
36
- UTF-8 decoder can round-trip, asserting the whole buffer rather than a prefix,
37
- because the broken version also started with "%PDF".
38
-
39
- **One more thing that had never run.** `app/openattachment` in MainPage.xaml.cs
40
- is the only place in the bridge that reads a RETURN VALUE from JS; every other
41
- path pushes C#→JS. Since the JS threw first, that code has never once executed.
42
- Android's evaluateJavascript hands back a JSON-encoded value, so the base64
43
- arrives quoted and with `=` liable to appear as = undone before decode,
44
- and a FormatException now logs the length and first characters of what actually
45
- arrived instead of dying as "invalid base64".
46
-
47
- Untested from here: the 3.3 MB hand-off itself. The bytes are correct and the
48
- call is wired; whether a payload that size survives one EvaluateJavaScriptAsync
49
- can only be answered on the device. If it does not, the new log line names it.
1
+ Ask at the pointer, not in the host's dialog
2
+
3
+ "clicking the x is supposed [to] delete the task but seems to do nothing
4
+ it eventually puts up a message far from where I clicked" (Bob 2026-09-04,
5
+ with both screenshots).
6
+
7
+ The × handler called the native `confirm()`. In the WebView that is the host's
8
+ own dialog: titled "msger.localhost says", centred in the window rather than
9
+ under the pointer, arriving late enough that the click reads as having done
10
+ nothing — and blocking the WebView event loop the whole time it is up, which is
11
+ the hazard message-viewer.ts already refuses `alert()` for. Compose reached the
12
+ same conclusion in its own window and wrote an in-page modal; the main window
13
+ never got one, so thirteen `confirm()` calls still go to the host.
14
+
15
+ `confirmAt(x, y, label)` in context-menu.ts asks the question as a two-item
16
+ menu at the pointer. Built on showContextMenu rather than as a new modal, so
17
+ there is one menu, one dismissal path, and one piece of stay-on-screen clamping
18
+ that clamping in particular was already solved here for narrow Android
19
+ screens, and a second implementation would have had to learn it again.
20
+
21
+ The label NAMES its target — `Delete "Records. Berkowitz"`, never "OK". By the
22
+ time a confirmation is read the pointer has moved and the row it came from is
23
+ no longer obvious; a named action can be checked, and "OK" cannot. The
24
+ explanation the old dialog text carried ("removes it from Google Tasks — to
25
+ mark done use the checkbox") is now the item's tooltip: worth having, not worth
26
+ a dialog.
27
+
28
+ Three call sites move: the row ×, the row context menu's Delete, and the batch
29
+ delete behind the Delete key / trash button. The batch one has no click to
30
+ anchor to, so it asks over the first selected row — where the tasks are and
31
+ where the user is looking.
32
+
33
+ Two supporting changes:
34
+
35
+ `ContextMenuOptions.onClose` fires whichever way the menu goes away, so a
36
+ promise-shaped menu can answer "cancelled" instead of hanging forever on a
37
+ dismissal. An item's click calls closeContextMenu() BEFORE item.action(), so
38
+ onClose always runs first; confirmAt defers its cancel by one microtask, and
39
+ since the action is synchronous and completes within the same task, a chosen
40
+ item has always settled the promise by the time that microtask drains.
41
+
42
+ `.ctx-emphasized` now renders. `MenuItem.emphasized` has been passed since the
43
+ right-drag menu was written ("Move here" as the default action) and no CSS rule
44
+ ever matched it, so it has never once looked different from its neighbours.
45
+
46
+ The other ten `confirm()` calls — rebuild, factory reset, permanent delete,
47
+ folder move/delete, contact delete, discard draft are untouched. They are
48
+ the same dialog with the same problems and they should follow, but each is a
49
+ destructive action worth reading before it moves.
@@ -5855,6 +5855,7 @@ var sql_wasm_default = __toBinary("AGFzbQEAAAABnwRFYAJ/fwF/YAF/AX9gA39/fwBgA39/f
5855
5855
 
5856
5856
  // packages/mailx-store-web/db.js
5857
5857
  init_mailx_types();
5858
+ var FLAGGED_SQL = " AND flags_json LIKE '%\\\\Flagged%'";
5858
5859
  var JUNK_LOCAL_RE = new RegExp(CONTACT_RULES.junk.localExact, "i");
5859
5860
  var JUNK_LOCAL_SUFFIX_RE = new RegExp(CONTACT_RULES.junk.localSuffix, "i");
5860
5861
  var JUNK_LOCAL_PREFIX_RE = new RegExp(CONTACT_RULES.junk.localPrefix, "i");
@@ -6304,7 +6305,8 @@ var WebMailxDB = class {
6304
6305
  const page = query.page || 1;
6305
6306
  const pageSize = query.pageSize || 50;
6306
6307
  const offset = (page - 1) * pageSize;
6307
- const sortCol = query.sort === "from" ? "from_name" : query.sort === "subject" ? "subject" : "date";
6308
+ const dateCol = query.dateBasis === "received" ? "date" : "COALESCE(sent_date, date)";
6309
+ const sortCol = query.sort === "from" ? "from_name" : query.sort === "subject" ? "subject" : dateCol;
6308
6310
  const sortDir = query.sortDir || "desc";
6309
6311
  let where = "account_id = ? AND folder_id = ?";
6310
6312
  const params = [query.accountId, query.folderId];
@@ -6313,21 +6315,25 @@ var WebMailxDB = class {
6313
6315
  const term = `%${query.search}%`;
6314
6316
  params.push(term, term, term);
6315
6317
  }
6318
+ if (query.flaggedOnly)
6319
+ where += FLAGGED_SQL;
6316
6320
  const countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE ${where}`, params);
6317
6321
  const total = countRow?.cnt || 0;
6318
6322
  const rows = this.all(`SELECT * FROM messages WHERE ${where} ORDER BY ${sortCol} ${sortDir} LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
6319
6323
  return { items: rows.map((r) => this.rowToEnvelope(r)), total, page, pageSize };
6320
6324
  }
6321
- getUnifiedInbox(page = 1, pageSize = 50) {
6325
+ getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis = "sent") {
6322
6326
  const offset = (page - 1) * pageSize;
6323
6327
  const inboxRows = this.all("SELECT id FROM folders WHERE special_use = 'inbox'");
6324
6328
  if (inboxRows.length === 0)
6325
6329
  return { items: [], total: 0, page, pageSize };
6326
6330
  const ids = inboxRows.map((r) => r.id);
6327
6331
  const placeholders = ids.map(() => "?").join(",");
6328
- const countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE folder_id IN (${placeholders})`, ids);
6332
+ const flagFilter = flaggedOnly ? FLAGGED_SQL : "";
6333
+ const dateCol = dateBasis === "received" ? "date" : "COALESCE(sent_date, date)";
6334
+ const countRow = this.get(`SELECT COUNT(*) as cnt FROM messages WHERE folder_id IN (${placeholders})${flagFilter}`, ids);
6329
6335
  const total = countRow?.cnt || 0;
6330
- const rows = this.all(`SELECT * FROM messages WHERE folder_id IN (${placeholders}) ORDER BY date DESC LIMIT ? OFFSET ?`, [...ids, pageSize, offset]);
6336
+ const rows = this.all(`SELECT * FROM messages WHERE folder_id IN (${placeholders})${flagFilter} ORDER BY ${dateCol} DESC LIMIT ? OFFSET ?`, [...ids, pageSize, offset]);
6331
6337
  return { items: rows.map((r) => this.rowToEnvelope(r)), total, page, pageSize };
6332
6338
  }
6333
6339
  getMessageByUid(accountId, uid, folderId) {
@@ -6936,11 +6942,21 @@ var WebMailxService = class _WebMailxService {
6936
6942
  return this.db.getFolders(accountId);
6937
6943
  }
6938
6944
  // ── Messages ──
6939
- getUnifiedInbox(page = 1, pageSize = 50) {
6940
- return this.db.getUnifiedInbox(page, pageSize);
6941
- }
6942
- getMessages(accountId, folderId, page = 1, pageSize = 50, sort = "date", sortDir = "desc", search) {
6943
- return this.db.getMessages({ accountId, folderId, page, pageSize, sort, sortDir, search });
6945
+ // 2026-09-04 Claude Code (Opus 5), at Bob's direction ("it then says no
6946
+ // flagged messages. but there are message flagged" — 260 of them). Both of
6947
+ // these took two or seven parameters while api-client has always CALLED
6948
+ // them with four and nine: `flaggedOnly` and `dateBasis` fell off the end
6949
+ // and JavaScript said nothing. The star filter therefore existed on the
6950
+ // phone only as the client's `.flagged-only` CSS rule, which can hide
6951
+ // unflagged rows in the loaded page but cannot reach a starred message on
6952
+ // page 4 — so a list whose newest 50 carry no star reads as "no flagged
6953
+ // messages". Keep these signatures in step with api-client's calls; the
6954
+ // language will not do it for you.
6955
+ getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis = "sent") {
6956
+ return this.db.getUnifiedInbox(page, pageSize, flaggedOnly, dateBasis);
6957
+ }
6958
+ getMessages(accountId, folderId, page = 1, pageSize = 50, sort = "date", sortDir = "desc", search, flaggedOnly = false, dateBasis = "sent") {
6959
+ return this.db.getMessages({ accountId, folderId, page, pageSize, sort, sortDir, search, flaggedOnly, dateBasis });
6944
6960
  }
6945
6961
  async getMessage(accountId, uid, allowRemote = false, folderId) {
6946
6962
  const envelope = this.db.getMessageByUid(accountId, uid, folderId);