From 2db9daf811fd8f781f8154d153dbd3af5c286b2e Mon Sep 17 00:00:00 2001 From: Clark Fischer Date: Thu, 19 Jan 2023 15:39:31 -0800 Subject: [PATCH] Unify room unread state determination Have both the class-based facility and the hook use the new unified logic in `RoomNotifs#determineUnreadState`. Addresses https://github.com/vector-im/element-web/issues/24229 Signed-off-by: Clark Fischer --- src/hooks/useUnreadNotifications.ts | 51 +++---------- .../notifications/RoomNotificationState.ts | 76 +++---------------- .../UnreadNotificationBadge-test.tsx | 62 ++++++--------- 3 files changed, 45 insertions(+), 144 deletions(-) diff --git a/src/hooks/useUnreadNotifications.ts b/src/hooks/useUnreadNotifications.ts index 22236d832f9..c5e0e3da165 100644 --- a/src/hooks/useUnreadNotifications.ts +++ b/src/hooks/useUnreadNotifications.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; -import { Thread } from "matrix-js-sdk/src/models/thread"; +import { RoomEvent } from "matrix-js-sdk/src/models/room"; import { useCallback, useEffect, useState } from "react"; -import { getUnsentMessages } from "../components/structures/RoomStatusBar"; -import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs"; -import { NotificationColor } from "../stores/notifications/NotificationColor"; -import { doesRoomOrThreadHaveUnreadMessages } from "../Unread"; -import { EffectiveMembership, getEffectiveMembership } from "../utils/membership"; +import type { NotificationCount, Room } from "matrix-js-sdk/src/models/room"; +import { determineUnreadState } from "../RoomNotifs"; +import type { NotificationColor } from "../stores/notifications/NotificationColor"; import { useEventEmitter } from "./useEventEmitter"; export const useUnreadNotifications = ( @@ -53,40 +50,10 @@ export const useUnreadNotifications = ( useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState()); const updateNotificationState = useCallback(() => { - if (getUnsentMessages(room, threadId).length > 0) { - setSymbol("!"); - setCount(1); - setColor(NotificationColor.Unsent); - } else if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) { - setSymbol("!"); - setCount(1); - setColor(NotificationColor.Red); - } else if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) { - setSymbol(null); - setCount(0); - setColor(NotificationColor.None); - } else { - const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId); - const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId); - - const trueCount = greyNotifs || redNotifs; - setCount(trueCount); - setSymbol(null); - if (redNotifs > 0) { - setColor(NotificationColor.Red); - } else if (greyNotifs > 0) { - setColor(NotificationColor.Grey); - } else { - // We don't have any notified messages, but we might have unread messages. Let's - // find out. - let roomOrThread: Room | Thread = room; - if (threadId) { - roomOrThread = room.getThread(threadId)!; - } - const hasUnread = doesRoomOrThreadHaveUnreadMessages(roomOrThread); - setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None); - } - } + const { symbol, count, color } = determineUnreadState(room, threadId); + setSymbol(symbol); + setCount(count); + setColor(color); }, [room, threadId]); useEffect(() => { diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index 1b3adeafcc2..b0358a34392 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; -import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { RoomEvent } from "matrix-js-sdk/src/models/room"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { NotificationColor } from "./NotificationColor"; -import { IDestroyable } from "../../utils/IDestroyable"; +import type { Room } from "matrix-js-sdk/src/models/room"; +import type { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import type { IDestroyable } from "../../utils/IDestroyable"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import * as RoomNotifs from "../../RoomNotifs"; -import * as Unread from "../../Unread"; import { NotificationState, NotificationStateEvents } from "./NotificationState"; -import { getUnsentMessages } from "../../components/structures/RoomStatusBar"; -import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; +import type { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState"; export class RoomNotificationState extends NotificationState implements IDestroyable { public constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) { @@ -49,10 +47,6 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.updateNotificationState(); } - private get roomIsInvite(): boolean { - return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite; - } - public destroy(): void { super.destroy(); const cli = this.room.client; @@ -112,58 +106,10 @@ export class RoomNotificationState extends NotificationState implements IDestroy private updateNotificationState(): void { const snapshot = this.snapshot(); - if (getUnsentMessages(this.room).length > 0) { - // When there are unsent messages we show a red `!` - this._color = NotificationColor.Unsent; - this._symbol = "!"; - this._count = 1; // not used, technically - } else if ( - RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute - ) { - // When muted we suppress all notification states, even if we have context on them. - this._color = NotificationColor.None; - this._symbol = null; - this._count = 0; - } else if (this.roomIsInvite) { - this._color = NotificationColor.Red; - this._symbol = "!"; - this._count = 1; // not used, technically - } else { - const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Highlight); - const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Total); - - // For a 'true count' we pick the grey notifications first because they include the - // red notifications. If we don't have a grey count for some reason we use the red - // count. If that count is broken for some reason, assume zero. This avoids us showing - // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). - const trueCount = greyNotifs ? greyNotifs : redNotifs ? redNotifs : 0; - - // Note: we only set the symbol if we have an actual count. We don't want to show - // zero on badges. - - if (redNotifs > 0) { - this._color = NotificationColor.Red; - this._count = trueCount; - this._symbol = null; // symbol calculated by component - } else if (greyNotifs > 0) { - this._color = NotificationColor.Grey; - this._count = trueCount; - this._symbol = null; // symbol calculated by component - } else { - // We don't have any notified messages, but we might have unread messages. Let's - // find out. - const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room); - if (hasUnread) { - this._color = NotificationColor.Bold; - } else { - this._color = NotificationColor.None; - } - - // no symbol or count for this state - this._count = 0; - this._symbol = null; - } - } + const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room); + this._color = color; + this._symbol = symbol; + this._count = count; // finally, publish an update if needed this.emitIfUpdated(snapshot); diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index cfa44165765..2ff0e60343d 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,36 +23,26 @@ import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { EventStatus } from "matrix-js-sdk/src/models/event-status"; import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; +import type { MatrixClient } from "matrix-js-sdk/src/client"; import { mkThread } from "../../../../test-utils/threads"; import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge"; -import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils"; -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { mkEvent, mkMessage, muteRoom, stubClient } from "../../../../test-utils/test-utils"; import * as RoomNotifs from "../../../../../src/RoomNotifs"; -jest.mock("../../../../../src/RoomNotifs"); -jest.mock("../../../../../src/RoomNotifs", () => ({ - ...(jest.requireActual("../../../../../src/RoomNotifs") as Object), - getRoomNotifsState: jest.fn(), -})); - const ROOM_ID = "!roomId:example.org"; let THREAD_ID: string; describe("UnreadNotificationBadge", () => { - stubClient(); - const client = MatrixClientPeg.get(); + let client: MatrixClient; let room: Room; function getComponent(threadId?: string) { return ; } - beforeAll(() => { - client.supportsExperimentalThreads = () => true; - }); - beforeEach(() => { - jest.clearAllMocks(); + client = stubClient(); + client.supportsExperimentalThreads = () => true; room = new Room(ROOM_ID, client, client.getUserId()!, { pendingEventOrdering: PendingEventOrdering.Detached, @@ -145,41 +135,39 @@ describe("UnreadNotificationBadge", () => { }); it("adds a warning for invites", () => { - jest.spyOn(room, "getMyMembership").mockReturnValue("invite"); + room.updateMyMembership("invite"); render(getComponent()); expect(screen.queryByText("!")).not.toBeNull(); }); it("hides counter for muted rooms", () => { - jest.spyOn(RoomNotifs, "getRoomNotifsState").mockReset().mockReturnValue(RoomNotifs.RoomNotifState.Mute); + muteRoom(room); const { container } = render(getComponent()); expect(container.querySelector(".mx_NotificationBadge")).toBeNull(); }); it("activity renders unread notification badge", () => { - act(() => { - room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0); - room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); - // Add another event on the thread which is not sent by us. - const event = mkEvent({ - event: true, - type: "m.room.message", - user: "@alice:server.org", - room: room.roomId, - content: { - "msgtype": MsgType.Text, - "body": "Hello from Bob", - "m.relates_to": { - event_id: THREAD_ID, - rel_type: RelationType.Thread, - }, + // Add another event on the thread which is not sent by us. + const event = mkEvent({ + event: true, + type: "m.room.message", + user: "@alice:server.org", + room: room.roomId, + content: { + "msgtype": MsgType.Text, + "body": "Hello from Bob", + "m.relates_to": { + event_id: THREAD_ID, + rel_type: RelationType.Thread, }, - ts: 5, - }); - room.addLiveEvents([event]); + }, + ts: 5, }); + room.addLiveEvents([event]); const { container } = render(getComponent(THREAD_ID)); expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();