Threads notifications after app startup (#7253)
This commit is contained in:
parent
b4b81a455e
commit
38e5e94ee4
10 changed files with 194 additions and 28 deletions
|
@ -19,7 +19,7 @@ import { TagID } from "../room-list/models";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { arrayDiff } from "../../utils/arrays";
|
||||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
|
||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||
|
||||
export type FetchRoomFn = (room: Room) => RoomNotificationState;
|
||||
|
||||
|
@ -50,11 +50,11 @@ export class ListNotificationState extends NotificationState {
|
|||
const state = this.states[oldRoom.roomId];
|
||||
if (!state) continue; // We likely just didn't have a badge (race condition)
|
||||
delete this.states[oldRoom.roomId];
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = this.getRoomFn(newRoom);
|
||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
this.states[newRoom.roomId] = state;
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ export class ListNotificationState extends NotificationState {
|
|||
public destroy() {
|
||||
super.destroy();
|
||||
for (const state of Object.values(this.states)) {
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
this.states = {};
|
||||
}
|
||||
|
|
|
@ -14,14 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { NotificationColor } from "./NotificationColor";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||
|
||||
export const NOTIFICATION_STATE_UPDATE = "update";
|
||||
export interface INotificationStateSnapshotParams {
|
||||
symbol: string | null;
|
||||
count: number;
|
||||
color: NotificationColor;
|
||||
}
|
||||
|
||||
export abstract class NotificationState extends EventEmitter implements IDestroyable {
|
||||
protected _symbol: string;
|
||||
export enum NotificationStateEvents {
|
||||
Update = "update",
|
||||
}
|
||||
|
||||
export abstract class NotificationState extends TypedEventEmitter<NotificationStateEvents>
|
||||
implements INotificationStateSnapshotParams, IDestroyable {
|
||||
protected _symbol: string | null;
|
||||
protected _count: number;
|
||||
protected _color: NotificationColor;
|
||||
|
||||
|
@ -55,7 +64,7 @@ export abstract class NotificationState extends EventEmitter implements IDestroy
|
|||
|
||||
protected emitIfUpdated(snapshot: NotificationStateSnapshot) {
|
||||
if (snapshot.isDifferentFrom(this)) {
|
||||
this.emit(NOTIFICATION_STATE_UPDATE);
|
||||
this.emit(NotificationStateEvents.Update);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +73,7 @@ export abstract class NotificationState extends EventEmitter implements IDestroy
|
|||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.removeAllListeners(NOTIFICATION_STATE_UPDATE);
|
||||
this.removeAllListeners(NotificationStateEvents.Update);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,13 +82,13 @@ export class NotificationStateSnapshot {
|
|||
private readonly count: number;
|
||||
private readonly color: NotificationColor;
|
||||
|
||||
constructor(state: NotificationState) {
|
||||
constructor(state: INotificationStateSnapshotParams) {
|
||||
this.symbol = state.symbol;
|
||||
this.count = state.count;
|
||||
this.color = state.color;
|
||||
}
|
||||
|
||||
public isDifferentFrom(other: NotificationState): boolean {
|
||||
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 };
|
||||
return JSON.stringify(before) !== JSON.stringify(after);
|
||||
|
|
|
@ -23,6 +23,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
||||
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
|
||||
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
||||
|
||||
interface IState {}
|
||||
|
||||
|
@ -30,6 +31,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
private static internalInstance = new RoomNotificationStateStore();
|
||||
|
||||
private roomMap = new Map<Room, RoomNotificationState>();
|
||||
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
||||
private listMap = new Map<TagID, ListNotificationState>();
|
||||
|
||||
private constructor() {
|
||||
|
@ -85,10 +87,22 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
public getRoomState(room: Room): RoomNotificationState {
|
||||
if (!this.roomMap.has(room)) {
|
||||
this.roomMap.set(room, new RoomNotificationState(room));
|
||||
// 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
|
||||
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
|
||||
}
|
||||
return this.roomMap.get(room);
|
||||
}
|
||||
|
||||
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { NotificationColor } from "./NotificationColor";
|
||||
import { arrayDiff } from "../../utils/arrays";
|
||||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
|
||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||
import { FetchRoomFn } from "./ListNotificationState";
|
||||
|
||||
export class SpaceNotificationState extends NotificationState {
|
||||
|
@ -42,11 +42,11 @@ export class SpaceNotificationState extends NotificationState {
|
|||
const state = this.states[oldRoom.roomId];
|
||||
if (!state) continue; // We likely just didn't have a badge (race condition)
|
||||
delete this.states[oldRoom.roomId];
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
for (const newRoom of diff.added) {
|
||||
const state = this.getRoomFn(newRoom);
|
||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
this.states[newRoom.roomId] = state;
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ export class SpaceNotificationState extends NotificationState {
|
|||
public destroy() {
|
||||
super.destroy();
|
||||
for (const state of Object.values(this.states)) {
|
||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||
state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
|
||||
}
|
||||
this.states = {};
|
||||
}
|
||||
|
|
69
src/stores/notifications/ThreadNotificationState.ts
Normal file
69
src/stores/notifications/ThreadNotificationState.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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 { NotificationColor } from "./NotificationColor";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { NotificationState } from "./NotificationState";
|
||||
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
export class ThreadNotificationState extends NotificationState implements IDestroyable {
|
||||
protected _symbol = null;
|
||||
protected _count = 0;
|
||||
protected _color = NotificationColor.None;
|
||||
|
||||
constructor(public readonly room: Room, public readonly thread: Thread) {
|
||||
super();
|
||||
this.thread.on(ThreadEvent.NewReply, this.handleNewThreadReply);
|
||||
this.thread.on(ThreadEvent.ViewThread, this.resetThreadNotification);
|
||||
}
|
||||
|
||||
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 isOwn = client.getUserId() === event.getSender();
|
||||
if (!isOwn) {
|
||||
const actions = client.getPushActionsForEvent(event, true);
|
||||
|
||||
const color = !!actions.tweaks.highlight
|
||||
? NotificationColor.Red
|
||||
: NotificationColor.Grey;
|
||||
|
||||
this.updateNotificationState(color);
|
||||
}
|
||||
}
|
||||
|
||||
private resetThreadNotification = (): void => {
|
||||
this.updateNotificationState(NotificationColor.None);
|
||||
};
|
||||
|
||||
private updateNotificationState(color: NotificationColor) {
|
||||
const snapshot = this.snapshot();
|
||||
|
||||
this._color = color;
|
||||
|
||||
// finally, publish an update if needed
|
||||
this.emitIfUpdated(snapshot);
|
||||
}
|
||||
}
|
72
src/stores/notifications/ThreadsRoomNotificationState.ts
Normal file
72
src/stores/notifications/ThreadsRoomNotificationState.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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 { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
||||
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||
import { ThreadNotificationState } from "./ThreadNotificationState";
|
||||
import { NotificationColor } from "./NotificationColor";
|
||||
|
||||
export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable {
|
||||
private threadsState = new Map<Thread, ThreadNotificationState>();
|
||||
|
||||
protected _symbol = null;
|
||||
protected _count = 0;
|
||||
protected _color = NotificationColor.None;
|
||||
|
||||
constructor(public readonly room: Room) {
|
||||
super();
|
||||
this.room.on(ThreadEvent.New, this.onNewThread);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
this.room.on(ThreadEvent.New, this.onNewThread);
|
||||
for (const [, notificationState] of this.threadsState) {
|
||||
notificationState.off(NotificationStateEvents.Update, this.onThreadUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private onNewThread = (thread: Thread): void => {
|
||||
const notificationState = new ThreadNotificationState(this.room, 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue