Style room header icons and facepile for toggled state (#28968)
* Fix tiny typo in existing code * Create a hook that uses the right panel store So that we track changes to the right panel phases * Create a context that wraps the previous hook we created We do this so that we can get by using a single event listener i.e we only need to call `useCurrentPhase` in the provider as opposed to calling it in each header icon. * Create a hook that tells you if a panel is open or not * Create component that wraps Icon and adds a class name when the corresponding panel is open * Style room header icons for when they are toggled * Style face pile for toggle state * Fix broken CI * Give directory a better name * Update year in license * Use a stronger type
This commit is contained in:
@@ -71,6 +71,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
padding: var(--cpd-space-1-5x);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font: var(--cpd-font-body-sm-medium);
|
||||
|
||||
/* RoomAvatar doesn't pass classes down to avatar
|
||||
So set style here
|
||||
@@ -83,6 +84,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: $primary-content;
|
||||
background: var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
&.mx_FacePile_toggled {
|
||||
background: var(--cpd-color-bg-success-subtle);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
font: var(--cpd-font-body-sm-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomHeader .mx_BaseAvatar {
|
||||
@@ -93,3 +100,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* Workaround for https://github.com/element-hq/compound/issues/331 */
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.mx_RoomHeader .mx_RoomHeader_toggled {
|
||||
color: var(--cpd-color-icon-accent-primary);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import RoomHeader from "../views/rooms/RoomHeader/RoomHeader";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from "../../effects/utils";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import RoomHeader from "../views/rooms/RoomHeader/RoomHeader.tsx";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import EventTileBubble from "../views/messages/EventTileBubble";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
|
||||
@@ -9,9 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
import { useToggled } from "../rooms/RoomHeader/toggle/useToggled";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
|
||||
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||
members: RoomMember[];
|
||||
@@ -57,8 +60,14 @@ const FacePile: FC<IProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const toggled = useToggled(RightPanelPhases.MemberList);
|
||||
const classes = classNames({
|
||||
mx_FacePile: true,
|
||||
mx_FacePile_toggled: toggled,
|
||||
});
|
||||
|
||||
const content = (
|
||||
<AccessibleButton {...props} className="mx_FacePile" onClick={onClick ?? null}>
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick ?? null}>
|
||||
<AvatarStack>{pileContents}</AvatarStack>
|
||||
{children}
|
||||
</AccessibleButton>
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
/*
|
||||
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, { useCallback, useMemo, useState } from "react";
|
||||
import { Body as BodyText, 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";
|
||||
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";
|
||||
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
||||
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
|
||||
import { useRoomName } from "../../../hooks/useRoomName";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Box } from "../../utils/Box";
|
||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../hooks/room/useRoomCall";
|
||||
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
|
||||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import { useRoomState } from "../../../hooks/useRoomState";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton";
|
||||
import { RoomKnocksBar } from "./RoomKnocksBar";
|
||||
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
|
||||
import { notificationLevelToIndicator } from "../../../utils/notifications";
|
||||
import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import { MainSplitContentType } from "../../structures/RoomView";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher.ts";
|
||||
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog.tsx";
|
||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
additionalButtons,
|
||||
oobData,
|
||||
}: {
|
||||
room: Room;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
oobData?: IOOBData;
|
||||
}): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const roomName = useRoomName(room);
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||
|
||||
const {
|
||||
voiceCallDisabledReason,
|
||||
voiceCallClick,
|
||||
videoCallDisabledReason,
|
||||
videoCallClick,
|
||||
toggleCallMaximized: toggleCall,
|
||||
isViewingCall,
|
||||
isConnectedToCall,
|
||||
hasActiveCallSession,
|
||||
callOptions,
|
||||
showVoiceCallButton,
|
||||
showVideoCallButton,
|
||||
} = useRoomCall(room);
|
||||
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
/**
|
||||
* A special mode where only Element Call is used. In this case we want to
|
||||
* hide the voice call button
|
||||
*/
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
return SdkConfig.get("element_call").use_exclusively && groupCallsEnabled;
|
||||
}, [groupCallsEnabled]);
|
||||
|
||||
const threadNotifications = useRoomThreadNotifications(room);
|
||||
const globalNotificationState = useGlobalNotificationState();
|
||||
|
||||
const dmMember = useDmMember(room);
|
||||
const isDirectMessage = !!dmMember;
|
||||
const e2eStatus = useEncryptionStatus(client, room);
|
||||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
||||
const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join");
|
||||
|
||||
const videoClick = useCallback(
|
||||
(ev: React.MouseEvent) => videoCallClick(ev, callOptions[0]),
|
||||
[callOptions, videoCallClick],
|
||||
);
|
||||
|
||||
const toggleCallButton = (
|
||||
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<VideoCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const joinCallButton = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={videoClick}
|
||||
Icon={VideoCallIcon}
|
||||
className="mx_RoomHeader_join_button"
|
||||
disabled={!!videoCallDisabledReason}
|
||||
color="primary"
|
||||
aria-label={videoCallDisabledReason ?? _t("action|join")}
|
||||
>
|
||||
{_t("action|join")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const callIconWithTooltip = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<VideoCallIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!videoCallDisabledReason) setMenuOpen(newOpen);
|
||||
},
|
||||
[videoCallDisabledReason],
|
||||
);
|
||||
|
||||
const startVideoCallButton = (
|
||||
<>
|
||||
{/* Can be either a menu or just a button depending on the number of call options.*/}
|
||||
{callOptions.length > 1 ? (
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={_t("voip|video_call_using")}
|
||||
trigger={
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
{callOptions.map((option) => {
|
||||
const { label, children } = getPlatformCallTypeProps(option);
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
label={label}
|
||||
aria-label={label}
|
||||
children={children}
|
||||
className="mx_RoomHeader_videoCallOption"
|
||||
onClick={(ev) => videoCallClick(ev, option)}
|
||||
Icon={VideoCallIcon}
|
||||
onSelect={() => {} /* Dummy handler since we want the click event.*/}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
onClick={videoClick}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
let voiceCallButton: JSX.Element | undefined = (
|
||||
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<IconButton
|
||||
// We need both: isViewingCall and isConnectedToCall
|
||||
// - in the Lobby we are viewing a call but are not connected to it.
|
||||
// - in pip view we are connected to the call but not viewing it.
|
||||
disabled={!!voiceCallDisabledReason || isViewingCall || isConnectedToCall}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
const closeLobbyButton = (
|
||||
<Tooltip label={_t("voip|close_lobby")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<CloseCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
let videoCallButton: JSX.Element | undefined = startVideoCallButton;
|
||||
if (isConnectedToCall) {
|
||||
videoCallButton = toggleCallButton;
|
||||
} else if (isViewingCall) {
|
||||
videoCallButton = closeLobbyButton;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const onAvatarClick = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: RoomSettingsTab.General,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex as="header" align="center" gap="var(--cpd-space-3x)" className="mx_RoomHeader light-panel">
|
||||
<WithPresenceIndicator room={room} size="8px">
|
||||
{/* We hide this from the tabIndex list as it is a pointer shortcut and superfluous for a11y */}
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
size="40px"
|
||||
oobData={oobData}
|
||||
onClick={onAvatarClick}
|
||||
tabIndex={-1}
|
||||
aria-label={_t("room|header_avatar_open_settings_label")}
|
||||
/>
|
||||
</WithPresenceIndicator>
|
||||
<button
|
||||
aria-label={_t("right_panel|room_summary_card|title")}
|
||||
tabIndex={0}
|
||||
onClick={() => RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary)}
|
||||
className="mx_RoomHeader_infoWrapper"
|
||||
>
|
||||
<Box flex="1" className="mx_RoomHeader_info">
|
||||
<BodyText
|
||||
as="div"
|
||||
size="lg"
|
||||
weight="semibold"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
aria-level={1}
|
||||
className="mx_RoomHeader_heading"
|
||||
>
|
||||
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>
|
||||
|
||||
{!isDirectMessage && joinRule === JoinRule.Public && (
|
||||
<Tooltip label={_t("common|public_room")} placement="right">
|
||||
<PublicIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon text-secondary"
|
||||
aria-label={_t("common|public_room")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDirectMessage && e2eStatus === E2EStatus.Verified && (
|
||||
<Tooltip label={_t("common|verified")} placement="right">
|
||||
<VerifiedIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon mx_Verified"
|
||||
aria-label={_t("common|verified")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDirectMessage && e2eStatus === E2EStatus.Warning && (
|
||||
<Tooltip label={_t("room|header_untrusted_label")} placement="right">
|
||||
<ErrorIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon mx_Untrusted"
|
||||
aria-label={_t("room|header_untrusted_label")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BodyText>
|
||||
</Box>
|
||||
</button>
|
||||
|
||||
{additionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
return (
|
||||
<Tooltip label={label} key={props.id}>
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
>
|
||||
{typeof props.icon === "function" ? props.icon() : props.icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
{isViewingCall && <CallGuestLinkButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||
joinCallButton
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showChatButton && <VideoRoomChatButton room={room} />}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel);
|
||||
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ThreadsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("notifications|enable_prompt_toast_title")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(globalNotificationState.level)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||
}}
|
||||
aria-label={_t("notifications|enable_prompt_toast_title")}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip label={_t("right_panel|room_summary_card|title")}>
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
|
||||
}}
|
||||
aria-label={_t("right_panel|room_summary_card|title")}
|
||||
>
|
||||
<RoomInfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{!isDirectMessage && (
|
||||
<BodyText as="div" size="sm" weight="medium">
|
||||
<FacePile
|
||||
className="mx_RoomHeader_members"
|
||||
members={members.slice(0, 3)}
|
||||
size="20px"
|
||||
overflow={false}
|
||||
viewUserOnClick={false}
|
||||
tooltipLabel={_t("room|header_face_pile_tooltip")}
|
||||
onClick={(e: ButtonEvent) => {
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
aria-label={_t("common|n_members", { count: memberCount })}
|
||||
>
|
||||
{formatCount(memberCount)}
|
||||
</FacePile>
|
||||
</BodyText>
|
||||
)}
|
||||
</Flex>
|
||||
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
412
src/components/views/rooms/RoomHeader/RoomHeader.tsx
Normal file
412
src/components/views/rooms/RoomHeader/RoomHeader.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
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, { useCallback, useMemo, useState } from "react";
|
||||
import { Body as BodyText, 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";
|
||||
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";
|
||||
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
||||
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
|
||||
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 { Flex } from "../../../utils/Flex.tsx";
|
||||
import { Box } from "../../../utils/Box.tsx";
|
||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
||||
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
||||
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
||||
import SdkConfig from "../../../../SdkConfig.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 { ButtonEvent } from "../../elements/AccessibleButton.tsx";
|
||||
import WithPresenceIndicator, { useDmMember } from "../../avatars/WithPresenceIndicator.tsx";
|
||||
import { 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";
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
additionalButtons,
|
||||
oobData,
|
||||
}: {
|
||||
room: Room;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
oobData?: IOOBData;
|
||||
}): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const roomName = useRoomName(room);
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||
|
||||
const {
|
||||
voiceCallDisabledReason,
|
||||
voiceCallClick,
|
||||
videoCallDisabledReason,
|
||||
videoCallClick,
|
||||
toggleCallMaximized: toggleCall,
|
||||
isViewingCall,
|
||||
isConnectedToCall,
|
||||
hasActiveCallSession,
|
||||
callOptions,
|
||||
showVoiceCallButton,
|
||||
showVideoCallButton,
|
||||
} = useRoomCall(room);
|
||||
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
/**
|
||||
* A special mode where only Element Call is used. In this case we want to
|
||||
* hide the voice call button
|
||||
*/
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
return SdkConfig.get("element_call").use_exclusively && groupCallsEnabled;
|
||||
}, [groupCallsEnabled]);
|
||||
|
||||
const threadNotifications = useRoomThreadNotifications(room);
|
||||
const globalNotificationState = useGlobalNotificationState();
|
||||
|
||||
const dmMember = useDmMember(room);
|
||||
const isDirectMessage = !!dmMember;
|
||||
const e2eStatus = useEncryptionStatus(client, room);
|
||||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
||||
const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join");
|
||||
|
||||
const videoClick = useCallback(
|
||||
(ev: React.MouseEvent) => videoCallClick(ev, callOptions[0]),
|
||||
[callOptions, videoCallClick],
|
||||
);
|
||||
|
||||
const toggleCallButton = (
|
||||
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<VideoCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const joinCallButton = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={videoClick}
|
||||
Icon={VideoCallIcon}
|
||||
className="mx_RoomHeader_join_button"
|
||||
disabled={!!videoCallDisabledReason}
|
||||
color="primary"
|
||||
aria-label={videoCallDisabledReason ?? _t("action|join")}
|
||||
>
|
||||
{_t("action|join")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const callIconWithTooltip = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<VideoCallIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!videoCallDisabledReason) setMenuOpen(newOpen);
|
||||
},
|
||||
[videoCallDisabledReason],
|
||||
);
|
||||
|
||||
const startVideoCallButton = (
|
||||
<>
|
||||
{/* Can be either a menu or just a button depending on the number of call options.*/}
|
||||
{callOptions.length > 1 ? (
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={_t("voip|video_call_using")}
|
||||
trigger={
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
{callOptions.map((option) => {
|
||||
const { label, children } = getPlatformCallTypeProps(option);
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
label={label}
|
||||
aria-label={label}
|
||||
children={children}
|
||||
className="mx_RoomHeader_videoCallOption"
|
||||
onClick={(ev) => videoCallClick(ev, option)}
|
||||
Icon={VideoCallIcon}
|
||||
onSelect={() => {} /* Dummy handler since we want the click event.*/}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
onClick={videoClick}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
let voiceCallButton: JSX.Element | undefined = (
|
||||
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<IconButton
|
||||
// We need both: isViewingCall and isConnectedToCall
|
||||
// - in the Lobby we are viewing a call but are not connected to it.
|
||||
// - in pip view we are connected to the call but not viewing it.
|
||||
disabled={!!voiceCallDisabledReason || isViewingCall || isConnectedToCall}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
const closeLobbyButton = (
|
||||
<Tooltip label={_t("voip|close_lobby")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<CloseCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
let videoCallButton: JSX.Element | undefined = startVideoCallButton;
|
||||
if (isConnectedToCall) {
|
||||
videoCallButton = toggleCallButton;
|
||||
} else if (isViewingCall) {
|
||||
videoCallButton = closeLobbyButton;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const onAvatarClick = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: RoomSettingsTab.General,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CurrentRightPanelPhaseContextProvider roomId={room.roomId}>
|
||||
<Flex as="header" align="center" gap="var(--cpd-space-3x)" className="mx_RoomHeader light-panel">
|
||||
<WithPresenceIndicator room={room} size="8px">
|
||||
{/* We hide this from the tabIndex list as it is a pointer shortcut and superfluous for a11y */}
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
size="40px"
|
||||
oobData={oobData}
|
||||
onClick={onAvatarClick}
|
||||
tabIndex={-1}
|
||||
aria-label={_t("room|header_avatar_open_settings_label")}
|
||||
/>
|
||||
</WithPresenceIndicator>
|
||||
<button
|
||||
aria-label={_t("right_panel|room_summary_card|title")}
|
||||
tabIndex={0}
|
||||
onClick={() => RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary)}
|
||||
className="mx_RoomHeader_infoWrapper"
|
||||
>
|
||||
<Box flex="1" className="mx_RoomHeader_info">
|
||||
<BodyText
|
||||
as="div"
|
||||
size="lg"
|
||||
weight="semibold"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
aria-level={1}
|
||||
className="mx_RoomHeader_heading"
|
||||
>
|
||||
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>
|
||||
|
||||
{!isDirectMessage && joinRule === JoinRule.Public && (
|
||||
<Tooltip label={_t("common|public_room")} placement="right">
|
||||
<PublicIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon text-secondary"
|
||||
aria-label={_t("common|public_room")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDirectMessage && e2eStatus === E2EStatus.Verified && (
|
||||
<Tooltip label={_t("common|verified")} placement="right">
|
||||
<VerifiedIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon mx_Verified"
|
||||
aria-label={_t("common|verified")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDirectMessage && e2eStatus === E2EStatus.Warning && (
|
||||
<Tooltip label={_t("room|header_untrusted_label")} placement="right">
|
||||
<ErrorIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon mx_Untrusted"
|
||||
aria-label={_t("room|header_untrusted_label")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</BodyText>
|
||||
</Box>
|
||||
</button>
|
||||
|
||||
{additionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
return (
|
||||
<Tooltip label={label} key={props.id}>
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
>
|
||||
{typeof props.icon === "function" ? props.icon() : props.icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
{isViewingCall && <CallGuestLinkButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||
joinCallButton
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showChatButton && <VideoRoomChatButton room={room} />}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel);
|
||||
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ToggleableIcon Icon={ThreadsIcon} phase={RightPanelPhases.ThreadPanel} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("notifications|enable_prompt_toast_title")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(globalNotificationState.level)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||
}}
|
||||
aria-label={_t("notifications|enable_prompt_toast_title")}
|
||||
>
|
||||
<ToggleableIcon Icon={NotificationsIcon} phase={RightPanelPhases.NotificationPanel} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip label={_t("right_panel|room_summary_card|title")}>
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
|
||||
}}
|
||||
aria-label={_t("right_panel|room_summary_card|title")}
|
||||
>
|
||||
<ToggleableIcon Icon={RoomInfoIcon} phase={RightPanelPhases.RoomSummary} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{!isDirectMessage && (
|
||||
<BodyText as="div" size="sm" weight="medium">
|
||||
<FacePile
|
||||
className="mx_RoomHeader_members"
|
||||
members={members.slice(0, 3)}
|
||||
size="20px"
|
||||
overflow={false}
|
||||
viewUserOnClick={false}
|
||||
tooltipLabel={_t("room|header_face_pile_tooltip")}
|
||||
onClick={(e: ButtonEvent) => {
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
aria-label={_t("common|n_members", { count: memberCount })}
|
||||
>
|
||||
{formatCount(memberCount)}
|
||||
</FacePile>
|
||||
</BodyText>
|
||||
)}
|
||||
</Flex>
|
||||
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
||||
</CurrentRightPanelPhaseContextProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { NotificationLevel } from "../../../../stores/notifications/Notification
|
||||
import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||
import { ToggleableIcon } from "./toggle/ToggleableIcon";
|
||||
|
||||
/**
|
||||
* Display a button to toggle timeline for video rooms
|
||||
@@ -54,7 +55,7 @@ export const VideoRoomChatButton: React.FC<{ room: Room }> = ({ room }) => {
|
||||
onClick={onClick}
|
||||
indicator={displayUnreadIndicator ? "default" : undefined}
|
||||
>
|
||||
<ChatIcon />
|
||||
<ToggleableIcon Icon={ChatIcon} phase={RightPanelPhases.Timeline} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
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 from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { RightPanelPhases } from "../../../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { useToggled } from "./useToggled";
|
||||
|
||||
type Props = {
|
||||
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
phase: RightPanelPhases;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this component for room header icons that toggle different right panel phases.
|
||||
* Will add a class to the icon when the specified phase is on.
|
||||
*/
|
||||
export function ToggleableIcon({ Icon, phase }: Props): React.ReactElement {
|
||||
const toggled = useToggled(phase);
|
||||
const highlightClass = classNames({
|
||||
mx_RoomHeader_toggled: toggled,
|
||||
});
|
||||
|
||||
return <Icon className={highlightClass} />;
|
||||
}
|
||||
23
src/components/views/rooms/RoomHeader/toggle/useToggled.tsx
Normal file
23
src/components/views/rooms/RoomHeader/toggle/useToggled.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
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 { useContext } from "react";
|
||||
|
||||
import { RightPanelPhases } from "../../../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { CurrentRightPanelPhaseContext } from "../../../../../contexts/CurrentRightPanelPhaseContext";
|
||||
|
||||
/**
|
||||
* Hook to easily track whether a given right panel phase is toggled on/off.
|
||||
*/
|
||||
export function useToggled(phase: RightPanelPhases): boolean {
|
||||
const context = useContext(CurrentRightPanelPhaseContext);
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
const { currentPhase, isPanelOpen } = context;
|
||||
return !!(isPanelOpen && currentPhase === phase);
|
||||
}
|
||||
34
src/contexts/CurrentRightPanelPhaseContext.tsx
Normal file
34
src/contexts/CurrentRightPanelPhaseContext.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
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, { createContext } from "react";
|
||||
|
||||
import { useCurrentPhase } from "../hooks/right-panel/useCurrentPhase";
|
||||
import { RightPanelPhases } from "../stores/right-panel/RightPanelStorePhases";
|
||||
|
||||
type Context = {
|
||||
isPanelOpen: boolean;
|
||||
currentPhase: RightPanelPhases | null;
|
||||
};
|
||||
|
||||
export const CurrentRightPanelPhaseContext = createContext<Context | null>(null);
|
||||
|
||||
type Props = {
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
export const CurrentRightPanelPhaseContextProvider: React.FC<React.PropsWithChildren<Props>> = ({
|
||||
roomId,
|
||||
children,
|
||||
}) => {
|
||||
const { currentPhase, isOpen } = useCurrentPhase(roomId);
|
||||
return (
|
||||
<CurrentRightPanelPhaseContext.Provider value={{ currentPhase, isPanelOpen: isOpen }}>
|
||||
{children}
|
||||
</CurrentRightPanelPhaseContext.Provider>
|
||||
);
|
||||
};
|
||||
45
src/hooks/right-panel/useCurrentPhase.ts
Normal file
45
src/hooks/right-panel/useCurrentPhase.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
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 { useContext, useState } from "react";
|
||||
|
||||
import { SDKContext } from "../../contexts/SDKContext";
|
||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
||||
import { useEventEmitter } from "../useEventEmitter";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
|
||||
/**
|
||||
* Returns:
|
||||
* - state which will always reflect the currently active right panel phase or null.
|
||||
* - boolean state representing whether any panel is open or not.
|
||||
* @param roomId room id if available.
|
||||
*/
|
||||
export function useCurrentPhase(roomId?: string): { currentPhase: RightPanelPhases | null; isOpen: boolean } {
|
||||
const sdkContext = useContext(SDKContext);
|
||||
|
||||
const getCurrentPhase = (): RightPanelPhases | null => {
|
||||
const card = roomId
|
||||
? sdkContext.rightPanelStore.currentCardForRoom(roomId)
|
||||
: sdkContext.rightPanelStore.currentCard;
|
||||
return card.phase;
|
||||
};
|
||||
|
||||
const getIsOpen = (): boolean => {
|
||||
const isOpen = roomId ? sdkContext.rightPanelStore.isOpenForRoom(roomId) : sdkContext.rightPanelStore.isOpen;
|
||||
return isOpen;
|
||||
};
|
||||
|
||||
const [currentPhase, setCurrentPhase] = useState<RightPanelPhases | null>(getCurrentPhase());
|
||||
const [isOpen, setIsOpen] = useState<boolean>(getIsOpen());
|
||||
|
||||
useEventEmitter(sdkContext.rightPanelStore, UPDATE_EVENT, () => {
|
||||
setCurrentPhase(getCurrentPhase());
|
||||
setIsOpen(getIsOpen());
|
||||
});
|
||||
|
||||
return { currentPhase, isOpen };
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||
* @param cardState The state within the phase.
|
||||
*/
|
||||
public showOrHidePhase(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
||||
if (this.currentCard.phase == phase && !cardState && this.isOpen) {
|
||||
if (this.currentCard.phase === phase && !cardState && this.isOpen) {
|
||||
this.togglePanel(null);
|
||||
} else {
|
||||
this.setCard({ phase, state: cardState });
|
||||
|
||||
@@ -110,6 +110,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -135,6 +136,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -323,6 +325,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -348,6 +351,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -621,6 +625,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -646,6 +651,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -996,6 +1002,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1021,6 +1028,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1379,6 +1387,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1404,6 +1413,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1585,6 +1595,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1610,6 +1621,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1912,6 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class="mx_RoomHeader_toggled"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1937,6 +1950,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1962,6 +1976,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -36,28 +36,33 @@ import {
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { filterConsole, stubClient } from "../../../../test-utils";
|
||||
import RoomHeader from "../../../../../src/components/views/rooms/RoomHeader";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||
import { Call, ElementCall } from "../../../../../src/models/Call";
|
||||
import * as ShieldUtils from "../../../../../src/utils/ShieldUtils";
|
||||
import { Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../../src/languageHandler";
|
||||
import * as UseCall from "../../../../../src/hooks/useCall";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
|
||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||
import dispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
import { CallStore } from "../../../../../../src/stores/CallStore";
|
||||
import { Call, ElementCall } from "../../../../../../src/models/Call";
|
||||
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
||||
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../../../src/languageHandler";
|
||||
import * as UseCall from "../../../../../../src/hooks/useCall";
|
||||
import { SdkContextClass } from "../../../../../../src/contexts/SDKContext";
|
||||
import WidgetStore, { IApp } from "../../../../../../src/stores/WidgetStore";
|
||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../../../src/utils/ShieldUtils");
|
||||
jest.mock("../../../../../../src/utils/ShieldUtils");
|
||||
jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
|
||||
useCurrentPhase: () => {
|
||||
return { currentPhase: "foo", isOpen: false };
|
||||
},
|
||||
}));
|
||||
|
||||
function getWrapper(): RenderOptions {
|
||||
return {
|
||||
@@ -105,6 +105,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -130,6 +131,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -16,6 +16,7 @@ exports[`<VideoRoomChatButton /> renders button with an unread marker when room
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
Reference in New Issue
Block a user