element-web/test/RoomNotifs-test.ts
Florian Duros 77e1649f0b
Add labs flag for Threads Activity Centre (#12137)
* Add `Thread Activity centre` labs flag

* Rename translation string

* Update supportedLevels

* Fix labs subsection test

* Update Threads Activity Centre label

* Make threads activity centre labs flag split out unread counts

Just shows notif & unread counts for main thread if the TAC is enabled.

* Fix tests

* Simpler fix

* Pass in & cache the status of the TAC labs flag

* Pass includeThreads as setting to doesRoomHaveUnreadMessages too

* Fix tests

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-01-29 17:52:48 +00:00

333 lines
14 KiB
TypeScript

/*
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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
PushRuleActionName,
TweakName,
NotificationCountType,
Room,
EventStatus,
EventType,
MatrixEvent,
PendingEventOrdering,
} from "matrix-js-sdk/src/matrix";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mkEvent, mkRoom, mkRoomMember, muteRoom, stubClient, upsertRoomStateEvents } from "./test-utils";
import {
getRoomNotifsState,
RoomNotifState,
getUnreadNotificationCount,
determineUnreadState,
} from "../src/RoomNotifs";
import { NotificationLevel } from "../src/stores/notifications/NotificationLevel";
import SettingsStore from "../src/settings/SettingsStore";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
describe("RoomNotifs test", () => {
let client: jest.Mocked<MatrixClient>;
beforeEach(() => {
client = stubClient() as jest.Mocked<MatrixClient>;
});
it("getRoomNotifsState handles rules with no conditions", () => {
mocked(client).pushRules = {
global: {
override: [
{
rule_id: "!roomId:server",
enabled: true,
default: false,
actions: [],
},
],
},
};
expect(getRoomNotifsState(client, "!roomId:server")).toBe(null);
});
it("getRoomNotifsState handles guest users", () => {
mocked(client).isGuest.mockReturnValue(true);
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessages);
});
it("getRoomNotifsState handles mute state", () => {
const room = mkRoom(client, "!roomId:server");
muteRoom(room);
expect(getRoomNotifsState(client, room.roomId)).toBe(RoomNotifState.Mute);
});
it("getRoomNotifsState handles mute state for legacy DontNotify action", () => {
const room = mkRoom(client, "!roomId:server");
muteRoom(room);
client.pushRules!.global.override![0]!.actions = [PushRuleActionName.DontNotify];
expect(getRoomNotifsState(client, room.roomId)).toBe(RoomNotifState.Mute);
});
it("getRoomNotifsState handles mentions only", () => {
(client as any).getRoomPushRule = () => ({
rule_id: "!roomId:server",
enabled: true,
default: false,
actions: [PushRuleActionName.DontNotify],
});
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.MentionsOnly);
});
it("getRoomNotifsState handles noisy", () => {
(client as any).getRoomPushRule = () => ({
rule_id: "!roomId:server",
enabled: true,
default: false,
actions: [{ set_tweak: TweakName.Sound, value: "default" }],
});
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessagesLoud);
});
describe("getUnreadNotificationCount", () => {
const ROOM_ID = "!roomId:example.org";
const THREAD_ID = "$threadId";
let room: Room;
beforeEach(() => {
room = new Room(ROOM_ID, client, client.getUserId()!);
});
it("counts room notification type", () => {
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(0);
});
it("counts notifications type", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 2);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});
describe("when there is a room predecessor", () => {
const OLD_ROOM_ID = "!oldRoomId:example.org";
const mkCreateEvent = (predecessorId?: string): MatrixEvent => {
return mkEvent({
event: true,
type: "m.room.create",
room: ROOM_ID,
user: "@zoe:localhost",
content: {
...(predecessorId ? { predecessor: { room_id: predecessorId, event_id: "$someevent" } } : {}),
creator: "@zoe:localhost",
room_version: "5",
},
ts: Date.now(),
});
};
const mkPredecessorEvent = (predecessorId: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomPredecessor,
room: ROOM_ID,
user: "@zoe:localhost",
skey: "",
content: {
predecessor_room_id: predecessorId,
},
ts: Date.now(),
});
};
const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => {
it("and there is a predecessor in the create event, it should count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});
};
const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => {
it("and there is a predecessor event, it should count predecessor highlight", () => {
client.getVisibleRooms();
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});
};
beforeEach(() => {
room.setUnreadNotificationCount(NotificationCountType.Total, 2);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
const oldRoom = new Room(OLD_ROOM_ID, client, client.getUserId()!);
oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10);
oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6);
client.getRoom.mockImplementation((roomId: string | undefined): Room | null => {
if (roomId === room.roomId) return room;
if (roomId === OLD_ROOM_ID) return oldRoom;
return null;
});
});
describe("and dynamic room predecessors are disabled", () => {
itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent();
itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent();
it("and there is only a predecessor event, it should not count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});
});
describe("and dynamic room predecessors are enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent();
itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent();
it("and there is only a predecessor event, it should count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});
it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});
});
});
it("counts thread notification type", () => {
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false, THREAD_ID)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false, THREAD_ID)).toBe(0);
});
it("counts thread notifications type", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 2);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false, THREAD_ID)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false, THREAD_ID)).toBe(1);
});
});
describe("determineUnreadState", () => {
let room: Room;
beforeEach(() => {
room = new Room("!room-id:example.com", client, "@user:example.com", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
});
it("shows nothing by default", async () => {
const { level, symbol, count } = determineUnreadState(room);
expect(symbol).toBe(null);
expect(level).toBe(NotificationLevel.None);
expect(count).toBe(0);
});
it("indicates if there are unsent messages", async () => {
const event = mkEvent({
event: true,
type: "m.message",
user: "@user:example.org",
content: {},
});
event.status = EventStatus.NOT_SENT;
room.addPendingEvent(event, "txn");
const { level, symbol, count } = determineUnreadState(room);
expect(symbol).toBe("!");
expect(level).toBe(NotificationLevel.Unsent);
expect(count).toBeGreaterThan(0);
});
it("indicates the user has been invited to a channel", async () => {
room.updateMyMembership("invite");
const { level, symbol, count } = determineUnreadState(room);
expect(symbol).toBe("!");
expect(level).toBe(NotificationLevel.Highlight);
expect(count).toBeGreaterThan(0);
});
it("indicates the user knock has been denied", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
return name === "feature_ask_to_join";
});
const roomMember = mkRoomMember(room.roomId, MatrixClientPeg.get()!.getSafeUserId(), "leave", true, {
membership: "knock",
});
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
const { level, symbol, count } = determineUnreadState(room);
expect(symbol).toBe("!");
expect(level).toBe(NotificationLevel.Highlight);
expect(count).toBeGreaterThan(0);
});
it("shows nothing for muted channels", async () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 99);
room.setUnreadNotificationCount(NotificationCountType.Total, 99);
muteRoom(room);
const { level, count } = determineUnreadState(room);
expect(level).toBe(NotificationLevel.None);
expect(count).toBe(0);
});
it("uses the correct number of unreads", async () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 999);
const { level, count } = determineUnreadState(room);
expect(level).toBe(NotificationLevel.Notification);
expect(count).toBe(999);
});
it("uses the correct number of highlights", async () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 888);
const { level, count } = determineUnreadState(room);
expect(level).toBe(NotificationLevel.Highlight);
expect(count).toBe(888);
});
});
});