From 1ae6478d2bb42bb442af7751bacd6311fc55096d Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 30 Jan 2026 09:42:28 +0000 Subject: [PATCH] Add RoomListItem component Add the RoomListItem component to shared-components. Includes context menu, hover menu, notification menu, and more options menu. --- .../RoomListItem.stories.tsx/bold-auto.png | Bin 0 -> 7391 bytes .../RoomListItem.stories.tsx/default-auto.png | Bin 0 -> 6804 bytes .../invitation-auto.png | Bin 0 -> 21906 bytes .../no-message-preview-auto.png | Bin 0 -> 5475 bytes .../selected-auto.png | Bin 0 -> 6865 bytes .../unsent-message-auto.png | Bin 0 -> 7549 bytes .../with-hover-menu-auto.png | Bin 0 -> 6804 bytes .../with-mention-auto.png | Bin 0 -> 7753 bytes .../with-notification-auto.png | Bin 0 -> 7271 bytes .../without-hover-menu-auto.png | Bin 0 -> 6804 bytes .../RoomListItem/RoomListItem.module.css | 106 ++ .../RoomListItem/RoomListItem.stories.tsx | 206 +++ .../RoomListItem/RoomListItem.test.tsx | 123 ++ .../room-list/RoomListItem/RoomListItem.tsx | 202 +++ .../RoomListItem/RoomListItemContextMenu.tsx | 40 + .../RoomListItem/RoomListItemHoverMenu.tsx | 42 + .../RoomListItemMoreOptionsMenu.test.tsx | 227 +++ .../RoomListItemMoreOptionsMenu.tsx | 137 ++ .../RoomListItemNotificationMenu.test.tsx | 164 +++ .../RoomListItemNotificationMenu.tsx | 105 ++ .../src/room-list/RoomListItem/RoomNotifs.ts | 20 + .../__snapshots__/RoomListItem.test.tsx.snap | 1236 +++++++++++++++++ .../RoomListItem/default-snapshot.ts | 39 + .../src/room-list/RoomListItem/index.ts | 25 + 24 files changed, 2672 insertions(+) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/default-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/invitation-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/no-message-preview-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/selected-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/unsent-message-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-mention-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/without-hover-menu-auto.png create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx create mode 100644 packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts create mode 100644 packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap create mode 100644 packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts create mode 100644 packages/shared-components/src/room-list/RoomListItem/index.ts diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/bold-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..d36edbed0c8de3dc1216532ef731579711aa0e4f GIT binary patch literal 7391 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 M`uvXe} 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 J#G?k^L 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 WY1((g;Hm(Cv)+E+H=O?I=l=n<873S6 literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-hover-menu-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..24dc3f42d3f434974be46f7ef851823447d8a53e GIT binary patch literal 6804 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 KDZgC*{eJ*0D9^(H literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItem/RoomListItem.stories.tsx/with-notification-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee34aabfefd9fb8ec4e0e5e5a8b99e9241ef2c0 GIT binary patch literal 7271 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 M`u container------------------------------------| + * | | room avatar <-8px-> content----------------| + * | | | room_name <- 20px ->| + * | | | --------------------| <-- border + * |-------------------------------------------------------| + */ +.roomListItem { + /* Remove button default style */ + background: unset; + border: none; + padding: 0; + text-align: unset; + + cursor: pointer; + height: 48px; + width: 100%; + + padding-left: var(--cpd-space-3x); + font: var(--cpd-font-body-md-regular); + color: var(--cpd-color-text-primary); + + /* Hide the menu by default */ + .hoverMenu { + display: none; + } +} + +/* Show hover menu and background on hover/focus/menu-open states */ +.roomListItem:hover, +.roomListItem:focus-visible, +/* When the context menu is opened */ +.roomListItem[data-state="open"], +/* When the options and notifications menu are opened */ +.roomListItem:has(.hoverMenu > button[data-state="open"]) { + background-color: var(--cpd-color-bg-action-secondary-hovered); + + .hoverMenu { + display: flex; + } + + /* When the menu is visible, hide the notification decoration to avoid clutter */ + .notificationDecoration { + display: none; + } + + /** + * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 + * the icon size of the menu is 18px instead of 20px with a different internal padding + * We need to use 18px to align the icon with the others icons + * 18px is not available in compound spacing + */ + .content { + padding-right: 18px; + } +} + +.content { + height: 100%; + flex: 1; + /* The border is only under the room name and the future hover menu */ + border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); + box-sizing: border-box; + min-width: 0; + padding-right: var(--cpd-space-5x); +} + +.text { + min-width: 0; +} + +.roomName { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.messagePreview { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.selected { + background-color: var(--cpd-color-bg-action-secondary-pressed); +} + +.bold .roomName { + font: var(--cpd-font-body-md-semibold); +} + +/* Set icon color for hover menu buttons */ +.hoverMenu svg { + fill: var(--cpd-color-icon-primary); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx new file mode 100644 index 0000000000..0da88a6739 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx @@ -0,0 +1,206 @@ +/* + * Copyright 2026 Element Creations 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 { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomListItemView, type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItem"; +import { useMockedViewModel } from "../../viewmodel"; +import { defaultSnapshot } from "./default-snapshot"; +import { renderAvatar } from "../story-mocks"; + +type RoomListItemProps = RoomListItemSnapshot & + RoomListItemActions & { + isSelected: boolean; + isFocused: boolean; + onFocus: (room: any, e: React.FocusEvent) => void; + roomIndex: number; + roomCount: number; + renderAvatar: (room: any) => React.ReactElement; + }; + +// Wrapper component that creates a mocked ViewModel +const RoomListItemWrapper = ({ + onOpenRoom, + onMarkAsRead, + onMarkAsUnread, + onToggleFavorite, + onToggleLowPriority, + onInvite, + onCopyRoomLink, + onLeaveRoom, + onSetRoomNotifState, + isSelected, + isFocused, + onFocus, + roomIndex, + roomCount, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListItemProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onOpenRoom, + onMarkAsRead, + onMarkAsUnread, + onToggleFavorite, + onToggleLowPriority, + onInvite, + onCopyRoomLink, + onLeaveRoom, + onSetRoomNotifState, + }); + return ( + + ); +}; + +const meta = { + title: "Room List/RoomListItem", + component: RoomListItemWrapper, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], + args: { + ...defaultSnapshot, + isSelected: false, + isFocused: false, + roomIndex: 0, + roomCount: 10, + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + onFocus: fn(), + renderAvatar, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Selected: Story = { + args: { + isSelected: true, + }, +}; + +export const Bold: Story = { + args: { + isBold: true, + name: "Team Updates", + }, +}; + +export const WithNotification: Story = { + args: { + isBold: true, + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: true, + hasUnreadCount: true, + count: 3, + muted: false, + }, + }, +}; + +export const WithMention: Story = { + args: { + isBold: true, + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: false, + isMention: true, + isActivityNotification: false, + isNotification: true, + hasUnreadCount: true, + count: 1, + muted: false, + }, + }, +}; + +export const Invitation: Story = { + args: { + name: "Secret Project", + messagePreview: "Bob invited you", + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: true, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + }, +}; + +export const UnsentMessage: Story = { + args: { + messagePreview: "Failed to send message", + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: true, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + }, +}; + +export const NoMessagePreview: Story = { + args: { + messagePreview: undefined, + }, +}; + +export const WithHoverMenu: Story = { + args: { + showMoreOptionsMenu: true, + }, +}; + +export const WithoutHoverMenu: Story = { + args: { + showMoreOptionsMenu: false, + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx new file mode 100644 index 0000000000..788c9f317f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2026 Element Creations 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 { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./RoomListItem.stories"; + +const { + Default, + Selected, + Bold, + WithNotification, + WithMention, + Invitation, + UnsentMessage, + NoMessagePreview, + WithHoverMenu, + WithoutHoverMenu, +} = composeStories(stories); + +describe("", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Selected story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Bold story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithNotification story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithMention story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Invitation story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders UnsentMessage story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NoMessagePreview story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithHoverMenu story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("should call onOpenRoom when clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("option")); + expect(Default.args.onOpenRoom).toHaveBeenCalled(); + }); + + it("should have aria-selected true when selected", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("aria-selected", "true"); + }); + + it("should have aria-selected false when not selected", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("aria-selected", "false"); + }); + + it("should have tabIndex -1 when not focused", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("tabIndex", "-1"); + }); + + it("should call onFocus when focused", () => { + render(); + screen.getByRole("option").focus(); + expect(Default.args.onFocus).toHaveBeenCalled(); + }); + + it("should display notification decoration when present", () => { + render(); + expect(screen.getByTestId("notification-decoration")).toBeInTheDocument(); + }); + + it("should hide notification decoration when not present", () => { + render(); + expect(screen.queryByTestId("notification-decoration")).toBeNull(); + }); + + it("should show hover menu when showMoreOptionsMenu is true", () => { + const { container } = render(); + expect(container.querySelector('[aria-label="More Options"]')).not.toBeNull(); + }); + + it("should hide hover menu when showMoreOptionsMenu is false", () => { + const { container } = render(); + expect(container.querySelector('[aria-label="More Options"]')).toBeNull(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx new file mode 100644 index 0000000000..ddde7ffeee --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx @@ -0,0 +1,202 @@ +/* + * Copyright 2026 Element Creations 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, useEffect, useRef, type ReactNode } from "react"; +import classNames from "classnames"; + +import { Flex } from "../../utils/Flex"; +import { NotificationDecoration, type NotificationDecorationData } from "./NotificationDecoration"; +import { RoomListItemHoverMenu } from "./RoomListItemHoverMenu"; +import { RoomListItemContextMenu } from "./RoomListItemContextMenu"; +import { type RoomNotifState } from "./RoomNotifs"; +import styles from "./RoomListItem.module.css"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import { _t } from "../../utils/i18n"; + +/** + * Generate an accessible label for a room based on its notification state. + */ +function getA11yLabel(roomName: string, notification: NotificationDecorationData): string { + if (notification.isUnsentMessage) { + return _t("room_list|a11y|unsent_message", { roomName }); + } else if (notification.invited) { + return _t("room_list|a11y|invitation", { roomName }); + } else if (notification.isMention && notification.count) { + return _t("room_list|a11y|mention", { roomName, count: notification.count }); + } else if (notification.hasUnreadCount && notification.count) { + return _t("room_list|a11y|unread", { roomName, count: notification.count }); + } else { + return _t("room_list|a11y|default", { roomName }); + } +} + +/** + * Snapshot for a room list item. + * Contains all the data needed to render a room in the list. + */ +export interface RoomListItemSnapshot { + /** Unique identifier for the room (used for list keying) */ + id: string; + /** The opaque Room object from the client (e.g., matrix-js-sdk Room) */ + room: any; + /** The name of the room */ + name: string; + /** Whether the room name should be bolded (has unread/activity) */ + isBold: boolean; + /** Optional message preview text */ + messagePreview?: string; + /** Notification decoration data */ + notification: NotificationDecorationData; + /** Whether the more options menu should be shown */ + showMoreOptionsMenu: boolean; + /** Whether the notification menu should be shown */ + showNotificationMenu: boolean; + /** Whether the room is a favourite room */ + isFavourite: boolean; + /** Whether the room is a low priority room */ + isLowPriority: boolean; + /** Can invite other users 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; + /** The room's notification state */ + roomNotifState: RoomNotifState; +} + +/** + * Actions interface for room list item operations. + * Implemented by the room item view model. + */ +export interface RoomListItemActions { + /** Called when the room should be opened */ + onOpenRoom: () => void; + /** Called when the room should be marked as read */ + onMarkAsRead: () => void; + /** Called when the room should be marked as unread */ + onMarkAsUnread: () => void; + /** Called when the room's favorite status should be toggled */ + onToggleFavorite: () => void; + /** Called when the room's low priority status should be toggled */ + onToggleLowPriority: () => void; + /** Called when inviting users to the room */ + onInvite: () => void; + /** Called when copying the room link */ + onCopyRoomLink: () => void; + /** Called when leaving the room */ + onLeaveRoom: () => void; + /** Called when setting the room notification state */ + onSetRoomNotifState: (state: RoomNotifState) => void; +} + +/** + * The view model type for a room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemView component + */ +export interface RoomListItemViewProps extends Omit, "onFocus"> { + /** The room item view model */ + vm: RoomItemViewModel; + /** Whether the room is selected */ + isSelected: boolean; + /** Whether the room should be focused */ + isFocused: boolean; + /** Callback when item receives focus */ + onFocus: (roomId: string, e: React.FocusEvent) => void; + /** Index of this room in the list (for accessibility) */ + roomIndex: number; + /** Total number of rooms in the list (for accessibility) */ + roomCount: number; + /** Function to render the room avatar */ + renderAvatar: (room: any) => ReactNode; +} + +/** + * A presentational room list item component. + * Displays room name, avatar, message preview, and notifications. + */ +export const RoomListItemView = memo(function RoomListItemView({ + vm, + isSelected, + isFocused, + onFocus, + roomIndex, + roomCount, + renderAvatar, + ...props +}: RoomListItemViewProps): JSX.Element { + const ref = useRef(null); + const item = useViewModel(vm); + + useEffect(() => { + if (isFocused) { + ref.current?.focus({ preventScroll: true, focusVisible: true } as FocusOptions); + } + }, [isFocused]); + + // Generate a11y label from notification state and room name + const a11yLabel = getA11yLabel(item.name, item.notification); + + const content = ( + ) => onFocus(item.id, e)} + tabIndex={isFocused ? 0 : -1} + {...props} + > + {renderAvatar(item.room)} + + {/* We truncate the room name when too long. Title here is to show the full name on hover */} +
+
+ {item.name} +
+ {item.messagePreview && ( +
+ {item.messagePreview} +
+ )} +
+ {(item.showMoreOptionsMenu || item.showNotificationMenu) && ( + + )} + + {/* aria-hidden because we summarise the unread count/notification status in a11yLabel */} +
+ +
+
+
+ ); + + return {content}; +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx new file mode 100644 index 0000000000..0d202474f8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Element Creations 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, type PropsWithChildren } from "react"; +import { ContextMenu } from "@vector-im/compound-web"; + +import { _t } from "../../utils/i18n"; +import { MoreOptionContent, type RoomItemViewModel } from "./RoomListItemMoreOptionsMenu"; + +/** + * Props for RoomListItemContextMenu component + */ +export interface RoomListItemContextMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The context menu for room list items. + * Wraps the trigger element with a right-click context menu displaying room options. + */ +export const RoomListItemContextMenu: React.FC> = ({ + vm, + children, +}): JSX.Element => { + return ( + + + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx new file mode 100644 index 0000000000..9a453b2014 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Element Creations 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 { Flex } from "../../utils/Flex"; +import { RoomListItemMoreOptionsMenu, type RoomItemViewModel } from "./RoomListItemMoreOptionsMenu"; +import { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +import styles from "./RoomListItem.module.css"; + +/** + * Props for RoomListItemHoverMenu component + */ +export interface RoomListItemHoverMenuProps { + /** Whether the more options menu should be shown */ + showMoreOptionsMenu: boolean; + /** Whether the notification menu should be shown */ + showNotificationMenu: boolean; + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The hover menu for room list items. + * Displays more options and notification settings menus. + */ +export const RoomListItemHoverMenu: React.FC = ({ + showMoreOptionsMenu, + showNotificationMenu, + vm, +}): JSX.Element => { + return ( + + {showMoreOptionsMenu && } + {showNotificationMenu && } + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx new file mode 100644 index 0000000000..40b9917c5b --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx @@ -0,0 +1,227 @@ +/* + * Copyright 2026 Element Creations 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 { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; + +import { RoomListItemMoreOptionsMenu } from "./RoomListItemMoreOptionsMenu"; +import { useMockedViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { defaultSnapshot } from "./default-snapshot"; + +describe("", () => { + const mockCallbacks = { + onOpenRoom: vi.fn(), + onMarkAsRead: vi.fn(), + onMarkAsUnread: vi.fn(), + onToggleFavorite: vi.fn(), + onToggleLowPriority: vi.fn(), + onInvite: vi.fn(), + onCopyRoomLink: vi.fn(), + onLeaveRoom: vi.fn(), + onSetRoomNotifState: vi.fn(), + }; + + const renderMenu = (overrides: Partial = {}): ReturnType => { + const TestComponent = (): JSX.Element => { + const vm = useMockedViewModel( + { + ...defaultSnapshot, + showMoreOptionsMenu: true, + showNotificationMenu: false, + ...overrides, + } as RoomListItemSnapshot, + mockCallbacks, + ); + return ; + }; + return render(); + }; + + it("should render the more options button", () => { + renderMenu(); + expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument(); + }); + + it("should open menu when clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + }); + + it("should show mark as read option when canMarkAsRead is true", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Mark as read" })).toBeInTheDocument(); + }); + + it("should not show mark as read option when canMarkAsRead is false", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "Mark as read" })).not.toBeInTheDocument(); + }); + + it("should call onMarkAsRead when mark as read clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const markAsReadOption = screen.getByRole("menuitem", { name: "Mark as read" }); + await user.click(markAsReadOption); + + expect(mockCallbacks.onMarkAsRead).toHaveBeenCalled(); + }); + + it("should show mark as unread option when canMarkAsUnread is true", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsUnread: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Mark as unread" })).toBeInTheDocument(); + }); + + it("should call onMarkAsUnread when mark as unread clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsUnread: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const markAsUnreadOption = screen.getByRole("menuitem", { name: "Mark as unread" }); + await user.click(markAsUnreadOption); + + expect(mockCallbacks.onMarkAsUnread).toHaveBeenCalled(); + }); + + it("should show favorite option and call onToggleFavorite", async () => { + const user = userEvent.setup(); + renderMenu({ isFavourite: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" }); + expect(favoriteOption).toBeInTheDocument(); + expect(favoriteOption).toHaveAttribute("aria-checked", "false"); + + await user.click(favoriteOption); + expect(mockCallbacks.onToggleFavorite).toHaveBeenCalled(); + }); + + it("should show favorite as checked when isFavourite is true", async () => { + const user = userEvent.setup(); + renderMenu({ isFavourite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" }); + expect(favoriteOption).toHaveAttribute("aria-checked", "true"); + }); + + it("should show low priority option and call onToggleLowPriority", async () => { + const user = userEvent.setup(); + renderMenu({ isLowPriority: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const lowPriorityOption = screen.getByRole("menuitemcheckbox", { name: "Low priority" }); + expect(lowPriorityOption).toBeInTheDocument(); + expect(lowPriorityOption).toHaveAttribute("aria-checked", "false"); + + await user.click(lowPriorityOption); + expect(mockCallbacks.onToggleLowPriority).toHaveBeenCalled(); + }); + + it("should show invite option when canInvite is true", async () => { + const user = userEvent.setup(); + renderMenu({ canInvite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument(); + }); + + it("should call onInvite when invite clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canInvite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const inviteOption = screen.getByRole("menuitem", { name: "Invite" }); + await user.click(inviteOption); + + expect(mockCallbacks.onInvite).toHaveBeenCalled(); + }); + + it("should show copy link option when canCopyRoomLink is true", async () => { + const user = userEvent.setup(); + renderMenu({ canCopyRoomLink: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Copy room link" })).toBeInTheDocument(); + }); + + it("should call onCopyRoomLink when copy link clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canCopyRoomLink: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const copyLinkOption = screen.getByRole("menuitem", { name: "Copy room link" }); + await user.click(copyLinkOption); + + expect(mockCallbacks.onCopyRoomLink).toHaveBeenCalled(); + }); + + it("should show leave room option", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Leave room" })).toBeInTheDocument(); + }); + + it("should call onLeaveRoom when leave room clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const leaveRoomOption = screen.getByRole("menuitem", { name: "Leave room" }); + await user.click(leaveRoomOption); + + expect(mockCallbacks.onLeaveRoom).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx new file mode 100644 index 0000000000..d10b5c32ec --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx @@ -0,0 +1,137 @@ +/* + * Copyright 2026 Element Creations 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, { useState, type JSX } from "react"; +import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem } from "@vector-im/compound-web"; +import { + MarkAsReadIcon, + MarkAsUnreadIcon, + FavouriteIcon, + ArrowDownIcon, + UserAddIcon, + LinkIcon, + LeaveIcon, + OverflowHorizontalIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot, RoomListItemActions } from "./RoomListItem"; + +/** + * View model type for room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemMoreOptionsMenu component + */ +export interface RoomListItemMoreOptionsMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The more options menu for room list items. + * Displays additional room actions like mark as read/unread, favorite, invite, etc. + */ +export function RoomListItemMoreOptionsMenu({ vm }: RoomListItemMoreOptionsMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + + + + } + > + + + ); +} + +interface MoreOptionContentProps { + vm: RoomItemViewModel; +} + +export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { + const snapshot = useViewModel(vm); + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
e.stopPropagation()}> + {snapshot.canMarkAsRead && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + {snapshot.canMarkAsUnread && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + evt.stopPropagation()} + /> + evt.stopPropagation()} + /> + {snapshot.canInvite && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + {snapshot.canCopyRoomLink && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + + evt.stopPropagation()} + hideChevron={true} + /> +
+ ); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx new file mode 100644 index 0000000000..3f88e2f8a1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 2026 Element Creations 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 { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; + +import { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +import { RoomNotifState } from "./RoomNotifs"; +import { useMockedViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { defaultSnapshot } from "./default-snapshot"; + +describe("", () => { + const mockCallbacks = { + onOpenRoom: vi.fn(), + onMarkAsRead: vi.fn(), + onMarkAsUnread: vi.fn(), + onToggleFavorite: vi.fn(), + onToggleLowPriority: vi.fn(), + onInvite: vi.fn(), + onCopyRoomLink: vi.fn(), + onLeaveRoom: vi.fn(), + onSetRoomNotifState: vi.fn(), + }; + + const renderMenu = (roomNotifState: RoomNotifState = RoomNotifState.AllMessages): ReturnType => { + const TestComponent = (): JSX.Element => { + const vm = useMockedViewModel( + { + ...defaultSnapshot, + showMoreOptionsMenu: false, + showNotificationMenu: true, + roomNotifState, + } as RoomListItemSnapshot, + mockCallbacks, + ); + return ; + }; + return render(); + }; + + it("should render the notification menu button", () => { + renderMenu(); + expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument(); + }); + + it("should show muted icon when notifications are muted", () => { + renderMenu(RoomNotifState.Mute); + const button = screen.getByRole("button", { name: "Notification options" }); + expect(button.querySelector("svg")).toBeInTheDocument(); + }); + + it("should open menu when clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + }); + + it("should call onSetRoomNotifState with AllMessages when default settings selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" }); + await user.click(defaultOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages); + }); + + it("should call onSetRoomNotifState with AllMessagesLoud when all messages selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" }); + await user.click(allMessagesOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud); + }); + + it("should call onSetRoomNotifState with MentionsOnly when mentions and keywords selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" }); + await user.click(mentionsOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly); + }); + + it("should call onSetRoomNotifState with Mute when mute selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const muteOption = screen.getByRole("menuitem", { name: "Mute room" }); + await user.click(muteOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); + }); + + it("should show check mark next to selected option - AllMessage", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.AllMessages); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" }); + expect(defaultOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - AllMessagesLoud", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.AllMessagesLoud); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" }); + expect(allMessagesOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - MentionsOnly", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.MentionsOnly); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" }); + expect(mentionsOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - Mute", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.Mute); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const muteOption = screen.getByRole("menuitem", { name: "Mute room" }); + expect(muteOption).toHaveAttribute("aria-selected", "true"); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx new file mode 100644 index 0000000000..e4038fae6c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2026 Element Creations 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, { useState, type JSX } from "react"; +import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import { + NotificationsSolidIcon, + NotificationsOffSolidIcon, + CheckIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; +import { RoomNotifState } from "./RoomNotifs"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot, RoomListItemActions } from "./RoomListItem"; + +/** + * View model type for room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemNotificationMenu component + */ +export interface RoomListItemNotificationMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The notification settings menu for room list items. + * Displays options to change notification settings. + */ +export function RoomListItemNotificationMenu({ vm }: RoomListItemNotificationMenuProps): JSX.Element { + const snapshot = useViewModel(vm); + const [open, setOpen] = useState(false); + const isMuted = snapshot.roomNotifState === RoomNotifState.Mute; + const checkComponent = ; + + return ( + + {isMuted ? : } + + } + > + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
e.stopPropagation()} + > + vm.onSetRoomNotifState(RoomNotifState.AllMessages)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.AllMessages && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.AllMessagesLoud)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.AllMessagesLoud && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.MentionsOnly)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.MentionsOnly && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.Mute)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.Mute && checkComponent} + +
+
+ ); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts b/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts new file mode 100644 index 0000000000..06fc0fc23d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +/** + * Notification state for a room. + */ +export enum RoomNotifState { + /** All messages (default) */ + AllMessages = "all_messages", + /** All messages with sound */ + AllMessagesLoud = "all_messages_loud", + /** Only mentions and keywords */ + MentionsOnly = "mentions_only", + /** Muted */ + Mute = "mute", +} diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap new file mode 100644 index 0000000000..ff1ccad613 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap @@ -0,0 +1,1236 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Bold story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders Default story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders Invitation story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; + +exports[` > renders NoMessagePreview story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders Selected story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders UnsentMessage story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; + +exports[` > renders WithHoverMenu story 1`] = ` +
+
+
+ + +
+ + +
+
+ +`; + +exports[` > renders WithMention story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; + +exports[` > renders WithNotification story 1`] = ` +
+
+
+ + +
+ +
+ +
+ + +`; diff --git a/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts new file mode 100644 index 0000000000..b5e263567f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Element Creations 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 RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomNotifs"; + +export const mockRoom = { name: "General" }; + +export const defaultSnapshot: RoomListItemSnapshot = { + id: "!room:server", + room: mockRoom, + name: "General", + isBold: false, + messagePreview: "Alice: Hey everyone!", + notification: { + hasAnyNotificationOrActivity: false, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/index.ts b/packages/shared-components/src/room-list/RoomListItem/index.ts new file mode 100644 index 0000000000..df4a68bf64 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +export { RoomListItemView } from "./RoomListItem"; +export type { + RoomListItemSnapshot, + RoomItemViewModel, + RoomListItemActions, + RoomListItemViewProps, +} from "./RoomListItem"; +export { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +export type { RoomListItemNotificationMenuProps } from "./RoomListItemNotificationMenu"; +export { RoomListItemMoreOptionsMenu, MoreOptionContent } from "./RoomListItemMoreOptionsMenu"; +export type { RoomListItemMoreOptionsMenuProps } from "./RoomListItemMoreOptionsMenu"; +export { RoomListItemHoverMenu } from "./RoomListItemHoverMenu"; +export type { RoomListItemHoverMenuProps } from "./RoomListItemHoverMenu"; +export { RoomListItemContextMenu } from "./RoomListItemContextMenu"; +export type { RoomListItemContextMenuProps } from "./RoomListItemContextMenu"; +export { NotificationDecoration } from "./NotificationDecoration"; +export type { NotificationDecorationProps, NotificationDecorationData } from "./NotificationDecoration"; +export { RoomNotifState } from "./RoomNotifs";