@bobfrankston/mailx 1.0.439 → 1.0.441
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.
|
@@ -99,9 +99,63 @@ function renderEvents(events) {
|
|
|
99
99
|
const today = new Date();
|
|
100
100
|
today.setHours(0, 0, 0, 0);
|
|
101
101
|
const tomorrow = new Date(today.getTime() + 86400_000);
|
|
102
|
-
|
|
102
|
+
// Daily-recurring detection: group expanded instances by recurringEventId,
|
|
103
|
+
// pull out series that fire (roughly) every day at the same time of day,
|
|
104
|
+
// and render them once at the top instead of cluttering each per-day
|
|
105
|
+
// section. "Daily" = same recurringEventId + same H:MM + appears on at
|
|
106
|
+
// least 80% of the days in the rendered horizon (allows for the
|
|
107
|
+
// occasional skipped day without losing the grouping).
|
|
108
|
+
const horizonDays = getHorizonDays();
|
|
109
|
+
const dailyThreshold = Math.max(2, Math.floor(horizonDays * 0.8));
|
|
110
|
+
const byRecurId = new Map();
|
|
111
|
+
for (const e of events) {
|
|
112
|
+
if (!e.recurringEventId)
|
|
113
|
+
continue;
|
|
114
|
+
let arr = byRecurId.get(e.recurringEventId);
|
|
115
|
+
if (!arr) {
|
|
116
|
+
arr = [];
|
|
117
|
+
byRecurId.set(e.recurringEventId, arr);
|
|
118
|
+
}
|
|
119
|
+
arr.push(e);
|
|
120
|
+
}
|
|
121
|
+
const dailyKeys = new Set();
|
|
122
|
+
const dailyHeads = [];
|
|
123
|
+
for (const [recId, instances] of byRecurId) {
|
|
124
|
+
if (instances.length < dailyThreshold)
|
|
125
|
+
continue;
|
|
126
|
+
// Same time-of-day across all instances?
|
|
127
|
+
const hm = (e) => {
|
|
128
|
+
if (e.allDay)
|
|
129
|
+
return "all";
|
|
130
|
+
const d = new Date(e.start);
|
|
131
|
+
return `${d.getHours()}:${d.getMinutes()}`;
|
|
132
|
+
};
|
|
133
|
+
const firstHm = hm(instances[0]);
|
|
134
|
+
const allSameTime = instances.every(e => hm(e) === firstHm);
|
|
135
|
+
if (!allSameTime)
|
|
136
|
+
continue;
|
|
137
|
+
dailyKeys.add(recId);
|
|
138
|
+
// Use the earliest instance as the "head" entry (its htmlLink is
|
|
139
|
+
// identical across instances of the same series).
|
|
140
|
+
dailyHeads.push(instances.reduce((a, b) => a.start < b.start ? a : b));
|
|
141
|
+
}
|
|
103
142
|
let html = "";
|
|
143
|
+
if (dailyHeads.length > 0) {
|
|
144
|
+
html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
|
|
145
|
+
for (const e of dailyHeads) {
|
|
146
|
+
const link = e.htmlLink || "";
|
|
147
|
+
html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
|
|
148
|
+
<span class="cal-side-event-dot ${e.source === "google" ? "g" : "l"}"></span>
|
|
149
|
+
<span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
|
|
150
|
+
<span class="cal-side-event-title">${escapeHtml(e.title)}<span class="cal-side-event-recur" title="Daily">↻</span></span>
|
|
151
|
+
</div>`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let lastDayKey = "";
|
|
104
155
|
for (const e of events) {
|
|
156
|
+
// Filter out daily-grouped instances — they're shown once at top.
|
|
157
|
+
if (e.recurringEventId && dailyKeys.has(e.recurringEventId))
|
|
158
|
+
continue;
|
|
105
159
|
const d = new Date(e.start);
|
|
106
160
|
const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
107
161
|
if (dayKey !== lastDayKey) {
|
|
@@ -320,6 +374,26 @@ export function initCalendarSidebar() {
|
|
|
320
374
|
}, 600);
|
|
321
375
|
}
|
|
322
376
|
});
|
|
377
|
+
// Manual events refresh — same pattern as tasks. fetchUpcoming hits the
|
|
378
|
+
// service which kicks a Google pull under the hood.
|
|
379
|
+
wireOnce("cal-side-refresh-events", async () => {
|
|
380
|
+
const btn = document.getElementById("cal-side-refresh-events");
|
|
381
|
+
if (btn?.classList.contains("cal-side-refreshing"))
|
|
382
|
+
return;
|
|
383
|
+
btn?.classList.add("cal-side-refreshing");
|
|
384
|
+
if (btn)
|
|
385
|
+
btn.disabled = true;
|
|
386
|
+
try {
|
|
387
|
+
await refresh();
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
setTimeout(() => {
|
|
391
|
+
btn?.classList.remove("cal-side-refreshing");
|
|
392
|
+
if (btn)
|
|
393
|
+
btn.disabled = false;
|
|
394
|
+
}, 600);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
323
397
|
const showDoneCb = document.getElementById("cal-side-show-done");
|
|
324
398
|
if (showDoneCb && !showDoneCb.__wired) {
|
|
325
399
|
showDoneCb.__wired = true;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Message viewer component -- displays full message in sandboxed iframe.
|
|
3
3
|
* Subscribes to message-state: clears when selected becomes null.
|
|
4
4
|
*/
|
|
5
|
-
import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, addContact, listContacts, upsertContact, unsubscribeOneClick } from "../lib/api-client.js";
|
|
5
|
+
import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, addContact, listContacts, upsertContact, unsubscribeOneClick, addPreferredContact } from "../lib/api-client.js";
|
|
6
6
|
import { showContextMenu } from "./context-menu.js";
|
|
7
7
|
import * as state from "../lib/message-state.js";
|
|
8
8
|
/** Currently displayed message (for reply/forward) */
|
|
@@ -348,6 +348,28 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
348
348
|
await showAddContactDialog(name, addr.address);
|
|
349
349
|
},
|
|
350
350
|
});
|
|
351
|
+
// "Add to preferred" — separate path: writes to
|
|
352
|
+
// contacts.jsonc#preferred[] with an optional source tag.
|
|
353
|
+
// Distinct from "Add to contacts" which goes into the DB +
|
|
354
|
+
// pushes to Google. Preferred entries rank higher in
|
|
355
|
+
// autocomplete and survive Google sync's churn.
|
|
356
|
+
items.push({
|
|
357
|
+
label: `Add to preferred: ${addr.address}`,
|
|
358
|
+
action: async () => {
|
|
359
|
+
const tag = prompt("Tag (e.g. work, family, vendor) — leave blank for default:", "");
|
|
360
|
+
if (tag === null)
|
|
361
|
+
return; // user cancelled
|
|
362
|
+
try {
|
|
363
|
+
await addPreferredContact({ name, email: addr.address, source: tag.trim() || undefined });
|
|
364
|
+
const status = document.getElementById("status-sync");
|
|
365
|
+
if (status)
|
|
366
|
+
status.textContent = `Added to preferred: ${addr.address}${tag ? ` [${tag}]` : ""}`;
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
alert(`Couldn't add to preferred: ${e?.message || e}`);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
});
|
|
351
373
|
items.push({ label: "", action: () => { }, separator: true });
|
|
352
374
|
}
|
|
353
375
|
items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
|
package/client/index.html
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
<label class="tb-menu-item" title="Ghost-text completions while composing — Ollama / Claude / OpenAI back-end, off by default"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
|
|
56
56
|
<label class="tb-menu-item" title="Right-click in message body → Translate"><input type="checkbox" id="opt-ai-translate"> AI translate (off by default)</label>
|
|
57
57
|
<label class="tb-menu-item" title="Right-click in compose editor → Proofread (when wired)"><input type="checkbox" id="opt-ai-proofread"> AI proofread (off by default)</label>
|
|
58
|
-
<label class="tb-menu-item" title="When opening a message with remote content, look up the sender's domain in Spamhaus DBL.
|
|
58
|
+
<label class="tb-menu-item" title="When opening a message with remote content, look up the sender's domain in three free DNS blocklists in parallel: Spamhaus DBL, SURBL, URIBL. The banner shows N-of-3 services flagging the domain. Each query leaks the bare domain to that DNSBL's DNS. Off by default."><input type="checkbox" id="opt-check-reputation"> Check sender reputation (DNSBLs)</label>
|
|
59
59
|
<hr class="tb-menu-sep">
|
|
60
60
|
<button class="tb-menu-item" id="btn-edit-jsonc" title="Edit accounts.jsonc / allowlist.jsonc">Edit config files...</button>
|
|
61
61
|
<button class="tb-menu-item" id="btn-open-mailx-dir" title="Open ~/.mailx in file explorer">Open mailx folder...</button>
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
</header>
|
|
185
185
|
<div class="cal-side-actions">
|
|
186
186
|
<button class="cal-side-new" id="cal-side-new" title="New event">+ New event</button>
|
|
187
|
+
<button class="cal-side-new" id="cal-side-refresh-events" title="Refresh events from Google">↻</button>
|
|
187
188
|
<label class="cal-side-opt" title="Include expanded instances of recurring events">
|
|
188
189
|
<input type="checkbox" id="cal-side-show-recurring" checked> Show recurring
|
|
189
190
|
</label>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.441",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@bobfrankston/iflow-node": "^0.1.8",
|
|
37
37
|
"@bobfrankston/miscinfo": "^1.0.10",
|
|
38
38
|
"@bobfrankston/oauthsupport": "^1.0.25",
|
|
39
|
-
"@bobfrankston/msger": "^0.1.
|
|
39
|
+
"@bobfrankston/msger": "^0.1.364",
|
|
40
40
|
"@bobfrankston/mailx-host": "^0.1.8",
|
|
41
41
|
"@capacitor/android": "^8.3.0",
|
|
42
42
|
"@capacitor/cli": "^8.3.0",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"@bobfrankston/iflow-node": "^0.1.8",
|
|
101
101
|
"@bobfrankston/miscinfo": "^1.0.10",
|
|
102
102
|
"@bobfrankston/oauthsupport": "^1.0.25",
|
|
103
|
-
"@bobfrankston/msger": "^0.1.
|
|
103
|
+
"@bobfrankston/msger": "^0.1.364",
|
|
104
104
|
"@bobfrankston/mailx-host": "^0.1.8",
|
|
105
105
|
"@capacitor/android": "^8.3.0",
|
|
106
106
|
"@capacitor/cli": "^8.3.0",
|