@bobfrankston/mailx 1.0.377 → 1.0.379
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/components/calendar-sidebar.js +40 -79
- package/client/components/message-list.js +37 -1
- package/client/components/message-viewer.js +17 -0
- package/client/index.html +5 -1
- package/client/lib/api-client.js +35 -4
- package/client/lib/mailxapi.js +14 -1
- package/client/styles/components.css +18 -0
- package/client/styles/layout.css +17 -10
- package/package.json +1 -1
- package/packages/mailx-imap/index.js +5 -1
- package/packages/mailx-service/google-sync.d.ts +83 -0
- package/packages/mailx-service/google-sync.js +140 -0
- package/packages/mailx-service/index.d.ts +52 -7
- package/packages/mailx-service/index.js +240 -9
- package/packages/mailx-service/jsonrpc.js +26 -1
- package/packages/mailx-settings/index.js +3 -0
- package/packages/mailx-store/db.d.ts +48 -0
- package/packages/mailx-store/db.js +246 -2
- package/packages/mailx-store-web/android-bootstrap.js +19 -0
- package/packages/mailx-types/index.d.ts +4 -1
|
@@ -10,68 +10,33 @@
|
|
|
10
10
|
* Sidebar and the full-screen calendar modal (calendar.ts) read the SAME
|
|
11
11
|
* underlying data — two views onto one source.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* All storage goes through the service-side two-way cache (calendar_events
|
|
14
|
+
* and tasks tables); this file does not use localStorage for data.
|
|
14
15
|
*/
|
|
15
|
-
import {
|
|
16
|
-
const LOCAL_STORE_KEY = "mailx-cal-events-v1";
|
|
17
|
-
const TASK_STORE_KEY = "mailx-tasks-v1";
|
|
16
|
+
import { getCalendarEvents, createCalendarEvent, getTasks, createTask, updateTask, } from "../lib/api-client.js";
|
|
18
17
|
const SIDEBAR_PREF = "mailx-calendar-sidebar-on";
|
|
19
18
|
let viewYear = new Date().getFullYear();
|
|
20
19
|
let viewMonth = new Date().getMonth();
|
|
21
20
|
let viewDay = new Date().getDate();
|
|
22
21
|
let lastEvents = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const arr = JSON.parse(raw);
|
|
29
|
-
return Array.isArray(arr) ? arr : [];
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function loadTasks() {
|
|
36
|
-
try {
|
|
37
|
-
const raw = localStorage.getItem(TASK_STORE_KEY);
|
|
38
|
-
if (!raw)
|
|
39
|
-
return [];
|
|
40
|
-
const arr = JSON.parse(raw);
|
|
41
|
-
return Array.isArray(arr) ? arr : [];
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function saveTasks(tasks) {
|
|
48
|
-
try {
|
|
49
|
-
localStorage.setItem(TASK_STORE_KEY, JSON.stringify(tasks));
|
|
50
|
-
}
|
|
51
|
-
catch { /* */ }
|
|
52
|
-
}
|
|
53
|
-
/** Fetch upcoming events from Google Calendar via the primary account's
|
|
54
|
-
* OAuth token. Returns merged Google + local events for the next 30 days
|
|
55
|
-
* starting at `from`. Quietly returns local-only on any error so the
|
|
56
|
-
* sidebar still works without network / without Google Calendar scope. */
|
|
22
|
+
/** Fetch events from the local two-way cache; service returns local rows
|
|
23
|
+
* immediately and kicks a background refresh from Google. Next render
|
|
24
|
+
* (view-nav or user action) picks up the refreshed rows. No localStorage
|
|
25
|
+
* — everything lives in the service-side DB so phone / desktop share
|
|
26
|
+
* the same events. */
|
|
57
27
|
async function fetchUpcoming(from) {
|
|
58
|
-
const local = loadLocalEvents();
|
|
59
|
-
let google = [];
|
|
60
|
-
try {
|
|
61
|
-
const primary = await getPrimaryAccount();
|
|
62
|
-
if (primary?.email) {
|
|
63
|
-
// The OAuth token is held by the service-side OAuthTokenManager;
|
|
64
|
-
// calendar fetch goes through a server-side proxy method (not
|
|
65
|
-
// implemented yet — this is the seam). For now: local-only.
|
|
66
|
-
// Wire-up: TODO — service.fetchGoogleCalendarEvents(accountId, from, days).
|
|
67
|
-
// Sidebar already renders correctly with the merged result.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch { /* google unavailable, fall back to local */ }
|
|
71
28
|
const horizon = from.getTime() + 30 * 86400_000;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
29
|
+
const rows = await getCalendarEvents(from.getTime(), horizon);
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
id: r.uuid,
|
|
32
|
+
title: r.title,
|
|
33
|
+
start: r.startMs,
|
|
34
|
+
end: r.endMs,
|
|
35
|
+
allDay: !!r.allDay,
|
|
36
|
+
location: r.location,
|
|
37
|
+
notes: r.notes,
|
|
38
|
+
source: r.providerId ? "google" : "local",
|
|
39
|
+
}));
|
|
75
40
|
}
|
|
76
41
|
function formatDayHeader(d, today, tomorrow) {
|
|
77
42
|
const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
@@ -124,9 +89,9 @@ function renderEvents(events) {
|
|
|
124
89
|
}
|
|
125
90
|
body.innerHTML = html;
|
|
126
91
|
}
|
|
127
|
-
function renderTasks() {
|
|
92
|
+
async function renderTasks() {
|
|
128
93
|
const showDone = document.getElementById("cal-side-show-done")?.checked || false;
|
|
129
|
-
const tasks =
|
|
94
|
+
const tasks = await getTasks(showDone);
|
|
130
95
|
const host = document.getElementById("cal-side-tasks");
|
|
131
96
|
if (!host)
|
|
132
97
|
return;
|
|
@@ -136,23 +101,19 @@ function renderTasks() {
|
|
|
136
101
|
}
|
|
137
102
|
let html = "<div class='cal-side-task-head'>Title</div>";
|
|
138
103
|
for (const t of tasks) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
104
|
+
const done = !!t.completedMs;
|
|
105
|
+
html += `<div class="cal-side-task" data-uuid="${t.uuid}">
|
|
106
|
+
<input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
|
|
107
|
+
<span class="cal-side-task-title${done ? " done" : ""}">${escapeHtml(t.title)}</span>
|
|
142
108
|
</div>`;
|
|
143
109
|
}
|
|
144
110
|
host.innerHTML = html;
|
|
145
111
|
host.querySelectorAll(".cal-side-task").forEach(row => {
|
|
146
|
-
const
|
|
147
|
-
row.querySelector(".cal-side-task-check")?.addEventListener("change", (e) => {
|
|
112
|
+
const uuid = row.dataset.uuid;
|
|
113
|
+
row.querySelector(".cal-side-task-check")?.addEventListener("change", async (e) => {
|
|
148
114
|
const checked = e.target.checked;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (t) {
|
|
152
|
-
t.completed = checked ? Date.now() : undefined;
|
|
153
|
-
saveTasks(all);
|
|
154
|
-
renderTasks();
|
|
155
|
-
}
|
|
115
|
+
await updateTask(uuid, { completedMs: checked ? Date.now() : null });
|
|
116
|
+
renderTasks();
|
|
156
117
|
});
|
|
157
118
|
});
|
|
158
119
|
}
|
|
@@ -225,19 +186,19 @@ export function initCalendarSidebar() {
|
|
|
225
186
|
alert("Couldn't parse that date.");
|
|
226
187
|
return;
|
|
227
188
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
title, start,
|
|
231
|
-
|
|
232
|
-
};
|
|
233
|
-
const all = loadLocalEvents();
|
|
234
|
-
all.push(ev);
|
|
235
|
-
try {
|
|
236
|
-
localStorage.setItem(LOCAL_STORE_KEY, JSON.stringify(all));
|
|
237
|
-
}
|
|
238
|
-
catch { /* */ }
|
|
189
|
+
// Two-way cache: commit locally, service queues the push to Google.
|
|
190
|
+
await createCalendarEvent({
|
|
191
|
+
title, startMs: start, endMs: start + (allDay ? 86400_000 : 3600_000), allDay,
|
|
192
|
+
});
|
|
239
193
|
await refresh();
|
|
240
194
|
});
|
|
195
|
+
wireOnce("cal-side-new-task", async () => {
|
|
196
|
+
const title = prompt("Task title:");
|
|
197
|
+
if (!title)
|
|
198
|
+
return;
|
|
199
|
+
await createTask({ title });
|
|
200
|
+
renderTasks();
|
|
201
|
+
});
|
|
241
202
|
const showDoneCb = document.getElementById("cal-side-show-done");
|
|
242
203
|
if (showDoneCb && !showDoneCb.__wired) {
|
|
243
204
|
showDoneCb.__wired = true;
|
|
@@ -370,7 +370,7 @@ function restoreSelection(body, savedUid) {
|
|
|
370
370
|
/** Show a floating list of all messages in a thread when the pill is clicked.
|
|
371
371
|
* Each entry in the popup selects that message in the viewer when clicked.
|
|
372
372
|
* This is simpler than inline expansion and avoids duplicating the row builder. */
|
|
373
|
-
async function showThreadPopup(pillEl, headMsg) {
|
|
373
|
+
export async function showThreadPopup(pillEl, headMsg) {
|
|
374
374
|
// Remove any existing popup
|
|
375
375
|
document.querySelectorAll(".ml-thread-popup").forEach(el => el.remove());
|
|
376
376
|
let thread = [];
|
|
@@ -464,6 +464,11 @@ function appendMessages(body, accountId, items) {
|
|
|
464
464
|
// action (move/flag/delete) hasn't been ACK'd by the server yet.
|
|
465
465
|
if (msg.pending)
|
|
466
466
|
row.classList.add("pending-reconcile");
|
|
467
|
+
// Reply-row marker: messages with In-Reply-To are replies. Shows a
|
|
468
|
+
// subtle left-border accent so the eye can pick out threaded replies
|
|
469
|
+
// without enabling full thread grouping.
|
|
470
|
+
if (msg.inReplyTo)
|
|
471
|
+
row.classList.add("is-reply");
|
|
467
472
|
row.dataset.uid = String(msg.uid);
|
|
468
473
|
row.dataset.accountId = msgAccountId;
|
|
469
474
|
row.dataset.folderId = String(msg.folderId);
|
|
@@ -600,6 +605,37 @@ function appendMessages(body, accountId, items) {
|
|
|
600
605
|
}
|
|
601
606
|
});
|
|
602
607
|
row.addEventListener("dragend", () => row.classList.remove("dragging"));
|
|
608
|
+
// ── Q66: long-press on touch → context menu ──
|
|
609
|
+
// Mirrors right-click on the phone where right-click isn't a thing.
|
|
610
|
+
// Cancelled by any touchmove or touchend before the threshold.
|
|
611
|
+
let longPressTimer = null;
|
|
612
|
+
const LONG_PRESS_MS = 550;
|
|
613
|
+
row.addEventListener("touchstart", (e) => {
|
|
614
|
+
const t = e.touches[0];
|
|
615
|
+
if (!t)
|
|
616
|
+
return;
|
|
617
|
+
const cx = t.clientX, cy = t.clientY;
|
|
618
|
+
if (longPressTimer)
|
|
619
|
+
clearTimeout(longPressTimer);
|
|
620
|
+
longPressTimer = setTimeout(() => {
|
|
621
|
+
longPressTimer = null;
|
|
622
|
+
// Synthesize a contextmenu event so the existing handler below
|
|
623
|
+
// owns all the menu logic — no per-event duplication.
|
|
624
|
+
const ev = new MouseEvent("contextmenu", {
|
|
625
|
+
clientX: cx, clientY: cy, bubbles: true, cancelable: true,
|
|
626
|
+
});
|
|
627
|
+
row.dispatchEvent(ev);
|
|
628
|
+
}, LONG_PRESS_MS);
|
|
629
|
+
}, { passive: true });
|
|
630
|
+
const cancelLongPress = () => {
|
|
631
|
+
if (longPressTimer) {
|
|
632
|
+
clearTimeout(longPressTimer);
|
|
633
|
+
longPressTimer = null;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
row.addEventListener("touchmove", cancelLongPress, { passive: true });
|
|
637
|
+
row.addEventListener("touchend", cancelLongPress, { passive: true });
|
|
638
|
+
row.addEventListener("touchcancel", cancelLongPress, { passive: true });
|
|
603
639
|
// ── Right-click context menu ──
|
|
604
640
|
row.addEventListener("contextmenu", (e) => {
|
|
605
641
|
e.preventDefault();
|
|
@@ -395,6 +395,23 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
395
395
|
unsubBtn.hidden = true;
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
|
+
// View Thread button — opens the thread popup from the message list
|
|
399
|
+
// so the user can see all messages in the conversation. Works from
|
|
400
|
+
// the viewer even when thread-grouping is off.
|
|
401
|
+
const threadBtn = document.getElementById("mv-view-thread");
|
|
402
|
+
if (threadBtn) {
|
|
403
|
+
const tid = msg.threadId || "";
|
|
404
|
+
if (tid) {
|
|
405
|
+
threadBtn.hidden = false;
|
|
406
|
+
threadBtn.onclick = async () => {
|
|
407
|
+
const { showThreadPopup } = await import("./message-list.js");
|
|
408
|
+
await showThreadPopup(threadBtn, { accountId, threadId: tid });
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
threadBtn.hidden = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
398
415
|
// View Source button — shows .eml file path
|
|
399
416
|
const srcBtn = document.getElementById("mv-view-source");
|
|
400
417
|
if (srcBtn) {
|
package/client/index.html
CHANGED
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
<button class="tb-btn" id="btn-spam" title="Mark as spam — move to configured spam folder" hidden>⚠</button>
|
|
135
135
|
<button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
|
|
136
136
|
<button class="tb-btn" id="btn-mark-unread" title="Mark unread (R)">◉</button>
|
|
137
|
+
<button class="tb-btn" id="mv-view-thread" title="View thread (conversation)" hidden>💬</button>
|
|
137
138
|
<span style="flex:1"></span>
|
|
138
139
|
<button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
|
|
139
140
|
<a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
|
|
@@ -169,7 +170,10 @@
|
|
|
169
170
|
<div class="cal-side-empty">Loading…</div>
|
|
170
171
|
</div>
|
|
171
172
|
<footer class="cal-side-foot">
|
|
172
|
-
<
|
|
173
|
+
<div class="cal-side-task-header-row">
|
|
174
|
+
<label><input type="checkbox" id="cal-side-show-done"> Show completed Tasks</label>
|
|
175
|
+
<button class="cal-side-new" id="cal-side-new-task" title="New task">+ Task</button>
|
|
176
|
+
</div>
|
|
173
177
|
<div class="cal-side-tasks" id="cal-side-tasks"></div>
|
|
174
178
|
</footer>
|
|
175
179
|
</aside>
|
package/client/lib/api-client.js
CHANGED
|
@@ -134,10 +134,41 @@ export function getSyncPending() {
|
|
|
134
134
|
export function getDiagnostics() {
|
|
135
135
|
return ipc().getDiagnostics?.() ?? Promise.resolve([]);
|
|
136
136
|
}
|
|
137
|
-
/** Account
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
/** Account that supplies `feature` data (calendar / tasks / contacts).
|
|
138
|
+
* Resolution: per-feature primary flag → catch-all `primary` → first account.
|
|
139
|
+
* Pass e.g. "calendar" to honor `primaryCalendar:true` overrides; omit for
|
|
140
|
+
* back-compat single-flag behavior. */
|
|
141
|
+
export function getPrimaryAccount(feature) {
|
|
142
|
+
return ipc().getPrimaryAccount?.(feature) ?? Promise.resolve(null);
|
|
143
|
+
}
|
|
144
|
+
// Calendar / Tasks: two-way cache. Reads return local-cached rows; writes
|
|
145
|
+
// commit locally and queue a push to Google. Service layer handles drain.
|
|
146
|
+
export function getCalendarEvents(fromMs, toMs) {
|
|
147
|
+
return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
|
|
148
|
+
}
|
|
149
|
+
export function createCalendarEvent(ev) {
|
|
150
|
+
return ipc().createCalendarEvent?.(ev);
|
|
151
|
+
}
|
|
152
|
+
export function updateCalendarEvent(uuid, patch) {
|
|
153
|
+
return ipc().updateCalendarEvent?.(uuid, patch);
|
|
154
|
+
}
|
|
155
|
+
export function deleteCalendarEvent(uuid) {
|
|
156
|
+
return ipc().deleteCalendarEvent?.(uuid);
|
|
157
|
+
}
|
|
158
|
+
export function getTasks(includeCompleted = false) {
|
|
159
|
+
return ipc().getTasks?.(includeCompleted) ?? Promise.resolve([]);
|
|
160
|
+
}
|
|
161
|
+
export function createTask(t) {
|
|
162
|
+
return ipc().createTask?.(t);
|
|
163
|
+
}
|
|
164
|
+
export function updateTask(uuid, patch) {
|
|
165
|
+
return ipc().updateTask?.(uuid, patch);
|
|
166
|
+
}
|
|
167
|
+
export function deleteTask(uuid) {
|
|
168
|
+
return ipc().deleteTask?.(uuid);
|
|
169
|
+
}
|
|
170
|
+
export function drainStoreSync() {
|
|
171
|
+
return ipc().drainStoreSync?.();
|
|
141
172
|
}
|
|
142
173
|
export function getOutboxStatus() {
|
|
143
174
|
return ipc().getOutboxStatus();
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -109,6 +109,19 @@
|
|
|
109
109
|
getThreadMessages: function(accountId, threadId) {
|
|
110
110
|
return callNode("getThreadMessages", { accountId: accountId, threadId: threadId });
|
|
111
111
|
},
|
|
112
|
+
// Calendar / Tasks two-way cache. Reads return local DB immediately;
|
|
113
|
+
// writes commit locally and queue a push-to-Google action.
|
|
114
|
+
getCalendarEvents: function(fromMs, toMs) {
|
|
115
|
+
return callNode("getCalendarEvents", { fromMs: fromMs, toMs: toMs });
|
|
116
|
+
},
|
|
117
|
+
createCalendarEvent: function(ev) { return callNode("createCalendarEvent", ev); },
|
|
118
|
+
updateCalendarEvent: function(uuid, patch) { return callNode("updateCalendarEvent", { uuid: uuid, patch: patch }); },
|
|
119
|
+
deleteCalendarEvent: function(uuid) { return callNode("deleteCalendarEvent", { uuid: uuid }); },
|
|
120
|
+
getTasks: function(includeCompleted) { return callNode("getTasks", { includeCompleted: !!includeCompleted }); },
|
|
121
|
+
createTask: function(t) { return callNode("createTask", t); },
|
|
122
|
+
updateTask: function(uuid, patch) { return callNode("updateTask", { uuid: uuid, patch: patch }); },
|
|
123
|
+
deleteTask: function(uuid) { return callNode("deleteTask", { uuid: uuid }); },
|
|
124
|
+
drainStoreSync: function() { return callNode("drainStoreSync"); },
|
|
112
125
|
readJsoncFile: function(name) {
|
|
113
126
|
return callNode("readJsoncFile", { name: name });
|
|
114
127
|
},
|
|
@@ -160,7 +173,7 @@
|
|
|
160
173
|
getSyncPending: function() { return callNode("getSyncPending"); },
|
|
161
174
|
getOutboxStatus: function() { return callNode("getOutboxStatus"); },
|
|
162
175
|
getDiagnostics: function() { return callNode("getDiagnostics"); },
|
|
163
|
-
getPrimaryAccount: function() { return callNode("getPrimaryAccount"); },
|
|
176
|
+
getPrimaryAccount: function(feature) { return callNode("getPrimaryAccount", { feature: feature }); },
|
|
164
177
|
listQueuedOutgoing: function() { return callNode("listQueuedOutgoing"); },
|
|
165
178
|
cancelQueuedOutgoing: function(p) { return callNode("cancelQueuedOutgoing", { path: p }); },
|
|
166
179
|
reauthenticate: function(accountId) { return callNode("reauthenticate", { accountId: accountId }); },
|
|
@@ -788,6 +788,24 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
|
|
|
788
788
|
opacity: 0.5;
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
+
/* Reply marker — messages whose In-Reply-To points at another message get a
|
|
792
|
+
subtle left-edge accent so threaded replies visibly distinguish from top-
|
|
793
|
+
level posts without needing thread-grouping mode on. Works regardless of
|
|
794
|
+
whether thread-grouping is enabled. */
|
|
795
|
+
.ml-row.is-reply::before {
|
|
796
|
+
content: "";
|
|
797
|
+
position: absolute;
|
|
798
|
+
left: 0;
|
|
799
|
+
top: 2px;
|
|
800
|
+
bottom: 2px;
|
|
801
|
+
width: 2px;
|
|
802
|
+
background: oklch(0.70 0.12 250); /* muted blue */
|
|
803
|
+
border-radius: 0 1px 1px 0;
|
|
804
|
+
pointer-events: none;
|
|
805
|
+
opacity: 0.5;
|
|
806
|
+
}
|
|
807
|
+
.ml-row { position: relative; }
|
|
808
|
+
|
|
791
809
|
/* S1 slice C — local action (move/flag/delete) queued but server hasn't
|
|
792
810
|
ACK'd. Reuses the same date-column dot as the download indicator so
|
|
793
811
|
"still on client, not yet on server" sits in the same visual slot as
|
package/client/styles/layout.css
CHANGED
|
@@ -44,15 +44,17 @@ body.calendar-sidebar-on {
|
|
|
44
44
|
.main-area { grid-area: main; }
|
|
45
45
|
.status-bar { grid-area: status; }
|
|
46
46
|
|
|
47
|
-
/* Vertical icon rail
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/* Vertical icon rail — Thunderbird Supernova style: dark background with
|
|
48
|
+
light icons so the rail reads as "chrome" and contrasts visibly against
|
|
49
|
+
the content area (whether the app's in light or dark theme). Always
|
|
50
|
+
visible on wide+medium tiers; hidden on narrow (icons fold into
|
|
51
|
+
hamburger — TBD). */
|
|
50
52
|
.icon-rail {
|
|
51
53
|
display: flex;
|
|
52
54
|
flex-direction: column;
|
|
53
55
|
justify-content: space-between;
|
|
54
|
-
background:
|
|
55
|
-
border-right: 1px solid
|
|
56
|
+
background: oklch(0.25 0.01 250);
|
|
57
|
+
border-right: 1px solid oklch(0.18 0.01 250);
|
|
56
58
|
padding: 6px 0;
|
|
57
59
|
overflow: hidden;
|
|
58
60
|
}
|
|
@@ -71,16 +73,21 @@ body.calendar-sidebar-on {
|
|
|
71
73
|
background: transparent;
|
|
72
74
|
cursor: pointer;
|
|
73
75
|
font-size: 16px;
|
|
74
|
-
color:
|
|
76
|
+
color: oklch(0.88 0.01 250);
|
|
75
77
|
border-left: 3px solid transparent;
|
|
76
|
-
transition: background 0.12s, border-color 0.12s;
|
|
78
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
77
79
|
}
|
|
78
80
|
.rail-btn:hover:not([disabled]) {
|
|
79
|
-
background:
|
|
81
|
+
background: oklch(0.32 0.02 250);
|
|
82
|
+
color: oklch(0.96 0.01 250);
|
|
80
83
|
}
|
|
81
84
|
.rail-btn[data-active="true"] {
|
|
82
|
-
background:
|
|
83
|
-
|
|
85
|
+
background: oklch(0.35 0.03 250);
|
|
86
|
+
color: oklch(0.98 0.01 250);
|
|
87
|
+
border-left-color: var(--color-accent, #4a9cff);
|
|
88
|
+
}
|
|
89
|
+
.rail-btn[disabled] {
|
|
90
|
+
color: oklch(0.55 0.01 250);
|
|
84
91
|
}
|
|
85
92
|
.rail-btn[disabled] {
|
|
86
93
|
opacity: 0.35;
|
package/package.json
CHANGED
|
@@ -564,7 +564,11 @@ export class ImapManager extends EventEmitter {
|
|
|
564
564
|
const hasToken = fs.existsSync(path.join(tokenDir, "oauth-token.json"));
|
|
565
565
|
const TOKEN_FETCH_TIMEOUT_MS = hasToken ? 30_000 : 120_000;
|
|
566
566
|
const authPromise = authenticateOAuth(credPath, {
|
|
567
|
-
|
|
567
|
+
// Scope set covers two-way sync of all mailx-managed local
|
|
568
|
+
// stores: mail (mail.google.com), contacts (full, not
|
|
569
|
+
// readonly — we write edits back), calendar (full), tasks
|
|
570
|
+
// (full), drive (for shared accounts.jsonc).
|
|
571
|
+
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/drive",
|
|
568
572
|
tokenDirectory: tokenDir,
|
|
569
573
|
credentialsKey: "installed",
|
|
570
574
|
loginHint: account.imap.user,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Calendar / Tasks / People (Contacts) two-way sync helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by MailxService to push local edits to Google and pull server
|
|
5
|
+
* changes into the local cache. All functions take a `getToken` function
|
|
6
|
+
* and a fetch implementation so they stay platform-agnostic (Node uses
|
|
7
|
+
* global `fetch` on Node 18+; browsers use window.fetch).
|
|
8
|
+
*
|
|
9
|
+
* Error handling: throws on network / HTTP errors. Caller catches and
|
|
10
|
+
* either retries via the store_sync drainer or surfaces to the UI.
|
|
11
|
+
*/
|
|
12
|
+
type TokenProvider = () => Promise<string>;
|
|
13
|
+
export interface GCalEvent {
|
|
14
|
+
id: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
start: {
|
|
17
|
+
dateTime?: string;
|
|
18
|
+
date?: string;
|
|
19
|
+
};
|
|
20
|
+
end: {
|
|
21
|
+
dateTime?: string;
|
|
22
|
+
date?: string;
|
|
23
|
+
};
|
|
24
|
+
location?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
etag?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function listCalendarEvents(tokenProvider: TokenProvider, fromMs: number, toMs: number, calendarId?: string): Promise<GCalEvent[]>;
|
|
29
|
+
export declare function createCalendarEvent(tokenProvider: TokenProvider, event: any, calendarId?: string): Promise<GCalEvent>;
|
|
30
|
+
export declare function updateCalendarEvent(tokenProvider: TokenProvider, eventId: string, event: any, calendarId?: string): Promise<GCalEvent>;
|
|
31
|
+
export declare function deleteCalendarEvent(tokenProvider: TokenProvider, eventId: string, calendarId?: string): Promise<void>;
|
|
32
|
+
export interface GTask {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
notes?: string;
|
|
36
|
+
due?: string;
|
|
37
|
+
completed?: string;
|
|
38
|
+
status?: "needsAction" | "completed";
|
|
39
|
+
etag?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function listTasks(tokenProvider: TokenProvider, listId?: string, showCompleted?: boolean): Promise<GTask[]>;
|
|
42
|
+
export declare function createTask(tokenProvider: TokenProvider, task: any, listId?: string): Promise<GTask>;
|
|
43
|
+
export declare function updateTask(tokenProvider: TokenProvider, taskId: string, task: any, listId?: string): Promise<GTask>;
|
|
44
|
+
export declare function deleteTask(tokenProvider: TokenProvider, taskId: string, listId?: string): Promise<void>;
|
|
45
|
+
export declare function createContact(tokenProvider: TokenProvider, person: any): Promise<any>;
|
|
46
|
+
export declare function updateContact(tokenProvider: TokenProvider, resourceName: string, updatePersonFields: string, person: any): Promise<any>;
|
|
47
|
+
export declare function deleteContact(tokenProvider: TokenProvider, resourceName: string): Promise<void>;
|
|
48
|
+
export declare function calendarEventToLocal(ev: GCalEvent, accountId: string): {
|
|
49
|
+
providerId: string;
|
|
50
|
+
accountId: string;
|
|
51
|
+
title: string;
|
|
52
|
+
startMs: number;
|
|
53
|
+
endMs: number;
|
|
54
|
+
allDay: boolean;
|
|
55
|
+
location: string;
|
|
56
|
+
notes: string;
|
|
57
|
+
etag: string;
|
|
58
|
+
};
|
|
59
|
+
export declare function localToCalendarEvent(local: {
|
|
60
|
+
title: string;
|
|
61
|
+
startMs: number;
|
|
62
|
+
endMs: number;
|
|
63
|
+
allDay?: boolean;
|
|
64
|
+
location?: string;
|
|
65
|
+
notes?: string;
|
|
66
|
+
}): any;
|
|
67
|
+
export declare function taskToLocal(t: GTask, accountId: string): {
|
|
68
|
+
providerId: string;
|
|
69
|
+
accountId: string;
|
|
70
|
+
title: string;
|
|
71
|
+
notes: string;
|
|
72
|
+
dueMs: number | undefined;
|
|
73
|
+
completedMs: number | undefined;
|
|
74
|
+
etag: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function localToTask(local: {
|
|
77
|
+
title: string;
|
|
78
|
+
notes?: string;
|
|
79
|
+
dueMs?: number;
|
|
80
|
+
completedMs?: number;
|
|
81
|
+
}): any;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=google-sync.d.ts.map
|