Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 82fdbac

Browse files
committed
Add a badge to the threads icon if any threads are unread.
1 parent ad4feef commit 82fdbac

File tree

2 files changed

+115
-5
lines changed

2 files changed

+115
-5
lines changed

src/components/views/right_panel/RoomHeaderButtons.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ limitations under the License.
2121
import React from "react";
2222
import classNames from "classnames";
2323
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
24+
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
2425
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
2526

2627
import { _t } from "../../../languageHandler";
@@ -44,6 +45,7 @@ import { NotificationStateEvents } from "../../../stores/notifications/Notificat
4445
import PosthogTrackers from "../../../PosthogTrackers";
4546
import { ButtonEvent } from "../elements/AccessibleButton";
4647
import { MatrixClientPeg } from "../../../MatrixClientPeg";
48+
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
4749

4850
const ROOM_INFO_PHASES = [
4951
RightPanelPhases.RoomSummary,
@@ -154,7 +156,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
154156
if (!this.supportsThreadNotifications) {
155157
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
156158
} else {
159+
// Notification badge may change if the notification counts from the
160+
// server change, if a new thread is created or updated, or if a
161+
// receipt is sent in the thread.
157162
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
163+
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
164+
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
165+
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
166+
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
167+
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
168+
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
169+
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
158170
}
159171
this.onNotificationUpdate();
160172
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
@@ -166,6 +178,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
166178
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
167179
} else {
168180
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
181+
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
182+
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
183+
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
184+
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
185+
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
186+
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
187+
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
169188
}
170189
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
171190
}
@@ -191,9 +210,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
191210
return NotificationColor.Red;
192211
case NotificationCountType.Total:
193212
return NotificationColor.Grey;
194-
default:
195-
return NotificationColor.None;
196213
}
214+
// We don't have any notified messages, but we might have unread messages. Let's
215+
// find out.
216+
for (const thread of this.props.room!.getThreads()) {
217+
// If the current thread has unread messages, we're done.
218+
if (doesRoomOrThreadHaveUnreadMessages(thread)) {
219+
return NotificationColor.Bold;
220+
}
221+
}
222+
// Otherwise, no notification color.
223+
return NotificationColor.None;
197224
}
198225

199226
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {

test/components/views/right_panel/RoomHeaderButtons-test.tsx

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ limitations under the License.
1515
*/
1616

1717
import { render } from "@testing-library/react";
18+
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
1819
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
1920
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
2021
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
22+
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
2123
import React from "react";
2224

2325
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
2426
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
2527
import SettingsStore from "../../../../src/settings/SettingsStore";
26-
import { stubClient } from "../../../test-utils";
28+
import { mkEvent, stubClient } from "../../../test-utils";
29+
import { mkThread } from "../../../test-utils/threads";
2730

2831
describe("RoomHeaderButtons-test.tsx", function () {
2932
const ROOM_ID = "!roomId:example.org";
@@ -35,6 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
3538

3639
stubClient();
3740
client = MatrixClientPeg.get();
41+
client.supportsExperimentalThreads = () => true;
3842
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
3943
pendingEventOrdering: PendingEventOrdering.Detached,
4044
});
@@ -52,7 +56,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
5256
return container.querySelector(".mx_RightPanel_threadsButton");
5357
}
5458

55-
function isIndicatorOfType(container, type: "red" | "gray") {
59+
function isIndicatorOfType(container, type: "red" | "gray" | "bold") {
5660
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator").className.includes(type);
5761
}
5862

@@ -76,7 +80,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
7680
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
7781
});
7882

79-
it("room wide notification does not change the thread button", () => {
83+
it("thread notification does change the thread button", () => {
8084
const { container } = getComponent(room);
8185

8286
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
@@ -91,6 +95,85 @@ describe("RoomHeaderButtons-test.tsx", function () {
9195
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
9296
});
9397

98+
it("thread activity does change the thread button", async () => {
99+
const { container } = getComponent(room);
100+
101+
// Thread activity should appear on the icon.
102+
const { rootEvent, events } = mkThread({
103+
room,
104+
client,
105+
authorId: client.getUserId()!,
106+
participantUserIds: ["@alice:example.org"],
107+
});
108+
expect(isIndicatorOfType(container, "bold")).toBe(true);
109+
110+
// Sending the last event should clear the notification.
111+
let event = mkEvent({
112+
event: true,
113+
type: "m.room.message",
114+
user: client.getUserId()!,
115+
room: room.roomId,
116+
content: {
117+
"msgtype": MsgType.Text,
118+
"body": "Test",
119+
"m.relates_to": {
120+
event_id: rootEvent.getId(),
121+
rel_type: RelationType.Thread,
122+
},
123+
},
124+
});
125+
room.addLiveEvents([event]);
126+
await expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
127+
128+
// Mark it as unread again.
129+
event = mkEvent({
130+
event: true,
131+
type: "m.room.message",
132+
user: "@alice:example.org",
133+
room: room.roomId,
134+
content: {
135+
"msgtype": MsgType.Text,
136+
"body": "Test",
137+
"m.relates_to": {
138+
event_id: rootEvent.getId(),
139+
rel_type: RelationType.Thread,
140+
},
141+
},
142+
});
143+
room.addLiveEvents([event]);
144+
expect(isIndicatorOfType(container, "bold")).toBe(true);
145+
146+
// Sending a read receipt on an earlier event shouldn't do anything.
147+
let receipt = new MatrixEvent({
148+
type: "m.receipt",
149+
room_id: room.roomId,
150+
content: {
151+
[events.at(-1).getId()!]: {
152+
[ReceiptType.Read]: {
153+
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
154+
},
155+
},
156+
},
157+
});
158+
room.addReceipt(receipt);
159+
expect(isIndicatorOfType(container, "bold")).toBe(true);
160+
161+
// Sending a receipt on the latest event should clear the notification.
162+
receipt = new MatrixEvent({
163+
type: "m.receipt",
164+
room_id: room.roomId,
165+
content: {
166+
[event.getId()!]: {
167+
[ReceiptType.Read]: {
168+
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
169+
},
170+
},
171+
},
172+
});
173+
room.addReceipt(receipt);
174+
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
175+
});
176+
94177
it("does not explode without a room", () => {
95178
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
96179
expect(() => getComponent()).not.toThrow();

0 commit comments

Comments
 (0)