mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
1117 lines
43 KiB
TypeScript
1117 lines
43 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {Routes} from '@app/app/Routes';
|
|
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
|
import {NativeDragRegion} from '@app/features/app/components/layout/NativeDragRegion';
|
|
import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar';
|
|
import {
|
|
reportSkeletonChannelHeaderLayout,
|
|
reportSkeletonChannelProjection,
|
|
} from '@app/features/app/components/skeleton/SkeletonLayoutMemory';
|
|
import {
|
|
measureSkeletonTextWidthPx,
|
|
measureSkeletonWidthPx,
|
|
useSkeletonLayoutReport,
|
|
} from '@app/features/app/hooks/useSkeletonLayoutMemoryCapture';
|
|
import {useTextOverflow} from '@app/features/app/hooks/useTextOverflow';
|
|
import {ChannelDetailsBottomSheet} from '@app/features/channel/components/bottomsheets/ChannelDetailsBottomSheet';
|
|
import {ChannelSearchBottomSheet} from '@app/features/channel/components/bottomsheets/ChannelSearchBottomSheet';
|
|
import styles from '@app/features/channel/components/ChannelHeader.module.css';
|
|
import {CHANNEL_HEADER_DM_AVATAR_SIZE_PX} from '@app/features/channel/components/ChannelHeaderMetrics';
|
|
import {UserTag} from '@app/features/channel/components/ChannelUserTag';
|
|
import {
|
|
ADD_FRIENDS_TO_GROUP_DESCRIPTOR,
|
|
BACK_DESCRIPTOR,
|
|
CHANNEL_ACTIONS_DESCRIPTOR,
|
|
CREATE_GROUP_DM_DESCRIPTOR,
|
|
EDIT_GROUP_DETAILS_DESCRIPTOR,
|
|
HIDE_MEMBERS_DESCRIPTOR,
|
|
MEMBERS_LIST_UNAVAILABLE_AT_THIS_SCREEN_WIDTH_DESCRIPTOR,
|
|
OPEN_CHANNEL_DETAILS_FOR_DESCRIPTOR,
|
|
OPEN_DIRECT_MESSAGE_DETAILS_FOR_DESCRIPTOR,
|
|
OPEN_DIRECT_MESSAGE_PROFILE_DESCRIPTOR,
|
|
OPEN_GROUP_DETAILS_FOR_DESCRIPTOR,
|
|
OPEN_PROFILE_FOR_DESCRIPTOR,
|
|
SEARCH_DESCRIPTOR,
|
|
SHOW_CHANNEL_LIST_DESCRIPTOR,
|
|
SHOW_MEMBERS_DESCRIPTOR,
|
|
VIDEO_CALL_DESCRIPTOR,
|
|
} from '@app/features/channel/components/channel_header/shared';
|
|
import {useChannelHeaderData} from '@app/features/channel/components/channel_header/useChannelHeaderData';
|
|
import {CallButtons} from '@app/features/channel/components/channel_header_components/CallButtons';
|
|
import {ChannelHeaderIcon} from '@app/features/channel/components/channel_header_components/ChannelHeaderIcon';
|
|
import {ChannelNotificationSettingsButton} from '@app/features/channel/components/channel_header_components/ChannelNotificationSettingsButton';
|
|
import {ChannelPinsButton} from '@app/features/channel/components/channel_header_components/ChannelPinsButton';
|
|
import {
|
|
isUpdaterIconVisible,
|
|
UpdaterIcon,
|
|
} from '@app/features/channel/components/channel_header_components/UpdaterIcon';
|
|
import {
|
|
InboxButton,
|
|
isStaffToolsButtonVisible,
|
|
StaffToolsButton,
|
|
} from '@app/features/channel/components/channel_header_components/UtilityButtons';
|
|
import {useChannelSearchState} from '@app/features/channel/components/channel_view/useChannelSearchState';
|
|
import {MessageSearchBar} from '@app/features/channel/components/message_search_bar/MessageSearchBar';
|
|
import {ChannelTopicModal} from '@app/features/channel/components/modals/ChannelTopicModal';
|
|
import {CreateDMModal} from '@app/features/channel/components/modals/CreateDMModal';
|
|
import {EditGroupModal} from '@app/features/channel/components/modals/EditGroupModal';
|
|
import type {Channel} from '@app/features/channel/models/Channel';
|
|
import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils';
|
|
import {isGroupDmFull} from '@app/features/channel/utils/GroupDmUtils';
|
|
import {
|
|
ADD_TO_FAVORITES_DESCRIPTOR,
|
|
CHANNEL_ADDED_TO_FAVORITES_DESCRIPTOR,
|
|
CHANNEL_REMOVED_FROM_FAVORITES_DESCRIPTOR,
|
|
HIDE_FAVORITES_DESCRIPTOR,
|
|
REMOVE_FROM_FAVORITES_DESCRIPTOR,
|
|
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
|
import type {LexicalSearchInputHandle} from '@app/features/lexical/search/LexicalSearchInput';
|
|
import {useCanFitMemberList} from '@app/features/member/hooks/useMemberListVisible';
|
|
import MemberList from '@app/features/member/state/MemberList';
|
|
import * as FavoritesCommands from '@app/features/messaging/commands/FavoritesCommands';
|
|
import {SafeMarkdown} from '@app/features/messaging/components/markdown';
|
|
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
|
|
import Favorites from '@app/features/messaging/state/Favorites';
|
|
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
|
|
import * as RouterUtils from '@app/features/navigation/utils/RouterUtils';
|
|
import {goBackOr} from '@app/features/platform/components/router/NavigationAdapter';
|
|
import {useLocation} from '@app/features/platform/components/router/RouterReact';
|
|
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
|
import {AddFriendsToGroupModal} from '@app/features/relationship/components/modals/AddFriendsToGroupModal';
|
|
import Relationships from '@app/features/relationship/state/Relationships';
|
|
import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
|
|
import markupStyles from '@app/features/theme/styles/Markup.module.css';
|
|
import {ChannelContextMenu} from '@app/features/ui/action_menu/ChannelContextMenu';
|
|
import {DMContextMenu} from '@app/features/ui/action_menu/DMContextMenu';
|
|
import {GroupDMContextMenu} from '@app/features/ui/action_menu/GroupDMContextMenu';
|
|
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
|
|
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
|
|
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 * as ToastCommands from '@app/features/ui/commands/ToastCommands';
|
|
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
|
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
|
import ContextMenuState, {isContextMenuNodeTarget} from '@app/features/ui/state/ContextMenu';
|
|
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
|
import PopoutState from '@app/features/ui/state/Popout';
|
|
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
|
import * as UserProfileCommands from '@app/features/user/commands/UserProfileCommands';
|
|
import * as CallCommands from '@app/features/voice/commands/CallCommands';
|
|
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
|
import CallState from '@app/features/voice/state/CallState';
|
|
import {computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus';
|
|
import * as CallUtils from '@app/features/voice/utils/CallUtils';
|
|
import {VOICE_CALL_DESCRIPTOR} from '@app/features/voice/utils/VoiceMessageDescriptors';
|
|
import {ME} from '@fluxer/constants/src/AppConstants';
|
|
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
|
import {RelationshipTypes} from '@fluxer/constants/src/UserConstants';
|
|
import {useLingui} from '@lingui/react/macro';
|
|
import {
|
|
ArrowLeftIcon,
|
|
CaretRightIcon,
|
|
EyeSlashIcon,
|
|
ListIcon,
|
|
MagnifyingGlassIcon,
|
|
PencilIcon,
|
|
PhoneIcon,
|
|
StarIcon,
|
|
UserPlusIcon,
|
|
UsersIcon,
|
|
VideoCameraIcon,
|
|
} from '@phosphor-icons/react';
|
|
import {clsx} from 'clsx';
|
|
import {observer} from 'mobx-react-lite';
|
|
import type React from 'react';
|
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
|
|
const {VoiceCallButton, VideoCallButton} = CallButtons;
|
|
|
|
interface ChannelHeaderProps {
|
|
channel?: Channel;
|
|
leftContent?: React.ReactNode;
|
|
voiceCallHeaderSupplement?: React.ReactNode;
|
|
showMembersToggle?: boolean;
|
|
showPins?: boolean;
|
|
onSearchSubmit?: (query: string, segments: Array<SearchSegment>) => void;
|
|
onSearchClose?: () => void;
|
|
isSearchResultsOpen?: boolean;
|
|
forceVoiceCallStyle?: boolean;
|
|
memberListDefaultHiddenForChannel?: boolean;
|
|
onBackClick?: () => void;
|
|
onVoiceCallChromePinChange?: (pinned: boolean) => void;
|
|
}
|
|
|
|
export const ChannelHeader = observer(
|
|
({
|
|
channel,
|
|
leftContent,
|
|
voiceCallHeaderSupplement = null,
|
|
showMembersToggle = false,
|
|
showPins = true,
|
|
onSearchSubmit,
|
|
onSearchClose,
|
|
isSearchResultsOpen,
|
|
forceVoiceCallStyle = false,
|
|
memberListDefaultHiddenForChannel = false,
|
|
onBackClick,
|
|
onVoiceCallChromePinChange,
|
|
}: ChannelHeaderProps) => {
|
|
const {i18n} = useLingui();
|
|
const location = useLocation();
|
|
const headerRef = useRef<HTMLElement>(null);
|
|
const {isMembersOpen} = MemberList;
|
|
const isMobile = MobileLayout.isMobileLayout();
|
|
const isCallChannelConnected = Boolean(MediaEngine.connected && MediaEngine.channelId === channel?.id);
|
|
const channelIsE2EEEncrypted =
|
|
isCallChannelConnected && channel
|
|
? computeChannelE2EEStatus(channel.guildId ?? null, channel.id) === 'encrypted'
|
|
: false;
|
|
const e2eeIconOptions = channelIsE2EEEncrypted ? {e2eeEncrypted: true} : undefined;
|
|
const isVoiceCallActive =
|
|
!isMobile &&
|
|
Boolean(
|
|
channel &&
|
|
(channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM) &&
|
|
isCallChannelConnected &&
|
|
CallState.hasActiveCall(channel.id),
|
|
);
|
|
const isVoiceHeaderActive = isVoiceCallActive || forceVoiceCallStyle;
|
|
const canFitMemberList = useCanFitMemberList();
|
|
const memberListChannelId = channel?.id ?? null;
|
|
const memberListUsesChannelOverride = Boolean(memberListDefaultHiddenForChannel && memberListChannelId);
|
|
const isMembersToggleOpen = memberListUsesChannelOverride
|
|
? MemberList.isDefaultHiddenChannelMembersOpen(memberListChannelId)
|
|
: isMembersOpen;
|
|
const [channelDetailsOpen, setChannelDetailsOpen] = useState(false);
|
|
const [searchSheetOpen, setSearchSheetOpen] = useState(false);
|
|
const [initialTab, setInitialTab] = useState<'members' | 'pins'>('members');
|
|
const {
|
|
searchQuery,
|
|
searchSegments,
|
|
isSearchActive,
|
|
handleSearchInputChange: updateSearchInput,
|
|
handleSearchSubmit: submitSearch,
|
|
handleSearchClose: closeSearch,
|
|
} = useChannelSearchState(channel);
|
|
const latestSearchQueryRef = useRef('');
|
|
const latestSearchSegmentsRef = useRef<Array<SearchSegment>>([]);
|
|
const topicButtonRef = useRef<HTMLDivElement>(null);
|
|
const [isTopicOverflowing, setIsTopicOverflowing] = useState(false);
|
|
const contextMenuTarget = ContextMenuState.contextMenu?.target?.target ?? null;
|
|
const isHeaderContextMenuOpen = Boolean(
|
|
headerRef.current && isContextMenuNodeTarget(contextMenuTarget) && headerRef.current.contains(contextMenuTarget),
|
|
);
|
|
const isHeaderPopoutOpen = PopoutState.isOpen('inbox') || PopoutState.isOpen('channel-pins');
|
|
const isVoiceCallChromePinned = isHeaderContextMenuOpen || isHeaderPopoutOpen;
|
|
const channelId = channel?.id;
|
|
const channelType = channel?.type;
|
|
const channelHasTopic = Boolean(channel?.topic);
|
|
const staffToolsVisible = isStaffToolsButtonVisible();
|
|
const updaterVisible = isUpdaterIconVisible();
|
|
const favoritesVisible = Accessibility.showFavorites;
|
|
useSkeletonLayoutReport(
|
|
() => reportSkeletonChannelHeaderLayout({staffToolsVisible, updaterVisible, favoritesVisible}),
|
|
`${staffToolsVisible}|${updaterVisible}|${favoritesVisible}`,
|
|
);
|
|
useEffect(() => {
|
|
latestSearchQueryRef.current = searchQuery;
|
|
latestSearchSegmentsRef.current = searchSegments;
|
|
}, [searchQuery, searchSegments]);
|
|
useEffect(() => {
|
|
onVoiceCallChromePinChange?.(isVoiceCallChromePinned);
|
|
}, [isVoiceCallChromePinned, onVoiceCallChromePinChange]);
|
|
useEffect(() => {
|
|
return () => {
|
|
onVoiceCallChromePinChange?.(false);
|
|
};
|
|
}, [onVoiceCallChromePinChange]);
|
|
const searchInputRef = useRef<LexicalSearchInputHandle>(null);
|
|
const isSearchResultsVisible = isSearchResultsOpen ?? isSearchActive;
|
|
const dmNameRef = useRef<HTMLSpanElement>(null);
|
|
const groupDMNameRef = useRef<HTMLSpanElement>(null);
|
|
const guildChannelNameRef = useRef<HTMLSpanElement>(null);
|
|
const isDMNameOverflowing = useTextOverflow(dmNameRef);
|
|
const isGroupDMNameOverflowing = useTextOverflow(groupDMNameRef);
|
|
const isGuildChannelNameOverflowing = useTextOverflow(guildChannelNameRef);
|
|
const {isDM, isGroupDM, isPersonalNotes, isGuildChannel, recipient, directMessageName, groupDMName, channelName} =
|
|
useChannelHeaderData(channel);
|
|
const isBotDMRecipient = isDM && (recipient?.bot || recipient?.system);
|
|
const isFavorited = channel && !isPersonalNotes ? !!Favorites.getChannel(channel.id) : false;
|
|
const channelDetailsLabel =
|
|
isDM && directMessageName
|
|
? i18n._(OPEN_DIRECT_MESSAGE_DETAILS_FOR_DESCRIPTOR, {directMessageName})
|
|
: isGroupDM && groupDMName
|
|
? i18n._(OPEN_GROUP_DETAILS_FOR_DESCRIPTOR, {groupDMName})
|
|
: i18n._(OPEN_CHANNEL_DETAILS_FOR_DESCRIPTOR, {channelName});
|
|
const userProfileLabel = directMessageName
|
|
? i18n._(OPEN_PROFILE_FOR_DESCRIPTOR, {directMessageName})
|
|
: i18n._(OPEN_DIRECT_MESSAGE_PROFILE_DESCRIPTOR);
|
|
const handleSearchInputChange = useCallback(
|
|
(query: string, segments: Array<SearchSegment>) => {
|
|
updateSearchInput(query, segments);
|
|
latestSearchQueryRef.current = query;
|
|
latestSearchSegmentsRef.current = segments;
|
|
},
|
|
[updateSearchInput],
|
|
);
|
|
const handleSearchSubmit = useCallback(() => {
|
|
const query = latestSearchQueryRef.current;
|
|
if (onSearchSubmit) {
|
|
onSearchSubmit(query, latestSearchSegmentsRef.current);
|
|
return;
|
|
}
|
|
submitSearch(query, latestSearchSegmentsRef.current);
|
|
}, [onSearchSubmit, submitSearch]);
|
|
const handleSearchClose = useCallback(() => {
|
|
updateSearchInput('', []);
|
|
latestSearchQueryRef.current = '';
|
|
latestSearchSegmentsRef.current = [];
|
|
if (onSearchClose) {
|
|
onSearchClose();
|
|
return;
|
|
}
|
|
closeSearch();
|
|
}, [updateSearchInput, onSearchClose, closeSearch]);
|
|
const handleOpenCreateGroupDM = useCallback(() => {
|
|
if (!channel) return;
|
|
const initialRecipientIds = Array.from(channel.recipientIds);
|
|
const excludeChannelId = channel.type === ChannelTypes.GROUP_DM ? channel.id : undefined;
|
|
ModalCommands.push(
|
|
modal(() => (
|
|
<CreateDMModal
|
|
initialSelectedUserIds={initialRecipientIds}
|
|
duplicateExcludeChannelId={excludeChannelId}
|
|
data-flx="channel.channel-header.handle-open-create-group-dm.create-dm-modal"
|
|
/>
|
|
)),
|
|
);
|
|
}, [channel]);
|
|
const handleOpenEditGroup = useCallback(() => {
|
|
if (!channel) return;
|
|
ModalCommands.push(
|
|
modal(() => (
|
|
<EditGroupModal
|
|
channelId={channel.id}
|
|
data-flx="channel.channel-header.handle-open-edit-group.edit-group-modal"
|
|
/>
|
|
)),
|
|
);
|
|
}, [channel]);
|
|
const handleOpenAddFriendsToGroup = useCallback(() => {
|
|
if (!channel) return;
|
|
ModalCommands.push(
|
|
modal(() => (
|
|
<AddFriendsToGroupModal
|
|
channelId={channel.id}
|
|
data-flx="channel.channel-header.handle-open-add-friends-to-group.add-friends-to-group-modal"
|
|
/>
|
|
)),
|
|
);
|
|
}, [channel]);
|
|
const handleToggleMembers = useCallback(() => {
|
|
if (!canFitMemberList) return;
|
|
if (memberListUsesChannelOverride) {
|
|
MemberList.toggleDefaultHiddenChannelMembers(memberListChannelId);
|
|
return;
|
|
}
|
|
LayoutCommands.toggleMembers(!isMembersOpen);
|
|
}, [isMembersOpen, canFitMemberList, memberListChannelId, memberListUsesChannelOverride]);
|
|
useEffect(() => {
|
|
const handleChannelDetailsOpen = (payload?: unknown) => {
|
|
const {initialTab} = (payload ?? {}) as {initialTab?: 'members' | 'pins'};
|
|
setInitialTab(initialTab || 'members');
|
|
setChannelDetailsOpen(true);
|
|
};
|
|
return ComponentBus.subscribe('CHANNEL_DETAILS_OPEN', handleChannelDetailsOpen);
|
|
}, []);
|
|
useEffect(() => {
|
|
if (!showMembersToggle) return;
|
|
return ComponentBus.subscribe('CHANNEL_MEMBER_LIST_TOGGLE', () => {
|
|
if (canFitMemberList) {
|
|
if (memberListUsesChannelOverride) {
|
|
MemberList.toggleDefaultHiddenChannelMembers(memberListChannelId);
|
|
return;
|
|
}
|
|
LayoutCommands.toggleMembers(!isMembersOpen);
|
|
}
|
|
});
|
|
}, [showMembersToggle, canFitMemberList, isMembersOpen, memberListChannelId, memberListUsesChannelOverride]);
|
|
useEffect(() => {
|
|
if (!channel?.topic) {
|
|
setIsTopicOverflowing(false);
|
|
return;
|
|
}
|
|
const el = topicButtonRef.current;
|
|
if (!el) return;
|
|
let rafId: number | null = null;
|
|
const checkOverflow = () => {
|
|
rafId = null;
|
|
const {scrollWidth, clientWidth} = el;
|
|
setIsTopicOverflowing(scrollWidth - clientWidth > 1);
|
|
};
|
|
const scheduleOverflowCheck = () => {
|
|
if (rafId != null) return;
|
|
rafId = requestAnimationFrame(checkOverflow);
|
|
};
|
|
scheduleOverflowCheck();
|
|
const resizeObserver = new ResizeObserver(scheduleOverflowCheck);
|
|
resizeObserver.observe(el);
|
|
return () => {
|
|
if (rafId != null) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
resizeObserver.disconnect();
|
|
};
|
|
}, [channel?.topic]);
|
|
const handleOpenUserProfile = useCallback(() => {
|
|
if (!recipient) return;
|
|
UserProfileCommands.openUserProfile(recipient.id);
|
|
}, [recipient]);
|
|
const handleBackClick = useCallback(() => {
|
|
if (onBackClick) {
|
|
onBackClick();
|
|
return;
|
|
}
|
|
if (isDM || isGroupDM || isPersonalNotes) {
|
|
RouterUtils.transitionTo(Routes.ME);
|
|
} else if (Routes.isFavoritesRoute(location.pathname)) {
|
|
RouterUtils.transitionTo(Routes.FAVORITES);
|
|
} else if (isGuildChannel && channel?.guildId) {
|
|
NavigationCommands.selectChannel(channel.guildId);
|
|
} else {
|
|
goBackOr(Routes.ME);
|
|
}
|
|
}, [onBackClick, isDM, isGroupDM, isPersonalNotes, isGuildChannel, channel?.guildId, location.pathname]);
|
|
const handleChannelDetailsClick = () => {
|
|
setInitialTab('members');
|
|
setChannelDetailsOpen(true);
|
|
};
|
|
const handleOpenChannelTopic = useCallback(() => {
|
|
if (!channel) return;
|
|
ModalCommands.push(
|
|
modal(() => <ChannelTopicModal channelId={channel.id} data-flx="channel.channel-header.channel-topic-modal" />),
|
|
);
|
|
}, [channel]);
|
|
const handleSearchClick = () => {
|
|
setSearchSheetOpen(true);
|
|
};
|
|
const handleContextMenu = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
if (channel && isGuildChannel) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
|
<ChannelContextMenu
|
|
channel={channel}
|
|
onClose={onClose}
|
|
data-flx="channel.channel-header.handle-context-menu.channel-context-menu"
|
|
/>
|
|
));
|
|
}
|
|
},
|
|
[channel, isGuildChannel],
|
|
);
|
|
const handleUserContextMenu = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
if (!channel) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (isGroupDM) {
|
|
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
|
<GroupDMContextMenu
|
|
channel={channel}
|
|
onClose={onClose}
|
|
data-flx="channel.channel-header.handle-user-context-menu.group-dm-context-menu"
|
|
/>
|
|
));
|
|
} else if (isDM && recipient) {
|
|
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
|
<DMContextMenu
|
|
channel={channel}
|
|
recipient={recipient}
|
|
onClose={onClose}
|
|
data-flx="channel.channel-header.handle-user-context-menu.dm-context-menu"
|
|
/>
|
|
));
|
|
}
|
|
},
|
|
[channel, isDM, isGroupDM, recipient],
|
|
);
|
|
const handleMobileVoiceCall = useCallback(
|
|
async (event: React.MouseEvent) => {
|
|
if (!channel) return;
|
|
const isConnected = MediaEngine.connected;
|
|
const connectedChannelId = MediaEngine.channelId;
|
|
const isInCall = isConnected && connectedChannelId === channel.id;
|
|
if (isInCall) {
|
|
void CallCommands.leaveCall(channel.id);
|
|
} else if (CallState.hasActiveCall(channel.id)) {
|
|
CallCommands.joinCall(channel.id);
|
|
} else {
|
|
await CallUtils.requestStartCall(
|
|
i18n,
|
|
channel.id,
|
|
CallUtils.getCallStartRequestOptions(event, {kind: 'voice'}),
|
|
);
|
|
}
|
|
},
|
|
[channel, i18n],
|
|
);
|
|
const handleMobileVideoCall = useCallback(
|
|
async (event: React.MouseEvent) => {
|
|
if (!channel) return;
|
|
const isConnected = MediaEngine.connected;
|
|
const connectedChannelId = MediaEngine.channelId;
|
|
const isInCall = isConnected && connectedChannelId === channel.id;
|
|
if (isInCall) {
|
|
void CallCommands.leaveCall(channel.id);
|
|
} else if (CallState.hasActiveCall(channel.id)) {
|
|
CallCommands.joinCall(channel.id);
|
|
} else {
|
|
await CallUtils.requestStartCall(
|
|
i18n,
|
|
channel.id,
|
|
CallUtils.getCallStartRequestOptions(event, {kind: 'video'}),
|
|
);
|
|
}
|
|
},
|
|
[channel, i18n],
|
|
);
|
|
const handleToggleFavorite = useCallback(() => {
|
|
if (!channel || isPersonalNotes) return;
|
|
if (isFavorited) {
|
|
Favorites.removeChannel(channel.id);
|
|
ToastCommands.createToast({type: 'success', children: i18n._(CHANNEL_REMOVED_FROM_FAVORITES_DESCRIPTOR)});
|
|
} else {
|
|
Favorites.addChannel(channel.id, channel.guildId ?? ME);
|
|
ToastCommands.createToast({type: 'success', children: i18n._(CHANNEL_ADDED_TO_FAVORITES_DESCRIPTOR)});
|
|
}
|
|
}, [channel, isPersonalNotes, isFavorited]);
|
|
const handleFavoriteContextMenu = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
|
|
<MenuGroup data-flx="channel.channel-header.handle-favorite-context-menu.menu-group">
|
|
<MenuItem
|
|
icon={<EyeSlashIcon data-flx="channel.channel-header.handle-favorite-context-menu.eye-slash-icon" />}
|
|
onClick={() => {
|
|
onClose();
|
|
FavoritesCommands.confirmHideFavorites(undefined, i18n);
|
|
}}
|
|
danger
|
|
data-flx="channel.channel-header.handle-favorite-context-menu.menu-item.close"
|
|
>
|
|
{i18n._(HIDE_FAVORITES_DESCRIPTOR)}
|
|
</MenuItem>
|
|
</MenuGroup>
|
|
));
|
|
},
|
|
[i18n],
|
|
);
|
|
const isGroupDMFull = isGroupDmFull(channel);
|
|
const isFriendDM =
|
|
isDM &&
|
|
recipient &&
|
|
!isBotDMRecipient &&
|
|
Relationships.getRelationship(recipient.id)?.type === RelationshipTypes.FRIEND;
|
|
const shouldShowCreateGroupButton = !!channel && !isMobile && !isPersonalNotes && isFriendDM && !isGroupDM;
|
|
const shouldShowAddFriendsButton = !!channel && !isMobile && !isPersonalNotes && isGroupDM && !isGroupDMFull;
|
|
const hasChannel = Boolean(channel) && !isPersonalNotes;
|
|
const hasCallableRecipients = hasChannel && (isDM || isGroupDM);
|
|
let desktopLeadingActionCount = 0;
|
|
if (hasChannel && isGuildChannel) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
if (hasCallableRecipients && !(isDM && isBotDMRecipient)) {
|
|
desktopLeadingActionCount += 2;
|
|
}
|
|
if (showPins && Boolean(channel)) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
if (hasChannel && isFriendDM && !isGroupDM) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
if (hasChannel && isGroupDM && !isGroupDMFull) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
if (hasChannel && favoritesVisible) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
if (showMembersToggle) {
|
|
desktopLeadingActionCount += 1;
|
|
}
|
|
let mobileActionCount = 0;
|
|
if (hasChannel && favoritesVisible) {
|
|
mobileActionCount += 1;
|
|
}
|
|
if (hasCallableRecipients) {
|
|
mobileActionCount += 2;
|
|
}
|
|
if (isGuildChannel) {
|
|
mobileActionCount += 1;
|
|
}
|
|
const searchPanelOpen = Boolean(isSearchResultsVisible);
|
|
useSkeletonLayoutReport(
|
|
() => {
|
|
if (channelId == null || channelType == null) {
|
|
return;
|
|
}
|
|
const nameElement = dmNameRef.current ?? groupDMNameRef.current ?? guildChannelNameRef.current;
|
|
reportSkeletonChannelProjection(channelId, channelType, {
|
|
showTopic: channelHasTopic,
|
|
nameWidthPx: measureSkeletonWidthPx(nameElement),
|
|
topicWidthPx: measureSkeletonTextWidthPx(topicButtonRef.current),
|
|
desktopLeadingActionCount,
|
|
mobileActionCount,
|
|
memberListVisible: isMembersToggleOpen,
|
|
searchPanelOpen,
|
|
});
|
|
},
|
|
`${channelId}|${channelType ?? ''}|${channelHasTopic}|${channel?.topic ?? ''}|${channelName}|${directMessageName}|${groupDMName}|${desktopLeadingActionCount}|${mobileActionCount}|${searchPanelOpen}|${isMembersToggleOpen}|${Accessibility.fontSize}|${Accessibility.zoomLevel}`,
|
|
);
|
|
return (
|
|
<>
|
|
<header
|
|
ref={headerRef}
|
|
className={clsx(styles.headerWrapper, isVoiceHeaderActive && styles.headerWrapperCallActive)}
|
|
data-voice-call-channel-header={isVoiceHeaderActive ? 'true' : undefined}
|
|
data-flx="channel.channel-header.header-wrapper"
|
|
>
|
|
<NativeDragRegion
|
|
className={clsx(styles.headerContainer, isVoiceHeaderActive && styles.headerContainerCallActive)}
|
|
data-flx="channel.channel-header.header-container"
|
|
>
|
|
<div className={styles.headerLeftSection} data-flx="channel.channel-header.header-left-section">
|
|
{isMobile ? (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring">
|
|
<button
|
|
type="button"
|
|
className={styles.backButton}
|
|
aria-label={i18n._(BACK_DESCRIPTOR)}
|
|
onClick={handleBackClick}
|
|
data-flx="channel.channel-header.back-button.back-click"
|
|
>
|
|
<ArrowLeftIcon
|
|
className={styles.backIconBold}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.back-icon-bold"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
) : (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--2">
|
|
<button
|
|
type="button"
|
|
className={styles.backButtonDesktop}
|
|
aria-label={i18n._(SHOW_CHANNEL_LIST_DESCRIPTOR)}
|
|
onClick={handleBackClick}
|
|
data-flx="channel.channel-header.back-button-desktop.back-click"
|
|
>
|
|
<ListIcon className={styles.backIcon} data-flx="channel.channel-header.back-icon" />
|
|
</button>
|
|
</FocusRing>
|
|
)}
|
|
<div className={styles.leftContentContainer} data-flx="channel.channel-header.left-content-container">
|
|
{leftContent ? (
|
|
leftContent
|
|
) : channel ? (
|
|
isMobile ? (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--3">
|
|
<button
|
|
type="button"
|
|
className={styles.mobileButton}
|
|
aria-label={channelDetailsLabel}
|
|
aria-haspopup="dialog"
|
|
onClick={handleChannelDetailsClick}
|
|
data-flx="channel.channel-header.mobile-button.channel-details-click"
|
|
>
|
|
{isDM && recipient ? (
|
|
<>
|
|
<StatusAwareAvatar
|
|
user={recipient}
|
|
size={CHANNEL_HEADER_DM_AVATAR_SIZE_PX}
|
|
showOffline={true}
|
|
data-flx="channel.channel-header.status-aware-avatar"
|
|
/>
|
|
<span className={styles.dmNameWrapper} data-flx="channel.channel-header.dm-name-wrapper">
|
|
<Tooltip
|
|
text={isDMNameOverflowing && directMessageName ? directMessageName : ''}
|
|
data-flx="channel.channel-header.tooltip"
|
|
>
|
|
<span
|
|
ref={dmNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name"
|
|
>
|
|
{directMessageName}
|
|
</span>
|
|
</Tooltip>
|
|
{isBotDMRecipient && (
|
|
<UserTag
|
|
className={styles.userTag}
|
|
system={recipient.system}
|
|
data-flx="channel.channel-header.user-tag"
|
|
/>
|
|
)}
|
|
</span>
|
|
<CaretRightIcon
|
|
className={styles.caretRight}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.caret-right"
|
|
/>
|
|
</>
|
|
) : isGroupDM ? (
|
|
<>
|
|
<GroupDMAvatar
|
|
channel={channel}
|
|
size={CHANNEL_HEADER_DM_AVATAR_SIZE_PX}
|
|
data-flx="channel.channel-header.group-dm-avatar"
|
|
/>
|
|
<Tooltip
|
|
text={isGroupDMNameOverflowing && groupDMName ? groupDMName : ''}
|
|
data-flx="channel.channel-header.tooltip--2"
|
|
>
|
|
<span
|
|
ref={groupDMNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--2"
|
|
>
|
|
{groupDMName}
|
|
</span>
|
|
</Tooltip>
|
|
<CaretRightIcon
|
|
className={styles.caretRight}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.caret-right--2"
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
{ChannelUtils.getIcon(channel, {className: styles.channelIcon}, e2eeIconOptions)}
|
|
<Tooltip
|
|
text={isGuildChannelNameOverflowing && channelName ? channelName : ''}
|
|
data-flx="channel.channel-header.tooltip--3"
|
|
>
|
|
<span
|
|
ref={guildChannelNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--3"
|
|
>
|
|
{channelName}
|
|
</span>
|
|
</Tooltip>
|
|
<CaretRightIcon
|
|
className={styles.caretRight}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.caret-right--3"
|
|
/>
|
|
</>
|
|
)}
|
|
</button>
|
|
</FocusRing>
|
|
) : isDM && recipient ? (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--4">
|
|
<button
|
|
type="button"
|
|
className={styles.desktopButton}
|
|
aria-label={userProfileLabel}
|
|
onClick={handleOpenUserProfile}
|
|
onContextMenu={handleUserContextMenu}
|
|
data-flx="channel.channel-header.desktop-button.open-user-profile"
|
|
>
|
|
<StatusAwareAvatar
|
|
user={recipient}
|
|
size={CHANNEL_HEADER_DM_AVATAR_SIZE_PX}
|
|
showOffline={true}
|
|
data-flx="channel.channel-header.status-aware-avatar--2"
|
|
/>
|
|
<span className={styles.dmNameWrapper} data-flx="channel.channel-header.dm-name-wrapper--2">
|
|
<Tooltip
|
|
text={isDMNameOverflowing ? directMessageName : ''}
|
|
data-flx="channel.channel-header.tooltip--4"
|
|
>
|
|
<span
|
|
ref={dmNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--4"
|
|
>
|
|
{directMessageName}
|
|
</span>
|
|
</Tooltip>
|
|
{isBotDMRecipient && (
|
|
<UserTag
|
|
className={styles.userTag}
|
|
system={recipient.system}
|
|
data-flx="channel.channel-header.user-tag--2"
|
|
/>
|
|
)}
|
|
</span>
|
|
</button>
|
|
</FocusRing>
|
|
) : isGroupDM ? (
|
|
isMobile ? (
|
|
<div className={styles.avatarWrapper} data-flx="channel.channel-header.avatar-wrapper">
|
|
<GroupDMAvatar
|
|
channel={channel}
|
|
size={CHANNEL_HEADER_DM_AVATAR_SIZE_PX}
|
|
data-flx="channel.channel-header.group-dm-avatar--2"
|
|
/>
|
|
<Tooltip
|
|
text={isGroupDMNameOverflowing && groupDMName ? groupDMName : ''}
|
|
data-flx="channel.channel-header.tooltip--5"
|
|
>
|
|
<span
|
|
ref={groupDMNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--5"
|
|
>
|
|
{groupDMName}
|
|
</span>
|
|
</Tooltip>
|
|
</div>
|
|
) : (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--5">
|
|
<div
|
|
className={styles.groupDmHeaderTrigger}
|
|
role="button"
|
|
aria-label={i18n._(EDIT_GROUP_DETAILS_DESCRIPTOR)}
|
|
tabIndex={0}
|
|
onClick={handleOpenEditGroup}
|
|
onContextMenu={handleUserContextMenu}
|
|
onKeyDown={(event) => {
|
|
if (isKeyboardActivationKey(event.key)) {
|
|
event.preventDefault();
|
|
handleOpenEditGroup();
|
|
}
|
|
}}
|
|
data-flx="channel.channel-header.group-dm-header-trigger.open-edit-group"
|
|
>
|
|
<div
|
|
className={styles.groupDmHeaderInner}
|
|
data-flx="channel.channel-header.group-dm-header-inner"
|
|
>
|
|
<GroupDMAvatar
|
|
channel={channel}
|
|
size={CHANNEL_HEADER_DM_AVATAR_SIZE_PX}
|
|
data-flx="channel.channel-header.group-dm-avatar--3"
|
|
/>
|
|
<div className={styles.dmNameWrapper} data-flx="channel.channel-header.dm-name-wrapper--3">
|
|
<Tooltip
|
|
text={isGroupDMNameOverflowing && groupDMName ? groupDMName : ''}
|
|
data-flx="channel.channel-header.tooltip--6"
|
|
>
|
|
<span
|
|
ref={groupDMNameRef}
|
|
className={clsx(styles.channelName, styles.groupDmChannelName)}
|
|
data-flx="channel.channel-header.channel-name--6"
|
|
>
|
|
{groupDMName}
|
|
</span>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<PencilIcon
|
|
className={styles.groupDmEditIcon}
|
|
size={16}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.group-dm-edit-icon"
|
|
/>
|
|
</div>
|
|
</FocusRing>
|
|
)
|
|
) : isPersonalNotes ? (
|
|
<div className={styles.avatarWrapper} data-flx="channel.channel-header.avatar-wrapper--2">
|
|
{ChannelUtils.getIcon(channel, {className: styles.channelIcon}, e2eeIconOptions)}
|
|
<Tooltip
|
|
text={isGuildChannelNameOverflowing && channelName ? channelName : ''}
|
|
data-flx="channel.channel-header.tooltip--7"
|
|
>
|
|
<span
|
|
ref={guildChannelNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--7"
|
|
>
|
|
{channelName}
|
|
</span>
|
|
</Tooltip>
|
|
</div>
|
|
) : (
|
|
<div
|
|
role="group"
|
|
className={styles.channelInfoContainer}
|
|
onContextMenu={handleContextMenu}
|
|
data-flx="channel.channel-header.channel-info-container.context-menu"
|
|
>
|
|
{ChannelUtils.getIcon(channel, {className: styles.channelIcon}, e2eeIconOptions)}
|
|
<Tooltip
|
|
text={isGuildChannelNameOverflowing && channelName ? channelName : ''}
|
|
data-flx="channel.channel-header.tooltip--8"
|
|
>
|
|
<span
|
|
ref={guildChannelNameRef}
|
|
className={styles.channelName}
|
|
data-flx="channel.channel-header.channel-name--8"
|
|
>
|
|
{channelName}
|
|
</span>
|
|
</Tooltip>
|
|
{channel.topic && (
|
|
<>
|
|
<span className={styles.topicDivider} data-flx="channel.channel-header.topic-divider">
|
|
•
|
|
</span>
|
|
<div className={styles.topicContainer} data-flx="channel.channel-header.topic-container">
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--6">
|
|
<div
|
|
role="button"
|
|
ref={topicButtonRef}
|
|
className={clsx(
|
|
markupStyles.markup,
|
|
styles.topicButton,
|
|
isTopicOverflowing && styles.topicButtonOverflow,
|
|
)}
|
|
onClick={handleOpenChannelTopic}
|
|
onKeyDown={(e) => {
|
|
if (!isKeyboardActivationKey(e.key)) return;
|
|
e.preventDefault();
|
|
handleOpenChannelTopic();
|
|
}}
|
|
tabIndex={0}
|
|
data-flx="channel.channel-header.topic-button.push"
|
|
>
|
|
<SafeMarkdown
|
|
content={channel.topic}
|
|
options={{
|
|
context: MarkdownContext.RESTRICTED_INLINE_REPLY,
|
|
disableInteractions: true,
|
|
channelId: channel.id,
|
|
}}
|
|
data-flx="channel.channel-header.safe-markdown"
|
|
/>
|
|
</div>
|
|
</FocusRing>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={styles.headerRightSection}
|
|
role="group"
|
|
aria-label={i18n._(CHANNEL_ACTIONS_DESCRIPTOR)}
|
|
data-flx="channel.channel-header.header-right-section"
|
|
>
|
|
{voiceCallHeaderSupplement && (
|
|
<div
|
|
className={styles.voiceCallHeaderSupplement}
|
|
data-flx="channel.channel-header.voice-call-header-supplement"
|
|
>
|
|
{voiceCallHeaderSupplement}
|
|
</div>
|
|
)}
|
|
{isMobile && channel && !isPersonalNotes && Accessibility.showFavorites && (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--7">
|
|
<button
|
|
type="button"
|
|
className={styles.iconButtonMobile}
|
|
aria-label={
|
|
isFavorited ? i18n._(REMOVE_FROM_FAVORITES_DESCRIPTOR) : i18n._(ADD_TO_FAVORITES_DESCRIPTOR)
|
|
}
|
|
aria-pressed={isFavorited}
|
|
onClick={handleToggleFavorite}
|
|
onContextMenu={handleFavoriteContextMenu}
|
|
data-flx="channel.channel-header.icon-button-mobile.toggle-favorite"
|
|
>
|
|
<StarIcon
|
|
className={styles.buttonIconMobile}
|
|
weight={isFavorited ? 'fill' : 'bold'}
|
|
data-flx="channel.channel-header.button-icon-mobile"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
)}
|
|
{isMobile && (isDM || isGroupDM) && !isPersonalNotes && (
|
|
<>
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--8">
|
|
<button
|
|
type="button"
|
|
className={styles.iconButtonMobile}
|
|
aria-label={i18n._(VOICE_CALL_DESCRIPTOR)}
|
|
onClick={handleMobileVoiceCall}
|
|
data-flx="channel.channel-header.icon-button-mobile.mobile-voice-call"
|
|
>
|
|
<PhoneIcon
|
|
className={styles.buttonIconMobile}
|
|
data-flx="channel.channel-header.button-icon-mobile--2"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--9">
|
|
<button
|
|
type="button"
|
|
className={styles.iconButtonMobile}
|
|
aria-label={i18n._(VIDEO_CALL_DESCRIPTOR)}
|
|
onClick={handleMobileVideoCall}
|
|
data-flx="channel.channel-header.icon-button-mobile.mobile-video-call"
|
|
>
|
|
<VideoCameraIcon
|
|
className={styles.buttonIconMobile}
|
|
data-flx="channel.channel-header.button-icon-mobile--3"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
</>
|
|
)}
|
|
{isMobile && isGuildChannel && (
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--10">
|
|
<button
|
|
type="button"
|
|
className={styles.iconButtonMobile}
|
|
aria-label={i18n._(SEARCH_DESCRIPTOR)}
|
|
onClick={handleSearchClick}
|
|
data-flx="channel.channel-header.icon-button-mobile.search-click"
|
|
>
|
|
<MagnifyingGlassIcon
|
|
className={styles.buttonIconMobile}
|
|
weight="bold"
|
|
data-flx="channel.channel-header.button-icon-mobile--4"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
)}
|
|
{channel && isGuildChannel && !isMobile && !isPersonalNotes && (
|
|
<ChannelNotificationSettingsButton
|
|
channel={channel}
|
|
data-flx="channel.channel-header.channel-notification-settings-button"
|
|
/>
|
|
)}
|
|
{(isDM || isGroupDM) && channel && !isMobile && !(isDM && isBotDMRecipient) && (
|
|
<>
|
|
<VoiceCallButton channel={channel} data-flx="channel.channel-header.voice-call-button" />
|
|
<VideoCallButton channel={channel} data-flx="channel.channel-header.video-call-button" />
|
|
</>
|
|
)}
|
|
{showPins && channel && !isMobile && (
|
|
<ChannelPinsButton channel={channel} data-flx="channel.channel-header.channel-pins-button" />
|
|
)}
|
|
{shouldShowCreateGroupButton && (
|
|
<ChannelHeaderIcon
|
|
icon={UserPlusIcon}
|
|
label={i18n._(CREATE_GROUP_DM_DESCRIPTOR)}
|
|
onClick={handleOpenCreateGroupDM}
|
|
data-flx="channel.channel-header.channel-header-icon.open-create-group-dm"
|
|
/>
|
|
)}
|
|
{shouldShowAddFriendsButton && (
|
|
<ChannelHeaderIcon
|
|
icon={UserPlusIcon}
|
|
label={i18n._(ADD_FRIENDS_TO_GROUP_DESCRIPTOR)}
|
|
onClick={handleOpenAddFriendsToGroup}
|
|
data-flx="channel.channel-header.channel-header-icon.open-add-friends-to-group"
|
|
/>
|
|
)}
|
|
{channel && !isMobile && !isPersonalNotes && Accessibility.showFavorites && (
|
|
<Tooltip
|
|
text={isFavorited ? i18n._(REMOVE_FROM_FAVORITES_DESCRIPTOR) : i18n._(ADD_TO_FAVORITES_DESCRIPTOR)}
|
|
position="bottom"
|
|
data-flx="channel.channel-header.tooltip--9"
|
|
>
|
|
<FocusRing offset={-2} data-flx="channel.channel-header.focus-ring--11">
|
|
<button
|
|
type="button"
|
|
className={isFavorited ? styles.iconButtonSelected : styles.iconButtonDefault}
|
|
aria-label={
|
|
isFavorited ? i18n._(REMOVE_FROM_FAVORITES_DESCRIPTOR) : i18n._(ADD_TO_FAVORITES_DESCRIPTOR)
|
|
}
|
|
aria-pressed={isFavorited}
|
|
onClick={handleToggleFavorite}
|
|
onContextMenu={handleFavoriteContextMenu}
|
|
data-flx="channel.channel-header.icon-button.toggle-favorite"
|
|
>
|
|
<StarIcon
|
|
className={styles.buttonIcon}
|
|
weight={isFavorited ? 'fill' : 'bold'}
|
|
data-flx="channel.channel-header.button-icon"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
</Tooltip>
|
|
)}
|
|
{showMembersToggle && !isMobile && (
|
|
<ChannelHeaderIcon
|
|
icon={UsersIcon}
|
|
isSelected={isMembersToggleOpen}
|
|
label={
|
|
!canFitMemberList
|
|
? i18n._(MEMBERS_LIST_UNAVAILABLE_AT_THIS_SCREEN_WIDTH_DESCRIPTOR)
|
|
: isMembersToggleOpen
|
|
? i18n._(HIDE_MEMBERS_DESCRIPTOR)
|
|
: i18n._(SHOW_MEMBERS_DESCRIPTOR)
|
|
}
|
|
onClick={handleToggleMembers}
|
|
disabled={!canFitMemberList}
|
|
aria-pressed={isMembersToggleOpen}
|
|
keybindAction="chat_toggle_member_list"
|
|
data-flx="channel.channel-header.channel-header-icon.toggle-members"
|
|
/>
|
|
)}
|
|
{!isMobile && channel && !isPersonalNotes && (
|
|
<FocusRing offset={-2} within data-flx="channel.channel-header.focus-ring--12">
|
|
<div
|
|
className={styles.messageSearchFocusWrapper}
|
|
data-flx="channel.channel-header.message-search-focus-wrapper"
|
|
>
|
|
<MessageSearchBar
|
|
channel={channel}
|
|
value={searchQuery}
|
|
onChange={handleSearchInputChange}
|
|
onSearch={handleSearchSubmit}
|
|
onClear={handleSearchClose}
|
|
isResultsOpen={Boolean(isSearchResultsVisible)}
|
|
onCloseResults={handleSearchClose}
|
|
inputRefExternal={searchInputRef}
|
|
highContrast={isVoiceHeaderActive}
|
|
data-flx="channel.channel-header.message-search-bar.search-input-change"
|
|
/>
|
|
</div>
|
|
</FocusRing>
|
|
)}
|
|
{!isMobile && <UpdaterIcon data-flx="channel.channel-header.updater-icon" />}
|
|
{!isMobile && <StaffToolsButton data-flx="channel.channel-header.staff-tools-button" />}
|
|
{!isMobile && <InboxButton data-flx="channel.channel-header.inbox-button" />}
|
|
</div>
|
|
</NativeDragRegion>
|
|
</header>
|
|
{channel && (
|
|
<>
|
|
<ChannelDetailsBottomSheet
|
|
isOpen={channelDetailsOpen}
|
|
onClose={() => {
|
|
setChannelDetailsOpen(false);
|
|
setInitialTab('members');
|
|
}}
|
|
channel={channel}
|
|
initialTab={initialTab}
|
|
data-flx="channel.channel-header.channel-details-bottom-sheet"
|
|
/>
|
|
<ChannelSearchBottomSheet
|
|
isOpen={searchSheetOpen}
|
|
onClose={() => setSearchSheetOpen(false)}
|
|
channel={channel}
|
|
data-flx="channel.channel-header.channel-search-bottom-sheet"
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
);
|