New room list: add notification decoration (#29552)
* chore: update @compound-web * feat(notification decoration): add NotificationDecoration component * feat(room list item): get notification state in view model * feat(room list item): use notification decoration in RoomListItemView * test(notification decoration): add tests * test(room list item view model): add a11yLabel tests * test(room list item): update tests * test(e2e): add decoration tests
This commit is contained in:
@@ -5,13 +5,16 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { hasAccessToOptionsMenu } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
export interface RoomListItemViewState {
|
||||
/**
|
||||
@@ -22,6 +25,14 @@ export interface RoomListItemViewState {
|
||||
* Open the room having given roomId.
|
||||
*/
|
||||
openRoom: () => void;
|
||||
/**
|
||||
* The a11y label for the room list item.
|
||||
*/
|
||||
a11yLabel: string;
|
||||
/**
|
||||
* The notification state of the room.
|
||||
*/
|
||||
notificationState: RoomNotificationState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +42,8 @@ export interface RoomListItemViewState {
|
||||
export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
// incoming: Check notification menu rights
|
||||
const showHoverMenu = hasAccessToOptionsMenu(room);
|
||||
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
|
||||
const a11yLabel = getA11yLabel(room, notificationState);
|
||||
|
||||
// Actions
|
||||
|
||||
@@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
}, [room]);
|
||||
|
||||
return {
|
||||
notificationState,
|
||||
showHoverMenu,
|
||||
openRoom,
|
||||
a11yLabel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the a11y label for the room list item
|
||||
* @param room
|
||||
* @param notificationState
|
||||
*/
|
||||
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
|
||||
if (notificationState.isUnsetMessage) {
|
||||
return _t("a11y|room_messsage_not_sent", {
|
||||
roomName: room.name,
|
||||
});
|
||||
} else if (notificationState.invited) {
|
||||
return _t("a11y|room_n_unread_invite", {
|
||||
roomName: room.name,
|
||||
});
|
||||
} else if (notificationState.isMention) {
|
||||
return _t("a11y|room_n_unread_messages_mentions", {
|
||||
roomName: room.name,
|
||||
count: notificationState.count,
|
||||
});
|
||||
} else if (notificationState.hasUnreadCount) {
|
||||
return _t("a11y|room_n_unread_messages", {
|
||||
roomName: room.name,
|
||||
count: notificationState.count,
|
||||
});
|
||||
} else {
|
||||
return _t("room_list|room|open_room", { roomName: room.name });
|
||||
}
|
||||
}
|
||||
|
||||
59
src/components/views/rooms/NotificationDecoration.tsx
Normal file
59
src/components/views/rooms/NotificationDecoration.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 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";
|
||||
import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid";
|
||||
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
||||
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
|
||||
interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
||||
/**
|
||||
* The notification state of the room or thread.
|
||||
*/
|
||||
notificationState: RoomNotificationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the notification decoration for a room or a thread.
|
||||
*/
|
||||
export function NotificationDecoration({
|
||||
notificationState,
|
||||
...props
|
||||
}: NotificationDecorationProps): JSX.Element | null {
|
||||
const {
|
||||
hasAnyNotificationOrActivity,
|
||||
isUnsetMessage,
|
||||
invited,
|
||||
isMention,
|
||||
isActivityNotification,
|
||||
isNotification,
|
||||
count,
|
||||
muted,
|
||||
} = notificationState;
|
||||
if (!hasAnyNotificationOrActivity && !muted) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="var(--cpd-space-1x)"
|
||||
{...props}
|
||||
data-testid="notification-decoration"
|
||||
>
|
||||
{isUnsetMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
|
||||
{invited && <UnreadCounter count={1} />}
|
||||
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||
{(isMention || isNotification) && <UnreadCounter count={count || null} />}
|
||||
{isActivityNotification && <Unread />}
|
||||
{muted && <NotificationOffIcon width="20px" height="20px" fill="var(--cpd-color-icon-tertiary)" />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import classNames from "classnames";
|
||||
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
|
||||
import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
import { NotificationDecoration } from "../NotificationDecoration";
|
||||
|
||||
interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
||||
})}
|
||||
type="button"
|
||||
aria-selected={isSelected}
|
||||
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
|
||||
aria-label={vm.a11yLabel}
|
||||
onClick={() => vm.openRoom()}
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseOut={() => setIsHover(false)}
|
||||
@@ -65,7 +65,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<span title={room.name}>{room.name}</span>
|
||||
{showHoverDecoration && (
|
||||
{showHoverDecoration ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => {
|
||||
@@ -74,6 +74,11 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
||||
else setTimeout(() => setIsMenuOpen(isOpen), 0);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
<NotificationDecoration notificationState={vm.notificationState} aria-hidden={true} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"other": "%(count)s unread messages including mentions."
|
||||
},
|
||||
"recent_rooms": "Recent rooms",
|
||||
"room_messsage_not_sent": "Open room %(roomName)s with an unset 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",
|
||||
|
||||
Reference in New Issue
Block a user