fix(deferred): close frontend UI/UX tails from the hygiene sweep
- player YB2: keepalive pagehide beacon (api.saveProgressBeacon) so the resume position isn't lost on F5/close within 5s of the last checkpoint (mirrors Plex). - player YB4: bound consecutive unplayable items in auto-advance so an auto-advancing/loop=all queue can't spin over a run of dead videos. - downloads B6 (client): localise structured quota/edit errors centrally in api.req() via localizeDetail() + Intl.NumberFormat (fixes HU/DE + decimal sep); + 3-locale download error strings. - channels FB1: focusChannelToken bump so re-clicking the same channel re-seeds the search box; FB2: syncSubs invalidates feed/feed-count (set can change now). - config CB2: reseed the ConfigPanel draft on a key-set dataVersion + explicit post-save token, so a mid-edit refetch can't clobber an in-progress edit. - config AB3 (UI): a blank allow_empty field stores "" instead of resetting. - admin SB2/SC2: confirm + success toast on demo-whitelist remove and deny-invite (+ trilingual strings); SC1: disable only the acted-on row (pendingId), not all; SB3: pause the 1s scheduler countdown ticker while the tab is hidden. - messages CT4: only invalidate conversations/unread when the incoming-message count actually changed, not on every 20s poll; MB3 (client): react to the live unread ping (onUnread → invalidate); MC3: extract e2ee installKey (setup/unlock); CC2: hoist POLL_MS to messaging.ts; MB4: document the reload-scoped socket.
This commit is contained in:
@@ -11,8 +11,10 @@ interface IncomingMessage {
|
||||
to?: MessageUser;
|
||||
}
|
||||
type Handler = (e: IncomingMessage) => void;
|
||||
type UnreadHandler = () => void;
|
||||
|
||||
const handlers = new Set<Handler>();
|
||||
const unreadHandlers = new Set<UnreadHandler>();
|
||||
let ws: WebSocket | null = null;
|
||||
let started = false;
|
||||
let backoff = 1000;
|
||||
@@ -21,7 +23,9 @@ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
function url(): string {
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
// A WebSocket can't carry the X-Siftlode-Account header, so the per-tab account rides in the
|
||||
// query string (validated against the browser wallet server-side).
|
||||
// query string (validated against the browser wallet server-side). The account is read once
|
||||
// here: switching accounts always goes through location.reload() (NavSidebar), which rebuilds
|
||||
// this module and reopens the socket, so it's intentionally fixed for the socket's lifetime.
|
||||
const account = getActiveAccount();
|
||||
const qs = account != null ? `?account=${account}` : "";
|
||||
return `${proto}://${location.host}/api/messages/ws${qs}`;
|
||||
@@ -43,6 +47,9 @@ function open() {
|
||||
if (data?.type === "message" && data.message) {
|
||||
const e: IncomingMessage = { message: data.message as Message, from: data.from, to: data.to };
|
||||
for (const h of handlers) h(e);
|
||||
} else if (data?.type === "unread") {
|
||||
// Sent to a user's OTHER tabs when they read a thread elsewhere, so the badge updates live.
|
||||
for (const h of unreadHandlers) h();
|
||||
}
|
||||
} catch {
|
||||
/* ignore malformed frames */
|
||||
@@ -87,3 +94,13 @@ export function onMessage(h: Handler): () => void {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Subscribe to "your unread changed elsewhere" pings (a read on another tab/device). The socket is
|
||||
// already opened/closed by onMessage subscribers (the app-wide ChatDock keeps one), so this only
|
||||
// registers a callback — it does not open the socket on its own.
|
||||
export function onUnread(h: UnreadHandler): () => void {
|
||||
unreadHandlers.add(h);
|
||||
return () => {
|
||||
unreadHandlers.delete(h);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user