mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Hook threads notification state to UI (#7298)
This commit is contained in:
parent
55eda7314b
commit
ce570ab827
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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)}
|
||||
|
@ -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 }),
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user