// SPDX-License-Identifier: AGPL-3.0-or-later import Accessibility, {ChannelTypingIndicatorMode} from '@app/features/accessibility/state/Accessibility'; import styles from '@app/features/app/components/layout/ChannelItem.module.css'; import {ChannelItemContent} from '@app/features/app/components/layout/ChannelItemContent'; import {ChannelItemIcon} from '@app/features/app/components/layout/ChannelItemIcon'; import channelItemSurfaceStyles from '@app/features/app/components/layout/ChannelItemSurface.module.css'; import { DirectSelectionSurface, markDirectSelection, peekDirectSelection, } from '@app/features/app/components/layout/DirectSelectionOrigin'; import { type ChannelReorderTarget, canChannelDropOnTarget, selectChannelReorderResolution, } from '@app/features/app/components/layout/dnd/ChannelReorderStateMachine'; import {GenericChannelItem} from '@app/features/app/components/layout/GenericChannelItem'; import type {ScrollIndicatorSeverity} from '@app/features/app/components/layout/ScrollIndicatorOverlay'; import {type DragItem, DragItemType, type DropResult} from '@app/features/app/components/layout/types/DndTypes'; import {isCategory, isTextChannel} from '@app/features/app/components/layout/utils/ChannelOrganization'; import {getChannelUnreadState} from '@app/features/app/components/layout/utils/ChannelUnreadState'; import {VoiceChannelUserCount} from '@app/features/app/components/layout/VoiceChannelUserCount'; import {useChannelHoverPreload} from '@app/features/app/hooks/useChannelHoverPreload'; import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState'; import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs'; import {useTextOverflow} from '@app/features/app/hooks/useTextOverflow'; import * as LinkChannelCommands from '@app/features/channel/commands/LinkChannelCommands'; import {CategoryBottomSheet} from '@app/features/channel/components/bottomsheets/CategoryBottomSheet'; import {ChannelBottomSheet} from '@app/features/channel/components/bottomsheets/ChannelBottomSheet'; import {Typing} from '@app/features/channel/components/ChannelTyping'; import {ChannelCreateModal} from '@app/features/channel/components/modals/ChannelCreateModal'; import {ChannelSettingsModal} from '@app/features/channel/components/modals/ChannelSettingsModal'; import {getTypingText, usePresentableTypingUsers} from '@app/features/channel/components/TypingUsers'; import type {Channel} from '@app/features/channel/models/Channel'; import Channels from '@app/features/channel/state/Channels'; import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils'; import type {Guild} from '@app/features/guild/models/Guild'; import { CREATE_CHANNEL_DESCRIPTOR, MENTION_COUNT_ARIA_DESCRIPTOR, } from '@app/features/i18n/utils/CommonMessageDescriptors'; import {isKeyboardActivationKey, stopPropagationOnEnterSpace} from '@app/features/input/utils/KeyboardUtils'; import {InviteModal} from '@app/features/invite/components/modals/InviteModal'; import * as InviteUtils from '@app/features/invite/utils/InviteUtils'; import * as GuildMemberCommands from '@app/features/member/commands/GuildMemberCommands'; import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import Permission from '@app/features/permissions/state/Permission'; import * as PermissionUtils from '@app/features/permissions/utils/PermissionUtils'; import ReadStates from '@app/features/read_state/state/ReadStates'; import Autocomplete from '@app/features/search/state/Autocomplete'; import {CategoryContextMenu} from '@app/features/ui/action_menu/CategoryContextMenu'; import {ChannelContextMenu} from '@app/features/ui/action_menu/ChannelContextMenu'; import {AvatarStack} from '@app/features/ui/avatars/AvatarStack'; import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands'; import * as LayoutCommands from '@app/features/ui/commands/LayoutCommands'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import {MentionBadge} from '@app/features/ui/components/MentionBadge'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import KeyboardMode from '@app/features/ui/state/KeyboardMode'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {CHANNEL_SETTINGS_LABEL_DESCRIPTOR} from '@app/features/user/components/settings_utils/ChannelSettingsConstants'; import type {User} from '@app/features/user/models/User'; import UserGuildSettings from '@app/features/user/state/UserGuildSettings'; import Users from '@app/features/user/state/Users'; import MediaEngine from '@app/features/voice/engine/MediaEngineFacade'; import {usePendingVoiceConnection} from '@app/features/voice/hooks/usePendingVoiceConnection'; import {computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus'; import CompactVoiceCallHeight, {getGuildVoiceCallExpansionKey} from '@app/features/voice/state/CompactVoiceCallHeight'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {CaretDownIcon, ChatTeardropIcon, GearIcon, PlusIcon, UserPlusIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {ConnectableElement} from 'react-dnd'; import {useDrag, useDrop} from 'react-dnd'; import {getEmptyImage} from 'react-dnd-html5-backend'; const CATEGORY_DESCRIPTOR = msg({ message: 'category', comment: 'Lowercase channel type label used inside channel-list accessible text.', }); const VOICE_CHANNEL_DESCRIPTOR = msg({ message: 'voice channel', comment: 'Lowercase channel type label used inside channel-list accessible text.', }); const CHANNEL_DESCRIPTOR = msg({ message: 'channel', comment: 'Lowercase generic channel type label used inside channel-list accessible text.', }); const SELECTED_DESCRIPTOR = msg({ message: 'selected', comment: 'Lowercase state label used inside channel-list accessible text for the selected channel.', }); const COLLAPSED_DESCRIPTOR = msg({ message: 'collapsed', comment: 'Lowercase state label used inside channel-list accessible text for a collapsed category.', }); const EXPANDED_DESCRIPTOR = msg({ message: 'expanded', comment: 'Lowercase state label used inside channel-list accessible text for an expanded category.', }); const UNREAD_DESCRIPTOR = msg({ message: 'unread', comment: 'Lowercase state label used inside channel-list accessible text when a channel has unread messages.', }); const MUTED_DESCRIPTOR = msg({ message: 'muted', comment: 'Lowercase state label used inside channel-list accessible text when a channel is muted.', }); const CONNECTED_DESCRIPTOR = msg({ message: 'connected', comment: 'Lowercase state label used inside channel-list accessible text for the current voice channel.', }); const EDIT_CATEGORY_DESCRIPTOR = msg({ message: 'Edit category', comment: 'Tooltip and accessible label for the category settings button in the channel list.', }); const INVITE_MEMBERS_DESCRIPTOR = msg({ message: 'Invite members', comment: 'Tooltip and accessible label for the invite button shown beside a channel in the channel list.', }); const OPEN_CHAT_DESCRIPTOR = msg({ message: 'Open chat', comment: 'Tooltip and accessible label for the chat icon shown beside a voice channel in the channel list.', }); const EMPTY_TYPING_USERS: ReadonlyArray = Object.freeze([]); interface ChannelItemCoreProps { channel: { name: string; type: number; }; isSelected?: boolean; forceHover?: boolean; typingIndicator?: React.ReactNode; className?: string; } export const ChannelItemCore: React.FC = observer( ({channel, isSelected = false, forceHover = false, typingIndicator, className}) => { const channelLabelRef = useRef(null); const isChannelNameOverflowing = useTextOverflow(channelLabelRef); return (
{ChannelUtils.getIcon(channel, { className: clsx( styles.channelItemIcon, isSelected ? styles.channelItemIconSelected : styles.channelItemIconUnselected, ), })}
{channel.name}
{typingIndicator}
); }, ); export interface ChannelItemProps { guild: Guild; channel: Channel; isCollapsed?: boolean; onToggle?: () => void; isDraggingAnything: boolean; activeDragItem?: DragItem | null; onChannelDrop?: (item: DragItem, result: DropResult) => void; onDragStateChange?: (item: DragItem | null) => void; isSelectedByPath?: boolean; isOnMembersRoute?: boolean; } export const ChannelItem = observer( ({ guild, channel, isCollapsed, onToggle, isDraggingAnything, activeDragItem, onChannelDrop, onDragStateChange, isSelectedByPath = false, isOnMembersRoute = false, }: ChannelItemProps) => { const {i18n} = useLingui(); const elementRef = useRef(null); const dropTargetRef = useRef(null); const contextMenuOpen = useContextMenuHoverState(elementRef, !MobileLayout.enabled); const channelType = channel.type; const channelIsCategory = isCategory(channel); const channelIsVoice = channelType === ChannelTypes.GUILD_VOICE; const channelIsText = isTextChannel(channel); const {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow} = useChannelHoverPreload({ channel, guild, defaultHiddenForChannel: channelIsVoice, enabled: !channelIsCategory, }); const draggingChannel = activeDragItem?.type === DragItemType.CHANNEL ? activeDragItem : null; const isVoiceDragActive = draggingChannel?.channelType === ChannelTypes.GUILD_VOICE; const shouldDimForVoiceDrag = Boolean(isVoiceDragActive && channelIsText && channel.parentId !== null); const unreadCount = ReadStates.getUnreadCount(channel.id); const hasUnread = ReadStates.hasUnread(channel.id); const connectedVoiceGuildId = channelIsVoice ? MediaEngine.guildId : null; const connectedVoiceChannelId = channelIsVoice ? MediaEngine.channelId : null; const canManageChannels = Permission.can(Permissions.MANAGE_CHANNELS, channel); const canUpdateRtcRegion = channelIsVoice && Permission.can(Permissions.UPDATE_RTC_REGION, channel); const canEditChannel = canManageChannels || canUpdateRtcRegion; const canInvite = InviteUtils.canInviteToChannel(channel.id, channel.guildId); const mobileLayout = MobileLayout; const isMuted = UserGuildSettings.isGuildOrChannelMuted(guild.id, channel.id); const isChannelDirectlyMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, channel.id); const currentUserCount = channelIsVoice && channel.userLimit != null && channel.userLimit > 0 ? Object.keys(MediaEngine.getAllVoiceStatesInChannel(guild.id, channel.id)).length : 0; const isMobileLayout = mobileLayout.enabled; const allowHoverAffordances = !isMobileLayout; const [menuOpen, setMenuOpen] = useState(false); const [categoryMenuOpen, setCategoryMenuOpen] = useState(false); const [isFocused, setIsFocused] = useState(false); const [actionModalOpen, setActionModalOpen] = useState(false); const {keyboardModeEnabled} = KeyboardMode; const showKeyboardAffordances = keyboardModeEnabled && isFocused; const revealActionButtons = showKeyboardAffordances || actionModalOpen; const actionButtonTabIndex = revealActionButtons ? 0 : -1; const isVoiceSelected = channelIsVoice && connectedVoiceGuildId === guild.id && connectedVoiceChannelId === channel.id; const channelIsE2EEEncrypted = isVoiceSelected && computeChannelE2EEStatus(guild.id, channel.id) === 'encrypted'; const isSelected = isVoiceSelected || (isSelectedByPath && !isOnMembersRoute); const mentionCount = ReadStates.getMentionCount(channel.id); const unreadBadgesLevel = UserGuildSettings.resolvedUnreadBadgesLevel({ id: channel.id, guildId: channel.guildId ?? undefined, parentId: channel.parentId ?? undefined, type: channel.type, }); const unreadState = getChannelUnreadState({ hasUnread, unreadCount, mentionCount, isMuted: isChannelDirectlyMuted, showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels, unreadBadgesLevel, }); const hasUnreadMessages = unreadState.hasUnreadMessages; const hasMentions = unreadState.hasMentions; const isHighlight = unreadState.isHighlight; let scrollIndicatorSeverity: ScrollIndicatorSeverity | undefined; if (!channelIsCategory) { if (hasMentions) scrollIndicatorSeverity = 'mention'; else if (unreadState.shouldShowUnreadIndicator) scrollIndicatorSeverity = 'unread'; } const scrollIndicatorId = `channel-${channel.id}`; const isAutocompleteHighlight = Autocomplete.highlightChannelId === channel.id; const channelTypingIndicatorMode = Accessibility.channelTypingIndicatorMode; const typingIndicatorEligible = !channelIsCategory && channelTypingIndicatorMode !== ChannelTypingIndicatorMode.HIDDEN; const typingUsers = typingIndicatorEligible ? usePresentableTypingUsers(channel) : EMPTY_TYPING_USERS; const [dropIndicator, setDropIndicator] = useState<{position: 'top' | 'bottom'; isValid: boolean} | null>(null); const dragItemData = useMemo( () => ({ type: channelIsCategory ? DragItemType.CATEGORY : DragItemType.CHANNEL, id: channel.id, channelType: channel.type, parentId: channel.parentId, guildId: guild.id, }), [channelIsCategory, channel.id, channel.type, channel.parentId, guild.id], ); const dropTargetData = useMemo( () => ({ id: channel.id, channelType: channel.type, parentId: channel.parentId, guildId: guild.id, }), [channel.id, channel.type, channel.parentId, guild.id], ); const dndEnabled = !mobileLayout.enabled; const [{isDragging}, dragRef, preview] = useDrag( () => ({ type: dragItemData.type, item: () => { onDragStateChange?.(dragItemData); return dragItemData; }, canDrag: dndEnabled && canManageChannels, collect: (monitor) => ({isDragging: monitor.isDragging()}), end: () => { onDragStateChange?.(null); setDropIndicator(null); }, }), [dragItemData, canManageChannels, dndEnabled, onDragStateChange], ); const [{isOver}, dropRef] = useDrop( () => ({ accept: [DragItemType.CHANNEL, DragItemType.CATEGORY, DragItemType.VOICE_PARTICIPANT], canDrop: (item: DragItem) => canChannelDropOnTarget(item, dropTargetData), hover: (item: DragItem, monitor) => { const node = dropTargetRef.current; if (!node) return; const hoverBoundingRect = node.getBoundingClientRect(); const clientOffset = monitor.getClientOffset(); if (!clientOffset) return; const resolution = selectChannelReorderResolution(item, dropTargetData, clientOffset, hoverBoundingRect); setDropIndicator(resolution.indicator); }, drop: (item: DragItem, monitor): DropResult | undefined => { if (!monitor.canDrop()) { setDropIndicator(null); return; } if (item.type === DragItemType.VOICE_PARTICIPANT && channelIsVoice) { const canMove = Permission.can(Permissions.MOVE_MEMBERS, {guildId: guild.id}); if (!canMove || item.currentChannelId === channel.id) { setDropIndicator(null); return; } const currentUserId = Users.getCurrentUser()?.id ?? null; if (currentUserId && item.userId === currentUserId) { void MediaEngine.connectToVoiceChannel(guild.id, channel.id); setDropIndicator(null); return; } const targetChannel = Channels.getChannel(channel.id); if (targetChannel) { const canTargetConnect = PermissionUtils.can(Permissions.CONNECT, item.userId!, targetChannel.toJSON()); if (!canTargetConnect) { setDropIndicator(null); return; } } void GuildMemberCommands.update(guild.id, item.userId!, {channel_id: channel.id}); setDropIndicator(null); return; } const node = dropTargetRef.current; if (!node) return; const hoverBoundingRect = node.getBoundingClientRect(); const clientOffset = monitor.getClientOffset(); if (!clientOffset) return; const result = selectChannelReorderResolution(item, dropTargetData, clientOffset, hoverBoundingRect).intent ?.result; if (!result) { setDropIndicator(null); return; } onChannelDrop?.(item, result); setDropIndicator(null); return result; }, collect: (monitor) => ({ isOver: monitor.isOver({shallow: true}), canDrop: monitor.canDrop(), }), }), [dropTargetData, guild.id, channelIsVoice, onChannelDrop], ); useEffect(() => { if (!isOver) setDropIndicator(null); }, [isOver]); useEffect(() => { preview(getEmptyImage(), {captureDraggingState: true}); }, [preview]); const {startConnection: startVoiceConnection} = usePendingVoiceConnection({ guildId: guild.id, channelId: channel.id, }); const singleClickConnectsToVoice = channelIsVoice && !Accessibility.voiceChannelJoinRequiresDoubleClick && !isVoiceSelected; const navigateToChannel = useCallback(() => { preloadChannelNow(); NavigationCommands.selectChannel(guild.id, channel.id); if (MobileLayout.isMobileLayout()) { LayoutCommands.updateMobileLayoutState(false, true); } }, [guild.id, channel.id, preloadChannelNow]); const collapseVoiceCallView = useCallback(() => { if (!channelIsVoice) return; CompactVoiceCallHeight.setExpandedForKey(getGuildVoiceCallExpansionKey(channel.id), false); }, [channelIsVoice, channel.id]); const handleSelect = useCallback(() => { markDirectSelection(DirectSelectionSurface.CHANNEL_LIST); if (channel.type === ChannelTypes.GUILD_CATEGORY) { onToggle?.(); return; } if (LinkChannelCommands.openLinkChannel(channel)) { return; } if (singleClickConnectsToVoice && Permission.can(Permissions.CONNECT, channel)) { startVoiceConnection(); return; } navigateToChannel(); }, [channel, onToggle, singleClickConnectsToVoice, startVoiceConnection, navigateToChannel]); const handleDoubleClick = useCallback(() => { if (!channelIsVoice || isVoiceSelected) return; if (!Permission.can(Permissions.CONNECT, channel)) return; startVoiceConnection({skipConfirm: true}); }, [channelIsVoice, isVoiceSelected, channel, startVoiceConnection]); const handleContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); if (isMobileLayout) { return; } ContextMenuCommands.openFromEvent(event, ({onClose}) => channelIsCategory ? ( ) : ( ), ); }, [channel, channelIsCategory, isMobileLayout], ); const dragConnectorRef = useCallback( (node: ConnectableElement | null) => { dragRef(node); }, [dragRef], ); const dropConnectorRef = useCallback( (node: ConnectableElement | null) => { dropRef(node); dropTargetRef.current = node as HTMLDivElement | null; }, [dropRef], ); const mergedRef = useMergeRefs([dragConnectorRef, elementRef]); const shouldShowSelectedState = !channelIsCategory && isSelected && (!channelIsVoice || isSelectedByPath); const hasMountedRef = useRef(false); useEffect(() => { const selectedFromThisRow = peekDirectSelection(DirectSelectionSurface.CHANNEL_LIST); if (shouldShowSelectedState && hasMountedRef.current && !selectedFromThisRow) { elementRef.current?.scrollIntoView({block: 'nearest'}); } hasMountedRef.current = true; }, [shouldShowSelectedState]); const extraContent = unreadState.shouldShowUnreadIndicator ? (
) : null; const channelKindLabel = channelIsCategory ? i18n._(CATEGORY_DESCRIPTOR) : channelIsVoice ? i18n._(VOICE_CHANNEL_DESCRIPTOR) : i18n._(CHANNEL_DESCRIPTOR); const ariaLabelParts = [channel.name, channelKindLabel]; if (shouldShowSelectedState) ariaLabelParts.push(i18n._(SELECTED_DESCRIPTOR)); if (channelIsCategory) ariaLabelParts.push(isCollapsed ? i18n._(COLLAPSED_DESCRIPTOR) : i18n._(EXPANDED_DESCRIPTOR)); if (!channelIsCategory) { if (mentionCount > 0) ariaLabelParts.push(i18n._(MENTION_COUNT_ARIA_DESCRIPTOR, {mentionCount})); else if (hasUnreadMessages) ariaLabelParts.push(i18n._(UNREAD_DESCRIPTOR)); if (isMuted) ariaLabelParts.push(i18n._(MUTED_DESCRIPTOR)); if (isVoiceSelected) ariaLabelParts.push(i18n._(CONNECTED_DESCRIPTOR)); } const ariaLabel = ariaLabelParts.join(', '); const [isPointerHovered, setIsPointerHovered] = useState(false); const handleMouseEnter = useCallback(() => { setIsPointerHovered(true); scheduleChannelPreload(); }, [scheduleChannelPreload]); const handleMouseLeave = useCallback(() => { setIsPointerHovered(false); cancelChannelPreload(); }, [cancelChannelPreload]); const hoverAffordancesActive = allowHoverAffordances && (contextMenuOpen || showKeyboardAffordances || shouldShowSelectedState || isPointerHovered); const hasVoiceUserLimit = channelIsVoice && channel.userLimit != null && channel.userLimit > 0; const showChatAffordance = allowHoverAffordances && channelIsVoice && !Accessibility.voiceChannelJoinRequiresDoubleClick; const hasVoiceHoverAffordances = allowHoverAffordances && !isDraggingAnything && !channelIsCategory && (canInvite || canEditChannel || showChatAffordance); const shouldShowVoiceUserCount = hasVoiceUserLimit && !(hasVoiceHoverAffordances && hoverAffordancesActive); const showMentionBadge = !isSelected && hasMentions && !hoverAffordancesActive; const channelItemClassName = clsx( styles.channelItem, channelItemSurfaceStyles.channelItemSurface, shouldShowSelectedState && channelItemSurfaceStyles.channelItemSurfaceSelected, isAutocompleteHighlight && styles.channelItemAutocompleteHighlight, channelIsCategory ? styles.channelItemCategory : styles.channelItemRegular, !channelIsCategory && isHighlight && !shouldShowSelectedState && styles.channelItemHighlight, !channelIsCategory && !(isHighlight || isSelected || isVoiceSelected) && styles.channelItemMuted, shouldShowSelectedState && styles.channelItemSelected, shouldShowSelectedState && isHighlight && styles.channelItemSelectedWithUnread, !channelIsCategory && (!isSelected || (channelIsVoice && !isSelectedByPath)) && styles.channelItemHoverable, isOver && styles.channelItemOver, contextMenuOpen && !isSelected && !channelIsCategory && styles.channelItemContextMenu, contextMenuOpen && channelIsCategory && styles.channelItemCategoryContextMenu, isDragging && styles.channelItemDragging, shouldDimForVoiceDrag && !isSelected && styles.channelItemDimmed, isChannelDirectlyMuted && styles.channelItemMutedState, contextMenuOpen && styles.contextMenuOpen, revealActionButtons && styles.keyboardFocus, channelIsVoice && styles.channelItemVoice, hoverAffordancesActive && styles.channelItemHoverAffordancesActive, ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!isKeyboardActivationKey(e.key)) return; e.preventDefault(); handleSelect(); }, [handleSelect], ); const handleFocus = useCallback(() => { setIsFocused(true); setActionModalOpen(false); }, []); const handleBlur = useCallback((e: React.FocusEvent) => { if (e.currentTarget.contains(e.relatedTarget as Node | null)) return; setIsFocused(false); }, []); const armActionModalReturn = useCallback(() => { if (KeyboardMode.keyboardModeEnabled) { setActionModalOpen(true); } }, []); const handleLongPress = useCallback(() => { if (isMobileLayout) { if (channelIsCategory) { setCategoryMenuOpen(true); } else { setMenuOpen(true); } } }, [isMobileLayout, channelIsCategory]); const handleInviteClick = useCallback(() => { armActionModalReturn(); ModalCommands.push( modal(() => ( )), ); }, [channel.id, armActionModalReturn]); const handleOpenChatClick = useCallback(() => { collapseVoiceCallView(); navigateToChannel(); }, [collapseVoiceCallView, navigateToChannel]); const handleCreateChannelClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); armActionModalReturn(); ModalCommands.push( modal(() => ( )), ); }, [guild.id, channel.id, armActionModalReturn], ); const handleChannelSettingsClick = useCallback(() => { armActionModalReturn(); ModalCommands.push( modal(() => ( )), ); }, [channel.id, armActionModalReturn]); const channelSettingsLabel = channelIsCategory ? i18n._(EDIT_CATEGORY_DESCRIPTOR) : i18n._(CHANNEL_SETTINGS_LABEL_DESCRIPTOR); const voiceLocked = channelIsVoice && !Permission.can(Permissions.CONNECT, channel); const channelIconNode = channelIsCategory ? null : ChannelUtils.getIcon( channel, { className: clsx( styles.channelItemIcon, shouldShowSelectedState || (isHighlight && isSelected) ? styles.channelItemIconSelected : isVoiceSelected && channel.type === ChannelTypes.GUILD_VOICE ? styles.channelItemHighlight : isHighlight && !isSelected ? styles.channelItemIconHighlight : styles.channelItemIconUnselected, ), }, voiceLocked ? {locked: true} : channelIsE2EEEncrypted ? {e2eeEncrypted: true} : undefined, ); const typingIndicatorNode = !channelIsCategory && typingUsers.length > 0 && channelTypingIndicatorMode !== ChannelTypingIndicatorMode.HIDDEN && !isSelected ? ( ( {getTypingText(i18n, typingUsers, channel)} )} data-flx="app.channel-item.tooltip" >
{channelTypingIndicatorMode === ChannelTypingIndicatorMode.AVATARS && ( )}
) : null; const categoryCaretNode = channelIsCategory ? ( ) : null; const channelActionsNode = ( <> {typingIndicatorNode} {!isDraggingAnything && ( <> {!channelIsCategory && showMentionBadge && ( )} {shouldShowVoiceUserCount && channel.userLimit != null && (
)} {showChatAffordance && (
)} {allowHoverAffordances && canInvite && !channelIsCategory && (
)} {allowHoverAffordances && channelIsCategory && canManageChannels && (
)} {allowHoverAffordances && canEditChannel && (
)} )} {categoryCaretNode} ); const channelItem = ( } containerClassName={clsx( styles.container, styles.channelReorderTarget, channelIsCategory && styles.channelCategoryReorderTarget, )} extraContent={extraContent} isOver={isOver} dropIndicator={dropIndicator} disabled={!isMobileLayout} data-dnd-name={channel.name} dataScrollIndicator={scrollIndicatorSeverity} dataScrollId={scrollIndicatorId} aria-label={ariaLabel} aria-current={shouldShowSelectedState ? 'page' : undefined} aria-expanded={channelIsCategory ? !isCollapsed : undefined} className={channelItemClassName} onClick={handleSelect} onDoubleClick={handleDoubleClick} onContextMenu={handleContextMenu} onKeyDown={handleKeyDown} onFocus={handleFocus} onBlur={handleBlur} onLongPress={handleLongPress} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} data-flx="app.channel-item.generic-channel-item.select" > ); const handleCloseMenu = useCallback(() => setMenuOpen(false), []); const handleCloseCategoryMenu = useCallback(() => setCategoryMenuOpen(false), []); return ( <> {channelItem} {isMobileLayout && !channelIsCategory && ( )} {isMobileLayout && channelIsCategory && ( )} ); }, );