+
+
+
+
{t("inbox.title")}
+
{t("inbox.subtitle")}
+
+ {items.length > 0 && (
+
+
+
+
+ )}
+
+
+ {q.isLoading && !q.data ? (
+
{t("common.loading")}
+ ) : items.length === 0 ? (
+
+
+ {t("inbox.empty")}
+
+ ) : (
+
+ {items.map((n) => (
+ readMut.mutate(n.id)}
+ onDismiss={() => dismissMut.mutate(n.id)}
+ />
+ ))}
+
+ )}
+
+ );
+}
+
+function NotificationRow({
+ n,
+ t,
+ onRead,
+ onDismiss,
+}: {
+ n: AppNotification;
+ t: (k: string, o?: any) => string;
+ onRead: () => void;
+ onDismiss: () => void;
+}) {
+ // The maintenance producer ships the affected videos in `data.videos`; list a few titles.
+ const videos: { id: string; title: string | null }[] = Array.isArray(n.data?.videos)
+ ? n.data!.videos
+ : [];
+ // Known notification types are rendered from i18n (so they're trilingual) using the typed
+ // payload; the server-stored title/body are an English fallback for any unknown type.
+ const isMaintenance = n.type === "maintenance" && typeof n.data?.count === "number";
+ const title = isMaintenance ? t("inbox.maintenance.title") : n.title;
+ const body = isMaintenance
+ ? t("inbox.maintenance.body", { count: n.data!.count })
+ : n.body;
+ return (
+
+ {!n.read &&
}
+
+
+ {title}
+
+ {relativeTime(n.created_at, t)}
+
+
+ {body &&
{body}
}
+ {videos.length > 0 && (
+
+ {videos.slice(0, 8).map((v) => (
+ -
+ {v.title || v.id}
+
+ ))}
+ {videos.length > 8 && (
+ -
+ {t("inbox.andMore", { count: videos.length - 8 })}
+
+ )}
+
+ )}
+
+
+ {!n.read && (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/i18n/locales/de/inbox.json b/frontend/src/i18n/locales/de/inbox.json
new file mode 100644
index 0000000..39a2ddb
--- /dev/null
+++ b/frontend/src/i18n/locales/de/inbox.json
@@ -0,0 +1,16 @@
+{
+ "navLabel": "Benachrichtigungen",
+ "title": "Benachrichtigungen",
+ "subtitle": "Neuigkeiten aus deiner Bibliothek und vom System.",
+ "empty": "Alles erledigt.",
+ "markAllRead": "Alle als gelesen markieren",
+ "clearAll": "Alle löschen",
+ "markRead": "Als gelesen markieren",
+ "dismiss": "Verwerfen",
+ "andMore": "und {{count}} weitere",
+ "maintenance": {
+ "title": "Videos entfernt",
+ "body_one": "{{count}} gespeichertes oder in einer Playlist befindliches Video wurde entfernt, weil es auf YouTube nicht mehr verfügbar ist.",
+ "body_other": "{{count}} gespeicherte oder in Playlists befindliche Videos wurden entfernt, weil sie auf YouTube nicht mehr verfügbar sind."
+ }
+}
diff --git a/frontend/src/i18n/locales/en/inbox.json b/frontend/src/i18n/locales/en/inbox.json
new file mode 100644
index 0000000..7f972a4
--- /dev/null
+++ b/frontend/src/i18n/locales/en/inbox.json
@@ -0,0 +1,16 @@
+{
+ "navLabel": "Notifications",
+ "title": "Notifications",
+ "subtitle": "Updates from your library and the system.",
+ "empty": "You're all caught up.",
+ "markAllRead": "Mark all read",
+ "clearAll": "Clear all",
+ "markRead": "Mark read",
+ "dismiss": "Dismiss",
+ "andMore": "and {{count}} more",
+ "maintenance": {
+ "title": "Videos removed",
+ "body_one": "{{count}} saved or playlisted video was removed because it's no longer available on YouTube.",
+ "body_other": "{{count}} saved or playlisted videos were removed because they're no longer available on YouTube."
+ }
+}
diff --git a/frontend/src/i18n/locales/hu/inbox.json b/frontend/src/i18n/locales/hu/inbox.json
new file mode 100644
index 0000000..9167954
--- /dev/null
+++ b/frontend/src/i18n/locales/hu/inbox.json
@@ -0,0 +1,16 @@
+{
+ "navLabel": "Értesítések",
+ "title": "Értesítések",
+ "subtitle": "Frissítések a könyvtáradból és a rendszertől.",
+ "empty": "Nincs új értesítés.",
+ "markAllRead": "Összes olvasott",
+ "clearAll": "Összes törlése",
+ "markRead": "Olvasottnak jelöl",
+ "dismiss": "Elvetés",
+ "andMore": "és még {{count}}",
+ "maintenance": {
+ "title": "Videók eltávolítva",
+ "body_one": "{{count}} mentett vagy lejátszási listás videó eltávolítva, mert már nem elérhető a YouTube-on.",
+ "body_other": "{{count}} mentett vagy lejátszási listás videó eltávolítva, mert már nem elérhetők a YouTube-on."
+ }
+}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 12d4d82..006a834 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -356,6 +356,19 @@ export interface Account {
active: boolean;
}
+// A durable, server-backed notification (the inbox center). Distinct from the client-side
+// transient toast/bell, which lives only in localStorage.
+export interface AppNotification {
+ id: number;
+ type: string;
+ title: string;
+ body: string | null;
+ data: Record