fix(inbox): access-requests nudge points to Users + a jump link, no pile-up
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.
This commit is contained in:
parent
105505f67e
commit
ec78c48a17
6 changed files with 54 additions and 12 deletions
|
|
@ -17,7 +17,7 @@ import {
|
||||||
saveLayoutLocal,
|
saveLayoutLocal,
|
||||||
type SidebarLayout,
|
type SidebarLayout,
|
||||||
} from "./lib/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 { hintsEnabled, setHintsEnabled } from "./lib/hints";
|
||||||
import { useConfirm } from "./components/ConfirmProvider";
|
import { useConfirm } from "./components/ConfirmProvider";
|
||||||
import type { PrefsController } from "./components/SettingsPanel";
|
import type { PrefsController } from "./components/SettingsPanel";
|
||||||
|
|
@ -307,13 +307,22 @@ export default function App() {
|
||||||
if (isSupported(prefs.language)) setLanguage(prefs.language);
|
if (isSupported(prefs.language)) setLanguage(prefs.language);
|
||||||
// Nudge admins when access requests are waiting (in lieu of a server-push bell).
|
// Nudge admins when access requests are waiting (in lieu of a server-push bell).
|
||||||
const pending = meQuery.data?.pending_invites ?? 0;
|
const pending = meQuery.data?.pending_invites ?? 0;
|
||||||
if (meQuery.data?.role === "admin" && pending > 0) {
|
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({
|
notify({
|
||||||
level: "warning",
|
level: "warning",
|
||||||
title: t("common.accessRequestsTitle"),
|
title: t("common.accessRequestsTitle"),
|
||||||
message: t("common.accessRequestsMessage", { count: pending }),
|
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
|
// 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
|
// library (the "my" feed would be empty). The communal-state warning is a permanent
|
||||||
// banner (see DemoBanner below), not a toast that re-pops on every reload.
|
// banner (see DemoBanner below), not a toast that re-pops on every reload.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useSyncExternalStore } from "react";
|
import { useEffect, useSyncExternalStore } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
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 { api, type AppNotification, type FeedFilters } from "../lib/api";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
import {
|
import {
|
||||||
|
|
@ -190,6 +190,7 @@ export default function NotificationsPanel({
|
||||||
onFind={locate}
|
onFind={locate}
|
||||||
onRevert={revertState}
|
onRevert={revertState}
|
||||||
onFocusChannel={onFocusChannel}
|
onFocusChannel={onFocusChannel}
|
||||||
|
onReviewAccess={() => setPage("users")}
|
||||||
onRemove={() => removeClient(n.id)}
|
onRemove={() => removeClient(n.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -306,6 +307,7 @@ function ClientActivityRow({
|
||||||
onFind,
|
onFind,
|
||||||
onRevert,
|
onRevert,
|
||||||
onFocusChannel,
|
onFocusChannel,
|
||||||
|
onReviewAccess,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
n: ClientNotif;
|
n: ClientNotif;
|
||||||
|
|
@ -313,6 +315,7 @@ function ClientActivityRow({
|
||||||
onFind: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
|
onFind: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
|
||||||
onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
|
onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
|
||||||
onFocusChannel: (name: string) => void;
|
onFocusChannel: (name: string) => void;
|
||||||
|
onReviewAccess: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { icon: Icon, color } = LEVEL_STYLE[n.level];
|
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 watched = n.meta?.kind === "video-watched" ? n.meta : null;
|
||||||
const subscribed: ChannelSubscribedMeta | null =
|
const subscribed: ChannelSubscribedMeta | null =
|
||||||
n.meta?.kind === "channel-subscribed" ? n.meta : null;
|
n.meta?.kind === "channel-subscribed" ? n.meta : null;
|
||||||
|
const access = n.meta?.kind === "access-requests" ? n.meta : null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`glass rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
|
className={`glass rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
|
||||||
|
|
@ -390,6 +394,14 @@ function ClientActivityRow({
|
||||||
{t("notifications.openOnYouTube")}
|
{t("notifications.openOnYouTube")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
) : access ? (
|
||||||
|
<button
|
||||||
|
onClick={() => onReviewAccess()}
|
||||||
|
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline mt-1"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-3.5 h-3.5" />
|
||||||
|
{t("common.accessRequestsReview")}
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
n.action &&
|
n.action &&
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"somethingWrong": "Etwas ist schiefgelaufen.",
|
"somethingWrong": "Etwas ist schiefgelaufen.",
|
||||||
"accessRequestsTitle": "Zugangsanfragen",
|
"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",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"somethingWrong": "Something went wrong.",
|
"somethingWrong": "Something went wrong.",
|
||||||
"accessRequestsTitle": "Access requests",
|
"accessRequestsTitle": "Access requests",
|
||||||
"accessRequestsMessage": "{{count}} pending — review in Settings → Account.",
|
"accessRequestsMessage": "{{count}} pending — review them on the Users page.",
|
||||||
|
"accessRequestsReview": "Review",
|
||||||
"demoTitle": "Demo account",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"somethingWrong": "Hiba történt.",
|
"somethingWrong": "Hiba történt.",
|
||||||
"accessRequestsTitle": "Hozzáférési kérések",
|
"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",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,16 @@ export type ChannelSubscribedMeta = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelName: 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 {
|
export interface Notification {
|
||||||
id: number;
|
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 {
|
export function notify(input: NotifyInput): number {
|
||||||
const id = counter++;
|
const id = counter++;
|
||||||
const requiresInteraction = input.requiresInteraction ?? false;
|
const requiresInteraction = input.requiresInteraction ?? false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue