relay-companion 0.1.45 → 0.1.46
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/overlay/inbox.html +29 -3
- package/overlay/main.cjs +33 -0
- package/overlay/preload.cjs +1 -0
- package/package.json +1 -1
- package/src/client.js +4 -0
- package/src/notifications.js +39 -7
- package/src/task-daemon.js +3 -0
package/overlay/inbox.html
CHANGED
|
@@ -152,6 +152,14 @@
|
|
|
152
152
|
}
|
|
153
153
|
.tab.active .tab-badge { color:var(--accent); }
|
|
154
154
|
.tab-badge.gone { display:none; }
|
|
155
|
+
.mark-all-read {
|
|
156
|
+
appearance:none; margin-left:auto; border:0; background:transparent; cursor:pointer;
|
|
157
|
+
padding:0 2px; color:var(--muted-2); font-family:var(--sans); font-size:10.5px; font-weight:500;
|
|
158
|
+
transition:color .15s var(--settle), opacity .15s var(--settle);
|
|
159
|
+
}
|
|
160
|
+
.mark-all-read:hover { color:var(--accent); }
|
|
161
|
+
.mark-all-read:disabled { opacity:.45; cursor:default; color:var(--muted-3); }
|
|
162
|
+
.mark-all-read.gone { visibility:hidden; pointer-events:none; }
|
|
155
163
|
|
|
156
164
|
/* scrolling content layer (host for each view) */
|
|
157
165
|
.scroll {
|
|
@@ -562,6 +570,7 @@
|
|
|
562
570
|
</button>
|
|
563
571
|
<button class="tab" type="button" data-view="sent">Sent</button>
|
|
564
572
|
<button class="tab" type="button" data-view="contacts">Contacts</button>
|
|
573
|
+
<button class="mark-all-read gone" id="markAllRead" type="button">Mark all as read</button>
|
|
565
574
|
</nav>
|
|
566
575
|
|
|
567
576
|
<div class="scroll" id="scroll">
|
|
@@ -636,6 +645,7 @@
|
|
|
636
645
|
const relaysListEl = document.getElementById("relaysList");
|
|
637
646
|
const relaysEmptyEl = document.getElementById("relaysEmpty");
|
|
638
647
|
const relaysBadgeEl = document.getElementById("relaysBadge");
|
|
648
|
+
const markAllReadEl = document.getElementById("markAllRead");
|
|
639
649
|
const sentViewEl = document.getElementById("sentView");
|
|
640
650
|
const sentListEl = document.getElementById("sentList");
|
|
641
651
|
const sentEmptyEl = document.getElementById("sentEmpty");
|
|
@@ -661,7 +671,7 @@
|
|
|
661
671
|
|
|
662
672
|
const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
663
673
|
const EXPANDED = { w: 344, h: 524 };
|
|
664
|
-
const PILL = { w: 244, h: 44 }; // wide enough for mark + word + "N
|
|
674
|
+
const PILL = { w: 244, h: 44 }; // wide enough for mark + word + "N unread" + ✕
|
|
665
675
|
const PEEK = { w: 344, h: 118 };
|
|
666
676
|
|
|
667
677
|
// ---------- zero-latency sound (Web Audio, pre-decoded) ----------
|
|
@@ -858,7 +868,7 @@
|
|
|
858
868
|
}
|
|
859
869
|
// New relays always enter the notification stack, including from a fully expanded
|
|
860
870
|
// card. The main process has already pushed the new payload, so render/sizing uses
|
|
861
|
-
// the latest
|
|
871
|
+
// the latest unread rows rather than the previous inbox.
|
|
862
872
|
function notifyArrival(rows) {
|
|
863
873
|
if (peekTimer) clearTimeout(peekTimer);
|
|
864
874
|
cancelBye();
|
|
@@ -1199,7 +1209,7 @@
|
|
|
1199
1209
|
|
|
1200
1210
|
let unreadCount = 0;
|
|
1201
1211
|
function refreshCountLabel() {
|
|
1202
|
-
const label = collapsed ? `${unreadCount}
|
|
1212
|
+
const label = collapsed ? `${unreadCount} unread` : String(unreadCount);
|
|
1203
1213
|
if (countEl.textContent !== label) {
|
|
1204
1214
|
countEl.textContent = label;
|
|
1205
1215
|
if (!REDUCED) { countEl.style.animation = "none"; void countEl.offsetWidth; countEl.style.animation = "countRoll .2s var(--settle)"; }
|
|
@@ -2155,11 +2165,27 @@
|
|
|
2155
2165
|
const unread = (payload.relays || []).filter((r) => r.unread && isRelayListKind(r)).length;
|
|
2156
2166
|
setCount(unread);
|
|
2157
2167
|
setBadge(relaysBadgeEl, unread);
|
|
2168
|
+
markAllReadEl.classList.toggle("gone", activeView !== "relays" || unread === 0);
|
|
2169
|
+
markAllReadEl.disabled = unread === 0;
|
|
2158
2170
|
if (activeView === "relays") renderRelays();
|
|
2159
2171
|
else if (activeView === "sent") renderSent();
|
|
2160
2172
|
// contacts renders from its own load path
|
|
2161
2173
|
}
|
|
2162
2174
|
|
|
2175
|
+
markAllReadEl.addEventListener("click", async () => {
|
|
2176
|
+
if (markAllReadEl.disabled || unreadCount === 0) return;
|
|
2177
|
+
markAllReadEl.disabled = true;
|
|
2178
|
+
markAllReadEl.textContent = "Marking…";
|
|
2179
|
+
const result = await window.relay.markAllRead();
|
|
2180
|
+
markAllReadEl.textContent = "Mark all as read";
|
|
2181
|
+
if (!result || !result.ok) {
|
|
2182
|
+
markAllReadEl.disabled = false;
|
|
2183
|
+
markAllReadEl.title = (result && result.error) || "Could not mark relays as read.";
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
markAllReadEl.title = "";
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2163
2189
|
function syncDismissControl() {
|
|
2164
2190
|
const ui = (payload && payload.ui) || {};
|
|
2165
2191
|
const canDismiss = ui.canDismiss !== false;
|
package/overlay/main.cjs
CHANGED
|
@@ -374,6 +374,38 @@ function visibleRelayRows(rows) {
|
|
|
374
374
|
return rows.filter((row) => !RELAY_HIDDEN_KINDS.has(row && row.relayNotificationKind));
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
async function markAllVisibleRelaysRead() {
|
|
378
|
+
try {
|
|
379
|
+
// Server first so the website and other devices share the same read cutoff.
|
|
380
|
+
const client = await relayClient();
|
|
381
|
+
const result = await client.markAllRead({ idempotencyKey: idempotencyKey("inbox_read_all") });
|
|
382
|
+
const cutoffMs = new Date(result.readAllAt || "").getTime();
|
|
383
|
+
let marked = 0;
|
|
384
|
+
withJsonLock(STATE_PATH, () => {
|
|
385
|
+
const store = readStore();
|
|
386
|
+
for (const [id, row] of Object.entries(store.packets || {})) {
|
|
387
|
+
if (!row || row.direction !== "inbound" || row.state === "read") continue;
|
|
388
|
+
if (RELAY_HIDDEN_KINDS.has(row.relayNotificationKind)) continue;
|
|
389
|
+
const reference = row.deliveryReferenceAt ||
|
|
390
|
+
(row.relayNotificationKind === "plain_relay" ? row.updatedAt : row.createdAt);
|
|
391
|
+
const referenceMs = new Date(reference || "").getTime();
|
|
392
|
+
if (Number.isFinite(cutoffMs) && Number.isFinite(referenceMs) && referenceMs > cutoffMs) continue;
|
|
393
|
+
row.state = "read";
|
|
394
|
+
activeAttentionIds.delete(id);
|
|
395
|
+
marked += 1;
|
|
396
|
+
}
|
|
397
|
+
writeStateAtomic(store);
|
|
398
|
+
});
|
|
399
|
+
writeOverlayPrefs();
|
|
400
|
+
await pushInbox(true);
|
|
401
|
+
return { ok: true, marked, readAllAt: result.readAllAt };
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const message = error && error.message ? error.message : String(error);
|
|
404
|
+
console.error("[overlay] mark all read failed:", message);
|
|
405
|
+
return { ok: false, error: message };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
377
409
|
// ---- sent relays (live) --------------------------------------------------
|
|
378
410
|
|
|
379
411
|
let sentCache = [];
|
|
@@ -1631,6 +1663,7 @@ ipcMain.on("relay:openTask", (_e, taskId) => openTaskDetail(taskId));
|
|
|
1631
1663
|
ipcMain.on("relay:openUrl", (_e, url) => openUrlTarget(url));
|
|
1632
1664
|
ipcMain.on("relay:ack", (_e, id) => ackPacket(id));
|
|
1633
1665
|
ipcMain.handle("relay:delete", (_e, id) => deletePacket(id));
|
|
1666
|
+
ipcMain.handle("relay:markAllRead", () => markAllVisibleRelaysRead());
|
|
1634
1667
|
ipcMain.handle("relay:refreshSent", async () => {
|
|
1635
1668
|
await refreshSent();
|
|
1636
1669
|
await pushInbox(true);
|
package/overlay/preload.cjs
CHANGED
|
@@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld("relay", {
|
|
|
16
16
|
openSent: (id) => ipcRenderer.send("relay:openSent", id),
|
|
17
17
|
ack: (id) => ipcRenderer.send("relay:ack", id),
|
|
18
18
|
deleteRelay: (id) => ipcRenderer.invoke("relay:delete", id),
|
|
19
|
+
markAllRead: () => ipcRenderer.invoke("relay:markAllRead"),
|
|
19
20
|
openUrl: (url) => ipcRenderer.send("relay:openUrl", url),
|
|
20
21
|
refreshSent: () => ipcRenderer.invoke("relay:refreshSent"),
|
|
21
22
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
package/src/notifications.js
CHANGED
|
@@ -198,12 +198,20 @@ export function buildHumanNotifications({ user, tasks = [], relays = [], connect
|
|
|
198
198
|
}
|
|
199
199
|
const suppressed = new Set(inboxState.suppressedItemIds || []);
|
|
200
200
|
const restoredAtByItemId = inboxState.restoredAtByItemId || {};
|
|
201
|
+
const readAllAtMs = inboxState.readAllAt ? new Date(inboxState.readAllAt).getTime() : Number.NaN;
|
|
201
202
|
return notifications
|
|
202
203
|
.filter((notification) => !suppressed.has(notification.id))
|
|
203
|
-
.map((notification) =>
|
|
204
|
-
|
|
205
|
-
deliveryVersion
|
|
206
|
-
|
|
204
|
+
.map((notification) => {
|
|
205
|
+
const deliveryVersion = restoredAtByItemId[notification.id] || "";
|
|
206
|
+
const deliveryReferenceAt = deliveryVersion || notification.createdAt || "";
|
|
207
|
+
const deliveryMs = new Date(deliveryReferenceAt).getTime();
|
|
208
|
+
return {
|
|
209
|
+
...notification,
|
|
210
|
+
deliveryVersion,
|
|
211
|
+
deliveryReferenceAt,
|
|
212
|
+
read: Number.isFinite(readAllAtMs) && Number.isFinite(deliveryMs) && deliveryMs <= readAllAtMs,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
export function freshNotifications(ledger, notifications) {
|
|
@@ -270,6 +278,28 @@ export function removeSuppressedCompanionItems(itemIds, { statePath = companionS
|
|
|
270
278
|
});
|
|
271
279
|
}
|
|
272
280
|
|
|
281
|
+
/** Reconcile an account-wide mark-all-read watermark onto already-staged rows. */
|
|
282
|
+
export function markCompanionItemsReadThrough(readAllAt, { statePath = companionStatePath() } = {}) {
|
|
283
|
+
const cutoffMs = new Date(readAllAt || "").getTime();
|
|
284
|
+
if (!Number.isFinite(cutoffMs)) return 0;
|
|
285
|
+
return withJsonLock(statePath, () => {
|
|
286
|
+
const state = readCompanionState(statePath);
|
|
287
|
+
state.packets ||= {};
|
|
288
|
+
let changed = 0;
|
|
289
|
+
for (const row of Object.values(state.packets)) {
|
|
290
|
+
if (!row || row.direction !== "inbound" || row.state === "read") continue;
|
|
291
|
+
const reference = row.deliveryReferenceAt ||
|
|
292
|
+
(row.relayNotificationKind === "plain_relay" ? row.updatedAt : row.createdAt);
|
|
293
|
+
const referenceMs = new Date(reference || "").getTime();
|
|
294
|
+
if (!Number.isFinite(referenceMs) || referenceMs > cutoffMs) continue;
|
|
295
|
+
row.state = "read";
|
|
296
|
+
changed += 1;
|
|
297
|
+
}
|
|
298
|
+
if (changed) writeCompanionState(statePath, state);
|
|
299
|
+
return changed;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
273
303
|
function accountMarker({ user = currentUser(), deviceId = readConfig().deviceId || "" } = {}) {
|
|
274
304
|
return {
|
|
275
305
|
userId: user?.id || "",
|
|
@@ -413,7 +443,7 @@ function relayPacketFromNotification(notification, { now = new Date().toISOStrin
|
|
|
413
443
|
id: notification.id,
|
|
414
444
|
createdAt: notification.createdAt || now,
|
|
415
445
|
updatedAt: now,
|
|
416
|
-
state: "unread",
|
|
446
|
+
state: notification.read ? "read" : "unread",
|
|
417
447
|
sender,
|
|
418
448
|
recipient: notificationRecipient(),
|
|
419
449
|
source: {
|
|
@@ -448,7 +478,7 @@ export function relayCompanionRowFromNotification(
|
|
|
448
478
|
return {
|
|
449
479
|
id: notification.id,
|
|
450
480
|
direction: "inbound",
|
|
451
|
-
state: "unread",
|
|
481
|
+
state: notification.read ? "read" : "unread",
|
|
452
482
|
kind: "message",
|
|
453
483
|
// Default unknown notification kinds to a neutral "message", never "task_completed"
|
|
454
484
|
// (which would falsely mark an unknown row as a finished task). main.cjs readRelays
|
|
@@ -461,6 +491,7 @@ export function relayCompanionRowFromNotification(
|
|
|
461
491
|
briefingMarkdown: packetMarkdownForNotification(notification),
|
|
462
492
|
bodyMarkdown: notification.body || notification.title || "",
|
|
463
493
|
createdAt: notification.createdAt || now,
|
|
494
|
+
deliveryReferenceAt: notification.deliveryReferenceAt || notification.createdAt || now,
|
|
464
495
|
updatedAt: now,
|
|
465
496
|
stagedAt: now,
|
|
466
497
|
actionUrl,
|
|
@@ -490,7 +521,7 @@ export function stageRelayCompanionItem(notification, { statePath = companionSta
|
|
|
490
521
|
const item = {
|
|
491
522
|
...existing,
|
|
492
523
|
...relayCompanionRowFromNotification(notification, { contentPath }),
|
|
493
|
-
state: existing.state === "read" ? "read" : "unread",
|
|
524
|
+
state: existing.state === "read" || packet.state === "read" ? "read" : "unread",
|
|
494
525
|
};
|
|
495
526
|
state.packets[item.id] = item;
|
|
496
527
|
writeCompanionState(statePath, state);
|
|
@@ -602,6 +633,7 @@ export function stagePlainRelayItem(
|
|
|
602
633
|
bodyMarkdown: packet?.bodyMarkdown || item.preview || "",
|
|
603
634
|
createdAt: item.createdAt || packet?.createdAt || now,
|
|
604
635
|
updatedAt: item.updatedAt || now,
|
|
636
|
+
deliveryReferenceAt: item.updatedAt || item.createdAt || now,
|
|
605
637
|
stagedAt: now,
|
|
606
638
|
actionUrl: "/app/relays",
|
|
607
639
|
action: { type: "open", url: "/app/relays" },
|
package/src/task-daemon.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
defaultStageRelayCompanionItem,
|
|
15
15
|
freshNotifications,
|
|
16
16
|
markNotificationsProcessed,
|
|
17
|
+
markCompanionItemsReadThrough,
|
|
17
18
|
removeSuppressedCompanionItems,
|
|
18
19
|
reconcileAnsweredQuestions,
|
|
19
20
|
stagePlainRelayItem as defaultStagePlainRelayItem,
|
|
@@ -174,6 +175,8 @@ async function pollHumanNotifications({ client, ledger, stageCompanionItem, stag
|
|
|
174
175
|
const events = await pollVisibleTaskEvents({ client, ledger, tasks, log });
|
|
175
176
|
const removed = removeSuppressedCompanionItems(relays.inboxState?.suppressedItemIds || []);
|
|
176
177
|
if (removed) log(`removed ${removed} Relay item(s) moved to Recently Deleted elsewhere`);
|
|
178
|
+
const markedRead = markCompanionItemsReadThrough(relays.inboxState?.readAllAt);
|
|
179
|
+
if (markedRead) log(`marked ${markedRead} Relay item(s) read from another surface`);
|
|
177
180
|
const notifications = buildHumanNotifications({
|
|
178
181
|
user: me.user,
|
|
179
182
|
tasks: tasks.tasks || [],
|