@bobfrankston/mailx 1.0.190 → 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.
- package/client/app.js +35 -18
- package/client/compose/compose.js +5 -0
- package/package.json +2 -2
- package/packages/mailx-imap/index.d.ts +3 -0
- package/packages/mailx-imap/index.js +39 -2
package/client/app.js
CHANGED
|
@@ -346,34 +346,46 @@ async function openCompose(mode) {
|
|
|
346
346
|
showComposeOverlay();
|
|
347
347
|
}
|
|
348
348
|
function showComposeOverlay() {
|
|
349
|
-
//
|
|
350
|
-
document.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 = "
|
|
357
|
-
// 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
|
-
|
|
363
|
-
win.close = () => overlay.remove();
|
|
381
|
+
win.close = () => wrapper.remove();
|
|
364
382
|
}
|
|
365
383
|
}
|
|
366
384
|
catch { /* cross-origin safety */ }
|
|
367
385
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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();
|
|
@@ -797,6 +809,11 @@ document.addEventListener("keydown", (e) => {
|
|
|
797
809
|
e.preventDefault();
|
|
798
810
|
openCompose("replyAll");
|
|
799
811
|
}
|
|
812
|
+
// Ctrl+F = Forward
|
|
813
|
+
if (e.ctrlKey && e.key.toLowerCase() === "f" && !e.shiftKey) {
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
openCompose("forward");
|
|
816
|
+
}
|
|
800
817
|
// Ctrl+A = Select all visible messages
|
|
801
818
|
if (e.ctrlKey && e.key === "a") {
|
|
802
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.
|
|
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.
|
|
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;
|
|
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) {
|