/* Copyright 2020 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 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 { 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 = ( ); } let icon: JSX.Element | undefined; if (this.state.icon !== Icon.None) { icon = (
); } const classes = classNames("mx_DecoratedRoomAvatar", { mx_DecoratedRoomAvatar_cutout: icon, }); return (
{icon && ( {icon} )} {badge}
); } }