Cleanup pre MSC3773 thread unread notif logic (#10023)

This commit is contained in:
Germain 2023-01-31 16:59:24 +00:00 committed by GitHub
parent eaf152ceef
commit 703587b8e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 23 additions and 367 deletions

View file

@ -22,7 +22,6 @@ import React from "react";
import classNames from "classnames";
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { _t } from "../../../languageHandler";
import HeaderButton from "./HeaderButton";
@ -39,12 +38,9 @@ import {
UPDATE_STATUS_INDICATOR,
} from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { ThreadsRoomNotificationState } from "../../../stores/notifications/ThreadsRoomNotificationState";
import { SummarizedNotificationState } from "../../../stores/notifications/SummarizedNotificationState";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import PosthogTrackers from "../../../PosthogTrackers";
import { ButtonEvent } from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
const ROOM_INFO_PHASES = [
@ -133,74 +129,48 @@ interface IProps {
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
private threadNotificationState: ThreadsRoomNotificationState | null;
private globalNotificationState: SummarizedNotificationState;
private get supportsThreadNotifications(): boolean {
const client = MatrixClientPeg.get();
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
}
public constructor(props: IProps) {
super(props, HeaderKind.Room);
this.threadNotificationState =
!this.supportsThreadNotifications && this.props.room
? RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room)
: null;
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
}
public componentDidMount(): void {
super.componentDidMount();
if (!this.supportsThreadNotifications) {
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
} else {
// Notification badge may change if the notification counts from the
// server change, if a new thread is created or updated, or if a
// receipt is sent in the thread.
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
}
// Notification badge may change if the notification counts from the
// server change, if a new thread is created or updated, or if a
// receipt is sent in the thread.
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
this.onNotificationUpdate();
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
}
public componentWillUnmount(): void {
super.componentWillUnmount();
if (!this.supportsThreadNotifications) {
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
} else {
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
}
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
}
private onNotificationUpdate = (): void => {
let threadNotificationColor: NotificationColor;
if (!this.supportsThreadNotifications) {
threadNotificationColor = this.threadNotificationState?.color ?? NotificationColor.None;
} else {
threadNotificationColor = this.notificationColor;
}
// console.log
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
this.setState({
threadNotificationColor,
threadNotificationColor: this.notificationColor,
});
};

View file

@ -27,7 +27,6 @@ import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@ -62,10 +61,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { ButtonEvent } from "../elements/AccessibleButton";
import { copyPlaintext, getSelectedText } from "../../../utils/strings";
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
@ -254,7 +249,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
private isListeningForReceipts: boolean;
private tile = React.createRef<IEventTileType>();
private replyChain = React.createRef<ReplyChain>();
private threadState: ThreadNotificationState;
public readonly ref = createRef<HTMLElement>();
@ -389,10 +383,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (SettingsStore.getValue("feature_threadenabled")) {
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
if (this.thread && !this.supportsThreadNotifications) {
this.setupNotificationListener(this.thread);
}
}
client.decryptEventIfNeeded(this.props.mxEvent);
@ -403,47 +393,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.verifyEvent();
}
private get supportsThreadNotifications(): boolean {
const client = MatrixClientPeg.get();
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
}
private setupNotificationListener(thread: Thread): void {
if (!this.supportsThreadNotifications) {
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
this.threadState = notifications.getThreadRoomState(thread);
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
this.onThreadStateUpdate();
}
}
private onThreadStateUpdate = (): void => {
if (!this.supportsThreadNotifications) {
let threadNotification = null;
switch (this.threadState?.color) {
case NotificationColor.Grey:
threadNotification = NotificationCountType.Total;
break;
case NotificationColor.Red:
threadNotification = NotificationCountType.Highlight;
break;
}
this.setState({
threadNotification,
});
}
};
private updateThread = (thread: Thread): void => {
if (thread !== this.state.thread && !this.supportsThreadNotifications) {
if (this.threadState) {
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
}
this.setupNotificationListener(thread);
}
this.setState({ thread });
};
@ -473,7 +423,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (SettingsStore.getValue("feature_threadenabled")) {
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
}
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
}
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
@ -1280,9 +1229,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
"data-shape": this.context.timelineRenderingType,
"data-self": isOwnEvent,
"data-has-reply": !!replyChain,
"data-notification": !this.supportsThreadNotifications
? this.state.threadNotification
: undefined,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
"onClick": (ev: MouseEvent) => {

View file

@ -17,7 +17,6 @@ limitations under the License.
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { RoomEvent } from "matrix-js-sdk/src/models/room";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -25,11 +24,10 @@ import type { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import * as RoomNotifs from "../../RoomNotifs";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import type { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
import { NotificationState } from "./NotificationState";
export class RoomNotificationState extends NotificationState implements IDestroyable {
public constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
public constructor(public readonly room: Room) {
super();
const cli = this.room.client;
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
@ -39,9 +37,6 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
this.threadsState?.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
}
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate);
this.updateNotificationState();
@ -55,19 +50,10 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
this.room.removeListener(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate);
} else if (this.threadsState) {
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
}
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
}
private handleThreadsUpdate = (): void => {
this.updateNotificationState();
};
private handleLocalEchoUpdated = (): void => {
this.updateNotificationState();
};

View file

@ -17,7 +17,6 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { ActionPayload } from "../../dispatcher/payloads";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
@ -26,7 +25,6 @@ import { DefaultTagID, TagID } from "../room-list/models";
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
import { PosthogAnalytics } from "../../PosthogAnalytics";
@ -42,7 +40,6 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
})();
private roomMap = new Map<Room, RoomNotificationState>();
private roomThreadsMap: Map<Room, ThreadsRoomNotificationState> = new Map<Room, ThreadsRoomNotificationState>();
private listMap = new Map<TagID, ListNotificationState>();
private _globalState = new SummarizedNotificationState();
@ -87,31 +84,11 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
*/
public getRoomState(room: Room): RoomNotificationState {
if (!this.roomMap.has(room)) {
let threadState;
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
// Not very elegant, but that way we ensure that we start tracking
// threads notification at the same time at rooms.
// There are multiple entry points, and it's unclear which one gets
// called first
const threadState = new ThreadsRoomNotificationState(room);
this.roomThreadsMap.set(room, threadState);
}
this.roomMap.set(room, new RoomNotificationState(room, threadState));
this.roomMap.set(room, new RoomNotificationState(room));
}
return this.roomMap.get(room);
}
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState | null {
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
return null;
}
if (!this.roomThreadsMap.has(room)) {
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
}
return this.roomThreadsMap.get(room);
}
public static get instance(): RoomNotificationStateStore {
return RoomNotificationStateStore.internalInstance;
}

View file

@ -1,77 +0,0 @@
/*
Copyright 2021 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 } from "matrix-js-sdk/src/models/event";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { NotificationState } from "./NotificationState";
export class ThreadNotificationState extends NotificationState implements IDestroyable {
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;
public constructor(public readonly thread: Thread) {
super();
this.thread.on(ThreadEvent.NewReply, this.handleNewThreadReply);
this.thread.on(ThreadEvent.ViewThread, this.resetThreadNotification);
if (this.thread.replyToEvent) {
// Process the current tip event
this.handleNewThreadReply(this.thread, this.thread.replyToEvent);
}
}
public destroy(): void {
super.destroy();
this.thread.off(ThreadEvent.NewReply, this.handleNewThreadReply);
this.thread.off(ThreadEvent.ViewThread, this.resetThreadNotification);
}
private handleNewThreadReply = (thread: Thread, event: MatrixEvent): void => {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const isOwn = myUserId === event.getSender();
const readReceipt = this.thread.room.getReadReceiptForUserId(myUserId);
if ((!isOwn && !readReceipt) || (readReceipt && event.getTs() >= readReceipt.data.ts)) {
const actions = client.getPushActionsForEvent(event, true);
if (actions?.tweaks) {
const color = !!actions.tweaks.highlight ? NotificationColor.Red : NotificationColor.Grey;
this.updateNotificationState(color);
}
}
};
private resetThreadNotification = (): void => {
this.updateNotificationState(NotificationColor.None);
};
private updateNotificationState(color: NotificationColor): void {
const snapshot = this.snapshot();
this._color = color;
// finally, publish an update if needed
this.emitIfUpdated(snapshot);
}
}

View file

@ -1,80 +0,0 @@
/*
Copyright 2021 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 { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { IDestroyable } from "../../utils/IDestroyable";
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { ThreadNotificationState } from "./ThreadNotificationState";
import { NotificationColor } from "./NotificationColor";
export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable {
public readonly threadsState = new Map<Thread, ThreadNotificationState>();
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;
public constructor(public readonly room: Room) {
super();
for (const thread of this.room.getThreads()) {
this.onNewThread(thread);
}
this.room.on(ThreadEvent.New, this.onNewThread);
}
public destroy(): void {
super.destroy();
this.room.off(ThreadEvent.New, this.onNewThread);
for (const [, notificationState] of this.threadsState) {
notificationState.off(NotificationStateEvents.Update, this.onThreadUpdate);
}
}
public getThreadRoomState(thread: Thread): ThreadNotificationState {
if (!this.threadsState.has(thread)) {
this.threadsState.set(thread, new ThreadNotificationState(thread));
}
return this.threadsState.get(thread);
}
private onNewThread = (thread: Thread): void => {
const notificationState = new ThreadNotificationState(thread);
this.threadsState.set(thread, notificationState);
notificationState.on(NotificationStateEvents.Update, this.onThreadUpdate);
};
private onThreadUpdate = (): void => {
let color = NotificationColor.None;
for (const [, notificationState] of this.threadsState) {
if (notificationState.color === NotificationColor.Red) {
color = NotificationColor.Red;
break;
} else if (notificationState.color === NotificationColor.Grey) {
color = NotificationColor.Grey;
}
}
this.updateNotificationState(color);
};
private updateNotificationState(color: NotificationColor): void {
const snapshot = this.snapshot();
this._color = color;
// finally, publish an update if needed
this.emitIfUpdated(snapshot);
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import { render } from "@testing-library/react";
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import React from "react";
@ -173,9 +172,4 @@ describe("RoomHeaderButtons-test.tsx", function () {
room.addReceipt(receipt);
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
});
it("does not explode without a room", () => {
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
expect(() => getComponent()).not.toThrow();
});
});

View file

@ -1,60 +0,0 @@
/*
Copyright 2022 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 { PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { stubClient } from "../../test-utils";
describe("RoomNotificationStateStore", () => {
const ROOM_ID = "!roomId:example.org";
let room;
let client;
beforeEach(() => {
stubClient();
client = MatrixClientPeg.get();
room = new Room(ROOM_ID, client, client.getUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
});
it("does not use legacy thread notification store", () => {
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
});
it("use legacy thread notification store", () => {
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
});
it("does not use legacy thread notification store", () => {
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
RoomNotificationStateStore.instance.getRoomState(room);
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
});
it("use legacy thread notification store", () => {
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
RoomNotificationStateStore.instance.getRoomState(room);
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
});
});