New room list: add more options menu on room list item (#29445)
* refactor(room list item): rename `RoomListCell` into `RoomListItemView` * refactor(room list item): move open room action to new room list item view model * feat(hover menu): add `hasAccessToOptionsMenu` * feat(hover menu): add to `RoomListItemViewModel` the condition to display or not the hover menu * feat(hover menu): add view model for the hover menu * feat(hover menu): add hover menu view * feat(hover menu): add hover menu to room list item * feat(hover menu): update i18n * test(view model list item): update test and add test to `showHoverMenu` * test(room list): update snapshot * test(room list item menu): add tests for view model * test(room list item menu): add tests for view * test(room list item): add tests * test(e2e): add tests for more options menu * chore: update compound web * test(e2e): fix typo
This commit is contained in:
180
src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx
Normal file
180
src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 { useCallback } from "react";
|
||||
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
|
||||
import { hasAccessToOptionsMenu } from "./utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { NotificationLevel } from "../../../stores/notifications/NotificationLevel";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { tagRoom } from "../../../utils/room/tagRoom";
|
||||
|
||||
export interface RoomListItemMenuViewState {
|
||||
/**
|
||||
* Whether the more options menu should be shown.
|
||||
*/
|
||||
showMoreOptionsMenu: boolean;
|
||||
/**
|
||||
* Whether the room is a favourite room.
|
||||
*/
|
||||
isFavourite: boolean;
|
||||
/**
|
||||
* Can invite other user's in the room.
|
||||
*/
|
||||
canInvite: boolean;
|
||||
/**
|
||||
* Can copy the room link.
|
||||
*/
|
||||
canCopyRoomLink: boolean;
|
||||
/**
|
||||
* Can mark the room as read.
|
||||
*/
|
||||
canMarkAsRead: boolean;
|
||||
/**
|
||||
* Can mark the room as unread.
|
||||
*/
|
||||
canMarkAsUnread: boolean;
|
||||
/**
|
||||
* Mark the room as read.
|
||||
* @param evt
|
||||
*/
|
||||
markAsRead: (evt: Event) => void;
|
||||
/**
|
||||
* Mark the room as unread.
|
||||
* @param evt
|
||||
*/
|
||||
markAsUnread: (evt: Event) => void;
|
||||
/**
|
||||
* Toggle the room as favourite.
|
||||
* @param evt
|
||||
*/
|
||||
toggleFavorite: (evt: Event) => void;
|
||||
/**
|
||||
* Toggle the room as low priority.
|
||||
*/
|
||||
toggleLowPriority: () => void;
|
||||
/**
|
||||
* Invite other users in the room.
|
||||
* @param evt
|
||||
*/
|
||||
invite: (evt: Event) => void;
|
||||
/**
|
||||
* Copy the room link in the clipboard.
|
||||
* @param evt
|
||||
*/
|
||||
copyRoomLink: (evt: Event) => void;
|
||||
/**
|
||||
* Leave the room.
|
||||
* @param evt
|
||||
*/
|
||||
leaveRoom: (evt: Event) => void;
|
||||
}
|
||||
|
||||
export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags);
|
||||
const { level: notificationLevel } = useUnreadNotifications(room);
|
||||
|
||||
const showMoreOptionsMenu = hasAccessToOptionsMenu(room);
|
||||
|
||||
const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
|
||||
const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]);
|
||||
const isArchived = Boolean(roomTags[DefaultTagID.Archived]);
|
||||
|
||||
const canMarkAsRead = notificationLevel > NotificationLevel.None;
|
||||
const canMarkAsUnread = !canMarkAsRead && !isArchived;
|
||||
|
||||
const canInvite =
|
||||
room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers);
|
||||
const canCopyRoomLink = !isDm;
|
||||
|
||||
// Actions
|
||||
|
||||
const markAsRead = useCallback(
|
||||
async (evt: Event): Promise<void> => {
|
||||
await clearRoomNotification(room, matrixClient);
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", evt);
|
||||
},
|
||||
[room, matrixClient],
|
||||
);
|
||||
|
||||
const markAsUnread = useCallback(
|
||||
async (evt: Event): Promise<void> => {
|
||||
await setMarkedUnreadState(room, matrixClient, true);
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", evt);
|
||||
},
|
||||
[room, matrixClient],
|
||||
);
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
(evt: Event): void => {
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt);
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
const toggleLowPriority = useCallback((): void => tagRoom(room, DefaultTagID.LowPriority), [room]);
|
||||
|
||||
const invite = useCallback(
|
||||
(evt: Event): void => {
|
||||
dispatcher.dispatch({
|
||||
action: "view_invite",
|
||||
roomId: room.roomId,
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", evt);
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
const copyRoomLink = useCallback(
|
||||
(evt: Event): void => {
|
||||
dispatcher.dispatch({
|
||||
action: "copy_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt);
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
const leaveRoom = useCallback(
|
||||
(evt: Event): void => {
|
||||
dispatcher.dispatch({
|
||||
action: isArchived ? "forget_room" : "leave_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", evt);
|
||||
},
|
||||
[room, isArchived],
|
||||
);
|
||||
|
||||
return {
|
||||
showMoreOptionsMenu,
|
||||
isFavourite,
|
||||
canInvite,
|
||||
canCopyRoomLink,
|
||||
canMarkAsRead,
|
||||
canMarkAsUnread,
|
||||
markAsRead,
|
||||
markAsUnread,
|
||||
toggleFavorite,
|
||||
toggleLowPriority,
|
||||
invite,
|
||||
copyRoomLink,
|
||||
leaveRoom,
|
||||
};
|
||||
}
|
||||
49
src/components/viewmodels/roomlist/RoomListItemViewModel.tsx
Normal file
49
src/components/viewmodels/roomlist/RoomListItemViewModel.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { useCallback } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { hasAccessToOptionsMenu } from "./utils";
|
||||
|
||||
export interface RoomListItemViewState {
|
||||
/**
|
||||
* Whether the hover menu should be shown.
|
||||
*/
|
||||
showHoverMenu: boolean;
|
||||
/**
|
||||
* Open the room having given roomId.
|
||||
*/
|
||||
openRoom: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for the room list item
|
||||
* @see {@link RoomListItemViewState} for more information about what this view model returns.
|
||||
*/
|
||||
export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
// incoming: Check notification menu rights
|
||||
const showHoverMenu = hasAccessToOptionsMenu(room);
|
||||
|
||||
// Actions
|
||||
|
||||
const openRoom = useCallback((): void => {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "RoomList",
|
||||
});
|
||||
}, [room]);
|
||||
|
||||
return {
|
||||
showHoverMenu,
|
||||
openRoom,
|
||||
};
|
||||
}
|
||||
@@ -5,12 +5,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 { useCallback } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms";
|
||||
import { type SortOption, useSorter } from "./useSorter";
|
||||
|
||||
@@ -19,12 +14,6 @@ export interface RoomListViewState {
|
||||
* A list of rooms to be displayed in the left panel.
|
||||
*/
|
||||
rooms: Room[];
|
||||
|
||||
/**
|
||||
* Open the room having given roomId.
|
||||
*/
|
||||
openRoom: (roomId: string) => void;
|
||||
|
||||
/**
|
||||
* A list of objects that provide the view enough information
|
||||
* to render primary room filters.
|
||||
@@ -60,17 +49,8 @@ export function useRoomListViewModel(): RoomListViewState {
|
||||
const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms();
|
||||
const { activeSortOption, sort } = useSorter();
|
||||
|
||||
const openRoom = useCallback((roomId: string): void => {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: "RoomList",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
rooms,
|
||||
openRoom,
|
||||
primaryFilters,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
|
||||
25
src/components/viewmodels/roomlist/utils.ts
Normal file
25
src/components/viewmodels/roomlist/utils.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { type Room, KnownMembership } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { isKnockDenied } from "../../../utils/membership";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
/**
|
||||
* Check if the user has access to the options menu.
|
||||
* @param room
|
||||
*/
|
||||
export function hasAccessToOptionsMenu(room: Room): boolean {
|
||||
return (
|
||||
room.getMyMembership() === KnownMembership.Invite ||
|
||||
(room.getMyMembership() !== KnownMembership.Knock &&
|
||||
!isKnockDenied(room) &&
|
||||
shouldShowComponent(UIComponent.RoomOptionsMenu))
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { AutoSizer, List, type ListRowProps } from "react-virtualized";
|
||||
|
||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListCell } from "./RoomListCell";
|
||||
import { RoomListItemView } from "./RoomListItemView";
|
||||
|
||||
interface RoomListProps {
|
||||
/**
|
||||
@@ -22,12 +22,10 @@ interface RoomListProps {
|
||||
/**
|
||||
* A virtualized list of rooms.
|
||||
*/
|
||||
export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Element {
|
||||
export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element {
|
||||
const roomRendererMemoized = useCallback(
|
||||
({ key, index, style }: ListRowProps) => (
|
||||
<RoomListCell room={rooms[index]} key={key} style={style} onClick={() => openRoom(rooms[index].roomId)} />
|
||||
),
|
||||
[rooms, openRoom],
|
||||
({ key, index, style }: ListRowProps) => <RoomListItemView room={rooms[index]} key={key} style={style} />,
|
||||
[rooms],
|
||||
);
|
||||
|
||||
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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, { type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
|
||||
|
||||
interface RoomListCellProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* The room to display
|
||||
*/
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cell in the room list
|
||||
*/
|
||||
export function RoomListCell({ room, ...props }: RoomListCellProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
className="mx_RoomListCell"
|
||||
type="button"
|
||||
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
|
||||
{...props}
|
||||
>
|
||||
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
|
||||
<Flex className="mx_RoomListCell_container" gap="var(--cpd-space-3x)" align="center">
|
||||
<DecoratedRoomAvatar room={room} size="32px" />
|
||||
<Flex className="mx_RoomListCell_content" align="center">
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<span title={room.name}>{room.name}</span>
|
||||
{/* Future hover menu et notification badges */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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, { type ComponentProps, forwardRef, type JSX, useState } from "react";
|
||||
import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read";
|
||||
import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread";
|
||||
import FavouriteIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite";
|
||||
import ArrowDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/arrow-down";
|
||||
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
|
||||
import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link";
|
||||
import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave";
|
||||
import OverflowIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import {
|
||||
type RoomListItemMenuViewState,
|
||||
useRoomListItemMenuViewModel,
|
||||
} from "../../../viewmodels/roomlist/RoomListItemMenuViewModel";
|
||||
|
||||
interface RoomListItemMenuViewProps {
|
||||
/**
|
||||
* The room to display the menu for.
|
||||
*/
|
||||
room: Room;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
* @param isOpen
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A view for the room list item menu.
|
||||
*/
|
||||
export function RoomListItemMenuView({ room, setMenuOpen }: RoomListItemMenuViewProps): JSX.Element {
|
||||
const vm = useRoomListItemMenuViewModel(room);
|
||||
|
||||
return (
|
||||
<Flex className="mx_RoomListItemMenuView" align="center" gap="var(--cpd-space-1x)">
|
||||
{vm.showMoreOptionsMenu && <MoreOptionsMenu setMenuOpen={setMenuOpen} vm={vm} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
interface MoreOptionsMenuProps {
|
||||
/**
|
||||
* The view model state for the menu.
|
||||
*/
|
||||
vm: RoomListItemMenuViewState;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
* @param isOpen
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The more options menu for the room list item.
|
||||
*/
|
||||
function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|room|more_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<MoreOptionsButton />}
|
||||
>
|
||||
{vm.canMarkAsRead && (
|
||||
<MenuItem
|
||||
Icon={MarkAsReadIcon}
|
||||
label={_t("room_list|more_options|mark_read")}
|
||||
onSelect={vm.markAsRead}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{vm.canMarkAsUnread && (
|
||||
<MenuItem
|
||||
Icon={MarkAsUnreadIcon}
|
||||
label={_t("room_list|more_options|mark_unread")}
|
||||
onSelect={vm.markAsUnread}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<ToggleMenuItem
|
||||
checked={vm.isFavourite}
|
||||
Icon={FavouriteIcon}
|
||||
label={_t("room_list|more_options|favourited")}
|
||||
onSelect={vm.toggleFavorite}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={ArrowDownIcon}
|
||||
label={_t("room_list|more_options|low_priority")}
|
||||
onSelect={vm.toggleLowPriority}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
{vm.canInvite && (
|
||||
<MenuItem
|
||||
Icon={UserAddIcon}
|
||||
label={_t("action|invite")}
|
||||
onSelect={vm.invite}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{vm.canCopyRoomLink && (
|
||||
<MenuItem
|
||||
Icon={LinkIcon}
|
||||
label={_t("room_list|more_options|copy_link")}
|
||||
onSelect={vm.copyRoomLink}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<Separator />
|
||||
<MenuItem
|
||||
kind="critical"
|
||||
Icon={LeaveIcon}
|
||||
label={_t("room_list|more_options|leave_room")}
|
||||
onSelect={vm.leaveRoom}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {}
|
||||
|
||||
/**
|
||||
* A button to trigger the more options menu.
|
||||
*/
|
||||
export const MoreOptionsButton = forwardRef<HTMLButtonElement, MoreOptionsButtonProps>(
|
||||
function MoreOptionsButton(props, ref) {
|
||||
return (
|
||||
<Tooltip label={_t("room_list|room|more_options")}>
|
||||
<IconButton aria-label={_t("room_list|room|more_options")} {...props} ref={ref}>
|
||||
<OverflowIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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, { type JSX, useState } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
|
||||
import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
|
||||
interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* The room to display
|
||||
*/
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* An item in the room list
|
||||
*/
|
||||
export function RoomListItemView({ room, ...props }: RoomListItemViewPropsProps): JSX.Element {
|
||||
const vm = useRoomListItemViewModel(room);
|
||||
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
// The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown
|
||||
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
||||
const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames("mx_RoomListItemView", {
|
||||
mx_RoomListItemView_menu_open: showHoverDecoration,
|
||||
})}
|
||||
type="button"
|
||||
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
|
||||
onClick={() => vm.openRoom()}
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseOut={() => setIsHover(false)}
|
||||
onFocus={() => setIsHover(true)}
|
||||
onBlur={() => setIsHover(false)}
|
||||
{...props}
|
||||
>
|
||||
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
|
||||
<Flex className="mx_RoomListItemView_container" gap="var(--cpd-space-3x)" align="center">
|
||||
<DecoratedRoomAvatar room={room} size="32px" />
|
||||
<Flex
|
||||
className="mx_RoomListItemView_content"
|
||||
gap="var(--cpd-space-3x)"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<span title={room.name}>{room.name}</span>
|
||||
{showHoverDecoration && (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => {
|
||||
if (isOpen) setIsMenuOpen(isOpen);
|
||||
// To avoid icon blinking when closing the menu, we delay the state update
|
||||
else setTimeout(() => setIsMenuOpen(isOpen), 0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2112,6 +2112,14 @@
|
||||
"other": "Currently joining %(count)s rooms"
|
||||
},
|
||||
"list_title": "Room list",
|
||||
"more_options": {
|
||||
"copy_link": "Copy room link",
|
||||
"favourited": "Favourited",
|
||||
"leave_room": "Leave room",
|
||||
"low_priority": "Low priority",
|
||||
"mark_read": "Mark as read",
|
||||
"mark_unread": "Mark as unread"
|
||||
},
|
||||
"notification_options": "Notification options",
|
||||
"open_space_menu": "Open space menu",
|
||||
"primary_filters": "Room list filters",
|
||||
@@ -2120,6 +2128,7 @@
|
||||
"other": "Currently removing messages in %(count)s rooms"
|
||||
},
|
||||
"room": {
|
||||
"more_options": "More Options",
|
||||
"open_room": "Open room %(roomName)s"
|
||||
},
|
||||
"show_less": "Show less",
|
||||
|
||||
Reference in New Issue
Block a user