mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-22 19:11:07 +01:00
New room list: avoid extra render for room list item (#29752)
* fix: avoid extra render in the new room list * fix: listen to room name changes * fix: trigger render when notification state change * test: fix room list item tests * chore: fix typo `RoomNotificationState.isUnsentMessage` * refactor: move `isNotificationDecorationVisible` into `useRoomListItemViewModel` * refactor: recalculate notification values on notification state changes * refactor: rename `isNotificationDecorationVisible` to `showNotificationDecoration` * test: add test for room list item view * test: add notification tests in room list item vm * fix: listen to notification updates in `NotificationDecoration` * test: update notification decoration tests * refactor: display notification decoration according to vm * test: update room list item view tests * fix: a11y label computation after room name change * refactor: improve notification handling
This commit is contained in:
parent
6767e4d6ad
commit
fd455179f7
@ -5,7 +5,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import dispatcher from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
@ -16,12 +16,17 @@ import { _t } from "../../../languageHandler";
|
|||||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||||
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
||||||
import { type ConnectionState } from "../../../models/Call";
|
import { type ConnectionState } from "../../../models/Call";
|
||||||
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
export interface RoomListItemViewState {
|
export interface RoomListItemViewState {
|
||||||
|
/**
|
||||||
|
* The name of the room.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
/**
|
/**
|
||||||
* Whether the hover menu should be shown.
|
* Whether the hover menu should be shown.
|
||||||
*/
|
*/
|
||||||
@ -55,6 +60,10 @@ export interface RoomListItemViewState {
|
|||||||
* Whether there are participants in the call.
|
* Whether there are participants in the call.
|
||||||
*/
|
*/
|
||||||
hasParticipantInCall: boolean;
|
hasParticipantInCall: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the notification decoration should be shown.
|
||||||
|
*/
|
||||||
|
showNotificationDecoration: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,11 +74,23 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
const matrixClient = useMatrixClientContext();
|
const matrixClient = useMatrixClientContext();
|
||||||
const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags);
|
const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags);
|
||||||
const isArchived = Boolean(roomTags[DefaultTagID.Archived]);
|
const isArchived = Boolean(roomTags[DefaultTagID.Archived]);
|
||||||
|
const name = useEventEmitterState(room, RoomEvent.Name, () => room.name);
|
||||||
|
|
||||||
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
|
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
|
||||||
const invited = notificationState.invited;
|
|
||||||
const a11yLabel = getA11yLabel(room, notificationState);
|
const [a11yLabel, setA11yLabel] = useState(getA11yLabel(name, notificationState));
|
||||||
const isBold = notificationState.hasAnyNotificationOrActivity;
|
const [{ isBold, invited, hasVisibleNotification }, setNotificationValues] = useState(
|
||||||
|
getNotificationValues(notificationState),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
setA11yLabel(getA11yLabel(name, notificationState));
|
||||||
|
}, [name, notificationState]);
|
||||||
|
|
||||||
|
// Listen to changes in the notification state and update the values
|
||||||
|
useTypedEventEmitter(notificationState, NotificationStateEvents.Update, () => {
|
||||||
|
setA11yLabel(getA11yLabel(name, notificationState));
|
||||||
|
setNotificationValues(getNotificationValues(notificationState));
|
||||||
|
});
|
||||||
|
|
||||||
// We don't want to show the hover menu if
|
// We don't want to show the hover menu if
|
||||||
// - there is an invitation for this room
|
// - there is an invitation for this room
|
||||||
@ -86,6 +107,8 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
const hasParticipantInCall = useParticipantCount(call) > 0;
|
const hasParticipantInCall = useParticipantCount(call) > 0;
|
||||||
const callConnectionState = call ? connectionState : null;
|
const callConnectionState = call ? connectionState : null;
|
||||||
|
|
||||||
|
const showNotificationDecoration = hasVisibleNotification || hasParticipantInCall;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
const openRoom = useCallback((): void => {
|
const openRoom = useCallback((): void => {
|
||||||
@ -97,6 +120,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
name,
|
||||||
notificationState,
|
notificationState,
|
||||||
showHoverMenu,
|
showHoverMenu,
|
||||||
openRoom,
|
openRoom,
|
||||||
@ -105,34 +129,59 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
|||||||
isVideoRoom,
|
isVideoRoom,
|
||||||
callConnectionState,
|
callConnectionState,
|
||||||
hasParticipantInCall,
|
hasParticipantInCall,
|
||||||
|
showNotificationDecoration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the values from the notification state
|
||||||
|
* @param notificationState
|
||||||
|
*/
|
||||||
|
function getNotificationValues(notificationState: RoomNotificationState): {
|
||||||
|
computeA11yLabel: (name: string) => string;
|
||||||
|
isBold: boolean;
|
||||||
|
invited: boolean;
|
||||||
|
hasVisibleNotification: boolean;
|
||||||
|
} {
|
||||||
|
const invited = notificationState.invited;
|
||||||
|
const computeA11yLabel = (name: string): string => getA11yLabel(name, notificationState);
|
||||||
|
const isBold = notificationState.hasAnyNotificationOrActivity;
|
||||||
|
|
||||||
|
const hasVisibleNotification = notificationState.hasAnyNotificationOrActivity || notificationState.muted;
|
||||||
|
|
||||||
|
return {
|
||||||
|
computeA11yLabel,
|
||||||
|
isBold,
|
||||||
|
invited,
|
||||||
|
hasVisibleNotification,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the a11y label for the room list item
|
* Get the a11y label for the room list item
|
||||||
* @param room
|
* @param roomName
|
||||||
* @param notificationState
|
* @param notificationState
|
||||||
*/
|
*/
|
||||||
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
|
function getA11yLabel(roomName: string, notificationState: RoomNotificationState): string {
|
||||||
if (notificationState.isUnsetMessage) {
|
if (notificationState.isUnsentMessage) {
|
||||||
return _t("a11y|room_messsage_not_sent", {
|
return _t("a11y|room_messsage_not_sent", {
|
||||||
roomName: room.name,
|
roomName,
|
||||||
});
|
});
|
||||||
} else if (notificationState.invited) {
|
} else if (notificationState.invited) {
|
||||||
return _t("a11y|room_n_unread_invite", {
|
return _t("a11y|room_n_unread_invite", {
|
||||||
roomName: room.name,
|
roomName,
|
||||||
});
|
});
|
||||||
} else if (notificationState.isMention) {
|
} else if (notificationState.isMention) {
|
||||||
return _t("a11y|room_n_unread_messages_mentions", {
|
return _t("a11y|room_n_unread_messages_mentions", {
|
||||||
roomName: room.name,
|
roomName,
|
||||||
count: notificationState.count,
|
count: notificationState.count,
|
||||||
});
|
});
|
||||||
} else if (notificationState.hasUnreadCount) {
|
} else if (notificationState.hasUnreadCount) {
|
||||||
return _t("a11y|room_n_unread_messages", {
|
return _t("a11y|room_n_unread_messages", {
|
||||||
roomName: room.name,
|
roomName,
|
||||||
count: notificationState.count,
|
count: notificationState.count,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t("room_list|room|open_room", { roomName: room.name });
|
return _t("room_list|room|open_room", { roomName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
|||||||
|
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||||
|
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
||||||
/**
|
/**
|
||||||
@ -35,16 +37,27 @@ export function NotificationDecoration({
|
|||||||
hasVideoCall,
|
hasVideoCall,
|
||||||
...props
|
...props
|
||||||
}: NotificationDecorationProps): JSX.Element | null {
|
}: NotificationDecorationProps): JSX.Element | null {
|
||||||
|
// Listen to the notification state and update the component when it changes
|
||||||
const {
|
const {
|
||||||
hasAnyNotificationOrActivity,
|
hasAnyNotificationOrActivity,
|
||||||
isUnsetMessage,
|
isUnsentMessage,
|
||||||
invited,
|
invited,
|
||||||
isMention,
|
isMention,
|
||||||
isActivityNotification,
|
isActivityNotification,
|
||||||
isNotification,
|
isNotification,
|
||||||
count,
|
count,
|
||||||
muted,
|
muted,
|
||||||
} = notificationState;
|
} = useTypedEventEmitterState(notificationState, NotificationStateEvents.Update, () => ({
|
||||||
|
hasAnyNotificationOrActivity: notificationState.hasAnyNotificationOrActivity,
|
||||||
|
isUnsentMessage: notificationState.isUnsentMessage,
|
||||||
|
invited: notificationState.invited,
|
||||||
|
isMention: notificationState.isMention,
|
||||||
|
isActivityNotification: notificationState.isActivityNotification,
|
||||||
|
isNotification: notificationState.isNotification,
|
||||||
|
count: notificationState.count,
|
||||||
|
muted: notificationState.muted,
|
||||||
|
}));
|
||||||
|
|
||||||
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
|
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,7 +68,7 @@ export function NotificationDecoration({
|
|||||||
{...props}
|
{...props}
|
||||||
data-testid="notification-decoration"
|
data-testid="notification-decoration"
|
||||||
>
|
>
|
||||||
{isUnsetMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
|
{isUnsentMessage && <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)" />}
|
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||||
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||||
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, useState } from "react";
|
import React, { type JSX, memo, useState } from "react";
|
||||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
@ -29,7 +29,11 @@ interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElem
|
|||||||
/**
|
/**
|
||||||
* An item in the room list
|
* An item in the room list
|
||||||
*/
|
*/
|
||||||
export function RoomListItemView({ room, isSelected, ...props }: RoomListItemViewPropsProps): JSX.Element {
|
export const RoomListItemView = memo(function RoomListItemView({
|
||||||
|
room,
|
||||||
|
isSelected,
|
||||||
|
...props
|
||||||
|
}: RoomListItemViewPropsProps): JSX.Element {
|
||||||
const vm = useRoomListItemViewModel(room);
|
const vm = useRoomListItemViewModel(room);
|
||||||
|
|
||||||
const [isHover, setIsHover] = useState(false);
|
const [isHover, setIsHover] = useState(false);
|
||||||
@ -38,9 +42,7 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
|||||||
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
||||||
const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu;
|
const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu;
|
||||||
|
|
||||||
const isNotificationDecorationVisible =
|
const isNotificationDecorationVisible = !showHoverDecoration && vm.showNotificationDecoration;
|
||||||
!showHoverDecoration &&
|
|
||||||
(vm.notificationState.hasAnyNotificationOrActivity || vm.notificationState.muted || vm.hasParticipantInCall);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -71,8 +73,8 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
|||||||
justify="space-between"
|
justify="space-between"
|
||||||
>
|
>
|
||||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||||
<span className="mx_RoomListItemView_roomName" title={room.name}>
|
<span className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||||
{room.name}
|
{vm.name}
|
||||||
</span>
|
</span>
|
||||||
{showHoverDecoration ? (
|
{showHoverDecoration ? (
|
||||||
<RoomListItemMenuView
|
<RoomListItemMenuView
|
||||||
@ -86,15 +88,17 @@ export function RoomListItemView({ room, isSelected, ...props }: RoomListItemVie
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||||
<NotificationDecoration
|
{vm.showNotificationDecoration && (
|
||||||
notificationState={vm.notificationState}
|
<NotificationDecoration
|
||||||
aria-hidden={true}
|
notificationState={vm.notificationState}
|
||||||
hasVideoCall={vm.hasParticipantInCall}
|
aria-hidden={true}
|
||||||
/>
|
hasVideoCall={vm.hasParticipantInCall}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@ -62,9 +62,9 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the notification is an unset message.
|
* True if the notification is an unsent message.
|
||||||
*/
|
*/
|
||||||
public get isUnsetMessage(): boolean {
|
public get isUnsentMessage(): boolean {
|
||||||
return this.level === NotificationLevel.Unsent;
|
return this.level === NotificationLevel.Unsent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||||
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||||
|
import * as UseCallModule from "../../../../../src/hooks/useCall";
|
||||||
|
|
||||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||||
@ -86,6 +87,49 @@ describe("RoomListItemViewModel", () => {
|
|||||||
expect(vm.current.showHoverMenu).toBe(true);
|
expect(vm.current.showHoverMenu).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("a11yLabel", () => {
|
describe("a11yLabel", () => {
|
||||||
let notificationState: RoomNotificationState;
|
let notificationState: RoomNotificationState;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -96,7 +140,7 @@ describe("RoomListItemViewModel", () => {
|
|||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
label: "unsent message",
|
label: "unsent message",
|
||||||
mock: () => jest.spyOn(notificationState, "isUnsetMessage", "get").mockReturnValue(true),
|
mock: () => jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true),
|
||||||
expected: "Open room roomName with an unsent message.",
|
expected: "Open room roomName with an unsent message.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,60 +8,94 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen } from "jest-matrix-react";
|
import { render, screen } from "jest-matrix-react";
|
||||||
|
|
||||||
import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||||
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
|
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
|
||||||
|
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||||
|
|
||||||
describe("<NotificationDecoration />", () => {
|
describe("<NotificationDecoration />", () => {
|
||||||
it("should not render if RoomNotificationState.isSilent=true", () => {
|
let roomNotificationState: RoomNotificationState;
|
||||||
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
|
beforeEach(() => {
|
||||||
render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
const matrixClient = createTestClient();
|
||||||
|
const room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||||
|
roomNotificationState = new RoomNotificationState(room, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
|
||||||
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||||
|
render(<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />);
|
||||||
expect(screen.queryByTestId("notification-decoration")).toBeNull();
|
expect(screen.queryByTestId("notification-decoration")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the unset message decoration", () => {
|
it("should render the unset message decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, isUnsetMessage: true } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the invitation decoration", () => {
|
it("should render the invitation decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, invited: true } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the mention decoration", () => {
|
it("should render the mention decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, isMention: true, count: 1 } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
|
||||||
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the notification decoration", () => {
|
it("should render the notification decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 1 } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||||
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the notification decoration without count", () => {
|
it("should render the notification decoration without count", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 0 } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||||
|
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the activity decoration", () => {
|
it("should render the activity decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, isActivityNotification: true } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the muted decoration", () => {
|
it("should render the muted decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: true, muted: true } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
|
||||||
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("should render the video decoration", () => {
|
it("should render the video decoration", () => {
|
||||||
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
|
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={true} />);
|
const { asFragment } = render(
|
||||||
|
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={true} />,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,7 +28,6 @@ describe("<RoomListItemView />", () => {
|
|||||||
let defaultValue: RoomListItemViewState;
|
let defaultValue: RoomListItemViewState;
|
||||||
let matrixClient: MatrixClient;
|
let matrixClient: MatrixClient;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
matrixClient = stubClient();
|
matrixClient = stubClient();
|
||||||
room = mkRoom(matrixClient, "room1");
|
room = mkRoom(matrixClient, "room1");
|
||||||
@ -36,15 +35,22 @@ describe("<RoomListItemView />", () => {
|
|||||||
DMRoomMap.makeShared(matrixClient);
|
DMRoomMap.makeShared(matrixClient);
|
||||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
|
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
|
||||||
|
|
||||||
|
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 = {
|
defaultValue = {
|
||||||
openRoom: jest.fn(),
|
openRoom: jest.fn(),
|
||||||
showHoverMenu: false,
|
showHoverMenu: false,
|
||||||
notificationState: new RoomNotificationState(room, false),
|
notificationState,
|
||||||
a11yLabel: "Open room room1",
|
a11yLabel: "Open room room1",
|
||||||
isBold: false,
|
isBold: false,
|
||||||
isVideoRoom: false,
|
isVideoRoom: false,
|
||||||
callConnectionState: null,
|
callConnectionState: null,
|
||||||
hasParticipantInCall: false,
|
hasParticipantInCall: false,
|
||||||
|
name: room.name,
|
||||||
|
showNotificationDecoration: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);
|
mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);
|
||||||
@ -84,4 +90,30 @@ describe("<RoomListItemView />", () => {
|
|||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should display notification decoration", async () => {
|
||||||
|
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||||
|
...defaultValue,
|
||||||
|
showNotificationDecoration: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { asFragment } = render(<RoomListItemView room={room} isSelected={false} />);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RoomListItemView room={room} isSelected={false} />);
|
||||||
|
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||||
|
await user.hover(listItem);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("notification-decoration")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -47,6 +47,65 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
|
|||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<button
|
||||||
|
aria-label="Open room room1"
|
||||||
|
aria-selected="false"
|
||||||
|
class="mx_RoomListItemView mx_RoomListItemView_notification_decoration"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
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;"
|
||||||
|
>
|
||||||
|
<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;"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
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;"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_RoomListItemView_roomName"
|
||||||
|
title="room1"
|
||||||
|
>
|
||||||
|
room1
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
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;"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="_unread-counter_9mg0k_8"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<RoomListItemView /> should render a room item 1`] = `
|
exports[`<RoomListItemView /> should render a room item 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -223,7 +223,7 @@ describe("RoomNotificationState", () => {
|
|||||||
it("should has isUnsetMessage at true", () => {
|
it("should has isUnsetMessage at true", () => {
|
||||||
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||||
const roomNotifState = new RoomNotificationState(room, false);
|
const roomNotifState = new RoomNotificationState(room, false);
|
||||||
expect(roomNotifState.isUnsetMessage).toBe(true);
|
expect(roomNotifState.isUnsentMessage).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should has isMention at false if the notification is invitation, an unset message or a knock", () => {
|
it("should has isMention at false if the notification is invitation, an unset message or a knock", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user