Hook threads notification state to UI (#7298)

This commit is contained in:
Germain 2021-12-13 14:05:42 +00:00 committed by GitHub
parent 55eda7314b
commit ce570ab827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 139 additions and 28 deletions

View File

@ -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;

View File

@ -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<P = {}> extends React.Component<IPro
const rps = RightPanelStore.getSharedInstance();
this.state = {
headerKind: kind,
threadNotificationColor: NotificationColor.None,
phase: kind === HeaderKind.Room ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase,
};
}

View File

@ -19,6 +19,7 @@ limitations under the License.
*/
import React from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
@ -36,6 +37,8 @@ import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { ThreadsRoomNotificationState } from "../../../stores/notifications/ThreadsRoomNotificationState";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
@ -48,14 +51,22 @@ const ROOM_INFO_PHASES = [
];
interface IUnreadIndicatorProps {
className: string;
color?: NotificationColor;
}
const UnreadIndicator = ({ className }: IUnreadIndicatorProps) => {
return <React.Fragment>
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 <>
<div className="mx_RightPanel_headerButton_unreadIndicator_bg" />
<div className={className} />
</React.Fragment>;
<div className={classes} />
</>;
};
interface IHeaderButtonProps {
@ -72,7 +83,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderBut
let unreadIndicator;
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
unreadIndicator = <UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator" />;
unreadIndicator = <UnreadIndicator />;
}
return <HeaderButton
@ -89,17 +100,11 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderBut
const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }: IHeaderButtonProps) => {
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 =
<UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator mx_Indicator_gray" />;
break;
case NotificationColor.Red:
unreadIndicator =
<UnreadIndicator className="mx_RightPanel_headerButton_unreadIndicator" />;
break;
default:
break;
unreadIndicator = <UnreadIndicator color={color} />;
}
return <HeaderButton
name="timelineCardButton"
@ -123,11 +128,30 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
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<IProps> {
rightPanelPhaseButtons.set(RightPanelPhases.PinnedMessages,
<PinnedMessagesHeaderButton
key="pinnedMessagesButton"
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked} />,
);
rightPanelPhaseButtons.set(RightPanelPhases.Timeline,
<TimelineCardHeaderButton
key="timelineButton"
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
onClick={this.onTimelineCardClicked} />,
@ -205,11 +231,14 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
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']}>
<UnreadIndicator color={this.threadNotificationState.color} />
</HeaderButton>
: null,
);
rightPanelPhaseButtons.set(RightPanelPhases.NotificationPanel,
<HeaderButton
key="notifsButton"
name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
@ -218,6 +247,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
);
rightPanelPhaseButtons.set(RightPanelPhases.RoomSummary,
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t('Room Info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}

View File

@ -23,6 +23,7 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { logger } from "matrix-js-sdk/src/logger";
import { NotificationCountType } from 'matrix-js-sdk/src/models/room';
import ReplyChain from "../elements/ReplyChain";
import { _t } from '../../../languageHandler';
@ -67,6 +68,10 @@ import Toolbar from '../../../accessibility/Toolbar';
import { POLL_START_EVENT_TYPE } from '../../../polls/consts';
import { RovingAccessibleTooltipButton } from '../../../accessibility/roving/RovingAccessibleTooltipButton';
import ThreadListContextMenu from '../context_menus/ThreadListContextMenu';
import { ThreadNotificationState } from '../../../stores/notifications/ThreadNotificationState';
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
import { NotificationColor } from '../../../stores/notifications/NotificationColor';
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@ -346,6 +351,7 @@ interface IState {
hover: boolean;
isQuoteExpanded?: boolean;
thread?: Thread;
threadNotification?: NotificationCountType;
}
@replaceableComponent("views.rooms.EventTile")
@ -355,6 +361,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// TODO: Types
private tile = React.createRef<unknown>();
private replyChain = React.createRef<ReplyChain>();
private threadState: ThreadNotificationState;
public readonly ref = createRef<HTMLElement>();
@ -492,17 +499,55 @@ export default class EventTile extends React.Component<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
"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 }),

View File

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

View File

@ -23,7 +23,7 @@ import { ThreadNotificationState } from "./ThreadNotificationState";
import { NotificationColor } from "./NotificationColor";
export class ThreadsRoomNotificationState extends NotificationState implements IDestroyable {
private threadsState = new Map<Thread, ThreadNotificationState>();
public readonly threadsState = new Map<Thread, ThreadNotificationState>();
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);
}