/* Copyright (C) 2025 Element Creations Ltd Copyright 2024 New Vector Ltd. Copyright 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import React, { type JSX, useCallback, useState } from "react"; import { Text, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid"; import CloseCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid"; import RoomInfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info-solid"; import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-solid"; import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; import { HistoryVisibility, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { Flex, Box } from "@element-hq/web-shared-components"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { HistoryIcon, UserProfileSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { useRoomName } from "../../../../hooks/useRoomName.ts"; import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx"; import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts"; import { _t } from "../../../../languageHandler.tsx"; import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx"; import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts"; import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts"; import { useFeatureEnabled } from "../../../../hooks/useSettings.ts"; import { useEncryptionStatus } from "../../../../hooks/useEncryptionStatus.ts"; import { E2EStatus } from "../../../../utils/ShieldUtils.ts"; import FacePile from "../../elements/FacePile.tsx"; import { useRoomState } from "../../../../hooks/useRoomState.ts"; import RoomAvatar from "../../avatars/RoomAvatar.tsx"; import { formatCount } from "../../../../utils/FormattingUtils.ts"; import RightPanelStore from "../../../../stores/right-panel/RightPanelStore.ts"; import PosthogTrackers from "../../../../PosthogTrackers.ts"; import { VideoRoomChatButton } from "./VideoRoomChatButton.tsx"; import { RoomKnocksBar } from "../RoomKnocksBar.tsx"; import { isVideoRoom as calcIsVideoRoom } from "../../../../utils/video-rooms.ts"; import { notificationLevelToIndicator } from "../../../../utils/notifications.ts"; import { CallGuestLinkButton } from "./CallGuestLinkButton.tsx"; import { type ButtonEvent } from "../../elements/AccessibleButton.tsx"; import WithPresenceIndicator, { useDmMember } from "../../avatars/WithPresenceIndicator.tsx"; import { type IOOBData } from "../../../../stores/ThreepidInviteStore.ts"; import { MainSplitContentType } from "../../../structures/RoomView.tsx"; import defaultDispatcher from "../../../../dispatcher/dispatcher.ts"; import { RoomSettingsTab } from "../../dialogs/RoomSettingsDialog.tsx"; import { useScopedRoomContext } from "../../../../contexts/ScopedRoomContext.tsx"; import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx"; import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx"; import { LocalRoom } from "../../../../models/LocalRoom.ts"; import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; function RoomHeaderButtons({ room, legacyAdditionalButtons, extraButtons, }: { room: Room; legacyAdditionalButtons?: ViewRoomOpts["buttons"]; extraButtons?: JSX.Element; }): JSX.Element { const members = useRoomMembers(room, 2500); const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true }); const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick, toggleCallMaximized: toggleCall, isViewingCall, isConnectedToCall, activeCallSessionType, callOptions, showVoiceCallButton, showVideoCallButton, } = useRoomCall(room); const threadNotifications = useRoomThreadNotifications(room); const globalNotificationState = useGlobalNotificationState(); const dmMember = useDmMember(room); const isDirectMessage = !!dmMember; const notificationsEnabled = useFeatureEnabled("feature_notifications"); const videoClick = useCallback( (ev: React.MouseEvent) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick], ); const voiceClick = useCallback( (ev: React.MouseEvent) => voiceCallClick(ev, callOptions[0]), [callOptions, voiceCallClick], ); const toggleCallButton = ( ); const joinCallButton = ( ); const videoCallIconWithTooltip = ( ); const voiceCallIconWithTooltip = ( ); const [videoMenuOpen, setVideoMenuOpen] = useState(false); const onVideoOpenChange = useCallback( (newOpen: boolean) => { if (!videoCallDisabledReason) setVideoMenuOpen(newOpen); }, [videoCallDisabledReason], ); const [voiceMenuOpen, setVoiceMenuOpen] = useState(false); const onVoiceOpenChange = useCallback( (newOpen: boolean) => { if (!voiceCallDisabledReason) setVoiceMenuOpen(newOpen); }, [voiceCallDisabledReason], ); const startVideoCallButton = ( <> {/* Can be either a menu or just a button depending on the number of call options.*/} {callOptions.length > 1 ? ( {videoCallIconWithTooltip} } side="left" align="start" > {callOptions.map((option) => { const { label, children } = getPlatformCallTypeProps(option); return ( { setVideoMenuOpen(false); videoCallClick(ev, option); }} Icon={VideoCallIcon} onSelect={() => {} /* Dummy handler since we want the click event.*/} /> ); })} ) : ( {videoCallIconWithTooltip} )} ); const startVoiceCallButton = ( <> {/* Can be either a menu or just a button depending on the number of call options.*/} {callOptions.length > 1 ? ( {voiceCallIconWithTooltip} } side="left" align="start" > {callOptions.map((option) => { const { label, children } = getPlatformCallTypeProps(option); return ( { setVoiceMenuOpen(false); voiceCallClick(ev, option); }} Icon={VoiceCallIcon} onSelect={() => {} /* Dummy handler since we want the click event.*/} /> ); })} ) : ( {voiceCallIconWithTooltip} )} ); const closeLobbyButton = ( ); let videoCallButton: JSX.Element | undefined = startVideoCallButton; let voiceCallButton: JSX.Element | undefined = startVoiceCallButton; if (isConnectedToCall) { videoCallButton = toggleCallButton; voiceCallButton = undefined; } else if (isViewingCall) { videoCallButton = closeLobbyButton; voiceCallButton = undefined; } if (!showVideoCallButton) { videoCallButton = undefined; } if (!showVoiceCallButton) { voiceCallButton = undefined; } const roomContext = useScopedRoomContext("mainSplitContentType"); const isVideoRoom = calcIsVideoRoom(room); const showChatButton = isVideoRoom || roomContext.mainSplitContentType === MainSplitContentType.MaximisedWidget || roomContext.mainSplitContentType === MainSplitContentType.Call; return ( <> {extraButtons} {legacyAdditionalButtons?.map((props) => { const label = props.label(); return ( { event.stopPropagation(); props.onClick(); }} > {typeof props.icon === "function" ? props.icon() : props.icon} ); })} {isViewingCall && } {activeCallSessionType && !isConnectedToCall && !isViewingCall ? ( joinCallButton ) : ( <> {!isVideoRoom && videoCallButton} {!isVideoRoom && voiceCallButton} )} {showChatButton && } { evt.stopPropagation(); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel); PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); }} aria-label={_t("common|threads")} > {notificationsEnabled && ( { evt.stopPropagation(); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); }} aria-label={_t("notifications|enable_prompt_toast_title")} > )} { evt.stopPropagation(); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); }} aria-label={_t("right_panel|room_summary_card|title")} > {!isDirectMessage && ( { RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); e.stopPropagation(); }} aria-label={_t("common|n_members", { count: memberCount })} > {formatCount(memberCount)} )} ); } /** Create an icon to warn the user about shared history visibility, in encrypted rooms. * * Note that we use the same icon as in the room summary card and elsewhere, to aid user recognition. */ function historyVisibilityIcon(historyVisibility: HistoryVisibility): JSX.Element | null { if (historyVisibility === HistoryVisibility.Shared) { return ( ); } else if (historyVisibility === HistoryVisibility.WorldReadable) { return ( ); } else { return null; } } export default function RoomHeader({ room, extraButtons, legacyAdditionalButtons, oobData, }: { room: Room | LocalRoom; // Extra buttons added by a new element web module API module extraButtons?: JSX.Element; // DEPRECATED: Buttons added by a legacy react-sdk module API module. legacyAdditionalButtons?: ViewRoomOpts["buttons"]; oobData?: IOOBData; }): JSX.Element { const client = useMatrixClientContext(); const roomName = useRoomName(room); const joinRule = useRoomState(room, (state) => state.getJoinRule()); const historyVisibility = useRoomState(room, (state) => state.getHistoryVisibility()); const dmMember = useDmMember(room); const isDirectMessage = !!dmMember; const isRoomEncrypted = useIsEncrypted(client, room); const e2eStatus = useEncryptionStatus(client, room); const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); const onAvatarClick = (): void => { defaultDispatcher.dispatch({ action: "open_room_settings", initial_tab_id: RoomSettingsTab.General, }); }; return ( <> {/* We hide this from the tabIndex list as it is a pointer shortcut and superfluous for a11y */} {/* Disable on-click actions until the room is created */} {/* Disable on-click actions until the room is created */} {/* If the room is local-only then we don't want to show any additional buttons, as it won't work */} {room instanceof LocalRoom === false && ( )} {askToJoinEnabled && } ); }