From 3e39e358f06b15393e291256baeb67db733bbbc0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 15 Apr 2025 15:16:53 +0200 Subject: [PATCH 01/16] fix: avoid extra render in the new room list --- .../views/rooms/RoomListPanel/RoomListItemView.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 89a353afb17..1dde5774c6b 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -5,7 +5,7 @@ * 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 classNames from "classnames"; @@ -29,7 +29,11 @@ interface RoomListItemViewPropsProps extends React.HTMLAttributes ); -} +}); From 428498a248119e47872ceafc08b969e27ec1283c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 16 Apr 2025 14:11:53 +0200 Subject: [PATCH 02/16] fix: listen to room name changes --- .../roomlist/RoomListItemViewModel.tsx | 22 ++++++++++++------- .../rooms/RoomListPanel/RoomListItemView.tsx | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index d20c834c70c..cce0f99d9c3 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -22,6 +22,10 @@ import { useCall, useConnectionState, useParticipantCount } from "../../../hooks import { type ConnectionState } from "../../../models/Call"; export interface RoomListItemViewState { + /** + * The name of the room. + */ + name: string; /** * Whether the hover menu should be shown. */ @@ -65,10 +69,11 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { const matrixClient = useMatrixClientContext(); const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); const isArchived = Boolean(roomTags[DefaultTagID.Archived]); + const name = useEventEmitterState(room, RoomEvent.Name, () => room.name); const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); const invited = notificationState.invited; - const a11yLabel = getA11yLabel(room, notificationState); + const a11yLabel = getA11yLabel(name, notificationState); const isBold = notificationState.hasAnyNotificationOrActivity; // We don't want to show the hover menu if @@ -97,6 +102,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { }, [room]); return { + name, notificationState, showHoverMenu, openRoom, @@ -110,29 +116,29 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { /** * Get the a11y label for the room list item - * @param room + * @param roomName * @param notificationState */ -function getA11yLabel(room: Room, notificationState: RoomNotificationState): string { +function getA11yLabel(roomName: string, notificationState: RoomNotificationState): string { if (notificationState.isUnsetMessage) { return _t("a11y|room_messsage_not_sent", { - roomName: room.name, + roomName, }); } else if (notificationState.invited) { return _t("a11y|room_n_unread_invite", { - roomName: room.name, + roomName, }); } else if (notificationState.isMention) { return _t("a11y|room_n_unread_messages_mentions", { - roomName: room.name, + roomName, count: notificationState.count, }); } else if (notificationState.hasUnreadCount) { return _t("a11y|room_n_unread_messages", { - roomName: room.name, + roomName, count: notificationState.count, }); } else { - return _t("room_list|room|open_room", { roomName: room.name }); + return _t("room_list|room|open_room", { roomName }); } } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 1dde5774c6b..4cd50edd285 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -75,8 +75,8 @@ export const RoomListItemView = memo(function RoomListItemView({ justify="space-between" > {/* We truncate the room name when too long. Title here is to show the full name on hover */} - - {room.name} + + {vm.name} {showHoverDecoration ? ( Date: Wed, 16 Apr 2025 14:53:38 +0200 Subject: [PATCH 03/16] fix: trigger render when notification state change --- .../viewmodels/roomlist/RoomListItemViewModel.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index cce0f99d9c3..4c9c35a32dd 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import dispatcher from "../../../dispatcher/dispatcher"; @@ -16,10 +16,11 @@ import { _t } from "../../../languageHandler"; import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import { useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { DefaultTagID } from "../../../stores/room-list/models"; import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall"; import { type ConnectionState } from "../../../models/Call"; +import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; export interface RoomListItemViewState { /** @@ -72,6 +73,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { const name = useEventEmitterState(room, RoomEvent.Name, () => room.name); const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); + // force re-render on notification state change + const [, triggerRender] = useState({}); + useTypedEventEmitter(notificationState, NotificationStateEvents.Update, () => triggerRender({})); const invited = notificationState.invited; const a11yLabel = getA11yLabel(name, notificationState); const isBold = notificationState.hasAnyNotificationOrActivity; From 5c49c2f0de979fa68007e5080a7dbb58134200ab Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 16 Apr 2025 15:05:41 +0200 Subject: [PATCH 04/16] test: fix room list item tests --- .../views/rooms/RoomListPanel/RoomListItemView-test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index 9cf2cefa7b1..1f0ca8e0a44 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -45,6 +45,7 @@ describe("", () => { isVideoRoom: false, callConnectionState: null, hasParticipantInCall: false, + name: room.name, }; mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); From 8760757e82ea31940ce34d738a3898af1c3a4f62 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 16 Apr 2025 16:23:18 +0200 Subject: [PATCH 05/16] chore: fix typo `RoomNotificationState.isUnsentMessage` --- src/components/viewmodels/roomlist/RoomListItemViewModel.tsx | 2 +- src/components/views/rooms/NotificationDecoration.tsx | 4 ++-- src/stores/notifications/RoomNotificationState.ts | 4 ++-- .../viewmodels/roomlist/RoomListItemViewModel-test.tsx | 2 +- .../components/views/rooms/NotificationDecoration-test.tsx | 2 +- .../stores/notifications/RoomNotificationState-test.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index 4c9c35a32dd..59ab2e01038 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -124,7 +124,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { * @param notificationState */ function getA11yLabel(roomName: string, notificationState: RoomNotificationState): string { - if (notificationState.isUnsetMessage) { + if (notificationState.isUnsentMessage) { return _t("a11y|room_messsage_not_sent", { roomName, }); diff --git a/src/components/views/rooms/NotificationDecoration.tsx b/src/components/views/rooms/NotificationDecoration.tsx index 9cc1bee738b..8e1fbded37c 100644 --- a/src/components/views/rooms/NotificationDecoration.tsx +++ b/src/components/views/rooms/NotificationDecoration.tsx @@ -37,7 +37,7 @@ export function NotificationDecoration({ }: NotificationDecorationProps): JSX.Element | null { const { hasAnyNotificationOrActivity, - isUnsetMessage, + isUnsentMessage, invited, isMention, isActivityNotification, @@ -55,7 +55,7 @@ export function NotificationDecoration({ {...props} data-testid="notification-decoration" > - {isUnsetMessage && } + {isUnsentMessage && } {hasVideoCall && } {invited && } {isMention && } diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index 3447257e96a..af873f712e8 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -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; } diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index be309b36ed6..7c5a16eb31a 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -96,7 +96,7 @@ describe("RoomListItemViewModel", () => { it.each([ { 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.", }, { diff --git a/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx b/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx index 9687ed86ec6..fb79a088052 100644 --- a/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx +++ b/test/unit-tests/components/views/rooms/NotificationDecoration-test.tsx @@ -19,7 +19,7 @@ describe("", () => { }); it("should render the unset message decoration", () => { - const state = { hasAnyNotificationOrActivity: true, isUnsetMessage: true } as RoomNotificationState; + const state = { hasAnyNotificationOrActivity: true, isUnsentMessage: true } as RoomNotificationState; const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/test/unit-tests/stores/notifications/RoomNotificationState-test.ts b/test/unit-tests/stores/notifications/RoomNotificationState-test.ts index f9ed754c178..91d13809ec6 100644 --- a/test/unit-tests/stores/notifications/RoomNotificationState-test.ts +++ b/test/unit-tests/stores/notifications/RoomNotificationState-test.ts @@ -223,7 +223,7 @@ describe("RoomNotificationState", () => { it("should has isUnsetMessage at true", () => { jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]); 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", () => { From 7daf3c6854c910ea1b0277432bbc2d2e68de222c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 16 Apr 2025 16:26:14 +0200 Subject: [PATCH 06/16] refactor: move `isNotificationDecorationVisible` into `useRoomListItemViewModel` --- .../viewmodels/roomlist/RoomListItemViewModel.tsx | 8 ++++++++ .../views/rooms/RoomListPanel/RoomListItemView.tsx | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index 59ab2e01038..441eef2b5b3 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -60,6 +60,10 @@ export interface RoomListItemViewState { * Whether there are participants in the call. */ hasParticipantInCall: boolean; + /** + * Whether the notification decoration should be shown. + */ + isNotificationDecorationVisible: boolean; } /** @@ -95,6 +99,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { const hasParticipantInCall = useParticipantCount(call) > 0; const callConnectionState = call ? connectionState : null; + const isNotificationDecorationVisible = + notificationState.hasAnyNotificationOrActivity || notificationState.muted || hasParticipantInCall; + // Actions const openRoom = useCallback((): void => { @@ -115,6 +122,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { isVideoRoom, callConnectionState, hasParticipantInCall, + isNotificationDecorationVisible, }; } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 4cd50edd285..a092a37483a 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -42,9 +42,7 @@ export const RoomListItemView = memo(function RoomListItemView({ // Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu; - const isNotificationDecorationVisible = - !showHoverDecoration && - (vm.notificationState.hasAnyNotificationOrActivity || vm.notificationState.muted || vm.hasParticipantInCall); + const isNotificationDecorationVisible = !showHoverDecoration && vm.isNotificationDecorationVisible; return ( + +`; + exports[` should render a room item 1`] = `