From a36e9b9124c6685c534482be051eff281afc761d Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 18 Jun 2026 04:37:20 +0200 Subject: [PATCH] feat(notifications): unify the two rail indicators into one inbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold the client-side transient bell into the inbox page so there's a single notification indicator. The inbox now has two groups — "System" (durable, server-backed) and "Activity" (client-side events with their Unhide/Unwatch/ Find-in-feed actions) — and the nav badge sums both unread counts. The separate rail bell (NotificationCenter) is removed. Activity items get a per-item clear (X) alongside the global Clear all. Unhiding or unwatching a video from anywhere — a card, the toast's Undo, or the inbox — now quietly resolves the original "Hidden/Watched X" entry (no duplicate "Unhidden X" toast, no stale entry with a dead action), via a new resolveVideo store helper. --- frontend/src/App.tsx | 4 +- frontend/src/components/Feed.tsx | 6 +- frontend/src/components/NavSidebar.tsx | 16 +- .../src/components/NotificationsPanel.tsx | 240 ++++++++++++++++-- frontend/src/i18n/locales/de/inbox.json | 2 + frontend/src/i18n/locales/en/inbox.json | 2 + frontend/src/i18n/locales/hu/inbox.json | 2 + frontend/src/lib/notifications.ts | 15 ++ 8 files changed, 246 insertions(+), 41 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ccfe17a..037cc89 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -252,8 +252,6 @@ export default function App() { onOpenAbout={() => setAboutOpen(true)} onChangeLanguage={changeLanguage} language={i18n.language as LangCode} - filters={filters} - setFilters={setFilters} />
) : page === "notifications" ? ( - + ) : page === "settings" ? ( onState(id, "new") }, meta: { kind: "video-watched", videoId: id, title: v?.title ?? "this video" }, }); + } else if (status === "new") { + // Unhide / unwatch (from a card, the toast's Undo, or the center): quietly resolve any + // stale hide/watch notice for this video so it doesn't linger with a dead action. + resolveVideo(id); } }, [qc] diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index f862adb..cef12ec 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; @@ -17,13 +17,13 @@ import { Tv, UserPlus, } from "lucide-react"; -import { api, type FeedFilters, type Me } from "../lib/api"; +import { api, type Me } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; +import { getUnreadCount, subscribe } from "../lib/notifications"; import type { Page } from "../lib/urlState"; import { type LangCode } from "../i18n"; import AvatarImg from "./Avatar"; import LanguageSwitcher from "./LanguageSwitcher"; -import NotificationCenter from "./NotificationCenter"; // Primary app navigation: a collapsible left rail with icon+label entries (Design C). The // modules used to hide under the avatar dropdown; they now live here. Collapsed, it becomes @@ -35,8 +35,6 @@ export default function NavSidebar({ onOpenAbout, onChangeLanguage, language, - filters, - setFilters, }: { me: Me; page: Page; @@ -44,8 +42,6 @@ export default function NavSidebar({ onOpenAbout: () => void; onChangeLanguage: (code: LangCode) => void; language: LangCode; - filters: FeedFilters; - setFilters: (f: FeedFilters) => void; }) { const { t } = useTranslation(); const [collapsed, setCollapsed] = useState( @@ -125,7 +121,10 @@ export default function NavSidebar({ api.notificationUnreadCount, { intervalMs: 30000 } ); - const unread = unreadQuery.data?.count ?? 0; + // One indicator for both layers: durable server notifications + the client-side transient + // events (the former separate bell is now folded into the inbox page). + const clientUnread = useSyncExternalStore(subscribe, getUnreadCount, getUnreadCount); + const unread = (unreadQuery.data?.count ?? 0) + clientUnread; type NavItem = { page: Page; icon: typeof Home; label: string; badge?: number }; // User-facing content modules vs. admin/system modules, separated by a divider in the rail. @@ -243,7 +242,6 @@ export default function NavSidebar({ > -
+ + + ) : watched ? ( +
+ +
+ ) : ( + + n.action && + !n.dismissed && ( + + ) + )} + + + + ); +} diff --git a/frontend/src/i18n/locales/de/inbox.json b/frontend/src/i18n/locales/de/inbox.json index 807e91f..734869f 100644 --- a/frontend/src/i18n/locales/de/inbox.json +++ b/frontend/src/i18n/locales/de/inbox.json @@ -7,6 +7,8 @@ "clearAll": "Alle löschen", "markRead": "Als gelesen markieren", "dismiss": "Verwerfen", + "sectionSystem": "System", + "sectionActivity": "Aktivität", "andMore": "und {{count}} weitere", "maintenance": { "title": "Videos entfernt", diff --git a/frontend/src/i18n/locales/en/inbox.json b/frontend/src/i18n/locales/en/inbox.json index 89d2fd8..8d7c7a6 100644 --- a/frontend/src/i18n/locales/en/inbox.json +++ b/frontend/src/i18n/locales/en/inbox.json @@ -7,6 +7,8 @@ "clearAll": "Clear all", "markRead": "Mark read", "dismiss": "Dismiss", + "sectionSystem": "System", + "sectionActivity": "Activity", "andMore": "and {{count}} more", "maintenance": { "title": "Videos removed", diff --git a/frontend/src/i18n/locales/hu/inbox.json b/frontend/src/i18n/locales/hu/inbox.json index 9f144d3..3f6e6d8 100644 --- a/frontend/src/i18n/locales/hu/inbox.json +++ b/frontend/src/i18n/locales/hu/inbox.json @@ -7,6 +7,8 @@ "clearAll": "Összes törlése", "markRead": "Olvasottnak jelöl", "dismiss": "Elvetés", + "sectionSystem": "Rendszer", + "sectionActivity": "Tevékenység", "andMore": "és még {{count}}", "maintenance": { "title": "Videók eltávolítva", diff --git a/frontend/src/lib/notifications.ts b/frontend/src/lib/notifications.ts index 530ce68..184d708 100644 --- a/frontend/src/lib/notifications.ts +++ b/frontend/src/lib/notifications.ts @@ -218,6 +218,21 @@ export function clearAll(): void { emit(); } +/** Remove a single history entry entirely (per-item clear in the notification center). */ +export function remove(id: number): void { + const before = items.length; + items = items.filter((n) => n.id !== id); + if (items.length !== before) emit(); +} + +/** Resolve any history entries tied to a video — called when it's unhidden/unwatched so the + * now-stale "Hidden/Watched X" notice disappears instead of lingering with a dead action. */ +export function resolveVideo(videoId: string): void { + const before = items.length; + items = items.filter((n) => n.meta?.videoId !== videoId); + if (items.length !== before) emit(); +} + export const getActiveToasts = (): Notification[] => cachedActive; export const getNotifications = (): Notification[] => cachedReversed; export const getUnreadCount = (): number => cachedUnread;