fix(ui): notification UX — outside-click close, toast countdown, hide entry actions

- NotificationCenter closes on outside click (document listener, not an overlay
  that the blurred header trapped) and no longer needs a second bell click.
- Toasts show a level-colored countdown bar and auto-dismiss faster (6s default).
- Hidden-video notifications carry structured meta so the center offers an in-app
  'Find in feed' (jump to that channel's hidden videos) and a one-click 'Unhide',
  working even after a reload when the live Undo callback is gone.
This commit is contained in:
npeter83 2026-06-11 19:46:58 +02:00
parent a105e5c184
commit a419ac2943
6 changed files with 173 additions and 71 deletions

View file

@ -10,13 +10,25 @@ export interface NotifAction {
onClick: () => void;
}
// Structured payload that lets the Notification Center offer in-app actions even
// after a reload (when live `action` callbacks are gone).
export type NotifMeta = {
kind: "video-hidden";
videoId: string;
title: string;
channelId: string;
channelName: string;
};
export interface Notification {
id: number;
level: NotifLevel;
title?: string;
message: string;
action?: NotifAction;
meta?: NotifMeta;
requiresInteraction: boolean;
duration?: number; // auto-dismiss ms (undefined when it stays until acted on)
ts: number;
read: boolean;
dismissed: boolean; // transient toast surface closed (still kept in history)
@ -27,12 +39,13 @@ export interface NotifyInput {
level?: NotifLevel;
title?: string;
action?: NotifAction;
meta?: NotifMeta;
requiresInteraction?: boolean;
}
const HISTORY_KEY = "subfeed.notifications";
const MAX_HISTORY = 100;
const DEFAULT_TTL = 9000;
const DEFAULT_TTL = 6000;
const ERROR_TTL = 15000;
let items: Notification[] = load();
@ -90,6 +103,11 @@ export function notify(input: NotifyInput): number {
const id = counter++;
const requiresInteraction = input.requiresInteraction ?? false;
const level = input.level ?? "info";
const duration = requiresInteraction
? undefined
: level === "error" || level === "fatal"
? ERROR_TTL
: DEFAULT_TTL;
items = [
...items,
{
@ -98,17 +116,16 @@ export function notify(input: NotifyInput): number {
title: input.title,
message: input.message,
action: input.action,
meta: input.meta,
requiresInteraction,
duration,
ts: Date.now(),
read: false,
dismissed: false,
},
];
emit();
if (!requiresInteraction) {
const ttl = level === "error" || level === "fatal" ? ERROR_TTL : DEFAULT_TTL;
setTimeout(() => dismiss(id), ttl);
}
if (duration) setTimeout(() => dismiss(id), duration);
return id;
}