@bobfrankston/rmfmail 1.1.45 → 1.1.49
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 +9 -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 +1 -1
- 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 +1 -0
- package/packages/mailx-service/google-sync.js.map +1 -1
- package/packages/mailx-service/google-sync.ts +4 -0
- 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-imap/{node_modules.npmglobalize-stash-28848 → node_modules.npmglobalize-stash-43988}/.package-lock.json +0 -0
package/client/app.bundle.js
CHANGED
|
@@ -42,6 +42,7 @@ __export(api_client_exports, {
|
|
|
42
42
|
getAttachment: () => getAttachment,
|
|
43
43
|
getAutocompleteSettings: () => getAutocompleteSettings,
|
|
44
44
|
getCalendarEvents: () => getCalendarEvents,
|
|
45
|
+
getCalendars: () => getCalendars,
|
|
45
46
|
getDeviceAccounts: () => getDeviceAccounts,
|
|
46
47
|
getDiagnostics: () => getDiagnostics,
|
|
47
48
|
getFolders: () => getFolders,
|
|
@@ -221,6 +222,9 @@ function getPrimaryAccount(feature) {
|
|
|
221
222
|
function getCalendarEvents(fromMs, toMs) {
|
|
222
223
|
return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
|
|
223
224
|
}
|
|
225
|
+
function getCalendars() {
|
|
226
|
+
return ipc().getCalendars?.() ?? Promise.resolve([]);
|
|
227
|
+
}
|
|
224
228
|
function createCalendarEvent(ev) {
|
|
225
229
|
return ipc().createCalendarEvent?.(ev);
|
|
226
230
|
}
|
|
@@ -3793,6 +3797,106 @@ __export(calendar_sidebar_exports, {
|
|
|
3793
3797
|
isCalendarSidebarOn: () => isCalendarSidebarOn,
|
|
3794
3798
|
showCalendarSidebar: () => showCalendarSidebar
|
|
3795
3799
|
});
|
|
3800
|
+
function calendarKind(id, primary) {
|
|
3801
|
+
if (primary)
|
|
3802
|
+
return "personal";
|
|
3803
|
+
const c = (id || "").toLowerCase();
|
|
3804
|
+
if (c.includes("addressbook#contacts"))
|
|
3805
|
+
return "birthday";
|
|
3806
|
+
if (c.includes("#holiday@") || c.includes("holiday@group")) {
|
|
3807
|
+
if (c.includes("usa"))
|
|
3808
|
+
return "usHoliday";
|
|
3809
|
+
if (c.includes("judaism") || c.includes("jewish"))
|
|
3810
|
+
return "jewishHoliday";
|
|
3811
|
+
return "otherHoliday";
|
|
3812
|
+
}
|
|
3813
|
+
return "other";
|
|
3814
|
+
}
|
|
3815
|
+
function calIconHtml(info) {
|
|
3816
|
+
const kind = calendarKind(info.id, info.primary);
|
|
3817
|
+
const t = escapeHtml4(info.name);
|
|
3818
|
+
if (kind === "personal")
|
|
3819
|
+
return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
|
|
3820
|
+
if (kind === "usHoliday")
|
|
3821
|
+
return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F1FA}\u{1F1F8}</span>`;
|
|
3822
|
+
if (kind === "jewishHoliday")
|
|
3823
|
+
return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2721\uFE0F</span>`;
|
|
3824
|
+
if (kind === "birthday")
|
|
3825
|
+
return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F382}</span>`;
|
|
3826
|
+
if (kind === "otherHoliday")
|
|
3827
|
+
return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2726</span>`;
|
|
3828
|
+
const letter = escapeHtml4((info.name.trim()[0] || "?").toUpperCase());
|
|
3829
|
+
const color = info.color || "#7a7a7a";
|
|
3830
|
+
return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml4(color)}" title="${t}">${letter}</span>`;
|
|
3831
|
+
}
|
|
3832
|
+
function calInfoFor(calendarId) {
|
|
3833
|
+
const id = calendarId || "primary";
|
|
3834
|
+
const found = calById.get(id);
|
|
3835
|
+
if (found)
|
|
3836
|
+
return found;
|
|
3837
|
+
return { id, name: id.split("@")[0] || id, color: "", primary: id === "primary" };
|
|
3838
|
+
}
|
|
3839
|
+
function isCalHidden(calendarId) {
|
|
3840
|
+
return hiddenCalendars.has(calInfoFor(calendarId).id);
|
|
3841
|
+
}
|
|
3842
|
+
async function loadHiddenCalendars() {
|
|
3843
|
+
try {
|
|
3844
|
+
const s = await getSettings();
|
|
3845
|
+
const arr = s?.calendar?.hiddenCalendars;
|
|
3846
|
+
hiddenCalendars = new Set(Array.isArray(arr) ? arr : []);
|
|
3847
|
+
} catch {
|
|
3848
|
+
hiddenCalendars = /* @__PURE__ */ new Set();
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
async function saveHiddenCalendars() {
|
|
3852
|
+
try {
|
|
3853
|
+
const s = await getSettings();
|
|
3854
|
+
s.calendar = { ...s.calendar || {}, hiddenCalendars: [...hiddenCalendars] };
|
|
3855
|
+
await saveSettings(s);
|
|
3856
|
+
} catch (e) {
|
|
3857
|
+
console.error("[cal] save hiddenCalendars failed:", e);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
async function renderCalendarList() {
|
|
3861
|
+
const host = document.getElementById("cal-side-calendars");
|
|
3862
|
+
let list = [];
|
|
3863
|
+
try {
|
|
3864
|
+
list = await getCalendars();
|
|
3865
|
+
} catch {
|
|
3866
|
+
}
|
|
3867
|
+
calendarList = list;
|
|
3868
|
+
calById.clear();
|
|
3869
|
+
for (const c of list) {
|
|
3870
|
+
calById.set(c.id, c);
|
|
3871
|
+
if (c.primary)
|
|
3872
|
+
calById.set("primary", c);
|
|
3873
|
+
}
|
|
3874
|
+
if (host) {
|
|
3875
|
+
if (list.length === 0) {
|
|
3876
|
+
host.innerHTML = "";
|
|
3877
|
+
} else {
|
|
3878
|
+
const sorted = [...list].sort((a, b) => (a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
|
|
3879
|
+
host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml4(c.name)}">
|
|
3880
|
+
<input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml4(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
|
|
3881
|
+
${calIconHtml(c)}
|
|
3882
|
+
<span class="cal-side-cal-name">${escapeHtml4(c.name)}</span>
|
|
3883
|
+
</label>`).join("");
|
|
3884
|
+
host.querySelectorAll(".cal-side-cal-check").forEach((cb) => {
|
|
3885
|
+
cb.addEventListener("change", async () => {
|
|
3886
|
+
const id = cb.dataset.calId || "";
|
|
3887
|
+
if (cb.checked)
|
|
3888
|
+
hiddenCalendars.delete(id);
|
|
3889
|
+
else
|
|
3890
|
+
hiddenCalendars.add(id);
|
|
3891
|
+
await saveHiddenCalendars();
|
|
3892
|
+
renderEvents(lastEvents);
|
|
3893
|
+
});
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
if (lastEvents.length > 0)
|
|
3898
|
+
renderEvents(lastEvents);
|
|
3899
|
+
}
|
|
3796
3900
|
function getSelectedTaskUuids() {
|
|
3797
3901
|
return [...selectedTaskUuids];
|
|
3798
3902
|
}
|
|
@@ -3869,11 +3973,26 @@ function renderHead() {
|
|
|
3869
3973
|
const d = new Date(viewYear, viewMonth, viewDay);
|
|
3870
3974
|
dateEl.innerHTML = `<strong>${d.getDate()}</strong> ${d.toLocaleDateString(void 0, { weekday: "short" })} <span class="cal-side-date-month">${d.toLocaleDateString(void 0, { month: "short", year: "numeric" })}</span>`;
|
|
3871
3975
|
}
|
|
3976
|
+
function overdueTaskRowHtml(t) {
|
|
3977
|
+
let dueLabel = "";
|
|
3978
|
+
if (t.dueMs) {
|
|
3979
|
+
const d = new Date(t.dueMs);
|
|
3980
|
+
const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
|
|
3981
|
+
dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
|
|
3982
|
+
}
|
|
3983
|
+
return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml4(t.uuid)}">
|
|
3984
|
+
<input type="checkbox" class="cal-side-overdue-check" title="Mark done">
|
|
3985
|
+
<span class="cal-side-task-title">${escapeHtml4(t.title)}</span>
|
|
3986
|
+
<span class="cal-side-task-due overdue">${escapeHtml4(dueLabel)}</span>
|
|
3987
|
+
</div>`;
|
|
3988
|
+
}
|
|
3872
3989
|
function renderEvents(events) {
|
|
3873
3990
|
const body = document.getElementById("cal-side-body");
|
|
3874
3991
|
if (!body)
|
|
3875
3992
|
return;
|
|
3876
|
-
|
|
3993
|
+
events = events.filter((e) => !isCalHidden(e.calendarId));
|
|
3994
|
+
const overdue = lastOverdueTasks;
|
|
3995
|
+
if (events.length === 0 && overdue.length === 0) {
|
|
3877
3996
|
body.innerHTML = `<div class="cal-side-empty">No upcoming events. Click + New event to add one.</div>`;
|
|
3878
3997
|
return;
|
|
3879
3998
|
}
|
|
@@ -3912,12 +4031,17 @@ function renderEvents(events) {
|
|
|
3912
4031
|
dailyHeads.push(instances.reduce((a, b) => a.start < b.start ? a : b));
|
|
3913
4032
|
}
|
|
3914
4033
|
let html = "";
|
|
4034
|
+
if (overdue.length > 0) {
|
|
4035
|
+
html += `<div class="cal-side-day cal-side-day-overdue">Overdue tasks</div>`;
|
|
4036
|
+
for (const t of overdue)
|
|
4037
|
+
html += overdueTaskRowHtml(t);
|
|
4038
|
+
}
|
|
3915
4039
|
if (dailyHeads.length > 0) {
|
|
3916
4040
|
html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
|
|
3917
4041
|
for (const e of dailyHeads) {
|
|
3918
4042
|
const link = e.htmlLink || "";
|
|
3919
4043
|
html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml4(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
|
|
3920
|
-
|
|
4044
|
+
${calIconHtml(calInfoFor(e.calendarId))}
|
|
3921
4045
|
<span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
|
|
3922
4046
|
<span class="cal-side-event-title">${escapeHtml4(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
|
|
3923
4047
|
</div>`;
|
|
@@ -3929,7 +4053,10 @@ function renderEvents(events) {
|
|
|
3929
4053
|
for (const e of events) {
|
|
3930
4054
|
if (e.recurringEventId && dailyKeys.has(e.recurringEventId))
|
|
3931
4055
|
continue;
|
|
3932
|
-
|
|
4056
|
+
const info = calInfoFor(e.calendarId);
|
|
4057
|
+
const kind = calendarKind(info.id, info.primary);
|
|
4058
|
+
const isHolidayKind = kind === "usHoliday" || kind === "jewishHoliday" || kind === "otherHoliday";
|
|
4059
|
+
if (isHolidayKind && e.start > holidayCutoff)
|
|
3933
4060
|
continue;
|
|
3934
4061
|
const d = new Date(e.start);
|
|
3935
4062
|
const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
@@ -3939,35 +4066,30 @@ function renderEvents(events) {
|
|
|
3939
4066
|
}
|
|
3940
4067
|
const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">\u21BB</span>` : "";
|
|
3941
4068
|
const link = e.htmlLink || "";
|
|
3942
|
-
let holidayKind;
|
|
3943
|
-
if (e.isHoliday) {
|
|
3944
|
-
const cid = (e.calendarId || "").toLowerCase();
|
|
3945
|
-
if (cid.includes("usa"))
|
|
3946
|
-
holidayKind = "us";
|
|
3947
|
-
else if (cid.includes("judaism") || cid.includes("jewish"))
|
|
3948
|
-
holidayKind = "jewish";
|
|
3949
|
-
else
|
|
3950
|
-
holidayKind = "other";
|
|
3951
|
-
}
|
|
3952
|
-
const holidayAttr = e.isHoliday ? ` data-holiday="1" data-holiday-kind="${holidayKind}"` : "";
|
|
3953
4069
|
const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
if (e.isHoliday) {
|
|
3958
|
-
const symbol = holidayKind === "us" ? "\u{1F1FA}\u{1F1F8}" : holidayKind === "jewish" ? "\u2721\uFE0F" : "\u2726";
|
|
3959
|
-
html += `<div class="cal-side-event"${holidayAttr} data-id="${e.id}">
|
|
3960
|
-
<span class="cal-side-event-title cal-side-event-holiday-title"><span class="cal-side-event-holiday-symbol">${symbol}</span> ${escapeHtml4(e.title)}</span>
|
|
4070
|
+
if (isHolidayKind || kind === "birthday") {
|
|
4071
|
+
html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
|
|
4072
|
+
<span class="cal-side-event-title cal-side-event-holiday-title">${calIconHtml(info)} ${escapeHtml4(e.title)}</span>
|
|
3961
4073
|
</div>`;
|
|
3962
4074
|
} else {
|
|
3963
|
-
|
|
3964
|
-
|
|
4075
|
+
const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
|
|
4076
|
+
html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml4(link)}" ${titleAttr}>
|
|
4077
|
+
${calIconHtml(info)}
|
|
3965
4078
|
<span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
|
|
3966
4079
|
<span class="cal-side-event-title">${escapeHtml4(e.title)}${recurMark}</span>
|
|
3967
4080
|
</div>`;
|
|
3968
4081
|
}
|
|
3969
4082
|
}
|
|
3970
4083
|
body.innerHTML = html;
|
|
4084
|
+
body.querySelectorAll(".cal-side-task-overdue").forEach((row) => {
|
|
4085
|
+
const uuid = row.dataset.uuid;
|
|
4086
|
+
row.querySelector(".cal-side-overdue-check")?.addEventListener("change", async () => {
|
|
4087
|
+
await updateTask(uuid, { completedMs: Date.now() });
|
|
4088
|
+
lastOverdueTasks = lastOverdueTasks.filter((t) => t.uuid !== uuid);
|
|
4089
|
+
renderEvents(lastEvents);
|
|
4090
|
+
renderTasks();
|
|
4091
|
+
});
|
|
4092
|
+
});
|
|
3971
4093
|
const openInCalendar = (url) => {
|
|
3972
4094
|
const api = window.mailxapi;
|
|
3973
4095
|
if (api?.openCalendarEvent) {
|
|
@@ -4010,10 +4132,10 @@ function renderEvents(events) {
|
|
|
4010
4132
|
});
|
|
4011
4133
|
});
|
|
4012
4134
|
}
|
|
4013
|
-
async function renderTasks() {
|
|
4135
|
+
async function renderTasks(prefetched) {
|
|
4014
4136
|
const cb = document.getElementById("cal-side-show-done");
|
|
4015
4137
|
const showDone = cb?.checked ?? false;
|
|
4016
|
-
const tasks = await getTasks(showDone);
|
|
4138
|
+
const tasks = prefetched ?? await getTasks(showDone);
|
|
4017
4139
|
const host = document.getElementById("cal-side-tasks");
|
|
4018
4140
|
if (!host)
|
|
4019
4141
|
return;
|
|
@@ -4102,15 +4224,25 @@ async function renderTasks() {
|
|
|
4102
4224
|
async function refresh() {
|
|
4103
4225
|
renderHead();
|
|
4104
4226
|
const from = new Date(viewYear, viewMonth, viewDay);
|
|
4227
|
+
let prefetchedTasks;
|
|
4105
4228
|
try {
|
|
4106
|
-
|
|
4229
|
+
const startOfToday = /* @__PURE__ */ new Date();
|
|
4230
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
4231
|
+
const showDone = document.getElementById("cal-side-show-done")?.checked ?? false;
|
|
4232
|
+
const [events, tasks] = await Promise.all([
|
|
4233
|
+
fetchUpcoming(from),
|
|
4234
|
+
getTasks(showDone).catch(() => [])
|
|
4235
|
+
]);
|
|
4236
|
+
lastEvents = events;
|
|
4237
|
+
prefetchedTasks = tasks;
|
|
4238
|
+
lastOverdueTasks = prefetchedTasks.filter((t) => t.dueMs && !t.completedMs && t.dueMs < startOfToday.getTime()).map((t) => ({ uuid: t.uuid, title: t.title, dueMs: t.dueMs }));
|
|
4107
4239
|
renderEvents(lastEvents);
|
|
4108
4240
|
} catch (e) {
|
|
4109
4241
|
const body = document.getElementById("cal-side-body");
|
|
4110
4242
|
if (body)
|
|
4111
4243
|
body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml4(e?.message || String(e))}</div>`;
|
|
4112
4244
|
}
|
|
4113
|
-
renderTasks();
|
|
4245
|
+
renderTasks(prefetchedTasks);
|
|
4114
4246
|
}
|
|
4115
4247
|
function openNewEventDialog() {
|
|
4116
4248
|
const backdrop = document.createElement("div");
|
|
@@ -4237,6 +4369,8 @@ async function showCalendarSidebar() {
|
|
|
4237
4369
|
localStorage.setItem(SIDEBAR_PREF, "true");
|
|
4238
4370
|
} catch {
|
|
4239
4371
|
}
|
|
4372
|
+
await loadHiddenCalendars();
|
|
4373
|
+
void renderCalendarList();
|
|
4240
4374
|
await refresh();
|
|
4241
4375
|
}
|
|
4242
4376
|
function hideCalendarSidebar() {
|
|
@@ -4321,7 +4455,7 @@ function initCalendarSidebar() {
|
|
|
4321
4455
|
if (btn)
|
|
4322
4456
|
btn.disabled = true;
|
|
4323
4457
|
try {
|
|
4324
|
-
await refresh();
|
|
4458
|
+
await Promise.all([renderCalendarList(), refresh()]);
|
|
4325
4459
|
} finally {
|
|
4326
4460
|
setTimeout(() => {
|
|
4327
4461
|
btn?.classList.remove("cal-side-refreshing");
|
|
@@ -4357,32 +4491,6 @@ function initCalendarSidebar() {
|
|
|
4357
4491
|
refresh();
|
|
4358
4492
|
});
|
|
4359
4493
|
}
|
|
4360
|
-
const wireHolidayCheckbox = (cbId, settingsKey) => {
|
|
4361
|
-
const cb = document.getElementById(cbId);
|
|
4362
|
-
if (!cb || cb.__wired)
|
|
4363
|
-
return;
|
|
4364
|
-
cb.__wired = true;
|
|
4365
|
-
let userTouched = false;
|
|
4366
|
-
getSettings().then((s) => {
|
|
4367
|
-
if (userTouched)
|
|
4368
|
-
return;
|
|
4369
|
-
cb.checked = !!s?.calendar?.[settingsKey];
|
|
4370
|
-
}).catch(() => {
|
|
4371
|
-
});
|
|
4372
|
-
cb.addEventListener("change", async () => {
|
|
4373
|
-
userTouched = true;
|
|
4374
|
-
try {
|
|
4375
|
-
const s = await getSettings();
|
|
4376
|
-
s.calendar = { ...s.calendar || {}, [settingsKey]: cb.checked };
|
|
4377
|
-
await saveSettings(s);
|
|
4378
|
-
} catch (e) {
|
|
4379
|
-
console.error(`[cal] ${settingsKey} save failed:`, e);
|
|
4380
|
-
}
|
|
4381
|
-
refresh();
|
|
4382
|
-
});
|
|
4383
|
-
};
|
|
4384
|
-
wireHolidayCheckbox("cal-side-show-holidays", "showHolidays");
|
|
4385
|
-
wireHolidayCheckbox("cal-side-show-jewish-holidays", "showJewishHolidays");
|
|
4386
4494
|
const horizonInput = document.getElementById("cal-side-horizon");
|
|
4387
4495
|
if (horizonInput && !horizonInput.__wired) {
|
|
4388
4496
|
horizonInput.__wired = true;
|
|
@@ -4441,7 +4549,7 @@ function initCalendarSidebar() {
|
|
|
4441
4549
|
}
|
|
4442
4550
|
}
|
|
4443
4551
|
}
|
|
4444
|
-
var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, selectedTaskUuids;
|
|
4552
|
+
var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, lastOverdueTasks, calById, calendarList, hiddenCalendars, selectedTaskUuids;
|
|
4445
4553
|
var init_calendar_sidebar = __esm({
|
|
4446
4554
|
"client/components/calendar-sidebar.js"() {
|
|
4447
4555
|
"use strict";
|
|
@@ -4457,6 +4565,10 @@ var init_calendar_sidebar = __esm({
|
|
|
4457
4565
|
viewMonth = (/* @__PURE__ */ new Date()).getMonth();
|
|
4458
4566
|
viewDay = (/* @__PURE__ */ new Date()).getDate();
|
|
4459
4567
|
lastEvents = [];
|
|
4568
|
+
lastOverdueTasks = [];
|
|
4569
|
+
calById = /* @__PURE__ */ new Map();
|
|
4570
|
+
calendarList = [];
|
|
4571
|
+
hiddenCalendars = /* @__PURE__ */ new Set();
|
|
4460
4572
|
selectedTaskUuids = /* @__PURE__ */ new Set();
|
|
4461
4573
|
}
|
|
4462
4574
|
});
|
|
@@ -5093,6 +5205,81 @@ var init_alarms = __esm({
|
|
|
5093
5205
|
}
|
|
5094
5206
|
});
|
|
5095
5207
|
|
|
5208
|
+
// client/help/search-help.js
|
|
5209
|
+
var search_help_exports = {};
|
|
5210
|
+
__export(search_help_exports, {
|
|
5211
|
+
SEARCH_HELP_HTML: () => SEARCH_HELP_HTML
|
|
5212
|
+
});
|
|
5213
|
+
var SEARCH_HELP_HTML;
|
|
5214
|
+
var init_search_help = __esm({
|
|
5215
|
+
"client/help/search-help.js"() {
|
|
5216
|
+
"use strict";
|
|
5217
|
+
SEARCH_HELP_HTML = `
|
|
5218
|
+
<p>Search runs in one of three modes depending on the scope you pick. The mode
|
|
5219
|
+
decides which operators work.</p>
|
|
5220
|
+
<ul>
|
|
5221
|
+
<li><strong>All folders</strong> (default) \u2014 searches the <em>local cache</em> with SQLite FTS5.</li>
|
|
5222
|
+
<li><strong>This folder</strong> \u2014 instant client-side filter on the visible rows; full FTS5 search on Enter.</li>
|
|
5223
|
+
<li><strong>Server</strong> (checkbox) \u2014 sends the query to the mail server (IMAP SEARCH; not yet wired for Gmail accounts).</li>
|
|
5224
|
+
</ul>
|
|
5225
|
+
|
|
5226
|
+
<h3>Qualifiers \u2014 work in every mode</h3>
|
|
5227
|
+
<table>
|
|
5228
|
+
<tr><th>Form</th><th>Effect</th></tr>
|
|
5229
|
+
<tr><td><code>from:bob</code></td><td>Sender contains</td></tr>
|
|
5230
|
+
<tr><td><code>to:eleanor</code></td><td>Recipient contains</td></tr>
|
|
5231
|
+
<tr><td><code>subject:lunch</code></td><td>Subject contains</td></tr>
|
|
5232
|
+
<tr><td><code>date:2026-05-01</code></td><td>On that date \u2014 also <code>date:>1w</code>, <code>date:<=2026-01-15</code></td></tr>
|
|
5233
|
+
<tr><td><code>after:1w</code> / <code>before:1m</code></td><td>Newer / older than \u2014 <code>d</code>, <code>w</code>, <code>m</code>, <code>y</code>, a date, <code>today</code>, <code>yesterday</code></td></tr>
|
|
5234
|
+
<tr><td><code>has:attachment</code></td><td>Has an attachment</td></tr>
|
|
5235
|
+
<tr><td><code>is:unread</code></td><td>Also <code>is:flagged</code>, <code>is:read</code>, <code>is:answered</code>, <code>is:draft</code></td></tr>
|
|
5236
|
+
<tr><td><code>folder:sent</code></td><td>Restrict to folders whose name contains the term</td></tr>
|
|
5237
|
+
<tr><td><code>/regex/</code></td><td>Client-side regex over the currently-visible rows. Local only \u2014 never sent to the server.</td></tr>
|
|
5238
|
+
</table>
|
|
5239
|
+
<p>Any remaining unqualified text is the free-text term.</p>
|
|
5240
|
+
|
|
5241
|
+
<h3>Local search (FTS5) \u2014 default</h3>
|
|
5242
|
+
<p>Full-text index over envelopes plus locally-cached bodies. Fast and indexed.</p>
|
|
5243
|
+
<table>
|
|
5244
|
+
<tr><th>Operator</th><th>Example</th><th>Meaning</th></tr>
|
|
5245
|
+
<tr><td>implicit AND</td><td><code>bob lunch</code></td><td>Both terms must appear</td></tr>
|
|
5246
|
+
<tr><td><code>OR</code></td><td><code>bob OR eleanor</code></td><td>Either term</td></tr>
|
|
5247
|
+
<tr><td><code>NOT</code></td><td><code>bob NOT spam</code></td><td>First without the second</td></tr>
|
|
5248
|
+
<tr><td><code>"phrase"</code></td><td><code>"happy birthday"</code></td><td>Exact phrase</td></tr>
|
|
5249
|
+
<tr><td><code>term*</code></td><td><code>lunch*</code></td><td>Prefix</td></tr>
|
|
5250
|
+
<tr><td><code>NEAR(a b, 5)</code></td><td><code>NEAR(bob lunch, 5)</code></td><td>Both within 5 tokens</td></tr>
|
|
5251
|
+
</table>
|
|
5252
|
+
<p>Indexed: subject, from, to, cc, a body snippet, and the full body <em>if</em> it
|
|
5253
|
+
has been downloaded locally (the blue dot in the list). Bodies not yet
|
|
5254
|
+
prefetched aren't searchable until they are.</p>
|
|
5255
|
+
|
|
5256
|
+
<h3>This-folder filter</h3>
|
|
5257
|
+
<p>With scope set to <strong>This folder</strong>, typing filters the rendered rows
|
|
5258
|
+
instantly \u2014 no server hit, no FTS5, just substring match. Press <kbd>Enter</kbd>
|
|
5259
|
+
to escalate to a full FTS5 search of the folder.</p>
|
|
5260
|
+
|
|
5261
|
+
<h3>Server search</h3>
|
|
5262
|
+
<p>Tick <strong>Server</strong> and the query goes to the mail server. On Dovecot
|
|
5263
|
+
(IMAP SEARCH) your <code>from:</code> / <code>to:</code> / <code>subject:</code>
|
|
5264
|
+
qualifiers map to IMAP keys; remaining text becomes a body search. IMAP has no
|
|
5265
|
+
literal <code>AND</code> keyword \u2014 keys are implicitly ANDed. Server search on
|
|
5266
|
+
Gmail accounts is not yet wired and currently returns local results only.</p>
|
|
5267
|
+
|
|
5268
|
+
<h3>Case sensitivity</h3>
|
|
5269
|
+
<p>All three modes \u2014 local FTS5, IMAP server SEARCH, and <code>/regex/</code> \u2014
|
|
5270
|
+
are <strong>case-insensitive</strong>. <code>Lunch</code>, <code>lunch</code>, and
|
|
5271
|
+
<code>LUNCH</code> match the same messages. There is no case-sensitive mode.</p>
|
|
5272
|
+
|
|
5273
|
+
<h3>Limitations</h3>
|
|
5274
|
+
<ul>
|
|
5275
|
+
<li>No regex on the server side, any provider \u2014 <code>/pattern/</code> only filters visible local rows.</li>
|
|
5276
|
+
<li>Server search results are stored locally (envelope-only) on first hit, so a server search also backfills those envelopes.</li>
|
|
5277
|
+
<li>Body search on bodies you haven't prefetched only works server-side; the local index can't search what isn't downloaded.</li>
|
|
5278
|
+
</ul>
|
|
5279
|
+
`;
|
|
5280
|
+
}
|
|
5281
|
+
});
|
|
5282
|
+
|
|
5096
5283
|
// client/components/folder-tree.js
|
|
5097
5284
|
init_api_client();
|
|
5098
5285
|
init_context_menu();
|
|
@@ -6539,7 +6726,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
6539
6726
|
currentFolderSpecialUse = specialUse;
|
|
6540
6727
|
currentAccountId3 = accountId;
|
|
6541
6728
|
currentFolderId2 = folderId;
|
|
6542
|
-
if (searchInput)
|
|
6729
|
+
if (searchInput) {
|
|
6730
|
+
searchInput.value = "";
|
|
6731
|
+
updateSearchHighlight();
|
|
6732
|
+
}
|
|
6543
6733
|
clearSearchMode();
|
|
6544
6734
|
markAsSeen();
|
|
6545
6735
|
releaseFocus();
|
|
@@ -6550,7 +6740,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
6550
6740
|
document.dispatchEvent(new CustomEvent("mailx-folder-changed", { detail: { accountId, folderId } }));
|
|
6551
6741
|
}, () => {
|
|
6552
6742
|
currentFolderSpecialUse = "inbox";
|
|
6553
|
-
if (searchInput)
|
|
6743
|
+
if (searchInput) {
|
|
6744
|
+
searchInput.value = "";
|
|
6745
|
+
updateSearchHighlight();
|
|
6746
|
+
}
|
|
6554
6747
|
clearSearchMode();
|
|
6555
6748
|
releaseFocus();
|
|
6556
6749
|
loadUnifiedInbox();
|
|
@@ -6559,7 +6752,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
6559
6752
|
setActiveView({ kind: "unified" }, "All Inboxes");
|
|
6560
6753
|
});
|
|
6561
6754
|
function applyTabView(tab) {
|
|
6562
|
-
if (searchInput)
|
|
6755
|
+
if (searchInput) {
|
|
6756
|
+
searchInput.value = "";
|
|
6757
|
+
updateSearchHighlight();
|
|
6758
|
+
}
|
|
6563
6759
|
clearSearchMode();
|
|
6564
6760
|
releaseFocus();
|
|
6565
6761
|
const v = tab.view;
|
|
@@ -6576,7 +6772,10 @@ function applyTabView(tab) {
|
|
|
6576
6772
|
setTitle(`${APP_NAME} - ${tab.title}`);
|
|
6577
6773
|
setNarrowFolderTitle(tab.title);
|
|
6578
6774
|
} else {
|
|
6579
|
-
if (searchInput)
|
|
6775
|
+
if (searchInput) {
|
|
6776
|
+
searchInput.value = v.query;
|
|
6777
|
+
updateSearchHighlight();
|
|
6778
|
+
}
|
|
6580
6779
|
loadSearchResults(v.query, v.scope, v.accountId, v.folderId, v.includeTrash);
|
|
6581
6780
|
setTitle(`${APP_NAME} - Search`);
|
|
6582
6781
|
setNarrowFolderTitle(`Search: ${v.query}`);
|
|
@@ -7566,6 +7765,64 @@ function recordSearchHistory(query) {
|
|
|
7566
7765
|
refreshSearchHistoryDatalist();
|
|
7567
7766
|
}
|
|
7568
7767
|
refreshSearchHistoryDatalist();
|
|
7768
|
+
var searchHl = document.getElementById("search-hl");
|
|
7769
|
+
var SEARCH_QUALIFIERS = /* @__PURE__ */ new Set(["from", "to", "subject", "date", "after", "before", "has", "is", "folder"]);
|
|
7770
|
+
var SEARCH_REGEX_SETTLE_MS = 500;
|
|
7771
|
+
var searchRegexSettleTimer = null;
|
|
7772
|
+
function escapeHl(s) {
|
|
7773
|
+
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c] || c);
|
|
7774
|
+
}
|
|
7775
|
+
function renderSearchHighlight(settled) {
|
|
7776
|
+
if (!searchHl || !searchInput) return;
|
|
7777
|
+
const text = searchInput.value;
|
|
7778
|
+
const tokens = text.match(/\s+|"[^"]*"?|\S+/g) || [];
|
|
7779
|
+
let html = "";
|
|
7780
|
+
for (const tok of tokens) {
|
|
7781
|
+
if (/^\s+$/.test(tok)) {
|
|
7782
|
+
html += escapeHl(tok);
|
|
7783
|
+
continue;
|
|
7784
|
+
}
|
|
7785
|
+
if (tok.startsWith("/") && tok.length > 1) {
|
|
7786
|
+
let bad = false;
|
|
7787
|
+
if (tok.endsWith("/")) {
|
|
7788
|
+
try {
|
|
7789
|
+
new RegExp(tok.slice(1, -1), "i");
|
|
7790
|
+
} catch {
|
|
7791
|
+
bad = true;
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
const cls = bad && settled ? "sh-regex-bad" : "sh-regex";
|
|
7795
|
+
html += `<span class="${cls}">${escapeHl(tok)}</span>`;
|
|
7796
|
+
continue;
|
|
7797
|
+
}
|
|
7798
|
+
const qm = tok.match(/^([a-z]+):(.*)$/i);
|
|
7799
|
+
if (qm && SEARCH_QUALIFIERS.has(qm[1].toLowerCase())) {
|
|
7800
|
+
html += `<span class="sh-key">${escapeHl(qm[1] + ":")}</span>${escapeHl(qm[2])}`;
|
|
7801
|
+
continue;
|
|
7802
|
+
}
|
|
7803
|
+
if (/^(OR|NOT|AND)$/.test(tok)) {
|
|
7804
|
+
html += `<span class="sh-op">${escapeHl(tok)}</span>`;
|
|
7805
|
+
continue;
|
|
7806
|
+
}
|
|
7807
|
+
if (tok.startsWith('"')) {
|
|
7808
|
+
html += `<span class="sh-phrase">${escapeHl(tok)}</span>`;
|
|
7809
|
+
continue;
|
|
7810
|
+
}
|
|
7811
|
+
html += escapeHl(tok);
|
|
7812
|
+
}
|
|
7813
|
+
searchHl.innerHTML = html;
|
|
7814
|
+
searchHl.scrollLeft = searchInput.scrollLeft;
|
|
7815
|
+
}
|
|
7816
|
+
function updateSearchHighlight() {
|
|
7817
|
+
renderSearchHighlight(false);
|
|
7818
|
+
if (searchRegexSettleTimer) clearTimeout(searchRegexSettleTimer);
|
|
7819
|
+
searchRegexSettleTimer = setTimeout(() => renderSearchHighlight(true), SEARCH_REGEX_SETTLE_MS);
|
|
7820
|
+
}
|
|
7821
|
+
searchInput?.addEventListener("scroll", () => {
|
|
7822
|
+
if (searchHl) searchHl.scrollLeft = searchInput.scrollLeft;
|
|
7823
|
+
});
|
|
7824
|
+
searchInput?.addEventListener("change", () => updateSearchHighlight());
|
|
7825
|
+
updateSearchHighlight();
|
|
7569
7826
|
function doSearch(immediate = false) {
|
|
7570
7827
|
const query = searchInput.value.trim();
|
|
7571
7828
|
if (query.length === 0) {
|
|
@@ -7602,6 +7859,7 @@ var currentFolderId2 = 0;
|
|
|
7602
7859
|
var reloadDebounceTimer = null;
|
|
7603
7860
|
searchInput?.addEventListener("input", () => {
|
|
7604
7861
|
clearTimeout(searchTimeout);
|
|
7862
|
+
updateSearchHighlight();
|
|
7605
7863
|
if (searchInput.value.trim() === "") {
|
|
7606
7864
|
clearSearchMode();
|
|
7607
7865
|
const body = document.getElementById("ml-body");
|
|
@@ -7619,6 +7877,7 @@ searchInput?.addEventListener("keydown", (e) => {
|
|
|
7619
7877
|
}
|
|
7620
7878
|
if (e.key === "Escape") {
|
|
7621
7879
|
searchInput.value = "";
|
|
7880
|
+
updateSearchHighlight();
|
|
7622
7881
|
clearSearchMode();
|
|
7623
7882
|
const body = document.getElementById("ml-body");
|
|
7624
7883
|
if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
|
|
@@ -8777,34 +9036,66 @@ optSnippet?.addEventListener("change", () => {
|
|
|
8777
9036
|
localStorage.setItem("mailx-snippet", String(optSnippet.checked));
|
|
8778
9037
|
});
|
|
8779
9038
|
document.getElementById("search-help")?.addEventListener("click", async () => {
|
|
8780
|
-
const
|
|
8781
|
-
|
|
8782
|
-
const
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
9039
|
+
const btn = document.getElementById("search-help");
|
|
9040
|
+
if (!btn) return;
|
|
9041
|
+
const existing = document.getElementById("search-help-panel");
|
|
9042
|
+
if (existing) {
|
|
9043
|
+
existing.remove();
|
|
9044
|
+
btn.setAttribute("aria-expanded", "false");
|
|
9045
|
+
return;
|
|
9046
|
+
}
|
|
9047
|
+
const { SEARCH_HELP_HTML: SEARCH_HELP_HTML2 } = await Promise.resolve().then(() => (init_search_help(), search_help_exports));
|
|
9048
|
+
const searchBar = document.querySelector("search.ml-search");
|
|
8786
9049
|
const panel = document.createElement("div");
|
|
8787
|
-
panel.
|
|
9050
|
+
panel.id = "search-help-panel";
|
|
9051
|
+
panel.className = "search-help-panel";
|
|
8788
9052
|
panel.innerHTML = `
|
|
8789
|
-
<div
|
|
8790
|
-
<span
|
|
8791
|
-
<button type="button" id="search-help-close"
|
|
9053
|
+
<div class="search-help-head">
|
|
9054
|
+
<span>Search syntax</span>
|
|
9055
|
+
<button type="button" id="search-help-close" title="Close (Esc)" aria-label="Close">×</button>
|
|
8792
9056
|
</div>
|
|
8793
|
-
<
|
|
9057
|
+
<div class="search-help-body">${SEARCH_HELP_HTML2}</div>
|
|
8794
9058
|
`;
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
const
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
9059
|
+
document.body.appendChild(panel);
|
|
9060
|
+
btn.setAttribute("aria-expanded", "true");
|
|
9061
|
+
const position = () => {
|
|
9062
|
+
const anchor = searchBar || btn;
|
|
9063
|
+
const rect = anchor.getBoundingClientRect();
|
|
9064
|
+
const margin = 8;
|
|
9065
|
+
const top = rect.bottom + 2;
|
|
9066
|
+
const width = Math.min(640, Math.max(320, rect.width));
|
|
9067
|
+
let left = rect.left;
|
|
9068
|
+
if (left + width > window.innerWidth - margin) left = window.innerWidth - margin - width;
|
|
9069
|
+
if (left < margin) left = margin;
|
|
9070
|
+
panel.style.top = `${top}px`;
|
|
9071
|
+
panel.style.left = `${left}px`;
|
|
9072
|
+
panel.style.width = `${width}px`;
|
|
9073
|
+
panel.style.maxHeight = `${Math.max(160, window.innerHeight - top - margin)}px`;
|
|
9074
|
+
};
|
|
9075
|
+
position();
|
|
9076
|
+
const close = () => {
|
|
9077
|
+
panel.remove();
|
|
9078
|
+
btn.setAttribute("aria-expanded", "false");
|
|
9079
|
+
window.removeEventListener("resize", position);
|
|
9080
|
+
document.removeEventListener("keydown", onKey, true);
|
|
9081
|
+
document.removeEventListener("mousedown", onOutside);
|
|
9082
|
+
};
|
|
9083
|
+
const onKey = (e) => {
|
|
8803
9084
|
if (e.key === "Escape") {
|
|
9085
|
+
e.preventDefault();
|
|
9086
|
+
e.stopPropagation();
|
|
8804
9087
|
close();
|
|
8805
|
-
document.removeEventListener("keydown", escClose);
|
|
8806
9088
|
}
|
|
8807
|
-
}
|
|
9089
|
+
};
|
|
9090
|
+
const onOutside = (e) => {
|
|
9091
|
+
const t = e.target;
|
|
9092
|
+
if (panel.contains(t) || btn.contains(t) || searchBar?.contains(t)) return;
|
|
9093
|
+
close();
|
|
9094
|
+
};
|
|
9095
|
+
panel.querySelector("#search-help-close")?.addEventListener("click", close);
|
|
9096
|
+
window.addEventListener("resize", position);
|
|
9097
|
+
document.addEventListener("keydown", onKey, true);
|
|
9098
|
+
document.addEventListener("mousedown", onOutside);
|
|
8808
9099
|
});
|
|
8809
9100
|
document.getElementById("btn-edit-jsonc")?.addEventListener("click", async () => {
|
|
8810
9101
|
const settingsDropdown2 = document.getElementById("settings-dropdown");
|