/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /** @jsxRuntime automatic */ /** @jsxImportSource hono/jsx */ import {hasPermission} from '@fluxer/admin/src/AccessControlList'; import type {Archive} from '@fluxer/admin/src/api/Archives'; import {listArchives} from '@fluxer/admin/src/api/Archives'; import {getErrorMessage} from '@fluxer/admin/src/api/Errors'; import type {GuildLookupResult} from '@fluxer/admin/src/api/Guilds'; import {lookupGuild} from '@fluxer/admin/src/api/Guilds'; import {ErrorCard} from '@fluxer/admin/src/components/ErrorDisplay'; import {Layout} from '@fluxer/admin/src/components/Layout'; import {HStack} from '@fluxer/admin/src/components/ui/Layout/HStack'; import {VStack} from '@fluxer/admin/src/components/ui/Layout/VStack'; import {Heading, Text} from '@fluxer/admin/src/components/ui/Typography'; import {EmojisTab} from '@fluxer/admin/src/pages/guild_detail/tabs/EmojisTab'; import {FeaturesTab} from '@fluxer/admin/src/pages/guild_detail/tabs/FeaturesTab'; import {MembersTab} from '@fluxer/admin/src/pages/guild_detail/tabs/MembersTab'; import {ModerationTab} from '@fluxer/admin/src/pages/guild_detail/tabs/ModerationTab'; import {OverviewTab} from '@fluxer/admin/src/pages/guild_detail/tabs/OverviewTab'; import {SettingsTab} from '@fluxer/admin/src/pages/guild_detail/tabs/SettingsTab'; import {StickersTab} from '@fluxer/admin/src/pages/guild_detail/tabs/StickersTab'; import type {Session} from '@fluxer/admin/src/types/App'; import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {formatTimestamp} from '@fluxer/date_utils/src/DateFormatting'; import type {Flash} from '@fluxer/hono/src/Flash'; import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas'; import {Button} from '@fluxer/ui/src/components/Button'; import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput'; import {EmptyState} from '@fluxer/ui/src/components/EmptyState'; import {getGuildIconUrl, getInitials as getInitialsFromName} from '@fluxer/ui/src/utils/FormatUser'; import type {Child, FC} from 'hono/jsx'; interface GuildDetailPageProps { config: Config; session: Session; currentAdmin: UserAdminResponse | undefined; flash: Flash | undefined; guildId: string; referrer?: string | undefined; tab?: string | undefined; page?: string | undefined; assetVersion: string; csrfToken: string; } interface Tab { label: string; path: string; active: boolean; } function canViewArchives(adminAcls: Array): boolean { return adminAcls.some( (acl) => acl === AdminACLs.ARCHIVE_VIEW_ALL || acl === AdminACLs.ARCHIVE_TRIGGER_GUILD || acl === AdminACLs.WILDCARD, ); } function canManageAssets(adminAcls: Array): boolean { return hasPermission(adminAcls, AdminACLs.ASSET_PURGE); } function getStatusText(archive: Archive): string { if (archive.failed_at) { return 'Failed'; } if (archive.completed_at) { return 'Completed'; } return archive.progress_step ?? 'In Progress'; } const RenderTabs: FC<{config: Config; tabs: Array}> = ({config, tabs}) => { return ( ); }; const RenderArchiveTable: FC<{config: Config; archives: Array}> = ({config, archives}) => { if (archives.length === 0) { return ( ); } return ( {archives.map((archive) => ( ))}
Requested At Status Actions
{formatTimestamp(archive.requested_at)} {getStatusText(archive)} ({archive.progress_percent}%) {archive.completed_at ? ( ) : ( Pending )}
); }; const ArchivesTab: FC<{ config: Config; session: Session; guildId: string; csrfToken: string; }> = async ({config, session, guildId, csrfToken}) => { const result = await listArchives(config, session, 'guild', guildId, false); return ( Guild Archives
{result.ok ? ( ) : ( )}
); }; const RenderGuildHeader: FC<{ config: Config; guild: GuildLookupResult; }> = ({config, guild}) => { const iconUrl = getGuildIconUrl(config.mediaEndpoint, guild.id, guild.icon, true); return ( {iconUrl ? ( {guild.name} ) : ( {getInitialsFromName(guild.name)} )} {guild.name} Guild ID: {guild.id} Owner ID: {guild.owner_id} ); }; interface RenderTabContentProps { config: Config; session: Session; guild: GuildLookupResult; adminAcls: Array; guildId: string; activeTab: string; currentPage: number; assetVersion: string; csrfToken: string; } async function renderTabContent({ config, session, guild, adminAcls, guildId, activeTab, currentPage, assetVersion, csrfToken, }: RenderTabContentProps) { switch (activeTab) { case 'members': return await MembersTab({ config, session, guildId, adminAcls, page: currentPage, assetVersion, csrfToken, }); case 'settings': return ( ); case 'features': return ( ); case 'moderation': return ( ); case 'archives': return ; case 'emojis': return await EmojisTab({config, session, guildId, adminAcls, csrfToken}); case 'stickers': return await StickersTab({config, session, guildId, adminAcls, csrfToken}); default: return ; } } const RenderGuildContent: FC<{ config: Config; guild: GuildLookupResult; adminAcls: Array; guildId: string; referrer: string | undefined; activeTab: string; tabContent: Child | null; }> = ({config, guild, adminAcls, guildId, referrer, activeTab, tabContent}) => { const tabList: Array = [ { label: 'Overview', path: `/guilds/${guildId}?tab=overview`, active: activeTab === 'overview', }, { label: 'Members', path: `/guilds/${guildId}?tab=members`, active: activeTab === 'members', }, { label: 'Settings', path: `/guilds/${guildId}?tab=settings`, active: activeTab === 'settings', }, { label: 'Features', path: `/guilds/${guildId}?tab=features`, active: activeTab === 'features', }, { label: 'Moderation', path: `/guilds/${guildId}?tab=moderation`, active: activeTab === 'moderation', }, ]; if (canViewArchives(adminAcls)) { tabList.push({ label: 'Archives', path: `/guilds/${guildId}?tab=archives`, active: activeTab === 'archives', }); } if (canManageAssets(adminAcls)) { tabList.push({ label: 'Emojis', path: `/guilds/${guildId}?tab=emojis`, active: activeTab === 'emojis', }); tabList.push({ label: 'Stickers', path: `/guilds/${guildId}?tab=stickers`, active: activeTab === 'stickers', }); } return ( Back to Guilds {tabContent} ); }; const RenderNotFoundContent: FC<{config: Config}> = ({config}) => { return ( Guild Not Found The requested guild could not be found. ); }; const RenderApiError: FC<{config: Config; errorMessage: string}> = ({config, errorMessage}) => { return ( Error {errorMessage} ); }; export async function GuildDetailPage({ config, session, currentAdmin, flash, guildId, referrer, tab, page, assetVersion, csrfToken, }: GuildDetailPageProps) { const result = await lookupGuild(config, session, guildId); const adminAcls = currentAdmin?.acls ?? []; let activeTab = tab ?? 'overview'; const validTabs = ['overview', 'settings', 'features', 'moderation', 'members', 'archives', 'emojis', 'stickers']; if (!validTabs.includes(activeTab)) { activeTab = 'overview'; } if (activeTab === 'archives' && !canViewArchives(adminAcls)) { activeTab = 'overview'; } if ((activeTab === 'emojis' || activeTab === 'stickers') && !canManageAssets(adminAcls)) { activeTab = 'overview'; } let currentPage = 0; if (page) { const parsed = parseInt(page, 10); if (!Number.isNaN(parsed) && parsed >= 0) { currentPage = parsed; } } if (!result.ok) { return ( ); } if (!result.data) { return ( ); } const guild = result.data; const tabContent = await renderTabContent({ config, session, guild, adminAcls, guildId, activeTab, currentPage, assetVersion, csrfToken, }); return ( ); }