@bobfrankston/mailx 1.0.189 → 1.0.191

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.
@@ -1 +1 @@
1
- {"height":1344,"width":2151,"x":626,"y":45}
1
+ {"height":1344,"width":2151,"x":500,"y":36}
package/client/app.js CHANGED
@@ -346,34 +346,46 @@ async function openCompose(mode) {
346
346
  showComposeOverlay();
347
347
  }
348
348
  function showComposeOverlay() {
349
- // Remove existing overlay if any
350
- document.getElementById("compose-overlay")?.remove();
351
- const overlay = document.createElement("div");
352
- overlay.id = "compose-overlay";
353
- overlay.style.cssText = "position:fixed;top:0;left:0;right:0;bottom:0;z-index:1000;background:rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;";
349
+ // Wrapper div for iframe + close button
350
+ const wrapper = document.createElement("div");
351
+ wrapper.className = "compose-overlay";
352
+ wrapper.style.cssText = "position:fixed;bottom:0;right:16px;width:min(900px,55vw);height:min(700px,70vh);z-index:1000;border-radius:8px 8px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;";
353
+ // Close button
354
+ const closeBtn = document.createElement("button");
355
+ closeBtn.textContent = "✕";
356
+ closeBtn.title = "Close compose";
357
+ closeBtn.style.cssText = "position:absolute;top:4px;right:8px;z-index:1001;background:none;border:none;font-size:18px;cursor:pointer;color:#666;padding:2px 6px;border-radius:4px;";
358
+ closeBtn.addEventListener("mouseenter", () => closeBtn.style.color = "#c00");
359
+ closeBtn.addEventListener("mouseleave", () => closeBtn.style.color = "#666");
360
+ closeBtn.addEventListener("click", () => {
361
+ // Trigger draft save in the iframe before closing
362
+ try {
363
+ const win = frame.contentWindow;
364
+ if (win) {
365
+ // Dispatch a custom event that compose.ts can listen to
366
+ win.dispatchEvent(new Event("compose-save-and-close"));
367
+ }
368
+ }
369
+ catch { /* cross-origin */ }
370
+ // Give the draft save a moment, then close
371
+ setTimeout(() => wrapper.remove(), 500);
372
+ });
354
373
  const frame = document.createElement("iframe");
355
374
  frame.src = "compose/compose.html";
356
- frame.style.cssText = "width:min(900px,90vw);height:min(700px,85vh);border:none;border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.4);background:#fff;";
357
- // Close overlay when compose calls window.close()
375
+ frame.style.cssText = "flex:1;border:none;border-radius:8px 8px 0 0;background:#fff;width:100%;";
376
+ // Close when compose calls window.close()
358
377
  frame.addEventListener("load", () => {
359
378
  try {
360
379
  const win = frame.contentWindow;
361
380
  if (win) {
362
- // Override window.close() in the iframe to remove the overlay instead
363
- win.close = () => overlay.remove();
381
+ win.close = () => wrapper.remove();
364
382
  }
365
383
  }
366
384
  catch { /* cross-origin safety */ }
367
385
  });
368
- // Click backdrop to close (with confirmation)
369
- overlay.addEventListener("click", (e) => {
370
- if (e.target === overlay) {
371
- if (confirm("Discard this message?"))
372
- overlay.remove();
373
- }
374
- });
375
- overlay.appendChild(frame);
376
- document.body.appendChild(overlay);
386
+ wrapper.appendChild(closeBtn);
387
+ wrapper.appendChild(frame);
388
+ document.body.appendChild(wrapper);
377
389
  }
378
390
  function quoteBody(msg) {
379
391
  const date = new Date(msg.date).toLocaleString();
@@ -664,6 +676,11 @@ onWsEvent((event) => {
664
676
  }
665
677
  break;
666
678
  }
679
+ case "syncComplete":
680
+ // After sync completes, refresh the folder tree (critical for first-run on Android
681
+ // where folders don't exist until sync fetches them from Gmail API)
682
+ refreshFolderTree();
683
+ break;
667
684
  case "folderCountsChanged": {
668
685
  // Update folder badges + silently refresh message list (preserves selection and viewer)
669
686
  updateFolderCounts();
@@ -792,6 +809,11 @@ document.addEventListener("keydown", (e) => {
792
809
  e.preventDefault();
793
810
  openCompose("replyAll");
794
811
  }
812
+ // Ctrl+F = Forward
813
+ if (e.ctrlKey && e.key.toLowerCase() === "f" && !e.shiftKey) {
814
+ e.preventDefault();
815
+ openCompose("forward");
816
+ }
795
817
  // Ctrl+A = Select all visible messages
796
818
  if (e.ctrlKey && e.key === "a") {
797
819
  const mlBody = document.getElementById("ml-body");
@@ -386,6 +386,11 @@ document.getElementById("btn-discard")?.addEventListener("click", () => {
386
386
  }
387
387
  window.close();
388
388
  });
389
+ // ── Save and close (X button from parent) ──
390
+ window.addEventListener("compose-save-and-close", async () => {
391
+ await saveDraft();
392
+ closeCompose();
393
+ });
389
394
  // ── Keyboard shortcuts ──
390
395
  document.addEventListener("keydown", (e) => {
391
396
  if (e.ctrlKey && e.key === "Enter") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.189",
3
+ "version": "1.0.191",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -24,7 +24,7 @@
24
24
  "@bobfrankston/iflow-node": "^0.1.2",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.21",
27
- "@bobfrankston/msger": "^0.1.239",
27
+ "@bobfrankston/msger": "^0.1.241",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -108,6 +108,9 @@ export declare class ImapManager extends EventEmitter {
108
108
  private quickCheckRunning;
109
109
  /** Check a single account's inbox — uses its own connection, never blocked by sync */
110
110
  quickInboxCheckAccount(accountId: string): Promise<void>;
111
+ /** Quick Gmail inbox check — one lightweight API call to check for new messages */
112
+ private lastGmailInboxTop;
113
+ private quickGmailCheck;
111
114
  /** Check all accounts (used by legacy callers) */
112
115
  quickInboxCheck(): Promise<void>;
113
116
  /** Start periodic sync */
@@ -1050,8 +1050,9 @@ export class ImapManager extends EventEmitter {
1050
1050
  return;
1051
1051
  if (this.reauthenticating.has(accountId))
1052
1052
  return;
1053
- if (this.isGmailAccount(accountId))
1054
- return; // Gmail uses API sync, not IMAP polling
1053
+ if (this.isGmailAccount(accountId)) {
1054
+ return this.quickGmailCheck(accountId);
1055
+ }
1055
1056
  this.quickCheckRunning.add(accountId);
1056
1057
  let client = null;
1057
1058
  try {
@@ -1082,6 +1083,42 @@ export class ImapManager extends EventEmitter {
1082
1083
  this.quickCheckRunning.delete(accountId);
1083
1084
  }
1084
1085
  }
1086
+ /** Quick Gmail inbox check — one lightweight API call to check for new messages */
1087
+ lastGmailInboxTop = new Map();
1088
+ async quickGmailCheck(accountId) {
1089
+ if (this.quickCheckRunning.has(accountId))
1090
+ return;
1091
+ this.quickCheckRunning.add(accountId);
1092
+ try {
1093
+ const config = this.configs.get(accountId);
1094
+ if (!config?.tokenProvider)
1095
+ return;
1096
+ const token = await config.tokenProvider();
1097
+ // Single API call: get just the first message ID
1098
+ const res = await globalThis.fetch(`https://gmail.googleapis.com/gmail/v1/users/me/messages?q=in:inbox&maxResults=1`, { headers: { "Authorization": `Bearer ${token}` } });
1099
+ if (!res.ok)
1100
+ return;
1101
+ const data = await res.json();
1102
+ const topId = data.messages?.[0]?.id || "";
1103
+ const prev = this.lastGmailInboxTop.get(accountId) ?? topId;
1104
+ this.lastGmailInboxTop.set(accountId, topId);
1105
+ if (topId && topId !== prev) {
1106
+ console.log(` [check] ${accountId} INBOX: new message detected`);
1107
+ const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
1108
+ if (inbox) {
1109
+ const api = this.getGmailProvider(accountId);
1110
+ await this.syncFolderViaApi(accountId, inbox, api);
1111
+ this.db.recalcFolderCounts(inbox.id);
1112
+ this.emit("folderCountsChanged", accountId, {});
1113
+ await api.close();
1114
+ }
1115
+ }
1116
+ }
1117
+ catch { /* lightweight — ignore errors */ }
1118
+ finally {
1119
+ this.quickCheckRunning.delete(accountId);
1120
+ }
1121
+ }
1085
1122
  /** Check all accounts (used by legacy callers) */
1086
1123
  async quickInboxCheck() {
1087
1124
  for (const [accountId] of this.configs) {