New room list: video room and video call decoration (#29693)

* feat: add video call and EC call to room list item vm

* feat: add video call notification decoration to notification decoration component

* feat: add video call support to room list item view

* feat: add new RoomAvatarView component

* feat: deprecate `DecoratedRoomAvatar`

* feat: use `RoomAvatarView` in room list item

* feat: allow custom class for `RoomAvatar`

* test: update notification decoration

* test: update room list item view

* test: update room list snapshot

* test: add tests for room avatar vm

* test: add tests for room avatar view

* test(e2e): update snapshots

* fix: video room creation rights

* test: e2e add test for public and video room
This commit is contained in:
Florian Duros
2025-04-14 11:27:43 +02:00
committed by GitHub
parent 1430fd5af6
commit 07d5a72f26
35 changed files with 1257 additions and 278 deletions

View File

@@ -7,7 +7,7 @@
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { expect, test } from "../../../element-web-test";
test.describe("Room list", () => {
test.use({
@@ -144,6 +144,32 @@ test.describe("Room list", () => {
});
});
test.describe("Avatar decoration", () => {
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
// @ts-ignore Visibility enum is not accessible
await app.client.createRoom({ name: "public room", visibility: "public" });
const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
await expect(publicRoom).toBeVisible();
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
});
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
await page.getByRole("button", { name: "Create video room" }).click();
const roomListView = getRoomList(page);
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
await expect(videoRoom).toBeVisible();
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
});
});
test.describe("Notification decoration", () => {
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -116,6 +116,7 @@
@import "./views/auth/_Welcome.pcss";
@import "./views/avatars/_BaseAvatar.pcss";
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
@import "./views/avatars/_RoomAvatarView.pcss";
@import "./views/avatars/_WidgetAvatar.pcss";
@import "./views/avatars/_WithPresenceIndicator.pcss";
@import "./views/beta/_BetaCard.pcss";

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
.mx_RoomAvatarView {
--room-avatar-size: 32px;
position: relative;
/* Keep the container to the same size than the avatar */
inline-size: var(--room-avatar-size);
block-size: var(--room-avatar-size);
.mx_RoomAvatarView_RoomAvatar {
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
}
.mx_RoomAvatarView_RoomAvatar_icon {
mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-icon-mask.svg");
}
.mx_RoomAvatarView_RoomAvatar_presence {
mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-presence-mask.svg");
}
.mx_RoomAvatarView_icon {
position: absolute;
/* Place half the icon inside the avatar */
/* Avatar size - (icon size (16px) / 2) */
left: calc((var(--room-avatar-size) - 8px));
bottom: var(--cpd-space-0-5x);
}
.mx_RoomAvatarView_PresenceDecoration {
position: absolute;
/* Place half the icon inside the avatar */
/* Avatar size - (icon size (8px) / 2) */
left: calc((var(--room-avatar-size) - 4px));
bottom: var(--cpd-space-0-5x);
}
}

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M32 0H0V32H32C26.4772 32 22 27.5228 22 22C22 16.4772 26.4772 12 32 12V0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M36 -4H-4V36H36V30.4722C34.9385 31.4223 33.5367 32 32 32C28.6863 32 26 29.3137 26 26C26 22.6863 28.6863 20 32 20C33.5367 20 34.9385 20.5777 36 21.5278V-4Z" />
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -0,0 +1,139 @@
/*
* 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 {
EventType,
JoinRule,
type MatrixEvent,
type Room,
RoomEvent,
type User,
UserEvent,
} from "matrix-js-sdk/src/matrix";
import { useState } from "react";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import DMRoomMap from "../../../utils/DMRoomMap";
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
import { BUSY_PRESENCE_NAME } from "../../views/rooms/PresenceLabel";
import { isPresenceEnabled } from "../../../utils/presence";
/**
* The presence of a user in a DM room.
* - "online": The user is online.
* - "offline": The user is offline.
* - "busy": The user is busy.
* - "unavailable": the presence is unavailable.
* - null: the user is not in a DM room or presence is not enabled.
*/
export type Presence = "online" | "offline" | "busy" | "unavailable" | null;
export interface RoomAvatarViewState {
/**
* Whether the room avatar has a decoration.
* A decoration can be a public or a video call icon or an indicator of presence.
*/
hasDecoration: boolean;
/**
* Whether the room is public.
*/
isPublic: boolean;
/**
* Whether the room is a video room.
*/
isVideoRoom: boolean;
/**
* The presence of the user in the DM room.
* If null, the user is not in a DM room or presence is not enabled.
*/
presence: Presence;
}
/**
* Hook to get the state of the room avatar.
* @param room
*/
export function useRoomAvatarViewModel(room: Room): RoomAvatarViewState {
const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom();
const presence = useDMPresence(room);
const isPublic = useIsPublic(room);
const hasDecoration = isPublic || isVideoRoom || presence !== null;
return { hasDecoration, isPublic, isVideoRoom, presence };
}
/**
* Hook listening to the room join rules.
* Return true if the room is public.
* @param room
*/
function useIsPublic(room: Room): boolean {
const [isPublic, setIsPublic] = useState(isRoomPublic(room));
// We don't use `useTypedEventEmitterState` because we don't want to update `isPublic` value at every `RoomEvent.Timeline` event.
useTypedEventEmitter(room, RoomEvent.Timeline, (ev: MatrixEvent, _room: Room) => {
if (room.roomId !== _room.roomId) return;
if (ev.getType() !== EventType.RoomJoinRules && ev.getType() !== EventType.RoomMember) return;
setIsPublic(isRoomPublic(_room));
});
return isPublic;
}
/**
* Whether the room is public.
* @param room
*/
function isRoomPublic(room: Room): boolean {
return room.getJoinRule() === JoinRule.Public;
}
/**
* Hook listening to the presence of the DM user.
* @param room
*/
function useDMPresence(room: Room): Presence {
const dmUser = getDMUser(room);
const [presence, setPresence] = useState<Presence>(getPresence(dmUser));
useTypedEventEmitter(dmUser, UserEvent.Presence, () => setPresence(getPresence(dmUser)));
useTypedEventEmitter(dmUser, UserEvent.CurrentlyActive, () => setPresence(getPresence(dmUser)));
return presence;
}
/**
* Get the DM user of the room.
* Return undefined if the room is not a DM room, if we can't find the user or if the presence is not enabled.
* @param room
* @returns found user
*/
function getDMUser(room: Room): User | undefined {
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (!otherUserId) return;
if (getJoinedNonFunctionalMembers(room).length !== 2) return;
if (!isPresenceEnabled(room.client)) return;
return room.client.getUser(otherUserId) || undefined;
}
/**
* Get the presence of the DM user.
* @param dmUser
*/
function getPresence(dmUser: User | undefined): Presence {
if (!dmUser) return null;
if (BUSY_PRESENCE_NAME.matches(dmUser.presence)) return "busy";
const isOnline = dmUser.currentlyActive || dmUser.presence === "online";
if (isOnline) return "online";
if (dmUser.presence === "offline") return "offline";
if (dmUser.presence === "unavailable") return "unavailable";
return null;
}

View File

@@ -128,8 +128,8 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
const isSpaceRoom = Boolean(activeSpace);
const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace);
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms");
const displayComposeMenu = canCreateRoom || canCreateVideoRoom;
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms") && canCreateRoom;
const displayComposeMenu = canCreateRoom;
const displaySpaceMenu = isSpaceRoom;
const canInviteInSpace = Boolean(
activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()),

View File

@@ -18,6 +18,8 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { DefaultTagID } from "../../../stores/room-list/models";
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
import { type ConnectionState } from "../../../models/Call";
export interface RoomListItemViewState {
/**
@@ -40,6 +42,19 @@ export interface RoomListItemViewState {
* Whether the room should be bolded.
*/
isBold: boolean;
/**
* Whether the room is a video room
*/
isVideoRoom: boolean;
/**
* The connection state of the call.
* `null` if there is no call in the room.
*/
callConnectionState: ConnectionState | null;
/**
* Whether there are participants in the call.
*/
hasParticipantInCall: boolean;
}
/**
@@ -57,6 +72,14 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
const a11yLabel = getA11yLabel(room, notificationState);
const isBold = notificationState.hasAnyNotificationOrActivity;
// Video room
const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom();
// EC video call or video room
const call = useCall(room.roomId);
const connectionState = useConnectionState(call);
const hasParticipantInCall = useParticipantCount(call) > 0;
const callConnectionState = call ? connectionState : null;
// Actions
const openRoom = useCallback((): void => {
@@ -73,6 +96,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
openRoom,
a11yLabel,
isBold,
isVideoRoom,
callConnectionState,
hasParticipantInCall,
};
}

View File

@@ -79,6 +79,9 @@ function tooltipText(variant: Icon): string | undefined {
}
}
/**
* @deprecated Use {@link RoomAvatarView} instead.
*/
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User | null = null;
private isUnmounted = false;

View File

@@ -144,7 +144,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}
public render(): React.ReactNode {
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
const roomName = room?.name ?? oobData.name ?? "?";
return (

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
import VideoIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8";
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
import BusyIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
import classNames from "classnames";
import RoomAvatar from "./RoomAvatar";
import { useRoomAvatarViewModel, type Presence } from "../../viewmodels/avatars/RoomAvatarViewModel";
import { _t } from "../../../languageHandler";
interface RoomAvatarViewProps {
/**
* The room to display the avatar for.
*/
room: Room;
}
/**
* Component to display the avatar of a room.
* Currently only 32px size is supported.
*/
export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
const vm = useRoomAvatarViewModel(room);
// No decoration, we just show the avatar
if (!vm.hasDecoration) return <RoomAvatar size="32px" room={room} />;
return (
<div className="mx_RoomAvatarView">
<RoomAvatar
className={classNames("mx_RoomAvatarView_RoomAvatar", {
// Presence indicator and video/public icons don't have the same size
// We use different masks
mx_RoomAvatarView_RoomAvatar_icon: vm.isVideoRoom || vm.isPublic,
mx_RoomAvatarView_RoomAvatar_presence: Boolean(vm.presence),
})}
size="32px"
room={room}
/>
{/* If the room is a public video room, we prefer to display only the video icon */}
{vm.isPublic && !vm.isVideoRoom && (
<PublicIcon
width="16px"
height="16px"
className="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("room|header|room_is_public")}
/>
)}
{vm.isVideoRoom && (
<VideoIcon
width="16px"
height="16px"
className="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("room|video_room")}
/>
)}
{vm.presence && <PresenceDecoration presence={vm.presence} />}
</div>
);
}
type PresenceDecorationProps = {
/**
* The presence of the user in the DM room.
*/
presence: NonNullable<Presence>;
};
/**
* Component to display the presence of a user in a DM room.
*/
function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element {
switch (presence) {
case "online":
return (
<OnlineOrUnavailableIcon
width="8px"
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-accent-primary)"
aria-label={_t("presence|online")}
/>
);
case "unavailable":
return (
<OnlineOrUnavailableIcon
width="8px"
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-quaternary)"
aria-label={_t("presence|away")}
/>
);
case "offline":
return (
<OfflineIcon
width="8px"
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("presence|offline")}
/>
);
case "busy":
return (
<BusyIcon
width="8px"
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("presence|busy")}
/>
);
}
}

View File

@@ -9,6 +9,7 @@ import React, { type HTMLProps, type JSX } from "react";
import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention";
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid";
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
@@ -19,6 +20,10 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
* The notification state of the room or thread.
*/
notificationState: RoomNotificationState;
/**
* Whether the room has a video call.
*/
hasVideoCall: boolean;
}
/**
@@ -26,6 +31,7 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
*/
export function NotificationDecoration({
notificationState,
hasVideoCall,
...props
}: NotificationDecorationProps): JSX.Element | null {
const {
@@ -38,7 +44,7 @@ export function NotificationDecoration({
count,
muted,
} = notificationState;
if (!hasAnyNotificationOrActivity && !muted) return null;
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
return (
<Flex
@@ -49,6 +55,7 @@ export function NotificationDecoration({
data-testid="notification-decoration"
>
{isUnsetMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{invited && <UnreadCounter count={1} />}
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{(isMention || isNotification) && <UnreadCounter count={count || null} />}

View File

@@ -10,10 +10,10 @@ import { type Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
import { Flex } from "../../../utils/Flex";
import { RoomListItemMenuView } from "./RoomListItemMenuView";
import { NotificationDecoration } from "../NotificationDecoration";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElement> {
/**
@@ -39,7 +39,8 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu;
const isNotificationDecorationVisible =
!showHoverDecoration && (vm.notificationState.hasAnyNotificationOrActivity || vm.notificationState.muted);
!showHoverDecoration &&
(vm.notificationState.hasAnyNotificationOrActivity || vm.notificationState.muted || vm.hasParticipantInCall);
return (
<button
@@ -62,7 +63,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
>
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
<Flex className="mx_RoomListItemView_container" gap="var(--cpd-space-3x)" align="center">
<DecoratedRoomAvatar room={room} size="32px" />
<RoomAvatarView room={room} />
<Flex
className="mx_RoomListItemView_content"
gap="var(--cpd-space-3x)"
@@ -85,7 +86,11 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
) : (
<>
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
<NotificationDecoration notificationState={vm.notificationState} aria-hidden={true} />
<NotificationDecoration
notificationState={vm.notificationState}
aria-hidden={true}
hasVideoCall={vm.hasParticipantInCall}
/>
</>
)}
</Flex>

View File

@@ -2098,6 +2098,7 @@
},
"uploading_single_file": "Uploading %(filename)s"
},
"video_room": "This room is a video room",
"waiting_for_join_subtitle": "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted",
"waiting_for_join_title": "Waiting for users to join %(brand)s"
},

View File

@@ -0,0 +1,124 @@
/*
* 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, act } from "jest-matrix-react";
import {
JoinRule,
type MatrixClient,
MatrixEvent,
type Room,
type RoomMember,
User,
UserEvent,
} from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { useRoomAvatarViewModel } from "../../../../../src/components/viewmodels/avatars/RoomAvatarViewModel";
import { createTestClient, mkStubRoom } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { getJoinedNonFunctionalMembers } from "../../../../../src/utils/room/getJoinedNonFunctionalMembers";
import { isPresenceEnabled } from "../../../../../src/utils/presence";
jest.mock("../../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([]),
}));
jest.mock("../../../../../src/utils/presence", () => ({
isPresenceEnabled: jest.fn().mockReturnValue(false),
}));
describe("RoomAvatarViewModel", () => {
let matrixClient: MatrixClient;
let room: Room;
beforeEach(() => {
matrixClient = createTestClient();
room = mkStubRoom("roomId", "roomName", matrixClient);
DMRoomMap.makeShared(matrixClient);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
});
it("should has hasDecoration to false", async () => {
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.hasDecoration).toBe(false);
});
it("should has isVideoRoom set to true", () => {
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.isVideoRoom).toBe(true);
expect(vm.current.hasDecoration).toBe(true);
});
it("should has isPublic set to true", () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.isPublic).toBe(true);
expect(vm.current.hasDecoration).toBe(true);
});
describe("presence", () => {
let user: User;
beforeEach(() => {
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("userId");
mocked(getJoinedNonFunctionalMembers).mockReturnValue([{}, {}] as RoomMember[]);
mocked(isPresenceEnabled).mockReturnValue(true);
user = User.createUser("userId", matrixClient);
jest.spyOn(matrixClient, "getUser").mockReturnValue(user);
});
it("should has presence set to null", () => {
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.presence).toBe(null);
});
it("should has online presence", async () => {
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.presence).toBe("offline");
user.presence = "online";
await act(() => user.emit(UserEvent.Presence, new MatrixEvent(), user));
await waitFor(() => expect(vm.current.presence).toBe("online"));
user.currentlyActive = true;
user.presence = "offline";
await act(() => user.emit(UserEvent.CurrentlyActive, new MatrixEvent(), user));
await waitFor(() => expect(vm.current.presence).toBe("online"));
});
it("should has busy presence", async () => {
user.presence = "busy";
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.presence).toBe("busy");
});
it("should has offline presence", async () => {
user.presence = "offline";
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.presence).toBe("offline");
});
it("should has unavailable presence", async () => {
user.presence = "unavailable";
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.presence).toBe("unavailable");
});
it("should has hasDecoration to true", async () => {
const { result: vm } = renderHook(() => useRoomAvatarViewModel(room));
expect(vm.current.hasDecoration).toBe(true);
});
});
});

View File

@@ -80,14 +80,6 @@ describe("useRoomListHeaderViewModel", () => {
expect(result.current.canCreateRoom).toBe(true);
});
it("should be displayComposeMenu=true if the user can creates video room", () => {
mocked(hasCreateRoomRights).mockReturnValue(false);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
const { result } = render();
expect(result.current.displayComposeMenu).toBe(true);
});
it("should be displaySpaceMenu=true if the user is in a space", () => {
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
const { result } = render();
@@ -118,8 +110,10 @@ describe("useRoomListHeaderViewModel", () => {
expect(result.current.canAccessSpaceSettings).toBe(true);
});
it("should be canCreateVideoRoom=true if feature_video_rooms is enabled", () => {
it("should be canCreateVideoRoom=true if feature_video_rooms is enabled and can create room", () => {
mocked(hasCreateRoomRights).mockReturnValue(true);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
const { result } = render();
expect(result.current.canCreateVideoRoom).toBe(true);
});

View File

@@ -0,0 +1,101 @@
/*
* 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 { mocked } from "jest-mock";
import { RoomAvatarView } from "../../../../../src/components/views/avatars/RoomAvatarView";
import { mkStubRoom, stubClient } from "../../../../test-utils";
import {
type Presence,
type RoomAvatarViewState,
useRoomAvatarViewModel,
} from "../../../../../src/components/viewmodels/avatars/RoomAvatarViewModel";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
jest.mock("../../../../../src/components/viewmodels/avatars/RoomAvatarViewModel", () => ({
useRoomAvatarViewModel: jest.fn(),
}));
describe("<RoomAvatarView />", () => {
const matrixClient = stubClient();
const room = mkStubRoom("roomId", "roomName", matrixClient);
DMRoomMap.makeShared(matrixClient);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
let defaultValue: RoomAvatarViewState;
beforeEach(() => {
defaultValue = {
hasDecoration: true,
isPublic: true,
isVideoRoom: true,
presence: null,
};
mocked(useRoomAvatarViewModel).mockReturnValue(defaultValue);
});
it("should not render a decoration", () => {
mocked(useRoomAvatarViewModel).mockReturnValue({ ...defaultValue, hasDecoration: false });
const { asFragment } = render(<RoomAvatarView room={room} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render a video room decoration", () => {
mocked(useRoomAvatarViewModel).mockReturnValue({ ...defaultValue, hasDecoration: true, isVideoRoom: true });
const { asFragment } = render(<RoomAvatarView room={room} />);
expect(screen.getByLabelText("This room is a video room")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should render a public room decoration", () => {
mocked(useRoomAvatarViewModel).mockReturnValue({
...defaultValue,
hasDecoration: true,
isPublic: true,
isVideoRoom: false,
});
const { asFragment } = render(<RoomAvatarView room={room} />);
expect(screen.getByLabelText("This room is public")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should not render a public room decoration if the room is a video room", () => {
mocked(useRoomAvatarViewModel).mockReturnValue({
...defaultValue,
hasDecoration: true,
isPublic: true,
isVideoRoom: true,
});
render(<RoomAvatarView room={room} />);
expect(screen.getByLabelText("This room is a video room")).toBeInTheDocument();
expect(screen.queryByLabelText("This room is public")).toBeNull();
});
it.each([
{ presence: "online" as Presence, label: "Online" },
{ presence: "offline" as Presence, label: "Offline" },
{ presence: "busy" as Presence, label: "Busy" },
{ presence: "unavailable" as Presence, label: "Away" },
])("should render the $presence presence", ({ presence, label }) => {
mocked(useRoomAvatarViewModel).mockReturnValue({
...defaultValue,
hasDecoration: true,
presence,
});
const { asFragment } = render(<RoomAvatarView room={room} />);
expect(screen.getByLabelText(label)).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,389 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomAvatarView /> should not render a decoration 1`] = `
<DocumentFragment>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render a public room decoration 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is public"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m-1-2.05V18q-.825 0-1.412-.587A1.93 1.93 0 0 1 9 16v-1l-4.8-4.8q-.075.45-.138.9Q4 11.55 4 12q0 3.025 1.987 5.3T11 19.95m6.9-2.55q.5-.55.9-1.187.4-.638.662-1.326.263-.687.4-1.412Q20 12.75 20 12a7.85 7.85 0 0 0-1.363-4.475A7.7 7.7 0 0 0 15 4.6V5q0 .824-.588 1.412A1.93 1.93 0 0 1 13 7h-2v2q0 .424-.287.713A.97.97 0 0 1 10 10H8v2h6q.424 0 .713.287.287.288.287.713v3h1q.65 0 1.175.387.525.388.725 1.013"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render a video room decoration 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is a video room"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render the busy presence 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon mx_RoomAvatarView_RoomAvatar_presence"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is a video room"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<svg
aria-label="Busy"
class="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0M5.435 6.048A2.5 2.5 0 0 1 1.687 3.05zm.914-1.19L2.648 1.897a2.5 2.5 0 0 1 3.701 2.961"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render the offline presence 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon mx_RoomAvatarView_RoomAvatar_presence"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is a video room"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<svg
aria-label="Offline"
class="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M4 6.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5M4 8a4 4 0 1 0 0-8 4 4 0 0 0 0 8"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render the online presence 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon mx_RoomAvatarView_RoomAvatar_presence"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is a video room"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<svg
aria-label="Online"
class="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-accent-primary)"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</DocumentFragment>
`;
exports[`<RoomAvatarView /> should render the unavailable presence 1`] = `
<DocumentFragment>
<div
class="mx_RoomAvatarView"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar mx_RoomAvatarView_RoomAvatar mx_RoomAvatarView_RoomAvatar_icon mx_RoomAvatarView_RoomAvatar_presence"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<svg
aria-label="This room is a video room"
class="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<svg
aria-label="Away"
class="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-quaternary)"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</DocumentFragment>
`;

View File

@@ -14,49 +14,54 @@ import { NotificationDecoration } from "../../../../../src/components/views/room
describe("<NotificationDecoration />", () => {
it("should not render if RoomNotificationState.isSilent=true", () => {
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
render(<NotificationDecoration notificationState={state} />);
render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(screen.queryByTestId("notification-decoration")).toBeNull();
});
it("should render the unset message decoration", () => {
const state = { hasAnyNotificationOrActivity: true, isUnsetMessage: true } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the invitation decoration", () => {
const state = { hasAnyNotificationOrActivity: true, invited: true } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the mention decoration", () => {
const state = { hasAnyNotificationOrActivity: true, isMention: true, count: 1 } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the notification decoration", () => {
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 1 } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the notification decoration without count", () => {
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 0 } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the activity decoration", () => {
const state = { hasAnyNotificationOrActivity: true, isActivityNotification: true } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the muted decoration", () => {
const state = { hasAnyNotificationOrActivity: true, muted: true } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} />);
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
expect(asFragment()).toMatchSnapshot();
});
it("should render the video decoration", () => {
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={true} />);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -42,6 +42,9 @@ describe("<RoomListItemView />", () => {
notificationState: new RoomNotificationState(room, false),
a11yLabel: "Open room room1",
isBold: false,
isVideoRoom: false,
callConnectionState: null,
hasParticipantInCall: false,
};
mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);

View File

@@ -34,29 +34,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -82,29 +78,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -130,29 +122,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -178,29 +166,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -226,29 +210,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="6"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="6"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -274,29 +254,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="1"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -322,29 +298,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -370,29 +342,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -418,29 +386,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -466,29 +430,25 @@ exports[`<RoomList /> should render a room list 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"

View File

@@ -12,29 +12,25 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -63,29 +59,25 @@ exports[`<RoomListItemView /> should render a room item 1`] = `
class="mx_Flex mx_RoomListItemView_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<div
class="mx_DecoratedRoomAvatar"
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
<div
class="mx_Flex mx_RoomListItemView_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"

View File

@@ -135,3 +135,25 @@ exports[`<NotificationDecoration /> should render the unset message decoration 1
</div>
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the video decoration 1`] = `
<DocumentFragment>
<div
class="mx_Flex"
data-testid="notification-decoration"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1-5x); --mx-flex-wrap: nowrap;"
>
<svg
fill="var(--cpd-color-icon-accent-primary)"
height="20px"
viewBox="0 0 24 24"
width="20px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</div>
</DocumentFragment>
`;