@bobfrankston/mailx 1.0.42 → 1.0.44
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 +47 -6
- package/client/index.html +17 -27
- package/client/styles/components.css +9 -2
- package/client/styles/layout.css +57 -5
- package/package.json +2 -2
- package/packages/mailx-server/index.js +7 -2
- package/packages/mailx-settings/index.js +19 -9
package/client/app.js
CHANGED
|
@@ -110,11 +110,40 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
110
110
|
});
|
|
111
111
|
initMessageList((accountId, uid, folderId) => {
|
|
112
112
|
showMessage(accountId, uid, folderId, currentFolderSpecialUse);
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
// Narrow screen: show message viewer, hide list
|
|
114
|
+
if (window.innerWidth <= 768) {
|
|
115
|
+
document.getElementById("message-viewer")?.classList.add("narrow-active");
|
|
116
|
+
document.getElementById("message-list")?.classList.add("narrow-hidden");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// ── Auto two-line when message list is narrow ──
|
|
120
|
+
const messageList = document.getElementById("message-list");
|
|
121
|
+
if (messageList) {
|
|
122
|
+
const twoLineThreshold = 600; // px — switch to two-line below this width
|
|
123
|
+
const userTwoLine = localStorage.getItem("mailx-two-line") === "true";
|
|
124
|
+
new ResizeObserver(([entry]) => {
|
|
125
|
+
const narrow = entry.contentRect.width < twoLineThreshold;
|
|
126
|
+
// Auto two-line when narrow, respect user preference when wide
|
|
127
|
+
if (narrow) {
|
|
128
|
+
messageList.classList.add("two-line");
|
|
129
|
+
}
|
|
130
|
+
else if (!userTwoLine) {
|
|
131
|
+
messageList.classList.remove("two-line");
|
|
132
|
+
}
|
|
133
|
+
}).observe(messageList);
|
|
134
|
+
}
|
|
135
|
+
// ── Narrow screen navigation ──
|
|
136
|
+
document.getElementById("btn-menu")?.addEventListener("click", () => {
|
|
137
|
+
document.querySelector(".folder-panel")?.classList.toggle("open");
|
|
138
|
+
});
|
|
139
|
+
document.getElementById("btn-back")?.addEventListener("click", () => {
|
|
140
|
+
document.getElementById("message-viewer")?.classList.remove("narrow-active");
|
|
141
|
+
document.getElementById("message-list")?.classList.remove("narrow-hidden");
|
|
142
|
+
});
|
|
143
|
+
// Close folder panel when a folder is selected (narrow mode)
|
|
144
|
+
document.getElementById("folder-tree")?.addEventListener("click", (e) => {
|
|
145
|
+
if (window.innerWidth <= 768 && e.target.closest(".ft-folder")) {
|
|
146
|
+
document.querySelector(".folder-panel")?.classList.remove("open");
|
|
118
147
|
}
|
|
119
148
|
});
|
|
120
149
|
// ── Toolbar actions ──
|
|
@@ -585,8 +614,9 @@ optFlagged?.addEventListener("change", () => {
|
|
|
585
614
|
const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
586
615
|
fetch("/api/version").then(r => r.json()).then(d => {
|
|
587
616
|
const el = document.getElementById("app-version");
|
|
617
|
+
const driveName = d.drive === "local" ? "" : ` [${d.drive?.split(/[/\\]/).pop() || d.drive}]`;
|
|
588
618
|
if (el)
|
|
589
|
-
el.textContent = `mailx s${d.server}/c${d.client}${isApp ? "" : " [browser]"}`;
|
|
619
|
+
el.textContent = `mailx s${d.server}/c${d.client}${driveName}${isApp ? "" : " [browser]"}`;
|
|
590
620
|
}).catch(async () => {
|
|
591
621
|
// Server not running — try to start it if we're in the app
|
|
592
622
|
const startupStatus = document.getElementById("startup-status");
|
|
@@ -639,6 +669,17 @@ setInterval(async () => {
|
|
|
639
669
|
}, 5000);
|
|
640
670
|
console.log("mailx client initialized, location:", location.href);
|
|
641
671
|
updateNewMessageCount();
|
|
672
|
+
// ── Midnight refresh — update date display when day changes ──
|
|
673
|
+
function scheduleMiddnightRefresh() {
|
|
674
|
+
const now = new Date();
|
|
675
|
+
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
676
|
+
const ms = midnight.getTime() - now.getTime();
|
|
677
|
+
setTimeout(() => {
|
|
678
|
+
reloadCurrentFolder();
|
|
679
|
+
scheduleMiddnightRefresh();
|
|
680
|
+
}, ms + 1000); // 1s after midnight
|
|
681
|
+
}
|
|
682
|
+
scheduleMiddnightRefresh();
|
|
642
683
|
// ── Apply theme from settings ──
|
|
643
684
|
fetch("/api/version").then(r => r.json()).then(d => {
|
|
644
685
|
if (d.theme === "dark")
|
package/client/index.html
CHANGED
|
@@ -13,25 +13,10 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<header class="toolbar">
|
|
15
15
|
<div class="toolbar-left">
|
|
16
|
+
<button class="tb-btn" id="btn-menu" title="Folders" hidden>☰</button>
|
|
16
17
|
<button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
|
|
17
18
|
<span class="tb-icon">✏</span> Compose
|
|
18
19
|
</button>
|
|
19
|
-
<button class="tb-btn" id="btn-reply" title="Reply (Ctrl+R)" disabled>
|
|
20
|
-
<span class="tb-icon">↩</span>
|
|
21
|
-
</button>
|
|
22
|
-
<button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)" disabled>
|
|
23
|
-
<span class="tb-icon">↩↩</span>
|
|
24
|
-
</button>
|
|
25
|
-
<button class="tb-btn" id="btn-forward" title="Forward" disabled>
|
|
26
|
-
<span class="tb-icon">→</span>
|
|
27
|
-
</button>
|
|
28
|
-
<span class="tb-sep"></span>
|
|
29
|
-
<button class="tb-btn" id="btn-delete" title="Delete (Del)" disabled>
|
|
30
|
-
<span class="tb-icon">🗑</span>
|
|
31
|
-
</button>
|
|
32
|
-
<button class="tb-btn" id="btn-flag" title="Flag" disabled>
|
|
33
|
-
<span class="tb-icon">⚑</span>
|
|
34
|
-
</button>
|
|
35
20
|
</div>
|
|
36
21
|
<div class="toolbar-center">
|
|
37
22
|
<div class="tb-menu" id="view-menu">
|
|
@@ -88,17 +73,22 @@
|
|
|
88
73
|
|
|
89
74
|
<section class="message-viewer" id="message-viewer">
|
|
90
75
|
<div class="mv-header" id="mv-header" hidden>
|
|
91
|
-
<div class="mv-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</
|
|
76
|
+
<div class="mv-toolbar">
|
|
77
|
+
<button class="tb-btn" id="btn-back" title="Back to list" hidden>←</button>
|
|
78
|
+
<button class="tb-btn" id="btn-reply" title="Reply (Ctrl+R)">↩</button>
|
|
79
|
+
<button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)">↩↩</button>
|
|
80
|
+
<button class="tb-btn" id="btn-forward" title="Forward">→</button>
|
|
81
|
+
<button class="tb-btn" id="btn-delete" title="Delete (Del)">🗑</button>
|
|
82
|
+
<button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
|
|
83
|
+
<span style="flex:1"></span>
|
|
84
|
+
<button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
|
|
85
|
+
<a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
|
|
86
|
+
<button class="mv-action" id="mv-view-source" title="View source (.eml)" hidden>Source</button>
|
|
87
|
+
<button class="mv-action" id="mv-toggle-details" title="Show/hide extra headers">Details</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="mv-header-info">
|
|
90
|
+
<div class="mv-from"></div>
|
|
91
|
+
<div class="mv-to"></div>
|
|
102
92
|
</div>
|
|
103
93
|
<div class="mv-subject"></div>
|
|
104
94
|
<div class="mv-date"></div>
|
|
@@ -374,8 +374,15 @@
|
|
|
374
374
|
font-size: var(--font-size-sm);
|
|
375
375
|
line-height: 1.5;
|
|
376
376
|
|
|
377
|
-
.mv-
|
|
378
|
-
|
|
377
|
+
.mv-toolbar {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: var(--gap-xs);
|
|
381
|
+
padding-bottom: var(--gap-xs);
|
|
382
|
+
border-bottom: 1px solid var(--color-border);
|
|
383
|
+
margin-bottom: var(--gap-xs);
|
|
384
|
+
}
|
|
385
|
+
.mv-header-info { }
|
|
379
386
|
.mv-header-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
|
|
380
387
|
.mv-from { font-weight: 600; }
|
|
381
388
|
.mv-to { color: var(--color-text-muted); }
|
package/client/styles/layout.css
CHANGED
|
@@ -55,8 +55,8 @@ body {
|
|
|
55
55
|
background: var(--color-accent);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/* Responsive: narrow viewport
|
|
59
|
-
@media (max-width:
|
|
58
|
+
/* Responsive: narrow viewport — single panel navigation */
|
|
59
|
+
@media (max-width: 768px) {
|
|
60
60
|
body {
|
|
61
61
|
grid-template-columns: 1fr;
|
|
62
62
|
grid-template-areas:
|
|
@@ -64,12 +64,64 @@ body {
|
|
|
64
64
|
"main"
|
|
65
65
|
"status";
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
|
|
68
|
+
/* Folder panel: overlay slide-in from left */
|
|
69
|
+
.folder-panel {
|
|
70
|
+
position: fixed;
|
|
71
|
+
left: -280px;
|
|
72
|
+
top: var(--toolbar-height);
|
|
73
|
+
bottom: var(--statusbar-height);
|
|
74
|
+
width: 280px;
|
|
75
|
+
z-index: 50;
|
|
76
|
+
transition: left 0.2s ease;
|
|
77
|
+
background: var(--color-bg);
|
|
78
|
+
border-right: 1px solid var(--color-border);
|
|
79
|
+
box-shadow: 2px 0 8px rgba(0,0,0,0.3);
|
|
80
|
+
}
|
|
81
|
+
.folder-panel.open { left: 0; }
|
|
82
|
+
|
|
83
|
+
/* Main area: single column */
|
|
68
84
|
.main-area {
|
|
69
85
|
grid-template-columns: 1fr;
|
|
86
|
+
grid-template-rows: 1fr;
|
|
70
87
|
}
|
|
71
88
|
.splitter { display: none; }
|
|
89
|
+
|
|
90
|
+
/* Show one panel at a time */
|
|
72
91
|
.message-viewer { display: none; }
|
|
73
|
-
.message-viewer.active {
|
|
74
|
-
|
|
92
|
+
.message-viewer.narrow-active {
|
|
93
|
+
display: flex;
|
|
94
|
+
position: absolute;
|
|
95
|
+
inset: 0;
|
|
96
|
+
z-index: 10;
|
|
97
|
+
background: var(--color-bg);
|
|
98
|
+
}
|
|
99
|
+
.message-list.narrow-hidden { display: none; }
|
|
100
|
+
|
|
101
|
+
/* Show hamburger and back buttons */
|
|
102
|
+
#btn-menu { display: inline-flex !important; }
|
|
103
|
+
#btn-back { display: inline-flex !important; }
|
|
104
|
+
|
|
105
|
+
/* Message list: full width, two-line rows */
|
|
106
|
+
.message-list {
|
|
107
|
+
grid-template-columns: 1.2em 1fr auto;
|
|
108
|
+
}
|
|
109
|
+
.message-list .ml-row {
|
|
110
|
+
grid-template-columns: subgrid;
|
|
111
|
+
grid-template-rows: auto auto;
|
|
112
|
+
}
|
|
113
|
+
.message-list .ml-flag { grid-row: 1 / 3; align-self: center; }
|
|
114
|
+
.message-list .ml-from { grid-column: 2; }
|
|
115
|
+
.message-list .ml-date { grid-column: 3; grid-row: 1; }
|
|
116
|
+
.message-list .ml-subject {
|
|
117
|
+
grid-column: 2 / 4;
|
|
118
|
+
grid-row: 2;
|
|
119
|
+
font-size: var(--font-size-sm);
|
|
120
|
+
color: var(--color-text-muted);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Hide hamburger and back on wide screens */
|
|
125
|
+
@media (min-width: 769px) {
|
|
126
|
+
#btn-menu, #btn-back { display: none !important; }
|
|
75
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"postinstall": "node launcher/builder/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow": "^1.0.
|
|
23
|
+
"@bobfrankston/iflow": "^1.0.24",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.11",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.2",
|
|
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import { MailxDB } from "@bobfrankston/mailx-store";
|
|
10
10
|
import { ImapManager } from "@bobfrankston/mailx-imap";
|
|
11
11
|
import { createApiRouter } from "@bobfrankston/mailx-api";
|
|
12
|
-
import { loadSettings, getConfigDir, initLocalConfig } from "@bobfrankston/mailx-settings";
|
|
12
|
+
import { loadSettings, getConfigDir, getSharedDir, initLocalConfig } from "@bobfrankston/mailx-settings";
|
|
13
13
|
import { ports } from "@bobfrankston/miscinfo";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
15
|
const PORT = ports.mailx;
|
|
@@ -81,7 +81,12 @@ app.use("/node_modules", express.static(path.join(rootDir, "node_modules"), { et
|
|
|
81
81
|
// Mount API
|
|
82
82
|
const apiRouter = createApiRouter(db, imapManager);
|
|
83
83
|
app.use("/api", apiRouter);
|
|
84
|
-
app.get("/api/version", (req, res) =>
|
|
84
|
+
app.get("/api/version", (req, res) => {
|
|
85
|
+
const sharedDir = getSharedDir();
|
|
86
|
+
const localDir = getConfigDir();
|
|
87
|
+
const drive = sharedDir === localDir ? "local" : sharedDir;
|
|
88
|
+
res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system", drive });
|
|
89
|
+
});
|
|
85
90
|
app.get("/status", (req, res) => {
|
|
86
91
|
const accounts = db.getAccounts();
|
|
87
92
|
const pendingSync = db.getTotalPendingSyncCount();
|
|
@@ -91,18 +91,28 @@ function resolveProvider(cfg) {
|
|
|
91
91
|
}
|
|
92
92
|
/** Pending cloud config for API fallback (set when mount not found) */
|
|
93
93
|
let pendingCloudConfig = null;
|
|
94
|
+
function resolveSharedEntry(entry) {
|
|
95
|
+
if (typeof entry === "string") {
|
|
96
|
+
const p = resolvePath(entry);
|
|
97
|
+
return fs.existsSync(p) ? p : undefined;
|
|
98
|
+
}
|
|
99
|
+
return resolveProvider(entry);
|
|
100
|
+
}
|
|
94
101
|
function getSharedDir() {
|
|
95
102
|
const config = readLocalConfig();
|
|
96
103
|
if (config.sharedDir) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const resolved = resolveSharedEntry(entry);
|
|
107
|
+
if (resolved)
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
// Nothing mounted — save last provider entry for API fallback
|
|
111
|
+
const lastProvider = [...entries].reverse().find(e => typeof e !== "string");
|
|
112
|
+
if (lastProvider) {
|
|
113
|
+
pendingCloudConfig = lastProvider;
|
|
114
|
+
console.log(` No cloud drive mounted — will try ${lastProvider.provider} API`);
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
117
|
// Legacy: derive from settingsPath
|
|
108
118
|
if (config.settingsPath)
|