From da1d29c6a20cb988fc83e071e3892f59cbcfed83 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 18 Aug 2022 13:16:19 +0100 Subject: [PATCH 1/5] MSC3773 POC --- .../notifications/ThreadNotificationState.ts | 113 +++++++++++----- .../ThreadsRoomNotificationState.ts | 122 ++++++++++++------ 2 files changed, 160 insertions(+), 75 deletions(-) diff --git a/src/stores/notifications/ThreadNotificationState.ts b/src/stores/notifications/ThreadNotificationState.ts index 2b2bcf175ce..5572ad5ffb3 100644 --- a/src/stores/notifications/ThreadNotificationState.ts +++ b/src/stores/notifications/ThreadNotificationState.ts @@ -14,64 +14,111 @@ 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 { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; import { NotificationColor } from "./NotificationColor"; import { IDestroyable } from "../../utils/IDestroyable"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import { NotificationState } from "./NotificationState"; +import { readReceiptChangeIsFor } from "../../utils/read-receipts"; +import * as RoomNotifs from '../../RoomNotifs'; export class ThreadNotificationState extends NotificationState implements IDestroyable { - protected _symbol = null; - protected _count = 0; - protected _color = NotificationColor.None; - - constructor(public readonly thread: Thread) { + constructor(public readonly room: Room, public readonly threadId: string) { 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); - } + this.room.on(RoomEvent.Receipt, this.handleReadReceipt); + this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate); + this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); + this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); + MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); + this.updateNotificationState(); } public destroy(): void { super.destroy(); - this.thread.off(ThreadEvent.NewReply, this.handleNewThreadReply); - this.thread.off(ThreadEvent.ViewThread, this.resetThreadNotification); + this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt); + this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate); + this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate); + this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); + } } + private handleLocalEchoUpdated = () => { + this.updateNotificationState(); + }; - private handleNewThreadReply = (thread: Thread, event: MatrixEvent) => { - const client = MatrixClientPeg.get(); + private handleReadReceipt = (event: MatrixEvent, room: Room) => { + if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore + if (room.roomId !== this.room.roomId) return; // not for us - ignore + this.updateNotificationState(); + }; - const myUserId = client.getUserId(); + private handleMembershipUpdate = () => { + this.updateNotificationState(); + }; - const isOwn = myUserId === event.getSender(); - const readReceipt = this.thread.room.getReadReceiptForUserId(myUserId); + private onEventDecrypted = (event: MatrixEvent) => { + if (event.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline - if (!isOwn && !readReceipt || (readReceipt && event.getTs() >= readReceipt.data.ts)) { - const actions = client.getPushActionsForEvent(event, true); + this.updateNotificationState(); + }; - if (actions?.tweaks) { - const color = !!actions.tweaks.highlight - ? NotificationColor.Red - : NotificationColor.Grey; + private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => { + if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline - this.updateNotificationState(color); - } - } + this.updateNotificationState(); }; - private resetThreadNotification = (): void => { - this.updateNotificationState(NotificationColor.None); + private handleAccountDataUpdate = (ev: MatrixEvent) => { + if (ev.getType() === "m.push_rules") { + this.updateNotificationState(); + } }; - private updateNotificationState(color: NotificationColor) { + private updateNotificationState() { const snapshot = this.snapshot(); - this._color = color; + if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute) { + // When muted we suppress all notification states, even if we have context on them. + this._color = NotificationColor.None; + this._symbol = null; + this._count = 0; + } else { + const redNotifs = this.room.getThreadUnreadNotificationCount( + this.threadId, + NotificationCountType.Highlight, + ); + const greyNotifs = this.room.getThreadUnreadNotificationCount( + this.threadId, + NotificationCountType.Total, + ); + + // For a 'true count' we pick the grey notifications first because they include the + // red notifications. If we don't have a grey count for some reason we use the red + // count. If that count is broken for some reason, assume zero. This avoids us showing + // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). + const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0); + + // Note: we only set the symbol if we have an actual count. We don't want to show + // zero on badges. + + if (redNotifs > 0) { + this._color = NotificationColor.Red; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } else if (greyNotifs > 0) { + this._color = NotificationColor.Grey; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } + } // finally, publish an update if needed this.emitIfUpdated(snapshot); diff --git a/src/stores/notifications/ThreadsRoomNotificationState.ts b/src/stores/notifications/ThreadsRoomNotificationState.ts index e0ec810cec4..ced50184828 100644 --- a/src/stores/notifications/ThreadsRoomNotificationState.ts +++ b/src/stores/notifications/ThreadsRoomNotificationState.ts @@ -14,70 +14,108 @@ 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 { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { IDestroyable } from "../../utils/IDestroyable"; -import { NotificationState, NotificationStateEvents } from "./NotificationState"; -import { ThreadNotificationState } from "./ThreadNotificationState"; +import { NotificationState } from "./NotificationState"; import { NotificationColor } from "./NotificationColor"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import * as RoomNotifs from '../../RoomNotifs'; +import { readReceiptChangeIsFor } from "../../utils/read-receipts"; export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable { - public readonly threadsState = new Map(); - - protected _symbol = null; - protected _count = 0; - protected _color = NotificationColor.None; - constructor(public readonly room: Room) { super(); - for (const thread of this.room.getThreads()) { - this.onNewThread(thread); - } - this.room.on(ThreadEvent.New, this.onNewThread); + this.room.on(RoomEvent.Receipt, this.handleReadReceipt); + this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate); + this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); + this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); + MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); + this.updateNotificationState(); } public destroy(): void { super.destroy(); - this.room.off(ThreadEvent.New, this.onNewThread); - for (const [, notificationState] of this.threadsState) { - notificationState.off(NotificationStateEvents.Update, this.onThreadUpdate); + this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt); + this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate); + this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate); + this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); } } + private handleLocalEchoUpdated = () => { + this.updateNotificationState(); + }; - public getThreadRoomState(thread: Thread): ThreadNotificationState { - if (!this.threadsState.has(thread)) { - this.threadsState.set(thread, new ThreadNotificationState(thread)); - } - return this.threadsState.get(thread); - } + private handleReadReceipt = (event: MatrixEvent, room: Room) => { + if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore + if (room.roomId !== this.room.roomId) return; // not for us - ignore + this.updateNotificationState(); + }; - private onNewThread = (thread: Thread): void => { - const notificationState = new ThreadNotificationState(thread); - this.threadsState.set( - thread, - notificationState, - ); - notificationState.on(NotificationStateEvents.Update, this.onThreadUpdate); + private handleMembershipUpdate = () => { + this.updateNotificationState(); }; - 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; - } + private onEventDecrypted = (event: MatrixEvent) => { + if (event.getRoomId() !== this.room.roomId) return; // ignore - not for us or notifications timeline + + this.updateNotificationState(); + }; + + private handleRoomEventUpdate = (event: MatrixEvent, room: Room | null) => { + if (room?.roomId !== this.room.roomId) return; // ignore - not for us or notifications timeline + + this.updateNotificationState(); + }; + + private handleAccountDataUpdate = (ev: MatrixEvent) => { + if (ev.getType() === "m.push_rules") { + this.updateNotificationState(); } - this.updateNotificationState(color); }; - private updateNotificationState(color: NotificationColor): void { + private updateNotificationState() { const snapshot = this.snapshot(); - this._color = color; + + if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute) { + // When muted we suppress all notification states, even if we have context on them. + this._color = NotificationColor.None; + this._symbol = null; + this._count = 0; + } else { + const redNotifs = this.room.getTotalThreadsUnreadNotificationCount(NotificationCountType.Highlight); + const greyNotifs = this.room.getTotalThreadsUnreadNotificationCount(NotificationCountType.Total); + + // For a 'true count' we pick the grey notifications first because they include the + // red notifications. If we don't have a grey count for some reason we use the red + // count. If that count is broken for some reason, assume zero. This avoids us showing + // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). + const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0); + + // Note: we only set the symbol if we have an actual count. We don't want to show + // zero on badges. + + if (redNotifs > 0) { + this._color = NotificationColor.Red; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } else if (greyNotifs > 0) { + this._color = NotificationColor.Grey; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } + } + // finally, publish an update if needed this.emitIfUpdated(snapshot); } } + From 5cd7ac87ff856a52dc47a4e2c36adf9f7f13765e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 18 Aug 2022 15:44:17 +0100 Subject: [PATCH 2/5] Fix unread room state updating --- src/Unread.ts | 28 ------------ src/components/views/rooms/EventTile.tsx | 45 ------------------- .../notifications/RoomNotificationState.ts | 4 +- 3 files changed, 2 insertions(+), 75 deletions(-) diff --git a/src/Unread.ts b/src/Unread.ts index 19dc07f4141..9e7f5a2c826 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -22,8 +22,6 @@ import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; import { haveRendererForEvent } from "./events/EventTileFactory"; -import SettingsStore from "./settings/SettingsStore"; -import { RoomNotificationStateStore } from "./stores/notifications/RoomNotificationStateStore"; /** * Returns true if this event arriving in a room should affect the room's @@ -62,32 +60,6 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { // despite the name of the method :(( const readUpToId = room.getEventReadUpTo(myUserId); - if (!SettingsStore.getValue("feature_thread")) { - // as we don't send RRs for our own messages, make sure we special case that - // if *we* sent the last message into the room, we consider it not unread! - // Should fix: https://github.com/vector-im/element-web/issues/3263 - // https://github.com/vector-im/element-web/issues/2427 - // ...and possibly some of the others at - // https://github.com/vector-im/element-web/issues/3363 - if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) { - return false; - } - } else { - const threadState = RoomNotificationStateStore.instance.getThreadsRoomState(room); - if (threadState.color > 0) { - return true; - } - } - - // if the read receipt relates to an event is that part of a thread - // we consider that there are no unread messages - // This might be a false negative, but probably the best we can do until - // the read receipts have evolved to cater for threads - const event = room.findEventById(readUpToId); - if (event?.getThread()) { - return false; - } - // this just looks at whatever history we have, which if we've only just started // up probably won't be very much, so if the last couple of events are ones that // don't count, we don't know if there are any events that do count between where diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8a4056ce1a3..c2e2134cb44 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -65,10 +65,6 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import Toolbar from '../../../accessibility/Toolbar'; import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton'; -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 AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; import { copyPlaintext, getSelectedText } from '../../../utils/strings'; import { DecryptionFailureTracker } from '../../../DecryptionFailureTracker'; @@ -252,7 +248,6 @@ export class UnwrappedEventTile extends React.Component { private isListeningForReceipts: boolean; private tile = React.createRef(); private replyChain = React.createRef(); - private threadState: ThreadNotificationState; public readonly ref = createRef(); @@ -392,10 +387,6 @@ export class UnwrappedEventTile extends React.Component { if (SettingsStore.getValue("feature_thread")) { this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); - - if (this.thread) { - this.setupNotificationListener(this.thread); - } } client.decryptEventIfNeeded(this.props.mxEvent); @@ -404,40 +395,7 @@ export class UnwrappedEventTile extends React.Component { room?.on(ThreadEvent.New, this.onNewThread); } - private setupNotificationListener(thread: Thread): void { - const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room); - - this.threadState = notifications.getThreadRoomState(thread); - - this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate); - this.onThreadStateUpdate(); - } - - private onThreadStateUpdate = (): void => { - 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) => { - if (thread !== this.state.thread) { - if (this.threadState) { - this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); - } - - this.setupNotificationListener(thread); - } - this.setState({ thread }); }; @@ -475,9 +433,6 @@ export class UnwrappedEventTile extends React.Component { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room?.off(ThreadEvent.New, this.onNewThread); - if (this.threadState) { - this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); - } } componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) { diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index c4c803483df..e078a625520 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -119,8 +119,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy this._symbol = "!"; this._count = 1; // not used, technically } else { - const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Highlight); - const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Total); + const redNotifs = this.room.getTotalUnreadNotificationCount(NotificationCountType.Highlight); + const greyNotifs = this.room.getTotalUnreadNotificationCount(NotificationCountType.Total); // For a 'true count' we pick the grey notifications first because they include the // red notifications. If we don't have a grey count for some reason we use the red From 0ceb29fe9fddbbbc4ba6c1fdcee7ef4fd6c3efed Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 18 Aug 2022 16:14:54 +0100 Subject: [PATCH 3/5] fix thread room state color --- .../views/right_panel/RoomHeaderButtons.tsx | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 4b5889bc820..87b44d9259c 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -20,7 +20,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; @@ -129,30 +129,11 @@ export default class RoomHeaderButtons extends HeaderButtons { RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView, ]; - private threadNotificationState: ThreadsRoomNotificationState; constructor(props: IProps) { super(props, HeaderKind.Room); - - this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room); - } - - public componentDidMount(): void { - super.componentDidMount(); - this.threadNotificationState.on(NotificationStateEvents.Update, this.onThreadNotification); - } - - public componentWillUnmount(): void { - super.componentWillUnmount(); - this.threadNotificationState.off(NotificationStateEvents.Update, this.onThreadNotification); } - private onThreadNotification = (): void => { - this.setState({ - threadNotificationColor: this.threadNotificationState.color, - }); - }; - protected onAction(payload: ActionPayload) { if (payload.action === Action.ViewUser) { if (payload.member) { @@ -233,6 +214,18 @@ export default class RoomHeaderButtons extends HeaderButtons { isHighlighted={this.isPhase(RightPanelPhases.Timeline)} onClick={this.onTimelineCardClicked} />, ); + + const unreadCount = this.props.room?.getTotalUnreadNotificationCount(NotificationCountType.Total) + ?? this.props.room?.getTotalUnreadNotificationCount(NotificationCountType.Highlight) + ?? 0; + + // Nested ternary, niiice 😏 + const color = this.props.room?.getTotalUnreadNotificationCount(NotificationCountType.Highlight) > 0 + ? NotificationColor.Red + : this.props.room?.getTotalUnreadNotificationCount(NotificationCountType.Total) > 0 + ? NotificationColor.Grey + : NotificationColor.None; + rightPanelPhaseButtons.set(RightPanelPhases.ThreadPanel, SettingsStore.getValue("feature_thread") ? { title={_t("Threads")} onClick={this.onThreadsPanelClicked} isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)} - isUnread={this.threadNotificationState.color > 0} + isUnread={unreadCount > 0} > - + : null, ); From 7825f2c5f0a38e7ffc0c8b17e909b408ef6a7eb9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 18 Aug 2022 16:26:59 +0100 Subject: [PATCH 4/5] threads list individual thread badge --- src/components/views/rooms/EventTile.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c2e2134cb44..7b830d4b50c 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1294,6 +1294,17 @@ export class UnwrappedEventTile extends React.Component { ]); } case TimelineRenderingType.ThreadsList: { + const evt = this.props.mxEvent; + const room = MatrixClientPeg.get().getRoom(evt.getRoomId()); + + const color = room.getThreadUnreadNotificationCount( + evt.threadRootId, NotificationCountType.Highlight, + ) > 0 + ? NotificationCountType.Highlight + : room.getThreadUnreadNotificationCount(evt.threadRootId, NotificationCountType.Total) > 0 + ? NotificationCountType.Total + : undefined; + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( React.createElement(this.props.as || "li", { @@ -1307,7 +1318,7 @@ export class UnwrappedEventTile extends React.Component { "data-shape": this.context.timelineRenderingType, "data-self": isOwnEvent, "data-has-reply": !!replyChain, - "data-notification": this.state.threadNotification, + "data-notification": color, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), "onClick": (ev: MouseEvent) => { From 0ffac5d32f095efb5e5a7c1792609045d55c5958 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 19 Aug 2022 18:07:17 +0100 Subject: [PATCH 5/5] Enable threads by default --- src/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index c5f1511420c..64f3032d610 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -262,7 +262,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { controller: new ThreadBetaController(), displayName: _td("Threaded messaging"), supportedLevels: LEVELS_FEATURE, - default: false, + default: true, betaInfo: { title: _td("Threads"), caption: () => <>