@bobfrankston/mailx 1.0.296 → 1.0.298
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.js +34 -1
- package/client/.msger-window.json +1 -1
- package/client/app.js +44 -0
- package/client/components/context-menu.js +32 -5
- package/client/lib/mailxapi.js +5 -0
- package/package.json +1 -1
- package/packages/mailx-imap/index.js +21 -3
package/bin/mailx.js
CHANGED
|
@@ -835,6 +835,17 @@ async function main() {
|
|
|
835
835
|
const clientDir = path.join(import.meta.dirname, "..", "client");
|
|
836
836
|
const mailxapiPath = path.join(clientDir, "lib", "mailxapi.js");
|
|
837
837
|
const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
|
|
838
|
+
// Restore saved window geometry (position + size) from previous session
|
|
839
|
+
const windowJsonPath = path.join(getConfigDir(), "window.json");
|
|
840
|
+
let savedGeometry = null;
|
|
841
|
+
try {
|
|
842
|
+
const raw = JSON.parse(fs.readFileSync(windowJsonPath, "utf-8"));
|
|
843
|
+
if (typeof raw.width === "number" && raw.width > 200 &&
|
|
844
|
+
typeof raw.height === "number" && raw.height > 200) {
|
|
845
|
+
savedGeometry = raw;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch { /* no saved geometry — use defaults */ }
|
|
838
849
|
const rootPkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version;
|
|
839
850
|
const handle = showService({
|
|
840
851
|
title: `mailx v${rootPkgVersion}`,
|
|
@@ -843,7 +854,12 @@ async function main() {
|
|
|
843
854
|
initScript: mailxapiScript,
|
|
844
855
|
icon: path.join(clientDir, "icon.png"),
|
|
845
856
|
aumid: "com.frankston.mailx",
|
|
846
|
-
size:
|
|
857
|
+
size: savedGeometry
|
|
858
|
+
? { width: savedGeometry.width, height: savedGeometry.height }
|
|
859
|
+
: { width: 1400, height: 900 },
|
|
860
|
+
pos: savedGeometry
|
|
861
|
+
? { x: savedGeometry.x, y: savedGeometry.y }
|
|
862
|
+
: undefined,
|
|
847
863
|
escapeCloses: false,
|
|
848
864
|
});
|
|
849
865
|
// Register ourselves as the live instance so subsequent `mailx` invocations
|
|
@@ -868,6 +884,23 @@ async function main() {
|
|
|
868
884
|
return;
|
|
869
885
|
}
|
|
870
886
|
console.log(`[ipc] ← ${req._action} (${req._cbid})`);
|
|
887
|
+
// Save window position+size for next launch (handled here, not in MailxService)
|
|
888
|
+
if (req._action === "saveWindowGeometry") {
|
|
889
|
+
try {
|
|
890
|
+
const geom = {
|
|
891
|
+
x: Number(req.x) || 0,
|
|
892
|
+
y: Number(req.y) || 0,
|
|
893
|
+
width: Math.max(400, Number(req.width) || 1400),
|
|
894
|
+
height: Math.max(300, Number(req.height) || 900),
|
|
895
|
+
};
|
|
896
|
+
fs.writeFileSync(windowJsonPath, JSON.stringify(geom, null, 2));
|
|
897
|
+
}
|
|
898
|
+
catch (e) {
|
|
899
|
+
console.error(`[window] Failed to save geometry: ${e.message}`);
|
|
900
|
+
}
|
|
901
|
+
handle.send({ _cbid: req._cbid, result: { ok: true } });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
871
904
|
// Auto-update action: run npm install then restart
|
|
872
905
|
if (req._action === "performUpdate") {
|
|
873
906
|
handle.send({ _cbid: req._cbid, ok: true, status: "updating" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":
|
|
1
|
+
{"height":1420,"width":2304,"x":528,"y":99}
|
package/client/app.js
CHANGED
|
@@ -691,6 +691,32 @@ document.addEventListener("mailx-moved", (e) => {
|
|
|
691
691
|
undoTimeout = setTimeout(() => { lastMoved = null; }, 60000);
|
|
692
692
|
});
|
|
693
693
|
document.getElementById("btn-delete")?.addEventListener("click", deleteSelectedMessages);
|
|
694
|
+
// ── Flag toggle ──
|
|
695
|
+
document.getElementById("btn-flag")?.addEventListener("click", async () => {
|
|
696
|
+
const sel = messageState.getSelected();
|
|
697
|
+
if (!sel)
|
|
698
|
+
return;
|
|
699
|
+
const isFlagged = sel.flags.includes("\\Flagged");
|
|
700
|
+
const newFlags = isFlagged
|
|
701
|
+
? sel.flags.filter((f) => f !== "\\Flagged")
|
|
702
|
+
: [...sel.flags, "\\Flagged"];
|
|
703
|
+
try {
|
|
704
|
+
await updateFlags(sel.accountId, sel.uid, newFlags);
|
|
705
|
+
sel.flags = newFlags;
|
|
706
|
+
messageState.updateMessageFlags(sel.accountId, sel.uid, newFlags);
|
|
707
|
+
// Update the message-list row's flag indicator
|
|
708
|
+
const row = document.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`);
|
|
709
|
+
if (row) {
|
|
710
|
+
row.classList.toggle("flagged", newFlags.includes("\\Flagged"));
|
|
711
|
+
const flagEl = row.querySelector(".ml-flag");
|
|
712
|
+
if (flagEl)
|
|
713
|
+
flagEl.textContent = newFlags.includes("\\Flagged") ? "\u2605" : "\u2606";
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch (e) {
|
|
717
|
+
console.error(`Flag toggle failed: ${e.message}`);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
694
720
|
async function spamSelectedMessages() {
|
|
695
721
|
const selected = getSelectedMessages();
|
|
696
722
|
if (selected.length === 0) {
|
|
@@ -1660,4 +1686,22 @@ versionPromise.then((d) => {
|
|
|
1660
1686
|
else if (d.theme === "light")
|
|
1661
1687
|
document.documentElement.classList.add("theme-light");
|
|
1662
1688
|
}).catch(() => { });
|
|
1689
|
+
// ── Save window geometry on close (IPC mode only) ──
|
|
1690
|
+
// Sends window position and size so the next launch restores them.
|
|
1691
|
+
if (isApp) {
|
|
1692
|
+
const ipcApi = window.mailxapi;
|
|
1693
|
+
function sendGeometry() {
|
|
1694
|
+
if (!ipcApi?.saveWindowGeometry)
|
|
1695
|
+
return;
|
|
1696
|
+
ipcApi.saveWindowGeometry({
|
|
1697
|
+
x: window.screenX,
|
|
1698
|
+
y: window.screenY,
|
|
1699
|
+
width: window.outerWidth,
|
|
1700
|
+
height: window.outerHeight,
|
|
1701
|
+
}).catch(() => { });
|
|
1702
|
+
}
|
|
1703
|
+
// Save on unload (window close) and periodically as a safety net
|
|
1704
|
+
window.addEventListener("beforeunload", sendGeometry);
|
|
1705
|
+
setInterval(sendGeometry, 60_000);
|
|
1706
|
+
}
|
|
1663
1707
|
//# sourceMappingURL=app.js.map
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
* Shows a menu at a given position with clickable items.
|
|
4
4
|
*/
|
|
5
5
|
let activeMenu = null;
|
|
6
|
-
|
|
6
|
+
let dismissListener = null;
|
|
7
|
+
let escapeListener = null;
|
|
8
|
+
/** Close any open context menu and remove dismiss listeners */
|
|
7
9
|
export function closeContextMenu() {
|
|
8
10
|
if (activeMenu) {
|
|
9
11
|
activeMenu.remove();
|
|
10
12
|
activeMenu = null;
|
|
11
13
|
}
|
|
14
|
+
if (dismissListener) {
|
|
15
|
+
document.removeEventListener("pointerdown", dismissListener, true);
|
|
16
|
+
dismissListener = null;
|
|
17
|
+
}
|
|
18
|
+
if (escapeListener) {
|
|
19
|
+
document.removeEventListener("keydown", escapeListener, true);
|
|
20
|
+
escapeListener = null;
|
|
21
|
+
}
|
|
12
22
|
}
|
|
13
23
|
/** Show a context menu at the given position */
|
|
14
24
|
export function showContextMenu(x, y, items) {
|
|
@@ -43,11 +53,28 @@ export function showContextMenu(x, y, items) {
|
|
|
43
53
|
if (rect.bottom > window.innerHeight)
|
|
44
54
|
menu.style.top = `${y - rect.height}px`;
|
|
45
55
|
activeMenu = menu;
|
|
56
|
+
// Dismiss on click/tap outside the menu. Uses pointerdown in capture phase
|
|
57
|
+
// so it fires before any child handler and catches both left- and right-clicks.
|
|
58
|
+
// Deferred by one frame so the opening pointerdown doesn't immediately close it.
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
dismissListener = (e) => {
|
|
61
|
+
if (activeMenu && !activeMenu.contains(e.target)) {
|
|
62
|
+
closeContextMenu();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener("pointerdown", dismissListener, true);
|
|
66
|
+
escapeListener = (e) => {
|
|
67
|
+
if (e.key === "Escape") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
closeContextMenu();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener("keydown", escapeListener, true);
|
|
74
|
+
});
|
|
46
75
|
}
|
|
47
|
-
//
|
|
48
|
-
document.addEventListener("click", closeContextMenu);
|
|
49
|
-
document.addEventListener("keydown", (e) => { if (e.key === "Escape")
|
|
50
|
-
closeContextMenu(); });
|
|
76
|
+
// Scroll anywhere closes the menu (capture phase so nested scrollers trigger it)
|
|
51
77
|
document.addEventListener("scroll", closeContextMenu, true);
|
|
78
|
+
// A new right-click that opens a different menu goes through showContextMenu→closeContextMenu
|
|
52
79
|
document.addEventListener("contextmenu", () => { });
|
|
53
80
|
//# sourceMappingURL=context-menu.js.map
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -172,6 +172,11 @@
|
|
|
172
172
|
},
|
|
173
173
|
repairAccounts: function() { return callNode("repairAccounts"); },
|
|
174
174
|
|
|
175
|
+
// Window geometry
|
|
176
|
+
saveWindowGeometry: function(geom) {
|
|
177
|
+
return callNode("saveWindowGeometry", geom);
|
|
178
|
+
},
|
|
179
|
+
|
|
175
180
|
// Events
|
|
176
181
|
onEvent: function(handler) { _eventHandlers.push(handler); },
|
|
177
182
|
};
|
package/package.json
CHANGED
|
@@ -1239,9 +1239,27 @@ export class ImapManager extends EventEmitter {
|
|
|
1239
1239
|
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
1240
1240
|
if (!inbox)
|
|
1241
1241
|
continue;
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1242
|
+
// Gmail: use REST API, NOT IMAP. Mixing paths causes UID
|
|
1243
|
+
// mismatch — API uses hashed IDs, IMAP uses server-assigned
|
|
1244
|
+
// UIDs. The IMAP reconcile then deletes every API-synced
|
|
1245
|
+
// message because their UIDs don't appear in the IMAP list.
|
|
1246
|
+
if (this.isGmailAccount(accountId)) {
|
|
1247
|
+
const api = this.getGmailProvider(accountId);
|
|
1248
|
+
try {
|
|
1249
|
+
await this.syncFolderViaApi(accountId, inbox, api);
|
|
1250
|
+
}
|
|
1251
|
+
finally {
|
|
1252
|
+
try {
|
|
1253
|
+
await api.close();
|
|
1254
|
+
}
|
|
1255
|
+
catch { /* ignore */ }
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
await this.withConnection(accountId, async (client) => {
|
|
1260
|
+
await this.syncFolder(accountId, inbox.id, client);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1245
1263
|
}
|
|
1246
1264
|
catch (e) {
|
|
1247
1265
|
console.error(` [inbox] Sync error for ${accountId}: ${e.message}`);
|