Threads notifications after app startup (#7253)

This commit is contained in:
Germain 2021-12-07 12:51:34 +00:00 committed by GitHub
parent b4b81a455e
commit 38e5e94ee4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 194 additions and 28 deletions

View file

@ -72,6 +72,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
super(props);
this.state = {};
}
public componentDidMount(): void {
this.setupThread(this.props.mxEvent);
this.dispatcherRef = dis.register(this.onAction);
@ -166,10 +167,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
};
private updateThread = (thread?: Thread) => {
if (thread) {
if (thread && this.state.thread !== thread) {
this.setState({
thread,
});
thread.emit(ThreadEvent.ViewThread);
}
this.timelinePanelRef.current?.refreshTimeline();

View file

@ -20,7 +20,7 @@ import { formatCount } from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import { _t } from "../../../languageHandler";
@ -60,7 +60,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
constructor(props: IProps) {
super(props);
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {
showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
@ -80,15 +80,15 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.countWatcherRef);
this.props.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
if (prevProps.notification) {
prevProps.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
prevProps.notification.off(NotificationStateEvents.Update, this.onNotificationUpdate);
}
this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate);
}
private countPreferenceChanged = () => {

View file

@ -38,8 +38,8 @@ import { ContextMenuTooltipButton } from '../../structures/ContextMenu';
import RoomContextMenu from "../context_menus/RoomContextMenu";
import { contextMenuBelow } from './RoomTile';
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { NOTIFICATION_STATE_UPDATE } from '../../../stores/notifications/NotificationState';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
export interface ISearchInfo {
searchTerm: string;
@ -76,7 +76,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
constructor(props, context) {
super(props, context);
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
notiStore.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {};
}
@ -91,7 +91,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
}
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
notiStore.removeListener(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
}
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {

View file

@ -37,7 +37,7 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import { ActionPayload } from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
@ -164,7 +164,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.props.room?.on("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.on(
@ -188,7 +188,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),

View file

@ -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 = {};
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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 = {};
}

View 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);
}
}

View 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);
}
}