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

Refactor notification state to prepare for threads work #9335

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 21 additions & 76 deletions src/stores/notifications/RoomNotificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { ClientEvent } from "matrix-js-sdk/src/client";

import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import * as RoomNotifs from '../../RoomNotifs';
import * as Unread from '../../Unread';
import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";

export class RoomNotificationState extends NotificationState implements IDestroyable {
constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
super();
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate);
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
import { TimelineNotificationState } from "./TimelineNotificationState";

export class RoomNotificationState extends TimelineNotificationState {
constructor(public readonly room: Room) {
super(room);
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate);
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
if (threadsState) {
threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
}
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate);
this.updateNotificationState();
}

Expand All @@ -51,57 +37,31 @@ export class RoomNotificationState extends NotificationState implements IDestroy

public destroy(): void {
super.destroy();
this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt);
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
if (this.threadsState) {
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
}
}

private handleThreadsUpdate = () => {
this.updateNotificationState();
};

private handleLocalEchoUpdated = () => {
this.updateNotificationState();
};

private handleReadReceipt = (event: MatrixEvent, room: Room) => {
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
if (room.roomId !== this.room.roomId) return; // not for us - ignore
this.updateNotificationState();
protected handleReadReceipt = (event: MatrixEvent): void => {
if (event.getRoomId() !== this.room.roomId) return; // not for us - ignore
super.handleReadReceipt(event);
};

private handleMembershipUpdate = () => {
this.updateNotificationState();
private handleMembershipUpdate = (): void => {
return this.updateNotificationState();
};

private onEventDecrypted = (event: MatrixEvent) => {
protected onEventDecrypted = (event: MatrixEvent): void => {
if (event.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline
if (event.threadRootId && !event.isThreadRoot) return; // ignore all threaded events

this.updateNotificationState();
};

private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => {
if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline

this.updateNotificationState();
super.onEventDecrypted(event);
};

private handleAccountDataUpdate = (ev: MatrixEvent) => {
if (ev.getType() === "m.push_rules") {
this.updateNotificationState();
}
protected handleRoomEventUpdate = (event: MatrixEvent): void => {
if (event.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline
super.handleRoomEventUpdate(event);
};

private updateNotificationState() {
protected updateNotificationState(): void {
const snapshot = this.snapshot();

if (getUnsentMessages(this.room).length > 0) {
Expand All @@ -119,27 +79,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._symbol = "!";
this._count = 1; // not used, technically
} else {
super.updateNotificationState();

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

// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red
// count. If that count is broken for some reason, assume zero. This avoids us showing
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);

// Note: we only set the symbol if we have an actual count. We don't want to show
// zero on badges.

if (redNotifs > 0) {
this._color = NotificationColor.Red;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else if (greyNotifs > 0) {
this._color = NotificationColor.Grey;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else {
if (redNotifs === 0 && greyNotifs === 0) {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);
Expand Down
12 changes: 10 additions & 2 deletions src/stores/notifications/RoomNotificationStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,17 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
private listMap = new Map<TagID, ListNotificationState>();
private _globalState = new SummarizedNotificationState();
private ThreadNotificationStateType;

private constructor() {
super(defaultDispatcher, {});
this.matrixClient.serverSupportsPrefix("org.matrix.msc3773").then(({ serverSupport }) => {
if (serverSupport) {
this.ThreadNotificationStateType = ThreadsRoomNotificationState;
} else {
this.ThreadNotificationStateType = ThreadsRoomNotificationState;
}
});
}

/**
Expand Down Expand Up @@ -90,7 +98,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
// 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);
const threadState = new this.ThreadNotificationStateType(room);
this.roomThreadsMap.set(room, threadState);
this.roomMap.set(room, new RoomNotificationState(room, threadState));
}
Expand All @@ -99,7 +107,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {

public getThreadsRoomState(room: Room): ThreadsRoomNotificationState {
if (!this.roomThreadsMap.has(room)) {
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
this.roomThreadsMap.set(room, new this.ThreadNotificationStateType(room));
}
return this.roomThreadsMap.get(room);
}
Expand Down
78 changes: 78 additions & 0 deletions src/stores/notifications/ThreadBetaNotificationState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";

import { NotificationColor } from "./NotificationColor";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { ThreadNotificationState } from "./ThreadNotificationState";

export class ThreadBetaNotificationState extends ThreadNotificationState {
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;

constructor(public readonly thread: Thread) {
super(thread);
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);
}

protected handleNewThreadReply = (thread: Thread, event: MatrixEvent) => {
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);
}
}
};

protected resetThreadNotification = (): void => {
this.updateNotificationState(NotificationColor.None);
};

protected updateNotificationState(color: NotificationColor) {
const snapshot = this.snapshot();

this._color = color;

// finally, publish an update if needed
this.emitIfUpdated(snapshot);
}
}
59 changes: 18 additions & 41 deletions src/stores/notifications/ThreadNotificationState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 - 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.
Expand All @@ -15,63 +15,40 @@ limitations under the License.
*/

import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
import { Room } from "matrix-js-sdk/src/models/room";

import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { NotificationState } from "./NotificationState";
import { TimelineNotificationState } from "./TimelineNotificationState";

export class ThreadNotificationState extends NotificationState implements IDestroyable {
export class ThreadNotificationState extends TimelineNotificationState {
protected _symbol = null;
protected _count = 0;
protected _color = NotificationColor.None;

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);
}
constructor(public readonly room: Room, public readonly threadId: string) {
super(room);
}

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) => {
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);
protected handleReadReceipt = (event: MatrixEvent): void => {
if (event.threadRootId !== this.threadId) return; // not for us - ignore
super.handleReadReceipt(event);
};

if (actions?.tweaks) {
const color = !!actions.tweaks.highlight
? NotificationColor.Red
: NotificationColor.Grey;
protected onEventDecrypted = (event: MatrixEvent): void => {
if (event.threadRootId !== this.threadId) return; // ignore - not for us or notifications timeline

this.updateNotificationState(color);
}
}
super.onEventDecrypted(event);
};

private resetThreadNotification = (): void => {
this.updateNotificationState(NotificationColor.None);
protected handleRoomEventUpdate = (event: MatrixEvent): void => {
if (event.threadRootId !== this.threadId) return; // ignore - not for us or notifications timeline
super.handleRoomEventUpdate(event);
};

private updateNotificationState(color: NotificationColor) {
protected updateNotificationState(): void {
const snapshot = this.snapshot();

this._color = color;
super.updateNotificationState();

// finally, publish an update if needed
this.emitIfUpdated(snapshot);
Expand Down
Loading