/* * 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 . */ import {Trans, useLingui} from '@lingui/react/macro'; import { BellIcon, BugIcon, CaretDownIcon, CaretRightIcon, CaretUpIcon, ChatCircleIcon, CheckIcon, CrownIcon, DotsThreeVerticalIcon, GearIcon, MagnifyingGlassIcon, PencilIcon, PushPinIcon, SignOutIcon, StarIcon, TicketIcon, UserPlusIcon, UsersIcon, XIcon, } from '@phosphor-icons/react'; import clsx from 'clsx'; import {observer} from 'mobx-react-lite'; import React from 'react'; import * as ChannelActionCreators from '~/actions/ChannelActionCreators'; import * as ModalActionCreators from '~/actions/ModalActionCreators'; import {modal} from '~/actions/ModalActionCreators'; import * as PrivateChannelActionCreators from '~/actions/PrivateChannelActionCreators'; import * as ReadStateActionCreators from '~/actions/ReadStateActionCreators'; import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators'; import * as ToastActionCreators from '~/actions/ToastActionCreators'; import * as UserGuildSettingsActionCreators from '~/actions/UserGuildSettingsActionCreators'; import * as UserProfileActionCreators from '~/actions/UserProfileActionCreators'; import {ChannelTypes, isOfflineStatus, ME, MessageNotifications, Permissions} from '~/Constants'; import {DMCloseFailedModal} from '~/components/alerts/DMCloseFailedModal'; import {ChannelSearchBottomSheet} from '~/components/bottomsheets/ChannelSearchBottomSheet'; import {createMuteConfig, getMuteDurationOptions} from '~/components/channel/muteOptions'; import {PreloadableUserPopout} from '~/components/channel/PreloadableUserPopout'; import {UserTag} from '~/components/channel/UserTag'; import {CustomStatusDisplay} from '~/components/common/CustomStatusDisplay/CustomStatusDisplay'; import {GroupDMAvatar} from '~/components/common/GroupDMAvatar'; import {ChannelDebugModal} from '~/components/debug/ChannelDebugModal'; import {UserDebugModal} from '~/components/debug/UserDebugModal'; import {LongPressable} from '~/components/LongPressable'; import {AddFriendsToGroupModal} from '~/components/modals/AddFriendsToGroupModal'; import {ChannelSettingsModal} from '~/components/modals/ChannelSettingsModal'; import {ChannelTopicModal} from '~/components/modals/ChannelTopicModal'; import {ConfirmModal} from '~/components/modals/ConfirmModal'; import {CreateDMModal} from '~/components/modals/CreateDMModal'; import {EditGroupModal} from '~/components/modals/EditGroupModal'; import {GroupInvitesModal} from '~/components/modals/GroupInvitesModal'; import {GuildNotificationSettingsModal} from '~/components/modals/GuildNotificationSettingsModal'; import {GuildMemberActionsSheet} from '~/components/modals/guildTabs/GuildMemberActionsSheet'; import {InviteModal} from '~/components/modals/InviteModal'; import {ChannelPinsContent} from '~/components/shared/ChannelPinsContent'; import { CopyIdIcon, CopyLinkIcon, DeleteIcon, EditIcon, InviteIcon, MarkAsReadIcon, } from '~/components/uikit/ContextMenu/ContextMenuIcons'; import { MenuBottomSheet, type MenuGroupType, type MenuItemType, type MenuRadioType, } from '~/components/uikit/MenuBottomSheet/MenuBottomSheet'; import {Scroller} from '~/components/uikit/Scroller'; import * as Sheet from '~/components/uikit/Sheet/Sheet'; import {StatusAwareAvatar} from '~/components/uikit/StatusAwareAvatar'; import {Tooltip} from '~/components/uikit/Tooltip/Tooltip'; import {useLeaveGroup} from '~/hooks/useLeaveGroup'; import {useMemberListSubscription} from '~/hooks/useMemberListSubscription'; import {usePressable} from '~/hooks/usePressable'; import {SafeMarkdown} from '~/lib/markdown'; import {MarkdownContext} from '~/lib/markdown/renderers'; import {Routes} from '~/Routes'; import type {ChannelRecord} from '~/records/ChannelRecord'; import type {GuildMemberRecord} from '~/records/GuildMemberRecord'; import type {GuildRecord} from '~/records/GuildRecord'; import type {UserRecord} from '~/records/UserRecord'; import AccessibilityStore from '~/stores/AccessibilityStore'; import AuthenticationStore from '~/stores/AuthenticationStore'; import FavoritesStore from '~/stores/FavoritesStore'; import GuildStore from '~/stores/GuildStore'; import MemberSidebarStore from '~/stores/MemberSidebarStore'; import PermissionStore from '~/stores/PermissionStore'; import PresenceStore from '~/stores/PresenceStore'; import ReadStateStore from '~/stores/ReadStateStore'; import SelectedChannelStore from '~/stores/SelectedChannelStore'; import TypingStore from '~/stores/TypingStore'; import UserGuildSettingsStore from '~/stores/UserGuildSettingsStore'; import UserSettingsStore from '~/stores/UserSettingsStore'; import UserStore from '~/stores/UserStore'; import markupStyles from '~/styles/Markup.module.css'; import * as ChannelUtils from '~/utils/ChannelUtils'; import {getMutedText, getNotificationSettingsLabel} from '~/utils/ContextMenuUtils'; import {MAX_GROUP_DM_RECIPIENTS} from '~/utils/groupDmUtils'; import * as InviteUtils from '~/utils/InviteUtils'; import * as MemberListUtils from '~/utils/MemberListUtils'; import {buildChannelLink} from '~/utils/messageLinkUtils'; import * as NicknameUtils from '~/utils/NicknameUtils'; import * as RouterUtils from '~/utils/RouterUtils'; import styles from './ChannelDetailsBottomSheet.module.css'; const MEMBER_ITEM_HEIGHT = 56; const INITIAL_MEMBER_RANGE: [number, number] = [0, 99]; const SCROLL_BUFFER = 50; const SkeletonMemberItem = () => (
); type ChannelDetailsTab = 'members' | 'pins'; interface ChannelDetailsBottomSheetProps { isOpen: boolean; onClose: () => void; channel: ChannelRecord; initialTab?: ChannelDetailsTab; openSearchImmediately?: boolean; } interface QuickActionButtonProps { icon: React.ReactNode; label: string; onClick: () => void; isActive?: boolean; danger?: boolean; disabled?: boolean; } const QuickActionButton: React.FC = ({icon, label, onClick, isActive, danger, disabled}) => { const {isPressed, pressableProps} = usePressable(disabled); return ( ); }; const MobileMemberListItem = observer( ({ guild, channelId, member, onLongPress, }: { guild: GuildRecord; channelId: string; member: GuildMemberRecord; onLongPress?: (member: GuildMemberRecord) => void; }) => { const {t} = useLingui(); const isTyping = TypingStore.isTyping(channelId, member.user.id); const status = PresenceStore.getStatus(member.user.id); const handleLongPress = React.useCallback(() => { onLongPress?.(member); }, [member, onLongPress]); const content = (
{NicknameUtils.getNickname(member.user, guild.id)} {guild.isOwner(member.user.id) && (
)} {member.user.bot && }
{!member.user.bot && ( )}
); if (onLongPress) { return ( {content} ); } return content; }, ); interface LazyMemberListGroupProps { guild: GuildRecord; group: {id: string; count: number}; channelId: string; members: Array; onMemberLongPress?: (member: GuildMemberRecord) => void; } const LazyMemberListGroup = observer( ({guild, group, channelId, members, onMemberLongPress}: LazyMemberListGroupProps) => { const {t} = useLingui(); const groupName = (() => { switch (group.id) { case 'online': return t`Online`; case 'offline': return t`Offline`; default: { const role = guild.getRole(group.id); return role?.name ?? group.id; } } })(); return (
{groupName} โ€” {group.count}
{members.map((member, index) => ( {index < members.length - 1 &&
} ))}
); }, ); const LazyGuildMemberList = observer( ({ guild, channel, onMemberLongPress, enabled = true, }: { guild: GuildRecord; channel: ChannelRecord; onMemberLongPress?: (member: GuildMemberRecord) => void; enabled?: boolean; }) => { const [subscribedRange, setSubscribedRange] = React.useState<[number, number]>(INITIAL_MEMBER_RANGE); const {subscribe} = useMemberListSubscription({ guildId: guild.id, channelId: channel.id, enabled, allowInitialUnfocusedLoad: true, }); const memberListState = MemberSidebarStore.getList(guild.id, channel.id); const isLoading = !memberListState || memberListState.items.size === 0; const handleScroll = React.useCallback( (event: React.UIEvent) => { const target = event.currentTarget; const scrollTop = target.scrollTop; const clientHeight = target.clientHeight; const startIndex = Math.max(0, Math.floor(scrollTop / MEMBER_ITEM_HEIGHT) - SCROLL_BUFFER); const endIndex = Math.ceil((scrollTop + clientHeight) / MEMBER_ITEM_HEIGHT) + SCROLL_BUFFER; if (startIndex !== subscribedRange[0] || endIndex !== subscribedRange[1]) { const newRange: [number, number] = [startIndex, endIndex]; setSubscribedRange(newRange); subscribe([newRange]); } }, [subscribedRange, subscribe], ); if (isLoading) { return (
{Array.from({length: 10}).map((_, i) => ( {i < 9 &&
} ))}
); } const groupedItems: Map> = new Map(); const groups = memberListState.groups; const seenMemberIds = new Set(); for (const group of groups) { groupedItems.set(group.id, []); } let currentGroup: string | null = null; const sortedItems = Array.from(memberListState.items.entries()).sort(([a], [b]) => a - b); for (const [, item] of sortedItems) { if (item.type === 'group') { currentGroup = (item.data as {id: string}).id; } else if (item.type === 'member' && currentGroup) { const member = item.data as GuildMemberRecord; if (!seenMemberIds.has(member.user.id)) { seenMemberIds.add(member.user.id); groupedItems.get(currentGroup)?.push(member); } } } return (
{groups.map((group) => { const members = groupedItems.get(group.id) ?? []; if (members.length === 0) { return null; } return ( ); })}
); }, ); const GuildMemberList = observer( ({ guild, channel, onMemberLongPress, enabled = true, }: { guild: GuildRecord; channel: ChannelRecord; onMemberLongPress?: (member: GuildMemberRecord) => void; enabled?: boolean; }) => { return ( ); }, ); export const ChannelDetailsBottomSheet: React.FC = observer( ({isOpen, onClose, channel, initialTab = 'members', openSearchImmediately = false}) => { const {t, i18n} = useLingui(); const [activeTab, setActiveTab] = React.useState(initialTab); const [muteSheetOpen, setMuteSheetOpen] = React.useState(false); const [searchSheetOpen, setSearchSheetOpen] = React.useState(false); const [isTopicExpanded, setIsTopicExpanded] = React.useState(false); const [moreOptionsSheetOpen, setMoreOptionsSheetOpen] = React.useState(false); const [notificationSheetOpen, setNotificationSheetOpen] = React.useState(false); const [activeMemberSheet, setActiveMemberSheet] = React.useState<{ member: GuildMemberRecord; user: UserRecord; } | null>(null); const leaveGroup = useLeaveGroup(); React.useEffect(() => { setActiveTab(initialTab); }, [initialTab]); React.useEffect(() => { if (openSearchImmediately && isOpen) { setSearchSheetOpen(true); } }, [openSearchImmediately, isOpen]); const isDM = channel.type === ChannelTypes.DM; const isPersonalNotes = channel.type === ChannelTypes.DM_PERSONAL_NOTES; const isGuildChannel = channel.guildId != null; const guild = isGuildChannel ? GuildStore.getGuild(channel.guildId) : null; const recipient = isDM && channel.recipientIds.length > 0 ? UserStore.getUser(channel.recipientIds[0]) : null; const currentUser = UserStore.currentUser; const currentUserId = AuthenticationStore.currentUserId; const guildId = channel.guildId ?? null; const settingsGuildId = isGuildChannel ? channel.guildId : null; const channelOverride = UserGuildSettingsStore.getChannelOverride(settingsGuildId, channel.id); const isMuted = channelOverride?.muted ?? false; const muteConfig = channelOverride?.mute_config; const mutedText = getMutedText(isMuted, muteConfig); const isGroupDMOwner = channel.type === ChannelTypes.GROUP_DM && channel.ownerId === currentUserId; const channelTypeLabel = React.useMemo(() => { switch (channel.type) { case ChannelTypes.GUILD_TEXT: return t`Text Channel`; case ChannelTypes.GUILD_VOICE: return t`Voice Channel`; case ChannelTypes.DM: return t`Direct Message`; case ChannelTypes.DM_PERSONAL_NOTES: return t`Personal Notes`; case ChannelTypes.GROUP_DM: return t`Group Direct Message`; default: return t`Channel`; } }, [channel.type]); const isFavorited = !!FavoritesStore.getChannel(channel.id); const showFavorites = AccessibilityStore.showFavorites; const isGroupDM = channel.type === ChannelTypes.GROUP_DM; const developerMode = UserSettingsStore.developerMode; const handleSearchClick = () => { setSearchSheetOpen(true); }; const handleBellClick = () => { setMuteSheetOpen(true); }; const handleCogClick = () => { setMoreOptionsSheetOpen(true); }; const handleMoreOptionsClose = () => { setMoreOptionsSheetOpen(false); }; const handleNotificationClose = () => { setNotificationSheetOpen(false); }; const handleMarkAsRead = React.useCallback(() => { ReadStateActionCreators.ack(channel.id, true, true); ToastActionCreators.createToast({type: 'success', children: t`Marked as read`}); }, [channel.id]); const handleInvite = React.useCallback(() => { ModalActionCreators.push(modal(() => )); onClose(); }, [channel.id, onClose]); const handleCopyLink = React.useCallback(() => { const channelLink = buildChannelLink({ guildId: channel.guildId, channelId: channel.id, }); TextCopyActionCreators.copy(i18n, channelLink); ToastActionCreators.createToast({type: 'success', children: t`Link copied to clipboard`}); }, [channel.id, channel.guildId, i18n]); const handleCopyId = React.useCallback(() => { TextCopyActionCreators.copy(i18n, channel.id); ToastActionCreators.createToast({type: 'success', children: t`Channel ID copied to clipboard`}); }, [channel.id, i18n]); const handleToggleFavorite = React.useCallback(() => { if (isFavorited) { FavoritesStore.removeChannel(channel.id); ToastActionCreators.createToast({type: 'success', children: t`Removed from favorites`}); } else { FavoritesStore.addChannel(channel.id, channel.guildId ?? ME, null); ToastActionCreators.createToast({type: 'success', children: t`Added to favorites`}); } }, [channel.id, channel.guildId, isFavorited]); const handleDebugChannel = React.useCallback(() => { const channelName = channel.name ?? t`Channel`; ModalActionCreators.push(modal(() => )); onClose(); }, [channel, onClose]); const handleDebugUser = React.useCallback(() => { if (!recipient) return; ModalActionCreators.push(modal(() => )); onClose(); }, [recipient, onClose]); const handlePinDM = React.useCallback(async () => { handleMoreOptionsClose(); try { await PrivateChannelActionCreators.pinDmChannel(channel.id); ToastActionCreators.createToast({ type: 'success', children: isGroupDM ? t`Pinned group` : t`Pinned DM`, }); } catch (error) { console.error('Failed to pin:', error); ToastActionCreators.createToast({ type: 'error', children: isGroupDM ? t`Failed to pin group` : t`Failed to pin DM`, }); } }, [channel.id, isGroupDM]); const handleUnpinDM = React.useCallback(async () => { handleMoreOptionsClose(); try { await PrivateChannelActionCreators.unpinDmChannel(channel.id); ToastActionCreators.createToast({ type: 'success', children: isGroupDM ? t`Unpinned group` : t`Unpinned DM`, }); } catch (error) { console.error('Failed to unpin:', error); ToastActionCreators.createToast({ type: 'error', children: isGroupDM ? t`Failed to unpin group` : t`Failed to unpin DM`, }); } }, [channel.id, isGroupDM]); const handleCloseDM = React.useCallback(() => { handleMoreOptionsClose(); onClose(); ModalActionCreators.push( modal(() => ( { try { await ChannelActionCreators.remove(channel.id); const selectedChannel = SelectedChannelStore.selectedChannelIds.get(ME); if (selectedChannel === channel.id) { RouterUtils.transitionTo(Routes.ME); } ToastActionCreators.createToast({ type: 'success', children: t`DM closed`, }); } catch (error) { console.error('Failed to close DM:', error); ModalActionCreators.push(modal(() => )); } }} /> )), ); }, [channel.id, recipient, onClose]); const handleLeaveGroup = React.useCallback(() => { handleMoreOptionsClose(); onClose(); leaveGroup(channel.id); }, [channel.id, onClose, leaveGroup]); const handleEditGroup = React.useCallback(() => { handleMoreOptionsClose(); onClose(); ModalActionCreators.push(modal(() => )); }, [channel.id, onClose]); const handleShowInvites = React.useCallback(() => { handleMoreOptionsClose(); onClose(); ModalActionCreators.push(modal(() => )); }, [channel.id, onClose]); const handleOpenAddFriendsToGroup = React.useCallback(() => { handleMoreOptionsClose(); onClose(); ModalActionCreators.push(modal(() => )); }, [channel.id, onClose]); const handleCopyUserId = React.useCallback(() => { if (!recipient) return; TextCopyActionCreators.copy(i18n, recipient.id); ToastActionCreators.createToast({type: 'success', children: t`User ID copied to clipboard`}); }, [recipient, i18n]); const handleEditChannel = React.useCallback(() => { ModalActionCreators.push(modal(() => )); onClose(); }, [channel.id, onClose]); const handleDeleteChannel = React.useCallback(() => { onClose(); const channelType = channel.type === ChannelTypes.GUILD_VOICE ? t`Voice Channel` : t`Text Channel`; ModalActionCreators.push( modal(() => ( { try { await ChannelActionCreators.remove(channel.id); ToastActionCreators.createToast({ type: 'success', children: t`Channel deleted`, }); } catch (error) { console.error('Failed to delete channel:', error); ToastActionCreators.createToast({ type: 'error', children: t`Failed to delete channel`, }); } }} /> )), ); }, [channel.id, channel.name, channel.type, onClose]); const handleOpenGuildNotificationSettings = React.useCallback(() => { if (!guildId) return; ModalActionCreators.push(modal(() => )); }, [guildId]); const handleOpenCreateGroupModal = React.useCallback(() => { const duplicateExcludeChannelId = channel.type === ChannelTypes.GROUP_DM ? channel.id : undefined; ModalActionCreators.push( modal(() => ( )), ); }, [channel.id, channel.recipientIds, channel.type]); const handleNotificationLevelChange = React.useCallback( (level: number) => { if (!guildId) return; if (level === MessageNotifications.INHERIT) { UserGuildSettingsActionCreators.updateChannelOverride( guildId, channel.id, { message_notifications: MessageNotifications.INHERIT, }, {persistImmediately: true}, ); } else { UserGuildSettingsActionCreators.updateMessageNotifications(guildId, level, channel.id, { persistImmediately: true, }); } }, [guildId, channel.id], ); const handleMemberLongPress = React.useCallback((member: GuildMemberRecord) => { setActiveMemberSheet({member, user: member.user}); }, []); const handleCloseMemberSheet = React.useCallback(() => { setActiveMemberSheet(null); }, []); const isMemberTabVisible = isOpen && activeTab === 'members'; const dmMemberGroups = (() => { if (!(isDM || isGroupDM || isPersonalNotes)) return []; const currentUserId = AuthenticationStore.currentUserId; let memberIds: Array = []; if (isPersonalNotes) { memberIds = currentUser ? [currentUser.id] : []; } else { memberIds = [...channel.recipientIds]; if (currentUserId && !memberIds.includes(currentUserId)) { memberIds.push(currentUserId); } } const users = memberIds.map((id) => UserStore.getUser(id)).filter((u): u is UserRecord => u !== null); return MemberListUtils.getGroupDMMemberGroups(users); })(); return ( <>
{isDM && recipient ? ( ) : isGroupDM ? ( ) : isPersonalNotes && currentUser ? ( ) : (
{ChannelUtils.getIcon(channel, {className: styles.iconLarge})}
)}
{isDM && recipient ? ( <>
{recipient.username} #{recipient.discriminator}
{recipient.bot && } ) : isGroupDM ? ( <>

{ChannelUtils.getDMDisplayName(channel)}

{t`Group DM ยท ${channel.recipientIds.length + 1} members`}

) : isPersonalNotes ? ( <>

Personal Notes

Your private space

) : ( <>

{ChannelUtils.getIcon(channel, {className: styles.channelNameIcon})} {channel.name}

{channelTypeLabel}

)}
{channel.topic && !isDM && !isPersonalNotes && (
ModalActionCreators.push(modal(() => )) } onKeyDown={(e) => e.key === 'Enter' && ModalActionCreators.push(modal(() => )) } tabIndex={0} >
)}
} label={isMuted ? t`Unmute` : t`Mute`} onClick={handleBellClick} isActive={isMuted} /> } label={t`Search`} onClick={handleSearchClick} /> } label={t`More`} onClick={handleCogClick} />
{activeTab === 'members' && (
{(isDM || isGroupDM || isPersonalNotes) && (
{isDM && recipient && ( )}
Members โ€” {dmMemberGroups.reduce((total, group) => total + group.count, 0)}
{dmMemberGroups.map((group) => (
{group.displayName} โ€” {group.count}
{group.users.map((user, index) => { const isCurrentUser = user.id === currentUser?.id; const isOwner = isGroupDM && channel.ownerId === user.id; const handleUserClick = () => { UserProfileActionCreators.openUserProfile(user.id); }; return ( {index < group.users.length - 1 &&
} ); })}
))}
)} {isGuildChannel && guild && ( )}
)} {activeTab === 'pins' && (
)}
setMuteSheetOpen(false)} snapPoints={[0, 1]} initialSnap={1}> setMuteSheetOpen(false)} />}> {(() => { if (isMuted) { if (isGuildChannel) { return t`Unmute Channel`; } return t`Unmute Conversation`; } if (isGuildChannel) { return t`Mute Channel`; } return t`Mute Conversation`; })()}
{isMuted && mutedText ? ( <>

Currently: {mutedText}

) : (
{getMuteDurationOptions(t).map((option, index, array) => { const isSelected = isMuted && ((option.value === null && !muteConfig?.end_time) || (option.value !== null && muteConfig?.selected_time_window === option.value)); return ( {index < array.length - 1 &&
} ); })}
)}
{ const groups: Array = []; const hasUnread = ReadStateStore.hasUnread(channel.id); const commonItems: Array = []; if (showFavorites && !isPersonalNotes) { commonItems.push({ id: 'favorite', icon: , label: isFavorited ? t`Remove from Favorites` : t`Add to Favorites`, onClick: () => { handleToggleFavorite(); handleMoreOptionsClose(); }, }); } if (hasUnread) { commonItems.push({ id: 'mark-as-read', icon: , label: t`Mark as Read`, onClick: handleMarkAsRead, }); } if (isDM || isGroupDM) { commonItems.push( channel.isPinned ? { id: 'unpin', icon: , label: isGroupDM ? t`Unpin Group DM` : t`Unpin DM`, onClick: handleUnpinDM, } : { id: 'pin', icon: , label: isGroupDM ? t`Pin Group DM` : t`Pin DM`, onClick: handlePinDM, }, ); } const canInvite = isGuildChannel ? InviteUtils.canInviteToChannel(channel.id, channel.guildId) : false; if (canInvite) { commonItems.push({ id: 'invite', icon: , label: t`Invite People`, onClick: handleInvite, }); } if (isGuildChannel) { commonItems.push( { id: 'copy-link', icon: , label: t`Copy Link`, onClick: handleCopyLink, }, { id: 'notification-settings', icon: , label: t`Notification Settings`, onClick: () => { handleMoreOptionsClose(); setNotificationSheetOpen(true); }, }, ); } if (commonItems.length > 0) { groups.push({items: commonItems}); } if (isGroupDM) { const groupItems: Array = [ { id: 'edit-group', icon: , label: t`Edit Group`, onClick: handleEditGroup, }, ]; if (channel.recipientIds.length + 1 < MAX_GROUP_DM_RECIPIENTS) { groupItems.push({ id: 'add-friends', icon: , label: t`Add Friends to Group`, onClick: () => { handleMoreOptionsClose(); ModalActionCreators.push(modal(() => )); }, }); } if (isGroupDMOwner) { groupItems.push({ id: 'invites', icon: , label: t`Invites`, onClick: handleShowInvites, }); } groups.push({items: groupItems}); } if (isGuildChannel) { const canManageChannels = PermissionStore.can(Permissions.MANAGE_CHANNELS, { channelId: channel.id, guildId: channel.guildId, }); if (canManageChannels) { const managerItems: Array = [ { id: 'edit-channel', icon: , label: t`Edit Channel`, onClick: handleEditChannel, }, { id: 'delete-channel', icon: , label: t`Delete Channel`, onClick: handleDeleteChannel, danger: true, }, ]; groups.push({items: managerItems}); } } if (isDM) { groups.push({ items: [ { id: 'close-dm', icon: , label: t`Close DM`, onClick: handleCloseDM, danger: true, }, ], }); } if (isGroupDM) { groups.push({ items: [ { id: 'leave-group', icon: , label: t`Leave Group`, onClick: handleLeaveGroup, danger: true, }, ], }); } const miscItems: Array = []; if (developerMode) { miscItems.push({ id: 'debug-channel', icon: , label: t`Debug Channel`, onClick: handleDebugChannel, }); if (isDM && recipient) { miscItems.push({ id: 'debug-user', icon: , label: t`Debug User`, onClick: handleDebugUser, }); } } if (isDM && recipient) { miscItems.push({ id: 'copy-user-id', icon: , label: t`Copy User ID`, onClick: handleCopyUserId, }); } miscItems.push({ id: 'copy-channel-id', icon: , label: t`Copy Channel ID`, onClick: handleCopyId, }); if (miscItems.length > 0) { groups.push({items: miscItems}); } return groups; }, [ channel.id, channel.guildId, channel.name, channel.type, channel.isPinned, channel.recipientIds, isDM, isGroupDM, isGuildChannel, isGroupDMOwner, isPersonalNotes, showFavorites, isFavorited, recipient, developerMode, handleMarkAsRead, handleInvite, handleCopyLink, handlePinDM, handleUnpinDM, handleEditChannel, handleDeleteChannel, handleEditGroup, handleShowInvites, handleOpenAddFriendsToGroup, handleCloseDM, handleLeaveGroup, handleToggleFavorite, handleDebugChannel, handleDebugUser, handleCopyUserId, handleCopyId, ])} /> => { const categoryId = channel.parentId; const hasCategory = categoryId != null; const channelNotifications = UserGuildSettingsStore.getChannelOverride( guildId, channel.id, )?.message_notifications; const currentNotificationLevel = channelNotifications ?? MessageNotifications.INHERIT; const guildNotificationLevel = UserGuildSettingsStore.getGuildMessageNotifications(guildId); const categoryOverride = UserGuildSettingsStore.getChannelOverride(guildId, categoryId ?? ''); const categoryNotifications = categoryId ? categoryOverride?.message_notifications : undefined; const resolveEffectiveLevel = (level: number | undefined, fallback: number): number => { if (level === undefined || level === MessageNotifications.INHERIT) { return fallback; } return level; }; const categoryDefaultLevel = resolveEffectiveLevel(categoryNotifications, guildNotificationLevel); const defaultSubtext = getNotificationSettingsLabel(categoryDefaultLevel) ?? undefined; return [ { items: [ { label: hasCategory ? t`Category Default` : t`Community Default`, subtext: defaultSubtext, selected: currentNotificationLevel === MessageNotifications.INHERIT, onSelect: () => handleNotificationLevelChange(MessageNotifications.INHERIT), }, { label: t`All Messages`, selected: currentNotificationLevel === MessageNotifications.ALL_MESSAGES, onSelect: () => handleNotificationLevelChange(MessageNotifications.ALL_MESSAGES), }, { label: t`Only @mentions`, selected: currentNotificationLevel === MessageNotifications.ONLY_MENTIONS, onSelect: () => handleNotificationLevelChange(MessageNotifications.ONLY_MENTIONS), }, { label: t`Nothing`, selected: currentNotificationLevel === MessageNotifications.NO_MESSAGES, onSelect: () => handleNotificationLevelChange(MessageNotifications.NO_MESSAGES), }, ] as Array, }, { items: [ { id: 'open-guild-settings', icon: , label: t`Open Community Notification Settings`, onClick: handleOpenGuildNotificationSettings, }, ] as Array, }, ]; }, [ guildId, channel.id, channel.parentId, handleNotificationLevelChange, handleOpenGuildNotificationSettings, ])} /> setSearchSheetOpen(false)} channel={channel} /> {activeMemberSheet && guildId && ( )} ); }, );