mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Threads notifications after app startup (#7253)
This commit is contained in:
parent
b4b81a455e
commit
38e5e94ee4
10 changed files with 194 additions and 28 deletions
|
@ -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();
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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…
Reference in a new issue