From c994fee45b08e59ca884995aa2810a81935f4afd Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 19 Jun 2026 16:10:08 +0200 Subject: [PATCH] fix(inbox): access-requests nudge points to Users + a jump link, no pile-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pending-access-requests notification still said "review in Settings -> Account", but that moved to the new Users page in 5a. Update the text (EN/HU/DE), and give the notification a durable inbox link ("Review" -> Users page) via a new access-requests meta kind (persists across reload, unlike a live action callback). Also stop it piling up: persisted notices reload as dismissed history, so the old dedupe never matched and a fresh copy was added every load — now removeByMetaKind drops any prior access-requests notice before re-issuing, keeping exactly one current nudge. --- frontend/src/App.tsx | 23 +++++++++++++------ .../src/components/NotificationsPanel.tsx | 14 ++++++++++- frontend/src/i18n/locales/de/common.json | 3 ++- frontend/src/i18n/locales/en/common.json | 3 ++- frontend/src/i18n/locales/hu/common.json | 3 ++- frontend/src/lib/notifications.ts | 20 +++++++++++++++- 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 35f526c..009f5a3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,7 @@ import { saveLayoutLocal, type SidebarLayout, } from "./lib/sidebarLayout"; -import { configureNotifications, getNotifSettings, notify, type NotifSettings } from "./lib/notifications"; +import { configureNotifications, getNotifSettings, notify, removeByMetaKind, type NotifSettings } from "./lib/notifications"; import { hintsEnabled, setHintsEnabled } from "./lib/hints"; import { useConfirm } from "./components/ConfirmProvider"; import type { PrefsController } from "./components/SettingsPanel"; @@ -307,12 +307,21 @@ export default function App() { if (isSupported(prefs.language)) setLanguage(prefs.language); // Nudge admins when access requests are waiting (in lieu of a server-push bell). const pending = meQuery.data?.pending_invites ?? 0; - if (meQuery.data?.role === "admin" && pending > 0) { - notify({ - level: "warning", - title: t("common.accessRequestsTitle"), - message: t("common.accessRequestsMessage", { count: pending }), - }); + if (meQuery.data?.role === "admin") { + // Keep a single, current nudge: drop any prior one (persisted copies reload as dismissed + // history, so they'd otherwise pile up one-per-reload) before re-issuing the live notice. + removeByMetaKind("access-requests"); + if (pending > 0) { + notify({ + level: "warning", + title: t("common.accessRequestsTitle"), + message: t("common.accessRequestsMessage", { count: pending }), + // Durable inbox link (survives reload, unlike a live action callback); the toast also + // gets a click via the action. + meta: { kind: "access-requests" }, + action: { label: t("common.accessRequestsReview"), onClick: () => setPage("users") }, + }); + } } // The demo account is shared: there are no subscriptions, so default it to the whole // library (the "my" feed would be empty). The communal-state warning is a permanent diff --git a/frontend/src/components/NotificationsPanel.tsx b/frontend/src/components/NotificationsPanel.tsx index bac67bf..e4883b5 100644 --- a/frontend/src/components/NotificationsPanel.tsx +++ b/frontend/src/components/NotificationsPanel.tsx @@ -1,7 +1,7 @@ import { useEffect, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, X } from "lucide-react"; +import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, UserPlus, X } from "lucide-react"; import { api, type AppNotification, type FeedFilters } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; import { @@ -190,6 +190,7 @@ export default function NotificationsPanel({ onFind={locate} onRevert={revertState} onFocusChannel={onFocusChannel} + onReviewAccess={() => setPage("users")} onRemove={() => removeClient(n.id)} /> ))} @@ -306,6 +307,7 @@ function ClientActivityRow({ onFind, onRevert, onFocusChannel, + onReviewAccess, onRemove, }: { n: ClientNotif; @@ -313,6 +315,7 @@ function ClientActivityRow({ onFind: (meta: VideoHiddenMeta | VideoWatchedMeta) => void; onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void; onFocusChannel: (name: string) => void; + onReviewAccess: () => void; onRemove: () => void; }) { const { icon: Icon, color } = LEVEL_STYLE[n.level]; @@ -321,6 +324,7 @@ function ClientActivityRow({ const watched = n.meta?.kind === "video-watched" ? n.meta : null; const subscribed: ChannelSubscribedMeta | null = n.meta?.kind === "channel-subscribed" ? n.meta : null; + const access = n.meta?.kind === "access-requests" ? n.meta : null; return (
+ ) : access ? ( + ) : ( n.action && diff --git a/frontend/src/i18n/locales/de/common.json b/frontend/src/i18n/locales/de/common.json index 70a8846..512fe96 100644 --- a/frontend/src/i18n/locales/de/common.json +++ b/frontend/src/i18n/locales/de/common.json @@ -12,7 +12,8 @@ "language": "Sprache", "somethingWrong": "Etwas ist schiefgelaufen.", "accessRequestsTitle": "Zugangsanfragen", - "accessRequestsMessage": "{{count}} ausstehend — prüfe sie unter Einstellungen → Konto.", + "accessRequestsMessage": "{{count}} ausstehend — prüfe sie auf der Benutzer-Seite.", + "accessRequestsReview": "Überprüfen", "demoTitle": "Demo-Konto", "demoSharedNotice": "Du bist im gemeinsamen Demo-Konto. Alles, was du hier tust — Playlists, ausgeblendete Videos, Einstellungen — ist gemeinsam und kann von anderen Demo-Besuchern gleichzeitig geändert werden." } diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index e282281..8082bbe 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -12,7 +12,8 @@ "language": "Language", "somethingWrong": "Something went wrong.", "accessRequestsTitle": "Access requests", - "accessRequestsMessage": "{{count}} pending — review in Settings → Account.", + "accessRequestsMessage": "{{count}} pending — review them on the Users page.", + "accessRequestsReview": "Review", "demoTitle": "Demo account", "demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time." } diff --git a/frontend/src/i18n/locales/hu/common.json b/frontend/src/i18n/locales/hu/common.json index 1389955..c004de7 100644 --- a/frontend/src/i18n/locales/hu/common.json +++ b/frontend/src/i18n/locales/hu/common.json @@ -12,7 +12,8 @@ "language": "Nyelv", "somethingWrong": "Hiba történt.", "accessRequestsTitle": "Hozzáférési kérések", - "accessRequestsMessage": "{{count}} függőben — nézd át a Beállítások → Fiók menüben.", + "accessRequestsMessage": "{{count}} függőben — nézd át a Felhasználók oldalon.", + "accessRequestsReview": "Áttekintés", "demoTitle": "Demo fiók", "demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják." } diff --git a/frontend/src/lib/notifications.ts b/frontend/src/lib/notifications.ts index cf5c47e..3df64d5 100644 --- a/frontend/src/lib/notifications.ts +++ b/frontend/src/lib/notifications.ts @@ -31,7 +31,16 @@ export type ChannelSubscribedMeta = { channelId: string; channelName: string; }; -export type NotifMeta = VideoHiddenMeta | VideoWatchedMeta | ChannelSubscribedMeta; +// Admin nudge that pending access requests are waiting — carries a durable inbox link to the +// Users page (where Access requests live). No payload; the panel provides the navigation. +export type AccessRequestsMeta = { + kind: "access-requests"; +}; +export type NotifMeta = + | VideoHiddenMeta + | VideoWatchedMeta + | ChannelSubscribedMeta + | AccessRequestsMeta; export interface Notification { id: number; @@ -173,6 +182,15 @@ export function subscribe(listener: () => void): () => void { }; } +/** Drop every notification (active or dismissed history) carrying this meta kind. Used to keep + * a singleton nudge — e.g. re-issuing the "access requests pending" notice without piling up a + * fresh copy (plus a stale history entry) on every reload. */ +export function removeByMetaKind(kind: NotifMeta["kind"]): void { + const before = items.length; + items = items.filter((n) => n.meta?.kind !== kind); + if (items.length !== before) emit(); +} + export function notify(input: NotifyInput): number { const id = counter++; const requiresInteraction = input.requiresInteraction ?? false;