diff --git a/apps/web/src/components/admin-allow-list-channel-list.tsx b/apps/web/src/components/admin-allow-list-channel-list.tsx index f58e0ee..627385d 100644 --- a/apps/web/src/components/admin-allow-list-channel-list.tsx +++ b/apps/web/src/components/admin-allow-list-channel-list.tsx @@ -35,7 +35,7 @@ export function AdminAllowListChannelList({ onRemove, }: Props) { return ( -
+

{title}

@@ -57,7 +57,7 @@ export function AdminAllowListChannelList({ return (
+

{title}

{description}

@@ -61,7 +61,7 @@ export function AdminAllowListForm({ title, description, trustedUrls, pending, o {channels.slice(0, 8).map((channel) => { const alreadyAdded = isTrusted(channel, trusted); return ( -
+
+

{title}

@@ -51,7 +51,7 @@ export function AdminAllowListPlaylistList({ title, playlists, onRemove }: Props return (
+

{title}

{description}

@@ -108,7 +108,7 @@ export function AdminAllowListPlaylistSearch({ {playlists.slice(0, 8).map((playlist) => { const alreadyAdded = added.has(playlist.url); return ( -
+
+
-
-
+
+

Entire instance

Restrict everyone to the admin allow list, including guests.

-
+
- + void; }; -export function AdminAllowListUserDetail({ user, onToast }: Props) { +function accessState(user: AdminAllowListUser, instanceRestricted: boolean) { + if (user.accessMode === "allow_list") { + return { + label: "User-specific allow-list", + action: instanceRestricted ? "Set unrestricted override" : "Unrestrict user", + nextMode: "unrestricted" as const, + toast: instanceRestricted ? "Unrestricted override set" : "User unrestricted", + active: true, + }; + } + if (instanceRestricted && user.adminManagedAccessMode) { + return { + label: "Admin unrestricted override", + action: "Restrict user", + nextMode: "allow_list" as const, + toast: "User restricted", + active: true, + }; + } + if (instanceRestricted) { + return { + label: "Restricted by entire instance", + action: "Set unrestricted override", + nextMode: "unrestricted" as const, + toast: "Unrestricted override set", + active: false, + }; + } + return { + label: "Unrestricted", + action: "Restrict user", + nextMode: "allow_list" as const, + toast: "User restricted", + active: false, + }; +} + +export function AdminAllowListUserDetail({ user, instanceRestricted, onToast }: Props) { const detail = useAdminUserAllowList(user.id); const mutations = useAdminAllowListMutations(user.id); const data = detail.data; - const selected = data?.user ?? user; - const restricted = selected.accessMode === "allow_list"; + const selected = data?.user + ? { + ...user, + ...data.user, + adminManagedAccessMode: data.user.adminManagedAccessMode ?? user.adminManagedAccessMode, + avatarUrl: data.user.avatarUrl ?? user.avatarUrl, + avatarType: data.user.avatarType ?? user.avatarType, + avatarCode: data.user.avatarCode ?? user.avatarCode, + } + : user; + const state = accessState(selected, instanceRestricted); function toggleMode() { - const accessMode = restricted ? "unrestricted" : "allow_list"; mutations.userMode.mutate( - { id: selected.id, accessMode }, + { id: selected.id, accessMode: state.nextMode }, { - onSuccess: () => - onToast(accessMode === "allow_list" ? "User restricted" : "User unrestricted"), + onSuccess: () => onToast(state.toast), onError: (error) => onToast(error instanceof Error ? error.message : "Unable to update user"), }, @@ -43,9 +88,9 @@ export function AdminAllowListUserDetail({ user, onToast }: Props) { } return ( -
+
-
+

{selected.email}

+

{state.label}

diff --git a/apps/web/src/components/admin-allow-list-users.tsx b/apps/web/src/components/admin-allow-list-users.tsx index 14606e8..c57d52b 100644 --- a/apps/web/src/components/admin-allow-list-users.tsx +++ b/apps/web/src/components/admin-allow-list-users.tsx @@ -1,30 +1,53 @@ -import { useEffect, useState } from "react"; -import { useAdminUserSearch } from "../hooks/use-admin-granular-allow-list"; +import { useState } from "react"; +import { useAdminAllowListUsers, useAdminUserSearch } from "../hooks/use-admin-granular-allow-list"; import type { AdminAllowListUser } from "../types/allow-list"; import { AdminAllowListUserDetail } from "./admin-allow-list-user-detail"; +import { AdminUserAvatar } from "./admin-user-avatar"; type Props = { enabled: boolean; + instanceRestricted: boolean; onToast: (message: string) => void; }; -export function AdminAllowListUsers({ enabled, onToast }: Props) { +function avatarUser(user: AdminAllowListUser) { + return { + ...user, + role: "user" as const, + publicUsername: null, + bio: null, + avatarUrl: user.avatarUrl ?? null, + avatarType: user.avatarType ?? null, + avatarCode: user.avatarCode ?? null, + suspended: false, + verified: false, + createdAt: 0, + }; +} + +function accessLabel(user: AdminAllowListUser, instanceRestricted: boolean): string { + if (user.accessMode === "allow_list") return "User restricted"; + if (!instanceRestricted) return "Unrestricted"; + return user.adminManagedAccessMode ? "Admin override" : "Instance restricted"; +} + +export function AdminAllowListUsers({ enabled, instanceRestricted, onToast }: Props) { const [search, setSearch] = useState(""); const [selected, setSelected] = useState(null); - const query = useAdminUserSearch(search, enabled); - const users = query.data ?? []; - - useEffect(() => { - if (!selected) return; - if (users.some((user) => user.id === selected.id)) return; - setSelected(null); - }, [selected, users]); + const searching = search.trim().length >= 2; + const managedQuery = useAdminAllowListUsers(enabled && !searching); + const searchQuery = useAdminUserSearch(search, enabled && searching); + const managedUsers = managedQuery.data?.pages.flatMap((page) => page.items) ?? []; + const visibleUsers = searching ? (searchQuery.data ?? []) : managedUsers; + const loading = searching ? searchQuery.isLoading : managedQuery.isLoading; return ( -
+

Specific users

-

Search a user, then configure only that account.

+

+ Users with admin-managed access appear here. Search to configure another account. +

- {search.trim().length < 2 ? ( -
Type at least two characters.
- ) : query.isLoading ? ( + {loading ? (
Searching users...
- ) : users.length === 0 ? ( -
No users found.
+ ) : visibleUsers.length === 0 ? ( +
+ {searching ? "No users found." : "No users are managed by allow-list rules yet."} +
) : (
- {users.map((user) => ( + {visibleUsers.map((user) => ( ))}
)}
+ {!searching && managedQuery.hasNextPage && ( + + )} {selected && (
- +
)}
diff --git a/apps/web/src/components/admin-console-nav.tsx b/apps/web/src/components/admin-console-nav.tsx index f0a6e6d..1c98beb 100644 --- a/apps/web/src/components/admin-console-nav.tsx +++ b/apps/web/src/components/admin-console-nav.tsx @@ -13,7 +13,7 @@ type Props = { export function AdminConsoleNav({ items, active, onSelect }: Props) { return ( -