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

View file

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

View file

@ -38,8 +38,8 @@ import { ContextMenuTooltipButton } from '../../structures/ContextMenu';
import RoomContextMenu from "../context_menus/RoomContextMenu"; import RoomContextMenu from "../context_menus/RoomContextMenu";
import { contextMenuBelow } from './RoomTile'; import { contextMenuBelow } from './RoomTile';
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore'; import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { NOTIFICATION_STATE_UPDATE } from '../../../stores/notifications/NotificationState';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
export interface ISearchInfo { export interface ISearchInfo {
searchTerm: string; searchTerm: string;
@ -76,7 +76,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room); const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
notiStore.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {}; this.state = {};
} }
@ -91,7 +91,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
cli.removeListener("RoomState.events", this.onRoomStateEvents); cli.removeListener("RoomState.events", this.onRoomStateEvents);
} }
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room); 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) => { 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 RoomListActions from "../../../actions/RoomListActions";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; 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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber"; 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), MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged, 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.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.props.room?.on("Room.name", this.onRoomNameUpdate); this.props.room?.on("Room.name", this.onRoomNameUpdate);
CommunityPrototypeStore.instance.on( CommunityPrototypeStore.instance.on(
@ -188,7 +188,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
} }
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef); 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); this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.off( CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), 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 { Room } from "matrix-js-sdk/src/models/room";
import { arrayDiff } from "../../utils/arrays"; import { arrayDiff } from "../../utils/arrays";
import { RoomNotificationState } from "./RoomNotificationState"; import { RoomNotificationState } from "./RoomNotificationState";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState"; import { NotificationState, NotificationStateEvents } from "./NotificationState";
export type FetchRoomFn = (room: Room) => RoomNotificationState; export type FetchRoomFn = (room: Room) => RoomNotificationState;
@ -50,11 +50,11 @@ export class ListNotificationState extends NotificationState {
const state = this.states[oldRoom.roomId]; const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition) if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId]; delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
} }
for (const newRoom of diff.added) { for (const newRoom of diff.added) {
const state = this.getRoomFn(newRoom); const state = this.getRoomFn(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
this.states[newRoom.roomId] = state; this.states[newRoom.roomId] = state;
} }
@ -70,7 +70,7 @@ export class ListNotificationState extends NotificationState {
public destroy() { public destroy() {
super.destroy(); super.destroy();
for (const state of Object.values(this.states)) { for (const state of Object.values(this.states)) {
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
} }
this.states = {}; this.states = {};
} }

View file

@ -14,14 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventEmitter } from "events";
import { NotificationColor } from "./NotificationColor"; import { NotificationColor } from "./NotificationColor";
import { IDestroyable } from "../../utils/IDestroyable"; 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 { export enum NotificationStateEvents {
protected _symbol: string; Update = "update",
}
export abstract class NotificationState extends TypedEventEmitter<NotificationStateEvents>
implements INotificationStateSnapshotParams, IDestroyable {
protected _symbol: string | null;
protected _count: number; protected _count: number;
protected _color: NotificationColor; protected _color: NotificationColor;
@ -55,7 +64,7 @@ export abstract class NotificationState extends EventEmitter implements IDestroy
protected emitIfUpdated(snapshot: NotificationStateSnapshot) { protected emitIfUpdated(snapshot: NotificationStateSnapshot) {
if (snapshot.isDifferentFrom(this)) { 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 { 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 count: number;
private readonly color: NotificationColor; private readonly color: NotificationColor;
constructor(state: NotificationState) { constructor(state: INotificationStateSnapshotParams) {
this.symbol = state.symbol; this.symbol = state.symbol;
this.count = state.count; this.count = state.count;
this.color = state.color; 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 before = { count: this.count, symbol: this.symbol, color: this.color };
const after = { count: other.count, symbol: other.symbol, color: other.color }; const after = { count: other.count, symbol: other.symbol, color: other.color };
return JSON.stringify(before) !== JSON.stringify(after); 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 { RoomNotificationState } from "./RoomNotificationState";
import { SummarizedNotificationState } from "./SummarizedNotificationState"; import { SummarizedNotificationState } from "./SummarizedNotificationState";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
interface IState {} interface IState {}
@ -30,6 +31,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new RoomNotificationStateStore(); private static internalInstance = new RoomNotificationStateStore();
private roomMap = new Map<Room, RoomNotificationState>(); private roomMap = new Map<Room, RoomNotificationState>();
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
private listMap = new Map<TagID, ListNotificationState>(); private listMap = new Map<TagID, ListNotificationState>();
private constructor() { private constructor() {
@ -85,10 +87,22 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
public getRoomState(room: Room): RoomNotificationState { public getRoomState(room: Room): RoomNotificationState {
if (!this.roomMap.has(room)) { if (!this.roomMap.has(room)) {
this.roomMap.set(room, new RoomNotificationState(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); 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 { public static get instance(): RoomNotificationStateStore {
return RoomNotificationStateStore.internalInstance; return RoomNotificationStateStore.internalInstance;
} }

View file

@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { NotificationColor } from "./NotificationColor"; import { NotificationColor } from "./NotificationColor";
import { arrayDiff } from "../../utils/arrays"; import { arrayDiff } from "../../utils/arrays";
import { RoomNotificationState } from "./RoomNotificationState"; import { RoomNotificationState } from "./RoomNotificationState";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState"; import { NotificationState, NotificationStateEvents } from "./NotificationState";
import { FetchRoomFn } from "./ListNotificationState"; import { FetchRoomFn } from "./ListNotificationState";
export class SpaceNotificationState extends NotificationState { export class SpaceNotificationState extends NotificationState {
@ -42,11 +42,11 @@ export class SpaceNotificationState extends NotificationState {
const state = this.states[oldRoom.roomId]; const state = this.states[oldRoom.roomId];
if (!state) continue; // We likely just didn't have a badge (race condition) if (!state) continue; // We likely just didn't have a badge (race condition)
delete this.states[oldRoom.roomId]; delete this.states[oldRoom.roomId];
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
} }
for (const newRoom of diff.added) { for (const newRoom of diff.added) {
const state = this.getRoomFn(newRoom); const state = this.getRoomFn(newRoom);
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.on(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
this.states[newRoom.roomId] = state; this.states[newRoom.roomId] = state;
} }
@ -60,7 +60,7 @@ export class SpaceNotificationState extends NotificationState {
public destroy() { public destroy() {
super.destroy(); super.destroy();
for (const state of Object.values(this.states)) { for (const state of Object.values(this.states)) {
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.off(NotificationStateEvents.Update, this.onRoomNotificationStateUpdate);
} }
this.states = {}; 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);
}
}