Cleanup pre MSC3773 thread unread notif logic (#10023)
This commit is contained in:
parent
eaf152ceef
commit
703587b8e9
8 changed files with 23 additions and 367 deletions
|
@ -22,7 +22,6 @@ import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import HeaderButton from "./HeaderButton";
|
import HeaderButton from "./HeaderButton";
|
||||||
|
@ -39,12 +38,9 @@ import {
|
||||||
UPDATE_STATUS_INDICATOR,
|
UPDATE_STATUS_INDICATOR,
|
||||||
} from "../../../stores/notifications/RoomNotificationStateStore";
|
} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
import { ThreadsRoomNotificationState } from "../../../stores/notifications/ThreadsRoomNotificationState";
|
|
||||||
import { SummarizedNotificationState } from "../../../stores/notifications/SummarizedNotificationState";
|
import { SummarizedNotificationState } from "../../../stores/notifications/SummarizedNotificationState";
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
|
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
|
@ -133,74 +129,48 @@ interface IProps {
|
||||||
|
|
||||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
|
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
|
||||||
private threadNotificationState: ThreadsRoomNotificationState | null;
|
|
||||||
private globalNotificationState: SummarizedNotificationState;
|
private globalNotificationState: SummarizedNotificationState;
|
||||||
|
|
||||||
private get supportsThreadNotifications(): boolean {
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props, HeaderKind.Room);
|
super(props, HeaderKind.Room);
|
||||||
|
|
||||||
this.threadNotificationState =
|
|
||||||
!this.supportsThreadNotifications && this.props.room
|
|
||||||
? RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room)
|
|
||||||
: null;
|
|
||||||
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
super.componentDidMount();
|
super.componentDidMount();
|
||||||
if (!this.supportsThreadNotifications) {
|
// Notification badge may change if the notification counts from the
|
||||||
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
// server change, if a new thread is created or updated, or if a
|
||||||
} else {
|
// receipt is sent in the thread.
|
||||||
// Notification badge may change if the notification counts from the
|
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
// server change, if a new thread is created or updated, or if a
|
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||||
// receipt is sent in the thread.
|
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
|
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
|
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
|
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
|
||||||
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
|
|
||||||
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
|
|
||||||
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
|
|
||||||
}
|
|
||||||
this.onNotificationUpdate();
|
this.onNotificationUpdate();
|
||||||
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
super.componentWillUnmount();
|
super.componentWillUnmount();
|
||||||
if (!this.supportsThreadNotifications) {
|
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||||
} else {
|
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
|
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
|
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
|
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
|
||||||
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
|
|
||||||
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
|
|
||||||
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
|
|
||||||
}
|
|
||||||
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNotificationUpdate = (): void => {
|
private onNotificationUpdate = (): void => {
|
||||||
let threadNotificationColor: NotificationColor;
|
|
||||||
if (!this.supportsThreadNotifications) {
|
|
||||||
threadNotificationColor = this.threadNotificationState?.color ?? NotificationColor.None;
|
|
||||||
} else {
|
|
||||||
threadNotificationColor = this.notificationColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log
|
// console.log
|
||||||
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
|
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
|
||||||
this.setState({
|
this.setState({
|
||||||
threadNotificationColor,
|
threadNotificationColor: this.notificationColor,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models
|
||||||
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
|
|
||||||
import ReplyChain from "../elements/ReplyChain";
|
import ReplyChain from "../elements/ReplyChain";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -62,10 +61,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState";
|
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { copyPlaintext, getSelectedText } from "../../../utils/strings";
|
import { copyPlaintext, getSelectedText } from "../../../utils/strings";
|
||||||
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||||
|
@ -254,7 +249,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
private tile = React.createRef<IEventTileType>();
|
private tile = React.createRef<IEventTileType>();
|
||||||
private replyChain = React.createRef<ReplyChain>();
|
private replyChain = React.createRef<ReplyChain>();
|
||||||
private threadState: ThreadNotificationState;
|
|
||||||
|
|
||||||
public readonly ref = createRef<HTMLElement>();
|
public readonly ref = createRef<HTMLElement>();
|
||||||
|
|
||||||
|
@ -389,10 +383,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_threadenabled")) {
|
if (SettingsStore.getValue("feature_threadenabled")) {
|
||||||
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||||
|
|
||||||
if (this.thread && !this.supportsThreadNotifications) {
|
|
||||||
this.setupNotificationListener(this.thread);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.decryptEventIfNeeded(this.props.mxEvent);
|
client.decryptEventIfNeeded(this.props.mxEvent);
|
||||||
|
@ -403,47 +393,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
this.verifyEvent();
|
this.verifyEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get supportsThreadNotifications(): boolean {
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupNotificationListener(thread: Thread): void {
|
|
||||||
if (!this.supportsThreadNotifications) {
|
|
||||||
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
|
|
||||||
this.threadState = notifications.getThreadRoomState(thread);
|
|
||||||
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
|
||||||
this.onThreadStateUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onThreadStateUpdate = (): void => {
|
|
||||||
if (!this.supportsThreadNotifications) {
|
|
||||||
let threadNotification = null;
|
|
||||||
switch (this.threadState?.color) {
|
|
||||||
case NotificationColor.Grey:
|
|
||||||
threadNotification = NotificationCountType.Total;
|
|
||||||
break;
|
|
||||||
case NotificationColor.Red:
|
|
||||||
threadNotification = NotificationCountType.Highlight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
threadNotification,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateThread = (thread: Thread): void => {
|
private updateThread = (thread: Thread): void => {
|
||||||
if (thread !== this.state.thread && !this.supportsThreadNotifications) {
|
|
||||||
if (this.threadState) {
|
|
||||||
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setupNotificationListener(thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ thread });
|
this.setState({ thread });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -473,7 +423,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
if (SettingsStore.getValue("feature_threadenabled")) {
|
if (SettingsStore.getValue("feature_threadenabled")) {
|
||||||
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
|
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
|
||||||
}
|
}
|
||||||
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
|
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
|
||||||
|
@ -1280,9 +1229,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
"data-shape": this.context.timelineRenderingType,
|
"data-shape": this.context.timelineRenderingType,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"data-notification": !this.supportsThreadNotifications
|
|
||||||
? this.state.threadNotification
|
|
||||||
: undefined,
|
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
"onClick": (ev: MouseEvent) => {
|
"onClick": (ev: MouseEvent) => {
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
@ -25,11 +24,10 @@ import type { IDestroyable } from "../../utils/IDestroyable";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import * as RoomNotifs from "../../RoomNotifs";
|
import * as RoomNotifs from "../../RoomNotifs";
|
||||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
import { NotificationState } from "./NotificationState";
|
||||||
import type { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
public constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
public constructor(public readonly room: Room) {
|
||||||
super();
|
super();
|
||||||
const cli = this.room.client;
|
const cli = this.room.client;
|
||||||
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
||||||
|
@ -39,9 +37,6 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
|
|
||||||
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
||||||
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
|
||||||
this.threadsState?.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
|
||||||
}
|
|
||||||
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
|
@ -55,19 +50,10 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||||
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate);
|
||||||
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate);
|
||||||
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
|
||||||
this.room.removeListener(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate);
|
|
||||||
} else if (this.threadsState) {
|
|
||||||
this.threadsState.removeListener(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
|
||||||
}
|
|
||||||
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||||
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleThreadsUpdate = (): void => {
|
|
||||||
this.updateNotificationState();
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleLocalEchoUpdated = (): void => {
|
private handleLocalEchoUpdated = (): void => {
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
|
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
@ -26,7 +25,6 @@ import { DefaultTagID, TagID } from "../room-list/models";
|
||||||
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
||||||
import { RoomNotificationState } from "./RoomNotificationState";
|
import { RoomNotificationState } from "./RoomNotificationState";
|
||||||
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
||||||
import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
|
||||||
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
|
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
|
||||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||||
|
|
||||||
|
@ -42,7 +40,6 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
})();
|
})();
|
||||||
private roomMap = new Map<Room, RoomNotificationState>();
|
private roomMap = new Map<Room, RoomNotificationState>();
|
||||||
|
|
||||||
private roomThreadsMap: Map<Room, ThreadsRoomNotificationState> = new Map<Room, ThreadsRoomNotificationState>();
|
|
||||||
private listMap = new Map<TagID, ListNotificationState>();
|
private listMap = new Map<TagID, ListNotificationState>();
|
||||||
private _globalState = new SummarizedNotificationState();
|
private _globalState = new SummarizedNotificationState();
|
||||||
|
|
||||||
|
@ -87,31 +84,11 @@ 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)) {
|
||||||
let threadState;
|
this.roomMap.set(room, new RoomNotificationState(room));
|
||||||
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
|
||||||
// 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
|
|
||||||
const threadState = new ThreadsRoomNotificationState(room);
|
|
||||||
this.roomThreadsMap.set(room, threadState);
|
|
||||||
}
|
|
||||||
this.roomMap.set(room, new RoomNotificationState(room, threadState));
|
|
||||||
}
|
}
|
||||||
return this.roomMap.get(room);
|
return this.roomMap.get(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getThreadsRoomState(room: Room): ThreadsRoomNotificationState | null {
|
|
||||||
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
|
||||||
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
||||||
import { NotificationState } from "./NotificationState";
|
|
||||||
|
|
||||||
export class ThreadNotificationState extends NotificationState implements IDestroyable {
|
|
||||||
protected _symbol = null;
|
|
||||||
protected _count = 0;
|
|
||||||
protected _color = NotificationColor.None;
|
|
||||||
|
|
||||||
public 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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): void => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private resetThreadNotification = (): void => {
|
|
||||||
this.updateNotificationState(NotificationColor.None);
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateNotificationState(color: NotificationColor): void {
|
|
||||||
const snapshot = this.snapshot();
|
|
||||||
|
|
||||||
this._color = color;
|
|
||||||
|
|
||||||
// finally, publish an update if needed
|
|
||||||
this.emitIfUpdated(snapshot);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
|
||||||
|
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { NotificationState, NotificationStateEvents } from "./NotificationState";
|
|
||||||
import { ThreadNotificationState } from "./ThreadNotificationState";
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
|
||||||
|
|
||||||
export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable {
|
|
||||||
public readonly threadsState = new Map<Thread, ThreadNotificationState>();
|
|
||||||
|
|
||||||
protected _symbol = null;
|
|
||||||
protected _count = 0;
|
|
||||||
protected _color = NotificationColor.None;
|
|
||||||
|
|
||||||
public constructor(public readonly room: Room) {
|
|
||||||
super();
|
|
||||||
for (const thread of this.room.getThreads()) {
|
|
||||||
this.onNewThread(thread);
|
|
||||||
}
|
|
||||||
this.room.on(ThreadEvent.New, this.onNewThread);
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
super.destroy();
|
|
||||||
this.room.off(ThreadEvent.New, this.onNewThread);
|
|
||||||
for (const [, notificationState] of this.threadsState) {
|
|
||||||
notificationState.off(NotificationStateEvents.Update, this.onThreadUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getThreadRoomState(thread: Thread): ThreadNotificationState {
|
|
||||||
if (!this.threadsState.has(thread)) {
|
|
||||||
this.threadsState.set(thread, new ThreadNotificationState(thread));
|
|
||||||
}
|
|
||||||
return this.threadsState.get(thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onNewThread = (thread: Thread): void => {
|
|
||||||
const notificationState = new ThreadNotificationState(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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -173,9 +172,4 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
room.addReceipt(receipt);
|
room.addReceipt(receipt);
|
||||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not explode without a room", () => {
|
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
|
||||||
expect(() => getComponent()).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
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 { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
|
||||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
|
||||||
import { stubClient } from "../../test-utils";
|
|
||||||
|
|
||||||
describe("RoomNotificationStateStore", () => {
|
|
||||||
const ROOM_ID = "!roomId:example.org";
|
|
||||||
|
|
||||||
let room;
|
|
||||||
let client;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
stubClient();
|
|
||||||
client = MatrixClientPeg.get();
|
|
||||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not use legacy thread notification store", () => {
|
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
|
|
||||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("use legacy thread notification store", () => {
|
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
|
||||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not use legacy thread notification store", () => {
|
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
|
|
||||||
RoomNotificationStateStore.instance.getRoomState(room);
|
|
||||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("use legacy thread notification store", () => {
|
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
|
||||||
RoomNotificationStateStore.instance.getRoomState(room);
|
|
||||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue