+`;
diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx
new file mode 100644
index 0000000000..42c7a3451f
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx
@@ -0,0 +1,9 @@
+/*
+ * 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 { NotificationDecoration } from "./NotificationDecoration";
+export type { NotificationDecorationProps, NotificationDecorationData } from "./NotificationDecoration";
diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css
new file mode 100644
index 0000000000..008a4462ac
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 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.
+ */
+
+/**
+ * The RoomListItem has the following structure:
+ * button--------------------------------------------------|
+ * | <-12px-> 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..fcc4017fb1
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx
@@ -0,0 +1,213 @@
+/*
+ * 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 type { Room } from "./RoomListItem";
+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: Room, e: React.FocusEvent) => void;
+ roomIndex: number;
+ roomCount: number;
+ renderAvatar: (room: Room) => 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,
+ },
+ parameters: {
+ design: {
+ type: "figma",
+ url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=101-13062",
+ },
+ },
+} 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..bc16d15cd3
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx
@@ -0,0 +1,207 @@
+/*
+ * 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";
+
+/**
+ * Opaque type representing a Room object from the parent application
+ */
+export type Room = unknown;
+
+/**
+ * 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: Room;
+ /** 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: Room) => 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
+
+`;
diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx
new file mode 100644
index 0000000000..7697d4829c
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx
@@ -0,0 +1,12 @@
+/*
+ * 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 { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
+export type { RoomListPrimaryFiltersProps } from "./RoomListPrimaryFilters";
+export { useCollapseFilters } from "./useCollapseFilters";
+export { useVisibleFilters } from "./useVisibleFilters";
+export type { FilterId } from "./useVisibleFilters";
diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts
new file mode 100644
index 0000000000..e3fbf74e54
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { useEffect, useRef, useState, type RefObject } from "react";
+
+/**
+ * A hook to manage the wrapping of filters in the room list.
+ * It observes the filter list and hides filters that are wrapping when the list is not expanded.
+ * @param isExpanded
+ * @param wrappingClassName - the CSS class to apply to wrapping filters
+ * @returns an object containing:
+ * - `ref`: a ref to put on the filter list element
+ * - `isWrapping`: a boolean indicating if the filters are wrapping
+ * - `wrappingIndex`: the index of the first filter that is wrapping
+ */
+export function useCollapseFilters(
+ isExpanded: boolean,
+ wrappingClassName: string,
+): {
+ ref: RefObject;
+ isWrapping: boolean;
+ wrappingIndex: number;
+} {
+ const ref = useRef(null);
+ const [isWrapping, setIsWrapping] = useState(false);
+ const [wrappingIndex, setWrappingIndex] = useState(-1);
+
+ useEffect(() => {
+ if (!ref.current) return;
+
+ const hideFilters = (list: Element): void => {
+ let isWrapping = false;
+ Array.from(list.children).forEach((node, i): void => {
+ const child = node as HTMLElement;
+ child.setAttribute("aria-hidden", "false");
+ child.classList.remove(wrappingClassName);
+
+ // If the filter list is expanded, all filters are visible
+ if (isExpanded) return;
+
+ // If the previous element is on the left element of the current one, it means that the filter is wrapping
+ const previousSibling = child.previousElementSibling as HTMLElement | null;
+ if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) {
+ if (!isWrapping) setWrappingIndex(i);
+ isWrapping = true;
+ }
+
+ // If the filter is wrapping, we hide it
+ child.classList.toggle(wrappingClassName, isWrapping);
+ child.setAttribute("aria-hidden", isWrapping.toString());
+ });
+
+ if (!isWrapping) setWrappingIndex(-1);
+ setIsWrapping(isExpanded || isWrapping);
+ };
+
+ hideFilters(ref.current);
+ const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target)));
+
+ observer.observe(ref.current);
+ return () => {
+ observer.disconnect();
+ };
+ }, [isExpanded, wrappingClassName]);
+
+ return { ref, isWrapping, wrappingIndex };
+}
diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts
new file mode 100644
index 0000000000..73a580b4d9
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { useEffect, useState } from "react";
+
+/**
+ * Standard filter identifiers that can be used across implementations.
+ * These are stable keys - the view layer maps them to translated labels.
+ */
+export type FilterId = "unread" | "people" | "rooms" | "favourite" | "mentions" | "invites" | "low_priority";
+
+/**
+ * A hook to sort the filter IDs by active state.
+ * The list is sorted if the active filter index is greater than or equal to the wrapping index.
+ * If the wrapping index is -1, the filters are not sorted.
+ *
+ * @param filterIds - the list of filter IDs to sort.
+ * @param activeFilterId - the currently active filter ID (if any).
+ * @param wrappingIndex - the index of the first filter that is wrapping.
+ */
+export function useVisibleFilters(
+ filterIds: FilterId[],
+ activeFilterId: FilterId | undefined,
+ wrappingIndex: number,
+): FilterId[] {
+ // By default, the filters are not sorted
+ const [sortedFilterIds, setSortedFilterIds] = useState(filterIds);
+
+ useEffect(() => {
+ const activeIndex = activeFilterId ? filterIds.indexOf(activeFilterId) : -1;
+ const isActiveFilterWrapping = activeIndex >= wrappingIndex;
+ // If the active filter is not wrapping, we don't need to sort the filters
+ if (!isActiveFilterWrapping || wrappingIndex === -1) {
+ setSortedFilterIds(filterIds);
+ return;
+ }
+
+ // Sort the filters with the active filter at first position
+ setSortedFilterIds(
+ filterIds.slice().sort((filterA, filterB) => {
+ // If the filter is active, it should be at the top of the list
+ if (filterA === activeFilterId && filterB !== activeFilterId) return -1;
+ if (filterA !== activeFilterId && filterB === activeFilterId) return 1;
+ // If both filters are active or not, keep their original order
+ return 0;
+ }),
+ );
+ }, [filterIds, activeFilterId, wrappingIndex]);
+
+ return sortedFilterIds;
+}
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css
new file mode 100644
index 0000000000..204e7615a4
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.module.css
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 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.
+ */
+
+.genericPlaceholder {
+ align-self: center;
+ /** It should take 2/3 of the width **/
+ width: 66%;
+ /** It should be positioned at 1/3 of the height **/
+ padding-top: 33%;
+}
+
+.title {
+ font: var(--cpd-font-body-lg-semibold);
+ text-align: center;
+}
+
+.description {
+ font: var(--cpd-font-body-sm-regular);
+ color: var(--cpd-color-text-secondary);
+ text-align: center;
+}
+
+.defaultPlaceholder {
+ margin-top: var(--cpd-space-4x);
+}
+
+.genericPlaceholder button {
+ width: 100%;
+}
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx
new file mode 100644
index 0000000000..12d26517b3
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListEmptyStateView.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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, type ReactNode } from "react";
+import { Button } from "@vector-im/compound-web";
+import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
+import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
+
+import { Flex } from "../../utils/Flex";
+import { _t } from "../../utils/i18n";
+import { useViewModel } from "../../viewmodel";
+import type { RoomListViewModel } from "./RoomListView";
+import styles from "./RoomListEmptyStateView.module.css";
+
+/**
+ * Props for RoomListEmptyStateView component
+ */
+export interface RoomListEmptyStateViewProps {
+ /** The view model containing all data and callbacks */
+ vm: RoomListViewModel;
+}
+
+/**
+ * Empty state component for the room list.
+ * Displays appropriate message and actions based on the active filter.
+ */
+export const RoomListEmptyStateView: React.FC = ({ vm }): JSX.Element => {
+ const snapshot = useViewModel(vm);
+
+ // If there is no active filter, show the default empty state
+ if (!snapshot.activeFilterId) {
+ return (
+
+
+
+ {_t("action|start_chat")}
+
+ {snapshot.canCreateRoom && (
+
+ {_t("action|new_room")}
+
+ )}
+
+
+ );
+ }
+
+ // Handle different filter cases based on filter ID
+ switch (snapshot.activeFilterId) {
+ case "favourite":
+ return (
+
+ );
+ case "people":
+ return (
+
+ );
+ case "rooms":
+ return (
+
+ );
+ case "unread":
+ return (
+ vm.onToggleFilter(snapshot.activeFilterId!)}
+ />
+ );
+ case "invites":
+ return (
+ vm.onToggleFilter(snapshot.activeFilterId!)}
+ />
+ );
+ case "mentions":
+ return (
+ vm.onToggleFilter(snapshot.activeFilterId!)}
+ />
+ );
+ case "low_priority":
+ return (
+ vm.onToggleFilter(snapshot.activeFilterId!)}
+ />
+ );
+ default:
+ return (
+
+ );
+ }
+};
+
+interface GenericPlaceholderProps {
+ /** The title of the placeholder */
+ title: string;
+ /** The description of the placeholder */
+ description?: string;
+ /** Optional children (e.g., action buttons) */
+ children?: ReactNode;
+}
+
+/**
+ * A generic placeholder for the room list
+ */
+function GenericPlaceholder({ title, description, children }: PropsWithChildren): JSX.Element {
+ return (
+
+ {title}
+ {description && {description}}
+ {children}
+
+ );
+}
+
+interface ActionPlaceholderProps {
+ /** The title to display */
+ title: string;
+ /** The action button text */
+ action: string;
+ /** Callback when the action button is clicked */
+ onAction?: () => void;
+}
+
+/**
+ * A placeholder for the room list when a filter is active
+ * The user can take action to toggle the filter
+ */
+function ActionPlaceholder({ title, action, onAction }: ActionPlaceholderProps): JSX.Element {
+ return (
+
+ {onAction && (
+
+ {action}
+
+ )}
+
+ );
+}
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css
new file mode 100644
index 0000000000..2f65f7969d
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 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.
+ */
+
+.skeleton {
+ position: relative;
+ margin-left: 4px;
+ height: 100%;
+ flex: 1;
+}
+
+.skeleton::before {
+ background-color: var(--cpd-color-bg-subtle-secondary);
+ width: 100%;
+ height: 100%;
+ content: "";
+ position: absolute;
+ mask-repeat: repeat-y;
+ mask-size: auto 96px;
+ mask-image: url("./assets/skeleton.svg");
+}
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx
new file mode 100644
index 0000000000..6ab8b80de3
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx
@@ -0,0 +1,18 @@
+/*
+ * 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 styles from "./RoomListLoadingSkeleton.module.css";
+
+/**
+ * Loading skeleton component for the room list.
+ * Displays a repeating skeleton pattern while rooms are being fetched.
+ */
+export const RoomListLoadingSkeleton: React.FC = (): JSX.Element => {
+ return ;
+};
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx
new file mode 100644
index 0000000000..206307262e
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx
@@ -0,0 +1,221 @@
+/*
+ * 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 type { Room } from "../RoomListItem/RoomListItem";
+import type { FilterId } from "../RoomListPrimaryFilters";
+import { RoomListView, type RoomListSnapshot, type RoomListViewActions } from "./RoomListView";
+import { useMockedViewModel } from "../../viewmodel";
+import {
+ renderAvatar,
+ createGetRoomItemViewModel,
+ mockRoomIds,
+ smallListRoomIds,
+ largeListRoomIds,
+} from "../story-mocks";
+
+type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement };
+
+const mockFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite"];
+
+// Wrapper component that creates a mocked ViewModel
+const RoomListViewWrapper = ({
+ onToggleFilter,
+ createChatRoom,
+ createRoom,
+ getRoomItemViewModel,
+ updateVisibleRooms,
+ renderAvatar: renderAvatarProp,
+ ...rest
+}: RoomListViewProps): JSX.Element => {
+ const vm = useMockedViewModel(rest, {
+ onToggleFilter,
+ createChatRoom,
+ createRoom,
+ getRoomItemViewModel,
+ updateVisibleRooms,
+ });
+ return ;
+};
+
+const meta = {
+ title: "Room List/RoomListView",
+ component: RoomListViewWrapper,
+ tags: ["autodocs"],
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ // Snapshot properties (state)
+ isLoadingRooms: false,
+ isRoomListEmpty: false,
+ filterIds: mockFilterIds,
+ activeFilterId: undefined,
+ roomListState: {
+ activeRoomIndex: undefined,
+ spaceId: "!space:server",
+ filterKeys: undefined,
+ },
+ roomIds: mockRoomIds,
+ canCreateRoom: true,
+ // Action properties (callbacks)
+ onToggleFilter: fn(),
+ createChatRoom: fn(),
+ createRoom: fn(),
+ getRoomItemViewModel: createGetRoomItemViewModel(mockRoomIds),
+ updateVisibleRooms: fn(),
+ renderAvatar,
+ },
+ parameters: {
+ design: {
+ type: "figma",
+ url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19126",
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Loading: Story = {
+ args: {
+ isLoadingRooms: true,
+ },
+};
+
+export const Empty: Story = {
+ args: {
+ isRoomListEmpty: true,
+ },
+};
+
+export const EmptyWithoutCreatePermission: Story = {
+ args: {
+ isRoomListEmpty: true,
+ canCreateRoom: false,
+ },
+};
+
+export const WithActiveFilter: Story = {
+ args: {
+ filterIds: ["unread", "people", "rooms", "favourite"],
+ activeFilterId: "favourite",
+ roomListState: {
+ activeRoomIndex: undefined,
+ spaceId: "!space:server",
+ filterKeys: ["favourites"],
+ },
+ },
+};
+
+export const WithSelection: Story = {
+ args: {
+ roomListState: {
+ activeRoomIndex: 0,
+ spaceId: "!space:server",
+ filterKeys: undefined,
+ },
+ },
+};
+
+export const EmptyFavouriteFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["favourite", "people"],
+ activeFilterId: "favourite",
+ },
+};
+
+export const EmptyPeopleFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["people", "rooms"],
+ activeFilterId: "people",
+ },
+};
+
+export const EmptyRoomsFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["rooms", "people"],
+ activeFilterId: "rooms",
+ },
+};
+
+export const EmptyUnreadFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["unread", "people"],
+ activeFilterId: "unread",
+ },
+};
+
+export const EmptyInvitesFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["invites", "people"],
+ activeFilterId: "invites",
+ },
+};
+
+export const EmptyMentionsFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["mentions", "people"],
+ activeFilterId: "mentions",
+ },
+};
+
+export const EmptyLowPriorityFilter: Story = {
+ args: {
+ isRoomListEmpty: true,
+ roomIds: [],
+ filterIds: ["low_priority", "people"],
+ activeFilterId: "low_priority",
+ },
+};
+
+export const SmallList: Story = {
+ args: {
+ roomIds: smallListRoomIds,
+ getRoomItemViewModel: createGetRoomItemViewModel(smallListRoomIds),
+ },
+};
+
+export const LargeList: Story = {
+ args: {
+ roomIds: largeListRoomIds,
+ getRoomItemViewModel: createGetRoomItemViewModel(largeListRoomIds),
+ },
+};
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx
new file mode 100644
index 0000000000..15237eed7e
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx
@@ -0,0 +1,177 @@
+/*
+ * 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 { VirtuosoMockContext } from "react-virtuoso";
+import { composeStories } from "@storybook/react-vite";
+import { describe, it, expect } from "vitest";
+
+import * as stories from "./RoomListView.stories";
+
+const {
+ Default,
+ Loading,
+ Empty,
+ EmptyWithoutCreatePermission,
+ WithActiveFilter,
+ SmallList,
+ LargeList,
+ EmptyFavouriteFilter,
+ EmptyPeopleFilter,
+ EmptyRoomsFilter,
+ EmptyUnreadFilter,
+ EmptyInvitesFilter,
+ EmptyMentionsFilter,
+ EmptyLowPriorityFilter,
+} = composeStories(stories);
+
+const renderWithMockContext = (component: React.ReactElement): ReturnType => {
+ return render(component, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+};
+
+describe("", () => {
+ it("renders Default story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders Loading story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders Empty story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyWithoutCreatePermission story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders WithActiveFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders SmallList story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders LargeList story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyFavouriteFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyPeopleFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyRoomsFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyUnreadFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyInvitesFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyMentionsFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders EmptyLowPriorityFilter story", () => {
+ const { container } = renderWithMockContext();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("should call onToggleFilter when filter is clicked", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("option", { name: "People" }));
+
+ expect(Default.args.onToggleFilter).toHaveBeenCalled();
+ });
+
+ it("should call createRoom when New room button is clicked", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "New room" }));
+
+ expect(Empty.args.createRoom).toHaveBeenCalled();
+ });
+
+ it("should call createChatRoom when Start chat button is clicked", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "Start chat" }));
+
+ expect(Empty.args.createChatRoom).toHaveBeenCalled();
+ });
+
+ it("should call onToggleFilter when Show all chats is clicked in unread empty state", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "Show all chats" }));
+
+ expect(EmptyUnreadFilter.args.onToggleFilter).toHaveBeenCalled();
+ });
+
+ it("should call onToggleFilter when See all activity is clicked in invites empty state", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "See all activity" }));
+
+ expect(EmptyInvitesFilter.args.onToggleFilter).toHaveBeenCalled();
+ });
+
+ it("should call onToggleFilter when See all activity is clicked in mentions empty state", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "See all activity" }));
+
+ expect(EmptyMentionsFilter.args.onToggleFilter).toHaveBeenCalled();
+ });
+
+ it("should call onToggleFilter when See all activity is clicked in low priority empty state", async () => {
+ const user = userEvent.setup();
+ renderWithMockContext();
+
+ await user.click(screen.getByRole("button", { name: "See all activity" }));
+
+ expect(EmptyLowPriorityFilter.args.onToggleFilter).toHaveBeenCalled();
+ });
+});
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx
new file mode 100644
index 0000000000..491c28d7d1
--- /dev/null
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 ReactNode } from "react";
+
+import { useViewModel, type ViewModel } from "../../viewmodel";
+import { RoomListPrimaryFilters, type FilterId } from "../RoomListPrimaryFilters";
+import { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton";
+import { RoomListEmptyStateView } from "./RoomListEmptyStateView";
+import { VirtualizedRoomListView, type RoomListViewState } from "../VirtualizedRoomListView";
+import { type Room } from "../RoomListItem";
+
+/**
+ * Snapshot for the room list view
+ */
+export type RoomListSnapshot = {
+ /** Whether the rooms are currently loading */
+ isLoadingRooms: boolean;
+ /** Whether the room list is empty */
+ isRoomListEmpty: boolean;
+ /** Array of filter IDs */
+ filterIds: FilterId[];
+ /** Currently active filter ID (if any) */
+ activeFilterId?: FilterId;
+ /** Room list state */
+ roomListState: RoomListViewState;
+ /** Array of room IDs for virtualization */
+ roomIds: string[];
+ /** Optional description for the empty state */
+ emptyStateDescription?: string;
+ /** Optional action element for the empty state */
+ emptyStateAction?: ReactNode;
+ /** Whether the user can create rooms */
+ canCreateRoom?: boolean;
+};
+
+/**
+ * Actions interface for room list operations
+ */
+export interface RoomListViewActions {
+ /** Called when a filter is toggled */
+ onToggleFilter: (filterId: FilterId) => void;
+ /** Called to create a new chat room */
+ createChatRoom: () => void;
+ /** Called to create a new room */
+ createRoom: () => void;
+ /** Get view model for a specific room (virtualization API) */
+ getRoomItemViewModel: (roomId: string) => any;
+ /** Called when the visible range changes (virtualization API) */
+ updateVisibleRooms: (startIndex: number, endIndex: number) => void;
+}
+
+/**
+ * The view model type for the room list view
+ */
+export type RoomListViewModel = ViewModel & RoomListViewActions;
+
+/**
+ * Props for RoomListView component
+ */
+export interface RoomListViewProps {
+ /** The view model containing all data and callbacks */
+ vm: RoomListViewModel;
+ /** Render function for room avatar */
+ renderAvatar: (room: Room) => ReactNode;
+ /** Optional callback for keyboard events on the room list */
+ onKeyDown?: (e: React.KeyboardEvent) => void;
+}
+
+/**
+ * Room list view component that manages filters, loading states, empty states, and the room list.
+ */
+export const RoomListView: React.FC = ({ vm, renderAvatar, onKeyDown }): JSX.Element => {
+ const snapshot = useViewModel(vm);
+ let listBody: ReactNode;
+
+ if (snapshot.isLoadingRooms) {
+ listBody = ;
+ } else if (snapshot.isRoomListEmpty) {
+ listBody = ;
+ } else {
+ listBody = ;
+ }
+
+ return (
+ <>
+
- );
-}
-
-interface NotificationMenuProps {
- /**
- * The view model state for the menu.
- */
- vm: RoomListItemMenuViewState;
-}
-
-function NotificationMenu({ vm }: NotificationMenuProps): JSX.Element {
- const [open, setOpen] = useState(false);
- const checkComponent = ;
-
- return (
-
- );
-}
diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx
deleted file mode 100644
index d87da9c034..0000000000
--- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx
+++ /dev/null
@@ -1,124 +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, memo, useEffect, useRef } from "react";
-import { type Room } from "matrix-js-sdk/src/matrix";
-import classNames from "classnames";
-import { Flex } from "@element-hq/web-shared-components";
-
-import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
-import { RoomListItemMenuView } from "./RoomListItemMenuView";
-import { NotificationDecoration } from "../NotificationDecoration";
-import { RoomAvatarView } from "../../avatars/RoomAvatarView";
-import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView";
-
-interface RoomListItemViewProps extends Omit, "onFocus"> {
- /**
- * 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: (room: Room, e: React.FocusEvent) => void;
- /**
- * The index of the room in the list
- */
- roomIndex: number;
- /**
- * The total number of rooms in the list
- */
- roomCount: number;
-}
-
-/**
- * An item in the room list
- */
-export const RoomListItemView = memo(function RoomListItemView({
- room,
- isSelected,
- isFocused,
- onFocus,
- roomIndex: index,
- roomCount: count,
- ...props
-}: RoomListItemViewProps): JSX.Element {
- const ref = useRef(null);
- const vm = useRoomListItemViewModel(room);
-
- useEffect(() => {
- if (isFocused) {
- ref.current?.focus({ preventScroll: true, focusVisible: true });
- }
- }, [isFocused]);
-
- const content = (
- vm.openRoom()}
- onFocus={(e: React.FocusEvent) => onFocus(room, e)}
- tabIndex={isFocused ? 0 : -1}
- {...props}
- >
-
-
- {/* We truncate the room name when too long. Title here is to show the full name on hover */}
-
-
- {vm.name}
-
- {vm.messagePreview && (
-
- {vm.messagePreview}
-
- )}
-
- {vm.showHoverMenu && }
-
- {/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
- {vm.showNotificationDecoration && (
-
- )}
-
-
- );
-
- if (!vm.showContextMenu) return content;
- return {content};
-});
diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
deleted file mode 100644
index 44f19a86da..0000000000
--- a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
+++ /dev/null
@@ -1,169 +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, useEffect, useId, useRef, useState, type RefObject } from "react";
-import { ChatFilter, IconButton } from "@vector-im/compound-web";
-import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
-import { Flex } from "@element-hq/web-shared-components";
-
-import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
-import { _t } from "../../../../languageHandler";
-
-interface RoomListPrimaryFiltersProps {
- /**
- * The view model for the room list
- */
- vm: RoomListViewState;
-}
-
-/**
- * The primary filters for the room list
- */
-export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element {
- const id = useId();
- const [isExpanded, setIsExpanded] = useState(false);
-
- const { ref, isWrapping: displayChevron, wrappingIndex } = useCollapseFilters(isExpanded);
- const filters = useVisibleFilters(vm.primaryFilters, wrappingIndex);
-
- return (
-
- {displayChevron && (
- setIsExpanded((_expanded) => !_expanded)}
- >
-
-
- )}
-
- {filters.map((filter, i) => (
- filter.toggle()}>
- {filter.name}
-
- ))}
-
-
- );
-}
-
-/**
- * A hook to manage the wrapping of filters in the room list.
- * It observes the filter list and hides filters that are wrapping when the list is not expanded.
- * @param isExpanded
- * @returns an object containing:
- * - `ref`: a ref to put on the filter list element
- * - `isWrapping`: a boolean indicating if the filters are wrapping
- * - `wrappingIndex`: the index of the first filter that is wrapping
- */
-function useCollapseFilters(
- isExpanded: boolean,
-): { ref: RefObject; isWrapping: boolean; wrappingIndex: number } {
- const ref = useRef(null);
- const [isWrapping, setIsWrapping] = useState(false);
- const [wrappingIndex, setWrappingIndex] = useState(-1);
-
- useEffect(() => {
- if (!ref.current) return;
-
- const hideFilters = (list: Element): void => {
- let isWrapping = false;
- Array.from(list.children).forEach((node, i): void => {
- const child = node as HTMLElement;
- const wrappingClass = "mx_RoomListPrimaryFilters_wrapping";
- child.setAttribute("aria-hidden", "false");
- child.classList.remove(wrappingClass);
-
- // If the filter list is expanded, all filters are visible
- if (isExpanded) return;
-
- // If the previous element is on the left element of the current one, it means that the filter is wrapping
- const previousSibling = child.previousElementSibling as HTMLElement | null;
- if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) {
- if (!isWrapping) setWrappingIndex(i);
- isWrapping = true;
- }
-
- // If the filter is wrapping, we hide it
- child.classList.toggle(wrappingClass, isWrapping);
- child.setAttribute("aria-hidden", isWrapping.toString());
- });
-
- if (!isWrapping) setWrappingIndex(-1);
- setIsWrapping(isExpanded || isWrapping);
- };
-
- hideFilters(ref.current);
- const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target)));
-
- observer.observe(ref.current);
- return () => {
- observer.disconnect();
- };
- }, [isExpanded]);
-
- return { ref, isWrapping, wrappingIndex };
-}
-
-/**
- * A hook to sort the filters by active state.
- * The list is sorted if the current filter index is greater than or equal to the wrapping index.
- * If the wrapping index is -1, the filters are not sorted.
- *
- * @param filters - the list of filters to sort.
- * @param wrappingIndex - the index of the first filter that is wrapping.
- */
-export function useVisibleFilters(
- filters: RoomListViewState["primaryFilters"],
- wrappingIndex: number,
-): RoomListViewState["primaryFilters"] {
- // By default, the filters are not sorted
- const [sortedFilters, setSortedFilters] = useState(filters);
-
- useEffect(() => {
- const isActiveFilterWrapping = filters.findIndex((f) => f.active) >= wrappingIndex;
- // If the active filter is not wrapping, we don't need to sort the filters
- if (!isActiveFilterWrapping || wrappingIndex === -1) {
- setSortedFilters(filters);
- return;
- }
-
- // Sort the filters with the current filter at first position
- setSortedFilters(
- filters.slice().sort((filterA, filterB) => {
- // If the filter is active, it should be at the top of the list
- if (filterA.active && !filterB.active) return -1;
- if (!filterA.active && filterB.active) return 1;
- // If both filters are active or not, keep their original order
- return 0;
- }),
- );
- }, [filters, wrappingIndex]);
-
- return sortedFilters;
-}
diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx
index b29affc0be..50dd83e505 100644
--- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx
+++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx
@@ -5,33 +5,47 @@
* Please see LICENSE files in the repository root for full details.
*/
-import React, { type JSX } from "react";
+import React, { useCallback, type JSX, type ReactNode } from "react";
+import {
+ RoomListView as SharedRoomListView,
+ useCreateAutoDisposedViewModel,
+ type Room as SharedRoom,
+} from "@element-hq/web-shared-components";
+import { type Room } from "matrix-js-sdk/src/matrix";
-import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";
-import { RoomList } from "./RoomList";
-import { EmptyRoomList } from "./EmptyRoomList";
-import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
+import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
+import { RoomAvatarView } from "../../avatars/RoomAvatarView";
+import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
+import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
+import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
+import { RoomListViewViewModel } from "../../../../viewmodels/room-list/RoomListViewViewModel";
/**
- * Host the room list and the (future) room filters
+ * RoomListView component using shared components with proper MVVM pattern.
*/
export function RoomListView(): JSX.Element {
- const vm = useRoomListViewModel();
- const isRoomListEmpty = vm.roomsResult.rooms.length === 0;
- let listBody;
- if (vm.isLoadingRooms) {
- listBody = ;
- } else if (isRoomListEmpty) {
- listBody = ;
- } else {
- listBody = ;
- }
- return (
- <>
-
-
-
- {listBody}
- >
- );
+ const matrixClient = useMatrixClientContext();
+
+ // Create and auto-dispose ViewModel instance
+ const vm = useCreateAutoDisposedViewModel(() => new RoomListViewViewModel({ client: matrixClient }));
+
+ // Render avatar for each room - memoized to prevent re-renders
+ const renderAvatar = useCallback((room: SharedRoom): ReactNode => {
+ return ;
+ }, []);
+
+ // Handle keyboard navigation for landmarks
+ const onKeyDown = useCallback((ev: React.KeyboardEvent) => {
+ const navAction = getKeyBindingsManager().getNavigationAction(ev);
+ if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) {
+ LandmarkNavigation.findAndFocusNextLandmark(
+ Landmark.ROOM_LIST,
+ navAction === KeyBindingAction.PreviousLandmark,
+ );
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ }, []);
+
+ return ;
}
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 4301446794..82358af880 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -12,16 +12,6 @@
"one": "Nepřečtená zmínka."
},
"recent_rooms": "Nedávné místnosti",
- "room_messsage_not_sent": "Otevřít místnost %(roomName)s s nenastavenou zprávou.",
- "room_n_unread_invite": "Otevřít pozvánku do místnosti %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Otevřít místnost %(roomName)s s 1 nepřečtenou zprávou.",
- "other": "Otevřít místnost %(roomName)s s %(count)s nepřečtenými zprávami."
- },
- "room_n_unread_messages_mentions": {
- "one": "Otevřít místnost %(roomName)s s 1 nepřečtenou zmínkou.",
- "other": "Otevřít místnost %(roomName)s s %(count)s nepřečtenými zprávami včetně zmínek."
- },
"room_name": "Místnost %(name)s",
"room_status_bar": "Stavový řádek místnosti",
"seek_bar_label": "Panel posunu zvuku",
@@ -1718,7 +1708,6 @@
"class_global": "Globální",
"class_other": "Další možnosti",
"default": "Výchozí",
- "default_settings": "Shoda výchozího nastavení",
"email_pusher_app_display_name": "E-mailová oznámení",
"enable_prompt_toast_description": "Povolit oznámení na ploše",
"enable_prompt_toast_title": "Oznámení",
@@ -1737,8 +1726,7 @@
"mentions_and_keywords_description": "Dostávat oznámení pouze o zmínkách a klíčových slovech podle nastavení",
"mentions_keywords": "Zmínky a klíčová slova",
"message_didnt_send": "Zpráva se neodeslala. Klikněte pro informace.",
- "mute_description": "Nebudete dostávat žádná oznámení",
- "mute_room": "Ztlumit místnost"
+ "mute_description": "Nebudete dostávat žádná oznámení"
},
"notifier": {
"m.key.verification.request": "%(name)s žádá o ověření"
@@ -2160,37 +2148,9 @@
"add_space_label": "Přidat prostor",
"breadcrumbs_empty": "Žádné nedávno navštívené místnosti",
"breadcrumbs_label": "Nedávno navštívené místnosti",
- "collapse_filters": "Sbalit seznam filtrů",
- "empty": {
- "no_chats": "Zatím žádné chaty",
- "no_chats_description": "Začněte tím, že někomu pošlete zprávu nebo vytvoříte místnost",
- "no_chats_description_no_room_rights": "Začněte tím, že někomu pošlete zprávu",
- "no_favourites": "Zatím nemáte oblíbený chat",
- "no_favourites_description": "Chat si můžete přidat do oblíbených v nastavení chatu",
- "no_invites": "Nemáte žádné nepřečtené pozvánky",
- "no_lowpriority": "Nemáte žádné místnosti s nízkou prioritou",
- "no_mentions": "Nemáte žádné nepřečtené zmínky",
- "no_people": "Zatím s nikým nemáte přímé chaty",
- "no_people_description": "Můžete zrušit výběr filtrů, abyste viděli ostatní chaty",
- "no_rooms": "Ještě nejste v žádné místnosti",
- "no_rooms_description": "Můžete zrušit výběr filtrů, abyste viděli své další chaty",
- "no_unread": "Gratulujeme! Nemáte žádné nepřečtené zprávy",
- "show_activity": "Zobrazit veškerou aktivitu",
- "show_chats": "Zobrazit všechny chaty"
- },
- "expand_filters": "Rozbalit seznam filtrů",
"failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti",
"failed_remove_tag": "Nepodařilo se odstranit štítek %(tagName)s z místnosti",
"failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy",
- "filters": {
- "favourite": "Oblíbené",
- "invites": "Pozvánky",
- "low_priority": "Nízká priorita",
- "mentions": "Zmínky",
- "people": "Lidé",
- "rooms": "Místnosti",
- "unread": "Nepřečtené"
- },
"home_menu_label": "Možnosti domovské obrazovky",
"join_public_room_label": "Připojit se k veřejné místnosti",
"joining_rooms_status": {
@@ -2199,23 +2159,13 @@
},
"list_title": "Seznam místností",
"more_options": {
- "copy_link": "Kopírovat odkaz na místnost",
- "favourited": "Oblíbené",
- "leave_room": "Opustit místnost",
- "low_priority": "Nízká priorita",
- "mark_read": "Označit jako přečtené",
- "mark_unread": "Označit jako nepřečtené"
+ "leave_room": "Opustit místnost"
},
"notification_options": "Možnosti oznámení",
- "primary_filters": "Filtry seznamu místností",
"redacting_messages_status": {
"one": "Momentálně se odstraňují zprávy v %(count)s místnosti",
"other": "Momentálně se odstraňují zprávy v %(count)s místnostech"
},
- "room": {
- "more_options": "Více možností",
- "open_room": "Otevřít místnost %(roomName)s"
- },
"show_less": "Zobrazit méně",
"show_n_more": {
"other": "Zobrazit %(count)s dalších",
diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json
index 8a7d596990..b4e9978f47 100644
--- a/src/i18n/strings/cy.json
+++ b/src/i18n/strings/cy.json
@@ -14,18 +14,6 @@
"%(count)s crybwylliad heb eu darllen": "other"
},
"recent_rooms": "Ystafelloedd diweddar",
- "room_messsage_not_sent": "Agor ystafell %(roomName)s gyda neges heb ei gosod.",
- "room_n_unread_invite": "Agor gwahoddiad i ystafell %(roomName)s.",
- "room_n_unread_messages": {
- "Ystafell agored%(roomName)s gyda %(count)s negeseuon heb eu darllen.": "zero",
- "Ystafell agored %(roomName)s gydag 1 neges heb ei darllen.": "one",
- "Ystafell agored%(roomName)s gyda %(count)s neges heb eu darllen.": "other"
- },
- "room_n_unread_messages_mentions": {
- "Ystafell agored %(roomName)s gyda %(count)s negeseuon heb eu darllen gan gynnwys crybwylliadau.": "zero",
- "Ystafell agored %(roomName)s gydag 1 crybwylliad heb ei ddarllen.": "one",
- "Ystafell agored %(roomName)s gyda %(count)s neges heb eu darllen gan gynnwys crybwylliadau.": "other"
- },
"room_name": "Ystafell %(matere)s",
"room_status_bar": "Bar statws ystafell",
"seek_bar_label": "Bar chwilio sain",
@@ -1730,7 +1718,6 @@
"class_global": "Eang",
"class_other": "Arall",
"default": "Rhagosodedig",
- "default_settings": "Cydweddu'r gosodiadau rhagosodedig",
"email_pusher_app_display_name": "Hysbysiadau E-bost",
"enable_prompt_toast_description": "Galluogi hysbysiadau bwrdd gwaith",
"enable_prompt_toast_title": "Hysbysiadau",
@@ -1749,8 +1736,7 @@
"mentions_and_keywords_description": "Dim ond gyda chyfeiriadau ac allweddeiriau fel y'u gosodwyd yn eich gosodiadau y cewch eich hysbysu",
"mentions_keywords": "Crybwylliadau ac allweddeiriau",
"message_didnt_send": "Heb anfon y neges. Cliciwch am wybodaeth.",
- "mute_description": "Fyddwch chi ddim yn cael unrhyw hysbysiadau",
- "mute_room": "Tewi'r ystafell"
+ "mute_description": "Fyddwch chi ddim yn cael unrhyw hysbysiadau"
},
"notifier": {
"m.key.verification.request": "Mae %(matere)s yn gofyn am ddilysiad"
@@ -2163,37 +2149,9 @@
"add_space_label": "Ychwanegu gofod",
"breadcrumbs_empty": "Dim ystafelloedd yr ymwelwyd â nhw yn ddiweddar",
"breadcrumbs_label": "Ymwelwyd ag ystafelloedd yn ddiweddar",
- "collapse_filters": "Cwympo rhestr hidlo",
- "empty": {
- "no_chats": "Dim sgyrsiau eto",
- "no_chats_description": "Dechreuwch drwy anfon neges at rywun neu drwy greu ystafell",
- "no_chats_description_no_room_rights": "Dechreuwch trwy anfon neges at rywun",
- "no_favourites": "Nid oes gennych hoff sgwrs eto",
- "no_favourites_description": "Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio",
- "no_invites": "Does gennych chi ddim gwahoddiadau heb eu darllen",
- "no_lowpriority": "Nid oes gennych unrhyw ystafelloedd blaenoriaeth isel",
- "no_mentions": "Does gennych chi ddim crybwylliadau heb eu darllen",
- "no_people": "Nid oes gennych chi sgyrsiau uniongyrchol gydag unrhyw un eto",
- "no_people_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill",
- "no_rooms": "Nid ydych mewn unrhyw ystafell eto",
- "no_rooms_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill",
- "no_unread": "Llongyfarchiadau! Nid oes gennych unrhyw negeseuon heb eu darllen",
- "show_activity": "Gweld yr holl weithgarwch",
- "show_chats": "Dangos pob sgwrs"
- },
- "expand_filters": "Ehangu rhestr hidlo",
"failed_add_tag": "Wedi methu ag ychwanegu tag %(tagName)s i'r ystafell",
"failed_remove_tag": "Wedi methu â thynnu'r tag %(tagName)s o'r ystafell",
"failed_set_dm_tag": "Wedi methu gosod tag neges uniongyrchol",
- "filters": {
- "favourite": "Ffefrynnau",
- "invites": "Gwahoddiadau",
- "low_priority": "Blaenoriaeth isel",
- "mentions": "Crybwylliadau",
- "people": "Pobl",
- "rooms": "Ystafelloedd",
- "unread": "Heb eu darllen"
- },
"home_menu_label": "Dewisiadau cartref",
"join_public_room_label": "Ymuno â'r ystafell gyhoeddus",
"joining_rooms_status": {
@@ -2201,22 +2159,12 @@
},
"list_title": "Rhestr ystafelloedd",
"more_options": {
- "copy_link": "Copïo dolen ystafell",
- "favourited": "Ffafrio",
- "leave_room": "Gadael yr ystafell",
- "low_priority": "Blaenoriaeth isel",
- "mark_read": "Marcio fel wedi'i ddarllen",
- "mark_unread": "Marcio fel heb ei ddarllen"
+ "leave_room": "Gadael yr ystafell"
},
"notification_options": "Dewisiadau hysbysu",
- "primary_filters": "Hidlau rhestr ystafelloedd",
"redacting_messages_status": {
"Yn tynnu negeseuon mewn %(count)s ystafell": "other"
},
- "room": {
- "more_options": "Rhagor o Ddewisiadau",
- "open_room": "Agor ystafell %(roomName)s"
- },
"show_less": "Dangos llai",
"show_n_more": {
"Dangos %(count)s yn rhagor": "other"
diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json
index e3e8ef4d07..8385d2cd51 100644
--- a/src/i18n/strings/da.json
+++ b/src/i18n/strings/da.json
@@ -12,8 +12,6 @@
"other": "%(count)s ulæste beskeder, herunder omtaler."
},
"recent_rooms": "Nylige rum",
- "room_messsage_not_sent": "Åbent rum %(roomName)s med en usendt besked.",
- "room_n_unread_invite": "Invitation til det åbne rum %(roomName)s.",
"room_name": "Rum %(name)s",
"room_status_bar": "Statusbjælke for rum",
"seek_bar_label": "Progressionsmarkør for lydafspiller",
@@ -1844,15 +1842,9 @@
"add_space_label": "Tilføj gruppe",
"breadcrumbs_empty": "Ingen nyligt besøgte rum",
"breadcrumbs_label": "Nyligt besøgte rum",
- "empty": {
- "no_rooms": "Du er ikke i noget rum endnu"
- },
"failed_add_tag": "Kunne ikke tilføje tag(s): %(tagName)s til gruppen",
"failed_remove_tag": "Kunne ikke fjerne tag(s): %(tagName)s fra gruppen",
"failed_set_dm_tag": "Kunne ikke indstille tagget til direkte beskeder",
- "filters": {
- "people": "Brugere"
- },
"home_menu_label": "Hjemmeindstillinger",
"join_public_room_label": "Deltag i offentligt rum",
"joining_rooms_status": {
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 326da77589..fbbe9f14de 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -12,16 +12,6 @@
"one": "1 ungelesene Erwähnung."
},
"recent_rooms": "Zuletzt besuchte Chats",
- "room_messsage_not_sent": "Öffne den Chat „%(roomName)s” mit einer ungesendeten Nachricht.",
- "room_n_unread_invite": "Offene Einladung zum Chat %(roomName)s",
- "room_n_unread_messages": {
- "one": "Chat %(roomName)s öffnen mit 1 ungelesenen Nachricht.",
- "other": "Chat %(roomName)s öffnen mit %(count)s ungelesene Nachrichten."
- },
- "room_n_unread_messages_mentions": {
- "one": "Öffne den den Chat %(roomName)s mit 1 ungelesenen Erwähnung.",
- "other": "Öffne den Chat %(roomName)s mit %(count)s ungelesenen Nachrichten einschließlich Erwähnungen."
- },
"room_name": "Chat %(name)s",
"room_status_bar": "Chat-Statusleiste",
"seek_bar_label": "Audio-Suchleiste",
@@ -1717,7 +1707,6 @@
"class_global": "Global",
"class_other": "Sonstiges",
"default": "Standard",
- "default_settings": "Standardeinstellungen verwenden",
"email_pusher_app_display_name": "E-Mail-Benachrichtigungen",
"enable_prompt_toast_description": "Aktiviere Desktopbenachrichtigungen",
"enable_prompt_toast_title": "Benachrichtigungen",
@@ -1736,8 +1725,7 @@
"mentions_and_keywords_description": "Nur bei Erwähnungen und Schlüsselwörtern benachrichtigen, die du in den Einstellungen konfigurieren kannst",
"mentions_keywords": "Erwähnungen und Schlüsselwörter",
"message_didnt_send": "Nachricht nicht gesendet. Klicke für Details.",
- "mute_description": "Du wirst keine Benachrichtigungen erhalten",
- "mute_room": "Chat stummschalten"
+ "mute_description": "Du wirst keine Benachrichtigungen erhalten"
},
"notifier": {
"m.key.verification.request": "%(name)s fordert eine Verifizierung an"
@@ -2153,37 +2141,9 @@
"add_space_label": "Space hinzufügen",
"breadcrumbs_empty": "Keine kürzlich besuchten Chats",
"breadcrumbs_label": "Kürzlich besuchte Chats",
- "collapse_filters": "Filterliste einklappen",
- "empty": {
- "no_chats": "Noch keine Chats",
- "no_chats_description": "Leg los, indem du jemandem eine Nachricht schickst oder einen Chat erstellst",
- "no_chats_description_no_room_rights": "Leg los, indem du jemandem eine Nachricht schickst",
- "no_favourites": "Du hast noch keine Chats als Favorit markiert",
- "no_favourites_description": "In den Chat Einstellungen kannst du einen Chat als Favorit markieren",
- "no_invites": "Du hast keine ungelesenen Einladungen",
- "no_lowpriority": "Du hast keine Chats mit niedriger Priorität.",
- "no_mentions": "Du hast keine ungelesenen Erwähnungen",
- "no_people": "Du hast noch keine Direktnachrichten",
- "no_people_description": "Wähle Filter ab, um Chats zu sehen.",
- "no_rooms": "Du bist noch in keinem Chat",
- "no_rooms_description": "Wähle Filter ab, um Chats zu sehen.",
- "no_unread": "Glückwunsch! Du hast keine ungelesenen Nachrichten.",
- "show_activity": "Alle Aktivitäten ansehen",
- "show_chats": "Alle Chats anzeigen"
- },
- "expand_filters": "Filterliste ausklappen",
"failed_add_tag": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an den Chat",
"failed_remove_tag": "Entfernen des Chat-Tags %(tagName)s fehlgeschlagen",
"failed_set_dm_tag": "Fehler beim Setzen der Nachrichtenmarkierung",
- "filters": {
- "favourite": "Favoriten",
- "invites": "Einladungen",
- "low_priority": "Niedrige Priorität",
- "mentions": "Erwähnungen",
- "people": "Personen",
- "rooms": "Gruppen",
- "unread": "Ungelesen"
- },
"home_menu_label": "Startseiteneinstellungen",
"join_public_room_label": "Öffentlichem Chat beitreten",
"joining_rooms_status": {
@@ -2192,23 +2152,13 @@
},
"list_title": "Chatliste",
"more_options": {
- "copy_link": "Chatlink kopieren",
- "favourited": "Favorisiert",
- "leave_room": "Chat verlassen",
- "low_priority": "Niedrige Priorität",
- "mark_read": "Als gelesen markieren",
- "mark_unread": "Als ungelesen markieren"
+ "leave_room": "Chat verlassen"
},
"notification_options": "Benachrichtigungsoptionen",
- "primary_filters": "Filter für die Chatliste",
"redacting_messages_status": {
"one": "Entferne Nachrichten in %(count)s Chat",
"other": "Entferne Nachrichten in %(count)s Chats"
},
- "room": {
- "more_options": "Weitere Optionen",
- "open_room": "Öffne Chat %(roomName)s"
- },
"show_less": "Weniger anzeigen",
"show_n_more": {
"other": "%(count)s weitere anzeigen",
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4f6fe35959..b498e20d72 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -12,16 +12,6 @@
"other": "%(count)s unread messages including mentions."
},
"recent_rooms": "Recent rooms",
- "room_messsage_not_sent": "Open room %(roomName)s with an unsent message.",
- "room_n_unread_invite": "Open room %(roomName)s invitation.",
- "room_n_unread_messages": {
- "one": "Open room %(roomName)s with 1 unread message.",
- "other": "Open room %(roomName)s with %(count)s unread messages."
- },
- "room_n_unread_messages_mentions": {
- "one": "Open room %(roomName)s with 1 unread mention.",
- "other": "Open room %(roomName)s with %(count)s unread messages including mentions."
- },
"room_name": "Room %(name)s",
"room_status_bar": "Room status bar",
"seek_bar_label": "Audio seek bar",
@@ -1717,7 +1707,6 @@
"class_global": "Global",
"class_other": "Other",
"default": "Default",
- "default_settings": "Match default settings",
"email_pusher_app_display_name": "Email Notifications",
"enable_prompt_toast_description": "Enable desktop notifications",
"enable_prompt_toast_title": "Notifications",
@@ -1736,8 +1725,7 @@
"mentions_and_keywords_description": "Get notified only with mentions and keywords as set up in your settings",
"mentions_keywords": "Mentions and keywords",
"message_didnt_send": "Message didn't send. Click for info.",
- "mute_description": "You won't get any notifications",
- "mute_room": "Mute room"
+ "mute_description": "You won't get any notifications"
},
"notifier": {
"m.key.verification.request": "%(name)s is requesting verification"
@@ -2154,37 +2142,9 @@
"add_space_label": "Add space",
"breadcrumbs_empty": "No recently visited rooms",
"breadcrumbs_label": "Recently visited rooms",
- "collapse_filters": "Collapse filter list",
- "empty": {
- "no_chats": "No chats yet",
- "no_chats_description": "Get started by messaging someone or by creating a room",
- "no_chats_description_no_room_rights": "Get started by messaging someone",
- "no_favourites": "You don't have favourite chats yet",
- "no_favourites_description": "You can add a chat to your favourites in the chat settings",
- "no_invites": "You don't have any unread invites",
- "no_lowpriority": "You don't have any low priority rooms",
- "no_mentions": "You don't have any unread mentions",
- "no_people": "You don’t have direct chats with anyone yet",
- "no_people_description": "You can deselect filters in order to see your other chats",
- "no_rooms": "You’re not in any room yet",
- "no_rooms_description": "You can deselect filters in order to see your other chats",
- "no_unread": "Congrats! You don’t have any unread messages",
- "show_activity": "See all activity",
- "show_chats": "Show all chats"
- },
- "expand_filters": "Expand filter list",
"failed_add_tag": "Failed to add tag %(tagName)s to room",
"failed_remove_tag": "Failed to remove tag %(tagName)s from room",
"failed_set_dm_tag": "Failed to set direct message tag",
- "filters": {
- "favourite": "Favourites",
- "invites": "Invites",
- "low_priority": "Low priority",
- "mentions": "Mentions",
- "people": "People",
- "rooms": "Rooms",
- "unread": "Unreads"
- },
"home_menu_label": "Home options",
"join_public_room_label": "Join public room",
"joining_rooms_status": {
@@ -2193,23 +2153,13 @@
},
"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"
+ "leave_room": "Leave room"
},
"notification_options": "Notification options",
- "primary_filters": "Room list filters",
"redacting_messages_status": {
"one": "Currently removing messages in %(count)s room",
"other": "Currently removing messages in %(count)s rooms"
},
- "room": {
- "more_options": "More Options",
- "open_room": "Open room %(roomName)s"
- },
"show_less": "Show less",
"show_n_more": {
"one": "Show %(count)s more",
diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index c62d9adcfb..1c800dda1c 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -12,8 +12,6 @@
"one": "1 mención sin leer."
},
"recent_rooms": "Salas recientes",
- "room_messsage_not_sent": "Abrir sala %(roomName)s con un mensaje no enviado.",
- "room_n_unread_invite": "Abrir invitación de sala %(roomName)s.",
"room_name": "Sala %(name)s",
"room_status_bar": "Barra de estado de la sala",
"seek_bar_label": "Barra de búsqueda de audio",
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 0cd207acbc..84916f2787 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -12,16 +12,6 @@
"other": "%(count)s lugemata sõnumit kaasa arvatud mainimised."
},
"recent_rooms": "Hiljuti kasutatud jututoad",
- "room_messsage_not_sent": "Ava „%(roomName)s“ jututuba saatmata sõnumiga.",
- "room_n_unread_invite": "Ava %(roomName)s jututoa kutse.",
- "room_n_unread_messages": {
- "one": "Ava %(roomName)s jututuba 1 lugemata sõnumiga.",
- "other": "Ava %(roomName)s jututuba %(count)s lugemata sõnumiga."
- },
- "room_n_unread_messages_mentions": {
- "one": "Ava %(roomName)s jututuba 1 lugemata mainimisega.",
- "other": "Ava %(roomName)s jututuba %(count)s lugemata sõnumiga, mille hulgas on ka mainimisi."
- },
"room_name": "Jututuba %(name)s",
"room_status_bar": "Jututoa olekuriba",
"seek_bar_label": "Heli kerimisriba",
@@ -1717,7 +1707,6 @@
"class_global": "Üldised",
"class_other": "Muud",
"default": "Tavaline",
- "default_settings": "Sobita vaikimisi seadistustega",
"email_pusher_app_display_name": "E-posti teel saadetavad teavitused",
"enable_prompt_toast_description": "Võta kasutusele töölauakeskkonna teavitused",
"enable_prompt_toast_title": "Teavitused",
@@ -1736,8 +1725,7 @@
"mentions_and_keywords_description": "Soovin teavitusi sellisena mainimiste ja võtmesõnade puhul, nagu ma neid olen seadistanud",
"mentions_keywords": "Mainimised ja märksõnad",
"message_didnt_send": "Sõnum jäi saatmata. Lisateabe saamiseks klõpsi.",
- "mute_description": "Sa ei saa üldse teavitusi",
- "mute_room": "Summuta jututuba"
+ "mute_description": "Sa ei saa üldse teavitusi"
},
"notifier": {
"m.key.verification.request": "%(name)s soovib verifitseerimist"
@@ -2153,37 +2141,9 @@
"add_space_label": "Lisa kogukond",
"breadcrumbs_empty": "Hiljuti külastatud jututubasid ei leidu",
"breadcrumbs_label": "Hiljuti külastatud jututoad",
- "collapse_filters": "Ahenda filtriloendit",
- "empty": {
- "no_chats": "Vestlusi veel ei leidu",
- "no_chats_description": "Alusta sellest, et leia mõni vestluspartner või loo oma jututuba",
- "no_chats_description_no_room_rights": "Alusta sellest, et leia mõni vestluspartner",
- "no_favourites": "Sa pole veel ühtegi vestlust märkinud lemmikuks",
- "no_favourites_description": "Vestluse saad märkida lemmikuks tema seadistustest",
- "no_invites": "Sul pole lugemata kutseid",
- "no_lowpriority": "Sul pole ühtegi vähetähtsat jututuba",
- "no_mentions": "Sul pole lugemata mainimisi",
- "no_people": "Sul pole veel ühtegi otsevestlust kellegagi",
- "no_people_description": "Kõikide muude vestluste nägemiseks eemalda otsingufiltrid",
- "no_rooms": "Sa veel ei osale mitte üheski jututoas",
- "no_rooms_description": "Kõikide oma muude vestluste nägemiseks eemalda otsingufiltrid",
- "no_unread": "Õnnitlused! Sul pole ühtegi lugemata sõnumit",
- "show_activity": "Vaata kõiki tegevusi",
- "show_chats": "Näita kõiki vestlusi"
- },
- "expand_filters": "Laienda filtriloendit",
"failed_add_tag": "Sildi %(tagName)s lisamine jututoale ebaõnnestus",
"failed_remove_tag": "Sildi %(tagName)s eemaldamine jututoast ebaõnnestus",
"failed_set_dm_tag": "Otsevestluse sildi seadmine ei õnnestunud",
- "filters": {
- "favourite": "Lemmikud",
- "invites": "Kutsed",
- "low_priority": "Vähetähtis",
- "mentions": "Mainimised",
- "people": "Inimesed",
- "rooms": "Jututoad",
- "unread": "Lugemata"
- },
"home_menu_label": "Avalehe valikud",
"join_public_room_label": "Liitu avaliku jututoaga",
"joining_rooms_status": {
@@ -2192,23 +2152,13 @@
},
"list_title": "Jututubade loend",
"more_options": {
- "copy_link": "Kopeeri jututoa link",
- "favourited": "Määratud lemmikuks",
- "leave_room": "Lahku jututoast",
- "low_priority": "Vähetähtis",
- "mark_read": "Märgi loetuks",
- "mark_unread": "Märgi mitteloetuks"
+ "leave_room": "Lahku jututoast"
},
"notification_options": "Teavituste eelistused",
- "primary_filters": "Jututubade loendi filtrid",
"redacting_messages_status": {
"other": "Kustutame sõnumeid %(count)s jututoas",
"one": "Kustutame sõnumeid %(count)s jututoas"
},
- "room": {
- "more_options": "Täiendavad seadistused",
- "open_room": "Ava jututuba: %(roomName)s"
- },
"show_less": "Näita vähem",
"show_n_more": {
"one": "Näita veel %(count)s vestlust",
diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index dfdebb2553..42a1ed4d85 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -12,7 +12,6 @@
"one": "Yksi lukematon maininta."
},
"recent_rooms": "Viimeisimmät huoneet",
- "room_n_unread_invite": "Avaa huoneen %(roomName)s kutsu.",
"room_name": "Huone %(name)s",
"room_status_bar": "Huoneen tilapalkki",
"seek_bar_label": "Äänen siirtymispalkki",
@@ -1414,8 +1413,7 @@
"mentions_and_keywords_description": "Vastaanota ilmoitukset maininnoista ja asiasanoista asetuksissa määrittämälläsi tavalla",
"mentions_keywords": "Maininnat ja avainsanat",
"message_didnt_send": "Viestiä ei lähetetty. Lisätietoa napsauttamalla.",
- "mute_description": "Et saa ilmoituksia",
- "mute_room": "Mykistä huone"
+ "mute_description": "Et saa ilmoituksia"
},
"notifier": {
"m.key.verification.request": "%(name)s pyytää varmennusta"
@@ -1760,22 +1758,8 @@
"add_space_label": "Lisää avaruus",
"breadcrumbs_empty": "Ei hiljattain vierailtuja huoneita",
"breadcrumbs_label": "Hiljattain vieraillut huoneet",
- "empty": {
- "no_chats": "Ei keskusteluja vielä",
- "no_chats_description": "Aloita lähettämällä viestejä jollekin henkilölle tai luomalla huone",
- "no_chats_description_no_room_rights": "Aloita lähettämällä viesti jollekin",
- "no_favourites": "Sinulla ei ole vielä suosikkikeskustelua",
- "no_rooms": "Et ole vielä missään huoneessa",
- "no_unread": "Onnittelut! Sinulla ei ole lukemattomia viestejä",
- "show_chats": "Näytä kaikki keskustelut"
- },
"failed_add_tag": "Tagin %(tagName)s lisääminen huoneeseen epäonnistui",
"failed_remove_tag": "Tagin %(tagName)s poistaminen huoneesta epäonnistui",
- "filters": {
- "favourite": "Suosikit",
- "people": "Ihmiset",
- "rooms": "Huoneet"
- },
"home_menu_label": "Etusivun valinnat",
"join_public_room_label": "Liity julkiseen huoneeseen",
"joining_rooms_status": {
@@ -1784,19 +1768,13 @@
},
"list_title": "Huoneluettelo",
"more_options": {
- "copy_link": "Kopioi huoneen linkki",
- "leave_room": "Poistu huoneesta",
- "mark_read": "Merkitse luetuksi",
- "mark_unread": "Merkitse lukemattomaksi"
+ "leave_room": "Poistu huoneesta"
},
"notification_options": "Ilmoitusasetukset",
"redacting_messages_status": {
"one": "Poistetaan parhaillaan viestejä yhdessä huoneessa",
"other": "Poistetaan parhaillaan viestejä %(count)s huoneesta"
},
- "room": {
- "open_room": "Avoin huone %(roomName)s"
- },
"show_less": "Näytä vähemmän",
"show_n_more": {
"one": "Näytä %(count)s lisää",
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 09efeb2e97..d354d07291 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -12,16 +12,6 @@
"one": "1 mention non lue."
},
"recent_rooms": "Salons récents",
- "room_messsage_not_sent": "Ouvrir le salon %(roomName)s avec un message non envoyé.",
- "room_n_unread_invite": "Ouvrir l'invitation du salon %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Ouvrir salon %(roomName)s avec 1 message non lu.",
- "other": "Ouvrir salon %(roomName)s avec %(count)s messages non lus."
- },
- "room_n_unread_messages_mentions": {
- "one": "Ouvrir salon %(roomName)s avec 1 mention non lue.",
- "other": "Ouvrir salon %(roomName)s avec %(count)s messages non lus comprenant des mentions."
- },
"room_name": "Salon %(name)s",
"room_status_bar": "Barre de statut du salon",
"seek_bar_label": "Barre de recherche audio",
@@ -1717,7 +1707,6 @@
"class_global": "Global",
"class_other": "Autre",
"default": "Par défaut",
- "default_settings": "Correspondre aux paramètres par défaut",
"email_pusher_app_display_name": "Notifications par courriel",
"enable_prompt_toast_description": "Activer les notifications sur le bureau",
"enable_prompt_toast_title": "Notifications",
@@ -1736,8 +1725,7 @@
"mentions_and_keywords_description": "Recevoir des notifications uniquement pour les mentions et mot-clés comme défini dans vos paramètres",
"mentions_keywords": "Mentions et mots-clés",
"message_didnt_send": "Le message n’a pas été envoyé. Cliquer pour plus d’info.",
- "mute_description": "Vous n’aurez aucune notification",
- "mute_room": "Rendre le salon muet"
+ "mute_description": "Vous n’aurez aucune notification"
},
"notifier": {
"m.key.verification.request": "%(name)s demande une vérification"
@@ -2151,37 +2139,9 @@
"add_space_label": "Ajouter un espace",
"breadcrumbs_empty": "Aucun salon visité récemment",
"breadcrumbs_label": "Salons visités récemment",
- "collapse_filters": "Réduire la liste des filtres",
- "empty": {
- "no_chats": "Pas encore de discussions",
- "no_chats_description": "Commencez par envoyer un message à quelqu'un ou en créant un salon",
- "no_chats_description_no_room_rights": "Commencez par envoyer un message à quelqu'un",
- "no_favourites": "Vous n'avez pas encore de discussion favorite",
- "no_favourites_description": "Vous pouvez ajouter une discussion à vos favoris dans les paramètres de discussion",
- "no_invites": "Vous n'avez aucune invitation non lue",
- "no_lowpriority": "Vous n'avez aucun salon avec une priorité basse",
- "no_mentions": "Vous n'avez aucune mention non lue",
- "no_people": "Vous n'avez encore de discussions",
- "no_people_description": "Veuillez désélectionner des filtres pour voir vos discussions",
- "no_rooms": "Vous n’êtes membre d’aucun salon",
- "no_rooms_description": "Veuillez désélectionner des filtres pour voir vos discussions",
- "no_unread": "Félicitations ! Vous n'avez aucun message non lu",
- "show_activity": "Voir toutes les activités",
- "show_chats": "Afficher toutes les discussions"
- },
- "expand_filters": "Développer la liste des filtres",
"failed_add_tag": "Échec de l’ajout de l’étiquette %(tagName)s au salon",
"failed_remove_tag": "Échec de la suppression de l’étiquette %(tagName)s du salon",
"failed_set_dm_tag": "Échec de l’ajout de l’étiquette de conversation privée",
- "filters": {
- "favourite": "Favoris",
- "invites": "Invitations",
- "low_priority": "Priorité basse",
- "mentions": "Mentions",
- "people": "Personnes",
- "rooms": "Salons",
- "unread": "Non-lus"
- },
"home_menu_label": "Options de l’accueil",
"join_public_room_label": "Rejoindre le salon public",
"joining_rooms_status": {
@@ -2190,23 +2150,13 @@
},
"list_title": "Liste de salons",
"more_options": {
- "copy_link": "Copier le lien du salon",
- "favourited": "Favorisé",
- "leave_room": "Quitter le salon",
- "low_priority": "Priorité basse",
- "mark_read": "Marquer comme lu",
- "mark_unread": "Marquer comme non lu"
+ "leave_room": "Quitter le salon"
},
"notification_options": "Paramètres de notifications",
- "primary_filters": "Filtre de la liste des salons",
"redacting_messages_status": {
"one": "Actuellement en train de supprimer les messages dans %(count)s salon",
"other": "Actuellement en train de supprimer les messages dans %(count)s salons"
},
- "room": {
- "more_options": "Plus d’options",
- "open_room": "Ouvrir salon %(roomName)s"
- },
"show_less": "En voir moins",
"show_n_more": {
"other": "En afficher %(count)s de plus",
diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json
index d2a680a414..6702fbaf09 100644
--- a/src/i18n/strings/hr.json
+++ b/src/i18n/strings/hr.json
@@ -14,18 +14,6 @@
"other": "%(count)s nepročitanih poruka, uključujući spominjanja."
},
"recent_rooms": "Nedavne sobe",
- "room_messsage_not_sent": "Otvori sobu %(roomName)s s porukom koja nije poslana.",
- "room_n_unread_invite": "Otvorite pozivnicu za sobu %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Otvori sobu %(roomName)s s 1 nepročitanom porukom.",
- "few": "Otvori sobu %(roomName)s s %(count)s nepročitane poruke.",
- "other": "Otvori sobu %(roomName)s s %(count)s nepročitanih poruka."
- },
- "room_n_unread_messages_mentions": {
- "one": "Otvori sobu %(roomName)s s 1 nepročitanim spominjanjem.",
- "few": "Otvori sobu %(roomName)s s %(count)s nepročitane poruke, uključujući spominjanja.",
- "other": "Otvori sobu %(roomName)s s %(count)s nepročitanih poruka, uključujući spominjanja."
- },
"room_name": "Soba %(name)s",
"room_status_bar": "Traka statusa sobe",
"seek_bar_label": "Traka za traženje zvuka",
@@ -1733,7 +1721,6 @@
"class_global": "Globalno",
"class_other": "Ostalo",
"default": "Zadano",
- "default_settings": "Uskladi zadane postavke",
"email_pusher_app_display_name": "Obavijesti e-poštom",
"enable_prompt_toast_description": "Omogući obavijesti na radnoj površini",
"enable_prompt_toast_title": "Obavijesti",
@@ -1752,8 +1739,7 @@
"mentions_and_keywords_description": "Primajte obavijesti samo o spominjanjima i ključnim riječima kako je postavljeno u vašim postavkama",
"mentions_keywords": "Spominjanja i ključne riječi",
"message_didnt_send": "Poruka nije poslana. Kliknite za informacije.",
- "mute_description": "Nećete primati nikakve obavijesti",
- "mute_room": "Utišaj sobu"
+ "mute_description": "Nećete primati nikakve obavijesti"
},
"notifier": {
"m.key.verification.request": "%(name)s traži potvrdu"
@@ -2183,37 +2169,9 @@
"add_space_label": "Dodaj prostor",
"breadcrumbs_empty": "Nema nedavno posjećenih soba",
"breadcrumbs_label": "Nedavno posjećene sobe",
- "collapse_filters": "Sažmi popis filtara",
- "empty": {
- "no_chats": "Još nema razgovora",
- "no_chats_description": "Započnite tako da nekome pošaljete poruku ili izradite sobu",
- "no_chats_description_no_room_rights": "Započnite tako da nekome pošaljete poruku",
- "no_favourites": "Još nemate omiljenih razgovora",
- "no_favourites_description": "Razgovor možete dodati u favorite u postavkama razgovora",
- "no_invites": "Nemate nepročitanih pozivnica",
- "no_lowpriority": "Nemate sobe niskog prioriteta",
- "no_mentions": "Nemate nepročitanih spominjanja",
- "no_people": "Još nemate izravne razgovore ni s kim",
- "no_people_description": "Možete poništiti odabir filtara kako biste vidjeli ostale razgovore",
- "no_rooms": "Niste još ni u jednoj sobi",
- "no_rooms_description": "Možete poništiti odabir filtera kako biste vidjeli ostale razgovore",
- "no_unread": "Čestitamo! Nemate nepročitanih poruka",
- "show_activity": "Prikaži sve aktivnosti",
- "show_chats": "Prikaži sve razgovore"
- },
- "expand_filters": "Proširi popis filtara",
"failed_add_tag": "Nije uspjelo dodavanje oznake %(tagName)s na sobu",
"failed_remove_tag": "Nije uspjelo uklanjanje oznake %(tagName)s sa sobe",
"failed_set_dm_tag": "Nije uspjelo postavljanje oznake za izravnu poruku",
- "filters": {
- "favourite": "Favoriti",
- "invites": "Pozivnice",
- "low_priority": "Niski prioritet",
- "mentions": "Spominjanja",
- "people": "Osobe",
- "rooms": "Sobe",
- "unread": "Nepročitano"
- },
"home_menu_label": "Mogućnosti početne stranice",
"join_public_room_label": "Pridruži se javnoj sobi",
"joining_rooms_status": {
@@ -2223,24 +2181,14 @@
},
"list_title": "Popis soba",
"more_options": {
- "copy_link": "Kopiraj poveznicu na sobu",
- "favourited": "Označeno kao favorit",
- "leave_room": "Napusti sobu",
- "low_priority": "Niski prioritet",
- "mark_read": "Označi kao pročitano",
- "mark_unread": "Označi kao nepročitano"
+ "leave_room": "Napusti sobu"
},
"notification_options": "Mogućnosti obavijesti",
- "primary_filters": "Filtri popisa soba",
"redacting_messages_status": {
"one": "Trenutačno se uklanjaju poruke u %(count)s sobi",
"few": "Trenutačno se uklanjaju poruke u %(count)s sobe",
"other": "Trenutačno se uklanjaju poruke u %(count)s soba"
},
- "room": {
- "more_options": "Više mogućnosti",
- "open_room": "Otvori sobu %(roomName)s"
- },
"show_less": "Prikaži manje",
"show_n_more": {
"one": "Prikaži još %(count)s",
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 33a2ac2003..2e34730273 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -12,16 +12,6 @@
"1 olvasatlan megemlítés.": "one"
},
"recent_rooms": "Legutóbbi szobák",
- "room_messsage_not_sent": "A(z) %(roomName)s szoba megnyitása nem beállított üzenettel.",
- "room_n_unread_invite": "A(z) %(roomName)s szoba meghívásának megnyitása.",
- "room_n_unread_messages": {
- "A(z) %(roomName)s szoba megnyitása 1 olvasatlan üzenettel.": "one",
- "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan üzenettel.": "other"
- },
- "room_n_unread_messages_mentions": {
- "A(z) %(roomName)s szoba megnyitása 1 olvasatlan megemlítéssel.": "one",
- "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan megemlítéssel.": "other"
- },
"room_name": "Szoba: %(name)s",
"room_status_bar": "Szoba állapotsora",
"seek_bar_label": "Hang keresősávja",
@@ -1708,7 +1698,6 @@
"class_global": "Globális",
"class_other": "Egyéb",
"default": "Alapértelmezett",
- "default_settings": "Megegyezik az alapértelmezett beállításokkal",
"email_pusher_app_display_name": "E-mail értesítések",
"enable_prompt_toast_description": "Asztali értesítések engedélyezése",
"enable_prompt_toast_title": "Értesítések",
@@ -1727,8 +1716,7 @@
"mentions_and_keywords_description": "Értesítések fogadása csak megemlítéseknél és kulcsszavaknál, a beállításokban megadottak szerint",
"mentions_keywords": "Megemlítések és kulcsszavak",
"message_didnt_send": "Az üzenet nincs elküldve. Kattintson az információkért.",
- "mute_description": "Nem kap semmilyen értesítést",
- "mute_room": "Szoba némítása"
+ "mute_description": "Nem kap semmilyen értesítést"
},
"notifier": {
"m.key.verification.request": "%(name)s ellenőrzést kér"
@@ -2137,37 +2125,9 @@
"add_space_label": "Tér hozzáadása",
"breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák",
"breadcrumbs_label": "Nemrég meglátogatott szobák",
- "collapse_filters": "Szűrőlista összecsukása",
- "empty": {
- "no_chats": "Még nincsenek csevegések",
- "no_chats_description": "Kezdje azzal, hogy üzenetet küld valakinek, vagy létrehoz egy szobát",
- "no_chats_description_no_room_rights": "Kezdje azzal, hogy üzenetet küld valakinek",
- "no_favourites": "Még nincs kedvenc csevegése",
- "no_favourites_description": "A csevegési beállításokban adhat hozzá csevegést a kedvencekhez",
- "no_invites": "Nincs olvasatlan meghívója",
- "no_lowpriority": "Nincs alacsony prioritású szobája",
- "no_mentions": "Nincs olvasatlan említése",
- "no_people": "Még nincs közvetlen csevegése senkivel",
- "no_people_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez",
- "no_rooms": "Még nincs egy szobában sem",
- "no_rooms_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez",
- "no_unread": "Gratulálunk! Nincsenek olvasatlan üzenetei.",
- "show_activity": "Összes tevékenység megtekintése",
- "show_chats": "Összes csevegés megjelenítése"
- },
- "expand_filters": "Szűrőlista kibontása",
"failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s",
"failed_remove_tag": "Nem sikerült a szobáról eltávolítani ezt: %(tagName)s",
"failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani",
- "filters": {
- "favourite": "Kedvencek",
- "invites": "Meghívók",
- "low_priority": "Alacsony prioritás",
- "mentions": "Említések",
- "people": "Emberek",
- "rooms": "Szobák",
- "unread": "Olvasatlan"
- },
"home_menu_label": "Kezdőlap beállítások",
"join_public_room_label": "Belépés nyilvános szobába",
"joining_rooms_status": {
@@ -2175,22 +2135,12 @@
},
"list_title": "Szobalista",
"more_options": {
- "copy_link": "Szoba hivatkozásának másolása",
- "favourited": "Kedvencnek jelölve",
- "leave_room": "Szoba elhagyása",
- "low_priority": "Alacsony prioritás",
- "mark_read": "Megjelölés olvasottként",
- "mark_unread": "Megjelölés olvasatlanként"
+ "leave_room": "Szoba elhagyása"
},
"notification_options": "Értesítési beállítások",
- "primary_filters": "Szobalistaszűrők",
"redacting_messages_status": {
"Üzenet törlése %(count)s szobából": "other"
},
- "room": {
- "more_options": "További lehetőségek",
- "open_room": "A(z) %(roomName)s szoba megnyitása"
- },
"show_less": "Kevesebb megjelenítése",
"show_n_more": {
"Még %(count)s megjelenítése": "one"
diff --git a/src/i18n/strings/hy.json b/src/i18n/strings/hy.json
index 4478bad9da..d291ad6885 100644
--- a/src/i18n/strings/hy.json
+++ b/src/i18n/strings/hy.json
@@ -12,16 +12,6 @@
"other": "%(count)s չկարդացված հաղորդագրություններ, ներառյալ հիշատակումները։"
},
"recent_rooms": "Վերջին սենյակները",
- "room_messsage_not_sent": "Բացել %(roomName)s սենյակը՝ չուղարկված հաղորդագրությամբ",
- "room_n_unread_invite": "Բացել %(roomName)s սենյակի հրավերը",
- "room_n_unread_messages": {
- "one": "Բացել %(roomName)s սենյակը՝ 1 չկարդացած հաղորդագրությամբ",
- "other": "Բացել %(roomName)s սենյակը՝ %(count)s չկարդացած հաղորդագրություններով"
- },
- "room_n_unread_messages_mentions": {
- "one": "Բացել %(roomName)s սենյակը՝ 1 չկարդացած հիշեցմամբ",
- "other": "Բացել %(roomName)s սենյակը՝ %(count)s չկարդացած հաղորդագրություններով, ներառյալ հիշեցումները"
- },
"room_name": "Սենյակ %(name)s",
"room_status_bar": "Սենյակի կարգավիճակի գոտի/վահանակ",
"seek_bar_label": "Աուդիո որոնման գոտի",
@@ -1659,7 +1649,6 @@
"class_global": "Գլոբալ",
"class_other": "Այլ",
"default": "Լռելյայն",
- "default_settings": "Համապատասխանեցնել լռելյայն կարգավորումները",
"email_pusher_app_display_name": "Ծանուցումներ էլ.փոստով",
"enable_prompt_toast_description": "Միացնել աշխատասեղանի ծանուցումները",
"enable_prompt_toast_title": "Ծանուցումներ",
@@ -1678,8 +1667,7 @@
"mentions_and_keywords_description": "Ստանալ ծանուցում միայն հիշատակումներով և բանալի բառերով, ինչպես սահմանված են ձեր կարգաբերումներում",
"mentions_keywords": "Նշումներ/հիշատակումներ և բանալի բառեր",
"message_didnt_send": "Հաղորդագրությունը չի ուղարկվել: Սեղմեք տեղեկությունների համար:",
- "mute_description": "Դուք չեք ստանա ոչ մի ծանուցում",
- "mute_room": "\"Խլացնել\" սենյակի ձայնը"
+ "mute_description": "Դուք չեք ստանա ոչ մի ծանուցում"
},
"notifier": {
"m.key.verification.request": "%(name)s-ը պահանջում է ստուգում"
@@ -2077,37 +2065,9 @@
"add_space_label": "Ավելացնել տարածք",
"breadcrumbs_empty": "Վերջերս այցելած սենյակներ չկան",
"breadcrumbs_label": "Վերջերս այցելած սենյակներ",
- "collapse_filters": "Ծալել ֆիլտրերի ցանկը",
- "empty": {
- "no_chats": "Դեռևս զրույցներ չկան",
- "no_chats_description": "Սկսեք՝ ուղարկելով հաղորդագրություն ինչ-որ մեկին կամ ստեղծելով սենյակ",
- "no_chats_description_no_room_rights": "Սկսեք՝ հաղորդագրություն ուղարկելով ինչ-որ մեկին։",
- "no_favourites": "Դուք դեռ չունեք սիրելի զրույց",
- "no_favourites_description": "Զրույցը նախընտրածների մեջ ավելացնելու համար օգտագործեք զրույցի կարգավորումները։",
- "no_invites": "Դուք չունեք չկարդացված հրավերներ",
- "no_lowpriority": "Դուք ցածր առաջնահերթության սենյակներ չունեք",
- "no_mentions": "Դուք չունեք չկարդացված հիշատակումներ",
- "no_people": "Դուք դեռ ոչ մեկի հետ անհատական զրույց չունեք",
- "no_people_description": "Դուք կարող եք անջատել ֆիլտրերը՝ ձեր մյուս զրույցները տեսնելու համար",
- "no_rooms": "Դուք դեռ որևէ սենյակում չեք գտնվում",
- "no_rooms_description": "Դուք կարող եք անջատել ֆիլտրերը՝ ձեր մյուս զրույցները տեսնելու համար",
- "no_unread": "Շնորհավորանքներ։ Դուք չունեք չկարդացված հաղորդագրություններ։",
- "show_activity": "Տեսնել ամբողջ ակտիվությունը",
- "show_chats": "Ցուցադրել բոլոր զրույցները"
- },
- "expand_filters": "Ընդարձակել ֆիլտրերի ցանկը",
"failed_add_tag": "Չհաջողվեց %(tagName)s պիտակը(tag) ավելացնել սենյակին",
"failed_remove_tag": "Չհաջողվեց հեռացնել %(tagName)s պիտակը(tag) սենյակից",
"failed_set_dm_tag": "Չհաջողվեց սահմանել ուղիղ հաղորդագրության պիտակը(tag)",
- "filters": {
- "favourite": "Ընտրյալներ",
- "invites": "Հրավերներ",
- "low_priority": "Ցածր առաջնահերթություն",
- "mentions": "Հիշատակումներ",
- "people": "Մարդիկ",
- "rooms": "Սենյակներ",
- "unread": "Չկարդացվածներ"
- },
"home_menu_label": "Գլխավոր էջի ընտրանքներ",
"join_public_room_label": "Միանալ հանրային սենյակին",
"joining_rooms_status": {
@@ -2116,23 +2076,13 @@
},
"list_title": "Սենյակների ցանկ",
"more_options": {
- "copy_link": "Պատճենել սենյակի հղումը",
- "favourited": "Ավելացված է ընտրյալների մեջ",
- "leave_room": "Լքել սենյակը",
- "low_priority": "Ցածր առաջնահերթություն",
- "mark_read": "Նշել որպես կարդացված",
- "mark_unread": "Նշել որպես չկարդացված"
+ "leave_room": "Լքել սենյակը"
},
"notification_options": "Ծանուցման ընտրանքներ",
- "primary_filters": "Սենյակների ցանկի ֆիլտրեր",
"redacting_messages_status": {
"one": "Ներկայումս ջնջվում են %(count)s սենյակում",
"other": "Ներկայումս ջնջվում են %(count)s սենյակներում"
},
- "room": {
- "more_options": "Լրացուցիչ ընտրանքներ",
- "open_room": "Բացել %(roomName)s սենյակը"
- },
"show_less": "Ցուցադրել ավելի քիչ",
"show_n_more": {
"one": "Ցուցադրել ևս %(count)s",
diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json
index 3666a705ba..04f49a6ee5 100644
--- a/src/i18n/strings/id.json
+++ b/src/i18n/strings/id.json
@@ -12,14 +12,6 @@
"other": "%(count)s pesan yang belum dibaca termasuk sebutan."
},
"recent_rooms": "Ruangan terkini",
- "room_messsage_not_sent": "Buka ruangan %(roomName)s dengan pesan yang belum diatur.",
- "room_n_unread_invite": "Buka undangan ruangan %(roomName)s.",
- "room_n_unread_messages": {
- "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca."
- },
- "room_n_unread_messages_mentions": {
- "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca termasuk sebutan."
- },
"room_name": "Ruangan %(name)s",
"room_status_bar": "Bilah status ruangan",
"seek_bar_label": "Bilah pencarian audio",
@@ -1713,7 +1705,6 @@
"class_global": "Global",
"class_other": "Lainnya",
"default": "Bawaan",
- "default_settings": "Cocokkan pengaturan bawaan",
"email_pusher_app_display_name": "Notifikasi Surel",
"enable_prompt_toast_description": "Aktifkan notifikasi desktop",
"enable_prompt_toast_title": "Notifikasi",
@@ -1732,8 +1723,7 @@
"mentions_and_keywords_description": "Dapatkan notifikasi hanya dengan sebutan dan kata kunci yang diatur di pengaturan Anda",
"mentions_keywords": "Sebutan dan kata kunci",
"message_didnt_send": "Pesan tidak terkirim. Klik untuk informasi.",
- "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun",
- "mute_room": "Bisukan ruangan"
+ "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun"
},
"notifier": {
"m.key.verification.request": "%(name)s meminta verifikasi"
@@ -2140,37 +2130,9 @@
"add_space_label": "Tambahkan space",
"breadcrumbs_empty": "Tidak ada ruangan yang baru saja dilihat",
"breadcrumbs_label": "Ruangan yang baru saja dilihat",
- "collapse_filters": "Tutup daftar filter",
- "empty": {
- "no_chats": "Belum ada obrolan",
- "no_chats_description": "Mulailah dengan mengirim pesan kepada seseorang atau dengan membuat ruangan",
- "no_chats_description_no_room_rights": "Mulailah dengan mengirim pesan kepada seseorang",
- "no_favourites": "Anda belum memiliki obrolan favorit",
- "no_favourites_description": "Anda dapat menambahkan obrolan ke favorit Anda di pengaturan obrolan",
- "no_invites": "Anda tidak memiliki undangan yang belum dibaca",
- "no_lowpriority": "Anda tidak memiliki ruangan dengan prioritas rendah",
- "no_mentions": "Anda tidak memiliki sebutan yang belum dibaca",
- "no_people": "Anda belum memiliki obrolan langsung dengan siapa pun",
- "no_people_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain",
- "no_rooms": "Anda belum berada di ruangan mana pun",
- "no_rooms_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain",
- "no_unread": "Selamat! Anda tidak memiliki pesan yang belum dibaca",
- "show_activity": "Lihat semua aktivitas",
- "show_chats": "Tampilkan semua obrolan"
- },
- "expand_filters": "Buka daftar filter",
"failed_add_tag": "Gagal menambahkan tag %(tagName)s ke ruangan",
"failed_remove_tag": "Gagal menghapus tanda %(tagName)s dari ruangan",
"failed_set_dm_tag": "Gagal menetapkan tanda pesan langsung",
- "filters": {
- "favourite": "Favorit",
- "invites": "Undangan",
- "low_priority": "Prioritas rendah",
- "mentions": "Sebutan",
- "people": "Orang",
- "rooms": "Ruangan",
- "unread": "Belum dibaca"
- },
"home_menu_label": "Opsi Beranda",
"join_public_room_label": "Bergabung dengan ruangan publik",
"joining_rooms_status": {
@@ -2179,23 +2141,13 @@
},
"list_title": "Daftar ruangan",
"more_options": {
- "copy_link": "Salin tautan ruangan",
- "favourited": "Difavorit",
- "leave_room": "Tinggalkan ruangan",
- "low_priority": "Prioritas rendah",
- "mark_read": "Tandai sebagai dibaca",
- "mark_unread": "Tandai sebagai belum dibaca"
+ "leave_room": "Tinggalkan ruangan"
},
"notification_options": "Opsi notifikasi",
- "primary_filters": "Filter daftar ruangan",
"redacting_messages_status": {
"one": "Saat ini menghapus pesan-pesan di %(count)s ruangan",
"other": "Saat ini menghapus pesan-pesan di %(count)s ruangan"
},
- "room": {
- "more_options": "Opsi Lainnya",
- "open_room": "Buka ruangan %(roomName)s"
- },
"show_less": "Tampilkan lebih sedikit",
"show_n_more": {
"one": "Tampilkan %(count)s lagi",
diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json
index 420548c3b4..64bfe96d6b 100644
--- a/src/i18n/strings/ko.json
+++ b/src/i18n/strings/ko.json
@@ -10,14 +10,6 @@
"other": "읽지 않은 메시지 %(count)s개 (멘션 포함)."
},
"recent_rooms": "최근 방",
- "room_messsage_not_sent": "%(roomName)s에 미발송 메시지가 있는 방을 엽니다.",
- "room_n_unread_invite": "공개 방 %(roomName)s에 초대 되었습니다.",
- "room_n_unread_messages": {
- "other": "읽지 않은 메시지 %(count)s개가 있는 채팅방 %(roomName)s 열기"
- },
- "room_n_unread_messages_mentions": {
- "other": "멘션을 포함해 읽지 않은 메시지 %(count)s개가 있는 채팅방 %(roomName)s 열기"
- },
"room_name": "%(name)s 방",
"room_status_bar": "방 상태 표시줄",
"seek_bar_label": "오디오 탐색 바",
@@ -1700,7 +1692,6 @@
"class_global": "글로벌",
"class_other": "기타",
"default": "기본",
- "default_settings": "기본 설정과 일치",
"email_pusher_app_display_name": "이메일 알림",
"enable_prompt_toast_description": "데스크톱 알림 활성화",
"enable_prompt_toast_title": "알림",
@@ -1719,8 +1710,7 @@
"mentions_and_keywords_description": "설정에서 지정한 멘션과 키워드인 경우에만 알림을 받습니다",
"mentions_keywords": "멘션 및 키워드",
"message_didnt_send": "메시지가 전송되지 않았습니다. 자세한 내용은 클릭하세요.",
- "mute_description": "어떤 알람도 받지 않습니다",
- "mute_room": "채팅방 음소거"
+ "mute_description": "어떤 알람도 받지 않습니다"
},
"notifier": {
"m.key.verification.request": "%(name)s님이 인증을 요청하고 있습니다"
@@ -2113,37 +2103,9 @@
"add_space_label": "스페이스 추가하기",
"breadcrumbs_empty": "최근에 방문하지 않은 방 목록",
"breadcrumbs_label": "최근 방문한 방 목록",
- "collapse_filters": "필터 목록 접기",
- "empty": {
- "no_chats": "아직 채팅이 없습니다.",
- "no_chats_description": "누군가에게 메시지를 보내거나 채팅방을 생성하여 시작하세요",
- "no_chats_description_no_room_rights": "누군가에게 메시지를 보내서 시작하세요",
- "no_favourites": "아직 즐겨찾는 채팅이 없습니다.",
- "no_favourites_description": "채팅 설정에서 채팅을 즐겨찾기에 추가할 수 있습니다",
- "no_invites": "읽지 않은 초대장이 없습니다",
- "no_lowpriority": "우선순위가 낮은 채팅방이 없습니다.",
- "no_mentions": "읽지 않은 멘션이 없습니다.",
- "no_people": "아직 누구와도 직접 채팅을 하지 않았습니다",
- "no_people_description": "다른 채팅을 보려면 필터 선택을 해제하세요.",
- "no_rooms": "아직 어떤 채팅방에도 있지 않습니다",
- "no_rooms_description": "다른 채팅을 보려면 필터 선택을 해제하세요.",
- "no_unread": "축하합니다! 읽지 않은 메시지가 없습니다.",
- "show_activity": "모든 활동 보기",
- "show_chats": "모든 채팅 보기"
- },
- "expand_filters": "필터 목록 확장",
"failed_add_tag": "방에 %(tagName)s 태그 추가에 실패함",
"failed_remove_tag": "방에 %(tagName)s 태그 제거에 실패함",
"failed_set_dm_tag": "다이렉트 메시지 태그 설정에 실패했습니다",
- "filters": {
- "favourite": "즐겨찾기",
- "invites": "초대",
- "low_priority": "낮은 우선순위",
- "mentions": "멘션",
- "people": "사람",
- "rooms": "채팅방",
- "unread": "읽지 않은 항목"
- },
"home_menu_label": "홈 옵션",
"join_public_room_label": "공개 방 참가하기",
"joining_rooms_status": {
@@ -2151,22 +2113,12 @@
},
"list_title": "채팅방 목록",
"more_options": {
- "copy_link": "채팅방 링크 복사",
- "favourited": "즐겨찾기 됨",
- "leave_room": "채팅방 떠나기",
- "low_priority": "낮은 우선순위",
- "mark_read": "읽음으로 표시",
- "mark_unread": "읽지 않음으로 표시"
+ "leave_room": "채팅방 떠나기"
},
"notification_options": "알림 옵션",
- "primary_filters": "채팅방 목록 필터",
"redacting_messages_status": {
"other": "현재 %(count)s 방에서 메시지를 삭제 중입니다"
},
- "room": {
- "more_options": "옵션 더보기",
- "open_room": "채팅방 %(roomName)s 열기"
- },
"show_less": "간단히 표시",
"show_n_more": {
"other": "%(count)s개 더 보기"
diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json
index 2269299115..3a2993ea87 100644
--- a/src/i18n/strings/nb_NO.json
+++ b/src/i18n/strings/nb_NO.json
@@ -12,16 +12,6 @@
"other": "%(count)s uleste meldinger inkludert der du nevnes."
},
"recent_rooms": "Nylige rom",
- "room_messsage_not_sent": "Åpent rom %(roomName)s med en usendt melding.",
- "room_n_unread_invite": "Invitasjon til det åpne rommet %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Åpne room %(roomName)s med 1 ulest message.",
- "other": "Åpne room %(roomName)s med %(count)s uleste messages."
- },
- "room_n_unread_messages_mentions": {
- "one": "Åpne room %(roomName)s med 1 ulest omtale.",
- "other": "Åpne room %(roomName)s med %(count)s uleste meldinger inkludert omtaler."
- },
"room_name": "Rom %(name)s",
"room_status_bar": "Statuslinje for rommet",
"seek_bar_label": "Søkelinje for lyd",
@@ -1711,7 +1701,6 @@
"class_global": "Globalt",
"class_other": "Andre",
"default": "Standard",
- "default_settings": "Match standardinnstillingene",
"email_pusher_app_display_name": "E-postvarsler",
"enable_prompt_toast_description": "Aktiver skrivebordsvarsler",
"enable_prompt_toast_title": "Varsler",
@@ -1730,8 +1719,7 @@
"mentions_and_keywords_description": "Bli varslet bare med omtaler og nøkkelord som konfigurert i innstillingene dine ",
"mentions_keywords": "Omtaler og nøkkelord",
"message_didnt_send": "Meldingen ble ikke sendt. Klikk for informasjon.",
- "mute_description": "Du vil ikke få noen varsler",
- "mute_room": "Demp rommet"
+ "mute_description": "Du vil ikke få noen varsler"
},
"notifier": {
"m.key.verification.request": "%(name)s ber om verifisering"
@@ -2145,37 +2133,9 @@
"add_space_label": "Legg til område",
"breadcrumbs_empty": "Ingen nylig besøkte rom",
"breadcrumbs_label": "Nylig besøkte rom",
- "collapse_filters": "Skjul filterlisten",
- "empty": {
- "no_chats": "Ingen chatter ennå",
- "no_chats_description": "Kom i gang ved å sende meldinger til noen eller ved å opprette et rom",
- "no_chats_description_no_room_rights": "Kom i gang med å sende meldinger til noen",
- "no_favourites": "Du har ikke favorittchat ennå",
- "no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene",
- "no_invites": "Du har ingen uleste invitasjoner",
- "no_lowpriority": "Du har ingen rom med lav prioritet",
- "no_mentions": "Du har ingen uleste omtaler",
- "no_people": "Du har ikke direkte chatter med noen ennå",
- "no_people_description": "Du kan fjerne merket for filtre for å se de andre chattene dine",
- "no_rooms": "Du er ikke med i noen rom ennå",
- "no_rooms_description": "Du kan fjerne merket for filtre for å se de andre chattene dine",
- "no_unread": "Gratulerer! Du har ingen uleste meldinger",
- "show_activity": "Se alle aktiviteter",
- "show_chats": "Vis alle chatter"
- },
- "expand_filters": "Utvid filterlisten",
"failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom",
"failed_remove_tag": "Kunne ikke fjerne tagg %(tagName)s fra rommet",
"failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen",
- "filters": {
- "favourite": "Favoritter",
- "invites": "Invitasjoner",
- "low_priority": "Lav prioritet",
- "mentions": "Omtaler",
- "people": "Personer",
- "rooms": "Rom",
- "unread": "Uleste"
- },
"home_menu_label": "Hjem alternativer",
"join_public_room_label": "Bli med i offentlig rom",
"joining_rooms_status": {
@@ -2184,23 +2144,13 @@
},
"list_title": "Romliste",
"more_options": {
- "copy_link": "Kopier romlenke",
- "favourited": "Favorittmerket",
- "leave_room": "Forlat rommet",
- "low_priority": "Lav prioritet",
- "mark_read": "Marker som lest",
- "mark_unread": "Marker som ulest"
+ "leave_room": "Forlat rommet"
},
"notification_options": "Varselsinnstillinger",
- "primary_filters": "Filtre for romliste",
"redacting_messages_status": {
"one": "Fjerner for øyeblikket meldinger i %(count)s rom",
"other": "Fjerner for øyeblikket meldinger i %(count)s rom"
},
- "room": {
- "more_options": "Flere alternativer",
- "open_room": "Åpne rom %(roomName)s"
- },
"show_less": "Vis mindre",
"show_n_more": {
"Vis %(count)s til": "Vis %(count)s mer"
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 4480de3c1e..95651e177e 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -12,18 +12,6 @@
"one": "1 nieprzeczytana wzmianka."
},
"recent_rooms": "Ostatnie pokoje",
- "room_messsage_not_sent": "Otwórz pokój %(roomName)s z niewysłaną wiadomością.room",
- "room_n_unread_invite": "Otwórz zaproszenie pokoju %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Otwórz pokój %(roomName)s z 1 nieprzeczytaną wiadomością.",
- "few": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wiadomościami.",
- "many": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wiadomościami."
- },
- "room_n_unread_messages_mentions": {
- "one": "Otwórz pokój %(roomName)s z 1 nieprzeczytaną wzmianką.",
- "few": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wzmiankami.",
- "many": "Otwórz pokój %(roomName)s z %(count)s nieprzeczytanymi wzmiankami."
- },
"room_name": "Pokój %(name)s",
"room_status_bar": "Pasek stanu pokoju",
"seek_bar_label": "Pasek wyszukiwania audio",
@@ -1695,7 +1683,6 @@
"class_global": "Globalne",
"class_other": "Inne",
"default": "Domyślne",
- "default_settings": "Ustawienia domyślne",
"email_pusher_app_display_name": "Powiadomienia e-mail",
"enable_prompt_toast_description": "Włącz powiadomienia na pulpicie",
"enable_prompt_toast_title": "Powiadomienia",
@@ -1714,8 +1701,7 @@
"mentions_and_keywords_description": "Otrzymuj powiadomienia tylko z wzmiankami i słowami kluczowymi zgodnie z Twoimi ustawieniami",
"mentions_keywords": "Wzmianki i słowa kluczowe",
"message_didnt_send": "Nie wysłano wiadomości. Kliknij po więcej informacji.",
- "mute_description": "Nie otrzymasz żadnych powiadomień",
- "mute_room": "Wycisz pokój"
+ "mute_description": "Nie otrzymasz żadnych powiadomień"
},
"notifier": {
"m.key.verification.request": "%(name)s prosi o weryfikację"
@@ -2119,37 +2105,9 @@
"add_space_label": "Dodaj przestrzeń",
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
- "collapse_filters": "Zwiń listę filtrów",
- "empty": {
- "no_chats": "Nie ma jeszcze czatów",
- "no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju",
- "no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.",
- "no_favourites": "Nie masz jeszcze ulubionego czatu",
- "no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu",
- "no_invites": "Nie masz żadnych nieprzeczytanych zaproszeń",
- "no_lowpriority": "Nie masz pokoi o niskim priorytecie",
- "no_mentions": "Nie masz żadnych nieprzeczytanych wzmianek",
- "no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych",
- "no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
- "no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
- "no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
- "no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
- "show_activity": "Wyświetl całą aktywność",
- "show_chats": "Pokaż wszystkie czaty"
- },
- "expand_filters": "Rozwiń listę filtrów",
"failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju",
"failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju",
"failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych",
- "filters": {
- "favourite": "Ulubione",
- "invites": "Zaproszenia",
- "low_priority": "Niski priorytet",
- "mentions": "Wzmianki",
- "people": "Osoby",
- "rooms": "Pokoje",
- "unread": "Nieprzeczytane"
- },
"home_menu_label": "Opcje głównej",
"join_public_room_label": "Dołącz do publicznego pokoju",
"joining_rooms_status": {
@@ -2158,23 +2116,13 @@
},
"list_title": "Lista pokojów",
"more_options": {
- "copy_link": "Kopiuj link do pokoju",
- "favourited": "Ulubione",
- "leave_room": "Opuść pokój",
- "low_priority": "Niski priorytet",
- "mark_read": "Oznacz jako przeczytane",
- "mark_unread": "Oznacz jako nieprzeczytane"
+ "leave_room": "Opuść pokój"
},
"notification_options": "Opcje powiadomień",
- "primary_filters": "Filtry listy pomieszczeń",
"redacting_messages_status": {
"one": "Aktualnie usuwanie wiadomości z %(count)s pokoju",
"other": "Aktualnie usuwanie wiadomości z %(count)s pokoi"
},
- "room": {
- "more_options": "Więcej opcji",
- "open_room": "Otwórz pokój %(roomName)s"
- },
"show_less": "Pokaż mniej",
"show_n_more": {
"one": "Pokaż %(count)s więcej",
diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json
index fa4bad8825..d34c3c2d43 100644
--- a/src/i18n/strings/pt.json
+++ b/src/i18n/strings/pt.json
@@ -2004,28 +2004,9 @@
"add_space_label": "Adiciona espaço",
"breadcrumbs_empty": "Nenhuma sala visitada recentemente",
"breadcrumbs_label": "Salas visitadas recentemente",
- "empty": {
- "no_chats": "Ainda sem conversas",
- "no_chats_description": "Começa a enviar mensagens a alguém ou a crie uma sala",
- "no_chats_description_no_room_rights": "Começa por enviar uma mensagem a alguém",
- "no_favourites": "Ainda não tem um conversa favorita",
- "no_favourites_description": "Pode adicionar uma conversa aos seus favoritos nas definições de conversa",
- "no_people": "Ainda não tem conversas diretas com ninguém",
- "no_people_description": "Pode desseleccionar filtros para veres as suas outras conversas",
- "no_rooms": "Você ainda não está em nenhuma sala",
- "no_rooms_description": "Pode desmarcar filtros para ver as suas outras conversas",
- "no_unread": "Parabéns! Não tens nenhuma mensagem por ler",
- "show_chats": "Mostra todas as conversas"
- },
"failed_add_tag": "Falha ao adicionar %(tagName)s à sala",
"failed_remove_tag": "Não foi possível remover a marcação %(tagName)s desta sala",
"failed_set_dm_tag": "Falha ao definir a etiqueta de mensagem direta",
- "filters": {
- "favourite": "Favoritos",
- "people": "Pessoas",
- "rooms": "Salas",
- "unread": "Não lido"
- },
"home_menu_label": "Opções de casa",
"join_public_room_label": "Participa na sala pública",
"joining_rooms_status": {
@@ -2034,23 +2015,13 @@
},
"list_title": "Lista de salas",
"more_options": {
- "copy_link": "Copiar link da sala",
- "favourited": "Adicionado aos favoritos",
- "leave_room": "Sair da sala",
- "low_priority": "Baixa prioridade",
- "mark_read": "Marcar como lido",
- "mark_unread": "Marcar como não lido"
+ "leave_room": "Sair da sala"
},
"notification_options": "Opções de notificação",
- "primary_filters": "Filtros da lista de salas",
"redacting_messages_status": {
"one": "Atualmente removendo mensagens na %(count)s sala",
"other": "Atualmente removendo mensagens em %(count)s salas"
},
- "room": {
- "more_options": "Mais opções",
- "open_room": "Abrir a sala %(roomName)s"
- },
"show_less": "Mostrar menos",
"show_n_more": {
"one": "Mostrar %(count)s mais",
diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 1656263d6e..e10ea69e53 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -12,16 +12,6 @@
"one": "1 menção não lida."
},
"recent_rooms": "Salas recentes",
- "room_messsage_not_sent": "Abra a sala %(roomName)s com uma mensagem não enviada.",
- "room_n_unread_invite": "Abra o convite da sala %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Sala aberta %(roomName)s com 1 mensagem não lida.",
- "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas."
- },
- "room_n_unread_messages_mentions": {
- "one": "Sala aberta %(roomName)s com 1 menção não lida.",
- "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas, incluindo menções."
- },
"room_name": "Sala %(name)s",
"room_status_bar": "Barra de status da sala",
"seek_bar_label": "Barra de busca de áudio",
@@ -1708,7 +1698,6 @@
"class_global": "Global",
"class_other": "Outros",
"default": "Padrão",
- "default_settings": "Corresponder às configurações padrão",
"email_pusher_app_display_name": "Notificações por e-mail",
"enable_prompt_toast_description": "Ativar notificações na área de trabalho",
"enable_prompt_toast_title": "Notificações",
@@ -1727,8 +1716,7 @@
"mentions_and_keywords_description": "Receba notificações apenas com menções e palavras-chave conforme definido em suas configurações",
"mentions_keywords": "Menções e palavras-chave!",
"message_didnt_send": "A mensagem não foi enviada. Clique para mais informações.",
- "mute_description": "Você não receberá nenhuma notificação",
- "mute_room": "Silenciar sala"
+ "mute_description": "Você não receberá nenhuma notificação"
},
"notifier": {
"m.key.verification.request": "%(name)s está solicitando confirmação"
@@ -2129,37 +2117,9 @@
"add_space_label": "Adicionar espaço",
"breadcrumbs_empty": "Nenhuma sala foi visitada recentemente",
"breadcrumbs_label": "Salas visitadas recentemente",
- "collapse_filters": "Recolher lista de filtros",
- "empty": {
- "no_chats": "Ainda não há conversas.",
- "no_chats_description": "Comece enviando uma mensagem para alguém ou criando uma sala",
- "no_chats_description_no_room_rights": "Comece enviando uma mensagem para alguém",
- "no_favourites": "Você ainda não tem o bate-papo favorito",
- "no_favourites_description": "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo",
- "no_invites": "Você não tem nenhum convite não lido",
- "no_lowpriority": "Você não tem nenhuma sala de baixa prioridade",
- "no_mentions": "Você não tem nenhuma menção não lida",
- "no_people": "Você ainda não tem conversas diretas com ninguém",
- "no_people_description": "Você pode desmarcar os filtros para ver suas outras conversas",
- "no_rooms": "Você não está em nenhuma sala ainda",
- "no_rooms_description": "Você pode desmarcar os filtros para ver suas outras conversas.",
- "no_unread": "Parabéns! Você não tem nenhuma mensagem não lida",
- "show_activity": "Ver todas as atividades",
- "show_chats": "Mostrar todas as conversas"
- },
- "expand_filters": "Expandir lista de filtros",
"failed_add_tag": "Falha ao adicionar a tag %(tagName)s para a sala",
"failed_remove_tag": "Falha ao remover a tag %(tagName)s da sala",
"failed_set_dm_tag": "Falha ao definir a marca de mensagem direta",
- "filters": {
- "favourite": "Favoritos",
- "invites": "Convites",
- "low_priority": "Baixa prioridade",
- "mentions": "Menções",
- "people": "Pessoas",
- "rooms": "Salas",
- "unread": "Não lido"
- },
"home_menu_label": "Opções do Início",
"join_public_room_label": "Entrar na sala pública",
"joining_rooms_status": {
@@ -2168,23 +2128,13 @@
},
"list_title": "Lista de salas",
"more_options": {
- "copy_link": "Copiar link da sala",
- "favourited": "Favoritado",
- "leave_room": "Sair da sala",
- "low_priority": "Baixa prioridade",
- "mark_read": "Marcar como lido",
- "mark_unread": "Marcar como não lido"
+ "leave_room": "Sair da sala"
},
"notification_options": "Alterar notificações",
- "primary_filters": "Filtros da lista de salas",
"redacting_messages_status": {
"one": "Atualmente removendo mensagens em %(count)s sala",
"other": "Atualmente removendo mensagens em %(count)s salas"
},
- "room": {
- "more_options": "Mais opções",
- "open_room": "Abrir sala %(roomName)s"
- },
"show_less": "Mostrar menos",
"show_n_more": {
"other": "Mostrar %(count)s a mais",
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 20c66a6333..e042f8f940 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -12,18 +12,6 @@
"one": "1 непрочитанное упоминание."
},
"recent_rooms": "Недавние комнаты",
- "room_messsage_not_sent": "Открыть комнату %(roomName)s с неотправленным сообщением.",
- "room_n_unread_invite": "Открыть приглашение в комнату %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Открыть комнату %(roomName)s с 1 непрочитанным сообщением.",
- "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями.",
- "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями."
- },
- "room_n_unread_messages_mentions": {
- "one": "Открыть комнату %(roomName)s с 1 непрочитанным упоминанием.",
- "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями.",
- "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями."
- },
"room_name": "Комната %(name)s",
"room_status_bar": "Строка состояния комнаты",
"seek_bar_label": "Панель поиска аудио",
@@ -1716,7 +1704,6 @@
"class_global": "Глобально",
"class_other": "Другие",
"default": "По умолчанию",
- "default_settings": "Соответствует настройкам по умолчанию",
"email_pusher_app_display_name": "Уведомления по электронной почте",
"enable_prompt_toast_description": "Включить уведомления на рабочем столе",
"enable_prompt_toast_title": "Уведомления",
@@ -1735,8 +1722,7 @@
"mentions_and_keywords_description": "Получать уведомления только по упоминаниям и ключевым словам, установленным в ваших настройках",
"mentions_keywords": "Упоминания и ключевые слова",
"message_didnt_send": "Сообщение не отправлено. Нажмите для получения информации.",
- "mute_description": "Вы не будете получать никаких уведомлений",
- "mute_room": "Заглушить комнату"
+ "mute_description": "Вы не будете получать никаких уведомлений"
},
"notifier": {
"m.key.verification.request": "%(name)s запрашивает проверку"
@@ -2147,37 +2133,9 @@
"add_space_label": "Добавить пространство",
"breadcrumbs_empty": "Нет недавно посещенных комнат",
"breadcrumbs_label": "Недавно посещённые комнаты",
- "collapse_filters": "Свернуть список фильтров",
- "empty": {
- "no_chats": "Пока нет доступных чатов",
- "no_chats_description": "Начните с отправки сообщений или создания комнаты",
- "no_chats_description_no_room_rights": "Начните переписку с отправки сообщения",
- "no_favourites": "У вас пока нет чатов в Избранное",
- "no_favourites_description": "Вы можете добавить в Избранное в настройках чата",
- "no_invites": "У вас нет непрочитанных приглашений",
- "no_lowpriority": "У вас нет комнат с низким приоритетом",
- "no_mentions": "У вас нет непрочитанных упоминаний",
- "no_people": "У вас пока нет личных чатов",
- "no_people_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты",
- "no_rooms": "Вы еще не находитесь ни в одной комнате",
- "no_rooms_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты",
- "no_unread": "Поздравляю! У вас нет непрочитанных сообщений",
- "show_activity": "Посмотреть всю активность",
- "show_chats": "Показать все чаты"
- },
- "expand_filters": "Развернуть список фильтров",
"failed_add_tag": "Не удалось добавить тег %(tagName)s в комнату",
"failed_remove_tag": "Не удалось удалить тег %(tagName)s из комнаты",
"failed_set_dm_tag": "Не удалось установить метку личного сообщения",
- "filters": {
- "favourite": "Избранное",
- "invites": "Приглашения",
- "low_priority": "Низкий приоритет",
- "mentions": "Упоминания",
- "people": "Люди",
- "rooms": "Комнаты",
- "unread": "Непрочитанные"
- },
"home_menu_label": "Параметры раздела \"Главная\"",
"join_public_room_label": "Присоединиться к публичной комнате",
"joining_rooms_status": {
@@ -2186,23 +2144,13 @@
},
"list_title": "Список комнат",
"more_options": {
- "copy_link": "Скопировать ссылку на комнату",
- "favourited": "Избранное",
- "leave_room": "Покинуть комнату",
- "low_priority": "Низкий приоритет",
- "mark_read": "Отметить как прочитанное",
- "mark_unread": "Отметить как непрочитанное"
+ "leave_room": "Покинуть комнату"
},
"notification_options": "Настройки уведомлений",
- "primary_filters": "Фильтры комнат",
"redacting_messages_status": {
"one": "Удаляются сообщения в %(count)s комнате",
"other": "Удаляются сообщения в %(count)s комнатах"
},
- "room": {
- "more_options": "Дополнительные параметры",
- "open_room": "Открыть комнату %(roomName)s"
- },
"show_less": "Показать меньше",
"show_n_more": {
"other": "Показать ещё %(count)s",
diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
index 9717f39a28..ccf06da08e 100644
--- a/src/i18n/strings/sk.json
+++ b/src/i18n/strings/sk.json
@@ -14,18 +14,6 @@
"other": "%(count)s neprečítaných správ vrátane zmienok."
},
"recent_rooms": "Nedávne miestnosti",
- "room_messsage_not_sent": "Otvoriť miestnosť %(roomName)s s neodoslanou správou.",
- "room_n_unread_invite": "Otvoriť pozvánku miestnosti %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Otvoriť miestnosť %(roomName)s s 1 neprečítanou správou.",
- "few": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami.",
- "other": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami."
- },
- "room_n_unread_messages_mentions": {
- "one": "Otvoriť miestnosť %(roomName)s s 1 neprečítanou zmienkou.",
- "few": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami vrátane zmienok.",
- "other": "Otvoriť miestnosť %(roomName)s s %(count)s neprečítanými správami vrátane zmienok."
- },
"room_name": "Miestnosť %(name)s",
"room_status_bar": "Stavový riadok miestnosti",
"seek_bar_label": "Panel vyhľadávania zvuku",
@@ -1734,7 +1722,6 @@
"class_global": "Celosystémové",
"class_other": "Ďalšie",
"default": "Predvolené",
- "default_settings": "Zhoda s predvolenými nastaveniami",
"email_pusher_app_display_name": "Emailové oznámenia",
"enable_prompt_toast_description": "Povoliť oznámenia na ploche",
"enable_prompt_toast_title": "Oznámenia",
@@ -1753,8 +1740,7 @@
"mentions_and_keywords_description": "Dostávajte upozornenia len na zmienky a kľúčové slová nastavené vo vašich nastaveniach",
"mentions_keywords": "Zmienky a kľúčové slová",
"message_didnt_send": "Správa sa neodoslala. Kliknite pre informácie.",
- "mute_description": "Nebudete dostávať žiadne oznámenia",
- "mute_room": "Stlmiť miestnosť"
+ "mute_description": "Nebudete dostávať žiadne oznámenia"
},
"notifier": {
"m.key.verification.request": "%(name)s žiada o overenie"
@@ -2184,37 +2170,9 @@
"add_space_label": "Pridať priestor",
"breadcrumbs_empty": "Žiadne nedávno navštívené miestnosti",
"breadcrumbs_label": "Nedávno navštívené miestnosti",
- "collapse_filters": "Zbaliť zoznam filtrov",
- "empty": {
- "no_chats": "Zatiaľ žiadne konverzácie",
- "no_chats_description": "Začnite tým, že niekomu napíšete správu alebo vytvoríte miestnosť",
- "no_chats_description_no_room_rights": "Začnite tým, že niekomu napíšete správu",
- "no_favourites": "Zatiaľ nemáte obľúbenú konverzáciu",
- "no_favourites_description": "V nastaveniach konverzácií môžete pridať konverzáciu medzi obľúbené",
- "no_invites": "Nemáte žiadne neprečítané pozvánky",
- "no_lowpriority": "Nemáte žiadne miestnosti s nízkou prioritou",
- "no_mentions": "Nemáte žiadne neprečítané zmienky",
- "no_people": "Zatiaľ s nikým nemáte priame konverzácie",
- "no_people_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie",
- "no_rooms": "Zatiaľ ešte nie ste v žiadnej miestnosti",
- "no_rooms_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie",
- "no_unread": "Gratulujeme! Nemáte žiadne neprečítané správy",
- "show_activity": "Zobraziť všetku aktivitu",
- "show_chats": "Zobraziť všetky konverzácie"
- },
- "expand_filters": "Rozbaliť zoznam filtrov",
"failed_add_tag": "Miestnosti sa nepodarilo pridať značku %(tagName)s",
"failed_remove_tag": "Z miestnosti sa nepodarilo odstrániť značku %(tagName)s",
"failed_set_dm_tag": "Nepodarilo sa nastaviť značku priamej správy",
- "filters": {
- "favourite": "Obľúbené",
- "invites": "Pozvánky",
- "low_priority": "Nízka priorita",
- "mentions": "Zmienky",
- "people": "Ľudia",
- "rooms": "Miestnosti",
- "unread": "Neprečítané"
- },
"home_menu_label": "Možnosti domovskej obrazovky",
"join_public_room_label": "Pripojiť sa k verejnej miestnosti",
"joining_rooms_status": {
@@ -2224,24 +2182,14 @@
},
"list_title": "Zoznam miestností",
"more_options": {
- "copy_link": "Kopírovať odkaz na miestnosť",
- "favourited": "Obľúbené",
- "leave_room": "Opustiť miestnosť",
- "low_priority": "Nízka priorita",
- "mark_read": "Označiť ako prečítané",
- "mark_unread": "Označiť ako neprečítané"
+ "leave_room": "Opustiť miestnosť"
},
"notification_options": "Možnosti oznámenia",
- "primary_filters": "Filtre zoznamu miestností",
"redacting_messages_status": {
"one": "V súčasnosti sa odstraňujú správy v %(count)s miestnosti",
"few": "V súčasnosti sa odstraňujú správy v %(count)s miestnostiach",
"other": "V súčasnosti sa odstraňujú správy v %(count)s miestnostiach"
},
- "room": {
- "more_options": "Viac možností",
- "open_room": "Otvoriť miestnosť %(roomName)s"
- },
"show_less": "Zobraziť menej",
"show_n_more": {
"one": "Zobraziť %(count)s ďalšiu",
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index ede4e07e1f..fbbd7563a0 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -12,16 +12,6 @@
"one": "1 oläst omnämnande."
},
"recent_rooms": "Nyliga rum",
- "room_messsage_not_sent": "Öppna rummet %(roomName)s med ett osänt meddelande.",
- "room_n_unread_invite": "Öppna inbjudan till rummet %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Öppna rummet %(roomName)s med 1 oläst meddelande.",
- "other": "Öppna rummet %(roomName)s med %(count)s olästa meddelanden."
- },
- "room_n_unread_messages_mentions": {
- "one": "Öppna rummet %(roomName)s med 1 oläst omnämnande.",
- "other": "Öppna rummet %(roomName)s med %(count)s olästa meddelanden inklusive omnämnanden."
- },
"room_name": "Rum %(name)s",
"room_status_bar": "Rumsstatusfält",
"seek_bar_label": "Förloppsfält för ljud",
@@ -1658,7 +1648,6 @@
"class_global": "Globalt",
"class_other": "Annat",
"default": "Standard",
- "default_settings": "Matcha standardinställningar",
"email_pusher_app_display_name": "E-postaviseringar",
"enable_prompt_toast_description": "Aktivera skrivbordsaviseringar",
"enable_prompt_toast_title": "Aviseringar",
@@ -1677,8 +1666,7 @@
"mentions_and_keywords_description": "Bli endast aviserad om omnämnanden och nyckelord i enlighet med dina inställningar",
"mentions_keywords": "Omnämnanden & nyckelord",
"message_didnt_send": "Meddelande skickades inte. Klicka för info.",
- "mute_description": "Du får inga aviseringar",
- "mute_room": "Tysta rum"
+ "mute_description": "Du får inga aviseringar"
},
"notifier": {
"m.key.verification.request": "%(name)s begär verifiering"
@@ -2075,37 +2063,9 @@
"add_space_label": "Lägg till utrymme",
"breadcrumbs_empty": "Inga nyligen besökta rum",
"breadcrumbs_label": "Nyligen besökta rum",
- "collapse_filters": "Kollapsa filterlista",
- "empty": {
- "no_chats": "Inga chattar än",
- "no_chats_description": "Kom igång genom att skicka meddelanden till någon eller genom att skapa ett rum",
- "no_chats_description_no_room_rights": "Kom igång genom att skicka meddelanden till någon",
- "no_favourites": "Du har ingen favoritchatt än",
- "no_favourites_description": "Du kan lägga till en chatt till dina favoriter i chattinställningarna",
- "no_invites": "Du har inga olästa inbjudningar",
- "no_lowpriority": "Du har inga lågprioriterade rum.",
- "no_mentions": "Du har inga olästa omnämnanden",
- "no_people": "Du har inte direktchattar med någon ännu",
- "no_people_description": "Du kan avmarkera filter för att se dina andra chattar",
- "no_rooms": "Du är inte i något rum än",
- "no_rooms_description": "Du kan avmarkera filter för att se dina andra chattar",
- "no_unread": "Grattis! Du har inga olästa meddelanden",
- "show_activity": "Visa all aktivitet",
- "show_chats": "Visa alla chattar"
- },
- "expand_filters": "Expandera filterlista",
"failed_add_tag": "Misslyckades att lägga till etiketten %(tagName)s till rummet",
"failed_remove_tag": "Misslyckades att radera etiketten %(tagName)s från rummet",
"failed_set_dm_tag": "Misslyckades att sätta direktmeddelandetagg",
- "filters": {
- "favourite": "Favoriter",
- "invites": "Inbjudningar",
- "low_priority": "Låg prioritet",
- "mentions": "Omnämnanden",
- "people": "Personer",
- "rooms": "Rum",
- "unread": "Olästa"
- },
"home_menu_label": "Hemalternativ",
"join_public_room_label": "Gå med i offentligt rum",
"joining_rooms_status": {
@@ -2114,23 +2074,13 @@
},
"list_title": "Rumslista",
"more_options": {
- "copy_link": "Kopiera rumslänk",
- "favourited": "Favoritmarkerad",
- "leave_room": "Lämna rum",
- "low_priority": "Låg prioritet",
- "mark_read": "Markera som läst",
- "mark_unread": "Markera som oläst"
+ "leave_room": "Lämna rum"
},
"notification_options": "Aviseringsinställningar",
- "primary_filters": "Filter för rumslista",
"redacting_messages_status": {
"one": "Tar just nu bort meddelanden i %(count)s rum",
"other": "Tar just nu bort meddelanden i %(count)s rum"
},
- "room": {
- "more_options": "Fler alternativ",
- "open_room": "Öppet rummet %(roomName)s"
- },
"show_less": "Visa mindre",
"show_n_more": {
"other": "Visa %(count)s till",
diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json
index e4b6fcca76..e5a9548339 100644
--- a/src/i18n/strings/tr.json
+++ b/src/i18n/strings/tr.json
@@ -2005,12 +2005,6 @@
"failed_add_tag": "%(tagName)s etiketi odaya eklenemedi",
"failed_remove_tag": "Odadan %(tagName)s etiketi kaldırılamadı",
"failed_set_dm_tag": "Doğrudan mesaj etiketi ayarlanamadı",
- "filters": {
- "favourite": "Favoriler",
- "people": "Kişiler",
- "rooms": "Odalar",
- "unread": "Okunmamış"
- },
"home_menu_label": "Ana sayfa seçenekleri",
"join_public_room_label": "Herkese açık odaya katıl",
"joining_rooms_status": {
@@ -2019,14 +2013,10 @@
},
"list_title": "Oda listesi",
"notification_options": "Bildirim ayarları",
- "primary_filters": "Oda listesi filtreleri",
"redacting_messages_status": {
"one": "Şu anda %(count)s odadaki mesajlar kaldırılıyor",
"other": "Şu anda %(count)s odadaki mesajlar kaldırılıyor"
},
- "room": {
- "open_room": "Açık oda %(roomName)s"
- },
"show_less": "Daha az göster",
"show_n_more": {
"one": "%(count)s adet daha fazla göster",
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 641f6dbe4b..08b4d8b2d1 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -12,18 +12,6 @@
"one": "1 непрочитана згадка."
},
"recent_rooms": "Недавні кімнати",
- "room_messsage_not_sent": "Відкрити кімнату %(roomName)s з не надісланим повідомленням.",
- "room_n_unread_invite": "Відкрити запрошення кімнати %(roomName)s.",
- "room_n_unread_messages": {
- "one": "Відкрити кімнату %(roomName)s з 1 непрочитаним повідомленням.",
- "few": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними повідомленнями.",
- "many": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними повідомленнями."
- },
- "room_n_unread_messages_mentions": {
- "one": "Відкрити кімнату %(roomName)s з 1 непрочитаною згадкою.",
- "few": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними згадками.",
- "many": "Відкрити кімнату %(roomName)s з %(count)s непрочитаними згадками."
- },
"room_name": "Кімната %(name)s",
"room_status_bar": "Панель стану кімнати",
"seek_bar_label": "Панель гортання аудіо",
@@ -1715,7 +1703,6 @@
"class_global": "Глобально",
"class_other": "Інше",
"default": "Типовий",
- "default_settings": "Згідно з усталеними налаштуваннями",
"email_pusher_app_display_name": "Сповіщення е-поштою",
"enable_prompt_toast_description": "Увімкнути сповіщення стільниці",
"enable_prompt_toast_title": "Сповіщення",
@@ -1734,8 +1721,7 @@
"mentions_and_keywords_description": "Отримувати лише вказані у ваших налаштуваннях згадки й ключові слова",
"mentions_keywords": "Згадки та ключові слова",
"message_didnt_send": "Повідомлення не надіслане. Натисніть, щоб дізнатись більше.",
- "mute_description": "Ви не отримуватимете жодних сповіщень",
- "mute_room": "Вимкнути сповіщення кімнати"
+ "mute_description": "Ви не отримуватимете жодних сповіщень"
},
"notifier": {
"m.key.verification.request": "%(name)s робить запит на звірення"
@@ -2151,37 +2137,9 @@
"add_space_label": "Додати простір",
"breadcrumbs_empty": "Немає недавно відвіданих кімнат",
"breadcrumbs_label": "Недавно відвідані кімнати",
- "collapse_filters": "Згорнути список фільтрів",
- "empty": {
- "no_chats": "Ще немає бесід",
- "no_chats_description": "Почніть користування, надіславши комусь повідомлення або створивши кімнату",
- "no_chats_description_no_room_rights": "Розпочніть користування, написавши комусь повідомлення",
- "no_favourites": "У вас ще немає обраних бесід",
- "no_favourites_description": "Ви можете додати бесіду до обраних у її налаштуваннях",
- "no_invites": "У вас немає непрочитаних запрошень",
- "no_lowpriority": "У вас немає неважливих кімнат",
- "no_mentions": "У вас немає непрочитаних згадок",
- "no_people": "У вас ще немає особистих бесід",
- "no_people_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди",
- "no_rooms": "Ви ще не входили до кімнат",
- "no_rooms_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди",
- "no_unread": "Вітаємо! У вас немає непрочитаних повідомлень",
- "show_activity": "Переглянути всю діяльність",
- "show_chats": "Показати всі бесіди"
- },
- "expand_filters": "Розгорнути список фільтрів",
"failed_add_tag": "Не вдалось додати до кімнати мітку %(tagName)s",
"failed_remove_tag": "Не вдалося прибрати з кімнати мітку %(tagName)s",
"failed_set_dm_tag": "Не вдалося встановити мітку особистого повідомлення",
- "filters": {
- "favourite": "Обрані",
- "invites": "Запрошення",
- "low_priority": "Неважливі",
- "mentions": "Згадування",
- "people": "Люди",
- "rooms": "Кімнати",
- "unread": "Непрочитані"
- },
"home_menu_label": "Параметри домівки",
"join_public_room_label": "Приєднатись до загальнодоступної кімнати",
"joining_rooms_status": {
@@ -2190,23 +2148,13 @@
},
"list_title": "Список кімнат",
"more_options": {
- "copy_link": "Копіювати посилання на кімнату",
- "favourited": "Обране",
- "leave_room": "Вийти з кімнати",
- "low_priority": "Неважливі",
- "mark_read": "Позначити прочитаним",
- "mark_unread": "Позначити непрочитаним"
+ "leave_room": "Вийти з кімнати"
},
"notification_options": "Параметри сповіщень",
- "primary_filters": "Фільтри списку кімнат",
"redacting_messages_status": {
"one": "Триває видалення повідомлень в %(count)s кімнаті",
"other": "Триває видалення повідомлень у %(count)s кімнатах"
},
- "room": {
- "more_options": "Інші опції",
- "open_room": "Відкрити кімнату %(roomName)s"
- },
"show_less": "Згорнути",
"show_n_more": {
"other": "Показати ще %(count)s",
diff --git a/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/src/viewmodels/room-list/RoomListHeaderViewModel.ts
index fee0c954c3..e99268190c 100644
--- a/src/viewmodels/room-list/RoomListHeaderViewModel.ts
+++ b/src/viewmodels/room-list/RoomListHeaderViewModel.ts
@@ -26,11 +26,11 @@ import {
showSpaceSettings,
} from "../../utils/space";
import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
-import { createRoom, hasCreateRoomRights } from "../../components/viewmodels/roomlist/utils";
import SettingsStore from "../../settings/SettingsStore";
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters";
import { SettingLevel } from "../../settings/SettingLevel";
+import { createRoom, hasCreateRoomRights } from "./utils";
export interface Props {
/**
diff --git a/src/viewmodels/room-list/RoomListItemViewModel.ts b/src/viewmodels/room-list/RoomListItemViewModel.ts
new file mode 100644
index 0000000000..d7ce4e6e7f
--- /dev/null
+++ b/src/viewmodels/room-list/RoomListItemViewModel.ts
@@ -0,0 +1,327 @@
+/*
+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 {
+ BaseViewModel,
+ RoomNotifState,
+ type RoomListItemSnapshot,
+ type RoomListItemActions,
+} from "@element-hq/web-shared-components";
+import { RoomEvent } from "matrix-js-sdk/src/matrix";
+import { CallType } from "matrix-js-sdk/src/webrtc/call";
+
+import type { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
+import type { RoomNotificationState } from "../../stores/notifications/RoomNotificationState";
+import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
+import { NotificationStateEvents } from "../../stores/notifications/NotificationState";
+import { MessagePreviewStore } from "../../stores/room-list/MessagePreviewStore";
+import { UPDATE_EVENT } from "../../stores/AsyncStore";
+import { DefaultTagID } from "../../stores/room-list/models";
+import DMRoomMap from "../../utils/DMRoomMap";
+import SettingsStore from "../../settings/SettingsStore";
+import { NotificationLevel } from "../../stores/notifications/NotificationLevel";
+import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils";
+import { EchoChamber } from "../../stores/local-echo/EchoChamber";
+import { RoomNotifState as ElementRoomNotifState } from "../../RoomNotifs";
+import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
+import { UIComponent } from "../../settings/UIFeature";
+import { CallStore, CallStoreEvent } from "../../stores/CallStore";
+import { clearRoomNotification, setMarkedUnreadState } from "../../utils/notifications";
+import { tagRoom } from "../../utils/room/tagRoom";
+import dispatcher from "../../dispatcher/dispatcher";
+import { Action } from "../../dispatcher/actions";
+import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
+import PosthogTrackers from "../../PosthogTrackers";
+
+interface RoomItemProps {
+ room: Room;
+ client: MatrixClient;
+}
+
+/**
+ * View model for an individual room list item.
+ * Manages per-room subscriptions and updates only when this specific room's data changes.
+ * Implements RoomListItemActions to provide interaction callbacks.
+ */
+export class RoomListItemViewModel
+ extends BaseViewModel
+ implements RoomListItemActions
+{
+ private notifState: RoomNotificationState;
+
+ public constructor(props: RoomItemProps) {
+ // Get notification state first so we can generate a complete initial snapshot
+ const notifState = RoomNotificationStateStore.instance.getRoomState(props.room);
+ const initialItem = RoomListItemViewModel.generateItemSync(props.room, props.client, notifState);
+ super(props, initialItem);
+
+ this.notifState = notifState;
+
+ // Subscribe to notification state changes for this room
+ this.disposables.trackListener(this.notifState, NotificationStateEvents.Update, this.onNotificationChanged);
+
+ // Subscribe to message preview changes (will filter to this room)
+ this.disposables.trackListener(MessagePreviewStore.instance, UPDATE_EVENT, this.onMessagePreviewChanged);
+
+ // Subscribe to settings changes for message preview toggle
+ const settingsWatchRef = SettingsStore.watchSetting(
+ "RoomList.showMessagePreview",
+ null,
+ this.onMessagePreviewSettingChanged,
+ );
+ this.disposables.track(() => {
+ SettingsStore.unwatchSetting(settingsWatchRef);
+ });
+
+ // Subscribe to call state changes
+ this.disposables.trackListener(CallStore.instance, CallStoreEvent.ConnectedCalls, this.onCallStateChanged);
+
+ // Subscribe to room-specific events
+ this.disposables.trackListener(props.room, RoomEvent.Name, this.onRoomChanged);
+ this.disposables.trackListener(props.room, RoomEvent.Tags, this.onRoomChanged);
+
+ // Load message preview asynchronously (sync data is already complete)
+ void this.loadAndSetMessagePreview();
+ }
+
+ private onNotificationChanged = (): void => {
+ this.updateItem();
+ };
+
+ private onMessagePreviewChanged = (): void => {
+ void this.loadAndSetMessagePreview();
+ };
+
+ private onMessagePreviewSettingChanged = (): void => {
+ void this.loadAndSetMessagePreview();
+ };
+
+ private onCallStateChanged = (): void => {
+ // Only update if call state for this room actually changed
+ const call = CallStore.instance.getCall(this.props.room.roomId);
+ const currentCallType = this.snapshot.current.notification.callType;
+ const newCallType =
+ call && call.participants.size > 0 ? (call.callType === CallType.Voice ? "voice" : "video") : undefined;
+
+ if (currentCallType !== newCallType) {
+ this.updateItem();
+ }
+ };
+
+ private onRoomChanged = (): void => {
+ this.updateItem();
+ };
+
+ /**
+ * Update the item snapshot with current sync data.
+ * Preserves the message preview which is managed separately.
+ */
+ private updateItem(): void {
+ const newItem = RoomListItemViewModel.generateItemSync(this.props.room, this.props.client, this.notifState);
+ // Preserve message preview - it's managed separately by loadAndSetMessagePreview
+ this.snapshot.set({ ...newItem, messagePreview: this.snapshot.current.messagePreview });
+ }
+
+ private getMessagePreviewTag(): string {
+ const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId));
+ return isDm ? DefaultTagID.DM : DefaultTagID.Untagged;
+ }
+
+ /**
+ * Load the message preview for this room if enabled.
+ * Returns undefined if previews are disabled or couldn't be loaded.
+ */
+ private async loadMessagePreview(): Promise {
+ const shouldShowMessagePreview = SettingsStore.getValue("RoomList.showMessagePreview");
+ if (!shouldShowMessagePreview) {
+ return undefined;
+ }
+
+ const messagePreviewTag = this.getMessagePreviewTag();
+ const preview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, messagePreviewTag);
+ return preview?.text;
+ }
+
+ /**
+ * Load and set the message preview if it differs from current.
+ */
+ private async loadAndSetMessagePreview(): Promise {
+ const messagePreview = await this.loadMessagePreview();
+ if (messagePreview !== this.snapshot.current.messagePreview) {
+ this.snapshot.merge({ messagePreview });
+ }
+ }
+
+ /**
+ * Generate a complete RoomListItem with all synchronous data.
+ * Message preview is loaded separately to avoid blocking initial render.
+ */
+ private static generateItemSync(
+ room: Room,
+ client: MatrixClient,
+ notifState: RoomNotificationState,
+ ): RoomListItemSnapshot {
+ // Get room tags for menu state
+ const roomTags = room.tags;
+ const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
+
+ // Message preview will be loaded asynchronously and updated separately
+ const messagePreview = undefined;
+
+ const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]);
+ const isLowPriority = Boolean(roomTags[DefaultTagID.LowPriority]);
+ const isArchived = Boolean(roomTags[DefaultTagID.Archived]);
+
+ // More options menu state
+ const showMoreOptionsMenu = hasAccessToOptionsMenu(room);
+ const showNotificationMenu = hasAccessToNotificationMenu(room, client.isGuest(), isArchived);
+
+ // Notification levels
+ const canMarkAsRead = notifState.level > NotificationLevel.None;
+ const canMarkAsUnread = !canMarkAsRead && !isArchived;
+
+ const canInvite = room.canInvite(client.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers);
+ const canCopyRoomLink = !isDm;
+
+ // Get the current room notification state from EchoChamber
+ const echoChamber = EchoChamber.forRoom(room);
+ const elementRoomNotifState = echoChamber.notificationVolume;
+
+ // Convert element-web RoomNotifState to shared-components RoomNotifState
+ let roomNotifState: RoomNotifState;
+ switch (elementRoomNotifState) {
+ case ElementRoomNotifState.AllMessages:
+ roomNotifState = RoomNotifState.AllMessages;
+ break;
+ case ElementRoomNotifState.AllMessagesLoud:
+ roomNotifState = RoomNotifState.AllMessagesLoud;
+ break;
+ case ElementRoomNotifState.MentionsOnly:
+ roomNotifState = RoomNotifState.MentionsOnly;
+ break;
+ case ElementRoomNotifState.Mute:
+ roomNotifState = RoomNotifState.Mute;
+ break;
+ default:
+ roomNotifState = RoomNotifState.AllMessages;
+ }
+
+ const isNotificationMute = elementRoomNotifState === ElementRoomNotifState.Mute;
+
+ // Video room and call state tracking
+ const call = CallStore.instance.getCall(room.roomId);
+ const participantCount = call?.participants.size ?? 0;
+ const hasParticipantsInCall = participantCount > 0;
+ const callType =
+ call?.callType === CallType.Voice ? "voice" : call?.callType === CallType.Video ? "video" : undefined;
+
+ return {
+ id: room.roomId,
+ room,
+ name: room.name,
+ isBold: notifState.hasAnyNotificationOrActivity,
+ messagePreview,
+ notification: {
+ hasAnyNotificationOrActivity: notifState.hasAnyNotificationOrActivity || hasParticipantsInCall,
+ isUnsentMessage: notifState.isUnsentMessage,
+ invited: notifState.invited,
+ isMention: notifState.isMention,
+ isActivityNotification: notifState.isActivityNotification,
+ isNotification: notifState.isNotification,
+ hasUnreadCount: notifState.hasUnreadCount,
+ count: notifState.count,
+ muted: isNotificationMute,
+ callType: hasParticipantsInCall ? callType : undefined,
+ },
+ showMoreOptionsMenu,
+ showNotificationMenu,
+ isFavourite,
+ isLowPriority,
+ canInvite,
+ canCopyRoomLink,
+ canMarkAsRead,
+ canMarkAsUnread,
+ roomNotifState,
+ };
+ }
+
+ public onOpenRoom = (): void => {
+ dispatcher.dispatch({
+ action: Action.ViewRoom,
+ room_id: this.props.room.roomId,
+ metricsTrigger: "RoomList",
+ });
+ };
+
+ public onMarkAsRead = async (): Promise => {
+ await clearRoomNotification(this.props.room, this.props.client);
+ PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead");
+ };
+
+ public onMarkAsUnread = async (): Promise => {
+ await setMarkedUnreadState(this.props.room, this.props.client, true);
+ PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread");
+ };
+
+ public onToggleFavorite = (): void => {
+ tagRoom(this.props.room, DefaultTagID.Favourite);
+ PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle");
+ };
+
+ public onToggleLowPriority = (): void => {
+ tagRoom(this.props.room, DefaultTagID.LowPriority);
+ };
+
+ public onInvite = (): void => {
+ dispatcher.dispatch({
+ action: "view_invite",
+ roomId: this.props.room.roomId,
+ });
+ PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem");
+ };
+
+ public onCopyRoomLink = (): void => {
+ dispatcher.dispatch({
+ action: "copy_room",
+ room_id: this.props.room.roomId,
+ });
+ };
+
+ public onLeaveRoom = (): void => {
+ const isArchived = Boolean(this.props.room.tags[DefaultTagID.Archived]);
+ dispatcher.dispatch({
+ action: isArchived ? "forget_room" : "leave_room",
+ room_id: this.props.room.roomId,
+ });
+ PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem");
+ };
+
+ public onSetRoomNotifState = (notifState: RoomNotifState): void => {
+ // Convert shared-components RoomNotifState to element-web RoomNotifState
+ let elementNotifState: ElementRoomNotifState;
+ switch (notifState) {
+ case "all_messages":
+ elementNotifState = ElementRoomNotifState.AllMessages;
+ break;
+ case "all_messages_loud":
+ elementNotifState = ElementRoomNotifState.AllMessagesLoud;
+ break;
+ case "mentions_only":
+ elementNotifState = ElementRoomNotifState.MentionsOnly;
+ break;
+ case "mute":
+ elementNotifState = ElementRoomNotifState.Mute;
+ break;
+ default:
+ elementNotifState = ElementRoomNotifState.AllMessages;
+ }
+
+ // Set the notification state using EchoChamber
+ const echoChamber = EchoChamber.forRoom(this.props.room);
+ echoChamber.notificationVolume = elementNotifState;
+ };
+}
diff --git a/src/viewmodels/room-list/RoomListViewViewModel.ts b/src/viewmodels/room-list/RoomListViewViewModel.ts
new file mode 100644
index 0000000000..a3618b93af
--- /dev/null
+++ b/src/viewmodels/room-list/RoomListViewViewModel.ts
@@ -0,0 +1,450 @@
+/*
+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 {
+ BaseViewModel,
+ type RoomListSnapshot,
+ type FilterId,
+ type RoomListViewActions,
+ type RoomListViewState,
+} from "@element-hq/web-shared-components";
+import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
+
+import { Action } from "../../dispatcher/actions";
+import dispatcher from "../../dispatcher/dispatcher";
+import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
+import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
+import SpaceStore from "../../stores/spaces/SpaceStore";
+import RoomListStoreV3, { RoomListStoreV3Event, type RoomsResult } from "../../stores/room-list-v3/RoomListStoreV3";
+import { FilterKey } from "../../stores/room-list-v3/skip-list/filters";
+import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
+import { RoomListItemViewModel } from "./RoomListItemViewModel";
+import { SdkContextClass } from "../../contexts/SDKContext";
+import { hasCreateRoomRights } from "./utils";
+
+interface RoomListViewViewModelProps {
+ client: MatrixClient;
+}
+
+const filterKeyToIdMap: Map = new Map([
+ [FilterKey.UnreadFilter, "unread"],
+ [FilterKey.PeopleFilter, "people"],
+ [FilterKey.RoomsFilter, "rooms"],
+ [FilterKey.FavouriteFilter, "favourite"],
+ [FilterKey.MentionsFilter, "mentions"],
+ [FilterKey.InvitesFilter, "invites"],
+ [FilterKey.LowPriorityFilter, "low_priority"],
+]);
+
+export class RoomListViewViewModel
+ extends BaseViewModel
+ implements RoomListViewActions
+{
+ // State tracking
+ private activeFilter: FilterKey | undefined = undefined;
+ private roomsResult: RoomsResult;
+ private lastActiveRoomIndex: number | undefined = undefined;
+
+ // Child view model management
+ private roomItemViewModels = new Map();
+ private roomsMap = new Map();
+
+ public constructor(props: RoomListViewViewModelProps) {
+ const activeSpace = SpaceStore.instance.activeSpaceRoom;
+
+ // Get initial rooms
+ const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined);
+ const canCreateRoom = hasCreateRoomRights(props.client, activeSpace);
+ const filterIds = [...filterKeyToIdMap.values()];
+
+ super(props, {
+ // Initial view state - start with empty, will populate in async init
+ isLoadingRooms: RoomListStoreV3.instance.isLoadingRooms,
+ isRoomListEmpty: roomsResult.rooms.length === 0,
+ filterIds,
+ activeFilterId: undefined,
+ roomListState: {
+ activeRoomIndex: undefined,
+ spaceId: roomsResult.spaceId,
+ filterKeys: undefined,
+ },
+ roomIds: roomsResult.rooms.map((room) => room.roomId),
+ canCreateRoom,
+ });
+
+ this.roomsResult = roomsResult;
+
+ // Build initial roomsMap from roomsResult
+ this.updateRoomsMap(roomsResult);
+
+ // Subscribe to room list updates
+ this.disposables.trackListener(
+ RoomListStoreV3.instance,
+ RoomListStoreV3Event.ListsUpdate as any,
+ this.onListsUpdate,
+ );
+
+ // Subscribe to room list loaded
+ this.disposables.trackListener(
+ RoomListStoreV3.instance,
+ RoomListStoreV3Event.ListsLoaded as any,
+ this.onListsLoaded,
+ );
+
+ // Subscribe to active room changes to update selected room
+ const dispatcherRef = dispatcher.register(this.onDispatch);
+ this.disposables.track(() => {
+ dispatcher.unregister(dispatcherRef);
+ });
+
+ // Track cleanup of all child view models
+ this.disposables.track(() => {
+ for (const viewModel of this.roomItemViewModels.values()) {
+ viewModel.dispose();
+ }
+ this.roomItemViewModels.clear();
+ });
+ }
+
+ public onToggleFilter = (filterId: FilterId): void => {
+ // Find the FilterKey by matching the filter ID
+ let filterKey: FilterKey | undefined = undefined;
+ for (const [key, id] of filterKeyToIdMap.entries()) {
+ if (id === filterId) {
+ filterKey = key;
+ break;
+ }
+ }
+
+ if (filterKey === undefined) return;
+
+ // Toggle the filter - if it's already active, deactivate it
+ const newFilter = this.activeFilter === filterKey ? undefined : filterKey;
+ this.activeFilter = newFilter;
+
+ // Update rooms result with new filter
+ const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined;
+ this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys);
+
+ // Update roomsMap immediately before clearing VMs
+ this.updateRoomsMap(this.roomsResult);
+
+ // Clear view models since room list changed
+ this.clearViewModels();
+
+ this.updateRoomListData();
+ };
+
+ /**
+ * Rebuild roomsMap when roomsResult changes.
+ * This maintains a quick lookup for room objects.
+ */
+ private updateRoomsMap(roomsResult: RoomsResult): void {
+ this.roomsMap.clear();
+ for (const room of roomsResult.rooms) {
+ this.roomsMap.set(room.roomId, room);
+ }
+ }
+
+ /**
+ * Clear all child view models.
+ * Called when the room list structure changes (space change, filter change, etc.)
+ */
+ private clearViewModels(): void {
+ for (const viewModel of this.roomItemViewModels.values()) {
+ viewModel.dispose();
+ }
+ this.roomItemViewModels.clear();
+ }
+
+ /**
+ * Get the ordered list of room IDs.
+ */
+ public get roomIds(): string[] {
+ return this.roomsResult.rooms.map((room) => room.roomId);
+ }
+
+ /**
+ * Get a RoomListItemViewModel for a specific room.
+ * Creates a RoomListItemViewModel if needed, which manages per-room subscriptions.
+ * The view should call this only for visible rooms from the roomIds list.
+ * @throws Error if room is not found in roomsMap (indicates a programming error)
+ */
+ public getRoomItemViewModel(roomId: string): RoomListItemViewModel {
+ // Check if we have a view model for this room
+ let viewModel = this.roomItemViewModels.get(roomId);
+
+ if (!viewModel) {
+ const room = this.roomsMap.get(roomId);
+ if (!room) {
+ throw new Error(`Room ${roomId} not found in roomsMap`);
+ }
+
+ // Create new view model
+ viewModel = new RoomListItemViewModel({
+ room,
+ client: this.props.client,
+ });
+
+ this.roomItemViewModels.set(roomId, viewModel);
+ }
+
+ // Return the view model - the view will call useViewModel() on it
+ return viewModel;
+ }
+
+ /**
+ * Update which rooms are currently visible.
+ * Called by the view when scroll position changes.
+ * Disposes of view models for rooms no longer visible.
+ */
+ public updateVisibleRooms(startIndex: number, endIndex: number): void {
+ const allRoomIds = this.roomIds;
+ const newVisibleIds = allRoomIds.slice(startIndex, Math.min(endIndex, allRoomIds.length));
+
+ const newVisibleSet = new Set(newVisibleIds);
+
+ // Dispose view models for rooms no longer visible
+ for (const [roomId, viewModel] of this.roomItemViewModels.entries()) {
+ if (!newVisibleSet.has(roomId)) {
+ viewModel.dispose();
+ this.roomItemViewModels.delete(roomId);
+ }
+ }
+ }
+
+ private onDispatch = (payload: any): void => {
+ if (payload.action === Action.ActiveRoomChanged) {
+ // When the active room changes, update the room list data to reflect the new selected room
+ // Pass isRoomChange=true so sticky logic doesn't prevent the index from updating
+ this.updateRoomListData(true);
+ } else if (payload.action === Action.ViewRoomDelta) {
+ // Handle keyboard navigation shortcuts (Alt+ArrowUp/Down)
+ // This was previously handled by useRoomListNavigation hook
+ this.handleViewRoomDelta(payload as ViewRoomDeltaPayload);
+ }
+ };
+
+ /**
+ * Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) to move between rooms.
+ * Supports both regular navigation and unread-only navigation.
+ * Migrated from useRoomListNavigation hook.
+ */
+ private handleViewRoomDelta(payload: ViewRoomDeltaPayload): void {
+ const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
+ if (!currentRoomId) return;
+
+ const { delta, unread } = payload;
+ const rooms = this.roomsResult.rooms;
+
+ const filteredRooms = unread
+ ? // Filter the rooms to only include unread ones and the active room
+ rooms.filter((room) => {
+ const state = RoomNotificationStateStore.instance.getRoomState(room);
+ return room.roomId === currentRoomId || state.isUnread;
+ })
+ : rooms;
+
+ const currentIndex = filteredRooms.findIndex((room) => room.roomId === currentRoomId);
+ if (currentIndex === -1) return;
+
+ // Get the next/previous new room according to the delta
+ // Use slice to loop on the list
+ // If delta is -1 at the start of the list, it will go to the end
+ // If delta is 1 at the end of the list, it will go to the start
+ const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length);
+ if (!newRoom) return;
+
+ dispatcher.dispatch({
+ action: Action.ViewRoom,
+ room_id: newRoom.roomId,
+ show_room_tile: true, // to make sure the room gets scrolled into view
+ metricsTrigger: "WebKeyboardShortcut",
+ metricsViaKeyboard: true,
+ });
+ }
+
+ /**
+ * Handle room list updates from RoomListStoreV3.
+ *
+ * This event fires when:
+ * - Room order changes (new messages, manual reordering)
+ * - Active space changes
+ * - Filters are applied
+ * - Rooms are added/removed
+ *
+ * Space changes are detected by comparing old vs new spaceId.
+ * This matches the old hook pattern where space changes were handled
+ * indirectly through room list updates.
+ */
+ private onListsUpdate = (): void => {
+ const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined;
+ const oldSpaceId = this.roomsResult.spaceId;
+
+ // Refresh room data from store
+ this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys);
+ this.updateRoomsMap(this.roomsResult);
+
+ const newSpaceId = this.roomsResult.spaceId;
+
+ // Clear view models since room list structure changed
+ this.clearViewModels();
+
+ // Detect space change
+ if (oldSpaceId !== newSpaceId) {
+ // Space changed - get the last selected room for the new space to prevent flicker
+ const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
+
+ this.updateRoomListData(true, lastSelectedRoom);
+ return;
+ }
+
+ // Normal room list update (not a space change)
+ this.updateRoomListData();
+ };
+
+ private onListsLoaded = (): void => {
+ // Room lists have finished loading
+ this.snapshot.merge({
+ isLoadingRooms: false,
+ });
+ };
+
+ /**
+ * Calculate the active room index based on the currently viewed room.
+ * Returns undefined if no room is selected or if the selected room is not in the current list.
+ *
+ * @param roomId - The room ID to find the index for (can be null/undefined)
+ */
+ private getActiveRoomIndex(roomId: string | null | undefined): number | undefined {
+ if (!roomId) {
+ return undefined;
+ }
+
+ const index = this.roomsResult.rooms.findIndex((room) => room.roomId === roomId);
+ return index >= 0 ? index : undefined;
+ }
+
+ /**
+ * Apply sticky room logic to keep the active room at the same index position.
+ * When the room list updates, this prevents the selected room from jumping around in the UI.
+ *
+ * @param isRoomChange - Whether this update is due to a room change (not a list update)
+ * @param roomId - The room ID to apply sticky logic for (can be null/undefined)
+ * @returns The modified rooms array with sticky positioning applied
+ */
+ private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] {
+ const rooms = this.roomsResult.rooms;
+
+ if (!roomId) {
+ return rooms;
+ }
+
+ const newIndex = rooms.findIndex((room) => room.roomId === roomId);
+ const oldIndex = this.lastActiveRoomIndex;
+
+ // When opening another room, the index should obviously change
+ if (isRoomChange) {
+ return rooms;
+ }
+
+ // If oldIndex is undefined, then there was no active room before
+ // Similarly, if newIndex is -1, the active room is not in the current list
+ if (newIndex === -1 || oldIndex === undefined) {
+ return rooms;
+ }
+
+ // If the index hasn't changed, we have nothing to do
+ if (newIndex === oldIndex) {
+ return rooms;
+ }
+
+ // If the old index falls out of the bounds of the rooms array
+ // (usually because rooms were removed), we can no longer place
+ // the active room in the same old index
+ if (oldIndex > rooms.length - 1) {
+ return rooms;
+ }
+
+ // Making the active room sticky is as simple as removing it from
+ // its new index and placing it in the old index
+ const newRooms = [...rooms];
+ const [stickyRoom] = newRooms.splice(newIndex, 1);
+ newRooms.splice(oldIndex, 0, stickyRoom);
+
+ return newRooms;
+ }
+
+ private async updateRoomListData(
+ isRoomChange: boolean = false,
+ roomIdOverride: string | null = null,
+ ): Promise {
+ // Determine the room ID to use for calculations
+ // Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
+ const roomId = roomIdOverride ?? SdkContextClass.instance.roomViewStore.getRoomId();
+
+ // Apply sticky room logic to keep selected room at same position
+ const stickyRooms = this.applyStickyRoom(isRoomChange, roomId);
+
+ // Update roomsResult with sticky rooms
+ this.roomsResult = {
+ ...this.roomsResult,
+ rooms: stickyRooms,
+ };
+
+ // Rebuild roomsMap with the reordered rooms
+ this.updateRoomsMap(this.roomsResult);
+
+ // Calculate the active room index after applying sticky logic
+ const activeRoomIndex = this.getActiveRoomIndex(roomId);
+
+ // Track the current active room index for future sticky calculations
+ this.lastActiveRoomIndex = activeRoomIndex;
+
+ // Build the complete state atomically to ensure consistency
+ // roomIds and roomListState must always be in sync
+ const roomIds = this.roomIds;
+ const roomListState: RoomListViewState = {
+ activeRoomIndex,
+ spaceId: this.roomsResult.spaceId,
+ filterKeys: this.roomsResult.filterKeys?.map((k) => String(k)),
+ };
+
+ const filterIds = [...filterKeyToIdMap.values()];
+ const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined;
+ const isRoomListEmpty = roomIds.length === 0;
+ const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;
+
+ // Single atomic snapshot update
+ this.snapshot.merge({
+ isLoadingRooms,
+ isRoomListEmpty,
+ filterIds,
+ activeFilterId,
+ roomListState,
+ roomIds,
+ });
+ }
+
+ public createChatRoom = (): void => {
+ dispatcher.fire(Action.CreateChat);
+ };
+
+ public createRoom = (): void => {
+ const activeSpace = SpaceStore.instance.activeSpaceRoom;
+ if (activeSpace) {
+ dispatcher.dispatch({
+ action: Action.CreateRoom,
+ parent_space: activeSpace,
+ });
+ } else {
+ dispatcher.dispatch({
+ action: Action.CreateRoom,
+ });
+ }
+ };
+}
diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/viewmodels/room-list/utils.ts
similarity index 83%
rename from src/components/viewmodels/roomlist/utils.ts
rename to src/viewmodels/room-list/utils.ts
index dfa20e0d1c..5cd2f58678 100644
--- a/src/components/viewmodels/roomlist/utils.ts
+++ b/src/viewmodels/room-list/utils.ts
@@ -7,12 +7,12 @@
import { type Room, KnownMembership, EventTimeline, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix";
-import { isKnockDenied } from "../../../utils/membership";
-import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
-import { UIComponent } from "../../../settings/UIFeature";
-import { showCreateNewRoom } from "../../../utils/space";
-import dispatcher from "../../../dispatcher/dispatcher";
-import { Action } from "../../../dispatcher/actions";
+import { isKnockDenied } from "../../utils/membership";
+import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
+import { UIComponent } from "../../settings/UIFeature";
+import { showCreateNewRoom } from "../../utils/space";
+import dispatcher from "../../dispatcher/dispatcher";
+import { Action } from "../../dispatcher/actions";
/**
* Check if the user has access to the options menu.
diff --git a/test/unit-tests/Searching-test.ts b/test/unit-tests/Searching-test.ts
new file mode 100644
index 0000000000..60b34fb345
--- /dev/null
+++ b/test/unit-tests/Searching-test.ts
@@ -0,0 +1,262 @@
+/*
+Copyright 2025 The Matrix.org Foundation C.I.C.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { type IResultRoomEvents } from "matrix-js-sdk/src/matrix";
+
+import eventSearch from "../../src/Searching";
+import EventIndexPeg from "../../src/indexing/EventIndexPeg";
+import { createTestClient } from "../test-utils";
+
+describe("Searching", () => {
+ const mockClient = createTestClient();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe("localSearch", () => {
+ it("removes state_key: null from search results", async () => {
+ // Mock search results from Seshat that include state_key: null
+ const mockSearchResults: IResultRoomEvents = {
+ count: 2,
+ results: [
+ {
+ rank: 1,
+ result: {
+ event_id: "$event1",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567890,
+ content: { body: "test message 1", msgtype: "m.text" },
+ // Seshat incorrectly includes state_key: null for non-state events
+ state_key: null,
+ } as any,
+ context: {
+ events_before: [
+ {
+ event_id: "$before1",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567889,
+ content: { body: "before message", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ ],
+ events_after: [
+ {
+ event_id: "$after1",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567891,
+ content: { body: "after message", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ ],
+ profile_info: {},
+ },
+ },
+ {
+ rank: 2,
+ result: {
+ event_id: "$event2",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567880,
+ content: { body: "test message 2", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ context: {
+ events_before: [],
+ events_after: [],
+ profile_info: {},
+ },
+ },
+ ],
+ highlights: ["test"],
+ };
+
+ // Mock EventIndex.search to return results with state_key: null
+ const mockEventIndex = {
+ search: jest.fn().mockResolvedValue(mockSearchResults),
+ };
+ jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
+
+ // Mock crypto to indicate room is encrypted
+ jest.spyOn(mockClient, "getCrypto").mockReturnValue({
+ isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
+ } as any);
+
+ // Perform search in an encrypted room
+ const roomId = "!room:example.org";
+ await eventSearch(mockClient, "test", roomId);
+
+ // Verify that state_key: null was removed from the search arguments passed to search
+ expect(mockEventIndex.search).toHaveBeenCalled();
+
+ // Get the mock search results that were passed to processRoomEventsSearch
+ // The state_key should have been deleted from the original results object
+ const mainEventResult = mockSearchResults.results![0].result as unknown as Record;
+ expect(mainEventResult.state_key).toBeUndefined();
+
+ const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record<
+ string,
+ unknown
+ >;
+ expect(beforeEvent.state_key).toBeUndefined();
+
+ const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record<
+ string,
+ unknown
+ >;
+ expect(afterEvent.state_key).toBeUndefined();
+
+ const secondResult = mockSearchResults.results![1].result as unknown as Record;
+ expect(secondResult.state_key).toBeUndefined();
+ });
+
+ it("does not modify events without state_key: null", async () => {
+ const mockSearchResults: IResultRoomEvents = {
+ count: 1,
+ results: [
+ {
+ rank: 1,
+ result: {
+ event_id: "$event1",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567890,
+ content: { body: "test message", msgtype: "m.text" },
+ // No state_key property at all (correct behavior)
+ } as any,
+ context: {
+ events_before: [],
+ events_after: [],
+ profile_info: {},
+ },
+ },
+ ],
+ highlights: ["test"],
+ };
+
+ const mockEventIndex = {
+ search: jest.fn().mockResolvedValue(mockSearchResults),
+ };
+ jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
+
+ jest.spyOn(mockClient, "getCrypto").mockReturnValue({
+ isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
+ } as any);
+
+ const roomId = "!room:example.org";
+ await eventSearch(mockClient, "test", roomId);
+
+ // Verify state_key is still undefined (not accidentally set to something)
+ const eventResult = mockSearchResults.results![0].result as unknown as Record;
+ expect("state_key" in eventResult).toBe(false);
+ });
+
+ it("handles missing context fields and empty result sets", async () => {
+ const mockSearchResults: IResultRoomEvents = {
+ count: 3,
+ results: [
+ {
+ rank: 1,
+ result: {
+ event_id: "$event1",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567890,
+ content: { body: "test message", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ context: {
+ events_before: [{ event_id: "$before1", state_key: "not-null" } as any],
+ events_after: [{ event_id: "$after1", state_key: "not-null" } as any],
+ profile_info: {},
+ },
+ },
+ {
+ rank: 2,
+ result: {
+ event_id: "$event2",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567891,
+ content: { body: "test message 2", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ context: {
+ profile_info: {},
+ } as any,
+ },
+ {
+ rank: 3,
+ result: {
+ event_id: "$event3",
+ room_id: "!room:example.org",
+ sender: "@user:example.org",
+ type: "m.room.message",
+ origin_server_ts: 1234567892,
+ content: { body: "test message 3", msgtype: "m.text" },
+ state_key: null,
+ } as any,
+ context: undefined as any,
+ },
+ ],
+ highlights: ["test"],
+ };
+
+ const mockEventIndex = {
+ search: jest
+ .fn()
+ .mockResolvedValueOnce(mockSearchResults)
+ .mockResolvedValueOnce({ count: 0, highlights: ["test"] } as IResultRoomEvents),
+ };
+ jest.spyOn(EventIndexPeg, "get").mockReturnValue(mockEventIndex as any);
+
+ jest.spyOn(mockClient, "getCrypto").mockReturnValue({
+ isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
+ } as any);
+
+ const roomId = "!room:example.org";
+ await eventSearch(mockClient, "test", roomId);
+ await eventSearch(mockClient, "test", roomId);
+
+ const firstMainEvent = mockSearchResults.results![0].result as unknown as Record;
+ expect(firstMainEvent.state_key).toBeUndefined();
+
+ const beforeEvent = mockSearchResults.results![0].context!.events_before![0] as unknown as Record<
+ string,
+ unknown
+ >;
+ expect(beforeEvent.state_key).toBe("not-null");
+
+ const afterEvent = mockSearchResults.results![0].context!.events_after![0] as unknown as Record<
+ string,
+ unknown
+ >;
+ expect(afterEvent.state_key).toBe("not-null");
+
+ const secondMainEvent = mockSearchResults.results![1].result as unknown as Record;
+ expect(secondMainEvent.state_key).toBeUndefined();
+
+ const thirdMainEvent = mockSearchResults.results![2].result as unknown as Record;
+ expect(thirdMainEvent.state_key).toBeUndefined();
+ });
+ });
+});
diff --git a/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx
deleted file mode 100644
index 6c5b121022..0000000000
--- a/test/unit-tests/components/viewmodels/roomlist/MessagePreviewViewModel-test.tsx
+++ /dev/null
@@ -1,58 +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 { renderHook, waitFor } from "jest-matrix-react";
-import { type Room } from "matrix-js-sdk/src/matrix";
-
-import { createTestClient, mkStubRoom } from "../../../../test-utils";
-import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
-import { useMessagePreviewViewModel } from "../../../../../src/components/viewmodels/roomlist/MessagePreviewViewModel";
-
-describe("MessagePreviewViewModel", () => {
- let room: Room;
-
- beforeEach(() => {
- const matrixClient = createTestClient();
- room = mkStubRoom("roomId", "roomName", matrixClient);
- });
-
- it("should do an initial fetch of the message preview", async () => {
- // Mock the store to return some text.
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => {
- return { text: "Hello world!" } as MessagePreview;
- });
-
- const { result: vm } = renderHook(() => useMessagePreviewViewModel(room));
-
- // Eventually, vm.message should have the text from the store.
- await waitFor(() => {
- expect(vm.current.message).toEqual("Hello world!");
- });
- });
-
- it("should fetch message preview again on update from store", async () => {
- // Mock the store to return the text in variable message.
- let message = "Hello World!";
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockImplementation(async (room) => {
- return { text: message } as MessagePreview;
- });
- jest.spyOn(MessagePreviewStore, "getPreviewChangedEventName").mockImplementation((room) => {
- return "UPDATE";
- });
-
- const { result: vm } = renderHook(() => useMessagePreviewViewModel(room));
-
- // Let's assume the message changed.
- message = "New message!";
- MessagePreviewStore.instance.emit("UPDATE");
-
- /// vm.message should be the updated message.
- await waitFor(() => {
- expect(vm.current.message).toEqual(message);
- });
- });
-});
diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx
deleted file mode 100644
index d017084db5..0000000000
--- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx
+++ /dev/null
@@ -1,221 +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 { renderHook } from "jest-matrix-react";
-import { mocked } from "jest-mock";
-import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
-
-import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
-import { useRoomListItemMenuViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel";
-import {
- hasAccessToNotificationMenu,
- hasAccessToOptionsMenu,
-} from "../../../../../src/components/viewmodels/roomlist/utils";
-import DMRoomMap from "../../../../../src/utils/DMRoomMap";
-import { DefaultTagID } from "../../../../../src/stores/room-list/models";
-import { useUnreadNotifications } from "../../../../../src/hooks/useUnreadNotifications";
-import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
-import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications";
-import { tagRoom } from "../../../../../src/utils/room/tagRoom";
-import dispatcher from "../../../../../src/dispatcher/dispatcher";
-import { useNotificationState } from "../../../../../src/hooks/useRoomNotificationState";
-import { RoomNotifState } from "../../../../../src/RoomNotifs";
-
-jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
- hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
- hasAccessToNotificationMenu: jest.fn().mockReturnValue(false),
-}));
-
-jest.mock("../../../../../src/hooks/useUnreadNotifications", () => ({
- useUnreadNotifications: jest.fn(),
-}));
-
-jest.mock("../../../../../src/hooks/useRoomNotificationState", () => ({
- useNotificationState: jest.fn(),
-}));
-
-jest.mock("../../../../../src/utils/notifications", () => ({
- clearRoomNotification: jest.fn(),
- setMarkedUnreadState: jest.fn(),
-}));
-
-jest.mock("../../../../../src/utils/room/tagRoom", () => ({
- tagRoom: jest.fn(),
-}));
-
-describe("RoomListItemMenuViewModel", () => {
- let matrixClient: MatrixClient;
- let room: Room;
-
- beforeEach(() => {
- matrixClient = stubClient();
- room = mkStubRoom("roomId", "roomName", matrixClient);
-
- DMRoomMap.makeShared(matrixClient);
- jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
-
- mocked(useUnreadNotifications).mockReturnValue({ symbol: null, count: 0, level: NotificationLevel.None });
- mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, jest.fn()]);
- jest.spyOn(dispatcher, "dispatch");
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- function render() {
- return renderHook(() => useRoomListItemMenuViewModel(room), withClientContextRenderOptions(matrixClient));
- }
-
- it("default", () => {
- const { result } = render();
- expect(result.current.showMoreOptionsMenu).toBe(false);
- expect(result.current.canInvite).toBe(false);
- expect(result.current.isFavourite).toBe(false);
- expect(result.current.canCopyRoomLink).toBe(true);
- expect(result.current.canMarkAsRead).toBe(false);
- expect(result.current.canMarkAsUnread).toBe(true);
- });
-
- it("should has showMoreOptionsMenu to be true", () => {
- mocked(hasAccessToOptionsMenu).mockReturnValue(true);
- const { result } = render();
- expect(result.current.showMoreOptionsMenu).toBe(true);
- });
-
- it("should has showNotificationMenu to be true", () => {
- mocked(hasAccessToNotificationMenu).mockReturnValue(true);
- const { result } = render();
- expect(result.current.showNotificationMenu).toBe(true);
- });
-
- it("should be able to invite", () => {
- jest.spyOn(room, "canInvite").mockReturnValue(true);
- const { result } = render();
- expect(result.current.canInvite).toBe(true);
- });
-
- it("should be a favourite", () => {
- room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
- const { result } = render();
- expect(result.current.isFavourite).toBe(true);
- });
-
- it("should not be able to copy the room link", () => {
- jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("userId");
- const { result } = render();
- expect(result.current.canCopyRoomLink).toBe(false);
- });
-
- it("should be able to mark as read", () => {
- // Add a notification
- mocked(useUnreadNotifications).mockReturnValue({
- symbol: null,
- count: 1,
- level: NotificationLevel.Notification,
- });
- const { result } = render();
- expect(result.current.canMarkAsRead).toBe(true);
- expect(result.current.canMarkAsUnread).toBe(false);
- });
-
- it("should has isNotificationAllMessage to be true", () => {
- const { result } = render();
- expect(result.current.isNotificationAllMessage).toBe(true);
- });
-
- it("should has isNotificationAllMessageLoud to be true", () => {
- mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessagesLoud, jest.fn()]);
- const { result } = render();
- expect(result.current.isNotificationAllMessageLoud).toBe(true);
- });
-
- it("should has isNotificationMentionOnly to be true", () => {
- mocked(useNotificationState).mockReturnValue([RoomNotifState.MentionsOnly, jest.fn()]);
- const { result } = render();
- expect(result.current.isNotificationMentionOnly).toBe(true);
- });
-
- it("should has isNotificationMute to be true", () => {
- mocked(useNotificationState).mockReturnValue([RoomNotifState.Mute, jest.fn()]);
- const { result } = render();
- expect(result.current.isNotificationMute).toBe(true);
- });
-
- // Actions
-
- it("should mark as read", () => {
- const { result } = render();
- result.current.markAsRead(new Event("click"));
- expect(mocked(clearRoomNotification)).toHaveBeenCalledWith(room, matrixClient);
- });
-
- it("should mark as unread", () => {
- const { result } = render();
- result.current.markAsUnread(new Event("click"));
- expect(mocked(setMarkedUnreadState)).toHaveBeenCalledWith(room, matrixClient, true);
- });
-
- it("should tag a room as favourite", () => {
- const { result } = render();
- result.current.toggleFavorite(new Event("click"));
- expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
- });
-
- it("should tag a room as low priority", () => {
- const { result } = render();
- result.current.toggleLowPriority();
- expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.LowPriority);
- });
-
- it("should dispatch invite action", () => {
- const { result } = render();
- result.current.invite(new Event("click"));
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: "view_invite",
- roomId: room.roomId,
- });
- });
-
- it("should dispatch a copy room action", () => {
- const { result } = render();
- result.current.copyRoomLink(new Event("click"));
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: "copy_room",
- room_id: room.roomId,
- });
- });
-
- it("should dispatch forget room action", () => {
- // forget room is only available for archived rooms
- room.tags = { [DefaultTagID.Archived]: { order: 0 } };
-
- const { result } = render();
- result.current.leaveRoom(new Event("click"));
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: "forget_room",
- room_id: room.roomId,
- });
- });
-
- it("should dispatch leave room action", () => {
- const { result } = render();
- result.current.leaveRoom(new Event("click"));
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: "leave_room",
- room_id: room.roomId,
- });
- });
-
- it("should call setRoomNotifState", () => {
- const setRoomNotifState = jest.fn();
- mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, setRoomNotifState]);
- const { result } = render();
- result.current.setRoomNotifState(RoomNotifState.Mute);
- expect(setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
- });
-});
diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx
deleted file mode 100644
index 96bc53016e..0000000000
--- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx
+++ /dev/null
@@ -1,280 +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 { renderHook, waitFor } from "jest-matrix-react";
-import { type Room } from "matrix-js-sdk/src/matrix";
-import { mocked } from "jest-mock";
-
-import dispatcher from "../../../../../src/dispatcher/dispatcher";
-import { Action } from "../../../../../src/dispatcher/actions";
-import { useRoomListItemViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel";
-import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
-import {
- hasAccessToNotificationMenu,
- hasAccessToOptionsMenu,
-} from "../../../../../src/components/viewmodels/roomlist/utils";
-import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
-import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
-import * as UseCallModule from "../../../../../src/hooks/useCall";
-import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
-import DMRoomMap from "../../../../../src/utils/DMRoomMap";
-import { useMessagePreviewToggle } from "../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle";
-
-jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
- hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
- hasAccessToNotificationMenu: jest.fn().mockReturnValue(false),
-}));
-
-jest.mock("../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle", () => ({
- useMessagePreviewToggle: jest.fn().mockReturnValue({ shouldShowMessagePreview: true }),
-}));
-
-describe("RoomListItemViewModel", () => {
- let room: Room;
-
- beforeEach(() => {
- const matrixClient = createTestClient();
- room = mkStubRoom("roomId", "roomName", matrixClient);
-
- const dmRoomMap = {
- getUserIdForRoomId: jest.fn(),
- getDMRoomsForUserId: jest.fn(),
- } as unknown as DMRoomMap;
- DMRoomMap.setShared(dmRoomMap);
-
- mocked(useMessagePreviewToggle).mockReturnValue({
- shouldShowMessagePreview: false,
- toggleMessagePreview: jest.fn(),
- });
- });
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it("should dispatch view room action on openRoom", async () => {
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
-
- const fn = jest.spyOn(dispatcher, "dispatch");
- vm.current.openRoom();
- expect(fn).toHaveBeenCalledWith(
- expect.objectContaining({
- action: Action.ViewRoom,
- room_id: room.roomId,
- metricsTrigger: "RoomList",
- }),
- );
- });
-
- it("should show context menu if user has access to options menu", async () => {
- mocked(hasAccessToOptionsMenu).mockReturnValue(true);
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showContextMenu).toBe(true);
- });
-
- it("should show hover menu if user has access to options menu", async () => {
- mocked(hasAccessToOptionsMenu).mockReturnValue(true);
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showHoverMenu).toBe(true);
- });
-
- it("should show hover menu if user has access to notification menu", async () => {
- mocked(hasAccessToNotificationMenu).mockReturnValue(true);
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showHoverMenu).toBe(true);
- });
-
- it("should not show hover menu if user has an invitation notification", async () => {
- mocked(hasAccessToOptionsMenu).mockReturnValue(true);
-
- const notificationState = new RoomNotificationState(room, false);
- jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
- jest.spyOn(notificationState, "invited", "get").mockReturnValue(false);
-
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showHoverMenu).toBe(true);
- });
-
- it("should return a message preview if one is available and they are enabled", async () => {
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
- text: "Message look like this",
- } as MessagePreview);
- mocked(useMessagePreviewToggle).mockReturnValue({
- shouldShowMessagePreview: true,
- toggleMessagePreview: jest.fn(),
- });
-
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this"));
- });
-
- it("should hide message previews when disabled", async () => {
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
- text: "Message look like this",
- } as MessagePreview);
-
- const { result: vm, rerender } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
-
- // This doesn't seem to test that the hook actually triggers an update,
- // but I can't see how to test that.
- rerender();
-
- expect(vm.current.messagePreview).toBe(undefined);
- });
-
- it("should check message preview when room change", async () => {
- const otherRoom = mkStubRoom("roomId2", "roomName2", room.client);
-
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
- text: "Message look like this",
- } as MessagePreview);
- mocked(useMessagePreviewToggle).mockReturnValue({
- shouldShowMessagePreview: true,
- toggleMessagePreview: jest.fn(),
- });
-
- const { result: vm, rerender } = renderHook((props) => useRoomListItemViewModel(props), {
- initialProps: room,
- ...withClientContextRenderOptions(room.client),
- });
- await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this"));
-
- jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
- rerender(otherRoom);
- await waitFor(() => expect(vm.current.messagePreview).toBe(undefined));
- });
-
- describe("notification", () => {
- let notificationState: RoomNotificationState;
- beforeEach(() => {
- notificationState = new RoomNotificationState(room, false);
- jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
- });
-
- it("should show notification decoration if there is call has participant", () => {
- jest.spyOn(UseCallModule, "useParticipantCount").mockReturnValue(1);
-
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showNotificationDecoration).toBe(true);
- });
-
- it.each([
- {
- label: "hasAnyNotificationOrActivity",
- mock: () => jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true),
- },
- { label: "muted", mock: () => jest.spyOn(notificationState, "muted", "get").mockReturnValue(true) },
- ])("should show notification decoration if $label=true", ({ mock }) => {
- mock();
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.showNotificationDecoration).toBe(true);
- });
-
- it("should be bold if there is a notification", () => {
- jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
-
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.isBold).toBe(true);
- });
-
- it("should recompute notification state when room changes", () => {
- const newRoom = mkStubRoom("room2", "Room 2", room.client);
- const newNotificationState = new RoomNotificationState(newRoom, false);
-
- const { result, rerender } = renderHook((room) => useRoomListItemViewModel(room), {
- ...withClientContextRenderOptions(room.client),
- initialProps: room,
- });
-
- expect(result.current.showNotificationDecoration).toBe(false);
-
- jest.spyOn(newNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
- jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(newNotificationState);
- rerender(newRoom);
-
- expect(result.current.showNotificationDecoration).toBe(true);
- });
- });
-
- describe("a11yLabel", () => {
- let notificationState: RoomNotificationState;
- beforeEach(() => {
- notificationState = new RoomNotificationState(room, false);
- jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
- });
-
- it.each([
- {
- label: "unsent message",
- mock: () => jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true),
- expected: "Open room roomName with an unsent message.",
- },
- {
- label: "invitation",
- mock: () => jest.spyOn(notificationState, "invited", "get").mockReturnValue(true),
- expected: "Open room roomName invitation.",
- },
- {
- label: "mention",
- mock: () => {
- jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true);
- jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
- },
- expected: "Open room roomName with 3 unread messages including mentions.",
- },
- {
- label: "unread",
- mock: () => {
- jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(true);
- jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
- },
- expected: "Open room roomName with 3 unread messages.",
- },
- {
- label: "default",
- expected: "Open room roomName",
- },
- ])("should return the $label label", ({ mock, expected }) => {
- mock?.();
- const { result: vm } = renderHook(
- () => useRoomListItemViewModel(room),
- withClientContextRenderOptions(room.client),
- );
- expect(vm.current.a11yLabel).toBe(expected);
- });
- });
-});
diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx
deleted file mode 100644
index c8ede64320..0000000000
--- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx
+++ /dev/null
@@ -1,341 +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 { range } from "lodash";
-import { act, renderHook, waitFor } from "jest-matrix-react";
-import { mocked } from "jest-mock";
-
-import RoomListStoreV3, { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list-v3/RoomListStoreV3";
-import { mkStubRoom } from "../../../../test-utils";
-import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
-import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
-import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
-import dispatcher from "../../../../../src/dispatcher/dispatcher";
-import { Action } from "../../../../../src/dispatcher/actions";
-import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
-import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
-import { UPDATE_SELECTED_SPACE } from "../../../../../src/stores/spaces";
-
-jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
- hasCreateRoomRights: jest.fn().mockReturnValue(false),
- createRoom: jest.fn(),
-}));
-
-describe("RoomListViewModel", () => {
- function mockAndCreateRooms() {
- const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
- const fn = jest
- .spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
- .mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
- return { rooms, fn };
- }
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it("should return a list of rooms", async () => {
- const { rooms } = mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
-
- expect(vm.current.roomsResult.rooms).toHaveLength(10);
- for (const room of rooms) {
- expect(vm.current.roomsResult.rooms).toContain(room);
- }
- });
-
- it("should update list of rooms on event from room list store", async () => {
- const { rooms } = mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
-
- const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined);
- rooms.push(newRoom);
- await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
-
- await waitFor(() => {
- expect(vm.current.roomsResult.rooms).toContain(newRoom);
- });
- });
-
- describe("Filters", () => {
- it("should provide list of available filters", () => {
- mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
- // should have 6 filters
- expect(vm.current.primaryFilters).toHaveLength(7);
- // check the order
- for (const [i, name] of [
- "Unreads",
- "People",
- "Rooms",
- "Favourites",
- "Mentions",
- "Invites",
- "Low priority",
- ].entries()) {
- expect(vm.current.primaryFilters[i].name).toEqual(name);
- expect(vm.current.primaryFilters[i].active).toEqual(false);
- }
- });
-
- it("should get filtered rooms from RLS on toggle", () => {
- const { fn } = mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
- // Let's say we toggle the People toggle
- const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
- act(() => {
- vm.current.primaryFilters[i].toggle();
- });
- expect(fn).toHaveBeenCalledWith([FilterKey.PeopleFilter]);
- expect(vm.current.primaryFilters[i].active).toEqual(true);
- });
-
- it("should change active property on toggle", () => {
- mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
- // Let's say we toggle the People filter
- const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
- expect(vm.current.primaryFilters[i].active).toEqual(false);
- act(() => {
- vm.current.primaryFilters[i].toggle();
- });
- expect(vm.current.primaryFilters[i].active).toEqual(true);
-
- // Let's say that we toggle the Favourite filter
- const j = vm.current.primaryFilters.findIndex((f) => f.name === "Favourites");
- act(() => {
- vm.current.primaryFilters[j].toggle();
- });
- expect(vm.current.primaryFilters[i].active).toEqual(false);
- expect(vm.current.primaryFilters[j].active).toEqual(true);
- });
-
- it("should return the current active primary filter", async () => {
- // Let's say that the user's preferred sorting is alphabetic
- mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
- // Toggle people filter
- const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
- expect(vm.current.primaryFilters[i].active).toEqual(false);
- act(() => vm.current.primaryFilters[i].toggle());
-
- // The active primary filter should be the People filter
- expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]);
- });
-
- it("should not remove all filters when active space is changed", async () => {
- mockAndCreateRooms();
- const { result: vm } = renderHook(() => useRoomListViewModel());
-
- // Let's first toggle the People filter
- const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
- act(() => {
- vm.current.primaryFilters[i].toggle();
- });
- expect(vm.current.primaryFilters[i].active).toEqual(true);
-
- // Simulate a space change
- await act(() => SpaceStore.instance.emit(UPDATE_SELECTED_SPACE));
-
- // Primary filter should remain unchanged
- expect(vm.current.activePrimaryFilter?.name).toEqual("People");
- });
- });
-
- describe("Create room and chat", () => {
- it("should be canCreateRoom=false if hasCreateRoomRights=false", () => {
- mocked(hasCreateRoomRights).mockReturnValue(false);
- const { result } = renderHook(() => useRoomListViewModel());
- expect(result.current.canCreateRoom).toBe(false);
- });
-
- it("should be canCreateRoom=true if hasCreateRoomRights=true", () => {
- mocked(hasCreateRoomRights).mockReturnValue(true);
- const { result } = renderHook(() => useRoomListViewModel());
- expect(result.current.canCreateRoom).toBe(true);
- });
-
- it("should call createRoom", () => {
- const { result } = renderHook(() => useRoomListViewModel());
- result.current.createRoom();
- expect(mocked(createRoom)).toHaveBeenCalled();
- });
-
- it("should dispatch Action.CreateChat", () => {
- const spy = jest.spyOn(dispatcher, "fire");
- const { result } = renderHook(() => useRoomListViewModel());
- result.current.createChatRoom();
- expect(spy).toHaveBeenCalledWith(Action.CreateChat);
- });
- });
-
- describe("Sticky room and active index", () => {
- function expectActiveRoom(vm: ReturnType, i: number, roomId: string) {
- expect(vm.activeIndex).toEqual(i);
- expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId);
- }
-
- it("active index is calculated with the last opened room in a space", () => {
- // Let's say there's two spaces: !space1:matrix.org and !space2:matrix.org
- // Let's also say that the current active space is !space1:matrix.org
- let currentSpace = "!space1:matrix.org";
- jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => currentSpace);
-
- const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
- // Let's say all the rooms are in space1
- const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] };
- // Let's say all rooms with even index are in space 2
- const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) };
- jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() =>
- currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2,
- );
-
- // Let's say that the room at index 4 is currently active
- const roomId = rooms[4].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expect(vm.current.activeIndex).toEqual(4);
-
- // Let's say that space is changed to "!space2:matrix.org"
- currentSpace = "!space2:matrix.org";
- // Let's say that room[6] is active in space 2
- const activeRoomIdInSpace2 = rooms[6].roomId;
- jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockImplementation(
- () => activeRoomIdInSpace2,
- );
- act(() => {
- RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT);
- });
-
- // Active index should be 3 even without the room change event.
- expectActiveRoom(vm.current, 3, activeRoomIdInSpace2);
- });
-
- it("active room and active index are retained on order change", () => {
- const { rooms } = mockAndCreateRooms();
-
- // Let's say that the room at index 5 is active
- const roomId = rooms[5].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expect(vm.current.activeIndex).toEqual(5);
-
- // Let's say that room at index 9 moves to index 5
- const room9 = rooms[9];
- rooms.splice(9, 1);
- rooms.splice(5, 0, room9);
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
-
- // Active room index should still be 5
- expectActiveRoom(vm.current, 5, roomId);
-
- // Let's add 2 new rooms from index 0
- const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined);
- const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined);
- rooms.unshift(newRoom1, newRoom2);
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
-
- // Active room index should still be 5
- expectActiveRoom(vm.current, 5, roomId);
- });
-
- it("active room and active index are updated when another room is opened", () => {
- const { rooms } = mockAndCreateRooms();
- const roomId = rooms[5].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expectActiveRoom(vm.current, 5, roomId);
-
- // Let's say that room at index 9 becomes active
- const room = rooms[9];
- act(() => {
- dispatcher.dispatch(
- {
- action: Action.ActiveRoomChanged,
- oldRoomId: null,
- newRoomId: room.roomId,
- },
- true,
- );
- });
-
- // Active room index should change to reflect new room
- expectActiveRoom(vm.current, 9, room.roomId);
- });
-
- it("active room and active index are updated when active index spills out of rooms array bounds", () => {
- const { rooms } = mockAndCreateRooms();
- // Let's say that the room at index 5 is active
- const roomId = rooms[5].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expectActiveRoom(vm.current, 5, roomId);
-
- // Let's say that we remove rooms from the start of the array
- for (let i = 0; i < 4; ++i) {
- // We should be able to do 4 deletions before we run out of rooms
- rooms.splice(0, 1);
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
- expectActiveRoom(vm.current, 5, roomId);
- }
-
- // If we remove one more room from the start, there's not going to be enough rooms
- // to maintain the active index.
- rooms.splice(0, 1);
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
- expectActiveRoom(vm.current, 0, roomId);
- });
-
- it("active room and active index are retained when rooms that appear after the active room are deleted", () => {
- const { rooms } = mockAndCreateRooms();
- // Let's say that the room at index 5 is active
- const roomId = rooms[5].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expectActiveRoom(vm.current, 5, roomId);
-
- // Let's say that we remove rooms from the start of the array
- for (let i = 0; i < 4; ++i) {
- // Deleting rooms after index 5 (active) should not update the active index
- rooms.splice(6, 1);
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
- expectActiveRoom(vm.current, 5, roomId);
- }
- });
-
- it("active room index becomes undefined when active room is deleted", () => {
- const { rooms } = mockAndCreateRooms();
- // Let's say that the room at index 5 is active
- let roomId: string | null = rooms[5].roomId;
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expectActiveRoom(vm.current, 5, roomId);
-
- // Let's remove the active room (i.e room at index 5)
- rooms.splice(5, 1);
- roomId = null;
- act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
- expect(vm.current.activeIndex).toBeUndefined();
- });
-
- it("active room index is initially undefined", () => {
- mockAndCreateRooms();
-
- // Let's say that there's no active room currently
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => null);
-
- const { result: vm } = renderHook(() => useRoomListViewModel());
- expect(vm.current.activeIndex).toEqual(undefined);
- });
- });
-});
diff --git a/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts b/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts
deleted file mode 100644
index 1ae8606697..0000000000
--- a/test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.ts
+++ /dev/null
@@ -1,152 +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 { renderHook } from "jest-matrix-react";
-import { type Room } from "matrix-js-sdk/src/matrix";
-import { waitFor } from "@testing-library/dom";
-
-import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
-import dispatcher from "../../../../../src/dispatcher/dispatcher";
-import { mkStubRoom, stubClient } from "../../../../test-utils";
-import { useRoomListNavigation } from "../../../../../src/components/viewmodels/roomlist/useRoomListNavigation";
-import { Action } from "../../../../../src/dispatcher/actions";
-import DMRoomMap from "../../../../../src/utils/DMRoomMap";
-import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
-import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
-
-describe("useRoomListNavigation", () => {
- let rooms: Room[];
-
- beforeEach(() => {
- const matrixClient = stubClient();
- rooms = [
- mkStubRoom("room1", "Room 1", matrixClient),
- mkStubRoom("room2", "Room 2", matrixClient),
- mkStubRoom("room3", "Room 3", matrixClient),
- ];
-
- DMRoomMap.makeShared(matrixClient);
- jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
- jest.spyOn(dispatcher, "dispatch");
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it("should navigate to the next room based on delta", async () => {
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
-
- renderHook(() => useRoomListNavigation(rooms));
- dispatcher.dispatch({
- action: Action.ViewRoomDelta,
- delta: 1,
- unread: false,
- });
-
- await waitFor(() =>
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: "room2",
- show_room_tile: true,
- metricsTrigger: "WebKeyboardShortcut",
- metricsViaKeyboard: true,
- }),
- );
- });
-
- it("should navigate to the previous room based on delta", async () => {
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room2");
-
- renderHook(() => useRoomListNavigation(rooms));
- dispatcher.dispatch({
- action: Action.ViewRoomDelta,
- delta: -1,
- unread: false,
- });
-
- await waitFor(() =>
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: "room1",
- show_room_tile: true,
- metricsTrigger: "WebKeyboardShortcut",
- metricsViaKeyboard: true,
- }),
- );
- });
-
- it("should wrap around to the first room when navigating past the last room", async () => {
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room3");
-
- renderHook(() => useRoomListNavigation(rooms));
- dispatcher.dispatch({
- action: Action.ViewRoomDelta,
- delta: 1,
- unread: false,
- });
-
- await waitFor(() =>
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: "room1",
- show_room_tile: true,
- metricsTrigger: "WebKeyboardShortcut",
- metricsViaKeyboard: true,
- }),
- );
- });
-
- it("should wrap around to the last room when navigating before the first room", async () => {
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
-
- renderHook(() => useRoomListNavigation(rooms));
- dispatcher.dispatch({
- action: Action.ViewRoomDelta,
- delta: -1,
- unread: false,
- });
-
- await waitFor(() =>
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: "room3",
- show_room_tile: true,
- metricsTrigger: "WebKeyboardShortcut",
- metricsViaKeyboard: true,
- }),
- );
- });
-
- it("should filter rooms to only unread when unread=true", async () => {
- jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1");
- jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation(
- (room) =>
- ({
- isUnread: room.roomId !== "room1",
- }) as RoomNotificationState,
- );
-
- renderHook(() => useRoomListNavigation(rooms));
-
- dispatcher.dispatch({
- action: Action.ViewRoomDelta,
- delta: 1,
- unread: true,
- });
-
- await waitFor(() =>
- expect(dispatcher.dispatch).toHaveBeenCalledWith({
- action: Action.ViewRoom,
- room_id: "room2",
- show_room_tile: true,
- metricsTrigger: "WebKeyboardShortcut",
- metricsViaKeyboard: true,
- }),
- );
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx
deleted file mode 100644
index 92466f685c..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx
+++ /dev/null
@@ -1,92 +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 from "react";
-import { render, screen } from "jest-matrix-react";
-import userEvent from "@testing-library/user-event";
-
-import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
-import { EmptyRoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/EmptyRoomList";
-import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
-
-describe("", () => {
- let vm: RoomListViewState;
-
- beforeEach(() => {
- vm = {
- isLoadingRooms: false,
- roomsResult: { spaceId: "home", rooms: [] },
- primaryFilters: [],
- createRoom: jest.fn(),
- createChatRoom: jest.fn(),
- canCreateRoom: true,
- activeIndex: undefined,
- };
- });
-
- test("should render the default placeholder when there is no filter", async () => {
- const user = userEvent.setup();
-
- const { asFragment } = render();
- expect(screen.getByText("No chats yet")).toBeInTheDocument();
- expect(asFragment()).toMatchSnapshot();
-
- await user.click(screen.getByRole("button", { name: "Start chat" }));
- expect(vm.createChatRoom).toHaveBeenCalled();
-
- await user.click(screen.getByRole("button", { name: "New room" }));
- expect(vm.createRoom).toHaveBeenCalled();
- });
-
- test("should not render the new room button if the user doesn't have the rights to create a room", async () => {
- const newState = { ...vm, canCreateRoom: false };
-
- const { asFragment } = render();
- expect(screen.queryByRole("button", { name: "New room" })).toBeNull();
- expect(asFragment()).toMatchSnapshot();
- });
-
- it.each([
- { key: FilterKey.UnreadFilter, name: "unread", action: "Show all chats" },
- { key: FilterKey.MentionsFilter, name: "mention", action: "See all activity" },
- { key: FilterKey.InvitesFilter, name: "invite", action: "See all activity" },
- { key: FilterKey.LowPriorityFilter, name: "low priority", action: "See all activity" },
- ])("should display the empty state for the $name filter", async ({ key, name, action }) => {
- const user = userEvent.setup();
- const activePrimaryFilter = {
- toggle: jest.fn(),
- active: true,
- name,
- key,
- };
- const newState = {
- ...vm,
- activePrimaryFilter,
- };
-
- const { asFragment } = render();
- await user.click(screen.getByRole("button", { name: action }));
- expect(activePrimaryFilter.toggle).toHaveBeenCalled();
- expect(asFragment()).toMatchSnapshot();
- });
-
- it.each([
- { key: FilterKey.FavouriteFilter, name: "favourite" },
- { key: FilterKey.PeopleFilter, name: "people" },
- { key: FilterKey.RoomsFilter, name: "rooms" },
- ])("should display empty state for filter $name", ({ name, key }) => {
- const activePrimaryFilter = {
- toggle: jest.fn(),
- active: true,
- name,
- key,
- };
- const newState = { ...vm, activePrimaryFilter };
- const { asFragment } = render();
- expect(asFragment()).toMatchSnapshot();
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx
deleted file mode 100644
index fa7b351bea..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx
+++ /dev/null
@@ -1,78 +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 from "react";
-import { type MatrixClient } from "matrix-js-sdk/src/matrix";
-import { render } from "jest-matrix-react";
-import { fireEvent } from "@testing-library/dom";
-import { VirtuosoMockContext } from "@element-hq/web-shared-components";
-
-import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
-import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
-import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
-import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
-import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
-import { mkRoom, stubClient } from "../../../../../test-utils";
-
-describe("", () => {
- let matrixClient: MatrixClient;
- let vm: RoomListViewState;
-
- beforeEach(() => {
- matrixClient = stubClient();
- const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`));
- vm = {
- isLoadingRooms: false,
- roomsResult: { spaceId: "home", rooms },
- primaryFilters: [],
- createRoom: jest.fn(),
- createChatRoom: jest.fn(),
- canCreateRoom: true,
- activeIndex: undefined,
- };
-
- // Needed to render a room list cell
- DMRoomMap.makeShared(matrixClient);
- jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
- });
-
- it("should render a room list", () => {
- const { asFragment } = render(, {
- wrapper: ({ children }) => (
-
-
- <>{children}>
-
-
- ),
- });
- // At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]".
- // This is a general issue with the react-virtuoso library.
- // TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281
- expect(asFragment()).toMatchSnapshot();
- });
-
- it.each([
- { shortcut: { key: "F6", ctrlKey: true, shiftKey: true }, isPreviousLandmark: true, label: "PreviousLandmark" },
- { shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" },
- ])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => {
- const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue();
- const { getByTestId } = render(, {
- wrapper: ({ children }) => (
-
-
- <>{children}>
-
-
- ),
- });
- const roomList = getByTestId("room-list");
- fireEvent.keyDown(roomList, shortcut);
-
- expect(spyFindLandmark).toHaveBeenCalledWith(Landmark.ROOM_LIST, isPreviousLandmark);
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx
deleted file mode 100644
index 58ab0c672b..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx
+++ /dev/null
@@ -1,144 +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 from "react";
-import { mocked } from "jest-mock";
-import { render, screen } from "jest-matrix-react";
-import userEvent from "@testing-library/user-event";
-
-import {
- type RoomListItemMenuViewState,
- useRoomListItemMenuViewModel,
-} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel";
-import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
-import { mkRoom, stubClient } from "../../../../../test-utils";
-import { RoomListItemMenuView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemMenuView";
-import { RoomNotifState } from "../../../../../../src/RoomNotifs";
-
-jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel", () => ({
- useRoomListItemMenuViewModel: jest.fn(),
-}));
-
-describe("", () => {
- const defaultValue: RoomListItemMenuViewState = {
- showMoreOptionsMenu: true,
- showNotificationMenu: true,
- isFavourite: true,
- isLowPriority: true,
- canInvite: true,
- canMarkAsUnread: true,
- canMarkAsRead: true,
- canCopyRoomLink: true,
- isNotificationAllMessage: true,
- isNotificationMentionOnly: true,
- isNotificationAllMessageLoud: true,
- isNotificationMute: true,
- copyRoomLink: jest.fn(),
- markAsUnread: jest.fn(),
- markAsRead: jest.fn(),
- leaveRoom: jest.fn(),
- toggleLowPriority: jest.fn(),
- toggleFavorite: jest.fn(),
- invite: jest.fn(),
- setRoomNotifState: jest.fn(),
- };
-
- let matrixClient: MatrixClient;
- let room: Room;
-
- beforeEach(() => {
- mocked(useRoomListItemMenuViewModel).mockReturnValue(defaultValue);
- matrixClient = stubClient();
- room = mkRoom(matrixClient, "room1");
- });
-
- function renderMenu() {
- return render();
- }
-
- it("should render the more options menu", () => {
- const { asFragment } = renderMenu();
- expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument();
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("should render the notification options menu", () => {
- const { asFragment } = renderMenu();
- expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument();
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("should not render the more options menu when showMoreOptionsMenu is false", () => {
- mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showMoreOptionsMenu: false });
- renderMenu();
- expect(screen.queryByRole("button", { name: "More Options" })).toBeNull();
- });
-
- it("should not render the notification options menu when showNotificationMenu is false", () => {
- mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showNotificationMenu: false });
- renderMenu();
- expect(screen.queryByRole("button", { name: "Notification options" })).toBeNull();
- });
-
- it("should display all the buttons and have the actions linked for the more options menu", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const openMenu = screen.getByRole("button", { name: "More Options" });
- await user.click(openMenu);
-
- await user.click(screen.getByRole("menuitem", { name: "Mark as read" }));
- expect(defaultValue.markAsRead).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Mark as unread" }));
- expect(defaultValue.markAsUnread).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitemcheckbox", { name: "Favourited" }));
- expect(defaultValue.toggleFavorite).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitemcheckbox", { name: "Low priority" }));
- expect(defaultValue.toggleLowPriority).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Invite" }));
- expect(defaultValue.invite).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Copy room link" }));
- expect(defaultValue.copyRoomLink).toHaveBeenCalled();
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Leave room" }));
- expect(defaultValue.leaveRoom).toHaveBeenCalled();
- });
-
- it("should display all the buttons and have the actions linked for the notification options menu", async () => {
- const user = userEvent.setup();
- renderMenu();
-
- const openMenu = screen.getByRole("button", { name: "Notification options" });
- await user.click(openMenu);
-
- await user.click(screen.getByRole("menuitem", { name: "Match default settings" }));
- expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages);
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "All messages" }));
- expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud);
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Mentions and keywords" }));
- expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly);
-
- await user.click(openMenu);
- await user.click(screen.getByRole("menuitem", { name: "Mute room" }));
- expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute);
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx
deleted file mode 100644
index b6127e1189..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx
+++ /dev/null
@@ -1,162 +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 from "react";
-import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
-import { render, screen, waitFor } from "jest-matrix-react";
-import userEvent from "@testing-library/user-event";
-import { mocked } from "jest-mock";
-import { CallType } from "matrix-js-sdk/src/webrtc/call";
-
-import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
-import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
-import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
-import {
- type RoomListItemViewState,
- useRoomListItemViewModel,
-} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel";
-import { RoomNotificationState } from "../../../../../../src/stores/notifications/RoomNotificationState";
-
-jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel", () => ({
- useRoomListItemViewModel: jest.fn(),
-}));
-
-describe("", () => {
- let defaultValue: RoomListItemViewState;
- let matrixClient: MatrixClient;
- let room: Room;
-
- const renderRoomListItem = (props: Partial> = {}) => {
- const defaultProps = {
- room,
- isSelected: false,
- isFocused: false,
- onFocus: jest.fn(),
- roomIndex: 0,
- roomCount: 1,
- listIsScrolling: false,
- };
-
- return render(, withClientContextRenderOptions(matrixClient));
- };
-
- beforeEach(() => {
- matrixClient = stubClient();
- room = mkRoom(matrixClient, "room1");
-
- DMRoomMap.makeShared(matrixClient);
- jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
-
- const notificationState = new RoomNotificationState(room, false);
- jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
- jest.spyOn(notificationState, "isNotification", "get").mockReturnValue(true);
- jest.spyOn(notificationState, "count", "get").mockReturnValue(1);
-
- defaultValue = {
- openRoom: jest.fn(),
- showContextMenu: false,
- showHoverMenu: false,
- notificationState,
- a11yLabel: "Open room room1",
- isBold: false,
- isVideoRoom: false,
- callConnectionState: null,
- callType: CallType.Video,
- hasParticipantInCall: false,
- name: room.name,
- showNotificationDecoration: false,
- messagePreview: undefined,
- };
-
- mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);
- });
-
- test("should render a room item", () => {
- const onClick = jest.fn();
- const { asFragment } = renderRoomListItem({
- onClick,
- roomCount: 0,
- });
- expect(asFragment()).toMatchSnapshot();
- });
-
- test("should render a room item with a message preview", () => {
- defaultValue.messagePreview = "The message looks like this";
-
- const onClick = jest.fn();
- const { asFragment } = renderRoomListItem({
- onClick,
- });
- expect(asFragment()).toMatchSnapshot();
- });
-
- test("should call openRoom when clicked", async () => {
- const user = userEvent.setup();
- renderRoomListItem();
-
- await user.click(screen.getByRole("option", { name: `Open room ${room.name}` }));
- expect(defaultValue.openRoom).toHaveBeenCalled();
- });
-
- test("should be selected if isSelected=true", async () => {
- const { asFragment } = renderRoomListItem({
- isSelected: true,
- });
-
- expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute(
- "aria-selected",
- "true",
- );
- expect(asFragment()).toMatchSnapshot();
- });
-
- test("should display notification decoration", async () => {
- mocked(useRoomListItemViewModel).mockReturnValue({
- ...defaultValue,
- showNotificationDecoration: true,
- });
-
- const { asFragment } = renderRoomListItem();
-
- expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
- expect(asFragment()).toMatchSnapshot();
- });
-
- test("should not display notification decoration when hovered", async () => {
- const user = userEvent.setup();
-
- mocked(useRoomListItemViewModel).mockReturnValue({
- ...defaultValue,
- showNotificationDecoration: true,
- });
-
- renderRoomListItem();
-
- const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
- await user.hover(listItem);
-
- expect(screen.queryByRole("notification-decoration")).toBeNull();
- });
-
- test("should render the context menu", async () => {
- const user = userEvent.setup();
-
- mocked(useRoomListItemViewModel).mockReturnValue({
- ...defaultValue,
- showContextMenu: true,
- });
-
- renderRoomListItem();
-
- const button = screen.getByRole("option", { name: `Open room ${room.name}` });
- await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]);
- await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
- // Menu should close
- await user.keyboard("{Escape}");
- expect(screen.queryByRole("menu")).toBeNull();
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx
deleted file mode 100644
index 8276c7340f..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx
+++ /dev/null
@@ -1,155 +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, { act } from "react";
-import { render, screen } from "jest-matrix-react";
-import userEvent from "@testing-library/user-event";
-
-import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
-import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
-import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
-
-describe("", () => {
- let vm: RoomListViewState;
- const filterToggleMocks = [jest.fn(), jest.fn(), jest.fn()];
-
- let resizeCallback: ResizeObserverCallback;
-
- beforeEach(() => {
- // Reset mocks between tests
- filterToggleMocks.forEach((mock) => mock.mockClear());
-
- // Mock ResizeObserver
- global.ResizeObserver = jest.fn().mockImplementation((callback) => {
- resizeCallback = callback;
- return {
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
- };
- });
-
- vm = {
- primaryFilters: [
- { name: "People", active: true, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
- { name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
- { name: "Unreads", active: false, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
- ],
- } as unknown as RoomListViewState;
- });
-
- function mockFiltersOffsetLeft() {
- // Use `getByText` instead of `getByRole` to bypass the aria-hidden
- jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
- jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
- jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60);
-
- // @ts-ignore
- act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
- }
-
- it("should renders all filters correctly", () => {
- const { asFragment } = render();
- mockFiltersOffsetLeft();
-
- // Check that all filters are rendered
- expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument();
- expect(screen.getByRole("option", { name: "Rooms" })).toBeInTheDocument();
- expect(screen.getByRole("option", { name: "Unreads" })).toBeInTheDocument();
-
- // Check that the active filter is marked as selected
- expect(screen.getByRole("option", { name: "People" })).toHaveAttribute("aria-selected", "true");
- expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "false");
- expect(screen.getByRole("option", { name: "Unreads" })).toHaveAttribute("aria-selected", "false");
-
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("should call toggle function when a filter is clicked", async () => {
- const user = userEvent.setup();
- render();
- mockFiltersOffsetLeft();
-
- // Click on an inactive filter
- await user.click(screen.getByRole("option", { name: "People" }));
-
- // Check that the toggle function was called
- expect(filterToggleMocks[0]).toHaveBeenCalledTimes(1);
- });
-
- function makeUnreadWrapping() {
- // Use `getByText` instead of `getByRole` to bypass the aria-hidden
- jest.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0);
- jest.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30);
- // Unreads is wrapping
- jest.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0);
-
- // @ts-ignore
- act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
- }
-
- it("should hide or display filters if they are wrapping", async () => {
- const user = userEvent.setup();
- render();
- mockFiltersOffsetLeft();
-
- // No filter is wrapping, so chevron shouldn't be visible
- expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull();
- expect(screen.queryByRole("option", { name: "Unreads" })).toBeVisible();
-
- makeUnreadWrapping();
-
- // The Unreads filter is wrapping, it should not be visible
- expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull();
- // Now filters are wrapping, so chevron should be visible
- await user.click(screen.getByRole("button", { name: "Expand filter list" }));
- // The list is expanded, so Unreads should be visible
- expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible();
- });
-
- it("should move the active filter if the list is collapsed and the filter is wrapping", async () => {
- vm = {
- primaryFilters: [
- { name: "People", active: false, toggle: filterToggleMocks[0], key: FilterKey.PeopleFilter },
- { name: "Rooms", active: false, toggle: filterToggleMocks[1], key: FilterKey.RoomsFilter },
- { name: "Unreads", active: true, toggle: filterToggleMocks[2], key: FilterKey.UnreadFilter },
- ],
- } as unknown as RoomListViewState;
-
- const user = userEvent.setup();
- render();
- makeUnreadWrapping();
-
- // Unread filter should be moved to the first position
- expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).toBe(
- screen.getByRole("option", { name: "Unreads" }),
- );
-
- // When the list is expanded, the Unreads filter should move to its original position
- await user.click(screen.getByRole("button", { name: "Expand filter list" }));
- expect(screen.getByRole("listbox", { name: "Room list filters" }).children[0]).not.toEqual(
- screen.getByRole("option", { name: "Unreads" }),
- );
- });
-
- it("should hide the filter is the previous is on the same vertical position", async () => {
- render();
- mockFiltersOffsetLeft();
-
- jest.spyOn(screen.getByRole("option", { name: "People" }), "offsetLeft", "get").mockReturnValue(0);
- // Rooms is wrapping
- jest.spyOn(screen.getByRole("option", { name: "Rooms" }), "offsetLeft", "get").mockReturnValue(0);
-
- // @ts-ignore
- act(() => resizeCallback([{ target: screen.getByRole("listbox", { name: "Room list filters" }) }]));
-
- // The Unreads filter is wrapping, it should not be visible
- expect(screen.queryByRole("option", { name: "Rooms" })).toBeNull();
- // Now filters are wrapping, so chevron should be visible
- expect(screen.getByRole("button", { name: "Expand filter list" })).toBeVisible();
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx
deleted file mode 100644
index 0081c6f350..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx
+++ /dev/null
@@ -1,65 +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 { mocked } from "jest-mock";
-import { render, screen } from "jest-matrix-react";
-import React from "react";
-
-import {
- type RoomListViewState,
- useRoomListViewModel,
-} from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
-import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListView";
-import { mkRoom, stubClient } from "../../../../../test-utils";
-
-jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewModel", () => ({
- useRoomListViewModel: jest.fn(),
-}));
-
-describe("", () => {
- const defaultValue: RoomListViewState = {
- isLoadingRooms: false,
- roomsResult: { spaceId: "home", rooms: [] },
- primaryFilters: [],
- createRoom: jest.fn(),
- createChatRoom: jest.fn(),
- canCreateRoom: true,
- activeIndex: undefined,
- };
- const matrixClient = stubClient();
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- it("should render the loading room list", () => {
- mocked(useRoomListViewModel).mockReturnValue({
- ...defaultValue,
- isLoadingRooms: true,
- });
-
- const roomList = render();
- expect(roomList.container.querySelector(".mx_RoomListSkeleton")).not.toBeNull();
- });
-
- it("should render an empty room list", () => {
- mocked(useRoomListViewModel).mockReturnValue(defaultValue);
-
- render();
- expect(screen.getByText("No chats yet")).toBeInTheDocument();
- });
-
- it("should render a room list", () => {
- mocked(useRoomListViewModel).mockReturnValue({
- ...defaultValue,
- roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] },
- });
-
- render();
- expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument();
- });
-});
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap
deleted file mode 100644
index 140e1f366b..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap
+++ /dev/null
@@ -1,279 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[` should display empty state for filter favourite 1`] = `
-
-
-
- You don't have favourite chats yet
-
-
- You can add a chat to your favourites in the chat settings
-
-
-
-`;
-
-exports[` should display empty state for filter people 1`] = `
-
-
-
- You don’t have direct chats with anyone yet
-
-
- You can deselect filters in order to see your other chats
-
-
-
-`;
-
-exports[` should display empty state for filter rooms 1`] = `
-
-
-
- You’re not in any room yet
-
-
- You can deselect filters in order to see your other chats
-
-
-
-`;
-
-exports[` should display the empty state for the invite filter 1`] = `
-
-
-
- You don't have any unread invites
-
-
- See all activity
-
-
-
-`;
-
-exports[` should display the empty state for the low priority filter 1`] = `
-
-
-
- You don't have any low priority rooms
-
-
- See all activity
-
-
-
-`;
-
-exports[` should display the empty state for the mention filter 1`] = `
-
-
-
- You don't have any unread mentions
-
-
- See all activity
-
-
-
-`;
-
-exports[` should display the empty state for the unread filter 1`] = `
-
-
-
- Congrats! You don’t have any unread messages
-
-
- Show all chats
-
-
-
-`;
-
-exports[` should not render the new room button if the user doesn't have the rights to create a room 1`] = `
-
-
-
- No chats yet
-
-
- Get started by messaging someone
-
-
-
-
- Start chat
-
-
-
-
-`;
-
-exports[` should render the default placeholder when there is no filter 1`] = `
-
-
-
- No chats yet
-
-
- Get started by messaging someone or by creating a room
-
-
-
-
- Start chat
-
-
-
- New room
-
-
-
-
-`;
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap
deleted file mode 100644
index eb833e64fa..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap
+++ /dev/null
@@ -1,1255 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[` should render a room list 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- room0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room3
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room4
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room5
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room6
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room7
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room8
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- room9
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap
deleted file mode 100644
index 8842b91e6f..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap
+++ /dev/null
@@ -1,155 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[` should render the more options menu 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[` should render the notification options menu 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap
deleted file mode 100644
index f46588370f..0000000000
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap
+++ /dev/null
@@ -1,234 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[` should be selected if isSelected=true 1`] = `
-
-
-
-
-
-