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);
}