// SPDX-License-Identifier: AGPL-3.0-or-later import {Routes} from '@app/app/Routes'; import Authentication from '@app/features/auth/state/Authentication'; import {CategoryCreateModal} from '@app/features/channel/components/modals/CategoryCreateModal'; import {ChannelCreateModal} from '@app/features/channel/components/modals/ChannelCreateModal'; import Channels from '@app/features/channel/state/Channels'; import {DELETE_MY_MESSAGES_DESCRIPTOR} from '@app/features/channel/utils/ChannelMessageDescriptors'; import {GuildDebugModal} from '@app/features/devtools/components/debug/GuildDebugModal'; import {GuildNotificationSettingsModal} from '@app/features/guild/components/modals/GuildNotificationSettingsModal'; import {GuildPrivacySettingsModal} from '@app/features/guild/components/modals/GuildPrivacySettingsModal'; import {GuildSettingsModal} from '@app/features/guild/components/modals/GuildSettingsModal'; import {useDeleteMyMessagesInGuild} from '@app/features/guild/hooks/useDeleteMyMessagesInGuild'; import {useLeaveGuild} from '@app/features/guild/hooks/useLeaveGuild'; import type {Guild} from '@app/features/guild/models/Guild'; import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree'; import {isStockCommunityGuild} from '@app/features/guild/utils/GuildCommunityUtils'; import { COPY_COMMUNITY_ID_DESCRIPTOR, CREATE_CATEGORY_DESCRIPTOR, CREATE_CHANNEL_DESCRIPTOR, HIDE_MUTED_CHANNELS_DESCRIPTOR, LEAVE_COMMUNITY_DESCRIPTOR, MARK_AS_READ_DESCRIPTOR, MUTE_COMMUNITY_DESCRIPTOR, NOTIFICATION_SETTINGS_DESCRIPTOR, PRIVACY_SETTINGS_DESCRIPTOR, RESET_MATURE_CONTENT_AGREE_STATE_DESCRIPTOR, UNMUTE_COMMUNITY_DESCRIPTOR, } from '@app/features/i18n/utils/CommonMessageDescriptors'; import {InviteModal} from '@app/features/invite/components/modals/InviteModal'; import * as InviteUtils from '@app/features/invite/utils/InviteUtils'; import {REPORT_COMMUNITY_DESCRIPTOR} from '@app/features/moderation/utils/ModerationMessageDescriptors'; import {openReportGuildModal} from '@app/features/moderation/utils/ReportActionUtils'; import * as RouterUtils from '@app/features/navigation/utils/RouterUtils'; import Permission from '@app/features/permissions/state/Permission'; import * as ReadStateCommands from '@app/features/read_state/commands/ReadStateCommands'; import ReadStates from '@app/features/read_state/state/ReadStates'; import { CopyIdIcon, CreateCategoryIcon, CreateChannelIcon, DebugChannelIcon, DeleteIcon, EditProfileIcon, InviteIcon, LeaveIcon, MarkAsReadIcon, MuteIcon, NotificationSettingsIcon, PrivacySettingsIcon, ReportUserIcon, SettingsIcon, } from '@app/features/ui/action_menu/ContextMenuIcons'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands'; import type { MenuCheckboxType, MenuGroupType, MenuItemType, MenuSubmenuItemType, } from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet'; import * as UserGuildSettingsCommands from '@app/features/user/commands/UserGuildSettingsCommands'; import {UserSettingsModal} from '@app/features/user/components/modals/UserSettingsModal'; import { GUILD_SETTINGS_LABEL_DESCRIPTOR, getGuildSettingsTabs, } from '@app/features/user/components/settings_utils/GuildSettingsConstants'; import UserGuildSettings from '@app/features/user/state/UserGuildSettings'; import UserSettings from '@app/features/user/state/UserSettings'; import Users from '@app/features/user/state/Users'; import {getMutedText} from '@app/lib/overlay/OverlayContextMenu'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {ContentWarningLevel} from '@fluxer/constants/src/GuildConstants'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {useMemo} from 'react'; const COMMUNITY_DEBUG_DESCRIPTOR = msg({ message: 'Community debug', comment: 'Title of the developer-mode community debug modal.', }); const INVITE_MEMBERS_DESCRIPTOR = msg({ message: 'Invite members', comment: 'Action label that opens the invite flow.', }); const EDIT_COMMUNITY_PROFILE_DESCRIPTOR = msg({ message: 'Edit community profile', comment: 'Action that opens the community profile editor for the current member.', }); const DEBUG_COMMUNITY_DESCRIPTOR = msg({ message: 'Debug community', comment: 'Developer-mode action that opens the community debug modal.', }); interface UseGuildMenuDataOptions { onClose: () => void; onOpenMuteSheet?: () => void; preserveInitialMarkAsReadVisibility?: boolean; } export interface GuildMenuHandlers { handleMarkAsRead: () => void; handleInviteMembers: () => void; handleCommunitySettings: () => void; handleCreateChannel: () => void; handleCreateCategory: () => void; handleNotificationSettings: () => void; handlePrivacySettings: () => void; handleEditCommunityProfile: () => void; handleLeaveCommunity: () => void; handleDeleteMyMessagesInCommunity: () => void; handleCopyGuildId: () => void; handleDebugGuild: () => void; handleResetMatureContentAgreeState: () => void; handleReportGuild: () => void; handleToggleHideMutedChannels: (checked: boolean) => void; } export interface GuildMenuPermissions { canManageGuild: boolean; canManageChannels: boolean; canInvite: boolean; canAccessGuildSettings: boolean; isOwner: boolean; hasGuildUnread: boolean; developerMode: boolean; } export interface GuildMenuData { groups: Array; handlers: GuildMenuHandlers; permissions: GuildMenuPermissions; isMuted: boolean; mutedText: string | null; hideMutedChannels: boolean; } export function useGuildMenuData(guild: Guild, options: UseGuildMenuDataOptions): GuildMenuData { const {i18n} = useLingui(); const {onClose, onOpenMuteSheet, preserveInitialMarkAsReadVisibility = false} = options; const leaveGuild = useLeaveGuild(); const deleteMyMessagesInGuild = useDeleteMyMessagesInGuild(); const channels = Channels.getGuildChannels(guild.id); const canManageGuild = Permission.can(Permissions.MANAGE_GUILD, {guildId: guild.id}); const canManageChannels = Permission.can(Permissions.MANAGE_CHANNELS, {guildId: guild.id}); const invitableChannelId = InviteUtils.getInvitableChannelId(guild.id); const canInvite = InviteUtils.canInviteToChannel(invitableChannelId, guild.id); const canManageRoles = Permission.can(Permissions.MANAGE_ROLES, {guildId: guild.id}); const canViewAuditLog = Permission.can(Permissions.VIEW_AUDIT_LOG, {guildId: guild.id}); const canManageWebhooks = Permission.can(Permissions.MANAGE_WEBHOOKS, {guildId: guild.id}); const canManageEmojis = Permission.can(Permissions.MANAGE_EXPRESSIONS, {guildId: guild.id}); const canCreateExpressions = Permission.can(Permissions.CREATE_EXPRESSIONS, {guildId: guild.id}); const canBanMembers = Permission.can(Permissions.BAN_MEMBERS, {guildId: guild.id}); const canAccessGuildSettings = canManageGuild || canManageRoles || canViewAuditLog || canManageWebhooks || canManageEmojis || canCreateExpressions || canBanMembers; const isOwner = guild.isOwner(Authentication.currentUserId); const developerMode = UserSettings.developerMode; const canEditCommunityProfile = Users.getCurrentUser()?.isClaimed() ?? true; const settings = UserGuildSettings.getSettingsForScope(guild.id); const hideMutedChannels = settings?.hide_muted_channels ?? false; const isMuted = settings?.muted ?? false; const muteConfig = settings?.mute_config; const mutedText = getMutedText(isMuted, muteConfig); const hasCurrentGuildMatureContentGate = guild.nsfw || guild.contentWarningLevel === ContentWarningLevel.CONTENT_WARNING; const initialHasGuildUnread = useMemo( () => Channels.getGuildChannels(guild.id).some((channel) => ReadStates.hasUnread(channel.id)), [guild.id], ); const hasGuildUnread = preserveInitialMarkAsReadVisibility ? initialHasGuildUnread : channels.some((channel) => ReadStates.hasUnread(channel.id)); const handlers = useMemo( () => ({ handleMarkAsRead: () => { const channelIds = channels .filter((channel) => ReadStates.getUnreadCount(channel.id) > 0) .map((channel) => channel.id); if (channelIds.length > 0) { void ReadStateCommands.bulkAckChannels(channelIds); } onClose(); }, handleInviteMembers: () => { const invitableChannelId = InviteUtils.getInvitableChannelId(guild.id); ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleCommunitySettings: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleCreateChannel: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleCreateCategory: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleNotificationSettings: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handlePrivacySettings: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleEditCommunityProfile: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleLeaveCommunity: () => { ModalCommands.runAfterBottomSheetClose(onClose, () => leaveGuild(guild.id)); }, handleDeleteMyMessagesInCommunity: () => { ModalCommands.runAfterBottomSheetClose(onClose, () => deleteMyMessagesInGuild(guild.id)); }, handleCopyGuildId: () => { void TextCopyCommands.copy(i18n, guild.id); onClose(); }, handleDebugGuild: () => { ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, handleResetMatureContentAgreeState: () => { GuildMatureContentAgree.revokeGuild(guild.id); onClose(); }, handleReportGuild: () => { ModalCommands.runAfterBottomSheetClose(onClose, () => openReportGuildModal({i18n, guild})); }, handleToggleHideMutedChannels: (checked: boolean) => { const currentSettings = UserGuildSettings.getSettingsForScope(guild.id); const currentValue = currentSettings?.hide_muted_channels ?? false; if (checked === currentValue) return; UserGuildSettingsCommands.toggleHideMutedChannels(guild.id); }, }), [channels, deleteMyMessagesInGuild, guild, i18n.locale, leaveGuild, onClose], ); const permissions: GuildMenuPermissions = useMemo( () => ({ canManageGuild, canManageChannels, canInvite, canAccessGuildSettings, isOwner, hasGuildUnread, developerMode, }), [canManageGuild, canManageChannels, canInvite, canAccessGuildSettings, isOwner, hasGuildUnread, developerMode], ); const availableSettingsTabs = useMemo(() => { const allTabs = getGuildSettingsTabs(i18n); return allTabs.filter((tab) => { if (tab.permission) { const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission]; if (!perms.some((p) => Permission.can(p, {guildId: guild.id}))) { return false; } } if (tab.requireFeature && !guild.features.has(tab.requireFeature)) { return false; } return true; }); }, [guild.features, guild.id, i18n.locale]); const groups = useMemo(() => { const menuGroups: Array = []; const quickActions: Array = []; if (hasGuildUnread) { quickActions.push({ icon: , label: i18n._(MARK_AS_READ_DESCRIPTOR), onClick: handlers.handleMarkAsRead, }); } if (canInvite) { quickActions.push({ icon: , label: i18n._(INVITE_MEMBERS_DESCRIPTOR), onClick: handlers.handleInviteMembers, }); } if (canAccessGuildSettings) { const settingsSubItems: Array = availableSettingsTabs.map((tab) => ({ id: tab.type, label: tab.label, onClick: () => { if (tab.type === 'members') { ModalCommands.runAfterBottomSheetClose(onClose, () => RouterUtils.transitionTo(Routes.guildMembers(guild.id)), ); return; } ModalCommands.pushAfterBottomSheetClose( onClose, modal(() => ( )), ); }, })); quickActions.push({ icon: , label: i18n._(GUILD_SETTINGS_LABEL_DESCRIPTOR), items: settingsSubItems, onTriggerSelect: handlers.handleCommunitySettings, }); } if (canManageChannels) { quickActions.push({ icon: ( ), label: i18n._(CREATE_CHANNEL_DESCRIPTOR), onClick: handlers.handleCreateChannel, }); quickActions.push({ icon: ( ), label: i18n._(CREATE_CATEGORY_DESCRIPTOR), onClick: handlers.handleCreateCategory, }); } if (quickActions.length > 0) { menuGroups.push({items: quickActions}); } const settingsItems: Array = [ { icon: ( ), label: i18n._(NOTIFICATION_SETTINGS_DESCRIPTOR), onClick: handlers.handleNotificationSettings, }, { icon: ( ), label: i18n._(PRIVACY_SETTINGS_DESCRIPTOR), onClick: handlers.handlePrivacySettings, }, ]; if (canEditCommunityProfile) { settingsItems.push({ icon: , label: i18n._(EDIT_COMMUNITY_PROFILE_DESCRIPTOR), onClick: handlers.handleEditCommunityProfile, }); } menuGroups.push({items: settingsItems}); const muteItems: Array = []; if (onOpenMuteSheet) { muteItems.push({ icon: , label: isMuted ? i18n._(UNMUTE_COMMUNITY_DESCRIPTOR) : i18n._(MUTE_COMMUNITY_DESCRIPTOR), onClick: onOpenMuteSheet, }); } const hideMutedChannelsItem: MenuCheckboxType = { label: i18n._(HIDE_MUTED_CHANNELS_DESCRIPTOR), checked: hideMutedChannels, onChange: handlers.handleToggleHideMutedChannels, }; muteItems.push(hideMutedChannelsItem); menuGroups.push({items: muteItems}); const dangerActions: Array = [ { icon: , label: i18n._(DELETE_MY_MESSAGES_DESCRIPTOR), onClick: handlers.handleDeleteMyMessagesInCommunity, danger: true, }, ]; if (!isOwner) { if (!isStockCommunityGuild(guild.id)) { dangerActions.push({ icon: , label: i18n._(LEAVE_COMMUNITY_DESCRIPTOR), onClick: handlers.handleLeaveCommunity, danger: true, }); } dangerActions.push({ icon: , label: i18n._(REPORT_COMMUNITY_DESCRIPTOR), onClick: handlers.handleReportGuild, danger: true, }); } menuGroups.push({items: dangerActions}); if (developerMode) { const debugItems: Array = [ { icon: ( ), label: i18n._(DEBUG_COMMUNITY_DESCRIPTOR), onClick: handlers.handleDebugGuild, }, ]; if (hasCurrentGuildMatureContentGate && GuildMatureContentAgree.hasAgreedToGuild(guild.id)) { debugItems.push({ icon: ( ), label: i18n._(RESET_MATURE_CONTENT_AGREE_STATE_DESCRIPTOR), onClick: handlers.handleResetMatureContentAgreeState, }); } menuGroups.push({items: debugItems}); } const utilityItems: Array = [ { icon: , label: i18n._(COPY_COMMUNITY_ID_DESCRIPTOR), onClick: handlers.handleCopyGuildId, }, ]; menuGroups.push({items: utilityItems}); return menuGroups; }, [ hasGuildUnread, canInvite, canAccessGuildSettings, canManageChannels, canEditCommunityProfile, availableSettingsTabs, guild.id, isMuted, hideMutedChannels, hasCurrentGuildMatureContentGate, isOwner, developerMode, handlers, onClose, onOpenMuteSheet, i18n.locale, ]); return { groups, handlers, permissions, isMuted, mutedText: mutedText ?? null, hideMutedChannels, }; }