@bobfrankston/rmfmail 1.2.316 → 1.2.317
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/.commitmsg +33 -28
- package/.llm/trusted-lists.md +5 -0
- package/client/app.bundle.js +117 -92
- package/client/app.bundle.js.map +2 -2
- package/client/components/calendar-sidebar.js +160 -133
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +146 -116
- package/client/index.html +6 -6
- package/npmchanges.md +35 -0
- package/package.json +5 -5
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
getPrimaryAccount,
|
|
19
|
-
getCalendarEvents, getCalendars, createCalendarEvent, refreshCalendarNow,
|
|
19
|
+
getCalendarEvents, getCalendars, createCalendarEvent, deleteCalendarEvent, refreshCalendarNow,
|
|
20
20
|
getTasks, createTask, updateTask, deleteTask,
|
|
21
21
|
reauthGoogleScopes,
|
|
22
22
|
getSettings, saveSettings,
|
|
23
23
|
} from "../lib/api-client.js";
|
|
24
|
-
import { showContextMenu, confirmAt } from "./context-menu.js";
|
|
24
|
+
import { showContextMenu, confirmAt, type MenuItem } from "./context-menu.js";
|
|
25
25
|
|
|
26
26
|
const SIDEBAR_PREF = "mailx-calendar-sidebar-on";
|
|
27
27
|
const SHOW_RECURRING_PREF = "mailx-cal-show-recurring";
|
|
@@ -60,6 +60,23 @@ const HOLIDAY_HORIZON_MS = 14 * 86400_000;
|
|
|
60
60
|
let viewYear = new Date().getFullYear();
|
|
61
61
|
let viewMonth = new Date().getMonth();
|
|
62
62
|
let viewDay = new Date().getDate();
|
|
63
|
+
/** True once the reader steps to another day with ‹ ›; "Today" clears it.
|
|
64
|
+
* While false the view FOLLOWS the calendar date: the app runs for days,
|
|
65
|
+
* and the date above was fixed when this module loaded (Bob 2026-09-10:
|
|
66
|
+
* "it is still showing me yesterday's calendar"). */
|
|
67
|
+
let viewPinned = false;
|
|
68
|
+
function followToday(): void {
|
|
69
|
+
if (viewPinned) return;
|
|
70
|
+
const t = new Date();
|
|
71
|
+
viewYear = t.getFullYear(); viewMonth = t.getMonth(); viewDay = t.getDate();
|
|
72
|
+
}
|
|
73
|
+
/** Re-render at the next local midnight, then re-arm. A setInterval would
|
|
74
|
+
* drift; computing the next midnight each time lands on it exactly. */
|
|
75
|
+
function armMidnightRollover(): void {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 5);
|
|
78
|
+
window.setTimeout(() => { void refresh(); armMidnightRollover(); }, next.getTime() - now.getTime());
|
|
79
|
+
}
|
|
63
80
|
let lastEvents: CalEvent[] = [];
|
|
64
81
|
/** Overdue tasks (due before today, not completed) — pinned to the top of
|
|
65
82
|
* the calendar list. Cached so renderEvents() can be re-run on a calendar
|
|
@@ -176,7 +193,7 @@ async function renderCalendarList(): Promise<void> {
|
|
|
176
193
|
// Personal first, then alphabetical.
|
|
177
194
|
const sorted = [...list].sort((a, b) =>
|
|
178
195
|
(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)}">
|
|
196
|
+
host.innerHTML = sorted.map(c => `<label class="cal-side-cal-row" title="${escapeHtml(c.name)} — untick to hide its events in this list. Which calendars are listed is set in Google Calendar: the ones selected there appear; use Refresh after changing it.">
|
|
180
197
|
<input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
|
|
181
198
|
${calIconHtml(c)}
|
|
182
199
|
<span class="cal-side-cal-name">${escapeHtml(c.name)}</span>
|
|
@@ -436,7 +453,9 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
436
453
|
<span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml(e.title)}">${calIconHtml(info)} ${escapeHtml(e.title)}</span>
|
|
437
454
|
</div>`;
|
|
438
455
|
} else {
|
|
439
|
-
const titleAttr = link
|
|
456
|
+
const titleAttr = link
|
|
457
|
+
? 'title="Click to open in Google Calendar to edit it. Right-click for more."'
|
|
458
|
+
: 'title="Saved in rmfmail, syncing to Google — it can be opened there once it has synced. Right-click to delete."';
|
|
440
459
|
html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml(link)}" ${titleAttr}>
|
|
441
460
|
${calIconHtml(info)}
|
|
442
461
|
<span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
|
|
@@ -482,16 +501,38 @@ function renderEvents(events: CalEvent[]): void {
|
|
|
482
501
|
el.addEventListener("contextmenu", (e) => {
|
|
483
502
|
e.preventDefault();
|
|
484
503
|
const link = el.dataset.link;
|
|
485
|
-
const
|
|
504
|
+
const uuid = el.dataset.id || "";
|
|
505
|
+
const title = el.querySelector(".cal-side-event-title")?.textContent?.trim() || "this event";
|
|
506
|
+
const items: MenuItem[] = [];
|
|
486
507
|
if (link) items.push({
|
|
487
|
-
label: "
|
|
508
|
+
label: `Edit "${title}" in Google Calendar`,
|
|
488
509
|
action: () => openInBrowser(link),
|
|
489
510
|
});
|
|
490
511
|
items.push({
|
|
491
512
|
label: "Open Google Calendar",
|
|
492
513
|
action: () => openInBrowser("https://calendar.google.com/"),
|
|
493
514
|
});
|
|
494
|
-
if (
|
|
515
|
+
if (uuid) {
|
|
516
|
+
items.push({ label: "", action: () => {}, separator: true });
|
|
517
|
+
items.push({
|
|
518
|
+
label: `Delete "${title}"`,
|
|
519
|
+
tooltip: "Removes it from your calendar in rmfmail now and from Google when it syncs.",
|
|
520
|
+
action: async () => {
|
|
521
|
+
if (!await confirmAt(e.clientX, e.clientY, `Delete "${title}"`)) return;
|
|
522
|
+
try {
|
|
523
|
+
await deleteCalendarEvent(uuid);
|
|
524
|
+
const st = document.getElementById("status-sync");
|
|
525
|
+
if (st) st.textContent = `Deleted event: ${title}`;
|
|
526
|
+
await refresh();
|
|
527
|
+
} catch (err: any) {
|
|
528
|
+
const st = document.getElementById("status-sync");
|
|
529
|
+
if (st) st.textContent = `Couldn't delete "${title}": ${err?.message || err}`;
|
|
530
|
+
console.error("[calendar] delete failed:", err);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
|
|
495
536
|
});
|
|
496
537
|
});
|
|
497
538
|
}
|
|
@@ -595,6 +636,7 @@ async function renderTasks(prefetched?: any[]): Promise<void> {
|
|
|
595
636
|
}
|
|
596
637
|
|
|
597
638
|
async function refresh(): Promise<void> {
|
|
639
|
+
followToday();
|
|
598
640
|
renderHead();
|
|
599
641
|
const from = new Date(viewYear, viewMonth, viewDay);
|
|
600
642
|
// A throw here (IPC reject, daemon not ready) must NOT leave the
|
|
@@ -630,12 +672,15 @@ async function refresh(): Promise<void> {
|
|
|
630
672
|
/** Open the natural-language new-event modal. The user types a free-form
|
|
631
673
|
* description ("Lunch with John tomorrow at noon at Joe's"); we send it
|
|
632
674
|
* to aiTransform with action=extractEvent, get back structured fields
|
|
633
|
-
* (title/start/end/location/notes), and
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
675
|
+
* (title/start/end/location/notes), and SAVE them — a local row at once,
|
|
676
|
+
* pushed to Google by the store-sync queue — exactly as the sidebar's
|
|
677
|
+
* task editor does. Until 2026-09-10 this opened Google Calendar's
|
|
678
|
+
* create page in the browser instead, and the reader had to save it
|
|
679
|
+
* there, in whatever Google account the browser was signed into (Bob:
|
|
680
|
+
* "add an event puts me into Google Calendar. It should just save the
|
|
681
|
+
* event because I can easily edit it later"). If AI is disabled or the
|
|
682
|
+
* parse fails, the dialog says so and offers "Create as text": the raw
|
|
683
|
+
* text becomes the title of an all-day event today, to be edited. */
|
|
639
684
|
function openNewEventDialog(): void {
|
|
640
685
|
const backdrop = document.createElement("div");
|
|
641
686
|
backdrop.className = "mailx-modal-backdrop";
|
|
@@ -650,7 +695,7 @@ function openNewEventDialog(): void {
|
|
|
650
695
|
|
|
651
696
|
const hint = document.createElement("div");
|
|
652
697
|
hint.style.cssText = "font-size:0.9em;color:var(--color-text-muted);";
|
|
653
|
-
hint.textContent = "Describe the event in plain English — AI extracts the details and
|
|
698
|
+
hint.textContent = "Describe the event in plain English — AI extracts the details and saves it to your calendar. Edit it afterwards if it needs adjusting.";
|
|
654
699
|
|
|
655
700
|
const ta = document.createElement("textarea");
|
|
656
701
|
ta.style.cssText = "width:100%;min-height:120px;font:inherit;padding:8px;border-radius:4px;border:1px solid var(--color-border, #ccc);box-sizing:border-box;resize:vertical;";
|
|
@@ -727,117 +772,80 @@ function openNewEventDialog(): void {
|
|
|
727
772
|
}
|
|
728
773
|
}
|
|
729
774
|
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
const api = (window as any).mailxapi;
|
|
775
|
+
// Save each extracted event (the text may describe several) as a
|
|
776
|
+
// local row; the store-sync queue pushes it to Google and binds the
|
|
777
|
+
// provider id when that lands. Local-first: the row is in the
|
|
778
|
+
// sidebar before Google has heard of it. The saved title is named
|
|
779
|
+
// in the status line, never "saved" alone.
|
|
736
780
|
const targets = events.length ? events : [null];
|
|
737
|
-
|
|
738
|
-
// not be the one mailx's calendar reads (Bob 2026-09-10: "I just used
|
|
739
|
-
// the add event but don't see them on Google calendar"). authuser=
|
|
740
|
-
// names the account, so the event lands where the sidebar looks.
|
|
741
|
-
let authuser = "";
|
|
742
|
-
try { authuser = String((await getPrimaryAccount("calendar"))?.email || ""); }
|
|
743
|
-
catch (e: any) { console.warn("[calendar] primary account lookup failed, opening without authuser:", e?.message || e); }
|
|
781
|
+
const saved: string[] = [];
|
|
744
782
|
for (const ev of targets) {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
783
|
+
const row = eventToLocalRow(ev, text);
|
|
784
|
+
try {
|
|
785
|
+
await createCalendarEvent(row);
|
|
786
|
+
saved.push(row.title);
|
|
787
|
+
} catch (e: any) {
|
|
788
|
+
// Stop at the first failure and leave the dialog open with the
|
|
789
|
+
// reason: a silent partial save is worse than none.
|
|
790
|
+
status.textContent = `Couldn't save "${row.title}": ${e?.message || e}`;
|
|
791
|
+
createBtn.disabled = false;
|
|
792
|
+
cancelBtn.disabled = false;
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
748
795
|
}
|
|
749
|
-
armPostCreateRefresh();
|
|
750
796
|
close();
|
|
797
|
+
const st = document.getElementById("status-sync");
|
|
798
|
+
if (st) st.textContent = saved.length === 1 ? `Saved event: ${saved[0]}` : `Saved ${saved.length} events: ${saved.join("; ")}`;
|
|
799
|
+
await refresh();
|
|
751
800
|
});
|
|
752
801
|
}
|
|
753
802
|
|
|
754
|
-
/**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
let
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (!r?.ok) console.log(`[calendar] post-create refresh (${why}) not started: ${r?.reason || "unknown"}`);
|
|
772
|
-
} catch (e: any) {
|
|
773
|
-
console.warn(`[calendar] post-create refresh (${why}) failed:`, e?.message || e);
|
|
774
|
-
}
|
|
775
|
-
};
|
|
776
|
-
postCreateFocusHandler = () => { void pull("window focus"); };
|
|
777
|
-
window.addEventListener("focus", postCreateFocusHandler);
|
|
778
|
-
postCreateTimers = POST_CREATE_REFRESH_DELAYS_MS.map((ms, i) => window.setTimeout(() => {
|
|
779
|
-
void pull(`${ms / 1000} s after create`);
|
|
780
|
-
if (i === POST_CREATE_REFRESH_DELAYS_MS.length - 1) disarmPostCreateRefresh();
|
|
781
|
-
}, ms));
|
|
782
|
-
}
|
|
783
|
-
function disarmPostCreateRefresh(): void {
|
|
784
|
-
for (const t of postCreateTimers) window.clearTimeout(t);
|
|
785
|
-
postCreateTimers = [];
|
|
786
|
-
if (postCreateFocusHandler) window.removeEventListener("focus", postCreateFocusHandler);
|
|
787
|
-
postCreateFocusHandler = null;
|
|
803
|
+
/** The extracted event (wall-clock ISO strings in its own zone) as the
|
|
804
|
+
* row createCalendarEvent takes (epoch ms). A null event — "Create as
|
|
805
|
+
* text" — becomes an all-day event today titled with the raw text. */
|
|
806
|
+
function eventToLocalRow(event: any, fallbackText: string): { title: string; startMs: number; endMs: number; allDay: boolean; location?: string; notes?: string } {
|
|
807
|
+
const startOfToday = new Date(); startOfToday.setHours(0, 0, 0, 0);
|
|
808
|
+
if (!event?.startISO) {
|
|
809
|
+
return { title: (event?.title || fallbackText).slice(0, 200), startMs: startOfToday.getTime(), endMs: startOfToday.getTime() + 86400_000, allDay: true };
|
|
810
|
+
}
|
|
811
|
+
const allDay = !!event.allDay || !event.startISO.includes("T");
|
|
812
|
+
const startMs = allDay ? new Date(event.startISO.slice(0, 10) + "T00:00:00").getTime() : wallClockToMs(event.startISO, event.timeZone);
|
|
813
|
+
let endMs: number;
|
|
814
|
+
if (event.endISO) endMs = allDay ? new Date(event.endISO.slice(0, 10) + "T00:00:00").getTime() : wallClockToMs(event.endISO, event.timeZone);
|
|
815
|
+
else endMs = startMs + (allDay ? 86400_000 : 3600_000);
|
|
816
|
+
// An all-day end is exclusive; the extractor may hand back the same day
|
|
817
|
+
// for start and end, which would be a zero-length event.
|
|
818
|
+
if (allDay && endMs <= startMs) endMs = startMs + 86400_000;
|
|
819
|
+
return { title: event.title || fallbackText.slice(0, 200), startMs, endMs, allDay, location: event.location || undefined, notes: event.notes || undefined };
|
|
788
820
|
}
|
|
789
821
|
|
|
790
|
-
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const date = `${y}${mo}${d}`;
|
|
808
|
-
if (!hh) return date; // all-day form: dates=YYYYMMDD/YYYYMMDD
|
|
809
|
-
return `${date}T${hh}${mm}${ss || "00"}`;
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
if (event?.title) params.push(`text=${encodeURIComponent(event.title)}`);
|
|
813
|
-
else params.push(`text=${encodeURIComponent(fallbackText.slice(0, 200))}`);
|
|
814
|
-
|
|
815
|
-
if (event?.startISO) {
|
|
816
|
-
const startFmt = fmt(event.startISO);
|
|
817
|
-
// For all-day events Google wants `dates=YYYYMMDD/YYYYMMDD` where
|
|
818
|
-
// the end date is exclusive (next day). For timed events it's
|
|
819
|
-
// `YYYYMMDDTHHMMSS/YYYYMMDDTHHMMSS`. Default end = +1h timed,
|
|
820
|
-
// +1 day all-day.
|
|
821
|
-
if (event.allDay) {
|
|
822
|
-
const startDate = new Date(event.startISO + (event.startISO.includes("T") ? "" : "T00:00:00"));
|
|
823
|
-
const endDate = new Date(startDate.getTime() + 86400_000);
|
|
824
|
-
const endFmt = `${endDate.getFullYear()}${String(endDate.getMonth() + 1).padStart(2, "0")}${String(endDate.getDate()).padStart(2, "0")}`;
|
|
825
|
-
params.push(`dates=${startFmt}/${endFmt}`);
|
|
826
|
-
} else {
|
|
827
|
-
const endFmt = event.endISO
|
|
828
|
-
? fmt(event.endISO)
|
|
829
|
-
: fmt(new Date(new Date(event.startISO).getTime() + 3600_000).toISOString().slice(0, 19));
|
|
830
|
-
params.push(`dates=${startFmt}/${endFmt}`);
|
|
822
|
+
/** Epoch ms for a wall-clock ISO string ("2026-09-12T09:00:00") read in
|
|
823
|
+
* `timeZone` — an invite that says "9am Kuala Lumpur" stays 9am there.
|
|
824
|
+
* Without a zone the string is read as local time, which is what
|
|
825
|
+
* `new Date(iso)` does for an offset-less string. */
|
|
826
|
+
function wallClockToMs(iso: string, timeZone?: string): number {
|
|
827
|
+
const local = new Date(iso).getTime();
|
|
828
|
+
if (!timeZone) return local;
|
|
829
|
+
try {
|
|
830
|
+
// Offset of the zone at that instant: format the local guess back in
|
|
831
|
+
// the zone, and the difference is the correction. Two passes settle a
|
|
832
|
+
// guess that straddles a DST change.
|
|
833
|
+
let ms = local;
|
|
834
|
+
for (let i = 0; i < 2; i++) {
|
|
835
|
+
const parts = new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" })
|
|
836
|
+
.formatToParts(new Date(ms)).reduce((o, p) => (o[p.type] = p.value, o), {} as Record<string, string>);
|
|
837
|
+
const asIfLocal = new Date(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`).getTime();
|
|
838
|
+
ms += local - asIfLocal;
|
|
831
839
|
}
|
|
840
|
+
return ms;
|
|
841
|
+
} catch (e: any) {
|
|
842
|
+
console.warn(`[calendar] unknown time zone "${timeZone}", reading the time as local:`, e?.message || e);
|
|
843
|
+
return local;
|
|
832
844
|
}
|
|
833
|
-
if (event?.location) params.push(`location=${encodeURIComponent(event.location)}`);
|
|
834
|
-
if (event?.notes) params.push(`details=${encodeURIComponent(event.notes)}`);
|
|
835
|
-
// Times above are wall-clock in the event's own zone (an invite that says
|
|
836
|
-
// "9am Kuala Lumpur" stays 9am); ctz tells Google which zone that is.
|
|
837
|
-
if (event?.timeZone) params.push(`ctz=${encodeURIComponent(event.timeZone)}`);
|
|
838
|
-
return `${base}&${params.join("&")}`;
|
|
839
845
|
}
|
|
840
846
|
|
|
847
|
+
|
|
848
|
+
|
|
841
849
|
/** Show the sidebar (called from the View menu toggle). Idempotent. */
|
|
842
850
|
export async function showCalendarSidebar(): Promise<void> {
|
|
843
851
|
const el = document.getElementById("calendar-sidebar");
|
|
@@ -877,9 +885,22 @@ export function initCalendarSidebar(): void {
|
|
|
877
885
|
(el as any).__wired = true;
|
|
878
886
|
el.addEventListener("click", fn);
|
|
879
887
|
};
|
|
880
|
-
wireOnce("cal-side-prev", () => { const d = new Date(viewYear, viewMonth, viewDay - 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
|
|
881
|
-
wireOnce("cal-side-next", () => { const d = new Date(viewYear, viewMonth, viewDay + 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
|
|
882
|
-
wireOnce("cal-side-today", () => {
|
|
888
|
+
wireOnce("cal-side-prev", () => { viewPinned = true; const d = new Date(viewYear, viewMonth, viewDay - 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
|
|
889
|
+
wireOnce("cal-side-next", () => { viewPinned = true; const d = new Date(viewYear, viewMonth, viewDay + 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
|
|
890
|
+
wireOnce("cal-side-today", () => { viewPinned = false; followToday(); refresh(); });
|
|
891
|
+
if (!(window as any).__mailxCalRolloverArmed) {
|
|
892
|
+
(window as any).__mailxCalRolloverArmed = true;
|
|
893
|
+
armMidnightRollover();
|
|
894
|
+
// Coming back to mailx from elsewhere — Google Calendar in the
|
|
895
|
+
// browser, most likely — pull what changed there. The daemon's 20 s
|
|
896
|
+
// floor turns a flapping window into at most one call per 20 s.
|
|
897
|
+
window.addEventListener("focus", () => {
|
|
898
|
+
if (!isCalendarSidebarOn()) return;
|
|
899
|
+
void refreshCalendarNow().then(r => {
|
|
900
|
+
if (!r?.ok && r?.reason && !/refreshed \d+ s ago/.test(r.reason)) console.log(`[calendar] focus refresh not started: ${r.reason}`);
|
|
901
|
+
}).catch((e: any) => console.warn("[calendar] focus refresh failed:", e?.message || e));
|
|
902
|
+
});
|
|
903
|
+
}
|
|
883
904
|
wireOnce("cal-side-new", () => { openNewEventDialog(); });
|
|
884
905
|
wireOnce("cal-side-new-task", async () => {
|
|
885
906
|
const title = prompt("Task title:");
|
|
@@ -918,7 +939,16 @@ export function initCalendarSidebar(): void {
|
|
|
918
939
|
try {
|
|
919
940
|
// Also re-enumerate calendars — the user may have selected /
|
|
920
941
|
// deselected one in Google since the sidebar opened.
|
|
921
|
-
|
|
942
|
+
// refreshCalendarNow is what makes this button honest: refresh()
|
|
943
|
+
// alone re-reads local rows, and the daemon's 5-minute throttle
|
|
944
|
+
// turned "Refresh events from Google" into a no-op most of the
|
|
945
|
+
// time (Bob 2026-09-10: "still showing me yesterday's calendar").
|
|
946
|
+
// Its emit re-renders the list when the pull changes anything.
|
|
947
|
+
const [, , pulled] = await Promise.all([renderCalendarList(), refresh(), refreshCalendarNow()]);
|
|
948
|
+
if (!pulled?.ok) {
|
|
949
|
+
const st = document.getElementById("status-sync");
|
|
950
|
+
if (st) st.textContent = `Calendar not pulled from Google: ${pulled?.reason || "unknown"}`;
|
|
951
|
+
}
|
|
922
952
|
} finally {
|
|
923
953
|
setTimeout(() => {
|
|
924
954
|
btn?.classList.remove("cal-side-refreshing");
|
package/client/index.html
CHANGED
|
@@ -908,14 +908,14 @@
|
|
|
908
908
|
|
|
909
909
|
<aside class="calendar-sidebar" id="calendar-sidebar" hidden aria-label="Calendar sidebar">
|
|
910
910
|
<header class="cal-side-head">
|
|
911
|
-
<button class="cal-side-nav" id="cal-side-prev" title="Previous">‹</button>
|
|
911
|
+
<button class="cal-side-nav" id="cal-side-prev" title="Previous day. The list then stays on that day until you press Today.">‹</button>
|
|
912
912
|
<span class="cal-side-date" id="cal-side-date"></span>
|
|
913
|
-
<button class="cal-side-nav" id="cal-side-today" title="
|
|
914
|
-
<button class="cal-side-nav" id="cal-side-next" title="Next">›</button>
|
|
913
|
+
<button class="cal-side-nav" id="cal-side-today" title="Back to today. The list follows the date on its own unless you have stepped to another day.">○</button>
|
|
914
|
+
<button class="cal-side-nav" id="cal-side-next" title="Next day. The list then stays on that day until you press Today.">›</button>
|
|
915
915
|
</header>
|
|
916
916
|
<div class="cal-side-actions">
|
|
917
|
-
<button class="cal-side-new" id="cal-side-new" title="New event">+ New event</button>
|
|
918
|
-
<button class="cal-side-new" id="cal-side-refresh-events" title="
|
|
917
|
+
<button class="cal-side-new" id="cal-side-new" title="New event: describe it in plain English and it is saved to your calendar. Click an event to edit it in Google Calendar; right-click to delete it.">+ New event</button>
|
|
918
|
+
<button class="cal-side-new" id="cal-side-refresh-events" title="Pull events and the calendar list from Google now. Otherwise they refresh every 5 minutes and whenever this window regains focus.">↻</button>
|
|
919
919
|
<label class="cal-side-opt" title="Include expanded instances of recurring events">
|
|
920
920
|
<input type="checkbox" id="cal-side-show-recurring" checked> Show recurring
|
|
921
921
|
</label>
|
|
@@ -925,7 +925,7 @@
|
|
|
925
925
|
</div>
|
|
926
926
|
<!-- One checkbox per Google-selected calendar, generated at runtime
|
|
927
927
|
by renderCalendarList(). Uncheck to hide that calendar in mailx. -->
|
|
928
|
-
<div class="cal-side-calendars" id="cal-side-calendars"></div>
|
|
928
|
+
<div class="cal-side-calendars" id="cal-side-calendars" title="Your Google calendars. Untick one to hide its events here; add or remove calendars in Google Calendar itself."></div>
|
|
929
929
|
<div class="cal-side-body" id="cal-side-body">
|
|
930
930
|
<div class="cal-side-empty">Loading…</div>
|
|
931
931
|
</div>
|
package/npmchanges.md
CHANGED
|
@@ -1829,3 +1829,38 @@ self-spoof findings 35 before v1.2.314, 20 after it, 15 now. The 15 that
|
|
|
1829
1829
|
remain are the sextortion, "failed to deliver", NDA-review and
|
|
1830
1830
|
document-share phishes the check exists for. mailx-types 0.1.91.
|
|
1831
1831
|
|
|
1832
|
+
## v1.2.316 — 2026-09-10
|
|
1833
|
+
|
|
1834
|
+
New event: spelling suggestions, the right Google account, and a prompt refresh
|
|
1835
|
+
|
|
1836
|
+
Three from Bob 2026-09-10 on the calendar sidebar's New event dialog.
|
|
1837
|
+
|
|
1838
|
+
"Spelling correct didn't work." The main window's right-click handler
|
|
1839
|
+
replaced the native WebView2 menu with mailx's Cut/Copy/Paste block on
|
|
1840
|
+
every surface — and Chromium's spelling suggestions and "Add to
|
|
1841
|
+
dictionary" exist only in the native menu, unreachable from JS. A word in
|
|
1842
|
+
the New event box was underlined with no menu that could fix it. Plain
|
|
1843
|
+
text controls (TEXTAREA, text-like INPUT) now keep the native menu, which
|
|
1844
|
+
already carries Cut/Copy/Paste/Select all for a text field; everything
|
|
1845
|
+
else is unchanged. (msger's compose items — Bold, Italic, View source —
|
|
1846
|
+
also appear on those fields via its IsEditable gate and forward to the
|
|
1847
|
+
compose frame, which is a no-op outside compose; cosmetic, noted.)
|
|
1848
|
+
|
|
1849
|
+
"I just used the add event but don't see them on Google calendar." The
|
|
1850
|
+
dialog opens Google Calendar's create page in the browser, which signs in
|
|
1851
|
+
as the browser's DEFAULT Google account, not necessarily the one mailx's
|
|
1852
|
+
calendar reads. The URL now carries authuser=<primary calendar account
|
|
1853
|
+
email> so the event lands where the sidebar looks.
|
|
1854
|
+
|
|
1855
|
+
"Creating an event should trigger an immediate update for the calendar
|
|
1856
|
+
view." The reader saves the event in the browser; mailx only learned of
|
|
1857
|
+
it at the next 5-minute throttled refresh. New refreshCalendarNow IPC
|
|
1858
|
+
(service, jsonrpc, mailxapi.js, api-client, MailxApi, Android stub that
|
|
1859
|
+
says ok:false) rewinds the throttle once and goes through
|
|
1860
|
+
getCalendarEvents, so the in-flight dedup and the calendarUpdated emit
|
|
1861
|
+
stay in one place; a 20 s floor of its own keeps it from reopening the
|
|
1862
|
+
quota hole the throttle closed. The sidebar arms it after a create: on
|
|
1863
|
+
the next window focus, and at 45 s / 2 min / 5 min in case the browser
|
|
1864
|
+
sits on another monitor. mailx-types 0.1.93, mailx-service 0.1.36,
|
|
1865
|
+
mailx-store-web 0.1.114.
|
|
1866
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/rmfmail",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.317",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@bobfrankston/iflow-direct": "^0.1.68",
|
|
36
36
|
"@bobfrankston/mailx-host": "^0.1.15",
|
|
37
|
-
"@bobfrankston/mailx-imap": "^0.1.
|
|
38
|
-
"@bobfrankston/mailx-store-web": "^0.1.
|
|
37
|
+
"@bobfrankston/mailx-imap": "^0.1.181",
|
|
38
|
+
"@bobfrankston/mailx-store-web": "^0.1.115",
|
|
39
39
|
"@bobfrankston/mailx-sync": "^0.1.29",
|
|
40
40
|
"@bobfrankston/miscinfo": "^1.0.21",
|
|
41
41
|
"@bobfrankston/msger": "^0.1.431",
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
"dependencies": {
|
|
120
120
|
"@bobfrankston/iflow-direct": "^0.1.68",
|
|
121
121
|
"@bobfrankston/mailx-host": "^0.1.15",
|
|
122
|
-
"@bobfrankston/mailx-imap": "^0.1.
|
|
123
|
-
"@bobfrankston/mailx-store-web": "^0.1.
|
|
122
|
+
"@bobfrankston/mailx-imap": "^0.1.181",
|
|
123
|
+
"@bobfrankston/mailx-store-web": "^0.1.115",
|
|
124
124
|
"@bobfrankston/mailx-sync": "^0.1.29",
|
|
125
125
|
"@bobfrankston/miscinfo": "^1.0.21",
|
|
126
126
|
"@bobfrankston/msger": "^0.1.431",
|