diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index ef04fb47d0..b7ad40d0e7 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -24,6 +24,24 @@ $left-gutter: 64px; font-size: $font-14px; position: relative; + &[data-shape=thread_list][data-notification]::before { + content: ""; + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + right: -16px; + top: 7px; + } + + &[data-shape=thread_list][data-notification=total]::before { + background-color: $roomtile-default-badge-bg-color; + } + + &[data-shape=thread_list][data-notification=highlight]::before { + background-color: $alert; + } + .mx_ThreadInfo { margin-right: 110px; margin-left: 64px; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 862f25238d..20ab61cfee 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -30,6 +30,7 @@ import { } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import type { EventSubscription } from "fbemitter"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { NotificationColor } from '../../../stores/notifications/NotificationColor'; export enum HeaderKind { Room = "room", @@ -39,6 +40,7 @@ export enum HeaderKind { interface IState { headerKind: HeaderKind; phase: RightPanelPhases; + threadNotificationColor: NotificationColor; } interface IProps {} @@ -54,6 +56,7 @@ export default abstract class HeaderButtons

extends React.Component { - return +const UnreadIndicator = ({ color }: IUnreadIndicatorProps) => { + if (color === NotificationColor.None) { + return null; + } + + const classes = classNames({ + "mx_RightPanel_headerButton_unreadIndicator": true, + "mx_Indicator_gray": color === NotificationColor.Grey, + }); + return <>

-
- ; +
+ ; }; interface IHeaderButtonProps { @@ -72,7 +83,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderBut let unreadIndicator; if (pinnedEvents.some(id => !readPinnedEvents.has(id))) { - unreadIndicator = ; + unreadIndicator = ; } return { if (!SettingsStore.getValue("feature_maximised_widgets")) return null; let unreadIndicator; - switch (RoomNotificationStateStore.instance.getRoomState(room).color) { + const color = RoomNotificationStateStore.instance.getRoomState(room).color; + switch (color) { case NotificationColor.Grey: - unreadIndicator = - ; - break; case NotificationColor.Red: - unreadIndicator = - ; - break; - default: - break; + unreadIndicator = ; } return { 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) { @@ -188,12 +212,14 @@ export default class RoomHeaderButtons extends HeaderButtons { rightPanelPhaseButtons.set(RightPanelPhases.PinnedMessages, , ); rightPanelPhaseButtons.set(RightPanelPhases.Timeline, , @@ -205,11 +231,14 @@ export default class RoomHeaderButtons extends HeaderButtons { title={_t("Threads")} onClick={this.onThreadsPanelClicked} isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)} - analytics={['Right Panel', 'Threads List Button', 'click']} /> + analytics={['Right Panel', 'Threads List Button', 'click']}> + + : null, ); rightPanelPhaseButtons.set(RightPanelPhases.NotificationPanel, { ); rightPanelPhaseButtons.set(RightPanelPhases.RoomSummary, { // TODO: Types private tile = React.createRef(); private replyChain = React.createRef(); + private threadState: ThreadNotificationState; public readonly ref = createRef(); @@ -492,17 +499,55 @@ export default class EventTile extends React.Component { if (SettingsStore.getValue("feature_thread")) { this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread); this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); + + if (this.thread) { + this.setupNotificationListener(this.thread); + } } const room = this.context.getRoom(this.props.mxEvent.getRoomId()); room?.on(ThreadEvent.New, this.onNewThread); } - private updateThread = (thread) => { + private setupNotificationListener = (thread): void => { + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(room); + + this.threadState = notifications.threadsState.get(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({ - thread, + threadNotification, }); - this.forceUpdate(); + }; + + private updateThread = (thread) => { + if (thread !== this.state.thread) { + if (this.threadState) { + this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); + } + + this.setupNotificationListener(thread); + this.setState({ + thread, + }); + + this.forceUpdate(); + } }; // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -540,6 +585,9 @@ export default class EventTile extends React.Component { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); room?.off(ThreadEvent.New, this.onNewThread); + if (this.threadState) { + this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); + } } componentDidUpdate(prevProps, prevState, snapshot) { @@ -1358,6 +1406,7 @@ export default class EventTile extends React.Component { "data-shape": this.props.tileShape, "data-self": isOwnEvent, "data-has-reply": !!replyChain, + "data-notification": this.state.threadNotification, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), diff --git a/src/stores/notifications/ThreadNotificationState.ts b/src/stores/notifications/ThreadNotificationState.ts index c8c74a07e7..aac3153c73 100644 --- a/src/stores/notifications/ThreadNotificationState.ts +++ b/src/stores/notifications/ThreadNotificationState.ts @@ -40,20 +40,26 @@ export class ThreadNotificationState extends NotificationState implements IDestr this.thread.off(ThreadEvent.ViewThread, this.resetThreadNotification); } - private handleNewThreadReply(thread: Thread, event: MatrixEvent) { + private handleNewThreadReply = (thread: Thread, event: MatrixEvent) => { const client = MatrixClientPeg.get(); - const isOwn = client.getUserId() === event.getSender(); - if (!isOwn) { + const myUserId = client.getUserId(); + + const isOwn = myUserId === event.getSender(); + const readReceipt = this.room.getReadReceiptForUserId(myUserId); + + if (!isOwn && !readReceipt || event.getTs() >= readReceipt.data.ts) { const actions = client.getPushActionsForEvent(event, true); - const color = !!actions.tweaks.highlight - ? NotificationColor.Red - : NotificationColor.Grey; + if (actions?.tweaks) { + const color = !!actions.tweaks.highlight + ? NotificationColor.Red + : NotificationColor.Grey; - this.updateNotificationState(color); + this.updateNotificationState(color); + } } - } + }; private resetThreadNotification = (): void => { this.updateNotificationState(NotificationColor.None); diff --git a/src/stores/notifications/ThreadsRoomNotificationState.ts b/src/stores/notifications/ThreadsRoomNotificationState.ts index eef8ff3991..bb3bbad716 100644 --- a/src/stores/notifications/ThreadsRoomNotificationState.ts +++ b/src/stores/notifications/ThreadsRoomNotificationState.ts @@ -23,7 +23,7 @@ import { ThreadNotificationState } from "./ThreadNotificationState"; import { NotificationColor } from "./NotificationColor"; export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable { - private threadsState = new Map(); + public readonly threadsState = new Map(); protected _symbol = null; protected _count = 0; @@ -31,6 +31,11 @@ export class ThreadsRoomNotificationState extends NotificationState implements I constructor(public readonly room: Room) { super(); + if (this.room?.threads) { + for (const [, thread] of this.room.threads) { + this.onNewThread(thread); + } + } this.room.on(ThreadEvent.New, this.onNewThread); }