fix(web): make the 'connection lost' notice self-resolving

The notice promised 'this will clear once it's back', but nothing ever
removed it: the toast auto-hid on a 15s timer (not on recovery) and the
v0.9.0 unified inbox kept every notify() in persistent history, so the
notice lingered there forever.

Model it as a single live status instead: one sticky, transient (never
persisted) notice while the server is unreachable, removed the moment any
request reaches the server again. Adds a 'transient' notification flag
(sticky toast + excluded from history so a reload can't orphan it) and
replaces the throttled-toast helper with a connectivity lost/restored pair.
i18n the strings (errors.offline.*) in EN/HU/DE — they were hardcoded English.
This commit is contained in:
npeter83 2026-06-18 23:17:32 +02:00
parent 4f7aac7b92
commit 1278d079e2
5 changed files with 50 additions and 20 deletions

View file

@ -38,6 +38,10 @@ export interface Notification {
ts: number;
read: boolean;
dismissed: boolean; // transient toast surface closed (still kept in history)
// Live session-only status (e.g. "connection lost"): sticky toast (no auto-dismiss
// timer) that the producer removes itself when the condition resolves. Never persisted
// to history, so a reload can't orphan it with no live handle to clear it.
transient: boolean;
}
export interface NotifyInput {
@ -48,6 +52,7 @@ export interface NotifyInput {
meta?: NotifMeta;
requiresInteraction?: boolean;
sound?: boolean; // force the alert tone (when sound is enabled) even for an info toast
transient?: boolean; // live status: sticky toast, not persisted (see Notification.transient)
}
const HISTORY_KEY = "subfeed.notifications";
@ -122,7 +127,7 @@ function load(): Notification[] {
// resurrect as active toasts on reload — their auto-dismiss timers aren't
// re-armed across a reload, so otherwise they'd stick forever. Also drop the
// live `action` callback, which can't survive serialization.
return raw.map((n) => ({ ...n, action: undefined, dismissed: true }) as Notification);
return raw.map((n) => ({ ...n, action: undefined, dismissed: true, transient: false }) as Notification);
} catch {
return [];
}
@ -130,7 +135,9 @@ function load(): Notification[] {
function persist() {
try {
const slim = items.map(({ action: _action, ...n }) => n);
// Transient status notices are live-session only — never write them to history,
// so a reload can't leave one stranded with no producer left to clear it.
const slim = items.filter((n) => !n.transient).map(({ action: _action, ...n }) => n);
localStorage.setItem(HISTORY_KEY, JSON.stringify(slim));
} catch {
/* ignore quota / serialization errors */
@ -164,7 +171,9 @@ export function notify(input: NotifyInput): number {
const requiresInteraction = input.requiresInteraction ?? false;
const level = input.level ?? "info";
// config.durationMs === 0 means "don't auto-dismiss" (toast stays until dismissed).
const duration = requiresInteraction
// A transient status stays put until its producer clears it (no auto-dismiss timer),
// same as an interaction-awaiting notice.
const duration = requiresInteraction || input.transient
? undefined
: level === "error" || level === "fatal"
? ERROR_TTL
@ -185,6 +194,7 @@ export function notify(input: NotifyInput): number {
ts: Date.now(),
read: false,
dismissed: false,
transient: input.transient ?? false,
},
];
emit();