Add thread notification with server assistance (MSC3773) (#9400)

Co-authored-by: Janne Mareike Koschinski <janne@kuschku.de>
This commit is contained in:
Germain 2022-10-24 07:50:21 +01:00 committed by GitHub
parent d4f1c573ad
commit 9eb4f8d723
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1014 additions and 142 deletions

View file

@ -27,6 +27,7 @@ import { NotificationCountType, Room, RoomEvent } from 'matrix-js-sdk/src/models
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { Feature, ServerSupport } from 'matrix-js-sdk/src/feature';
import { Icon as LinkIcon } from '../../../../res/img/element-icons/link.svg';
import { Icon as ViewInRoomIcon } from '../../../../res/img/element-icons/view-in-room.svg';
@ -84,6 +85,7 @@ import { useTooltip } from "../../../utils/useTooltip";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { ElementCall } from "../../../models/Call";
import { UnreadNotificationBadge } from './NotificationBadge/UnreadNotificationBadge';
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
@ -113,7 +115,7 @@ export interface IEventTileType extends React.Component {
getEventTileOps?(): IEventTileOps;
}
interface IProps {
export interface EventTileProps {
// the MatrixEvent to show
mxEvent: MatrixEvent;
@ -248,7 +250,7 @@ interface IState {
}
// MUST be rendered within a RoomContext with a set timelineRenderingType
export class UnwrappedEventTile extends React.Component<IProps, IState> {
export class UnwrappedEventTile extends React.Component<EventTileProps, IState> {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
private tile = React.createRef<IEventTileType>();
@ -267,7 +269,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
constructor(props: EventTileProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
const thread = this.thread;
@ -394,7 +396,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_thread")) {
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
if (this.thread) {
if (this.thread && !this.supportsThreadNotifications) {
this.setupNotificationListener(this.thread);
}
}
@ -405,33 +407,40 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
room?.on(ThreadEvent.New, this.onNewThread);
}
private get supportsThreadNotifications(): boolean {
const client = MatrixClientPeg.get();
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
}
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();
if (!this.supportsThreadNotifications) {
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;
}
if (!this.supportsThreadNotifications) {
let threadNotification = null;
switch (this.threadState?.color) {
case NotificationColor.Grey:
threadNotification = NotificationCountType.Total;
break;
case NotificationColor.Red:
threadNotification = NotificationCountType.Highlight;
break;
}
this.setState({
threadNotification,
});
this.setState({
threadNotification,
});
}
};
private updateThread = (thread: Thread) => {
if (thread !== this.state.thread) {
if (thread !== this.state.thread && !this.supportsThreadNotifications) {
if (this.threadState) {
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
}
@ -444,7 +453,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps: IProps) {
UNSAFE_componentWillReceiveProps(nextProps: EventTileProps) {
// re-check the sender verification as outgoing events progress through
// the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
@ -452,7 +461,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
}
}
shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
shouldComponentUpdate(nextProps: EventTileProps, nextState: IState): boolean {
if (objectHasDiff(this.state, nextState)) {
return true;
}
@ -481,7 +490,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
}
}
componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) {
componentDidUpdate() {
// If we're not listening for receipts and expect to be, register a listener.
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
@ -667,7 +676,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
}, this.props.onHeightChanged); // Decryption may have caused a change in size
}
private propsEqual(objA: IProps, objB: IProps): boolean {
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
@ -1348,6 +1357,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
]);
}
case TimelineRenderingType.ThreadsList: {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
// 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", {
@ -1361,7 +1371,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
"data-shape": this.context.timelineRenderingType,
"data-self": isOwnEvent,
"data-has-reply": !!replyChain,
"data-notification": this.state.threadNotification,
"data-notification": !this.supportsThreadNotifications
? this.state.threadNotification
: undefined,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
"onClick": (ev: MouseEvent) => {
@ -1409,6 +1421,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
</RovingAccessibleTooltipButton>
</Toolbar>
{ msgOption }
<UnreadNotificationBadge
room={room}
threadId={this.props.mxEvent.getId()} />
</>)
);
}
@ -1512,7 +1527,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
}
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
const SafeEventTile = forwardRef((props: IProps, ref: RefObject<UnwrappedEventTile>) => {
const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => {
return <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
<UnwrappedEventTile ref={ref} {...props} />
</TileErrorBoundary>;