Unify unread notification state determination (#9941)
* Add tests for unread notification facilities Add some tests to guarantee some consistency in `useUnreadNotifications` and `RoomNotificationState`. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Add RoomNotifs#determineUnreadState Intended as a singular replacement for the divergent implementations before. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * 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 <clark.fischer@gmail.com> --------- Signed-off-by: Clark Fischer <clark.fischer@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
53a9b6447b
commit
431afaafc6
9 changed files with 499 additions and 231 deletions
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2019, 2023 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,18 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType } from "matrix-js-sdk/src/models/room";
|
||||||
import {
|
import { ConditionKind, PushRuleActionName, PushRuleKind, TweakName } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
ConditionKind,
|
|
||||||
IPushRule,
|
|
||||||
PushRuleActionName,
|
|
||||||
PushRuleKind,
|
|
||||||
TweakName,
|
|
||||||
} from "matrix-js-sdk/src/@types/PushRules";
|
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
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 { 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 {
|
export enum RoomNotifState {
|
||||||
AllMessagesLoud = "all_messages_loud",
|
AllMessagesLoud = "all_messages_loud",
|
||||||
|
@ -36,7 +35,7 @@ export enum RoomNotifState {
|
||||||
Mute = "mute",
|
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;
|
if (client.isGuest()) return RoomNotifState.AllMessages;
|
||||||
|
|
||||||
// look through the override rules for a rule affecting this room:
|
// look through the override rules for a rule affecting this room:
|
||||||
|
@ -177,7 +176,7 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId: string): IPushRule {
|
function findOverrideMuteRule(roomId: string): IPushRule | null {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli?.pushRules?.global?.override) {
|
if (!cli?.pushRules?.global?.override) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -201,3 +200,44 @@ function isRuleForRoom(roomId: string, rule: IPushRule): boolean {
|
||||||
function isMuteRule(rule: IPushRule): boolean {
|
function isMuteRule(rule: IPushRule): boolean {
|
||||||
return rule.actions.length === 1 && rule.actions[0] === PushRuleActionName.DontNotify;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
import type { NotificationCount, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
import { determineUnreadState } from "../RoomNotifs";
|
||||||
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
import type { NotificationColor } from "../stores/notifications/NotificationColor";
|
||||||
import { doesRoomOrThreadHaveUnreadMessages } from "../Unread";
|
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
|
||||||
import { useEventEmitter } from "./useEventEmitter";
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
export const useUnreadNotifications = (
|
export const useUnreadNotifications = (
|
||||||
|
@ -53,40 +50,10 @@ export const useUnreadNotifications = (
|
||||||
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());
|
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());
|
||||||
|
|
||||||
const updateNotificationState = useCallback(() => {
|
const updateNotificationState = useCallback(() => {
|
||||||
if (getUnsentMessages(room, threadId).length > 0) {
|
const { symbol, count, color } = determineUnreadState(room, threadId);
|
||||||
setSymbol("!");
|
setSymbol(symbol);
|
||||||
setCount(1);
|
setCount(count);
|
||||||
setColor(NotificationColor.Unsent);
|
setColor(color);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [room, threadId]);
|
}, [room, threadId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -58,10 +58,9 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateNotificationVolume(): void {
|
private updateNotificationVolume(): void {
|
||||||
this.properties.set(
|
const state = getRoomNotifsState(this.matrixClient, this.context.room.roomId);
|
||||||
CachedRoomKey.NotificationVolume,
|
if (state) this.properties.set(CachedRoomKey.NotificationVolume, state);
|
||||||
getRoomNotifsState(this.matrixClient, this.context.room.roomId),
|
else this.properties.delete(CachedRoomKey.NotificationVolume);
|
||||||
);
|
|
||||||
this.markEchoReceived(CachedRoomKey.NotificationVolume);
|
this.markEchoReceived(CachedRoomKey.NotificationVolume);
|
||||||
this.emit(PROPERTY_UPDATED, CachedRoomKey.NotificationVolume);
|
this.emit(PROPERTY_UPDATED, CachedRoomKey.NotificationVolume);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import type { IDestroyable } from "../../utils/IDestroyable";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import * as RoomNotifs from "../../RoomNotifs";
|
import * as RoomNotifs from "../../RoomNotifs";
|
||||||
import * as Unread from "../../Unread";
|
|
||||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||||
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
|
import type { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
||||||
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
public constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
public constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
||||||
|
@ -49,10 +47,6 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get roomIsInvite(): boolean {
|
|
||||||
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
const cli = this.room.client;
|
const cli = this.room.client;
|
||||||
|
@ -112,58 +106,10 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
private updateNotificationState(): void {
|
private updateNotificationState(): void {
|
||||||
const snapshot = this.snapshot();
|
const snapshot = this.snapshot();
|
||||||
|
|
||||||
if (getUnsentMessages(this.room).length > 0) {
|
const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room);
|
||||||
// When there are unsent messages we show a red `!`
|
this._color = color;
|
||||||
this._color = NotificationColor.Unsent;
|
this._symbol = symbol;
|
||||||
this._symbol = "!";
|
this._count = count;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally, publish an update if needed
|
// finally, publish an update if needed
|
||||||
this.emitIfUpdated(snapshot);
|
this.emitIfUpdated(snapshot);
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,21 +15,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { ConditionKind, PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules";
|
import { PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventStatus, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { mkEvent, stubClient } from "./test-utils";
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
import { mkEvent, mkRoom, muteRoom, stubClient } from "./test-utils";
|
||||||
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", () => {
|
describe("RoomNotifs test", () => {
|
||||||
|
let client: jest.Mocked<MatrixClient>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getRoomNotifsState handles rules with no conditions", () => {
|
it("getRoomNotifsState handles rules with no conditions", () => {
|
||||||
const cli = MatrixClientPeg.get();
|
mocked(client).pushRules = {
|
||||||
mocked(cli).pushRules = {
|
|
||||||
global: {
|
global: {
|
||||||
override: [
|
override: [
|
||||||
{
|
{
|
||||||
|
@ -41,70 +49,47 @@ describe("RoomNotifs test", () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(null);
|
expect(getRoomNotifsState(client, "!roomId:server")).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getRoomNotifsState handles guest users", () => {
|
it("getRoomNotifsState handles guest users", () => {
|
||||||
const cli = MatrixClientPeg.get();
|
mocked(client).isGuest.mockReturnValue(true);
|
||||||
mocked(cli).isGuest.mockReturnValue(true);
|
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.AllMessages);
|
||||||
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.AllMessages);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getRoomNotifsState handles mute state", () => {
|
it("getRoomNotifsState handles mute state", () => {
|
||||||
const cli = MatrixClientPeg.get();
|
const room = mkRoom(client, "!roomId:server");
|
||||||
cli.pushRules = {
|
muteRoom(room);
|
||||||
global: {
|
expect(getRoomNotifsState(client, room.roomId)).toBe(RoomNotifState.Mute);
|
||||||
override: [
|
|
||||||
{
|
|
||||||
rule_id: "!roomId:server",
|
|
||||||
enabled: true,
|
|
||||||
default: false,
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
kind: ConditionKind.EventMatch,
|
|
||||||
key: "room_id",
|
|
||||||
pattern: "!roomId:server",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: [PushRuleActionName.DontNotify],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.Mute);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getRoomNotifsState handles mentions only", () => {
|
it("getRoomNotifsState handles mentions only", () => {
|
||||||
const cli = MatrixClientPeg.get();
|
(client as any).getRoomPushRule = () => ({
|
||||||
cli.getRoomPushRule = () => ({
|
|
||||||
rule_id: "!roomId:server",
|
rule_id: "!roomId:server",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
default: false,
|
default: false,
|
||||||
actions: [PushRuleActionName.DontNotify],
|
actions: [PushRuleActionName.DontNotify],
|
||||||
});
|
});
|
||||||
expect(getRoomNotifsState(cli, "!roomId:server")).toBe(RoomNotifState.MentionsOnly);
|
expect(getRoomNotifsState(client, "!roomId:server")).toBe(RoomNotifState.MentionsOnly);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getRoomNotifsState handles noisy", () => {
|
it("getRoomNotifsState handles noisy", () => {
|
||||||
const cli = MatrixClientPeg.get();
|
(client as any).getRoomPushRule = () => ({
|
||||||
cli.getRoomPushRule = () => ({
|
|
||||||
rule_id: "!roomId:server",
|
rule_id: "!roomId:server",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
default: false,
|
default: false,
|
||||||
actions: [{ set_tweak: TweakName.Sound, value: "default" }],
|
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", () => {
|
describe("getUnreadNotificationCount", () => {
|
||||||
const ROOM_ID = "!roomId:example.org";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
const THREAD_ID = "$threadId";
|
const THREAD_ID = "$threadId";
|
||||||
|
|
||||||
let cli;
|
|
||||||
let room: Room;
|
let room: Room;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cli = MatrixClientPeg.get();
|
room = new Room(ROOM_ID, client, client.getUserId()!);
|
||||||
room = new Room(ROOM_ID, cli, cli.getUserId());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("counts room notification type", () => {
|
it("counts room notification type", () => {
|
||||||
|
@ -125,19 +110,19 @@ describe("RoomNotifs test", () => {
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||||
|
|
||||||
const OLD_ROOM_ID = "!oldRoomId:example.org";
|
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.Total, 10);
|
||||||
oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6);
|
oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6);
|
||||||
|
|
||||||
cli.getRoom.mockReset().mockReturnValue(oldRoom);
|
client.getRoom.mockReset().mockReturnValue(oldRoom);
|
||||||
|
|
||||||
const predecessorEvent = mkEvent({
|
const predecessorEvent = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: "m.room.create",
|
type: "m.room.create",
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
user: cli.getUserId(),
|
user: client.getUserId()!,
|
||||||
content: {
|
content: {
|
||||||
creator: cli.getUserId(),
|
creator: client.getUserId(),
|
||||||
room_version: "5",
|
room_version: "5",
|
||||||
predecessor: {
|
predecessor: {
|
||||||
room_id: OLD_ROOM_ID,
|
room_id: OLD_ROOM_ID,
|
||||||
|
@ -165,4 +150,78 @@ describe("RoomNotifs test", () => {
|
||||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(1);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { EventStatus } from "matrix-js-sdk/src/models/event-status";
|
||||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
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 { mkThread } from "../../../../test-utils/threads";
|
||||||
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
||||||
import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
import { mkEvent, mkMessage, muteRoom, stubClient } from "../../../../test-utils/test-utils";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
|
||||||
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
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";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
let THREAD_ID: string;
|
let THREAD_ID: string;
|
||||||
|
|
||||||
describe("UnreadNotificationBadge", () => {
|
describe("UnreadNotificationBadge", () => {
|
||||||
stubClient();
|
let client: MatrixClient;
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
function getComponent(threadId?: string) {
|
function getComponent(threadId?: string) {
|
||||||
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
client.supportsThreads = () => true;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
client = stubClient();
|
||||||
|
client.supportsThreads = () => true;
|
||||||
|
|
||||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
@ -145,41 +135,39 @@ describe("UnreadNotificationBadge", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a warning for invites", () => {
|
it("adds a warning for invites", () => {
|
||||||
jest.spyOn(room, "getMyMembership").mockReturnValue("invite");
|
room.updateMyMembership("invite");
|
||||||
render(getComponent());
|
render(getComponent());
|
||||||
expect(screen.queryByText("!")).not.toBeNull();
|
expect(screen.queryByText("!")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides counter for muted rooms", () => {
|
it("hides counter for muted rooms", () => {
|
||||||
jest.spyOn(RoomNotifs, "getRoomNotifsState").mockReset().mockReturnValue(RoomNotifs.RoomNotifState.Mute);
|
muteRoom(room);
|
||||||
|
|
||||||
const { container } = render(getComponent());
|
const { container } = render(getComponent());
|
||||||
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("activity renders unread notification badge", () => {
|
it("activity renders unread notification badge", () => {
|
||||||
act(() => {
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
|
||||||
|
|
||||||
// Add another event on the thread which is not sent by us.
|
// Add another event on the thread which is not sent by us.
|
||||||
const event = mkEvent({
|
const event = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
user: "@alice:server.org",
|
user: "@alice:server.org",
|
||||||
room: room.roomId,
|
room: room.roomId,
|
||||||
content: {
|
content: {
|
||||||
"msgtype": MsgType.Text,
|
"msgtype": MsgType.Text,
|
||||||
"body": "Hello from Bob",
|
"body": "Hello from Bob",
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
event_id: THREAD_ID,
|
event_id: THREAD_ID,
|
||||||
rel_type: RelationType.Thread,
|
rel_type: RelationType.Thread,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ts: 5,
|
},
|
||||||
});
|
ts: 5,
|
||||||
room.addLiveEvents([event]);
|
|
||||||
});
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
|
||||||
const { container } = render(getComponent(THREAD_ID));
|
const { container } = render(getComponent(THREAD_ID));
|
||||||
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
|
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
|
||||||
|
|
110
test/hooks/useUnreadNotifications-test.ts
Normal file
110
test/hooks/useUnreadNotifications-test.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
Copyright 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 { renderHook } from "@testing-library/react-hooks";
|
||||||
|
import { EventStatus, NotificationCountType, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { useUnreadNotifications } from "../../src/hooks/useUnreadNotifications";
|
||||||
|
import { NotificationColor } from "../../src/stores/notifications/NotificationColor";
|
||||||
|
import { mkEvent, muteRoom, stubClient } from "../test-utils";
|
||||||
|
|
||||||
|
describe("useUnreadNotifications", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = stubClient();
|
||||||
|
room = new Room("!room:example.org", client, "@user:example.org", {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setUnreads(greys: number, reds: number): void {
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, reds);
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Total, greys);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("shows nothing by default", async () => {
|
||||||
|
const { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, symbol, count } = result.current;
|
||||||
|
|
||||||
|
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 { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, symbol, count } = result.current;
|
||||||
|
|
||||||
|
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 { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, symbol, count } = result.current;
|
||||||
|
|
||||||
|
expect(symbol).toBe("!");
|
||||||
|
expect(color).toBe(NotificationColor.Red);
|
||||||
|
expect(count).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows nothing for muted channels", async () => {
|
||||||
|
setUnreads(999, 999);
|
||||||
|
muteRoom(room);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, count } = result.current;
|
||||||
|
|
||||||
|
expect(color).toBe(NotificationColor.None);
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the correct number of unreads", async () => {
|
||||||
|
setUnreads(999, 0);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, count } = result.current;
|
||||||
|
|
||||||
|
expect(color).toBe(NotificationColor.Grey);
|
||||||
|
expect(count).toBe(999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the correct number of highlights", async () => {
|
||||||
|
setUnreads(0, 888);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUnreadNotifications(room));
|
||||||
|
const { color, count } = result.current;
|
||||||
|
|
||||||
|
expect(color).toBe(NotificationColor.Red);
|
||||||
|
expect(count).toBe(888);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,38 +15,165 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEventEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import {
|
||||||
|
MatrixEventEvent,
|
||||||
|
PendingEventOrdering,
|
||||||
|
EventStatus,
|
||||||
|
NotificationCountType,
|
||||||
|
EventType,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { stubClient } from "../../test-utils";
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
import { mkEvent, muteRoom, stubClient } from "../../test-utils";
|
||||||
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
||||||
import * as testUtils from "../../test-utils";
|
|
||||||
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
||||||
|
import { NotificationColor } from "../../../src/stores/notifications/NotificationColor";
|
||||||
|
import { createMessageEventContent } from "../../test-utils/events";
|
||||||
|
|
||||||
describe("RoomNotificationState", () => {
|
describe("RoomNotificationState", () => {
|
||||||
let testRoom: Room;
|
let room: Room;
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
client = stubClient();
|
||||||
client = MatrixClientPeg.get();
|
room = new Room("!room:example.com", client, "@user:example.org", {
|
||||||
testRoom = testUtils.mkStubRoom("$aroomid", "Test room", client);
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function addThread(room: Room): void {
|
||||||
|
const threadId = "thread_id";
|
||||||
|
jest.spyOn(room, "eventShouldLiveIn").mockReturnValue({
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
const thread = room.createThread(
|
||||||
|
threadId,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: room.roomId,
|
||||||
|
event_id: "event_root_1",
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "userId",
|
||||||
|
content: createMessageEventContent("RootEvent"),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
thread.addEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: room.roomId,
|
||||||
|
event_id: "event_reply_1" + i,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "userId",
|
||||||
|
content: createMessageEventContent("ReplyEvent" + 1),
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUnreads(room: Room, greys: number, reds: number): void {
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, reds);
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Total, greys);
|
||||||
|
}
|
||||||
|
|
||||||
it("Updates on event decryption", () => {
|
it("Updates on event decryption", () => {
|
||||||
const roomNotifState = new RoomNotificationState(testRoom as any as Room);
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
const listener = jest.fn();
|
const listener = jest.fn();
|
||||||
roomNotifState.addListener(NotificationStateEvents.Update, listener);
|
roomNotifState.addListener(NotificationStateEvents.Update, listener);
|
||||||
const testEvent = {
|
const testEvent = {
|
||||||
getRoomId: () => testRoom.roomId,
|
getRoomId: () => room.roomId,
|
||||||
} as unknown as MatrixEvent;
|
} as unknown as MatrixEvent;
|
||||||
testRoom.getUnreadNotificationCount = jest.fn().mockReturnValue(1);
|
room.getUnreadNotificationCount = jest.fn().mockReturnValue(1);
|
||||||
client.emit(MatrixEventEvent.Decrypted, testEvent);
|
client.emit(MatrixEventEvent.Decrypted, testEvent);
|
||||||
expect(listener).toHaveBeenCalled();
|
expect(listener).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes listeners", () => {
|
it("removes listeners", () => {
|
||||||
const roomNotifState = new RoomNotificationState(testRoom as any as Room);
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
expect(() => roomNotifState.destroy()).not.toThrow();
|
expect(() => roomNotifState.destroy()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suggests an 'unread' ! if there are unsent messages", () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
const event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.message",
|
||||||
|
user: "@user:example.org",
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
event.status = EventStatus.NOT_SENT;
|
||||||
|
room.addPendingEvent(event, "txn");
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.Unsent);
|
||||||
|
expect(roomNotifState.symbol).toBe("!");
|
||||||
|
expect(roomNotifState.count).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests nothing if the room is muted", () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
muteRoom(room);
|
||||||
|
setUnreads(room, 1234, 0);
|
||||||
|
room.updateMyMembership("join"); // emit
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.None);
|
||||||
|
expect(roomNotifState.symbol).toBe(null);
|
||||||
|
expect(roomNotifState.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suggests a red ! if the user has been invited to a room", () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
room.updateMyMembership("invite"); // emit
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.Red);
|
||||||
|
expect(roomNotifState.symbol).toBe("!");
|
||||||
|
expect(roomNotifState.count).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a proper count and color for regular unreads", () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
setUnreads(room, 4321, 0);
|
||||||
|
room.updateMyMembership("join"); // emit
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.Grey);
|
||||||
|
expect(roomNotifState.symbol).toBe(null);
|
||||||
|
expect(roomNotifState.count).toBe(4321);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a proper count and color for highlights", () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
setUnreads(room, 0, 69);
|
||||||
|
room.updateMyMembership("join"); // emit
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.Red);
|
||||||
|
expect(roomNotifState.symbol).toBe(null);
|
||||||
|
expect(roomNotifState.count).toBe(69);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes threads", async () => {
|
||||||
|
const roomNotifState = new RoomNotificationState(room);
|
||||||
|
|
||||||
|
room.timeline.push(
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: room.roomId,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "userId",
|
||||||
|
content: createMessageEventContent("timeline event"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
addThread(room);
|
||||||
|
room.updateMyMembership("join"); // emit
|
||||||
|
|
||||||
|
expect(roomNotifState.color).toBe(NotificationColor.Bold);
|
||||||
|
expect(roomNotifState.symbol).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -33,6 +33,9 @@ import {
|
||||||
IPusher,
|
IPusher,
|
||||||
RoomType,
|
RoomType,
|
||||||
KNOWN_SAFE_ROOM_VERSION,
|
KNOWN_SAFE_ROOM_VERSION,
|
||||||
|
ConditionKind,
|
||||||
|
PushRuleActionName,
|
||||||
|
IPushRules,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { normalize } from "matrix-js-sdk/src/utils";
|
import { normalize } from "matrix-js-sdk/src/utils";
|
||||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||||
|
@ -139,7 +142,7 @@ export function createTestClient(): MatrixClient {
|
||||||
getThirdpartyUser: jest.fn().mockResolvedValue([]),
|
getThirdpartyUser: jest.fn().mockResolvedValue([]),
|
||||||
getAccountData: jest.fn().mockImplementation((type) => {
|
getAccountData: jest.fn().mockImplementation((type) => {
|
||||||
return mkEvent({
|
return mkEvent({
|
||||||
user: undefined,
|
user: "@user:example.com",
|
||||||
room: undefined,
|
room: undefined,
|
||||||
type,
|
type,
|
||||||
event: true,
|
event: true,
|
||||||
|
@ -480,8 +483,12 @@ export function mkMessage({
|
||||||
return mkEvent(event);
|
return mkEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkStubRoom(roomId: string = null, name: string, client: MatrixClient): Room {
|
export function mkStubRoom(
|
||||||
const stubTimeline = { getEvents: () => [] as MatrixEvent[] } as unknown as EventTimeline;
|
roomId: string | null | undefined = null,
|
||||||
|
name: string | undefined,
|
||||||
|
client: MatrixClient | undefined,
|
||||||
|
): Room {
|
||||||
|
const stubTimeline = { getEvents: (): MatrixEvent[] => [] } as unknown as EventTimeline;
|
||||||
return {
|
return {
|
||||||
canInvite: jest.fn(),
|
canInvite: jest.fn(),
|
||||||
client,
|
client,
|
||||||
|
@ -565,22 +572,25 @@ export function mkServerConfig(hsUrl: string, isUrl: string) {
|
||||||
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
|
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
|
||||||
// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
|
// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
|
||||||
|
|
||||||
export const setupAsyncStoreWithClient = async <T = unknown>(store: AsyncStoreWithClient<T>, client: MatrixClient) => {
|
export const setupAsyncStoreWithClient = async <T extends Object = any>(
|
||||||
// @ts-ignore
|
store: AsyncStoreWithClient<T>,
|
||||||
|
client: MatrixClient,
|
||||||
|
) => {
|
||||||
|
// @ts-ignore protected access
|
||||||
store.readyStore.useUnitTestClient(client);
|
store.readyStore.useUnitTestClient(client);
|
||||||
// @ts-ignore
|
// @ts-ignore protected access
|
||||||
await store.onReady();
|
await store.onReady();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetAsyncStoreWithClient = async <T = unknown>(store: AsyncStoreWithClient<T>) => {
|
export const resetAsyncStoreWithClient = async <T extends Object = any>(store: AsyncStoreWithClient<T>) => {
|
||||||
// @ts-ignore
|
// @ts-ignore protected access
|
||||||
await store.onNotReady();
|
await store.onNotReady();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockStateEventImplementation = (events: MatrixEvent[]) => {
|
export const mockStateEventImplementation = (events: MatrixEvent[]) => {
|
||||||
const stateMap = new EnhancedMap<string, Map<string, MatrixEvent>>();
|
const stateMap = new EnhancedMap<string, Map<string, MatrixEvent>>();
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event);
|
stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey()!, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// recreate the overloading in RoomState
|
// recreate the overloading in RoomState
|
||||||
|
@ -617,7 +627,7 @@ export const upsertRoomStateEvents = (room: Room, events: MatrixEvent[]): void =
|
||||||
if (!acc.has(eventType)) {
|
if (!acc.has(eventType)) {
|
||||||
acc.set(eventType, new Map());
|
acc.set(eventType, new Map());
|
||||||
}
|
}
|
||||||
acc.get(eventType).set(event.getStateKey(), event);
|
acc.get(eventType)?.set(event.getStateKey()!, event);
|
||||||
return acc;
|
return acc;
|
||||||
}, room.currentState.events || new Map<string, Map<string, MatrixEvent>>());
|
}, room.currentState.events || new Map<string, Map<string, MatrixEvent>>());
|
||||||
|
|
||||||
|
@ -674,3 +684,25 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||||
pushkey: "pushpush",
|
pushkey: "pushpush",
|
||||||
...extra,
|
...extra,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Add a mute rule for a room. */
|
||||||
|
export function muteRoom(room: Room): void {
|
||||||
|
const client = room.client!;
|
||||||
|
client.pushRules = client.pushRules ?? ({ global: [] } as IPushRules);
|
||||||
|
client.pushRules.global = client.pushRules.global ?? {};
|
||||||
|
client.pushRules.global.override = [
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
enabled: true,
|
||||||
|
rule_id: "rule_id",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
kind: ConditionKind.EventMatch,
|
||||||
|
key: "room_id",
|
||||||
|
pattern: room.roomId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [PushRuleActionName.DontNotify],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue