@bobfrankston/rmfmail 1.1.45 → 1.1.51
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/TODO.md +11 -0
- package/client/app.bundle.js +371 -80
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +167 -32
- package/client/app.js.map +1 -1
- package/client/app.ts +153 -32
- package/client/components/calendar-sidebar.js +221 -88
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +224 -83
- package/client/compose/compose.bundle.js +14 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/spellcheck.js +15 -0
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +14 -0
- package/client/help/search-help.js +75 -0
- package/client/help/search-help.js.map +1 -0
- package/client/help/search-help.ts +75 -0
- package/client/index.html +7 -7
- package/client/lib/api-client.js +5 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +5 -0
- package/client/lib/mailxapi.js +1 -0
- package/client/styles/components.css +204 -6
- package/docs/search.md +5 -1
- package/package.json +2 -3
- package/packages/mailx-service/google-sync.d.ts +3 -0
- package/packages/mailx-service/google-sync.d.ts.map +1 -1
- package/packages/mailx-service/google-sync.js +14 -2
- package/packages/mailx-service/google-sync.js.map +1 -1
- package/packages/mailx-service/google-sync.ts +17 -2
- package/packages/mailx-service/index.d.ts +11 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +99 -127
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +91 -122
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +4 -1
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +4 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +8 -3
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +8 -3
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-28848 → node_modules.npmglobalize-stash-40840}/.package-lock.json +0 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
getPrimaryAccount,
|
|
19
|
-
getCalendarEvents, createCalendarEvent,
|
|
19
|
+
getCalendarEvents, getCalendars, createCalendarEvent,
|
|
20
20
|
getTasks, createTask, updateTask, deleteTask,
|
|
21
21
|
reauthGoogleScopes,
|
|
22
22
|
getSettings, saveSettings,
|
|
@@ -61,6 +61,140 @@ let viewYear = new Date().getFullYear();
|
|
|
61
61
|
let viewMonth = new Date().getMonth();
|
|
62
62
|
let viewDay = new Date().getDate();
|
|
63
63
|
let lastEvents: CalEvent[] = [];
|
|
64
|
+
/** Overdue tasks (due before today, not completed) — pinned to the top of
|
|
65
|
+
* the calendar list. Cached so renderEvents() can be re-run on a calendar
|
|
66
|
+
* visibility toggle without re-fetching tasks. */
|
|
67
|
+
let lastOverdueTasks: Array<{ uuid: string; title: string; dueMs?: number }> = [];
|
|
68
|
+
|
|
69
|
+
// ── Per-calendar metadata ─────────────────────────────────────────────
|
|
70
|
+
// The user curates which calendars exist by selecting them in Google
|
|
71
|
+
// Calendar; mailx enumerates them via getCalendars() and renders one
|
|
72
|
+
// checkbox + icon per calendar. Holiday calendars are no longer hard-coded.
|
|
73
|
+
|
|
74
|
+
type CalKind = "personal" | "usHoliday" | "jewishHoliday" | "birthday" | "otherHoliday" | "other";
|
|
75
|
+
|
|
76
|
+
interface CalInfo {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
color: string;
|
|
80
|
+
primary: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Selected Google calendars, by id. The primary calendar is also keyed
|
|
84
|
+
* under the literal "primary" so legacy/stale rows (whose `calendar_id`
|
|
85
|
+
* predates per-calendar tagging) still resolve. */
|
|
86
|
+
const calById = new Map<string, CalInfo>();
|
|
87
|
+
let calendarList: CalInfo[] = [];
|
|
88
|
+
/** Calendar IDs the user has hidden in mailx (still selected in Google —
|
|
89
|
+
* hidden is a display filter, not a fetch filter). Persisted in
|
|
90
|
+
* settings.calendar.hiddenCalendars. */
|
|
91
|
+
let hiddenCalendars = new Set<string>();
|
|
92
|
+
|
|
93
|
+
/** Classify a calendar by its Google id. Birthdays live on Google's
|
|
94
|
+
* auto `addressbook#contacts` calendar; holiday calendars carry
|
|
95
|
+
* `#holiday@group.v.calendar.google.com`. The personal calendar is the
|
|
96
|
+
* `primary` one. Everything else is a generic named calendar. */
|
|
97
|
+
function calendarKind(id: string, primary: boolean): CalKind {
|
|
98
|
+
if (primary) return "personal";
|
|
99
|
+
const c = (id || "").toLowerCase();
|
|
100
|
+
if (c.includes("addressbook#contacts")) return "birthday";
|
|
101
|
+
if (c.includes("#holiday@") || c.includes("holiday@group")) {
|
|
102
|
+
if (c.includes("usa")) return "usHoliday";
|
|
103
|
+
if (c.includes("judaism") || c.includes("jewish")) return "jewishHoliday";
|
|
104
|
+
return "otherHoliday";
|
|
105
|
+
}
|
|
106
|
+
return "other";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Icon markup for a calendar: blue dot = personal, themed emoji for
|
|
110
|
+
* holiday/birthday calendars, monogram badge (first letter in a circle
|
|
111
|
+
* tinted with the Google calendar color) for any other named calendar. */
|
|
112
|
+
function calIconHtml(info: CalInfo): string {
|
|
113
|
+
const kind = calendarKind(info.id, info.primary);
|
|
114
|
+
const t = escapeHtml(info.name);
|
|
115
|
+
if (kind === "personal") return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
|
|
116
|
+
if (kind === "usHoliday") return `<span class="cal-ico cal-ico-emoji" title="${t}">🇺🇸</span>`;
|
|
117
|
+
if (kind === "jewishHoliday") return `<span class="cal-ico cal-ico-emoji" title="${t}">✡️</span>`;
|
|
118
|
+
if (kind === "birthday") return `<span class="cal-ico cal-ico-emoji" title="${t}">🎂</span>`;
|
|
119
|
+
if (kind === "otherHoliday") return `<span class="cal-ico cal-ico-emoji" title="${t}">✦</span>`;
|
|
120
|
+
const letter = escapeHtml((info.name.trim()[0] || "?").toUpperCase());
|
|
121
|
+
const color = info.color || "#7a7a7a";
|
|
122
|
+
return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml(color)}" title="${t}">${letter}</span>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Resolve an event's `calendarId` to a CalInfo. Falls back to a
|
|
126
|
+
* synthesised entry (pattern-based kind, generic monogram) when the id
|
|
127
|
+
* isn't in the enumerated list — e.g. a stale row, or before
|
|
128
|
+
* getCalendars() has resolved. */
|
|
129
|
+
function calInfoFor(calendarId: string | undefined): CalInfo {
|
|
130
|
+
const id = calendarId || "primary";
|
|
131
|
+
const found = calById.get(id);
|
|
132
|
+
if (found) return found;
|
|
133
|
+
return { id, name: (id.split("@")[0] || id), color: "", primary: id === "primary" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isCalHidden(calendarId: string | undefined): boolean {
|
|
137
|
+
return hiddenCalendars.has(calInfoFor(calendarId).id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadHiddenCalendars(): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const s: any = await getSettings();
|
|
143
|
+
const arr = s?.calendar?.hiddenCalendars;
|
|
144
|
+
hiddenCalendars = new Set(Array.isArray(arr) ? arr : []);
|
|
145
|
+
} catch { hiddenCalendars = new Set(); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function saveHiddenCalendars(): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
const s: any = await getSettings();
|
|
151
|
+
s.calendar = { ...(s.calendar || {}), hiddenCalendars: [...hiddenCalendars] };
|
|
152
|
+
await saveSettings(s);
|
|
153
|
+
} catch (e: any) { console.error("[cal] save hiddenCalendars failed:", e); }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Pull the user's selected Google calendars and render the per-calendar
|
|
157
|
+
* checkbox list. Each row toggles `hiddenCalendars` (a display filter —
|
|
158
|
+
* instant, no Google round-trip, no re-fetch). Called on init and on
|
|
159
|
+
* manual refresh; cheap enough but not run on every nav click. */
|
|
160
|
+
async function renderCalendarList(): Promise<void> {
|
|
161
|
+
const host = document.getElementById("cal-side-calendars");
|
|
162
|
+
let list: CalInfo[] = [];
|
|
163
|
+
try {
|
|
164
|
+
list = await getCalendars() as CalInfo[];
|
|
165
|
+
} catch { /* offline / no calendar scope — leave the list empty */ }
|
|
166
|
+
calendarList = list;
|
|
167
|
+
calById.clear();
|
|
168
|
+
for (const c of list) {
|
|
169
|
+
calById.set(c.id, c);
|
|
170
|
+
if (c.primary) calById.set("primary", c);
|
|
171
|
+
}
|
|
172
|
+
if (host) {
|
|
173
|
+
if (list.length === 0) {
|
|
174
|
+
host.innerHTML = "";
|
|
175
|
+
} else {
|
|
176
|
+
// Personal first, then alphabetical.
|
|
177
|
+
const sorted = [...list].sort((a, b) =>
|
|
178
|
+
(a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
|
|
179
|
+
host.innerHTML = sorted.map(c => `<label class="cal-side-cal-row" title="${escapeHtml(c.name)}">
|
|
180
|
+
<input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
|
|
181
|
+
${calIconHtml(c)}
|
|
182
|
+
<span class="cal-side-cal-name">${escapeHtml(c.name)}</span>
|
|
183
|
+
</label>`).join("");
|
|
184
|
+
host.querySelectorAll<HTMLInputElement>(".cal-side-cal-check").forEach(cb => {
|
|
185
|
+
cb.addEventListener("change", async () => {
|
|
186
|
+
const id = cb.dataset.calId || "";
|
|
187
|
+
if (cb.checked) hiddenCalendars.delete(id);
|
|
188
|
+
else hiddenCalendars.add(id);
|
|
189
|
+
await saveHiddenCalendars();
|
|
190
|
+
renderEvents(lastEvents); // re-filter instantly
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Repaint events so rows pick up proper icons now the list is known.
|
|
196
|
+
if (lastEvents.length > 0) renderEvents(lastEvents);
|
|
197
|
+
}
|
|
64
198
|
|
|
65
199
|
// Set of selected task UUIDs. Persists across renderTasks() calls so the
|
|
66
200
|
// user keeps their selection when a re-render happens (e.g., after a
|
|
@@ -151,10 +285,28 @@ function renderHead(): void {
|
|
|
151
285
|
dateEl.innerHTML = `<strong>${d.getDate()}</strong> ${d.toLocaleDateString(undefined, { weekday: "short" })} <span class="cal-side-date-month">${d.toLocaleDateString(undefined, { month: "short", year: "numeric" })}</span>`;
|
|
152
286
|
}
|
|
153
287
|
|
|
288
|
+
/** Markup for one overdue-task row (pinned at the top of the calendar). */
|
|
289
|
+
function overdueTaskRowHtml(t: { uuid: string; title: string; dueMs?: number }): string {
|
|
290
|
+
let dueLabel = "";
|
|
291
|
+
if (t.dueMs) {
|
|
292
|
+
const d = new Date(t.dueMs);
|
|
293
|
+
const sameYear = d.getFullYear() === new Date().getFullYear();
|
|
294
|
+
dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
|
|
295
|
+
}
|
|
296
|
+
return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml(t.uuid)}">
|
|
297
|
+
<input type="checkbox" class="cal-side-overdue-check" title="Mark done">
|
|
298
|
+
<span class="cal-side-task-title">${escapeHtml(t.title)}</span>
|
|
299
|
+
<span class="cal-side-task-due overdue">${escapeHtml(dueLabel)}</span>
|
|
300
|
+
</div>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
154
303
|
function renderEvents(events: CalEvent[]): void {
|
|
155
304
|
const body = document.getElementById("cal-side-body");
|
|
156
305
|
if (!body) return;
|
|
157
|
-
|
|
306
|
+
// Drop events from calendars the user has hidden in mailx.
|
|
307
|
+
events = events.filter(e => !isCalHidden(e.calendarId));
|
|
308
|
+
const overdue = lastOverdueTasks;
|
|
309
|
+
if (events.length === 0 && overdue.length === 0) {
|
|
158
310
|
body.innerHTML = `<div class="cal-side-empty">No upcoming events. Click + New event to add one.</div>`;
|
|
159
311
|
return;
|
|
160
312
|
}
|
|
@@ -196,12 +348,21 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
196
348
|
}
|
|
197
349
|
|
|
198
350
|
let html = "";
|
|
351
|
+
|
|
352
|
+
// Overdue tasks pinned to the very top — a persistent "behind on these"
|
|
353
|
+
// block. Marking one done (the checkbox) completes it in Google and it
|
|
354
|
+
// drops off on the next render.
|
|
355
|
+
if (overdue.length > 0) {
|
|
356
|
+
html += `<div class="cal-side-day cal-side-day-overdue">Overdue tasks</div>`;
|
|
357
|
+
for (const t of overdue) html += overdueTaskRowHtml(t);
|
|
358
|
+
}
|
|
359
|
+
|
|
199
360
|
if (dailyHeads.length > 0) {
|
|
200
361
|
html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
|
|
201
362
|
for (const e of dailyHeads) {
|
|
202
363
|
const link = e.htmlLink || "";
|
|
203
364
|
html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
|
|
204
|
-
|
|
365
|
+
${calIconHtml(calInfoFor(e.calendarId))}
|
|
205
366
|
<span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
|
|
206
367
|
<span class="cal-side-event-title">${escapeHtml(e.title)}<span class="cal-side-event-recur" title="Daily">↻</span></span>
|
|
207
368
|
</div>`;
|
|
@@ -214,10 +375,13 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
214
375
|
for (const e of events) {
|
|
215
376
|
// Filter out daily-grouped instances — they're shown once at top.
|
|
216
377
|
if (e.recurringEventId && dailyKeys.has(e.recurringEventId)) continue;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
378
|
+
const info = calInfoFor(e.calendarId);
|
|
379
|
+
const kind = calendarKind(info.id, info.primary);
|
|
380
|
+
const isHolidayKind = kind === "usHoliday" || kind === "jewishHoliday" || kind === "otherHoliday";
|
|
381
|
+
// Holiday calendars get the 2-week cap — they otherwise clutter a
|
|
382
|
+
// long horizon. Birthdays and recurring events follow the user's
|
|
383
|
+
// overall horizon.
|
|
384
|
+
if (isHolidayKind && e.start > holidayCutoff) continue;
|
|
221
385
|
const d = new Date(e.start);
|
|
222
386
|
const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
223
387
|
if (dayKey !== lastDayKey) {
|
|
@@ -226,44 +390,19 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
226
390
|
}
|
|
227
391
|
const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">↻</span>` : "";
|
|
228
392
|
const link = e.htmlLink || "";
|
|
229
|
-
// Distinguish holiday sources for coloring: US (en.usa) → light
|
|
230
|
-
// green; Jewish (en.jewish) → light blue. Anything else falls
|
|
231
|
-
// through to a generic "h" class so a future calendar still gets
|
|
232
|
-
// marked-as-holiday styling even if it isn't specifically themed.
|
|
233
|
-
let holidayKind: string | undefined;
|
|
234
|
-
if (e.isHoliday) {
|
|
235
|
-
const cid = (e.calendarId || "").toLowerCase();
|
|
236
|
-
if (cid.includes("usa")) holidayKind = "us";
|
|
237
|
-
// Match both `en.judaism` (the actually-clean Jewish-only
|
|
238
|
-
// calendar) and `en.jewish` (mis-curated by Google, retained
|
|
239
|
-
// here so any leftover rows from the prior calendar ID still
|
|
240
|
-
// render with the right symbol until reconcile evicts them).
|
|
241
|
-
else if (cid.includes("judaism") || cid.includes("jewish")) holidayKind = "jewish";
|
|
242
|
-
else holidayKind = "other";
|
|
243
|
-
}
|
|
244
|
-
const holidayAttr = e.isHoliday ? ` data-holiday="1" data-holiday-kind="${holidayKind}"` : "";
|
|
245
393
|
const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// typically no useful clock time). Regular events keep the
|
|
254
|
-
// existing dot/time/title columns. A leading symbol identifies
|
|
255
|
-
// the source at a glance: 🇺🇸 for US (federal-secular), Star of
|
|
256
|
-
// David for Jewish, generic ✦ otherwise.
|
|
257
|
-
if (e.isHoliday) {
|
|
258
|
-
const symbol = holidayKind === "us" ? "🇺🇸"
|
|
259
|
-
: holidayKind === "jewish" ? "✡️"
|
|
260
|
-
: "✦";
|
|
261
|
-
html += `<div class="cal-side-event"${holidayAttr} data-id="${e.id}">
|
|
262
|
-
<span class="cal-side-event-title cal-side-event-holiday-title"><span class="cal-side-event-holiday-symbol">${symbol}</span> ${escapeHtml(e.title)}</span>
|
|
394
|
+
// Holiday + birthday calendars render as a single full-width row
|
|
395
|
+
// (all-day, no useful clock time) led by the calendar's icon.
|
|
396
|
+
// Regular events keep the icon/time/title columns and the
|
|
397
|
+
// click-to-open-in-Google deep link.
|
|
398
|
+
if (isHolidayKind || kind === "birthday") {
|
|
399
|
+
html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
|
|
400
|
+
<span class="cal-side-event-title cal-side-event-holiday-title">${calIconHtml(info)} ${escapeHtml(e.title)}</span>
|
|
263
401
|
</div>`;
|
|
264
402
|
} else {
|
|
265
|
-
|
|
266
|
-
|
|
403
|
+
const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
|
|
404
|
+
html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml(link)}" ${titleAttr}>
|
|
405
|
+
${calIconHtml(info)}
|
|
267
406
|
<span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
|
|
268
407
|
<span class="cal-side-event-title">${escapeHtml(e.title)}${recurMark}</span>
|
|
269
408
|
</div>`;
|
|
@@ -271,6 +410,18 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
271
410
|
}
|
|
272
411
|
body.innerHTML = html;
|
|
273
412
|
|
|
413
|
+
// Overdue-task checkboxes — mark the task completed in Google (non-
|
|
414
|
+
// destructive; it survives in the Completed section) and re-render.
|
|
415
|
+
body.querySelectorAll<HTMLElement>(".cal-side-task-overdue").forEach(row => {
|
|
416
|
+
const uuid = row.dataset.uuid!;
|
|
417
|
+
row.querySelector<HTMLInputElement>(".cal-side-overdue-check")?.addEventListener("change", async () => {
|
|
418
|
+
await updateTask(uuid, { completedMs: Date.now() });
|
|
419
|
+
lastOverdueTasks = lastOverdueTasks.filter(t => t.uuid !== uuid);
|
|
420
|
+
renderEvents(lastEvents);
|
|
421
|
+
renderTasks();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
274
425
|
// Click-to-open — interim per user 2026-04-23: route to Google Calendar's
|
|
275
426
|
// web UI via openExternal until we build an in-app event editor.
|
|
276
427
|
// Right-click gives a context menu with "View in browser" explicit.
|
|
@@ -309,10 +460,12 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
309
460
|
});
|
|
310
461
|
}
|
|
311
462
|
|
|
312
|
-
async function renderTasks(): Promise<void> {
|
|
463
|
+
async function renderTasks(prefetched?: any[]): Promise<void> {
|
|
313
464
|
const cb = document.getElementById("cal-side-show-done") as HTMLInputElement | null;
|
|
314
465
|
const showDone = cb?.checked ?? false;
|
|
315
|
-
|
|
466
|
+
// Reuse the list refresh() already fetched (avoids a duplicate getTasks
|
|
467
|
+
// IPC + Google pull); fall back to fetching when called standalone.
|
|
468
|
+
const tasks = prefetched ?? await getTasks(showDone);
|
|
316
469
|
const host = document.getElementById("cal-side-tasks");
|
|
317
470
|
if (!host) return;
|
|
318
471
|
if (tasks.length === 0) {
|
|
@@ -406,14 +559,30 @@ async function refresh(): Promise<void> {
|
|
|
406
559
|
// sidebar stuck on its initial "Loading…" placeholder — that was the
|
|
407
560
|
// "calendar shows nothing" symptom. Render an explicit state either
|
|
408
561
|
// way: events on success, an error line on failure.
|
|
562
|
+
// Tasks are fetched ONCE here and handed to renderTasks() so the events
|
|
563
|
+
// pane and the task pane don't each fire their own getTasks() — that
|
|
564
|
+
// doubled the Google Tasks API call rate and helped trip the 429 storm.
|
|
565
|
+
let prefetchedTasks: any[] | undefined;
|
|
409
566
|
try {
|
|
410
|
-
|
|
567
|
+
const startOfToday = new Date(); startOfToday.setHours(0, 0, 0, 0);
|
|
568
|
+
const showDone = (document.getElementById("cal-side-show-done") as HTMLInputElement | null)?.checked ?? false;
|
|
569
|
+
const [events, tasks] = await Promise.all([
|
|
570
|
+
fetchUpcoming(from),
|
|
571
|
+
getTasks(showDone).catch(() => [] as any[]),
|
|
572
|
+
]);
|
|
573
|
+
lastEvents = events;
|
|
574
|
+
prefetchedTasks = tasks as any[];
|
|
575
|
+
// Overdue tasks (due before today, not completed) pin to the top of
|
|
576
|
+
// the calendar list.
|
|
577
|
+
lastOverdueTasks = prefetchedTasks
|
|
578
|
+
.filter(t => t.dueMs && !t.completedMs && t.dueMs < startOfToday.getTime())
|
|
579
|
+
.map(t => ({ uuid: t.uuid, title: t.title, dueMs: t.dueMs }));
|
|
411
580
|
renderEvents(lastEvents);
|
|
412
581
|
} catch (e: any) {
|
|
413
582
|
const body = document.getElementById("cal-side-body");
|
|
414
583
|
if (body) body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml(e?.message || String(e))}</div>`;
|
|
415
584
|
}
|
|
416
|
-
renderTasks();
|
|
585
|
+
renderTasks(prefetchedTasks);
|
|
417
586
|
}
|
|
418
587
|
|
|
419
588
|
/** Open the natural-language new-event modal. The user types a free-form
|
|
@@ -562,6 +731,8 @@ export async function showCalendarSidebar(): Promise<void> {
|
|
|
562
731
|
el.hidden = false;
|
|
563
732
|
document.body.classList.add("calendar-sidebar-on");
|
|
564
733
|
try { localStorage.setItem(SIDEBAR_PREF, "true"); } catch { /* */ }
|
|
734
|
+
await loadHiddenCalendars();
|
|
735
|
+
void renderCalendarList(); // async — repaints events when it resolves
|
|
565
736
|
await refresh();
|
|
566
737
|
}
|
|
567
738
|
|
|
@@ -631,7 +802,9 @@ export function initCalendarSidebar(): void {
|
|
|
631
802
|
btn?.classList.add("cal-side-refreshing");
|
|
632
803
|
if (btn) btn.disabled = true;
|
|
633
804
|
try {
|
|
634
|
-
|
|
805
|
+
// Also re-enumerate calendars — the user may have selected /
|
|
806
|
+
// deselected one in Google since the sidebar opened.
|
|
807
|
+
await Promise.all([renderCalendarList(), refresh()]);
|
|
635
808
|
} finally {
|
|
636
809
|
setTimeout(() => {
|
|
637
810
|
btn?.classList.remove("cal-side-refreshing");
|
|
@@ -660,42 +833,10 @@ export function initCalendarSidebar(): void {
|
|
|
660
833
|
refresh();
|
|
661
834
|
});
|
|
662
835
|
}
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// and Jewish holidays. Generalization to arbitrary user-supplied
|
|
668
|
-
// calendars is filed as Q140 (low priority unless the audience
|
|
669
|
-
// broadens beyond Bob).
|
|
670
|
-
const wireHolidayCheckbox = (
|
|
671
|
-
cbId: string,
|
|
672
|
-
settingsKey: "showHolidays" | "showJewishHolidays",
|
|
673
|
-
): void => {
|
|
674
|
-
const cb = document.getElementById(cbId) as HTMLInputElement | null;
|
|
675
|
-
if (!cb || (cb as any).__wired) return;
|
|
676
|
-
(cb as any).__wired = true;
|
|
677
|
-
// Race guard: the async getSettings() promise can resolve AFTER the
|
|
678
|
-
// user clicks the checkbox, overwriting their input with the prior
|
|
679
|
-
// value. If the user touched the checkbox while the initial load
|
|
680
|
-
// was in flight, drop the late initial value on the floor — the
|
|
681
|
-
// change handler has already taken control.
|
|
682
|
-
let userTouched = false;
|
|
683
|
-
getSettings().then((s: any) => {
|
|
684
|
-
if (userTouched) return;
|
|
685
|
-
cb.checked = !!s?.calendar?.[settingsKey];
|
|
686
|
-
}).catch(() => { /* */ });
|
|
687
|
-
cb.addEventListener("change", async () => {
|
|
688
|
-
userTouched = true;
|
|
689
|
-
try {
|
|
690
|
-
const s = await getSettings();
|
|
691
|
-
s.calendar = { ...(s.calendar || {}), [settingsKey]: cb.checked };
|
|
692
|
-
await saveSettings(s);
|
|
693
|
-
} catch (e: any) { console.error(`[cal] ${settingsKey} save failed:`, e); }
|
|
694
|
-
refresh();
|
|
695
|
-
});
|
|
696
|
-
};
|
|
697
|
-
wireHolidayCheckbox("cal-side-show-holidays", "showHolidays");
|
|
698
|
-
wireHolidayCheckbox("cal-side-show-jewish-holidays", "showJewishHolidays");
|
|
836
|
+
// Per-calendar checkboxes are generated dynamically from the user's
|
|
837
|
+
// selected Google calendars — see renderCalendarList(), driven from
|
|
838
|
+
// showCalendarSidebar() + the manual refresh button. No hard-coded
|
|
839
|
+
// holiday toggles any more.
|
|
699
840
|
// Horizon input — how many days ahead to list events. Bounded 1..365.
|
|
700
841
|
const horizonInput = document.getElementById("cal-side-horizon") as HTMLInputElement | null;
|
|
701
842
|
if (horizonInput && !(horizonInput as any).__wired) {
|
|
@@ -65,6 +65,7 @@ __export(api_client_exports, {
|
|
|
65
65
|
getAttachment: () => getAttachment,
|
|
66
66
|
getAutocompleteSettings: () => getAutocompleteSettings,
|
|
67
67
|
getCalendarEvents: () => getCalendarEvents,
|
|
68
|
+
getCalendars: () => getCalendars,
|
|
68
69
|
getDeviceAccounts: () => getDeviceAccounts,
|
|
69
70
|
getDiagnostics: () => getDiagnostics,
|
|
70
71
|
getFolders: () => getFolders,
|
|
@@ -244,6 +245,9 @@ function getPrimaryAccount(feature) {
|
|
|
244
245
|
function getCalendarEvents(fromMs, toMs) {
|
|
245
246
|
return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
|
|
246
247
|
}
|
|
248
|
+
function getCalendars() {
|
|
249
|
+
return ipc().getCalendars?.() ?? Promise.resolve([]);
|
|
250
|
+
}
|
|
247
251
|
function createCalendarEvent(ev) {
|
|
248
252
|
return ipc().createCalendarEvent?.(ev);
|
|
249
253
|
}
|
|
@@ -1898,15 +1902,25 @@ function decorate(editor2, sp) {
|
|
|
1898
1902
|
const hits = [];
|
|
1899
1903
|
let n = walker.nextNode();
|
|
1900
1904
|
const WORD_RE = /[\p{L}][\p{L}'’\-]*/gu;
|
|
1905
|
+
const EMAIL_RE = /[^\s@<>()]+@[^\s@<>()]+\.[^\s@<>()]+/g;
|
|
1901
1906
|
while (n) {
|
|
1902
1907
|
const tn = n;
|
|
1903
1908
|
const text = tn.data;
|
|
1909
|
+
const emailRanges = [];
|
|
1910
|
+
EMAIL_RE.lastIndex = 0;
|
|
1911
|
+
let em;
|
|
1912
|
+
while ((em = EMAIL_RE.exec(text)) !== null) {
|
|
1913
|
+
emailRanges.push([em.index, em.index + em[0].length]);
|
|
1914
|
+
}
|
|
1904
1915
|
let m;
|
|
1905
1916
|
WORD_RE.lastIndex = 0;
|
|
1906
1917
|
while ((m = WORD_RE.exec(text)) !== null) {
|
|
1907
1918
|
const word = m[0];
|
|
1908
1919
|
if (word.length < MIN_WORD_LEN)
|
|
1909
1920
|
continue;
|
|
1921
|
+
const wStart = m.index, wEnd = m.index + word.length;
|
|
1922
|
+
if (emailRanges.some(([s, e]) => wStart < e && wEnd > s))
|
|
1923
|
+
continue;
|
|
1910
1924
|
if (caretNode === tn && caretOffset >= m.index && caretOffset <= m.index + word.length) {
|
|
1911
1925
|
continue;
|
|
1912
1926
|
}
|