From b9374388c1ad8e14028e34847d1e2402589714c5 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 13 Apr 2023 18:24:39 +1200 Subject: [PATCH 01/26] muted-to-the-bottom POC --- src/RoomNotifs.ts | 21 ++- src/stores/notifications/NotificationColor.ts | 1 + src/stores/notifications/NotificationState.ts | 4 + .../notifications/RoomNotificationState.ts | 3 +- .../list-ordering/ImportanceAlgorithm.ts | 5 +- .../tag-sorting/RecentMuteAlgorithm.ts | 129 ++++++++++++++++++ 6 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index aa0d89df796..944ea6b25ab 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -27,6 +27,7 @@ import { getUnsentMessages } from "./components/structures/RoomStatusBar"; import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; import SettingsStore from "./settings/SettingsStore"; +import { EchoChamber } from "./stores/local-echo/EchoChamber"; export enum RoomNotifState { AllMessagesLoud = "all_messages_loud", @@ -204,21 +205,26 @@ function isMuteRule(rule: IPushRule): boolean { export function determineUnreadState( room?: Room, threadId?: string, -): { color: NotificationColor; symbol: string | null; count: number } { +): { color: NotificationColor; symbol: string | null; count: number; muted: boolean; } { if (!room) { - return { symbol: null, count: 0, color: NotificationColor.None }; + return { symbol: null, count: 0, color: NotificationColor.None, muted: false }; } + const echoChamber = EchoChamber.forRoom(room); + const notifVolume= echoChamber.notificationVolume; + const muted = notifVolume === RoomNotifState.Mute; + console.log('hhh', room?.roomId, muted, { room, echoChamber }); + if (getUnsentMessages(room, threadId).length > 0) { - return { symbol: "!", count: 1, color: NotificationColor.Unsent }; + return { symbol: "!", count: 1, color: NotificationColor.Unsent, muted }; } if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) { - return { symbol: "!", count: 1, color: NotificationColor.Red }; + return { symbol: "!", count: 1, color: NotificationColor.Red, muted }; } if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) { - return { symbol: null, count: 0, color: NotificationColor.None }; + return { symbol: null, count: 0, color: NotificationColor.None, muted }; } const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId); @@ -226,11 +232,11 @@ export function determineUnreadState( const trueCount = greyNotifs || redNotifs; if (redNotifs > 0) { - return { symbol: null, count: trueCount, color: NotificationColor.Red }; + return { symbol: null, count: trueCount, color: NotificationColor.Red, muted }; } if (greyNotifs > 0) { - return { symbol: null, count: trueCount, color: NotificationColor.Grey }; + return { symbol: null, count: trueCount, color: NotificationColor.Grey, muted }; } // We don't have any notified messages, but we might have unread messages. Let's @@ -243,5 +249,6 @@ export function determineUnreadState( symbol: null, count: trueCount, color: hasUnread ? NotificationColor.Bold : NotificationColor.None, + muted, }; } diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts index f89bb1728d1..ecb0e312d81 100644 --- a/src/stores/notifications/NotificationColor.ts +++ b/src/stores/notifications/NotificationColor.ts @@ -17,6 +17,7 @@ limitations under the License. import { _t } from "../../languageHandler"; export enum NotificationColor { + Muted, // the room // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special // TODO: Remove bold with notifications: https://github.com/vector-im/element-web/issues/14227 diff --git a/src/stores/notifications/NotificationState.ts b/src/stores/notifications/NotificationState.ts index 2445ec6d361..2e13cb054ae 100644 --- a/src/stores/notifications/NotificationState.ts +++ b/src/stores/notifications/NotificationState.ts @@ -66,6 +66,10 @@ export abstract class NotificationState return this._color; } + public get muted(): boolean { + return this._muted; + } + public get isIdle(): boolean { return this.color <= NotificationColor.None; } diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index cde911f7021..b32066a16b0 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -92,10 +92,11 @@ export class RoomNotificationState extends NotificationState implements IDestroy private updateNotificationState(): void { const snapshot = this.snapshot(); - const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room); + const { color, symbol, count, muted } = RoomNotifs.determineUnreadState(this.room); this._color = color; this._symbol = symbol; this._count = count; + this._muted = muted; // finally, publish an update if needed this.emitIfUpdated(snapshot); diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 7c6ae4a617f..c7bba9885e4 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -42,6 +42,7 @@ const CATEGORY_ORDER = [ NotificationColor.Grey, NotificationColor.Bold, NotificationColor.None, // idle + NotificationColor.Muted ]; /** @@ -81,6 +82,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { [NotificationColor.Grey]: [], [NotificationColor.Bold]: [], [NotificationColor.None]: [], + [NotificationColor.Muted]: [], }; for (const room of rooms) { const category = this.getRoomCategory(room); @@ -94,7 +96,8 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); - return state.color; + console.log('hhh', room.roomId, state) + return state.muted ? NotificationColor.Muted : state.color; } public setRooms(rooms: Room[]): void { diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts new file mode 100644 index 00000000000..e05b4e7e586 --- /dev/null +++ b/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts @@ -0,0 +1,129 @@ +/* +Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; +import { MatrixClientPeg } from "../../../../MatrixClientPeg"; +import * as Unread from "../../../../Unread"; +import { EffectiveMembership, getEffectiveMembership } from "../../../../utils/membership"; + +export function shouldCauseReorder(event: MatrixEvent): boolean { + const type = event.getType(); + const content = event.getContent(); + const prevContent = event.getPrevContent(); + + // Never ignore membership changes + if (type === EventType.RoomMember && prevContent.membership !== content.membership) return true; + + // Ignore display name changes + if (type === EventType.RoomMember && prevContent.displayname !== content.displayname) return false; + // Ignore avatar changes + if (type === EventType.RoomMember && prevContent.avatar_url !== content.avatar_url) return false; + + return true; +} + +export const sortRooms = (rooms: Room[]): Room[] => { + // We cache the timestamp lookup to avoid iterating forever on the timeline + // of events. This cache only survives a single sort though. + // We wouldn't need this if `.sort()` didn't constantly try and compare all + // of the rooms to each other. + + // TODO: We could probably improve the sorting algorithm here by finding changes. + // See https://github.com/vector-im/element-web/issues/14459 + // For example, if we spent a little bit of time to determine which elements have + // actually changed (probably needs to be done higher up?) then we could do an + // insertion sort or similar on the limited set of changes. + + // TODO: Don't assume we're using the same client as the peg + // See https://github.com/vector-im/element-web/issues/14458 + let myUserId = ""; + if (MatrixClientPeg.get()) { + myUserId = MatrixClientPeg.get().getUserId()!; + } + + const tsCache: { [roomId: string]: number } = {}; + + return rooms.sort((a, b) => { + const roomALastTs = tsCache[a.roomId] ?? getLastTs(a, myUserId); + const roomBLastTs = tsCache[b.roomId] ?? getLastTs(b, myUserId); + + tsCache[a.roomId] = roomALastTs; + tsCache[b.roomId] = roomBLastTs; + + return roomBLastTs - roomALastTs; + }); +}; + +const getLastTs = (r: Room, userId: string): number => { + const mainTimelineLastTs = ((): number => { + // Apparently we can have rooms without timelines, at least under testing + // environments. Just return MAX_INT when this happens. + if (!r?.timeline) { + return Number.MAX_SAFE_INTEGER; + } + + // If the room hasn't been joined yet, it probably won't have a timeline to + // parse. We'll still fall back to the timeline if this fails, but chances + // are we'll at least have our own membership event to go off of. + const effectiveMembership = getEffectiveMembership(r.getMyMembership()); + if (effectiveMembership !== EffectiveMembership.Join) { + const membershipEvent = r.currentState.getStateEvents(EventType.RoomMember, userId); + if (membershipEvent && !Array.isArray(membershipEvent)) { + return membershipEvent.getTs(); + } + } + + for (let i = r.timeline.length - 1; i >= 0; --i) { + const ev = r.timeline[i]; + if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) + + if ((ev.getSender() === userId && shouldCauseReorder(ev)) || Unread.eventTriggersUnreadCount(ev)) { + return ev.getTs(); + } + } + + // we might only have events that don't trigger the unread indicator, + // in which case use the oldest event even if normally it wouldn't count. + // This is better than just assuming the last event was forever ago. + return r.timeline[0]?.getTs() ?? Number.MAX_SAFE_INTEGER; + })(); + + const threadLastEventTimestamps = r.getThreads().map((thread) => { + const event = thread.replyToEvent ?? thread.rootEvent; + return event?.getTs() ?? 0; + }); + + return Math.max(mainTimelineLastTs, ...threadLastEventTimestamps); +}; + +/** + * Sorts rooms according to the last event's timestamp in each room that seems + * useful to the user. + */ +export class RecentMuteAlgorithm implements IAlgorithm { + public sortRooms(rooms: Room[], tagId: TagID): Room[] { + return sortRooms(rooms); + } + + public getLastTs(room: Room, userId: string): number { + return getLastTs(room, userId); + } +} From 5292e47fcc07744294ec4f41d19856cfaf15379b Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 18 Apr 2023 13:30:14 +1200 Subject: [PATCH 02/26] split muted rooms in natural algorithm --- src/stores/notifications/NotificationState.ts | 1 + .../list-ordering/ImportanceAlgorithm.ts | 1 - .../list-ordering/NaturalAlgorithm.ts | 118 ++++++++++++++++-- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/stores/notifications/NotificationState.ts b/src/stores/notifications/NotificationState.ts index 2e13cb054ae..0d7ebb024d8 100644 --- a/src/stores/notifications/NotificationState.ts +++ b/src/stores/notifications/NotificationState.ts @@ -24,6 +24,7 @@ export interface INotificationStateSnapshotParams { symbol: string | null; count: number; color: NotificationColor; + muted: boolean; } export enum NotificationStateEvents { diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index c7bba9885e4..48372a008f8 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -96,7 +96,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); - console.log('hhh', room.roomId, state) return state.muted ? NotificationColor.Muted : state.color; } diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 61a3d29dd2b..076cb5ee5cf 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -21,42 +21,142 @@ import { SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; import { OrderingAlgorithm } from "./OrderingAlgorithm"; import { RoomUpdateCause, TagID } from "../../models"; +import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore"; + +type NaturalCategorizedRoomMap = { + defaultRooms: Room[]; + mutedRooms: Room[]; +} /** * Uses the natural tag sorting algorithm order to determine tag ordering. No * additional behavioural changes are present. */ export class NaturalAlgorithm extends OrderingAlgorithm { + private cachedCategorizedOrderedRooms: NaturalCategorizedRoomMap = { + defaultRooms: [], mutedRooms: [] + }; public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { super(tagId, initialSortingAlgorithm); } public setRooms(rooms: Room[]): void { - this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); + const { defaultRooms, mutedRooms} = this.categorizeRooms(rooms); + + console.log('hhh', { rooms, defaultRooms, mutedRooms }); + + this.cachedCategorizedOrderedRooms = { + defaultRooms: sortRoomsWithAlgorithm(defaultRooms, this.tagId, this.sortingAlgorithm), + mutedRooms: sortRoomsWithAlgorithm( + mutedRooms, + this.tagId, + this.sortingAlgorithm, + ) + } + this.buildCachedOrderedRooms(); } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt; + const isMuted = this.getRoomIsMuted(room); + + // TODO(kerrya) case for room becoming muted + // TODO(kerrya) can we assume room mutedness won't change without an event? + if (!isSplice && !isInPlace) { throw new Error(`Unsupported update cause: ${cause}`); } if (cause === RoomUpdateCause.NewRoom) { - this.cachedOrderedRooms.push(room); - } else if (cause === RoomUpdateCause.RoomRemoved) { - const idx = this.getRoomIndex(room); - if (idx >= 0) { - this.cachedOrderedRooms.splice(idx, 1); + if (isMuted) { + this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm( + [...this.cachedCategorizedOrderedRooms.mutedRooms, room], + this.tagId, + this.sortingAlgorithm, + ) } else { - logger.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`); + this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm( + [...this.cachedCategorizedOrderedRooms.defaultRooms, room], + this.tagId, + this.sortingAlgorithm, + ) } + this.buildCachedOrderedRooms(); + return true; + } else if (cause === RoomUpdateCause.RoomRemoved) { + return this.removeRoom(room); } + // @TODO(kerrya) what cases are hitting here? should they cause a reorder?) // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457 // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags - this.cachedOrderedRooms = sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); - + if (isMuted) { + this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm( + this.cachedCategorizedOrderedRooms.mutedRooms, + this.tagId, + this.sortingAlgorithm, + ) + } else { + this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm( + this.cachedCategorizedOrderedRooms.defaultRooms, + this.tagId, + this.sortingAlgorithm, + ) + } + this.buildCachedOrderedRooms(); return true; } + + /** + * Remove a room from the cached room list + * @param room Room to remove + * @returns {boolean} true when room list should update as result + */ + private removeRoom(room: Room): boolean { + const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex(r => r.roomId === room.roomId); + if (defaultIndex > -1) { + this.cachedCategorizedOrderedRooms.defaultRooms.splice(defaultIndex, 1); + this.buildCachedOrderedRooms(); + return true; + } + const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex(r => r.roomId === room.roomId); + if (mutedIndex > -1) { + this.cachedCategorizedOrderedRooms.mutedRooms.splice(mutedIndex, 1); + this.buildCachedOrderedRooms(); + return true; + } + + logger.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`); + // room was not in cached lists, no update + return false; + } + + /** + * Sets cachedOrderedRooms from cachedCategorizedOrderedRooms + */ + private buildCachedOrderedRooms(): void { + this.cachedOrderedRooms = [ + ...this.cachedCategorizedOrderedRooms.defaultRooms, + ...this.cachedCategorizedOrderedRooms.mutedRooms, + ]; + } + + private getRoomIsMuted(room: Room): boolean { + // It's fine for us to call this a lot because it's cached, and we shouldn't be + // wasting anything by doing so as the store holds single references + const state = RoomNotificationStateStore.instance.getRoomState(room); + return state.muted; + } + + private categorizeRooms(rooms: Room[]): NaturalCategorizedRoomMap { + return rooms.reduce((acc, room: Room) => { + if (this.getRoomIsMuted(room)) { + acc.mutedRooms.push(room); + } else { + acc.defaultRooms.push(room); + }; + return acc; + }, { defaultRooms: [], mutedRooms: []} as NaturalCategorizedRoomMap) + } } From 07606b151cf46f269a3229ad86a4861bb71d4116 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 18 Apr 2023 16:15:03 +1200 Subject: [PATCH 03/26] add previous event to account data dispatch --- src/actions/MatrixActionCreators.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/actions/MatrixActionCreators.ts b/src/actions/MatrixActionCreators.ts index bb40a463baa..6e5fb28b9a5 100644 --- a/src/actions/MatrixActionCreators.ts +++ b/src/actions/MatrixActionCreators.ts @@ -58,12 +58,17 @@ function createSyncAction(matrixClient: MatrixClient, state: string, prevState: * @param {MatrixEvent} accountDataEvent the account data event. * @returns {AccountDataAction} an action of type MatrixActions.accountData. */ -function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload { +function createAccountDataAction( + matrixClient: MatrixClient, + accountDataEvent: MatrixEvent, + previousAccountDataEvent?: MatrixEvent, +): ActionPayload { return { action: "MatrixActions.accountData", event: accountDataEvent, event_type: accountDataEvent.getType(), event_content: accountDataEvent.getContent(), + previousEvent: previousAccountDataEvent, }; } From 668cf8ef27ba9a5a5ee1ad30e16b451780a0837f Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 18 Apr 2023 16:25:17 +1200 Subject: [PATCH 04/26] add muted to notification state --- src/stores/notifications/NotificationColor.ts | 2 +- src/stores/notifications/NotificationState.ts | 7 +++++-- src/stores/room-list/models.ts | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts index ecb0e312d81..a2e41ed53ed 100644 --- a/src/stores/notifications/NotificationColor.ts +++ b/src/stores/notifications/NotificationColor.ts @@ -17,7 +17,7 @@ limitations under the License. import { _t } from "../../languageHandler"; export enum NotificationColor { - Muted, // the room + Muted, // the room // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special // TODO: Remove bold with notifications: https://github.com/vector-im/element-web/issues/14227 diff --git a/src/stores/notifications/NotificationState.ts b/src/stores/notifications/NotificationState.ts index 0d7ebb024d8..6f356081b01 100644 --- a/src/stores/notifications/NotificationState.ts +++ b/src/stores/notifications/NotificationState.ts @@ -43,6 +43,7 @@ export abstract class NotificationState protected _symbol: string | null = null; protected _count = 0; protected _color: NotificationColor = NotificationColor.None; + protected _muted: boolean = false; private watcherReferences: string[] = []; @@ -115,16 +116,18 @@ export class NotificationStateSnapshot { private readonly symbol: string | null; private readonly count: number; private readonly color: NotificationColor; + private readonly muted: boolean; public constructor(state: INotificationStateSnapshotParams) { this.symbol = state.symbol; this.count = state.count; this.color = state.color; + this.muted = state.muted; } public isDifferentFrom(other: INotificationStateSnapshotParams): boolean { - const before = { count: this.count, symbol: this.symbol, color: this.color }; - const after = { count: other.count, symbol: other.symbol, color: other.color }; + const before = { count: this.count, symbol: this.symbol, color: this.color, muted: this.muted }; + const after = { count: other.count, symbol: other.symbol, color: other.color, muted: other.muted }; return JSON.stringify(before) !== JSON.stringify(after); } } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 0345ff60534..d9f17cc2713 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -43,6 +43,7 @@ export type TagID = string | DefaultTagID; export enum RoomUpdateCause { Timeline = "TIMELINE", PossibleTagChange = "POSSIBLE_TAG_CHANGE", + PossibleMuteChange = "POSSIBLE_MUTE_CHANGE", ReadReceipt = "READ_RECEIPT", NewRoom = "NEW_ROOM", RoomRemoved = "ROOM_REMOVED", From 6450a73858a67f9407e853826b85c223b946c842 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 18 Apr 2023 16:25:55 +1200 Subject: [PATCH 05/26] sort muted rooms to the bottom --- src/RoomNotifs.ts | 5 +- src/stores/room-list/RoomListStore.ts | 12 +++ .../list-ordering/ImportanceAlgorithm.ts | 8 +- .../list-ordering/NaturalAlgorithm.ts | 101 +++++++++++++----- src/stores/room-list/utils/roomMute.ts | 46 ++++++++ 5 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 src/stores/room-list/utils/roomMute.ts diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 944ea6b25ab..5a23ba39434 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -205,15 +205,14 @@ function isMuteRule(rule: IPushRule): boolean { export function determineUnreadState( room?: Room, threadId?: string, -): { color: NotificationColor; symbol: string | null; count: number; muted: boolean; } { +): { color: NotificationColor; symbol: string | null; count: number; muted: boolean } { if (!room) { return { symbol: null, count: 0, color: NotificationColor.None, muted: false }; } const echoChamber = EchoChamber.forRoom(room); - const notifVolume= echoChamber.notificationVolume; + const notifVolume = echoChamber.notificationVolume; const muted = notifVolume === RoomNotifState.Mute; - console.log('hhh', room?.roomId, muted, { room, echoChamber }); if (getUnsentMessages(room, threadId).length > 0) { return { symbol: "!", count: 1, color: NotificationColor.Unsent, muted }; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index cb7ce972a86..f71bf24807b 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -40,6 +40,7 @@ import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; +import { getChangedOverridePushRules } from "./utils/roomMute"; interface IState { // state is tracked in underlying classes @@ -289,6 +290,17 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements this.onDispatchMyMembership(payload); return; } + + const possibleMuteChangeRoomIds = getChangedOverridePushRules(payload); + if (possibleMuteChangeRoomIds) { + for (const roomId of possibleMuteChangeRoomIds) { + const room = roomId && this.matrixClient.getRoom(roomId); + if (room) { + await this.handleRoomUpdate(room, RoomUpdateCause.PossibleMuteChange); + } + } + this.updateFn.trigger(); + } } /** diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 48372a008f8..c8f3692d376 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -42,7 +42,7 @@ const CATEGORY_ORDER = [ NotificationColor.Grey, NotificationColor.Bold, NotificationColor.None, // idle - NotificationColor.Muted + NotificationColor.Muted, ]; /** @@ -166,7 +166,11 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { return this.handleSplice(room, cause); } - if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { + if ( + cause !== RoomUpdateCause.Timeline && + cause !== RoomUpdateCause.ReadReceipt && + cause !== RoomUpdateCause.PossibleMuteChange + ) { throw new Error(`Unsupported update cause: ${cause}`); } diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 076cb5ee5cf..69a18f597ae 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -26,7 +26,7 @@ import { RoomNotificationStateStore } from "../../../notifications/RoomNotificat type NaturalCategorizedRoomMap = { defaultRooms: Room[]; mutedRooms: Room[]; -} +}; /** * Uses the natural tag sorting algorithm order to determine tag ordering. No @@ -34,36 +34,31 @@ type NaturalCategorizedRoomMap = { */ export class NaturalAlgorithm extends OrderingAlgorithm { private cachedCategorizedOrderedRooms: NaturalCategorizedRoomMap = { - defaultRooms: [], mutedRooms: [] + defaultRooms: [], + mutedRooms: [], }; public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { super(tagId, initialSortingAlgorithm); } public setRooms(rooms: Room[]): void { - const { defaultRooms, mutedRooms} = this.categorizeRooms(rooms); - - console.log('hhh', { rooms, defaultRooms, mutedRooms }); + const { defaultRooms, mutedRooms } = this.categorizeRooms(rooms); this.cachedCategorizedOrderedRooms = { defaultRooms: sortRoomsWithAlgorithm(defaultRooms, this.tagId, this.sortingAlgorithm), - mutedRooms: sortRoomsWithAlgorithm( - mutedRooms, - this.tagId, - this.sortingAlgorithm, - ) - } + mutedRooms: sortRoomsWithAlgorithm(mutedRooms, this.tagId, this.sortingAlgorithm), + }; this.buildCachedOrderedRooms(); } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; - const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt; + const isInPlace = + cause === RoomUpdateCause.Timeline || + cause === RoomUpdateCause.ReadReceipt || + cause === RoomUpdateCause.PossibleMuteChange; const isMuted = this.getRoomIsMuted(room); - // TODO(kerrya) case for room becoming muted - // TODO(kerrya) can we assume room mutedness won't change without an event? - if (!isSplice && !isInPlace) { throw new Error(`Unsupported update cause: ${cause}`); } @@ -74,18 +69,20 @@ export class NaturalAlgorithm extends OrderingAlgorithm { [...this.cachedCategorizedOrderedRooms.mutedRooms, room], this.tagId, this.sortingAlgorithm, - ) + ); } else { this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm( [...this.cachedCategorizedOrderedRooms.defaultRooms, room], this.tagId, this.sortingAlgorithm, - ) + ); } this.buildCachedOrderedRooms(); return true; } else if (cause === RoomUpdateCause.RoomRemoved) { return this.removeRoom(room); + } else if (cause === RoomUpdateCause.PossibleMuteChange) { + return this.onPossibleMuteChange(room); } // @TODO(kerrya) what cases are hitting here? should they cause a reorder?) @@ -96,13 +93,13 @@ export class NaturalAlgorithm extends OrderingAlgorithm { this.cachedCategorizedOrderedRooms.mutedRooms, this.tagId, this.sortingAlgorithm, - ) + ); } else { this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm( this.cachedCategorizedOrderedRooms.defaultRooms, this.tagId, this.sortingAlgorithm, - ) + ); } this.buildCachedOrderedRooms(); return true; @@ -114,13 +111,13 @@ export class NaturalAlgorithm extends OrderingAlgorithm { * @returns {boolean} true when room list should update as result */ private removeRoom(room: Room): boolean { - const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex(r => r.roomId === room.roomId); + const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex((r) => r.roomId === room.roomId); if (defaultIndex > -1) { this.cachedCategorizedOrderedRooms.defaultRooms.splice(defaultIndex, 1); this.buildCachedOrderedRooms(); return true; } - const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex(r => r.roomId === room.roomId); + const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex((r) => r.roomId === room.roomId); if (mutedIndex > -1) { this.cachedCategorizedOrderedRooms.mutedRooms.splice(mutedIndex, 1); this.buildCachedOrderedRooms(); @@ -150,13 +147,59 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } private categorizeRooms(rooms: Room[]): NaturalCategorizedRoomMap { - return rooms.reduce((acc, room: Room) => { - if (this.getRoomIsMuted(room)) { - acc.mutedRooms.push(room); - } else { - acc.defaultRooms.push(room); - }; - return acc; - }, { defaultRooms: [], mutedRooms: []} as NaturalCategorizedRoomMap) + return rooms.reduce( + (acc, room: Room) => { + if (this.getRoomIsMuted(room)) { + acc.mutedRooms.push(room); + } else { + acc.defaultRooms.push(room); + } + return acc; + }, + { defaultRooms: [], mutedRooms: [] } as NaturalCategorizedRoomMap, + ); + } + + private onPossibleMuteChange(room: Room): boolean { + const isMuted = this.getRoomIsMuted(room); + if (isMuted) { + const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex( + (r) => r.roomId === room.roomId, + ); + + // room has been muted + if (defaultIndex > -1) { + // remove from the default list + this.cachedCategorizedOrderedRooms.defaultRooms.splice(defaultIndex, 1); + // add to muted list and reorder + this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm( + [...this.cachedCategorizedOrderedRooms.mutedRooms, room], + this.tagId, + this.sortingAlgorithm, + ); + // rebuild + this.buildCachedOrderedRooms(); + return true; + } + } else { + const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex((r) => r.roomId === room.roomId); + + // room has been unmuted + if (mutedIndex > -1) { + // remove from the muted list + this.cachedCategorizedOrderedRooms.mutedRooms.splice(mutedIndex, 1); + // add to default list and reorder + this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm( + [...this.cachedCategorizedOrderedRooms.defaultRooms, room], + this.tagId, + this.sortingAlgorithm, + ); + // rebuild + this.buildCachedOrderedRooms(); + return true; + } + } + + return false; } } diff --git a/src/stores/room-list/utils/roomMute.ts b/src/stores/room-list/utils/roomMute.ts new file mode 100644 index 00000000000..c1c36346b54 --- /dev/null +++ b/src/stores/room-list/utils/roomMute.ts @@ -0,0 +1,46 @@ +/* +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 { MatrixEvent, EventType, IPushRules } from "matrix-js-sdk/src/matrix"; + +import { ActionPayload } from "../../../dispatcher/payloads"; +import { arrayDiff } from "../../../utils/arrays"; + +export const getChangedOverridePushRules = (actionPayload: ActionPayload): string[] | undefined => { + if ( + actionPayload.action !== "MatrixActions.accountData" || + actionPayload.event?.getType() !== EventType.PushRules + ) { + return undefined; + } + const event = actionPayload.event as MatrixEvent; + const prevEvent = actionPayload.previousEvent as MatrixEvent | undefined; + + if (!event || !prevEvent) { + return undefined; + } + + // room mute rules are overrides + const roomPushRules = (event.getContent() as IPushRules)?.global?.override; + const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override; + + const { added, removed } = arrayDiff( + prevRoomPushRules?.map((rule) => rule.rule_id), + roomPushRules?.map((rule) => rule.rule_id), + ); + + return [...added, ...removed]; +}; From 94ed2a6e6a9f0896965230acb532437ce6c7d853 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 19 Apr 2023 14:59:44 +1200 Subject: [PATCH 06/26] only split muted rooms when sorting is RECENT --- src/stores/room-list/RoomListStore.ts | 2 ++ .../algorithms/list-ordering/ImportanceAlgorithm.ts | 8 +++++++- .../algorithms/list-ordering/NaturalAlgorithm.ts | 9 +++++++-- .../algorithms/list-ordering/OrderingAlgorithm.ts | 4 ++++ src/stores/room-list/algorithms/list-ordering/index.ts | 9 +++++---- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index f71bf24807b..4094961358d 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -553,6 +553,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements */ public regenerateAllLists({ trigger = true }): void { logger.warn("Regenerating all room lists"); + + debugger; const rooms = this.getPlausibleRooms(); diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index c8f3692d376..db1b96af344 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -96,10 +96,11 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); - return state.muted ? NotificationColor.Muted : state.color; + return (this.isMutedToBottom && state.muted) ? NotificationColor.Muted : state.color; } public setRooms(rooms: Room[]): void { + console.log('hhh', 'ImportanceAlgorithm:setRooms', this.isMutedToBottom); if (this.sortingAlgorithm === SortAlgorithm.Manual) { this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); } else { @@ -162,6 +163,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { + console.log('hhh', 'ImportanceAlgorithm:handleRoomUpdate', this.isMutedToBottom); if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) { return this.handleSplice(room, cause); } @@ -174,6 +176,10 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { throw new Error(`Unsupported update cause: ${cause}`); } + if (cause === RoomUpdateCause.PossibleMuteChange && !this.isMutedToBottom) { + return; + } + const category = this.getRoomCategory(room); if (this.sortingAlgorithm === SortAlgorithm.Manual) { return false; // Nothing to do here. diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 69a18f597ae..d77a4bd7028 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -42,6 +42,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public setRooms(rooms: Room[]): void { + console.log('hhh', 'NaturalAlgorithm:setRooms', this.isMutedToBottom); const { defaultRooms, mutedRooms } = this.categorizeRooms(rooms); this.cachedCategorizedOrderedRooms = { @@ -52,12 +53,13 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { + console.log('hhh', 'NaturalAlgorithm:handleRoomUpdate', this.isMutedToBottom); const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt || cause === RoomUpdateCause.PossibleMuteChange; - const isMuted = this.getRoomIsMuted(room); + const isMuted = this.isMutedToBottom && this.getRoomIsMuted(room); if (!isSplice && !isInPlace) { throw new Error(`Unsupported update cause: ${cause}`); @@ -81,7 +83,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm { return true; } else if (cause === RoomUpdateCause.RoomRemoved) { return this.removeRoom(room); - } else if (cause === RoomUpdateCause.PossibleMuteChange) { + } else if (this.isMutedToBottom && cause === RoomUpdateCause.PossibleMuteChange) { return this.onPossibleMuteChange(room); } @@ -147,6 +149,9 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } private categorizeRooms(rooms: Room[]): NaturalCategorizedRoomMap { + if (!this.isMutedToBottom) { + return { defaultRooms: rooms, mutedRooms: []}; + } return rooms.reduce( (acc, room: Room) => { if (this.getRoomIsMuted(room)) { diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts index 6cf8b4606fd..309f96498f2 100644 --- a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -42,6 +42,10 @@ export abstract class OrderingAlgorithm { return this.cachedOrderedRooms; } + private get isMutedToBottom(): boolean { + return this.sortingAlgorithm === SortAlgorithm.Recent; + } + /** * Sets the sorting algorithm to use within the list. * @param newAlgorithm The new algorithm. Must be defined. diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts index c002dac4211..e2bfa14702c 100644 --- a/src/stores/room-list/algorithms/list-ordering/index.ts +++ b/src/stores/room-list/algorithms/list-ordering/index.ts @@ -21,12 +21,12 @@ import { TagID } from "../../models"; import { OrderingAlgorithm } from "./OrderingAlgorithm"; interface AlgorithmFactory { - (tagId: TagID, initialSortingAlgorithm: SortAlgorithm): OrderingAlgorithm; + (tagId: TagID, initialSortingAlgorithm: SortAlgorithm, mutedToTheBottom?: boolean): OrderingAlgorithm; } const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: AlgorithmFactory } = { - [ListAlgorithm.Natural]: (tagId, initSort) => new NaturalAlgorithm(tagId, initSort), - [ListAlgorithm.Importance]: (tagId, initSort) => new ImportanceAlgorithm(tagId, initSort), + [ListAlgorithm.Natural]: (tagId, initSort, mutedToTheBottom) => new NaturalAlgorithm(tagId, initSort, mutedToTheBottom), + [ListAlgorithm.Importance]: (tagId, initSort, mutedToTheBottom) => new ImportanceAlgorithm(tagId, initSort, mutedToTheBottom), }; /** @@ -40,10 +40,11 @@ export function getListAlgorithmInstance( algorithm: ListAlgorithm, tagId: TagID, initSort: SortAlgorithm, + mutedToTheBottom?: boolean, ): OrderingAlgorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } - return ALGORITHM_FACTORIES[algorithm](tagId, initSort); + return ALGORITHM_FACTORIES[algorithm](tagId, initSort, mutedToTheBottom); } From 1b095d3a548a7a91effea6889f9b3fb335763088 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 19 Apr 2023 15:24:30 +1200 Subject: [PATCH 07/26] remove debugs --- src/stores/notifications/NotificationState.ts | 2 +- src/stores/room-list/RoomListStore.ts | 2 -- .../algorithms/list-ordering/ImportanceAlgorithm.ts | 4 +--- .../algorithms/list-ordering/NaturalAlgorithm.ts | 4 +--- .../algorithms/list-ordering/OrderingAlgorithm.ts | 2 +- src/stores/room-list/algorithms/list-ordering/index.ts | 9 ++++----- 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/stores/notifications/NotificationState.ts b/src/stores/notifications/NotificationState.ts index 6f356081b01..b4db29c1354 100644 --- a/src/stores/notifications/NotificationState.ts +++ b/src/stores/notifications/NotificationState.ts @@ -43,7 +43,7 @@ export abstract class NotificationState protected _symbol: string | null = null; protected _count = 0; protected _color: NotificationColor = NotificationColor.None; - protected _muted: boolean = false; + protected _muted = false; private watcherReferences: string[] = []; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 4094961358d..f71bf24807b 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -553,8 +553,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements */ public regenerateAllLists({ trigger = true }): void { logger.warn("Regenerating all room lists"); - - debugger; const rooms = this.getPlausibleRooms(); diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index db1b96af344..0117941ec8f 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -96,11 +96,10 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); - return (this.isMutedToBottom && state.muted) ? NotificationColor.Muted : state.color; + return this.isMutedToBottom && state.muted ? NotificationColor.Muted : state.color; } public setRooms(rooms: Room[]): void { - console.log('hhh', 'ImportanceAlgorithm:setRooms', this.isMutedToBottom); if (this.sortingAlgorithm === SortAlgorithm.Manual) { this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); } else { @@ -163,7 +162,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { - console.log('hhh', 'ImportanceAlgorithm:handleRoomUpdate', this.isMutedToBottom); if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) { return this.handleSplice(room, cause); } diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index d77a4bd7028..0190bd2a027 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -42,7 +42,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public setRooms(rooms: Room[]): void { - console.log('hhh', 'NaturalAlgorithm:setRooms', this.isMutedToBottom); const { defaultRooms, mutedRooms } = this.categorizeRooms(rooms); this.cachedCategorizedOrderedRooms = { @@ -53,7 +52,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { - console.log('hhh', 'NaturalAlgorithm:handleRoomUpdate', this.isMutedToBottom); const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; const isInPlace = cause === RoomUpdateCause.Timeline || @@ -150,7 +148,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm { private categorizeRooms(rooms: Room[]): NaturalCategorizedRoomMap { if (!this.isMutedToBottom) { - return { defaultRooms: rooms, mutedRooms: []}; + return { defaultRooms: rooms, mutedRooms: [] }; } return rooms.reduce( (acc, room: Room) => { diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts index 309f96498f2..2bcb5b52899 100644 --- a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -42,7 +42,7 @@ export abstract class OrderingAlgorithm { return this.cachedOrderedRooms; } - private get isMutedToBottom(): boolean { + public get isMutedToBottom(): boolean { return this.sortingAlgorithm === SortAlgorithm.Recent; } diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts index e2bfa14702c..c002dac4211 100644 --- a/src/stores/room-list/algorithms/list-ordering/index.ts +++ b/src/stores/room-list/algorithms/list-ordering/index.ts @@ -21,12 +21,12 @@ import { TagID } from "../../models"; import { OrderingAlgorithm } from "./OrderingAlgorithm"; interface AlgorithmFactory { - (tagId: TagID, initialSortingAlgorithm: SortAlgorithm, mutedToTheBottom?: boolean): OrderingAlgorithm; + (tagId: TagID, initialSortingAlgorithm: SortAlgorithm): OrderingAlgorithm; } const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: AlgorithmFactory } = { - [ListAlgorithm.Natural]: (tagId, initSort, mutedToTheBottom) => new NaturalAlgorithm(tagId, initSort, mutedToTheBottom), - [ListAlgorithm.Importance]: (tagId, initSort, mutedToTheBottom) => new ImportanceAlgorithm(tagId, initSort, mutedToTheBottom), + [ListAlgorithm.Natural]: (tagId, initSort) => new NaturalAlgorithm(tagId, initSort), + [ListAlgorithm.Importance]: (tagId, initSort) => new ImportanceAlgorithm(tagId, initSort), }; /** @@ -40,11 +40,10 @@ export function getListAlgorithmInstance( algorithm: ListAlgorithm, tagId: TagID, initSort: SortAlgorithm, - mutedToTheBottom?: boolean, ): OrderingAlgorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } - return ALGORITHM_FACTORIES[algorithm](tagId, initSort, mutedToTheBottom); + return ALGORITHM_FACTORIES[algorithm](tagId, initSort); } From acb76067abe9203bf9fda1976240b855045a3513 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 20 Apr 2023 11:07:58 +1200 Subject: [PATCH 08/26] use RoomNotifState better --- src/RoomNotifs.ts | 31 ++++++++++--------- .../notifications/RoomNotificationState.ts | 4 ++- src/stores/room-list/RoomListStore.ts | 4 +-- src/stores/room-list/utils/roomMute.ts | 15 ++++++--- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 5a23ba39434..0389cce9f9b 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -190,12 +190,20 @@ function findOverrideMuteRule(roomId: string): IPushRule | null { return null; } -function isRuleForRoom(roomId: string, rule: IPushRule): boolean { +export function isRuleRoomSpecific(rule: IPushRule): boolean { if (rule.conditions?.length !== 1) { return false; } const cond = rule.conditions[0]; - return cond.kind === ConditionKind.EventMatch && cond.key === "room_id" && cond.pattern === roomId; + return cond.kind === ConditionKind.EventMatch && cond.key === "room_id"; +} + +function isRuleForRoom(roomId: string, rule: IPushRule): boolean { + if (!isRuleRoomSpecific(rule)) { + return false; + } + const cond = rule.conditions[0]; + return cond.pattern === roomId; } function isMuteRule(rule: IPushRule): boolean { @@ -205,25 +213,21 @@ function isMuteRule(rule: IPushRule): boolean { export function determineUnreadState( room?: Room, threadId?: string, -): { color: NotificationColor; symbol: string | null; count: number; muted: boolean } { +): { color: NotificationColor; symbol: string | null; count: number } { if (!room) { - return { symbol: null, count: 0, color: NotificationColor.None, muted: false }; + return { symbol: null, count: 0, color: NotificationColor.None }; } - const echoChamber = EchoChamber.forRoom(room); - const notifVolume = echoChamber.notificationVolume; - const muted = notifVolume === RoomNotifState.Mute; - if (getUnsentMessages(room, threadId).length > 0) { - return { symbol: "!", count: 1, color: NotificationColor.Unsent, muted }; + return { symbol: "!", count: 1, color: NotificationColor.Unsent }; } if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) { - return { symbol: "!", count: 1, color: NotificationColor.Red, muted }; + return { symbol: "!", count: 1, color: NotificationColor.Red }; } if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) { - return { symbol: null, count: 0, color: NotificationColor.None, muted }; + return { symbol: null, count: 0, color: NotificationColor.None }; } const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId); @@ -231,11 +235,11 @@ export function determineUnreadState( const trueCount = greyNotifs || redNotifs; if (redNotifs > 0) { - return { symbol: null, count: trueCount, color: NotificationColor.Red, muted }; + return { symbol: null, count: trueCount, color: NotificationColor.Red }; } if (greyNotifs > 0) { - return { symbol: null, count: trueCount, color: NotificationColor.Grey, muted }; + return { symbol: null, count: trueCount, color: NotificationColor.Grey }; } // We don't have any notified messages, but we might have unread messages. Let's @@ -248,6 +252,5 @@ export function determineUnreadState( symbol: null, count: trueCount, color: hasUnread ? NotificationColor.Bold : NotificationColor.None, - muted, }; } diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index b32066a16b0..3c0447a1434 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -92,7 +92,9 @@ export class RoomNotificationState extends NotificationState implements IDestroy private updateNotificationState(): void { const snapshot = this.snapshot(); - const { color, symbol, count, muted } = RoomNotifs.determineUnreadState(this.room); + const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room); + const muted = + RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute; this._color = color; this._symbol = symbol; this._count = count; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index f71bf24807b..46b21777121 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -40,7 +40,7 @@ import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; -import { getChangedOverridePushRules } from "./utils/roomMute"; +import { getChangedOverrideRoomPushRules } from "./utils/roomMute"; interface IState { // state is tracked in underlying classes @@ -291,7 +291,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements return; } - const possibleMuteChangeRoomIds = getChangedOverridePushRules(payload); + const possibleMuteChangeRoomIds = getChangedOverrideRoomPushRules(payload); if (possibleMuteChangeRoomIds) { for (const roomId of possibleMuteChangeRoomIds) { const room = roomId && this.matrixClient.getRoom(roomId); diff --git a/src/stores/room-list/utils/roomMute.ts b/src/stores/room-list/utils/roomMute.ts index c1c36346b54..a75c0efd943 100644 --- a/src/stores/room-list/utils/roomMute.ts +++ b/src/stores/room-list/utils/roomMute.ts @@ -14,12 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, EventType, IPushRules } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, EventType, IPushRules, IPushRule } from "matrix-js-sdk/src/matrix"; import { ActionPayload } from "../../../dispatcher/payloads"; +import { isRuleRoomSpecific } from "../../../RoomNotifs"; import { arrayDiff } from "../../../utils/arrays"; -export const getChangedOverridePushRules = (actionPayload: ActionPayload): string[] | undefined => { +/** + * Gets any changed push rules that are room specific overrides + * @param actionPayload + * @returns {string[]} ruleIds of added or removed rules + */ +export const getChangedOverrideRoomPushRules = (actionPayload: ActionPayload): string[] | undefined => { if ( actionPayload.action !== "MatrixActions.accountData" || actionPayload.event?.getType() !== EventType.PushRules @@ -33,9 +39,8 @@ export const getChangedOverridePushRules = (actionPayload: ActionPayload): strin return undefined; } - // room mute rules are overrides - const roomPushRules = (event.getContent() as IPushRules)?.global?.override; - const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override; + const roomPushRules = (event.getContent() as IPushRules)?.global?.override.filter(isRuleRoomSpecific); + const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override.filter(isRuleRoomSpecific); const { added, removed } = arrayDiff( prevRoomPushRules?.map((rule) => rule.rule_id), From 17c72fbbbfb443314e1f8ae7a32588dc57bd8f7c Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 17 Apr 2023 17:44:46 +1200 Subject: [PATCH 09/26] add default notifications test util --- test/test-utils/pushRules.ts | 351 +++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 test/test-utils/pushRules.ts diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts new file mode 100644 index 00000000000..638f547738f --- /dev/null +++ b/test/test-utils/pushRules.ts @@ -0,0 +1,351 @@ +/* +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 { + IAnnotatedPushRule, + IPushRule, + IPushRules, PushRuleActionName, PushRuleKind, RuleId, TweakName, +} from "matrix-js-sdk/src/matrix"; + +export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ + global: { + underride: [ + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.call.invite" }], + actions: ["notify", { set_tweak: "sound", value: "ring" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.call", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.message" }, + { kind: "room_member_count", is: "2" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.encrypted" }, + { kind: "room_member_count", is: "2" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.encrypted_room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.encrypted" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.encrypted_room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.message" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.message.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.file" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.file.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.image" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.image.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.video" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.video.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.audio" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.audio.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.message" }], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.message", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.encrypted" }], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.encrypted", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "im.vector.modular.widgets" }, + { kind: "event_match", key: "content.type", pattern: "jitsi" }, + { kind: "event_match", key: "state_key", pattern: "*" }, + ], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".im.vector.jitsi", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" }], + actions: ["notify"], + rule_id: ".org.matrix.msc3930.rule.poll_start", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" }], + actions: ["notify"], + rule_id: ".org.matrix.msc3930.rule.poll_end", + default: true, + enabled: true, + }, + ], + sender: [], + room: [], + content: [ + { + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.contains_user_name", + default: true, + pattern: "alice", + enabled: true, + }, + ], + override: [ + { conditions: [], actions: ["dont_notify"], rule_id: ".m.rule.master", default: true, enabled: false }, + { + conditions: [{ kind: "event_match", key: "content.msgtype", pattern: "m.notice" }], + actions: ["dont_notify"], + rule_id: ".m.rule.suppress_notices", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.member" }, + { kind: "event_match", key: "content.membership", pattern: "invite" }, + { kind: "event_match", key: "state_key", pattern: "@alice:example.org" }, + ], + actions: ["notify", { set_tweak: "highlight", value: false }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.invite_for_me", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.member" }], + actions: ["dont_notify"], + rule_id: ".m.rule.member_event", + default: true, + enabled: true, + }, + { + conditions: [ + { + kind: "event_property_contains", + key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + value_type: "user_id", + }, + ], + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3952.is_user_mention", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "contains_display_name" }], + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.contains_display_name", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_property_is", key: "content.org\\.matrix\\.msc3952\\.mentions.room", value: true }, + { kind: "sender_notification_permission", key: "room" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".org.matrix.msc3952.is_room_mention", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "sender_notification_permission", key: "room" }, + { kind: "event_match", key: "content.body", pattern: "@room" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".m.rule.roomnotif", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.tombstone" }, + { kind: "event_match", key: "state_key", pattern: "" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".m.rule.tombstone", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.reaction" }], + actions: [], + rule_id: ".m.rule.reaction", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.server_acl" }, + { kind: "event_match", key: "state_key", pattern: "" }, + ], + actions: [], + rule_id: ".m.rule.room.server_acl", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.response" }], + actions: [], + rule_id: ".org.matrix.msc3930.rule.poll_response", + default: true, + enabled: true, + }, + ], + }, +} as IPushRules); + +/** + * gets default rule by id from default rules + * @param ruleId + * @returns {IPushRule} matching push rule + * @returns {PushRuleKind} + * @throws when no rule is found with ruleId + */ +export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule, kind: PushRuleKind} => { + for (const kind of Object.keys(DEFAULT_PUSH_RULES.global)) { + const rule = DEFAULT_PUSH_RULES.global[kind].find(r => r.rule_id === ruleId); + if (rule) { + return { rule, kind: kind as PushRuleKind }; + } + } + + throw new Error(`Could not find default rule for id ${ruleId}`); +} + +export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPushRule => { + const { rule, kind } = getDefaultRuleWithKind(ruleId); + + return { + ...rule, + kind + }; +} + +export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial = {}): IPushRule => ({ + actions: [], + enabled: true, + default: false, + ...ruleOverrides, + rule_id: ruleId, +}); + +export const makeAnnotatedPushRule = (kind: PushRuleKind, ruleId: RuleId | string, ruleOverrides: Partial = {}): IAnnotatedPushRule => ({ + ...makePushRule(ruleId, ruleOverrides), + kind +}); \ No newline at end of file From e638e15202f222136c4dfb01d712b6e3acc09fb2 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 20 Apr 2023 12:42:18 +1200 Subject: [PATCH 10/26] test getChangedOverrideRoomPushRules --- src/RoomNotifs.ts | 1 - src/stores/room-list/utils/roomMute.ts | 2 +- test/stores/room-list/utils/roomMute-test.ts | 93 ++++++++++++++++++++ test/test-utils/pushRules.ts | 28 +++--- 4 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 test/stores/room-list/utils/roomMute-test.ts diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 0389cce9f9b..7922f35d73d 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -27,7 +27,6 @@ import { getUnsentMessages } from "./components/structures/RoomStatusBar"; import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; import SettingsStore from "./settings/SettingsStore"; -import { EchoChamber } from "./stores/local-echo/EchoChamber"; export enum RoomNotifState { AllMessagesLoud = "all_messages_loud", diff --git a/src/stores/room-list/utils/roomMute.ts b/src/stores/room-list/utils/roomMute.ts index a75c0efd943..871bae0778e 100644 --- a/src/stores/room-list/utils/roomMute.ts +++ b/src/stores/room-list/utils/roomMute.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, EventType, IPushRules, IPushRule } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, EventType, IPushRules } from "matrix-js-sdk/src/matrix"; import { ActionPayload } from "../../../dispatcher/payloads"; import { isRuleRoomSpecific } from "../../../RoomNotifs"; diff --git a/test/stores/room-list/utils/roomMute-test.ts b/test/stores/room-list/utils/roomMute-test.ts new file mode 100644 index 00000000000..7c1a9e0cb9d --- /dev/null +++ b/test/stores/room-list/utils/roomMute-test.ts @@ -0,0 +1,93 @@ +/* +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 { ConditionKind, EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { getChangedOverrideRoomPushRules } from "../../../../src/stores/room-list/utils/roomMute"; +import { DEFAULT_PUSH_RULES, getDefaultRuleWithKind, makePushRule } from "../../../test-utils/pushRules"; + +describe("getChangedOverrideRoomPushRules()", () => { + const makePushRulesEvent = (overrideRules = []): MatrixEvent => { + return new MatrixEvent({ + type: EventType.PushRules, + content: { + global: { + ...DEFAULT_PUSH_RULES.global, + override: overrideRules, + }, + }, + }); + }; + + it("returns undefined when dispatched action is not accountData", () => { + const action = { action: "MatrixActions.Event.decrypted", event: new MatrixEvent({}) }; + expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + }); + + it("returns undefined when dispatched action is not pushrules", () => { + const action = { action: "MatrixActions.accountData", event: new MatrixEvent({ type: "not-push-rules" }) }; + expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + }); + + it("returns undefined when actions event is falsy", () => { + const action = { action: "MatrixActions.accountData", event: undefined }; + expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + }); + + it("returns undefined when actions previousEvent is falsy", () => { + const pushRulesEvent = makePushRulesEvent(); + const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: undefined }; + expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + }); + + it("filters out non-room specific rules", () => { + // an override rule that exists in default rules + const { rule } = getDefaultRuleWithKind(".m.rule.contains_display_name"); + const updatedRule = { + ...rule, + enabled: false, + }; + const previousEvent = makePushRulesEvent([rule]); + const pushRulesEvent = makePushRulesEvent([updatedRule]); + const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; + // contains_display_name changed, but is not room-specific + expect(getChangedOverrideRoomPushRules(action)).toEqual([]); + }); + + it("returns ruleIds for added room rules", () => { + const roomId1 = "!room1:server.org"; + const rule = makePushRule(roomId1, { + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }], + }); + const previousEvent = makePushRulesEvent(); + const pushRulesEvent = makePushRulesEvent([rule]); + const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; + // contains_display_name changed, but is not room-specific + expect(getChangedOverrideRoomPushRules(action)).toEqual([rule.rule_id]); + }); + + it("returns ruleIds for removed room rules", () => { + const roomId1 = "!room1:server.org"; + const rule = makePushRule(roomId1, { + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }], + }); + const previousEvent = makePushRulesEvent([rule]); + const pushRulesEvent = makePushRulesEvent(); + const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; + // contains_display_name changed, but is not room-specific + expect(getChangedOverrideRoomPushRules(action)).toEqual([rule.rule_id]); + }); +}); diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts index 638f547738f..ac9a99f0b39 100644 --- a/test/test-utils/pushRules.ts +++ b/test/test-utils/pushRules.ts @@ -14,11 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - IAnnotatedPushRule, - IPushRule, - IPushRules, PushRuleActionName, PushRuleKind, RuleId, TweakName, -} from "matrix-js-sdk/src/matrix"; +import { IAnnotatedPushRule, IPushRule, IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ global: { @@ -312,30 +308,30 @@ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ /** * gets default rule by id from default rules - * @param ruleId + * @param ruleId * @returns {IPushRule} matching push rule * @returns {PushRuleKind} * @throws when no rule is found with ruleId */ -export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule, kind: PushRuleKind} => { +export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule; kind: PushRuleKind } => { for (const kind of Object.keys(DEFAULT_PUSH_RULES.global)) { - const rule = DEFAULT_PUSH_RULES.global[kind].find(r => r.rule_id === ruleId); + const rule = DEFAULT_PUSH_RULES.global[kind].find((r) => r.rule_id === ruleId); if (rule) { return { rule, kind: kind as PushRuleKind }; } } throw new Error(`Could not find default rule for id ${ruleId}`); -} +}; export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPushRule => { const { rule, kind } = getDefaultRuleWithKind(ruleId); return { ...rule, - kind + kind, }; -} +}; export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial = {}): IPushRule => ({ actions: [], @@ -345,7 +341,11 @@ export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial = {}): IAnnotatedPushRule => ({ +export const makeAnnotatedPushRule = ( + kind: PushRuleKind, + ruleId: RuleId | string, + ruleOverrides: Partial = {}, +): IAnnotatedPushRule => ({ ...makePushRule(ruleId, ruleOverrides), - kind -}); \ No newline at end of file + kind, +}); From cd007197cc9c08a5afc661b3f52057a01b135f43 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 20 Apr 2023 12:44:18 +1200 Subject: [PATCH 11/26] remove file --- .../tag-sorting/RecentMuteAlgorithm.ts | 129 ------------------ 1 file changed, 129 deletions(-) delete mode 100644 src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts deleted file mode 100644 index e05b4e7e586..00000000000 --- a/src/stores/room-list/algorithms/tag-sorting/RecentMuteAlgorithm.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room"; -import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; - -import { TagID } from "../../models"; -import { IAlgorithm } from "./IAlgorithm"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import * as Unread from "../../../../Unread"; -import { EffectiveMembership, getEffectiveMembership } from "../../../../utils/membership"; - -export function shouldCauseReorder(event: MatrixEvent): boolean { - const type = event.getType(); - const content = event.getContent(); - const prevContent = event.getPrevContent(); - - // Never ignore membership changes - if (type === EventType.RoomMember && prevContent.membership !== content.membership) return true; - - // Ignore display name changes - if (type === EventType.RoomMember && prevContent.displayname !== content.displayname) return false; - // Ignore avatar changes - if (type === EventType.RoomMember && prevContent.avatar_url !== content.avatar_url) return false; - - return true; -} - -export const sortRooms = (rooms: Room[]): Room[] => { - // We cache the timestamp lookup to avoid iterating forever on the timeline - // of events. This cache only survives a single sort though. - // We wouldn't need this if `.sort()` didn't constantly try and compare all - // of the rooms to each other. - - // TODO: We could probably improve the sorting algorithm here by finding changes. - // See https://github.com/vector-im/element-web/issues/14459 - // For example, if we spent a little bit of time to determine which elements have - // actually changed (probably needs to be done higher up?) then we could do an - // insertion sort or similar on the limited set of changes. - - // TODO: Don't assume we're using the same client as the peg - // See https://github.com/vector-im/element-web/issues/14458 - let myUserId = ""; - if (MatrixClientPeg.get()) { - myUserId = MatrixClientPeg.get().getUserId()!; - } - - const tsCache: { [roomId: string]: number } = {}; - - return rooms.sort((a, b) => { - const roomALastTs = tsCache[a.roomId] ?? getLastTs(a, myUserId); - const roomBLastTs = tsCache[b.roomId] ?? getLastTs(b, myUserId); - - tsCache[a.roomId] = roomALastTs; - tsCache[b.roomId] = roomBLastTs; - - return roomBLastTs - roomALastTs; - }); -}; - -const getLastTs = (r: Room, userId: string): number => { - const mainTimelineLastTs = ((): number => { - // Apparently we can have rooms without timelines, at least under testing - // environments. Just return MAX_INT when this happens. - if (!r?.timeline) { - return Number.MAX_SAFE_INTEGER; - } - - // If the room hasn't been joined yet, it probably won't have a timeline to - // parse. We'll still fall back to the timeline if this fails, but chances - // are we'll at least have our own membership event to go off of. - const effectiveMembership = getEffectiveMembership(r.getMyMembership()); - if (effectiveMembership !== EffectiveMembership.Join) { - const membershipEvent = r.currentState.getStateEvents(EventType.RoomMember, userId); - if (membershipEvent && !Array.isArray(membershipEvent)) { - return membershipEvent.getTs(); - } - } - - for (let i = r.timeline.length - 1; i >= 0; --i) { - const ev = r.timeline[i]; - if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) - - if ((ev.getSender() === userId && shouldCauseReorder(ev)) || Unread.eventTriggersUnreadCount(ev)) { - return ev.getTs(); - } - } - - // we might only have events that don't trigger the unread indicator, - // in which case use the oldest event even if normally it wouldn't count. - // This is better than just assuming the last event was forever ago. - return r.timeline[0]?.getTs() ?? Number.MAX_SAFE_INTEGER; - })(); - - const threadLastEventTimestamps = r.getThreads().map((thread) => { - const event = thread.replyToEvent ?? thread.rootEvent; - return event?.getTs() ?? 0; - }); - - return Math.max(mainTimelineLastTs, ...threadLastEventTimestamps); -}; - -/** - * Sorts rooms according to the last event's timestamp in each room that seems - * useful to the user. - */ -export class RecentMuteAlgorithm implements IAlgorithm { - public sortRooms(rooms: Room[], tagId: TagID): Room[] { - return sortRooms(rooms); - } - - public getLastTs(room: Room, userId: string): number { - return getLastTs(room, userId); - } -} From 625e286e5c684ade9e0fda53d4aa4ec0f83ffadd Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 20 Apr 2023 14:56:29 +1200 Subject: [PATCH 12/26] test roomudpate in roomliststore --- test/stores/room-list/RoomListStore-test.ts | 74 ++++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index 5c6435780e2..4dc53c1a7a8 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -14,16 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; -import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; +import defaultDispatcher, { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import SettingsStore, { CallbackFn } from "../../../src/settings/SettingsStore"; import { ListAlgorithm, SortAlgorithm } from "../../../src/stores/room-list/algorithms/models"; import { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../src/stores/room-list/models"; import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; import DMRoomMap from "../../../src/utils/DMRoomMap"; -import { stubClient, upsertRoomStateEvents } from "../../test-utils"; +import { flushPromises, stubClient, upsertRoomStateEvents } from "../../test-utils"; +import { DEFAULT_PUSH_RULES, makePushRule } from "../../test-utils/pushRules"; describe("RoomListStore", () => { const client = stubClient(); @@ -69,12 +70,15 @@ describe("RoomListStore", () => { }); upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]); const oldRoom = new Room(oldRoomId, client, userId, {}); + const normalRoom = new Room("!normal:server.org", client, userId); client.getRoom = jest.fn().mockImplementation((roomId) => { switch (roomId) { case newRoomId: return roomWithCreatePredecessor; case oldRoomId: return oldRoom; + case normalRoom.roomId: + return normalRoom; default: return null; } @@ -274,4 +278,68 @@ describe("RoomListStore", () => { expect(client.getVisibleRooms).toHaveBeenCalledTimes(1); }); }); + + describe("room updates", () => { + const makeStore = async () => { + const store = new RoomListStoreClass(defaultDispatcher); + await store.start(); + return store; + }; + + describe("push rules updates", () => { + const makePushRulesEvent = (overrideRules = []): MatrixEvent => { + return new MatrixEvent({ + type: EventType.PushRules, + content: { + global: { + ...DEFAULT_PUSH_RULES.global, + override: overrideRules, + }, + }, + }); + }; + + it("triggers a room update when room mutes have changed", async () => { + const rule = makePushRule(normalRoom.roomId, { + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }], + }); + const event = makePushRulesEvent([rule]); + const previousEvent = makePushRulesEvent(); + + const store = await makeStore(); + // @ts-ignore private property alg + const algorithmSpy = jest.spyOn(store.algorithm, "handleRoomUpdate").mockReturnValue(undefined); + // @ts-ignore cheat and call protected fn + store.onAction({ action: "MatrixActions.accountData", event, previousEvent }); + // flush setImmediate + await flushPromises(); + + expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange); + }); + + it("handles when a muted room is unknown by the room list", async () => { + const rule = makePushRule(normalRoom.roomId, { + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }], + }); + const unknownRoomRule = makePushRule("!unknown:server.org", { + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: "!unknown:server.org" }], + }); + const event = makePushRulesEvent([unknownRoomRule, rule]); + const previousEvent = makePushRulesEvent(); + + const store = await makeStore(); + // @ts-ignore private property alg + const algorithmSpy = jest.spyOn(store.algorithm, "handleRoomUpdate").mockReturnValue(undefined); + + // @ts-ignore cheat and call protected fn + store.onAction({ action: "MatrixActions.accountData", event, previousEvent }); + // flush setImmediate + await flushPromises(); + + // only one call to update made for normalRoom + expect(algorithmSpy).toHaveBeenCalledTimes(1); + expect(algorithmSpy).toHaveBeenCalledWith(normalRoom, RoomUpdateCause.PossibleMuteChange); + }); + }); + }); }); From b8cb0508d3b760a30f5487fc74d645d3961eed60 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Apr 2023 13:21:58 +1200 Subject: [PATCH 13/26] unit test ImportanceAlgorithm --- .../list-ordering/ImportanceAlgorithm-test.ts | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts new file mode 100644 index 00000000000..3c83d89887e --- /dev/null +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -0,0 +1,298 @@ +/* +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 { MatrixEvent, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; +import { ImportanceAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm"; +import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models"; +import * as RoomNotifs from "../../../../../src/RoomNotifs"; +import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models"; +import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; +import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; + +describe("ImportanceAlgorithm", () => { + const userId = "@alice:server.org"; + const tagId = DefaultTagID.Favourite; + + const makeRoom = (id: string, name: string, order?: number): Room => { + const room = new Room(id, client, userId); + room.name = name; + const tagEvent = new MatrixEvent({ + type: "m.tag", + content: { + tags: { + [tagId]: { + order, + }, + }, + }, + }); + room.addTags(tagEvent); + return room; + }; + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + }); + const roomA = makeRoom("!aaa:server.org", "Alpha", 2); + const roomB = makeRoom("!bbb:server.org", "Bravo", 5); + const roomC = makeRoom("!ccc:server.org", "Charlie", 1); + const roomD = makeRoom("!ddd:server.org", "Delta", 4); + const roomE = makeRoom("!eee:server.org", "Echo", 3); + const roomX = makeRoom("!xxx:server.org", "Xylophone", 99); + + const unreadStates = { + red: { symbol: null, count: 1, color: NotificationColor.Red }, + grey: { symbol: null, count: 1, color: NotificationColor.Grey }, + none: { symbol: null, count: 0, color: NotificationColor.None }, + }; + + beforeEach(() => { + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + }); + + const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { + const algorithm = new ImportanceAlgorithm(tagId, sortAlgorithm); + algorithm.setRooms(rooms || [roomA, roomB, roomC]); + return algorithm; + }; + + describe("When sortAlgorithm is manual", () => { + const sortAlgorithm = SortAlgorithm.Manual; + it("orders rooms by tag order without categorizing", () => { + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState"); + const algorithm = setupAlgorithm(sortAlgorithm); + + // didn't check notif state + expect(RoomNotificationStateStore.instance.getRoomState).not.toHaveBeenCalled(); + // sorted according to room tag order + expect(algorithm.orderedRooms).toEqual([roomC, roomA, roomB]); + }); + + describe("handleRoomUpdate", () => { + // XXX: This doesn't work because manual ordered rooms dont get categoryindices + // possibly related https://github.com/vector-im/element-web/issues/25099 + it.skip("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB]); + }); + + // XXX: This doesn't work because manual ordered rooms dont get categoryindices + it.skip("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomD, roomE]); + }); + + it("does nothing and returns false for a timeline update", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const beforeRooms = algorithm.orderedRooms; + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(false); + // strict equal + expect(algorithm.orderedRooms).toBe(beforeRooms); + }); + + it("does nothing and returns false for a read receipt update", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const beforeRooms = algorithm.orderedRooms; + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.ReadReceipt); + + expect(shouldTriggerUpdate).toBe(false); + // strict equal + expect(algorithm.orderedRooms).toBe(beforeRooms); + }); + + it("throws for an unhandle update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + }); + }); + + describe("When sortAlgorithm is alphabetical", () => { + const sortAlgorithm = SortAlgorithm.Alphabetic; + + beforeEach(async () => { + // destroy roomMap so we can start fresh + // @ts-ignore private property + RoomNotificationStateStore.instance.roomMap = new Map(); + + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + jest.spyOn(RoomNotifs, "determineUnreadState") + .mockClear() + .mockImplementation((room) => { + switch (room) { + // b and e have red notifs + case roomB: + case roomE: + return unreadStates.red; + // c is grey + case roomC: + return unreadStates.grey; + default: + return unreadStates.none; + } + }); + }); + + it("orders rooms by alpha when they have the same notif state", () => { + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to alpha + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + it("orders rooms by notification state then alpha", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + + expect(algorithm.orderedRooms).toEqual([ + // alpha within red + roomB, + roomE, + // grey + roomC, + // alpha within none + roomA, + roomD, + ]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC]); + // no re-sorting on a remove + expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + + it("warns and returns without change when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + // inserted according to notif state + expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId); + }); + + it("throws for an unhandled update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + + describe("time and read receipt updates", () => { + it("throws for when a room is not indexed", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + + expect(() => algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline)).toThrow( + `Room ${roomX.roomId} has no index in ${tagId}`, + ); + }); + + it("re-sorts category when updated room has not changed category", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomE, roomC, roomA, roomD]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomB, roomE], tagId); + }); + + it("re-sorts category when updated room has changed category", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + // change roomE to unreadState.none + jest.spyOn(RoomNotifs, "determineUnreadState").mockImplementation((room) => { + switch (room) { + // b and e have red notifs + case roomB: + return unreadStates.red; + // c is grey + case roomC: + return unreadStates.grey; + case roomE: + default: + return unreadStates.none; + } + }); + // @ts-ignore don't bother mocking rest of emit properties + roomE.emit(RoomEvent.Timeline, new MatrixEvent({ type: "whatever", room_id: roomE.roomId })); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC, roomA, roomD, roomE]); + + // only sorted within roomE's new category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId); + }); + }); + }); + }); +}); From e44f8bedf02868094e73f98a8cc3bb7b40a5d7a8 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Apr 2023 13:42:22 +1200 Subject: [PATCH 14/26] strict fixes --- .../algorithms/list-ordering/ImportanceAlgorithm-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts index 3c83d89887e..6db3c369fb0 100644 --- a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -57,7 +57,7 @@ describe("ImportanceAlgorithm", () => { const roomE = makeRoom("!eee:server.org", "Echo", 3); const roomX = makeRoom("!xxx:server.org", "Xylophone", 99); - const unreadStates = { + const unreadStates: Record> = { red: { symbol: null, count: 1, color: NotificationColor.Red }, grey: { symbol: null, count: 1, color: NotificationColor.Grey }, none: { symbol: null, count: 0, color: NotificationColor.None }, From 40281310b438c753b04b9e49cff3591e4754d9e9 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Apr 2023 15:43:04 +1200 Subject: [PATCH 15/26] test recent x importance with muted rooms --- .../list-ordering/ImportanceAlgorithm.ts | 7 +- .../list-ordering/ImportanceAlgorithm-test.ts | 140 +++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 0117941ec8f..ab90f51e98c 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -96,6 +96,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); + console.log(room.roomId, state.muted, state.color); return this.isMutedToBottom && state.muted ? NotificationColor.Muted : state.color; } @@ -174,15 +175,17 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { throw new Error(`Unsupported update cause: ${cause}`); } + // don't react to mute changes when we are not sorting by mute if (cause === RoomUpdateCause.PossibleMuteChange && !this.isMutedToBottom) { - return; + return false; } - const category = this.getRoomCategory(room); if (this.sortingAlgorithm === SortAlgorithm.Manual) { return false; // Nothing to do here. } + const category = this.getRoomCategory(room); + const roomIdx = this.getRoomIndex(room); if (roomIdx === -1) { throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts index 6db3c369fb0..fd86455fa02 100644 --- a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, MatrixEvent, PushRuleActionName, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; @@ -25,6 +25,8 @@ import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-li import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; +import { RecentAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import { DEFAULT_PUSH_RULES, makePushRule } from "../../../../test-utils/pushRules"; describe("ImportanceAlgorithm", () => { const userId = "@alice:server.org"; @@ -50,6 +52,7 @@ describe("ImportanceAlgorithm", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), }); + const roomA = makeRoom("!aaa:server.org", "Alpha", 2); const roomB = makeRoom("!bbb:server.org", "Bravo", 5); const roomC = makeRoom("!ccc:server.org", "Charlie", 1); @@ -57,6 +60,21 @@ describe("ImportanceAlgorithm", () => { const roomE = makeRoom("!eee:server.org", "Echo", 3); const roomX = makeRoom("!xxx:server.org", "Xylophone", 99); + const muteRoomARule = makePushRule(roomA.roomId, { + actions: [PushRuleActionName.DontNotify], + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomA.roomId }], + }); + const muteRoomBRule = makePushRule(roomB.roomId, { + actions: [PushRuleActionName.DontNotify], + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomB.roomId }], + }); + client.pushRules = { + global: { + ...DEFAULT_PUSH_RULES.global, + override: [...DEFAULT_PUSH_RULES.global.override, muteRoomARule, muteRoomBRule], + }, + }; + const unreadStates: Record> = { red: { symbol: null, count: 1, color: NotificationColor.Red }, grey: { symbol: null, count: 1, color: NotificationColor.Grey }, @@ -69,6 +87,8 @@ describe("ImportanceAlgorithm", () => { count: 0, color: NotificationColor.None, }); + + jest.spyOn; }); const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { @@ -240,6 +260,18 @@ describe("ImportanceAlgorithm", () => { ).toThrow("Unsupported update cause: something unexpected"); }); + it("ignores a mute change", () => { + // muted rooms are not pushed to the bottom when sort is alpha + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange); + + expect(shouldTriggerUpdate).toBe(false); + // no sorting + expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + describe("time and read receipt updates", () => { it("throws for when a room is not indexed", () => { const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); @@ -295,4 +327,110 @@ describe("ImportanceAlgorithm", () => { }); }); }); + + describe("When sortAlgorithm is recent", () => { + const sortAlgorithm = SortAlgorithm.Recent; + + // mock recent algorithm sorting + const fakeRecentOrder = [roomC, roomB, roomE, roomD, roomA]; + + beforeEach(async () => { + // destroy roomMap so we can start fresh + // @ts-ignore private property + RoomNotificationStateStore.instance.roomMap = new Map(); + + jest.spyOn(RecentAlgorithm.prototype, "sortRooms") + .mockClear() + .mockImplementation((rooms: Room[]) => + fakeRecentOrder.filter((sortedRoom) => rooms.includes(sortedRoom)), + ); + jest.spyOn(RoomNotifs, "determineUnreadState") + .mockClear() + .mockImplementation((room) => { + switch (room) { + // b, c and e have red notifs + case roomB: + case roomE: + case roomC: + return unreadStates.red; + default: + return unreadStates.none; + } + }); + }); + + it("orders rooms by recent when they have the same notif state", () => { + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to recent + expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomA]); + }); + + it("orders rooms by notification state then recent", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + + expect(algorithm.orderedRooms).toEqual([ + // recent within red + roomC, + roomE, + // recent within none + roomD, + // muted + roomB, + roomA, + ]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB]); + // no re-sorting on a remove + expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + + it("warns and returns without change when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + // inserted according to notif state and mute + expect(algorithm.orderedRooms).toEqual([roomC, roomE, roomB, roomA]); + // only sorted within category + expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomE, roomC], tagId); + }); + + it("re-sorts on a mute change", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange); + + expect(shouldTriggerUpdate).toBe(true); + expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomC, roomE], tagId); + }); + }); + }); }); From f5faa5f9acf8babe9d060c631c1bdde900f8278e Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Apr 2023 16:30:52 +1200 Subject: [PATCH 16/26] unit test NaturalAlgorithm --- .../list-ordering/NaturalAlgorithm-test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts diff --git a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts new file mode 100644 index 00000000000..21879586b38 --- /dev/null +++ b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts @@ -0,0 +1,136 @@ +/* +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 { Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { NaturalAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm"; +import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models"; +import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models"; +import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; + +describe("NaturalAlgorithm", () => { + const userId = "@alice:server.org"; + const tagId = DefaultTagID.Favourite; + + const makeRoom = (id: string, name: string): Room => { + const room = new Room(id, client, userId); + room.name = name; + return room; + }; + + const client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + }); + const roomA = makeRoom("!aaa:server.org", "Alpha"); + const roomB = makeRoom("!bbb:server.org", "Bravo"); + const roomC = makeRoom("!ccc:server.org", "Charlie"); + const roomD = makeRoom("!ddd:server.org", "Delta"); + const roomE = makeRoom("!eee:server.org", "Echo"); + const roomX = makeRoom("!xxx:server.org", "Xylophone"); + + const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { + const algorithm = new NaturalAlgorithm(tagId, sortAlgorithm); + algorithm.setRooms(rooms || [roomA, roomB, roomC]); + return algorithm; + }; + + describe("When sortAlgorithm is alphabetical", () => { + const sortAlgorithm = SortAlgorithm.Alphabetic; + + beforeEach(async () => { + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + }); + + it("orders rooms by alpha", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to alpha + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomB, roomC]); + }); + + it("warns when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC, roomE]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith( + [roomA, roomB, roomC, roomE], + tagId, + ); + }); + + it("throws for an unhandled update cause", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + expect(() => + algorithm.handleRoomUpdate(roomA, "something unexpected" as unknown as RoomUpdateCause), + ).toThrow("Unsupported update cause: something unexpected"); + }); + + describe("time and read receipt updates", () => { + it("handles when a room is not indexed", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomX, RoomUpdateCause.Timeline); + + // for better or worse natural alg sets this to true + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + }); + + it("re-sorts rooms when timeline updates", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.Timeline); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomC]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomB, roomC], tagId); + }); + }); + }); + }); +}); From 652a41fcb1587708aae4d65f1487a89de4fc9f36 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 24 Apr 2023 13:56:58 +1200 Subject: [PATCH 17/26] test naturalalgorithm with muted rooms --- .../list-ordering/NaturalAlgorithm.ts | 9 +- .../list-ordering/ImportanceAlgorithm-test.ts | 2 - .../list-ordering/NaturalAlgorithm-test.ts | 157 +++++++++++++++++- 3 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 0190bd2a027..36019599503 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -81,11 +81,14 @@ export class NaturalAlgorithm extends OrderingAlgorithm { return true; } else if (cause === RoomUpdateCause.RoomRemoved) { return this.removeRoom(room); - } else if (this.isMutedToBottom && cause === RoomUpdateCause.PossibleMuteChange) { - return this.onPossibleMuteChange(room); + } else if (cause === RoomUpdateCause.PossibleMuteChange) { + if (this.isMutedToBottom) { + return this.onPossibleMuteChange(room); + } else { + return false; + } } - // @TODO(kerrya) what cases are hitting here? should they cause a reorder?) // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457 // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags if (isMuted) { diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts index fd86455fa02..87c76aa5083 100644 --- a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -87,8 +87,6 @@ describe("ImportanceAlgorithm", () => { count: 0, color: NotificationColor.None, }); - - jest.spyOn; }); const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { diff --git a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts index 21879586b38..0536dbcd092 100644 --- a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts @@ -14,14 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, EventType, MatrixEvent, PushRuleActionName, Room } from "matrix-js-sdk/src/matrix"; +import { ClientEvent } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { NaturalAlgorithm } from "../../../../../src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm"; import { SortAlgorithm } from "../../../../../src/stores/room-list/algorithms/models"; import { DefaultTagID, RoomUpdateCause } from "../../../../../src/stores/room-list/models"; import { AlphabeticAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm"; +import { RecentAlgorithm } from "../../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; +import * as RoomNotifs from "../../../../../src/RoomNotifs"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; +import { DEFAULT_PUSH_RULES, makePushRule } from "../../../../test-utils/pushRules"; +import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor"; describe("NaturalAlgorithm", () => { const userId = "@alice:server.org"; @@ -43,6 +49,21 @@ describe("NaturalAlgorithm", () => { const roomE = makeRoom("!eee:server.org", "Echo"); const roomX = makeRoom("!xxx:server.org", "Xylophone"); + const muteRoomARule = makePushRule(roomA.roomId, { + actions: [PushRuleActionName.DontNotify], + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomA.roomId }], + }); + const muteRoomDRule = makePushRule(roomD.roomId, { + actions: [PushRuleActionName.DontNotify], + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomD.roomId }], + }); + client.pushRules = { + global: { + ...DEFAULT_PUSH_RULES.global, + override: [...DEFAULT_PUSH_RULES.global.override, muteRoomARule, muteRoomDRule], + }, + }; + const setupAlgorithm = (sortAlgorithm: SortAlgorithm, rooms?: Room[]) => { const algorithm = new NaturalAlgorithm(tagId, sortAlgorithm); algorithm.setRooms(rooms || [roomA, roomB, roomC]); @@ -80,7 +101,7 @@ describe("NaturalAlgorithm", () => { const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); - expect(shouldTriggerUpdate).toBe(true); + expect(shouldTriggerUpdate).toBe(false); expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); }); @@ -99,6 +120,29 @@ describe("NaturalAlgorithm", () => { ); }); + it("adds a new muted room", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomA, roomB, roomE]); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + // muted room mixed in main category + expect(algorithm.orderedRooms).toEqual([roomA, roomB, roomD, roomE]); + // only sorted within category + expect(AlphabeticAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + }); + + it("ignores a mute change update", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(AlphabeticAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.PossibleMuteChange); + + expect(shouldTriggerUpdate).toBe(false); + expect(AlphabeticAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + it("throws for an unhandled update cause", () => { const algorithm = setupAlgorithm(sortAlgorithm); @@ -133,4 +177,113 @@ describe("NaturalAlgorithm", () => { }); }); }); + + describe("When sortAlgorithm is recent", () => { + const sortAlgorithm = SortAlgorithm.Recent; + + // mock recent algorithm sorting + const fakeRecentOrder = [roomC, roomA, roomB, roomD, roomE]; + + beforeEach(async () => { + // destroy roomMap so we can start fresh + // @ts-ignore private property + RoomNotificationStateStore.instance.roomMap = new Map(); + + jest.spyOn(RecentAlgorithm.prototype, "sortRooms") + .mockClear() + .mockImplementation((rooms: Room[]) => + fakeRecentOrder.filter((sortedRoom) => rooms.includes(sortedRoom)), + ); + + jest.spyOn(RoomNotifs, "determineUnreadState").mockReturnValue({ + symbol: null, + count: 0, + color: NotificationColor.None, + }); + }); + + it("orders rooms by recent with muted rooms to the bottom", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + + // sorted according to recent + expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomA]); + }); + + describe("handleRoomUpdate", () => { + it("removes a room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomA, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([roomC, roomB]); + // no re-sorting on a remove + expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + + it("warns and returns without change when removing a room that is not indexed", () => { + jest.spyOn(logger, "warn").mockReturnValue(undefined); + const algorithm = setupAlgorithm(sortAlgorithm); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomD, RoomUpdateCause.RoomRemoved); + + expect(shouldTriggerUpdate).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(`Tried to remove unknown room from ${tagId}: ${roomD.roomId}`); + }); + + it("adds a new room", () => { + const algorithm = setupAlgorithm(sortAlgorithm); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.NewRoom); + + expect(shouldTriggerUpdate).toBe(true); + // inserted according to mute then recentness + expect(algorithm.orderedRooms).toEqual([roomC, roomB, roomE, roomA]); + // only sorted within category, muted roomA is not resorted + expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomC, roomB, roomE], tagId); + }); + + it("does not re-sort on possible mute change when room did not change effective mutedness", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange); + + expect(shouldTriggerUpdate).toBe(false); + expect(RecentAlgorithm.prototype.sortRooms).not.toHaveBeenCalled(); + }); + + it("re-sorts on a mute change", () => { + const algorithm = setupAlgorithm(sortAlgorithm, [roomC, roomB, roomE, roomD, roomA]); + jest.spyOn(RecentAlgorithm.prototype, "sortRooms").mockClear(); + + // mute roomE + const muteRoomERule = makePushRule(roomE.roomId, { + actions: [PushRuleActionName.DontNotify], + conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomE.roomId }], + }); + const pushRulesEvent = new MatrixEvent({ type: EventType.PushRules }); + client.pushRules.global.override.push(muteRoomERule); + client.emit(ClientEvent.AccountData, pushRulesEvent); + + const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange); + + expect(shouldTriggerUpdate).toBe(true); + expect(algorithm.orderedRooms).toEqual([ + // unmuted, sorted by recent + roomC, + roomB, + // muted, sorted by recent + roomA, + roomD, + roomE, + ]); + // only sorted muted category + expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledTimes(1); + expect(RecentAlgorithm.prototype.sortRooms).toHaveBeenCalledWith([roomA, roomD, roomE], tagId); + }); + }); + }); }); From 84ea40f05e61b4297b712f2c1451d46ce6a591e8 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 24 Apr 2023 16:28:31 +1200 Subject: [PATCH 18/26] strict fixes --- src/RoomNotifs.ts | 3 ++- src/stores/room-list/utils/roomMute.ts | 8 ++++---- test/stores/room-list/RoomListStore-test.ts | 4 ++-- .../list-ordering/ImportanceAlgorithm-test.ts | 2 +- test/stores/room-list/utils/roomMute-test.ts | 4 ++-- test/test-utils/pushRules.ts | 11 +++++++++-- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 7922f35d73d..771c630fa80 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -201,7 +201,8 @@ function isRuleForRoom(roomId: string, rule: IPushRule): boolean { if (!isRuleRoomSpecific(rule)) { return false; } - const cond = rule.conditions[0]; + // isRuleRoomSpecific checks this condition exists + const cond = rule.conditions![0]!; return cond.pattern === roomId; } diff --git a/src/stores/room-list/utils/roomMute.ts b/src/stores/room-list/utils/roomMute.ts index 871bae0778e..771a9a603cc 100644 --- a/src/stores/room-list/utils/roomMute.ts +++ b/src/stores/room-list/utils/roomMute.ts @@ -39,12 +39,12 @@ export const getChangedOverrideRoomPushRules = (actionPayload: ActionPayload): s return undefined; } - const roomPushRules = (event.getContent() as IPushRules)?.global?.override.filter(isRuleRoomSpecific); - const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override.filter(isRuleRoomSpecific); + const roomPushRules = (event.getContent() as IPushRules)?.global?.override?.filter(isRuleRoomSpecific); + const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override?.filter(isRuleRoomSpecific); const { added, removed } = arrayDiff( - prevRoomPushRules?.map((rule) => rule.rule_id), - roomPushRules?.map((rule) => rule.rule_id), + prevRoomPushRules?.map((rule) => rule.rule_id) || [], + roomPushRules?.map((rule) => rule.rule_id) || [], ); return [...added, ...removed]; diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index 4dc53c1a7a8..e2636c49039 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConditionKind, EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, EventType, IPushRule, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; import defaultDispatcher, { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; import { SettingLevel } from "../../../src/settings/SettingLevel"; @@ -287,7 +287,7 @@ describe("RoomListStore", () => { }; describe("push rules updates", () => { - const makePushRulesEvent = (overrideRules = []): MatrixEvent => { + const makePushRulesEvent = (overrideRules: IPushRule[] = []): MatrixEvent => { return new MatrixEvent({ type: EventType.PushRules, content: { diff --git a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts index 87c76aa5083..c9d6b14d2d6 100644 --- a/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm-test.ts @@ -71,7 +71,7 @@ describe("ImportanceAlgorithm", () => { client.pushRules = { global: { ...DEFAULT_PUSH_RULES.global, - override: [...DEFAULT_PUSH_RULES.global.override, muteRoomARule, muteRoomBRule], + override: [...DEFAULT_PUSH_RULES.global.override!, muteRoomARule, muteRoomBRule], }, }; diff --git a/test/stores/room-list/utils/roomMute-test.ts b/test/stores/room-list/utils/roomMute-test.ts index 7c1a9e0cb9d..82b0001b3ab 100644 --- a/test/stores/room-list/utils/roomMute-test.ts +++ b/test/stores/room-list/utils/roomMute-test.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConditionKind, EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, EventType, IPushRule, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { getChangedOverrideRoomPushRules } from "../../../../src/stores/room-list/utils/roomMute"; import { DEFAULT_PUSH_RULES, getDefaultRuleWithKind, makePushRule } from "../../../test-utils/pushRules"; describe("getChangedOverrideRoomPushRules()", () => { - const makePushRulesEvent = (overrideRules = []): MatrixEvent => { + const makePushRulesEvent = (overrideRules: IPushRule[] = []): MatrixEvent => { return new MatrixEvent({ type: EventType.PushRules, content: { diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts index ac9a99f0b39..31dea308ba4 100644 --- a/test/test-utils/pushRules.ts +++ b/test/test-utils/pushRules.ts @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAnnotatedPushRule, IPushRule, IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; +import { + IAnnotatedPushRule, + IPushRule, + IPushRules, + PushRuleActionName, + PushRuleKind, + RuleId, +} from "matrix-js-sdk/src/matrix"; export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ global: { @@ -315,7 +322,7 @@ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ */ export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule; kind: PushRuleKind } => { for (const kind of Object.keys(DEFAULT_PUSH_RULES.global)) { - const rule = DEFAULT_PUSH_RULES.global[kind].find((r) => r.rule_id === ruleId); + const rule = DEFAULT_PUSH_RULES.global[kind as PushRuleActionName].find((r: IPushRule) => r.rule_id === ruleId); if (rule) { return { rule, kind: kind as PushRuleKind }; } From bbb19f57d4ff8f7b16d2de6155e7c40267a0d2e2 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 26 Apr 2023 11:56:07 +1200 Subject: [PATCH 19/26] comments --- test/test-utils/pushRules.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts index 31dea308ba4..ab8a1dcd917 100644 --- a/test/test-utils/pushRules.ts +++ b/test/test-utils/pushRules.ts @@ -23,6 +23,11 @@ import { RuleId, } from "matrix-js-sdk/src/matrix"; +/** + * Default set of push rules for a new account + * Use to mock push rule fetching, or use `getDefaultRuleWithKind` + * to use default examples of specific push rules + */ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ global: { underride: [ @@ -314,7 +319,7 @@ export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ } as IPushRules); /** - * gets default rule by id from default rules + * Get rule by id from default rules * @param ruleId * @returns {IPushRule} matching push rule * @returns {PushRuleKind} @@ -331,6 +336,11 @@ export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRu throw new Error(`Could not find default rule for id ${ruleId}`); }; +/** + * Get rule by id from default rules as an IAnnotatedPushRule + * @param ruleId + * @returns + */ export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPushRule => { const { rule, kind } = getDefaultRuleWithKind(ruleId); @@ -340,6 +350,12 @@ export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPush }; }; +/** + * Make a push rule with default values + * @param ruleId + * @param ruleOverrides + * @returns IPushRule + */ export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial = {}): IPushRule => ({ actions: [], enabled: true, From 6a15a4869a7dbfe4dbd302b3a824036108aa3fcb Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 26 Apr 2023 11:56:49 +1200 Subject: [PATCH 20/26] add push rules test utility --- test/test-utils/pushRules.ts | 374 +++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 test/test-utils/pushRules.ts diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts new file mode 100644 index 00000000000..ab8a1dcd917 --- /dev/null +++ b/test/test-utils/pushRules.ts @@ -0,0 +1,374 @@ +/* +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 { + IAnnotatedPushRule, + IPushRule, + IPushRules, + PushRuleActionName, + PushRuleKind, + RuleId, +} from "matrix-js-sdk/src/matrix"; + +/** + * Default set of push rules for a new account + * Use to mock push rule fetching, or use `getDefaultRuleWithKind` + * to use default examples of specific push rules + */ +export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({ + global: { + underride: [ + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.call.invite" }], + actions: ["notify", { set_tweak: "sound", value: "ring" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.call", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.message" }, + { kind: "room_member_count", is: "2" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.encrypted" }, + { kind: "room_member_count", is: "2" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.encrypted_room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.encrypted" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.encrypted_room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.message" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.message.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.file" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.file.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.image" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.image.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.video" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.video.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "org.matrix.msc1767.audio" }, + { kind: "room_member_count", is: "2" }, + { + kind: "org.matrix.msc3931.room_version_supports", + feature: "org.matrix.msc3932.extensible_events", + }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }], + rule_id: ".org.matrix.msc3933.rule.extensible.audio.room_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.message" }], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.message", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.encrypted" }], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".m.rule.encrypted", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "im.vector.modular.widgets" }, + { kind: "event_match", key: "content.type", pattern: "jitsi" }, + { kind: "event_match", key: "state_key", pattern: "*" }, + ], + actions: ["notify", { set_tweak: "highlight", value: false }], + rule_id: ".im.vector.jitsi", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" }], + actions: ["notify"], + rule_id: ".org.matrix.msc3930.rule.poll_start", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "room_member_count", is: "2" }, + { kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" }, + ], + actions: ["notify", { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" }], + actions: ["notify"], + rule_id: ".org.matrix.msc3930.rule.poll_end", + default: true, + enabled: true, + }, + ], + sender: [], + room: [], + content: [ + { + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.contains_user_name", + default: true, + pattern: "alice", + enabled: true, + }, + ], + override: [ + { conditions: [], actions: ["dont_notify"], rule_id: ".m.rule.master", default: true, enabled: false }, + { + conditions: [{ kind: "event_match", key: "content.msgtype", pattern: "m.notice" }], + actions: ["dont_notify"], + rule_id: ".m.rule.suppress_notices", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.member" }, + { kind: "event_match", key: "content.membership", pattern: "invite" }, + { kind: "event_match", key: "state_key", pattern: "@alice:example.org" }, + ], + actions: ["notify", { set_tweak: "highlight", value: false }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.invite_for_me", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.room.member" }], + actions: ["dont_notify"], + rule_id: ".m.rule.member_event", + default: true, + enabled: true, + }, + { + conditions: [ + { + kind: "event_property_contains", + key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids", + value_type: "user_id", + }, + ], + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".org.matrix.msc3952.is_user_mention", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "contains_display_name" }], + actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }], + rule_id: ".m.rule.contains_display_name", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_property_is", key: "content.org\\.matrix\\.msc3952\\.mentions.room", value: true }, + { kind: "sender_notification_permission", key: "room" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".org.matrix.msc3952.is_room_mention", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "sender_notification_permission", key: "room" }, + { kind: "event_match", key: "content.body", pattern: "@room" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".m.rule.roomnotif", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.tombstone" }, + { kind: "event_match", key: "state_key", pattern: "" }, + ], + actions: ["notify", { set_tweak: "highlight" }], + rule_id: ".m.rule.tombstone", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "m.reaction" }], + actions: [], + rule_id: ".m.rule.reaction", + default: true, + enabled: true, + }, + { + conditions: [ + { kind: "event_match", key: "type", pattern: "m.room.server_acl" }, + { kind: "event_match", key: "state_key", pattern: "" }, + ], + actions: [], + rule_id: ".m.rule.room.server_acl", + default: true, + enabled: true, + }, + { + conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.response" }], + actions: [], + rule_id: ".org.matrix.msc3930.rule.poll_response", + default: true, + enabled: true, + }, + ], + }, +} as IPushRules); + +/** + * Get rule by id from default rules + * @param ruleId + * @returns {IPushRule} matching push rule + * @returns {PushRuleKind} + * @throws when no rule is found with ruleId + */ +export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule; kind: PushRuleKind } => { + for (const kind of Object.keys(DEFAULT_PUSH_RULES.global)) { + const rule = DEFAULT_PUSH_RULES.global[kind as PushRuleActionName].find((r: IPushRule) => r.rule_id === ruleId); + if (rule) { + return { rule, kind: kind as PushRuleKind }; + } + } + + throw new Error(`Could not find default rule for id ${ruleId}`); +}; + +/** + * Get rule by id from default rules as an IAnnotatedPushRule + * @param ruleId + * @returns + */ +export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPushRule => { + const { rule, kind } = getDefaultRuleWithKind(ruleId); + + return { + ...rule, + kind, + }; +}; + +/** + * Make a push rule with default values + * @param ruleId + * @param ruleOverrides + * @returns IPushRule + */ +export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial = {}): IPushRule => ({ + actions: [], + enabled: true, + default: false, + ...ruleOverrides, + rule_id: ruleId, +}); + +export const makeAnnotatedPushRule = ( + kind: PushRuleKind, + ruleId: RuleId | string, + ruleOverrides: Partial = {}, +): IAnnotatedPushRule => ({ + ...makePushRule(ruleId, ruleOverrides), + kind, +}); From bfb9ed630a7904364195095cefbd1c503239fe4f Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 26 Apr 2023 13:38:20 +1200 Subject: [PATCH 21/26] strict fixes --- .../algorithms/list-ordering/NaturalAlgorithm-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts index 0536dbcd092..46bef644cf4 100644 --- a/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts +++ b/test/stores/room-list/algorithms/list-ordering/NaturalAlgorithm-test.ts @@ -60,7 +60,7 @@ describe("NaturalAlgorithm", () => { client.pushRules = { global: { ...DEFAULT_PUSH_RULES.global, - override: [...DEFAULT_PUSH_RULES.global.override, muteRoomARule, muteRoomDRule], + override: [...DEFAULT_PUSH_RULES.global!.override!, muteRoomARule, muteRoomDRule], }, }; @@ -265,7 +265,7 @@ describe("NaturalAlgorithm", () => { conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomE.roomId }], }); const pushRulesEvent = new MatrixEvent({ type: EventType.PushRules }); - client.pushRules.global.override.push(muteRoomERule); + client.pushRules!.global!.override!.push(muteRoomERule); client.emit(ClientEvent.AccountData, pushRulesEvent); const shouldTriggerUpdate = algorithm.handleRoomUpdate(roomE, RoomUpdateCause.PossibleMuteChange); From 6eb155bf823c689b76b2896ccd5991bbd424e186 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 26 Apr 2023 14:23:14 +1200 Subject: [PATCH 22/26] more strict --- test/stores/room-list/utils/roomMute-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/stores/room-list/utils/roomMute-test.ts b/test/stores/room-list/utils/roomMute-test.ts index 82b0001b3ab..f83e2c327a8 100644 --- a/test/stores/room-list/utils/roomMute-test.ts +++ b/test/stores/room-list/utils/roomMute-test.ts @@ -43,13 +43,13 @@ describe("getChangedOverrideRoomPushRules()", () => { }); it("returns undefined when actions event is falsy", () => { - const action = { action: "MatrixActions.accountData", event: undefined }; + const action = { action: "MatrixActions.accountData" }; expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); }); it("returns undefined when actions previousEvent is falsy", () => { const pushRulesEvent = makePushRulesEvent(); - const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: undefined }; + const action = { action: "MatrixActions.accountData", event: pushRulesEvent }; expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); }); From 0b9997c1d25e2961b3084fab0d6194c270e5d7c3 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 27 Apr 2023 17:50:37 +1200 Subject: [PATCH 23/26] tidy comment --- src/stores/notifications/NotificationColor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts index a2e41ed53ed..ab7097265ed 100644 --- a/src/stores/notifications/NotificationColor.ts +++ b/src/stores/notifications/NotificationColor.ts @@ -17,7 +17,7 @@ limitations under the License. import { _t } from "../../languageHandler"; export enum NotificationColor { - Muted, // the room + Muted, // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special // TODO: Remove bold with notifications: https://github.com/vector-im/element-web/issues/14227 From 237b4ca841d48b448b9b244dfd281d3781e4d8ed Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 27 Apr 2023 17:59:06 +1200 Subject: [PATCH 24/26] document previousevent on account data dispatch event --- src/actions/MatrixActionCreators.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/actions/MatrixActionCreators.ts b/src/actions/MatrixActionCreators.ts index 6e5fb28b9a5..3cc1828a565 100644 --- a/src/actions/MatrixActionCreators.ts +++ b/src/actions/MatrixActionCreators.ts @@ -48,6 +48,7 @@ function createSyncAction(matrixClient: MatrixClient, state: string, prevState: * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". * @property {Object} event_content the content of the MatrixEvent. + * @property {MatrixEvent} previousEvent the previous account data event of the same type, if present */ /** @@ -56,6 +57,7 @@ function createSyncAction(matrixClient: MatrixClient, state: string, prevState: * * @param {MatrixClient} matrixClient the matrix client. * @param {MatrixEvent} accountDataEvent the account data event. + * @param {MatrixEvent | undefined} previousAccountDataEvent the previous account data event of the same type, if present * @returns {AccountDataAction} an action of type MatrixActions.accountData. */ function createAccountDataAction( From 43f33161726a340101cae080473886d6dd87a466 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 27 Apr 2023 18:35:57 +1200 Subject: [PATCH 25/26] simplify (?) room mute rule utilities, comments --- src/RoomNotifs.ts | 36 ++++++++++++++------ src/stores/room-list/RoomListStore.ts | 4 +-- src/stores/room-list/utils/roomMute.ts | 11 +++--- test/stores/room-list/RoomListStore-test.ts | 12 ++++++- test/stores/room-list/utils/roomMute-test.ts | 23 +++++++------ test/test-utils/pushRules.ts | 9 +---- 6 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 771c630fa80..f386f50ada8 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -182,26 +182,42 @@ function findOverrideMuteRule(roomId: string): IPushRule | null { return null; } for (const rule of cli.pushRules.global.override) { - if (rule.enabled && isRuleForRoom(roomId, rule) && isMuteRule(rule)) { + if (rule.enabled && isRuleRoomMuteRuleForRoomId(roomId, rule)) { return rule; } } return null; } -export function isRuleRoomSpecific(rule: IPushRule): boolean { - if (rule.conditions?.length !== 1) { - return false; - } - const cond = rule.conditions[0]; - return cond.kind === ConditionKind.EventMatch && cond.key === "room_id"; +/** + * Checks if a given rule is a room mute rule as implemented by EW + * - matches every event in one room (one condition that is an event match on roomId) + * - silences notifications (one action that is `DontNotify`) + * @param rule - push rule + * @returns {boolean} - true when rule mutes a room + */ +export function isRuleMaybeRoomMuteRule(rule: IPushRule): boolean { + return ( + // matches every event in one room + rule.conditions?.length === 1 && + rule.conditions[0].kind === ConditionKind.EventMatch && + rule.conditions[0].key === "room_id" && + // silences notifications + isMuteRule(rule) + ); } -function isRuleForRoom(roomId: string, rule: IPushRule): boolean { - if (!isRuleRoomSpecific(rule)) { +/** + * Checks if a given rule is a room mute rule as implemented by EW + * @param roomId - id of room to match + * @param rule - push rule + * @returns {boolean} true when rule mutes the given room + */ +function isRuleRoomMuteRuleForRoomId(roomId: string, rule: IPushRule): boolean { + if (!isRuleMaybeRoomMuteRule(rule)) { return false; } - // isRuleRoomSpecific checks this condition exists + // isRuleMaybeRoomMuteRule checks this condition exists const cond = rule.conditions![0]!; return cond.pattern === roomId; } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 46b21777121..2048aad080a 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -40,7 +40,7 @@ import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; -import { getChangedOverrideRoomPushRules } from "./utils/roomMute"; +import { getChangedOverrideRoomMutePushRules } from "./utils/roomMute"; interface IState { // state is tracked in underlying classes @@ -291,7 +291,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements return; } - const possibleMuteChangeRoomIds = getChangedOverrideRoomPushRules(payload); + const possibleMuteChangeRoomIds = getChangedOverrideRoomMutePushRules(payload); if (possibleMuteChangeRoomIds) { for (const roomId of possibleMuteChangeRoomIds) { const room = roomId && this.matrixClient.getRoom(roomId); diff --git a/src/stores/room-list/utils/roomMute.ts b/src/stores/room-list/utils/roomMute.ts index 771a9a603cc..d5b1c3520ff 100644 --- a/src/stores/room-list/utils/roomMute.ts +++ b/src/stores/room-list/utils/roomMute.ts @@ -17,15 +17,16 @@ limitations under the License. import { MatrixEvent, EventType, IPushRules } from "matrix-js-sdk/src/matrix"; import { ActionPayload } from "../../../dispatcher/payloads"; -import { isRuleRoomSpecific } from "../../../RoomNotifs"; +import { isRuleMaybeRoomMuteRule } from "../../../RoomNotifs"; import { arrayDiff } from "../../../utils/arrays"; /** * Gets any changed push rules that are room specific overrides + * that mute notifications * @param actionPayload * @returns {string[]} ruleIds of added or removed rules */ -export const getChangedOverrideRoomPushRules = (actionPayload: ActionPayload): string[] | undefined => { +export const getChangedOverrideRoomMutePushRules = (actionPayload: ActionPayload): string[] | undefined => { if ( actionPayload.action !== "MatrixActions.accountData" || actionPayload.event?.getType() !== EventType.PushRules @@ -39,8 +40,10 @@ export const getChangedOverrideRoomPushRules = (actionPayload: ActionPayload): s return undefined; } - const roomPushRules = (event.getContent() as IPushRules)?.global?.override?.filter(isRuleRoomSpecific); - const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override?.filter(isRuleRoomSpecific); + const roomPushRules = (event.getContent() as IPushRules)?.global?.override?.filter(isRuleMaybeRoomMuteRule); + const prevRoomPushRules = (prevEvent?.getContent() as IPushRules)?.global?.override?.filter( + isRuleMaybeRoomMuteRule, + ); const { added, removed } = arrayDiff( prevRoomPushRules?.map((rule) => rule.rule_id) || [], diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index e2636c49039..aaec6942132 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -14,7 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConditionKind, EventType, IPushRule, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { + ConditionKind, + EventType, + IPushRule, + MatrixEvent, + PendingEventOrdering, + PushRuleActionName, + Room, +} from "matrix-js-sdk/src/matrix"; import defaultDispatcher, { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; import { SettingLevel } from "../../../src/settings/SettingLevel"; @@ -301,6 +309,7 @@ describe("RoomListStore", () => { it("triggers a room update when room mutes have changed", async () => { const rule = makePushRule(normalRoom.roomId, { + actions: [PushRuleActionName.DontNotify], conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }], }); const event = makePushRulesEvent([rule]); @@ -319,6 +328,7 @@ describe("RoomListStore", () => { it("handles when a muted room is unknown by the room list", async () => { const rule = makePushRule(normalRoom.roomId, { + actions: [PushRuleActionName.DontNotify], conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: normalRoom.roomId }], }); const unknownRoomRule = makePushRule("!unknown:server.org", { diff --git a/test/stores/room-list/utils/roomMute-test.ts b/test/stores/room-list/utils/roomMute-test.ts index f83e2c327a8..b7f8442fb9d 100644 --- a/test/stores/room-list/utils/roomMute-test.ts +++ b/test/stores/room-list/utils/roomMute-test.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConditionKind, EventType, IPushRule, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { ConditionKind, EventType, IPushRule, MatrixEvent, PushRuleActionName } from "matrix-js-sdk/src/matrix"; -import { getChangedOverrideRoomPushRules } from "../../../../src/stores/room-list/utils/roomMute"; +import { getChangedOverrideRoomMutePushRules } from "../../../../src/stores/room-list/utils/roomMute"; import { DEFAULT_PUSH_RULES, getDefaultRuleWithKind, makePushRule } from "../../../test-utils/pushRules"; -describe("getChangedOverrideRoomPushRules()", () => { +describe("getChangedOverrideRoomMutePushRules()", () => { const makePushRulesEvent = (overrideRules: IPushRule[] = []): MatrixEvent => { return new MatrixEvent({ type: EventType.PushRules, @@ -34,23 +34,23 @@ describe("getChangedOverrideRoomPushRules()", () => { it("returns undefined when dispatched action is not accountData", () => { const action = { action: "MatrixActions.Event.decrypted", event: new MatrixEvent({}) }; - expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined(); }); it("returns undefined when dispatched action is not pushrules", () => { const action = { action: "MatrixActions.accountData", event: new MatrixEvent({ type: "not-push-rules" }) }; - expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined(); }); it("returns undefined when actions event is falsy", () => { const action = { action: "MatrixActions.accountData" }; - expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined(); }); it("returns undefined when actions previousEvent is falsy", () => { const pushRulesEvent = makePushRulesEvent(); const action = { action: "MatrixActions.accountData", event: pushRulesEvent }; - expect(getChangedOverrideRoomPushRules(action)).toBeUndefined(); + expect(getChangedOverrideRoomMutePushRules(action)).toBeUndefined(); }); it("filters out non-room specific rules", () => { @@ -58,36 +58,39 @@ describe("getChangedOverrideRoomPushRules()", () => { const { rule } = getDefaultRuleWithKind(".m.rule.contains_display_name"); const updatedRule = { ...rule, + actions: [PushRuleActionName.DontNotify], enabled: false, }; const previousEvent = makePushRulesEvent([rule]); const pushRulesEvent = makePushRulesEvent([updatedRule]); const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; // contains_display_name changed, but is not room-specific - expect(getChangedOverrideRoomPushRules(action)).toEqual([]); + expect(getChangedOverrideRoomMutePushRules(action)).toEqual([]); }); it("returns ruleIds for added room rules", () => { const roomId1 = "!room1:server.org"; const rule = makePushRule(roomId1, { + actions: [PushRuleActionName.DontNotify], conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }], }); const previousEvent = makePushRulesEvent(); const pushRulesEvent = makePushRulesEvent([rule]); const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; // contains_display_name changed, but is not room-specific - expect(getChangedOverrideRoomPushRules(action)).toEqual([rule.rule_id]); + expect(getChangedOverrideRoomMutePushRules(action)).toEqual([rule.rule_id]); }); it("returns ruleIds for removed room rules", () => { const roomId1 = "!room1:server.org"; const rule = makePushRule(roomId1, { + actions: [PushRuleActionName.DontNotify], conditions: [{ kind: ConditionKind.EventMatch, key: "room_id", pattern: roomId1 }], }); const previousEvent = makePushRulesEvent([rule]); const pushRulesEvent = makePushRulesEvent(); const action = { action: "MatrixActions.accountData", event: pushRulesEvent, previousEvent: previousEvent }; // contains_display_name changed, but is not room-specific - expect(getChangedOverrideRoomPushRules(action)).toEqual([rule.rule_id]); + expect(getChangedOverrideRoomMutePushRules(action)).toEqual([rule.rule_id]); }); }); diff --git a/test/test-utils/pushRules.ts b/test/test-utils/pushRules.ts index a2c2b48526f..cffc423d1c6 100644 --- a/test/test-utils/pushRules.ts +++ b/test/test-utils/pushRules.ts @@ -14,14 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - IAnnotatedPushRule, - IPushRule, - IPushRules, - PushRuleActionName, - PushRuleKind, - RuleId, -} from "matrix-js-sdk/src/matrix"; +import { IAnnotatedPushRule, IPushRule, IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix"; /** * Default set of push rules for a new account From 30be86ff041f20efdbeef0009f94412f50c7b926 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 5 May 2023 13:29:25 +1200 Subject: [PATCH 26/26] remove debug --- .../room-list/algorithms/list-ordering/ImportanceAlgorithm.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index a2950008c0e..d29d48f7ea0 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -96,7 +96,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // It's fine for us to call this a lot because it's cached, and we shouldn't be // wasting anything by doing so as the store holds single references const state = RoomNotificationStateStore.instance.getRoomState(room); - console.log(room.roomId, state.muted, state.color); return this.isMutedToBottom && state.muted ? NotificationColor.Muted : state.color; }