element-portable/src/components/views/avatars/DecoratedRoomAvatar.tsx
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

221 lines
7.6 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import classNames from "classnames";
import { EventType, JoinRule, MatrixEvent, Room, RoomEvent, User, UserEvent } from "matrix-js-sdk/src/matrix";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import { Tooltip } from "@vector-im/compound-web";
import RoomAvatar from "./RoomAvatar";
import NotificationBadge from "../rooms/NotificationBadge";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import { isPresenceEnabled } from "../../../utils/presence";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
interface IProps {
room: Room;
size: string;
displayBadge?: boolean;
/**
* If true, show nothing if the notification would only cause a dot to be shown rather than
* a badge. That is: only display badges and not dots. Default: false.
*/
hideIfDot?: boolean;
oobData?: IOOBData;
viewAvatarOnClick?: boolean;
tooltipProps?: {
tabIndex?: number;
};
}
interface IState {
notificationState?: NotificationState;
icon: Icon;
}
const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
enum Icon {
// Note: the names here are used in CSS class names
None = "NONE", // ... except this one
Globe = "GLOBE",
PresenceOnline = "ONLINE",
PresenceAway = "AWAY",
PresenceOffline = "OFFLINE",
PresenceBusy = "BUSY",
}
function tooltipText(variant: Icon): string | undefined {
switch (variant) {
case Icon.Globe:
return _t("room|header|room_is_public");
case Icon.PresenceOnline:
return _t("presence|online");
case Icon.PresenceAway:
return _t("presence|away");
case Icon.PresenceOffline:
return _t("presence|offline");
case Icon.PresenceBusy:
return _t("presence|busy");
}
}
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User | null = null;
private isUnmounted = false;
private isWatchingTimeline = false;
public constructor(props: IProps) {
super(props);
this.state = {
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
icon: this.calculateIcon(),
};
}
public componentWillUnmount(): void {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off(RoomEvent.Timeline, this.onRoomTimeline);
this.dmUser = null; // clear listeners, if any
}
private get isPublicRoom(): boolean {
return this.props.room.getJoinRule() === JoinRule.Public;
}
private get dmUser(): User | null {
return this._dmUser;
}
private set dmUser(val: User | null) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
oldUser.off(UserEvent.CurrentlyActive, this.onPresenceUpdate);
oldUser.off(UserEvent.Presence, this.onPresenceUpdate);
}
if (this._dmUser && oldUser !== this._dmUser) {
this._dmUser.on(UserEvent.CurrentlyActive, this.onPresenceUpdate);
this._dmUser.on(UserEvent.Presence, this.onPresenceUpdate);
}
}
private onRoomTimeline = (ev: MatrixEvent, room?: Room): void => {
if (this.isUnmounted) return;
if (this.props.room.roomId !== room?.roomId) return;
if (ev.getType() === EventType.RoomJoinRules || ev.getType() === EventType.RoomMember) {
const newIcon = this.calculateIcon();
if (newIcon !== this.state.icon) {
this.setState({ icon: newIcon });
}
}
};
private onPresenceUpdate = (): void => {
if (this.isUnmounted) return;
const newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({ icon: newIcon });
};
private getPresenceIcon(): Icon {
if (!this.dmUser) return Icon.None;
let icon = Icon.None;
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === "online";
if (BUSY_PRESENCE_NAME.matches(this.dmUser.presence)) {
icon = Icon.PresenceBusy;
} else if (isOnline) {
icon = Icon.PresenceOnline;
} else if (this.dmUser.presence === "offline") {
icon = Icon.PresenceOffline;
} else if (this.dmUser.presence === "unavailable") {
icon = Icon.PresenceAway;
}
return icon;
}
private calculateIcon(): Icon {
let icon = Icon.None;
// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId && getJoinedNonFunctionalMembers(this.props.room).length === 2) {
// Track presence, if available
if (isPresenceEnabled(this.props.room.client)) {
this.dmUser = MatrixClientPeg.safeGet().getUser(otherUserId);
icon = this.getPresenceIcon();
}
} else {
// Track publicity
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
if (!this.isWatchingTimeline) {
this.props.room.on(RoomEvent.Timeline, this.onRoomTimeline);
this.isWatchingTimeline = true;
}
}
return icon;
}
public render(): React.ReactNode {
// Spread the remaining props to make it work with compound component
const { room, size, displayBadge, hideIfDot, oobData, viewAvatarOnClick, tooltipProps, ...props } = this.props;
let badge: React.ReactNode;
if (this.props.displayBadge && this.state.notificationState) {
badge = (
<NotificationBadge
notification={this.state.notificationState}
hideIfDot={this.props.hideIfDot}
roomId={this.props.room.roomId}
/>
);
}
let icon: JSX.Element | undefined;
if (this.state.icon !== Icon.None) {
icon = (
<div
tabIndex={this.props.tooltipProps?.tabIndex ?? 0}
className={`mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_${this.state.icon.toLowerCase()}`}
/>
);
}
const classes = classNames("mx_DecoratedRoomAvatar", {
mx_DecoratedRoomAvatar_cutout: icon,
});
return (
<div className={classes} {...props}>
<RoomAvatar
room={this.props.room}
size={this.props.size}
oobData={this.props.oobData}
viewAvatarOnClick={this.props.viewAvatarOnClick}
/>
{icon && (
<Tooltip label={tooltipText(this.state.icon)!} placement="bottom">
{icon}
</Tooltip>
)}
{badge}
</div>
);
}
}