Mvvm RoomSummaryCard (#29674)

* feat: create roomsummarycard viewmodel

* feat: use roomsummurycard vm in component

* test: jest unit RoomSummaryCard and RoomSummaryCardViewModel

* chore: rename to roomsummarycardview

* feat: reput room topic without vm

* test: roomSummaryCard and roomSummaryCardVM tests

* chore: add comments on roomsummarycardVM

* fix: merge conflict with roomsummarytopic, and move to vm right_panel

* fix(roomsummarycard): remove usetransition for search update

* fix: merged file that should be deleted

* fix: roomsummurycard not well merge with roomtopic

* test: update snapshots
This commit is contained in:
Marc
2025-05-15 16:17:21 +02:00
committed by GitHub
parent 9642af9930
commit b07225eb60
7 changed files with 1108 additions and 570 deletions

View File

@@ -15,7 +15,7 @@ import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import RoomSummaryCardView from "../views/right_panel/RoomSummaryCardView";
import WidgetCard from "../views/right_panel/WidgetCard";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
@@ -255,7 +255,7 @@ export default class RightPanel extends React.Component<Props, IState> {
case RightPanelPhases.RoomSummary:
if (!!this.props.room) {
card = (
<RoomSummaryCard
<RoomSummaryCardView
room={this.props.room}
// whenever RightPanel is passed a room it is passed a permalinkcreator
permalinkCreator={this.props.permalinkCreator!}

View File

@@ -0,0 +1,278 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { useEffect, useRef, useState } from "react";
import { EventType, type JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
import { type E2EStatus } from "../../../utils/ShieldUtils";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
import { useRoomState } from "../../../hooks/useRoomState";
import { useAccountData } from "../../../hooks/useAccountData";
import { useDispatcher } from "../../../hooks/useDispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { canInviteTo } from "../../../utils/room/canInviteTo";
import { DefaultTagID } from "../../../stores/room-list/models";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import PosthogTrackers from "../../../PosthogTrackers";
import { PollHistoryDialog } from "../../views/dialogs/PollHistoryDialog";
import Modal from "../../../Modal";
import ExportDialog from "../../views/dialogs/ExportDialog";
import { ShareDialog } from "../../views/dialogs/ShareDialog";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { ReportRoomDialog } from "../../views/dialogs/ReportRoomDialog";
import { Key } from "../../../Keyboard";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
import { tagRoom } from "../../../utils/room/tagRoom";
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
export interface RoomSummaryCardState {
isDirectMessage: boolean;
/**
* Whether the room is encrypted, used to display the correct badge and icon
*/
isRoomEncrypted: boolean;
/**
* The e2e status of the room, used to display the correct badge and icon
*/
e2eStatus: E2EStatus | undefined;
/**
* The join rule of the room, used to display the correct badge and icon
*/
roomJoinRule: JoinRule;
/**
* if it is a video room, it should not display export chat, polls, files, extensions
*/
isVideoRoom: boolean;
/**
* display the alias of the room, if it exists
*/
alias: string;
/**
* value to check if the room is a favorite or not
*/
isFavorite: boolean;
/**
* value to check if we disable invite button or not
*/
canInviteToState: boolean;
/**
* Getting the number of pinned messages in the room, next to the pin button
*/
pinCount: number;
searchInputRef: React.RefObject<HTMLInputElement | null>;
/**
* The callback when new value is entered in the search input
*/
onUpdateSearchInput: (e: React.KeyboardEvent<HTMLInputElement>) => void;
/**
* Callbacks to all the actions button in the right panel
*/
onRoomMembersClick: () => void;
onRoomThreadsClick: () => void;
onRoomFilesClick: () => void;
onRoomExtensionsClick: () => void;
onRoomPinsClick: () => void;
onRoomSettingsClick: (ev: Event) => void;
onLeaveRoomClick: () => void;
onShareRoomClick: () => void;
onRoomExportClick: () => Promise<void>;
onRoomPollHistoryClick: () => void;
onReportRoomClick: () => Promise<void>;
onFavoriteToggleClick: () => void;
onInviteToRoomClick: () => void;
}
/**
* Hook to check if the room is a direct message or not
* @param room - The room to check
* @returns Whether the room is a direct message
*/
const useIsDirectMessage = (room: Room): boolean => {
const directRoomsList = useAccountData<Record<string, string[]>>(room.client, EventType.Direct);
const [isDirectMessage, setDirectMessage] = useState(false);
useEffect(() => {
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
if (dmRoomList.includes(room?.roomId ?? "")) {
setDirectMessage(true);
break;
}
}
}, [room, directRoomsList]);
return isDirectMessage;
};
/**
* Hook to handle the search input in the right panel
* @param onSearchCancel - The callback when the search input is cancelled
* @returns The search input ref and the callback when the search input is updated
*/
const useSearchInput = (
onSearchCancel?: () => void,
): {
searchInputRef: React.RefObject<HTMLInputElement | null>;
onUpdateSearchInput: (e: React.KeyboardEvent<HTMLInputElement>) => void;
} => {
const searchInputRef = useRef<HTMLInputElement>(null);
const onUpdateSearchInput = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (searchInputRef.current && e.key === Key.ESCAPE) {
searchInputRef.current.value = "";
onSearchCancel?.();
}
};
// Focus the search field when the user clicks on the search button component
useDispatcher(defaultDispatcher, (payload) => {
if (payload.action === Action.FocusMessageSearch) {
searchInputRef.current?.focus();
}
});
return {
searchInputRef,
onUpdateSearchInput,
};
};
export function useRoomSummaryCardViewModel(
room: Room,
permalinkCreator: RoomPermalinkCreator,
onSearchCancel?: () => void,
): RoomSummaryCardState {
const cli = useMatrixClientContext();
const isRoomEncrypted = useIsEncrypted(cli, room) ?? false;
const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType");
const e2eStatus = roomContext.e2eStatus;
const isVideoRoom = calcIsVideoRoom(room);
const roomState = useRoomState(room);
// used to check if the room is public or not
const roomJoinRule = roomState.getJoinRule();
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const pinCount = usePinnedEvents(room).length;
// value to check if the user can invite to the room
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const isDirectMessage = useIsDirectMessage(room);
const onRoomMembersClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true);
};
const onRoomThreadsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true);
};
const onRoomFilesClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
};
const onRoomExtensionsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
};
const onRoomPinsClick = (): void => {
PosthogTrackers.trackInteraction("PinnedMessageRoomInfoButton");
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true);
};
const onRoomSettingsClick = (ev: Event): void => {
defaultDispatcher.dispatch({ action: "open_room_settings" });
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev);
};
const onShareRoomClick = (): void => {
Modal.createDialog(ShareDialog, {
target: room,
});
};
const onRoomExportClick = async (): Promise<void> => {
Modal.createDialog(ExportDialog, {
room,
});
};
const onRoomPollHistoryClick = (): void => {
Modal.createDialog(PollHistoryDialog, {
room,
matrixClient: cli,
permalinkCreator,
});
};
const onLeaveRoomClick = (): void => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
};
const onReportRoomClick = async (): Promise<void> => {
const [leave] = await Modal.createDialog(ReportRoomDialog, {
roomId: room.roomId,
}).finished;
if (leave) {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
}
};
const onFavoriteToggleClick = (): void => {
tagRoom(room, DefaultTagID.Favourite);
};
const onInviteToRoomClick = (): void => {
inviteToRoom(room);
};
// Room Search element ref
const { searchInputRef, onUpdateSearchInput } = useSearchInput(onSearchCancel);
return {
isDirectMessage,
isRoomEncrypted,
roomJoinRule,
e2eStatus,
isVideoRoom,
alias,
isFavorite,
canInviteToState,
searchInputRef,
pinCount,
onRoomMembersClick,
onRoomThreadsClick,
onRoomFilesClick,
onRoomExtensionsClick,
onRoomPinsClick,
onRoomSettingsClick,
onLeaveRoomClick,
onShareRoomClick,
onRoomExportClick,
onRoomPollHistoryClick,
onReportRoomClick,
onUpdateSearchInput,
onFavoriteToggleClick,
onInviteToRoomClick,
};
}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useContext, useEffect, useRef, useState } from "react";
import React, { useEffect, useState, type JSX } from "react";
import classNames from "classnames";
import {
MenuItem,
@@ -38,43 +38,19 @@ import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/publi
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import Modal from "../../../Modal";
import { ShareDialog } from "../dialogs/ShareDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { E2EStatus } from "../../../utils/ShieldUtils";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import RoomName from "../elements/RoomName";
import ExportDialog from "../dialogs/ExportDialog";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import PosthogTrackers from "../../../PosthogTrackers";
import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
import { Flex } from "../../utils/Flex";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { tagRoom } from "../../../utils/room/tagRoom";
import { canInviteTo } from "../../../utils/room/canInviteTo";
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
import { useAccountData } from "../../../hooks/useAccountData";
import { useRoomState } from "../../../hooks/useRoomState";
import { Linkify, topicToHtml } from "../../../HtmlUtils";
import { Box } from "../../utils/Box";
import { useDispatcher } from "../../../hooks/useDispatcher";
import { Action } from "../../../dispatcher/actions";
import { Key } from "../../../Keyboard";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
import BaseCard from "./BaseCard.tsx";
import { _t } from "../../../languageHandler.tsx";
import RoomAvatar from "../avatars/RoomAvatar.tsx";
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
import RoomName from "../elements/RoomName.tsx";
import { Flex } from "../../utils/Flex.tsx";
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
import { Box } from "../../utils/Box.tsx";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
import { ReportRoomDialog } from "../dialogs/ReportRoomDialog.tsx";
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
interface IProps {
@@ -86,32 +62,6 @@ interface IProps {
searchTerm?: string;
}
const onRoomMembersClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true);
};
const onRoomThreadsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true);
};
const onRoomFilesClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
};
const onRoomExtensionsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
};
const onRoomPinsClick = (): void => {
PosthogTrackers.trackInteraction("PinnedMessageRoomInfoButton");
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true);
};
const onRoomSettingsClick = (ev: Event): void => {
defaultDispatcher.dispatch({ action: "open_room_settings" });
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev);
};
const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null => {
const vm = useRoomTopicViewModel(room);
@@ -142,6 +92,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
}
const content = vm.expanded ? <Linkify>{body}</Linkify> : body;
return (
<Flex
as="section"
@@ -173,7 +124,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
);
};
const RoomSummaryCard: React.FC<IProps> = ({
const RoomSummaryCardView: React.FC<IProps> = ({
room,
permalinkCreator,
onSearchChange,
@@ -181,69 +132,7 @@ const RoomSummaryCard: React.FC<IProps> = ({
focusRoomSearch,
searchTerm = "",
}) => {
const cli = useContext(MatrixClientContext);
const onShareRoomClick = (): void => {
Modal.createDialog(ShareDialog, {
target: room,
});
};
const onRoomExportClick = async (): Promise<void> => {
Modal.createDialog(ExportDialog, {
room,
});
};
const onRoomPollHistoryClick = (): void => {
Modal.createDialog(PollHistoryDialog, {
room,
matrixClient: cli,
permalinkCreator,
});
};
const onLeaveRoomClick = (): void => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
};
const onReportRoomClick = async (): Promise<void> => {
const [leave] = await Modal.createDialog(ReportRoomDialog, {
roomId: room.roomId,
}).finished;
if (leave) {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
}
};
const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType");
const e2eStatus = roomContext.e2eStatus;
const isVideoRoom = calcIsVideoRoom(room);
const roomState = useRoomState(room);
const directRoomsList = useAccountData<Record<string, string[]>>(room.client, EventType.Direct);
const [isDirectMessage, setDirectMessage] = useState(false);
useEffect(() => {
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
if (dmRoomList.includes(room?.roomId ?? "")) {
setDirectMessage(true);
break;
}
}
}, [room, directRoomsList]);
const searchInputRef = useRef<HTMLInputElement>(null);
useDispatcher(defaultDispatcher, (payload) => {
if (payload.action === Action.FocusMessageSearch) {
searchInputRef.current?.focus();
}
});
const vm = useRoomSummaryCardViewModel(room, permalinkCreator, onSearchCancel);
// The search field is controlled and onSearchChange is debounced in RoomView,
// so we need to set the value of the input right away
@@ -252,7 +141,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
setSearchValue(searchTerm);
}, [searchTerm]);
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const roomInfo = (
<header className="mx_RoomSummaryCard_container">
<RoomAvatar room={room} size="80px" viewAvatarOnClick />
@@ -274,34 +162,34 @@ const RoomSummaryCard: React.FC<IProps> = ({
size="sm"
weight="semibold"
className="mx_RoomSummaryCard_alias text-secondary"
title={alias}
title={vm.alias}
>
{alias}
{vm.alias}
</Text>
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
{!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && (
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
<Badge kind="grey">
<PublicIcon width="1em" />
{_t("common|public_room")}
</Badge>
)}
{isRoomEncrypted && e2eStatus !== E2EStatus.Warning && (
{vm.isRoomEncrypted && vm.e2eStatus !== E2EStatus.Warning && (
<Badge kind="green">
<LockIcon width="1em" />
{_t("common|encrypted")}
</Badge>
)}
{!e2eStatus && (
{!vm.isRoomEncrypted && (
<Badge kind="grey">
<LockOffIcon width="1em" />
{_t("common|unencrypted")}
</Badge>
)}
{e2eStatus === E2EStatus.Warning && (
{vm.e2eStatus === E2EStatus.Warning && (
<Badge kind="red">
<ErrorSolidIcon width="1em" />
{_t("common|not_trusted")}
@@ -313,14 +201,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
</header>
);
const pinCount = usePinnedEvents(room).length;
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const header = onSearchChange && (
<Form.Root className="mx_RoomSummaryCard_search" onSubmit={(e) => e.preventDefault()}>
<Search
@@ -332,14 +212,9 @@ const RoomSummaryCard: React.FC<IProps> = ({
}}
value={searchValue}
className="mx_no_textinput"
ref={searchInputRef}
ref={vm.searchInputRef}
autoFocus={focusRoomSearch}
onKeyDown={(e) => {
if (searchInputRef.current && e.key === Key.ESCAPE) {
searchInputRef.current.value = "";
onSearchCancel?.();
}
}}
onKeyDown={vm.onUpdateSearchInput}
/>
</Form.Root>
);
@@ -360,21 +235,21 @@ const RoomSummaryCard: React.FC<IProps> = ({
<ToggleMenuItem
Icon={FavouriteIcon}
label={_t("room|context_menu|favourite")}
checked={isFavorite}
onSelect={() => tagRoom(room, DefaultTagID.Favourite)}
checked={vm.isFavorite}
onSelect={vm.onFavoriteToggleClick}
/>
<MenuItem
Icon={UserAddIcon}
label={_t("action|invite")}
disabled={!canInviteToState}
onSelect={() => inviteToRoom(room)}
disabled={!vm.canInviteToState}
onSelect={vm.onInviteToRoomClick}
/>
<Separator />
<MenuItem Icon={UserProfileIcon} label={_t("common|people")} onSelect={onRoomMembersClick} />
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={onRoomThreadsClick} />
{!isVideoRoom && (
<MenuItem Icon={UserProfileIcon} label={_t("common|people")} onSelect={vm.onRoomMembersClick} />
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
{!vm.isVideoRoom && (
<>
<ReleaseAnnouncement
feature="pinningMessageList"
@@ -387,43 +262,47 @@ const RoomSummaryCard: React.FC<IProps> = ({
<MenuItem
Icon={PinIcon}
label={_t("right_panel|pinned_messages_button")}
onSelect={onRoomPinsClick}
onSelect={vm.onRoomPinsClick}
>
<Text as="span" size="sm">
{pinCount}
{vm.pinCount}
</Text>
</MenuItem>
</div>
</ReleaseAnnouncement>
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
<MenuItem
Icon={FilesIcon}
label={_t("right_panel|files_button")}
onSelect={vm.onRoomFilesClick}
/>
<MenuItem
Icon={ExtensionsIcon}
label={_t("right_panel|extensions_button")}
onSelect={onRoomExtensionsClick}
onSelect={vm.onRoomExtensionsClick}
/>
</>
)}
<Separator />
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onSelect={onShareRoomClick} />
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onSelect={vm.onShareRoomClick} />
{!isVideoRoom && (
{!vm.isVideoRoom && (
<>
<MenuItem
Icon={PollsIcon}
label={_t("right_panel|polls_button")}
onSelect={onRoomPollHistoryClick}
onSelect={vm.onRoomPollHistoryClick}
/>
<MenuItem
Icon={ExportArchiveIcon}
label={_t("export_chat|title")}
onSelect={onRoomExportClick}
onSelect={vm.onRoomExportClick}
/>
</>
)}
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={vm.onRoomSettingsClick} />
<Separator />
<div className="mx_RoomSummaryCard_bottomOptions">
@@ -431,14 +310,14 @@ const RoomSummaryCard: React.FC<IProps> = ({
Icon={ErrorIcon}
kind="critical"
label={_t("action|report_room")}
onSelect={onReportRoomClick}
onSelect={vm.onReportRoomClick}
/>
<MenuItem
className="mx_RoomSummaryCard_leave"
Icon={LeaveIcon}
kind="critical"
label={_t("action|leave_room")}
onSelect={onLeaveRoomClick}
onSelect={vm.onLeaveRoomClick}
/>
</div>
</div>
@@ -446,4 +325,4 @@ const RoomSummaryCard: React.FC<IProps> = ({
);
};
export default RoomSummaryCard;
export default RoomSummaryCardView;