Add thread notification with server assistance (MSC3773) (#9400)
Co-authored-by: Janne Mareike Koschinski <janne@kuschku.de>
This commit is contained in:
parent
d4f1c573ad
commit
9eb4f8d723
22 changed files with 1014 additions and 142 deletions
|
@ -20,7 +20,8 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
|
@ -43,6 +44,7 @@ import { SummarizedNotificationState } from "../../../stores/notifications/Summa
|
|||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -136,32 +138,67 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
private threadNotificationState: ThreadsRoomNotificationState;
|
||||
private globalNotificationState: SummarizedNotificationState;
|
||||
|
||||
private get supportsThreadNotifications(): boolean {
|
||||
const client = MatrixClientPeg.get();
|
||||
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
}
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Room);
|
||||
|
||||
this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room);
|
||||
if (!this.supportsThreadNotifications) {
|
||||
this.threadNotificationState = RoomNotificationStateStore.instance.getThreadsRoomState(this.props.room);
|
||||
}
|
||||
this.globalNotificationState = RoomNotificationStateStore.instance.globalState;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
super.componentDidMount();
|
||||
this.threadNotificationState.on(NotificationStateEvents.Update, this.onThreadNotification);
|
||||
if (!this.supportsThreadNotifications) {
|
||||
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
} else {
|
||||
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||
}
|
||||
this.onNotificationUpdate();
|
||||
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
super.componentWillUnmount();
|
||||
this.threadNotificationState.off(NotificationStateEvents.Update, this.onThreadNotification);
|
||||
if (!this.supportsThreadNotifications) {
|
||||
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
} else {
|
||||
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||
}
|
||||
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||
}
|
||||
|
||||
private onThreadNotification = (): void => {
|
||||
private onNotificationUpdate = (): void => {
|
||||
let threadNotificationColor: NotificationColor;
|
||||
if (!this.supportsThreadNotifications) {
|
||||
threadNotificationColor = this.threadNotificationState.color;
|
||||
} else {
|
||||
threadNotificationColor = this.notificationColor;
|
||||
}
|
||||
|
||||
// console.log
|
||||
// XXX: why don't we read from this.state.threadNotificationColor in the render methods?
|
||||
this.setState({
|
||||
threadNotificationColor: this.threadNotificationState.color,
|
||||
threadNotificationColor,
|
||||
});
|
||||
};
|
||||
|
||||
private get notificationColor(): NotificationColor {
|
||||
switch (this.props.room.threadsAggregateNotificationType) {
|
||||
case NotificationCountType.Highlight:
|
||||
return NotificationColor.Red;
|
||||
case NotificationCountType.Total:
|
||||
return NotificationColor.Grey;
|
||||
default:
|
||||
return NotificationColor.None;
|
||||
}
|
||||
}
|
||||
|
||||
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
||||
// XXX: why don't we read from this.state.globalNotificationCount in the render methods?
|
||||
this.globalNotificationState = notificationState;
|
||||
|
@ -255,12 +292,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
? <HeaderButton
|
||||
key={RightPanelPhases.ThreadPanel}
|
||||
name="threadsButton"
|
||||
data-testid="threadsButton"
|
||||
title={_t("Threads")}
|
||||
onClick={this.onThreadsPanelClicked}
|
||||
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
||||
isUnread={this.threadNotificationState.color > 0}
|
||||
isUnread={this.state.threadNotificationColor > 0}
|
||||
>
|
||||
<UnreadIndicator color={this.threadNotificationState.color} />
|
||||
<UnreadIndicator color={this.state.threadNotificationColor} />
|
||||
</HeaderButton>
|
||||
: null,
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -15,16 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { XOR } from "../../../@types/common";
|
||||
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { StatelessNotificationBadge } from "./NotificationBadge/StatelessNotificationBadge";
|
||||
|
||||
interface IProps {
|
||||
notification: NotificationState;
|
||||
|
@ -113,61 +111,25 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
|
||||
public render(): React.ReactElement {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
||||
const { notification, showUnsentTooltip, onClick } = this.props;
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
if (notification.isIdle) return null;
|
||||
|
||||
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/element-web/issues/14261
|
||||
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
||||
// See git diff for what that boolean state looks like.
|
||||
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
||||
const hasAnySymbol = notification.symbol || notification.count > 0;
|
||||
let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount;
|
||||
if (forceCount) {
|
||||
isEmptyBadge = false;
|
||||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||
let label: string;
|
||||
let tooltip: JSX.Element;
|
||||
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||
label = _t("Message didn't send. Click for info.");
|
||||
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||
}
|
||||
|
||||
let symbol = notification.symbol || formatCount(notification.count);
|
||||
if (isEmptyBadge) symbol = "";
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount,
|
||||
'mx_NotificationBadge_highlighted': notification.hasMentions,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
||||
'mx_NotificationBadge_3char': symbol.length > 2,
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
let label: string;
|
||||
let tooltip: JSX.Element;
|
||||
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
|
||||
label = _t("Message didn't send. Click for info.");
|
||||
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
aria-label={label}
|
||||
{...props}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
</div>
|
||||
);
|
||||
return <StatelessNotificationBadge
|
||||
label={label}
|
||||
symbol={notification.symbol}
|
||||
count={notification.count}
|
||||
color={notification.color}
|
||||
onClick={onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ tooltip }
|
||||
</StatelessNotificationBadge>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { formatCount } from "../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||
|
||||
interface Props {
|
||||
symbol: string | null;
|
||||
count: number;
|
||||
color: NotificationColor;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
onMouseOver?: (ev: MouseEvent) => void;
|
||||
onMouseLeave?: (ev: MouseEvent) => void;
|
||||
children?: React.ReactChildren | JSX.Element;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function StatelessNotificationBadge({
|
||||
symbol,
|
||||
count,
|
||||
color,
|
||||
...props }: Props) {
|
||||
// Don't show a badge if we don't need to
|
||||
if (color === NotificationColor.None) return null;
|
||||
|
||||
const hasUnreadCount = color >= NotificationColor.Grey && (!!count || !!symbol);
|
||||
|
||||
const isEmptyBadge = symbol === null && count === 0;
|
||||
|
||||
if (symbol === null && count > 0) {
|
||||
symbol = formatCount(count);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount,
|
||||
'mx_NotificationBadge_highlighted': color === NotificationColor.Red,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3,
|
||||
'mx_NotificationBadge_3char': symbol?.length > 2,
|
||||
});
|
||||
|
||||
if (props.onClick) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
aria-label={props.label}
|
||||
{...props}
|
||||
className={classes}
|
||||
onClick={props.onClick}
|
||||
onMouseOver={props.onMouseOver}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
{ props.children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import React from "react";
|
||||
|
||||
import { useUnreadNotifications } from "../../../../hooks/useUnreadNotifications";
|
||||
import { StatelessNotificationBadge } from "./StatelessNotificationBadge";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
export function UnreadNotificationBadge({ room, threadId }: Props) {
|
||||
const { symbol, count, color } = useUnreadNotifications(room, threadId);
|
||||
|
||||
return <StatelessNotificationBadge
|
||||
symbol={symbol}
|
||||
count={count}
|
||||
color={color}
|
||||
/>;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue