From 9220a0c1037f0c36ad78311b360b3bc3446bab62 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 9 Dec 2022 09:05:39 -0500 Subject: [PATCH 1/2] Mark a thread as unread in the threads list due to activity. --- src/Unread.ts | 2 +- src/hooks/useUnreadNotifications.ts | 13 +++-- .../components/views/rooms/EventTile-test.tsx | 3 +- .../UnreadNotificationBadge-test.tsx | 55 ++++++++++++++++--- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/Unread.ts b/src/Unread.ts index e492c7890fc..925a5e4bdbc 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -70,7 +70,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { return false; } -function doesRoomOrThreadHaveUnreadMessages(room: Room | Thread): boolean { +export function doesRoomOrThreadHaveUnreadMessages(room: Room | Thread): boolean { const myUserId = MatrixClientPeg.get().getUserId(); // as we don't send RRs for our own messages, make sure we special case that diff --git a/src/hooks/useUnreadNotifications.ts b/src/hooks/useUnreadNotifications.ts index 5510eae46a3..f956f286284 100644 --- a/src/hooks/useUnreadNotifications.ts +++ b/src/hooks/useUnreadNotifications.ts @@ -15,12 +15,13 @@ 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 { useCallback, useEffect, useState } from "react"; import { getUnsentMessages } from "../components/structures/RoomStatusBar"; import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs"; import { NotificationColor } from "../stores/notifications/NotificationColor"; -import { doesRoomHaveUnreadMessages } from "../Unread"; +import { doesRoomOrThreadHaveUnreadMessages } from "../Unread"; import { EffectiveMembership, getEffectiveMembership } from "../utils/membership"; import { useEventEmitter } from "./useEventEmitter"; @@ -70,12 +71,14 @@ export const useUnreadNotifications = (room: Room, threadId?: string): { setColor(NotificationColor.Red); } else if (greyNotifs > 0) { setColor(NotificationColor.Grey); - } else if (!threadId) { - // TODO: No support for `Bold` on threads at the moment - + } else { // We don't have any notified messages, but we might have unread messages. Let's // find out. - const hasUnread = doesRoomHaveUnreadMessages(room); + let roomOrThread: Room | Thread = room; + if (threadId) { + roomOrThread = room.getThread(threadId)!; + } + const hasUnread = doesRoomOrThreadHaveUnreadMessages(roomOrThread); setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None); } } diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index dd0cda23b4c..4a921b8b181 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -133,7 +133,8 @@ describe("EventTile", () => { it("shows an unread notification bage", () => { const { container } = getComponent({}, TimelineRenderingType.ThreadsList); - expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0); + // By default, the thread will assume there's unread activity in it. + expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1); act(() => { room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3); diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index 20289dc6b91..2d64c94aafb 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -17,15 +17,16 @@ limitations under the License. import React from "react"; import "jest-mock"; import { screen, act, render } from "@testing-library/react"; -import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { MsgType } from "matrix-js-sdk/src/matrix"; +import { PendingEventOrdering } from "matrix-js-sdk/src/client"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; -import { mocked } from "jest-mock"; import { EventStatus } from "matrix-js-sdk/src/models/event-status"; import { UnreadNotificationBadge, } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge"; -import { mkMessage, stubClient } from "../../../../test-utils/test-utils"; +import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils"; +import { mkThread } from "../../../../test-utils/threads"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import * as RoomNotifs from "../../../../../src/RoomNotifs"; @@ -36,28 +37,35 @@ jest.mock('../../../../../src/RoomNotifs', () => ({ })); const ROOM_ID = "!roomId:example.org"; -let THREAD_ID; +let THREAD_ID: string; describe("UnreadNotificationBadge", () => { - let mockClient: MatrixClient; + stubClient(); + const client = MatrixClientPeg.get(); let room: Room; function getComponent(threadId?: string) { return ; } + beforeAll(() => { + client.supportsExperimentalThreads = () => true; + }); + beforeEach(() => { jest.clearAllMocks(); - stubClient(); - mockClient = mocked(MatrixClientPeg.get()); - - room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + room = new Room(ROOM_ID, client, client.getUserId()!, { pendingEventOrdering: PendingEventOrdering.Detached, }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + const { rootEvent } = mkThread( + { room, client, authorId: client.getUserId()!, participantUserIds: [client.getUserId()!] }, + ); + THREAD_ID = rootEvent.getId()!; + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1); room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); @@ -129,4 +137,33 @@ describe("UnreadNotificationBadge", () => { 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); + + // 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: "m.thread", + }, + }, + }); + room.addLiveEvents([event]); + }); + + const { container } = render(getComponent(THREAD_ID)); + expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy(); + expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy(); + expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy(); + }); }); From 73bb85103b32c3a66082b95709f64871c56bf175 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 12 Dec 2022 11:23:40 -0500 Subject: [PATCH 2/2] Show activity in the threads icon. --- .../views/right_panel/RoomHeaderButtons.tsx | 13 +++++++++++-- .../views/right_panel/RoomHeaderButtons-test.tsx | 9 +++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0ba64c2f5e6..be42e44dc21 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -44,6 +44,7 @@ import { NotificationStateEvents } from "../../../stores/notifications/Notificat import PosthogTrackers from "../../../PosthogTrackers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread"; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, @@ -192,9 +193,17 @@ export default class RoomHeaderButtons extends HeaderButtons { return NotificationColor.Red; case NotificationCountType.Total: return NotificationColor.Grey; - default: - return NotificationColor.None; } + // We don't have any notified messages, but we might have unread messages. Let's + // find out. + for (const thread of this.props.room!.getThreads()) { + // If the current thread has unread messages, we're done. + if (doesRoomOrThreadHaveUnreadMessages(thread)) { + return NotificationColor.Bold; + } + } + // Otherwise, no notification color. + return NotificationColor.None; } private onUpdateStatus = (notificationState: SummarizedNotificationState): void => { diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx index 4d8537fdba2..4cee6d354d8 100644 --- a/test/components/views/right_panel/RoomHeaderButtons-test.tsx +++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx @@ -24,6 +24,7 @@ import RoomHeaderButtons from "../../../../src/components/views/right_panel/Room import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { stubClient } from "../../../test-utils"; +import { mkThread } from "../../../test-utils/threads"; describe("RoomHeaderButtons-test.tsx", function() { const ROOM_ID = "!roomId:example.org"; @@ -55,7 +56,7 @@ describe("RoomHeaderButtons-test.tsx", function() { return container.querySelector(".mx_RightPanel_threadsButton"); } - function isIndicatorOfType(container, type: "red" | "gray") { + function isIndicatorOfType(container, type: "red" | "gray" | "bold") { return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator") .className .includes(type); @@ -81,7 +82,7 @@ describe("RoomHeaderButtons-test.tsx", function() { expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); }); - it("room wide notification does not change the thread button", () => { + it.only("thread notification does change the thread button", () => { const { container } = getComponent(room); room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1); @@ -94,6 +95,10 @@ describe("RoomHeaderButtons-test.tsx", function() { room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0); expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); + + // Thread activity should appear on the icon. + mkThread({ room, client, authorId: client.getUserId()!, participantUserIds: ["@alice:example.org"] }); + expect(isIndicatorOfType(getComponent(room), "bold")).toBe(true); }); it("does not explode without a room", () => {