Add missing presence indicator to new room header (#12865)
* Add missing presence indicator to new room header DecoratedRoomAvatar doesn't match Figma styles so created a composable avatar wrapper Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add oobData to new room header avatar Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
ca8d63af37
commit
dde19f36ac
9 changed files with 414 additions and 67 deletions
141
src/components/views/avatars/WithPresenceIndicator.tsx
Normal file
141
src/components/views/avatars/WithPresenceIndicator.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Copyright 2024 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, { ReactNode, useEffect, useState } from "react";
|
||||
import { ClientEvent, Room, RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { isPresenceEnabled } from "../../../utils/presence";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { BUSY_PRESENCE_NAME } from "../rooms/PresenceLabel";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
size: string; // CSS size
|
||||
tooltipProps?: {
|
||||
tabIndex?: number;
|
||||
};
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
enum Presence {
|
||||
// Note: the names here are used in CSS class names
|
||||
Online = "ONLINE",
|
||||
Away = "AWAY",
|
||||
Offline = "OFFLINE",
|
||||
Busy = "BUSY",
|
||||
}
|
||||
|
||||
function tooltipText(variant: Presence): string {
|
||||
switch (variant) {
|
||||
case Presence.Online:
|
||||
return _t("presence|online");
|
||||
case Presence.Away:
|
||||
return _t("presence|away");
|
||||
case Presence.Offline:
|
||||
return _t("presence|offline");
|
||||
case Presence.Busy:
|
||||
return _t("presence|busy");
|
||||
}
|
||||
}
|
||||
|
||||
function getDmMember(room: Room): RoomMember | null {
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
return otherUserId ? room.getMember(otherUserId) : null;
|
||||
}
|
||||
|
||||
export const useDmMember = (room: Room): RoomMember | null => {
|
||||
const [dmMember, setDmMember] = useState<RoomMember | null>(getDmMember(room));
|
||||
const updateDmMember = (): void => {
|
||||
setDmMember(getDmMember(room));
|
||||
};
|
||||
|
||||
useEventEmitter(room.currentState, RoomStateEvent.Members, updateDmMember);
|
||||
useEventEmitter(room.client, ClientEvent.AccountData, updateDmMember);
|
||||
useEffect(updateDmMember, [room]);
|
||||
|
||||
return dmMember;
|
||||
};
|
||||
|
||||
function getPresence(member: RoomMember | null): Presence | null {
|
||||
if (!member?.user) return null;
|
||||
|
||||
const presence = member.user.presence;
|
||||
const isOnline = member.user.currentlyActive || presence === "online";
|
||||
if (BUSY_PRESENCE_NAME.matches(member.user.presence)) {
|
||||
return Presence.Busy;
|
||||
}
|
||||
if (isOnline) {
|
||||
return Presence.Online;
|
||||
}
|
||||
if (presence === "offline") {
|
||||
return Presence.Offline;
|
||||
}
|
||||
if (presence === "unavailable") {
|
||||
return Presence.Away;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const usePresence = (room: Room, member: RoomMember | null): Presence | null => {
|
||||
const [presence, setPresence] = useState<Presence | null>(getPresence(member));
|
||||
const updatePresence = (): void => {
|
||||
setPresence(getPresence(member));
|
||||
};
|
||||
|
||||
useEventEmitter(member?.user, UserEvent.Presence, updatePresence);
|
||||
useEventEmitter(member?.user, UserEvent.CurrentlyActive, updatePresence);
|
||||
useEffect(updatePresence, [member]);
|
||||
|
||||
if (getJoinedNonFunctionalMembers(room).length !== 2 || !isPresenceEnabled(room.client)) return null;
|
||||
return presence;
|
||||
};
|
||||
|
||||
const WithPresenceIndicator: React.FC<Props> = ({ room, size, tooltipProps, children }) => {
|
||||
const dmMember = useDmMember(room);
|
||||
const presence = usePresence(room, dmMember);
|
||||
|
||||
let icon: JSX.Element | undefined;
|
||||
if (presence) {
|
||||
icon = (
|
||||
<div
|
||||
tabIndex={tooltipProps?.tabIndex ?? 0}
|
||||
className={`mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_${presence.toLowerCase()}`}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!presence) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<div className="mx_WithPresenceIndicator">
|
||||
{children}
|
||||
<Tooltip label={tooltipText(presence)} placement="bottom">
|
||||
{icon}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithPresenceIndicator;
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { formatDuration } from "../../../DateUtils";
|
||||
|
||||
const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
||||
export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
||||
|
||||
interface IProps {
|
||||
// number of milliseconds ago this user was last active.
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
||||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||
|
@ -25,12 +25,11 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico
|
|||
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
||||
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
|
||||
import { useRoomName } from "../../../hooks/useRoomName";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { useAccountData } from "../../../hooks/useAccountData";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -58,18 +57,22 @@ import { ButtonEvent } from "../elements/AccessibleButton";
|
|||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
||||
import { useIsReleaseAnnouncementOpen } from "../../../hooks/useIsReleaseAnnouncementOpen";
|
||||
import { ReleaseAnnouncementStore } from "../../../stores/ReleaseAnnouncementStore";
|
||||
import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
additionalButtons,
|
||||
oobData,
|
||||
}: {
|
||||
room: Room;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
oobData?: IOOBData;
|
||||
}): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const roomName = useRoomName(room);
|
||||
const roomState = useRoomState(room);
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||
|
@ -100,16 +103,8 @@ export default function RoomHeader({
|
|||
const threadNotifications = useRoomThreadNotifications(room);
|
||||
const globalNotificationState = useGlobalNotificationState();
|
||||
|
||||
const directRoomsList = useAccountData<Record<string, string[]>>(client, EventType.Direct);
|
||||
const [isDirectMessage, setDirectMessage] = useState(false);
|
||||
useEffect(() => {
|
||||
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
|
||||
if (dmRoomList.includes(room?.roomId ?? "")) {
|
||||
setDirectMessage(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [room, directRoomsList]);
|
||||
const dmMember = useDmMember(room);
|
||||
const isDirectMessage = !!dmMember;
|
||||
const e2eStatus = useEncryptionStatus(client, room);
|
||||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
@ -259,7 +254,9 @@ export default function RoomHeader({
|
|||
}}
|
||||
className="mx_RoomHeader_infoWrapper"
|
||||
>
|
||||
<RoomAvatar room={room} size="40px" />
|
||||
<WithPresenceIndicator room={room} size="8px">
|
||||
<RoomAvatar room={room} size="40px" oobData={oobData} />
|
||||
</WithPresenceIndicator>
|
||||
<Box flex="1" className="mx_RoomHeader_info">
|
||||
<BodyText
|
||||
as="div"
|
||||
|
@ -272,7 +269,7 @@ export default function RoomHeader({
|
|||
>
|
||||
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>
|
||||
|
||||
{!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && (
|
||||
{!isDirectMessage && joinRule === JoinRule.Public && (
|
||||
<Tooltip label={_t("common|public_room")} placement="right">
|
||||
<PublicIcon
|
||||
width="16px"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue