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

Commit

Permalink
Add RoomNotifs#determineUnreadState
Browse files Browse the repository at this point in the history
Intended as a singular replacement for the divergent implementations before.

Signed-off-by: Clark Fischer <[email protected]>
  • Loading branch information
clarkf committed Jan 20, 2023
1 parent 4866e4f commit 86adc52
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 39 deletions.
66 changes: 53 additions & 13 deletions src/RoomNotifs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016, 2019, 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.
Expand All @@ -16,18 +15,18 @@ limitations under the License.
*/

import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import {
ConditionKind,
IPushRule,
PushRuleActionName,
PushRuleKind,
TweakName,
} from "matrix-js-sdk/src/@types/PushRules";
import { NotificationCountType } from "matrix-js-sdk/src/models/room";
import { ConditionKind, PushRuleActionName, PushRuleKind, TweakName } from "matrix-js-sdk/src/@types/PushRules";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/matrix";

import type { IPushRule } from "matrix-js-sdk/src/@types/PushRules";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { NotificationColor } from "./stores/notifications/NotificationColor";
import { getUnsentMessages } from "./components/structures/RoomStatusBar";
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
import { getEffectiveMembership, EffectiveMembership } from "./utils/membership";

export enum RoomNotifState {
AllMessagesLoud = "all_messages_loud",
Expand All @@ -36,7 +35,7 @@ export enum RoomNotifState {
Mute = "mute",
}

export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNotifState {
export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNotifState | null {
if (client.isGuest()) return RoomNotifState.AllMessages;

// look through the override rules for a rule affecting this room:
Expand Down Expand Up @@ -177,7 +176,7 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr
return Promise.all(promises);
}

function findOverrideMuteRule(roomId: string): IPushRule {
function findOverrideMuteRule(roomId: string): IPushRule | null {
const cli = MatrixClientPeg.get();
if (!cli?.pushRules?.global?.override) {
return null;
Expand All @@ -201,3 +200,44 @@ function isRuleForRoom(roomId: string, rule: IPushRule): boolean {
function isMuteRule(rule: IPushRule): boolean {
return rule.actions.length === 1 && rule.actions[0] === PushRuleActionName.DontNotify;
}

export function determineUnreadState(
room: Room,
threadId?: string,
): { color: NotificationColor; symbol: string | null; count: number } {
if (getUnsentMessages(room, threadId).length > 0) {
return { symbol: "!", count: 1, color: NotificationColor.Unsent };
}

if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
return { symbol: "!", count: 1, color: NotificationColor.Red };
}

if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
return { symbol: null, count: 0, color: NotificationColor.None };
}

const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId);
const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId);

const trueCount = greyNotifs || redNotifs;
if (redNotifs > 0) {
return { symbol: null, count: trueCount, color: NotificationColor.Red };
}

if (greyNotifs > 0) {
return { symbol: null, count: trueCount, color: NotificationColor.Grey };
}

// We don't have any notified messages, but we might have unread messages. Let's
// find out.
let hasUnread = false;
if (threadId) hasUnread = doesRoomOrThreadHaveUnreadMessages(room.getThread(threadId)!);
else hasUnread = doesRoomHaveUnreadMessages(room);

return {
symbol: null,
count: trueCount,
color: hasUnread ? NotificationColor.Bold : NotificationColor.None,
};
}
127 changes: 101 additions & 26 deletions test/RoomNotifs-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ limitations under the License.
import { mocked } from "jest-mock";
import { PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { EventStatus, PendingEventOrdering } from "matrix-js-sdk/src/matrix";

import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { mkEvent, mkRoom, muteRoom, stubClient } from "./test-utils";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import { getRoomNotifsState, RoomNotifState, getUnreadNotificationCount } from "../src/RoomNotifs";
import {
getRoomNotifsState,
RoomNotifState,
getUnreadNotificationCount,
determineUnreadState,
} from "../src/RoomNotifs";
import { NotificationColor } from "../src/stores/notifications/NotificationColor";

describe("RoomNotifs test", () => {
let client: jest.Mocked<MatrixClient>;

beforeEach(() => {
stubClient();
client = stubClient() as jest.Mocked<MatrixClient>;
});

it("getRoomNotifsState handles rules with no conditions", () => {
const cli = MatrixClientPeg.get();
mocked(cli).pushRules = {
mocked(client).pushRules = {
global: {
override: [
{
Expand All @@ -42,53 +49,47 @@ describe("RoomNotifs test", () => {
],
},
};
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(null);
expect(getRoomNotifsState(client, "!roomId:server")).toBe(null);
});

it("getRoomNotifsState handles guest users", () => {
const cli = MatrixClientPeg.get();
mocked(cli).isGuest.mockReturnValue(true);
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.AllMessages);
mocked(client).isGuest.mockReturnValue(true);
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessages);
});

it("getRoomNotifsState handles mute state", () => {
const cli = MatrixClientPeg.get();
const room = mkRoom(cli, "!roomId:server");
const room = mkRoom(client, "!roomId:server");
muteRoom(room);
expect(getRoomNotifsState(cli, room.roomId)).toBe(RoomNotifState.Mute);
expect(getRoomNotifsState(client, room.roomId)).toBe(RoomNotifState.Mute);
});

it("getRoomNotifsState handles mentions only", () => {
const cli = MatrixClientPeg.get();
cli.getRoomPushRule = () => ({
(client as any).getRoomPushRule = () => ({
rule_id: "!roomId:server",
enabled: true,
default: false,
actions: [PushRuleActionName.DontNotify],
});
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.MentionsOnly);
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.MentionsOnly);
});

it("getRoomNotifsState handles noisy", () => {
const cli = MatrixClientPeg.get();
cli.getRoomPushRule = () => ({
(client as any).getRoomPushRule = () => ({
rule_id: "!roomId:server",
enabled: true,
default: false,
actions: [{ set_tweak: TweakName.Sound, value: "default" }],
});
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.AllMessagesLoud);
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessagesLoud);
});

describe("getUnreadNotificationCount", () => {
const ROOM_ID = "!roomId:example.org";
const THREAD_ID = "$threadId";

let cli: jest.Mocked<MatrixClient>;
let room: Room;
beforeEach(() => {
cli = MatrixClientPeg.get() as jest.Mocked<MatrixClient>;
room = new Room(ROOM_ID, cli, cli.getUserId()!);
room = new Room(ROOM_ID, client, client.getUserId()!);
});

it("counts room notification type", () => {
Expand All @@ -109,19 +110,19 @@ describe("RoomNotifs test", () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);

const OLD_ROOM_ID = "!oldRoomId:example.org";
const oldRoom = new Room(OLD_ROOM_ID, cli, cli.getUserId()!);
const oldRoom = new Room(OLD_ROOM_ID, client, client.getUserId()!);
oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10);
oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6);

cli.getRoom.mockReset().mockReturnValue(oldRoom);
client.getRoom.mockReset().mockReturnValue(oldRoom);

const predecessorEvent = mkEvent({
event: true,
type: "m.room.create",
room: ROOM_ID,
user: cli.getUserId()!,
user: client.getUserId()!,
content: {
creator: cli.getUserId(),
creator: client.getUserId(),
room_version: "5",
predecessor: {
room_id: OLD_ROOM_ID,
Expand Down Expand Up @@ -149,4 +150,78 @@ describe("RoomNotifs test", () => {
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, 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 { color, symbol, count } = determineUnreadState(room);

expect(symbol).toBe(null);
expect(color).toBe(NotificationColor.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 { color, symbol, count } = determineUnreadState(room);

expect(symbol).toBe("!");
expect(color).toBe(NotificationColor.Unsent);
expect(count).toBeGreaterThan(0);
});

it("indicates the user has been invited to a channel", async () => {
room.updateMyMembership("invite");

const { color, symbol, count } = determineUnreadState(room);

expect(symbol).toBe("!");
expect(color).toBe(NotificationColor.Red);
expect(count).toBeGreaterThan(0);
});

it("shows nothing for muted channels", async () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 99);
room.setUnreadNotificationCount(NotificationCountType.Total, 99);
muteRoom(room);

const { color, count } = determineUnreadState(room);

expect(color).toBe(NotificationColor.None);
expect(count).toBe(0);
});

it("uses the correct number of unreads", async () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 999);

const { color, count } = determineUnreadState(room);

expect(color).toBe(NotificationColor.Grey);
expect(count).toBe(999);
});

it("uses the correct number of highlights", async () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 888);

const { color, count } = determineUnreadState(room);

expect(color).toBe(NotificationColor.Red);
expect(count).toBe(888);
});
});
});

0 comments on commit 86adc52

Please sign in to comment.