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:
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
127
src/components/views/avatars/RoomAvatarView.tsx
Normal file
127
src/components/views/avatars/RoomAvatarView.tsx
Normal 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")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user