Files
ThreadNet-Web/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx
T
David BakerandGitHub 2698ad422e Move shared components to a packages/ directory (#30962)
* Move shared components to a packages/ directory

so they can be publish more sensibly

* Iterate towards split out shared-components module

 * Move shared component source into src/ subdir
 * Fix up imports
 * Include shared components in babel-ing (again)

* Remove now unused dependencies

* Update import in storybook preview

* ...except of course they aren't unused

if we import the shared components by source

* Ignore shared components deps

* Add shared-components to i18n paths

and upgrade web-i18n to version that supports doing so

* Move storybook stuff to shared-components

* Seems we don't need this anymore...

* Remove unused deps

and remove storybook plugin from eslint

* Presumably working-directory is only valid on run steps

* Ignore dep & run prettier

* Prettier on knips.ts

* Hopefully run in right dir

* Remember how to software write

* Okay... how about THIS way?

* Oh right, they were git ignored. Sigh.

* Add concurrently

* Ignore in knip

* Better?

* Paaaaaaaackageeeeeeees

* More packages

* Move playwright snapshots

* Still need a custom snapshots dir

* Add eslint back

* Oh, now knip sees them

* Fix another import

* Don't lint shared-components with everything else

Okay, eslint & tsconfig are tied too closely for this to work and
running tsc on the shared components will need its deps installing

* Maybe lint shared components

please?

* Not quite

* Remove storybook again

Re-check if it does work without it

* Remove storybook eslint plugin

as we're not linting storybook here anymore

* Remove this too

* We do need it here though
2025-10-13 10:54:50 +00:00

171 lines
6.0 KiB
TypeScript

/*
* 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, memo, useCallback, useEffect, useRef, useState } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
import { Flex } from "../../../../../packages/shared-components/src/utils/Flex";
import { RoomListItemMenuView } from "./RoomListItemMenuView";
import { NotificationDecoration } from "../NotificationDecoration";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView";
interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement> {
/**
* The room to display
*/
room: Room;
/**
* Whether the room is selected
*/
isSelected: boolean;
/**
* Whether the room is focused
*/
isFocused: boolean;
/**
* A callback that indicates the item has received focus
*/
onFocus: (e: React.FocusEvent) => void;
/**
* The index of the room in the list
*/
roomIndex: number;
/**
* The total number of rooms in the list
*/
roomCount: number;
/**
* Whether the list is currently scrolling
*/
listIsScrolling: boolean;
}
/**
* An item in the room list
*/
export const RoomListItemView = memo(function RoomListItemView({
room,
isSelected,
isFocused,
onFocus,
roomIndex: index,
roomCount: count,
listIsScrolling,
...props
}: RoomListItemViewProps): JSX.Element {
const ref = useRef<HTMLButtonElement>(null);
const vm = useRoomListItemViewModel(room);
const [isHover, setHover] = 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 || isFocused || isHover;
const showHoverMenu = showHoverDecoration && vm.showHoverMenu;
const closeMenu = useCallback(() => {
// To avoid icon blinking when closing the menu, we delay the state update
// Also, let the focus move to the menu trigger before closing the menu
setTimeout(() => setIsMenuOpen(false), 10);
}, []);
useEffect(() => {
if (isFocused) {
ref.current?.focus({ preventScroll: true, focusVisible: true });
}
}, [isFocused]);
const content = (
<Flex
as="button"
ref={ref}
className={classNames("mx_RoomListItemView", {
mx_RoomListItemView_hover: showHoverDecoration,
mx_RoomListItemView_menu_open: showHoverMenu,
mx_RoomListItemView_selected: isSelected,
mx_RoomListItemView_bold: vm.isBold,
})}
gap="var(--cpd-space-3x)"
align="center"
type="button"
role="option"
aria-posinset={index + 1}
aria-setsize={count}
aria-selected={isSelected}
aria-label={vm.a11yLabel}
onClick={() => vm.openRoom()}
onFocus={onFocus}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onBlur={() => setHover(false)}
tabIndex={isFocused ? 0 : -1}
{...props}
>
<RoomAvatarView room={room} />
<Flex
className="mx_RoomListItemView_content"
gap="var(--cpd-space-2x)"
align="center"
justify="space-between"
>
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
<div className="mx_RoomListItemView_text">
<div className="mx_RoomListItemView_roomName" title={vm.name}>
{vm.name}
</div>
{vm.messagePreview && (
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
{vm.messagePreview}
</div>
)}
</div>
{showHoverMenu ? (
<RoomListItemMenuView
room={room}
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
/>
) : (
<>
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
{vm.showNotificationDecoration && (
<NotificationDecoration
notificationState={vm.notificationState}
aria-hidden={true}
hasVideoCall={vm.hasParticipantInCall}
/>
)}
</>
)}
</Flex>
</Flex>
);
// Rendering multiple context menus can causes crashes in radix upstream,
// See https://github.com/radix-ui/primitives/issues/2717.
// We also don't need the context menu while scrolling so can improve scroll performance
// by not rendering it.
if (!vm.showContextMenu || listIsScrolling) return content;
return (
<RoomListItemContextMenuView
room={room}
setMenuOpen={(isOpen) => {
if (isOpen) {
// To avoid icon blinking when the context menu is re-opened
setTimeout(() => setIsMenuOpen(true), 0);
} else {
closeMenu();
}
}}
>
{content}
</RoomListItemContextMenuView>
);
});