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);
|
padding: var(--cpd-space-1-5x);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
font: var(--cpd-font-body-sm-medium);
|
||||||
|
|
||||||
/* RoomAvatar doesn't pass classes down to avatar
|
/* RoomAvatar doesn't pass classes down to avatar
|
||||||
So set style here
|
So set style here
|
||||||
@@ -83,6 +84,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
background: var(--cpd-color-bg-subtle-primary);
|
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 {
|
.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 */
|
/* Workaround for https://github.com/element-hq/compound/issues/331 */
|
||||||
min-width: 240px;
|
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 RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
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 { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
import { containsEmoji } from "../../effects/utils";
|
import { containsEmoji } from "../../effects/utils";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|||||||
|
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
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 ScrollPanel from "./ScrollPanel";
|
||||||
import EventTileBubble from "../views/messages/EventTileBubble";
|
import EventTileBubble from "../views/messages/EventTileBubble";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
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 React, { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
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"> {
|
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||||
members: RoomMember[];
|
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 = (
|
const content = (
|
||||||
<AccessibleButton {...props} className="mx_FacePile" onClick={onClick ?? null}>
|
<AccessibleButton {...props} className={classes} onClick={onClick ?? null}>
|
||||||
<AvatarStack>{pileContents}</AvatarStack>
|
<AvatarStack>{pileContents}</AvatarStack>
|
||||||
{children}
|
{children}
|
||||||
</AccessibleButton>
|
</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} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import { SDKContext } from "../../../../contexts/SDKContext";
|
import { SDKContext } from "../../../../contexts/SDKContext";
|
||||||
import { ButtonEvent } from "../../elements/AccessibleButton";
|
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||||
|
import { ToggleableIcon } from "./toggle/ToggleableIcon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a button to toggle timeline for video rooms
|
* Display a button to toggle timeline for video rooms
|
||||||
@@ -54,7 +55,7 @@ export const VideoRoomChatButton: React.FC<{ room: Room }> = ({ room }) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
indicator={displayUnreadIndicator ? "default" : undefined}
|
indicator={displayUnreadIndicator ? "default" : undefined}
|
||||||
>
|
>
|
||||||
<ChatIcon />
|
<ToggleableIcon Icon={ChatIcon} phase={RightPanelPhases.Timeline} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</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} />;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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.
|
* @param cardState The state within the phase.
|
||||||
*/
|
*/
|
||||||
public showOrHidePhase(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
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);
|
this.togglePanel(null);
|
||||||
} else {
|
} else {
|
||||||
this.setCard({ phase, state: cardState });
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class="mx_RoomHeader_toggled"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
+26
-21
@@ -36,28 +36,33 @@ import {
|
|||||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { filterConsole, stubClient } from "../../../../test-utils";
|
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||||
import RoomHeader from "../../../../../src/components/views/rooms/RoomHeader";
|
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||||
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
|
||||||
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||||
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
|
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import SdkConfig from "../../../../../src/SdkConfig";
|
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
import dispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../../../src/stores/CallStore";
|
||||||
import { Call, ElementCall } from "../../../../../src/models/Call";
|
import { Call, ElementCall } from "../../../../../../src/models/Call";
|
||||||
import * as ShieldUtils from "../../../../../src/utils/ShieldUtils";
|
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
||||||
import { Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
import { _t } from "../../../../../src/languageHandler";
|
import { _t } from "../../../../../../src/languageHandler";
|
||||||
import * as UseCall from "../../../../../src/hooks/useCall";
|
import * as UseCall from "../../../../../../src/hooks/useCall";
|
||||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../../../src/contexts/SDKContext";
|
||||||
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
|
import WidgetStore, { IApp } from "../../../../../../src/stores/WidgetStore";
|
||||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
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 {
|
function getWrapper(): RenderOptions {
|
||||||
return {
|
return {
|
||||||
+2
@@ -105,6 +105,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
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%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
+1
@@ -16,6 +16,7 @@ exports[`<VideoRoomChatButton /> renders button with an unread marker when room
|
|||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
class=""
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
Reference in New Issue
Block a user