// SPDX-License-Identifier: AGPL-3.0-or-later import {Routes} from '@app/app/Routes'; import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal'; import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals'; import {TopNagbarContext} from '@app/features/app/components/layout/app_layout/TopNagbarContext'; import styles from '@app/features/app/components/layout/GuildLayout.module.css'; import {GuildNavbar} from '@app/features/app/components/layout/GuildNavbar'; import {Nagbar} from '@app/features/app/components/layout/Nagbar'; import {NagbarButton} from '@app/features/app/components/layout/NagbarButton'; import {GuildSidebarSkeleton} from '@app/features/app/components/skeleton/GuildSidebarSkeleton'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import Accounts from '@app/features/auth/state/Accounts'; import {MatureContentChannelGate} from '@app/features/channel/components/MatureContentChannelGate'; import Channels from '@app/features/channel/state/Channels'; import * as GuildCommands from '@app/features/guild/commands/GuildCommands'; import GuildAvailability from '@app/features/guild/state/GuildAvailability'; import GuildMatureContentAgree, {MatureContentGateReason} from '@app/features/guild/state/GuildMatureContentAgree'; import Guilds from '@app/features/guild/state/Guilds'; import { CANCEL_DESCRIPTOR, DISMISS_DESCRIPTOR, ENABLE_TWO_FACTOR_AUTH_DESCRIPTOR, } from '@app/features/i18n/utils/CommonMessageDescriptors'; import * as InviteUtils from '@app/features/invite/utils/InviteUtils'; import {adminUrl} from '@app/features/messaging/utils/MessagingUrlUtils'; import Navigation from '@app/features/navigation/state/Navigation'; import SelectedChannel from '@app/features/navigation/state/SelectedChannel'; import Permission from '@app/features/permissions/state/Permission'; import * as PermissionUtils from '@app/features/permissions/utils/PermissionUtils'; import {useParams} from '@app/features/platform/components/router/RouterReact'; import {ComponentBus} from '@app/features/platform/utils/ComponentBus'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import * as NagbarCommands from '@app/features/ui/commands/NagbarCommands'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import NagbarState from '@app/features/ui/state/Nagbar'; import {openExternalUrl} from '@app/features/ui/utils/NativeUtils'; import Users from '@app/features/user/state/Users'; import VoiceCallFullscreen from '@app/features/voice/state/VoiceCallFullscreen'; import {ElevatedPermissions, GUILD_TEXT_BASED_CHANNEL_TYPES, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {GuildFeatures, GuildMFALevel} from '@fluxer/constants/src/GuildConstants'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {type Icon, NetworkSlashIcon, SmileySadIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useContext, useEffect, useMemo, useRef} from 'react'; const ENABLE_INVITES_FOR_THIS_COMMUNITY_DESCRIPTOR = msg({ message: 'Enable invites for this community', comment: 'Confirmation modal title for re-enabling invite links in the current community.', }); const ENABLE_DESCRIPTOR = msg({ message: 'Enable', comment: 'Primary button label in the enable-invites confirmation modal.', }); const ENABLE_INVITES_CONFIRM_DESCRIPTION_DESCRIPTOR = msg({ message: 'Are you sure you want to enable invites? This will allow users to join this community through invite links again.', comment: 'Confirmation modal body shown before re-enabling invite links in the current community.', }); const INVITES_DISABLED_RAID_MESSAGE_DESCRIPTOR = msg({ message: 'Invites to {communityName} are currently disabled because {productName} detected a potential raid. New people cannot join right now.', comment: 'Guild nagbar body shown when invite links are paused because raid detection is active. {communityName} is the community name and {productName} is Fluxer.', }); const INVITES_DISABLED_MESSAGE_DESCRIPTOR = msg({ message: 'Invites to {communityName} are currently disabled', comment: 'Guild nagbar body shown when invite links are paused in the current community.', }); const ENABLE_INVITES_AGAIN_DESCRIPTOR = msg({ message: 'Enable invites again', comment: 'Button label on the invites-disabled guild nagbar. Opens a confirmation to re-enable invites.', }); const GUILD_MFA_REQUIREMENT_MESSAGE_DESCRIPTOR = msg({ message: 'Moderation actions in {communityName} require two-factor authentication. Enable 2FA to kick, ban, time out, or delete messages here.', comment: 'Guild nagbar body shown when moderation actions in a community require two-factor authentication. {communityName} is the community name.', }); const STAFF_ONLY_GUILD_MESSAGE_DESCRIPTOR = msg({ message: '{communityName} is currently only accessible to {productName} staff members', comment: 'Guild nagbar body shown for staff-only communities. {communityName} is the community name and {productName} is Fluxer.', }); const MANAGE_COMMUNITY_FEATURES_DESCRIPTOR = msg({ message: 'Manage community features', comment: 'Button label on the staff-only community nagbar. Opens the admin feature-management page.', }); const COMMUNITY_TEMPORARILY_UNAVAILABLE_DESCRIPTOR = msg({ message: 'Community temporarily unavailable', comment: 'Short label in the app layout guild layout.', }); const SOMETHING_WENT_WRONG_WE_RE_WORKING_ON_IT_DESCRIPTOR = msg({ message: "Something went wrong. We're working on it.", comment: 'Community layout error-boundary body shown when rendering the community pane crashes. Keep plain and calm.', }); const THIS_IS_NOT_THE_COMMUNITY_YOU_RE_LOOKING_DESCRIPTOR = msg({ message: "This is not the community you're looking for.", comment: 'Body text in the app layout guild layout.', }); const THE_COMMUNITY_YOU_RE_LOOKING_FOR_MAY_HAVE_DESCRIPTOR = msg({ message: "The community you're looking for may have been deleted or you may not have access to it.", comment: 'Community layout empty-state body shown when the active community is missing (deleted or no longer accessible).', }); const NO_ACCESSIBLE_CHANNELS_DESCRIPTOR = msg({ message: 'No accessible channels', comment: 'Short label in the app layout guild layout.', }); const YOU_DON_T_HAVE_ACCESS_TO_ANY_CHANNELS_DESCRIPTOR = msg({ message: "You don't have access to any channels in this community.", comment: 'Community layout empty-state body shown when the user has joined the community but cannot see any channels.', }); const InvitesDisabledNagbar = observer(({isMobile, guildId}: {isMobile: boolean; guildId: string}) => { const {i18n} = useLingui(); const guild = Guilds.getGuild(guildId); const selectedChannelId = SelectedChannel.currentChannelId; const canManageGuild = selectedChannelId ? Permission.can(Permissions.MANAGE_GUILD, {guildId}) : false; const isRaidDetected = guild?.features.has(GuildFeatures.RAID_DETECTED) ?? false; if (!guild) return null; const handleEnableInvites = () => { ModalCommands.push( modal(() => ( { await GuildCommands.toggleFeature(guildId, GuildFeatures.INVITES_DISABLED, false); }} data-flx="app.guild-layout.handle-enable-invites.confirm-modal" /> )), ); }; const handleDismiss = () => { NagbarCommands.dismissInvitesDisabledNagbar(guildId); }; return (

{isRaidDetected ? i18n._(INVITES_DISABLED_RAID_MESSAGE_DESCRIPTOR, { communityName: guild.name, productName: RuntimeConfig.productName, }) : i18n._(INVITES_DISABLED_MESSAGE_DESCRIPTOR, {communityName: guild.name})}

{isMobile && ( {i18n._(DISMISS_DESCRIPTOR)} )} {canManageGuild && ( {i18n._(ENABLE_INVITES_AGAIN_DESCRIPTOR)} )}
); }); const GuildMfaRequirementNagbar = observer(({isMobile, guildId}: {isMobile: boolean; guildId: string}) => { const {i18n} = useLingui(); const guild = Guilds.getGuild(guildId); if (!guild) return null; const handleEnableMfa = () => { ModalCommands.push( modal( () => ( ), 'user-settings', ), ); }; const handleDismiss = () => { NagbarCommands.dismissGuildMfaRequirementNagbar(guildId); }; return (

{i18n._(GUILD_MFA_REQUIREMENT_MESSAGE_DESCRIPTOR, {communityName: guild.name})}

{isMobile && ( {i18n._(DISMISS_DESCRIPTOR)} )} {i18n._(ENABLE_TWO_FACTOR_AUTH_DESCRIPTOR)}
); }); const StaffOnlyGuildNagbar = observer(({isMobile, guildId}: {isMobile: boolean; guildId: string}) => { const {i18n} = useLingui(); const guild = Guilds.getGuild(guildId); if (!guild) return null; const handleManageFeatures = () => { const featuresUrl = new URL(adminUrl(`guilds/${guildId}`)); featuresUrl.searchParams.set('tab', 'features'); void openExternalUrl(featuresUrl.toString()); }; return (

{i18n._(STAFF_ONLY_GUILD_MESSAGE_DESCRIPTOR, { communityName: guild.name, productName: RuntimeConfig.productName, })}

{i18n._(MANAGE_COMMUNITY_FEATURES_DESCRIPTOR)}
); }); const GuildUnavailable = observer(function GuildUnavailable({ icon: Icon, title, description, }: { icon: Icon; title: string; description: string; }) { return (

{title}

{description}

); }); export const GuildLayout = observer(({children}: {children: React.ReactNode}) => { const {i18n} = useLingui(); const {guildId, channelId} = useParams() as {guildId: string; channelId?: string}; const mobileLayout = MobileLayout; const guild = Guilds.getGuild(guildId); const unavailableGuilds = GuildAvailability.unavailableGuilds; const channels = Channels.getGuildChannels(guildId); const user = Users.currentUser; const nagbarState = NagbarState; const selectedChannelId = SelectedChannel.currentChannelId; const channel = Channels.getChannel(selectedChannelId ?? ''); const isStaff = user?.isStaff() ?? false; const invitesDisabledDismissed = NagbarState.getInvitesDisabledDismissed(guild?.id ?? ''); const guildMfaRequirementDismissed = NagbarState.getGuildMfaRequirementDismissed(guild?.id ?? ''); const hasMfaGatedGuildPermissions = guild && user ? (PermissionUtils.computePermissions(user, guild.toJSON(), undefined, undefined, false) & ElevatedPermissions) !== PermissionUtils.NONE : false; const guildUnavailable = guildId && (unavailableGuilds.has(guildId) || guild?.unavailable); const viewLive = Accounts.isViewLive; const guildNotFound = viewLive && !guildUnavailable && !guild; const firstAccessibleTextChannel = useMemo(() => { if (!guild) return null; for (const ch of channels) { if (GUILD_TEXT_BASED_CHANNEL_TYPES.has(ch.type)) { return ch; } } return null; }, [guild, channels]); const shouldShowInvitesDisabled = useMemo(() => { if (!selectedChannelId) return false; if (!channel?.guildId) return false; if (!guild) return false; if (nagbarState.forceHideInvitesDisabled) return false; if (nagbarState.forceInvitesDisabled) return true; const hasInvitesDisabled = guild.features.has(GuildFeatures.INVITES_DISABLED); if (!hasInvitesDisabled) return false; const canInvite = InviteUtils.canInviteToChannel(selectedChannelId, channel.guildId); const canManageGuild = Permission.can(Permissions.MANAGE_GUILD, {guildId: channel.guildId}); if (!canInvite && !canManageGuild) return false; if (invitesDisabledDismissed && !nagbarState.forceInvitesDisabled) return false; return true; }, [ selectedChannelId, channel, guild, invitesDisabledDismissed, nagbarState.forceInvitesDisabled, nagbarState.forceHideInvitesDisabled, ]); const shouldShowStaffOnlyGuild = useMemo(() => { if (!selectedChannelId) return false; if (!channel?.guildId) return false; if (!guild) return false; if (!isStaff) return false; const isStaffOnly = guild.features.has(GuildFeatures.UNAVAILABLE_FOR_EVERYONE_BUT_STAFF); return isStaffOnly; }, [selectedChannelId, channel, guild, isStaff]); const shouldShowGuildMfaRequirement = useMemo(() => { if (nagbarState.forceHideGuildMfaRequirement) return false; if (nagbarState.forceGuildMfaRequirement) return Boolean(selectedChannelId) && Boolean(guild); return ( Boolean(selectedChannelId) && Boolean(guild) && !user?.mfaEnabled && guild?.mfaLevel === GuildMFALevel.ELEVATED && hasMfaGatedGuildPermissions && !guildMfaRequirementDismissed ); }, [ selectedChannelId, guild, user?.mfaEnabled, hasMfaGatedGuildPermissions, guildMfaRequirementDismissed, nagbarState.forceGuildMfaRequirement, nagbarState.forceHideGuildMfaRequirement, ]); const guildLevelGateReason = guild ? GuildMatureContentAgree.getGuildLevelGateReason(guild.id) : MatureContentGateReason.NONE; const showGuildLevelGate = guildLevelGateReason !== MatureContentGateReason.NONE; const topNagbarCount = useContext(TopNagbarContext); const maxVisibleNagbars = 2; const availableGuildNagbarSlots = Math.max(0, maxVisibleNagbars - topNagbarCount); const showStaffOnlyGuildNagbar = shouldShowStaffOnlyGuild && availableGuildNagbarSlots > 0; const showGuildMfaRequirementNagbar = shouldShowGuildMfaRequirement && availableGuildNagbarSlots > (showStaffOnlyGuildNagbar ? 1 : 0); const showInvitesDisabledNagbar = shouldShowInvitesDisabled && availableGuildNagbarSlots > (showStaffOnlyGuildNagbar ? 1 : 0) + (showGuildMfaRequirementNagbar ? 1 : 0); const guildNagbarCount = (showStaffOnlyGuildNagbar ? 1 : 0) + (showGuildMfaRequirementNagbar ? 1 : 0) + (showInvitesDisabledNagbar ? 1 : 0); const hasGuildNagbars = guildNagbarCount > 0; const nagbarCount = topNagbarCount + guildNagbarCount; const prevNagbarCount = useRef(nagbarCount); const nagbarContextValue = nagbarCount; useEffect(() => { if (prevNagbarCount.current !== nagbarCount) { prevNagbarCount.current = nagbarCount; ComponentBus.dispatch('LAYOUT_RESIZED'); } }, [nagbarCount]); useEffect(() => { if (!viewLive || !guild || !channelId || guildUnavailable) return; const currentChannel = Channels.getChannel(channelId); const currentPath = Navigation.pathname; const expectedPath = Routes.guildChannel(guildId, channelId); if (currentPath === expectedPath && !currentChannel) { if (firstAccessibleTextChannel) { Navigation.navigateToGuild(guildId, firstAccessibleTextChannel.id, undefined, 'replace'); } } }, [viewLive, guild, guildId, channelId, firstAccessibleTextChannel, guildUnavailable]); const guildNagbars = ( <> {showStaffOnlyGuildNagbar && guildId && ( )} {showGuildMfaRequirementNagbar && guildId && ( )} {showInvitesDisabledNagbar && guildId && ( )} ); if (VoiceCallFullscreen.isActive) { return (
{children}
); } if (!viewLive && !guildUnavailable && !guild) { return (
); } if (mobileLayout.enabled) { if (!channelId && (guildUnavailable || guildNotFound)) { return (
{guildUnavailable ? ( ) : ( )}
); } if (showGuildLevelGate && guild) { return (
{guildNagbars}
); } return ( {guild && (
)} {channelId && (
{guildNagbars}
{children}
)}
); } if (guildUnavailable) { return (
{guildNagbars}
); } if (guildNotFound) { return (
{guildNagbars}
); } if (channelId && !Channels.getChannel(channelId) && !firstAccessibleTextChannel) { return (
{guildNagbars}
); } if (showGuildLevelGate && guild) { return (
{guildNagbars}
); } return (
{guildNagbars}
{children}
); });