Files
ThreadNet-Web/src/components/views/right_panel/UserInfo.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

757 lines
27 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2017, 2018 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
import classNames from "classnames";
import {
ClientEvent,
2025-02-05 13:25:06 +00:00
type MatrixClient,
RoomMember,
2025-02-05 13:25:06 +00:00
type Room,
RoomStateEvent,
2025-02-05 13:25:06 +00:00
type MatrixEvent,
User,
2025-02-05 13:25:06 +00:00
type Device,
EventType,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
2025-02-05 13:25:06 +00:00
import { type UserVerificationStatus, type VerificationRequest, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
import { MenuItem } from "@vector-im/compound-web";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block";
import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete";
2020-09-29 10:10:32 +01:00
2020-05-13 20:41:41 -06:00
import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import { _t, UserFriendlyError } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import { type ButtonEvent } from "../elements/AccessibleButton";
import MultiInviter from "../../../utils/MultiInviter";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
2019-12-17 17:26:12 +00:00
import MatrixClientContext from "../../../contexts/MatrixClientContext";
2022-01-05 16:14:44 +01:00
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
2021-06-03 08:41:22 +01:00
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import { Action } from "../../../dispatcher/actions";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
2020-09-08 10:19:51 +01:00
import BaseCard from "./BaseCard";
2020-09-29 10:10:32 +01:00
import Spinner from "../elements/Spinner";
2024-12-02 19:10:17 +01:00
import { ShareDialog } from "../dialogs/ShareDialog";
2020-09-29 10:10:32 +01:00
import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
2025-02-05 13:25:06 +00:00
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
2022-01-05 16:14:44 +01:00
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2025-02-05 13:25:06 +00:00
import { type IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
import PosthogTrackers from "../../../PosthogTrackers";
2025-02-05 13:25:06 +00:00
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
2022-08-04 08:19:52 +02:00
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
export interface IDevice extends Device {
2020-09-29 10:10:32 +01:00
ambiguous?: boolean;
}
export const disambiguateDevices = (devices: IDevice[]): void => {
const names = Object.create(null);
for (let i = 0; i < devices.length; i++) {
const name = devices[i].displayName ?? "";
const indexList = names[name] || [];
indexList.push(i);
names[name] = indexList;
}
for (const name in names) {
if (names[name].length > 1) {
2023-01-11 10:46:35 +00:00
names[name].forEach((j: number) => {
devices[j].ambiguous = true;
});
}
}
};
2023-03-03 13:34:38 +01:00
/**
* Converts the member to a DirectoryMember and starts a DM with them.
*/
async function openDmForUser(matrixClient: MatrixClient, user: Member): Promise<void> {
const avatarUrl = user instanceof User ? user.avatarUrl : user.getMxcAvatarUrl();
const startDmUser = new DirectoryMember({
2022-08-04 08:19:52 +02:00
user_id: user.userId,
display_name: user.rawDisplayName,
2023-03-03 13:34:38 +01:00
avatar_url: avatarUrl,
2022-08-04 08:19:52 +02:00
});
2023-03-06 16:09:56 +01:00
await startDmOnFirstMessage(matrixClient, [startDmUser]);
2019-11-13 12:11:06 +01:00
}
2023-03-03 13:34:38 +01:00
const MessageButton = ({ member }: { member: Member }): JSX.Element => {
const cli = useContext(MatrixClientContext);
const [busy, setBusy] = useState(false);
return (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
if (busy) return;
setBusy(true);
2023-03-03 13:34:38 +01:00
await openDmForUser(cli, member);
setBusy(false);
}}
disabled={busy}
label={_t("user_info|send_message")}
Icon={ChatIcon}
/>
);
};
2023-01-11 10:46:35 +00:00
export const UserOptionsSection: React.FC<{
2023-03-03 13:34:38 +01:00
member: Member;
2020-09-29 10:10:32 +01:00
canInvite: boolean;
isSpace?: boolean;
2024-08-05 08:59:27 +01:00
children?: ReactNode;
}> = ({ member, canInvite, isSpace, children }) => {
2019-12-17 17:26:12 +00:00
const cli = useContext(MatrixClientContext);
let insertPillButton: JSX.Element | undefined;
let inviteUserButton: JSX.Element | undefined;
let readReceiptButton: JSX.Element | undefined;
2019-11-12 17:34:35 +01:00
const isMe = member.userId === cli.getUserId();
const onShareUserClick = (): void => {
2022-06-14 17:51:51 +01:00
Modal.createDialog(ShareDialog, {
target: member,
});
};
// Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt
2019-11-12 17:34:35 +01:00
if (!isMe) {
const onReadReceiptButton = function (room: Room): void {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
highlighted: true,
// this could return null, the default prevents a type error
event_id: room.getEventReadUpTo(member.userId) || undefined,
room_id: room.roomId,
metricsTrigger: undefined, // room doesn't change
});
};
const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : null;
const readReceiptButtonDisabled = isSpace || !room?.getEventReadUpTo(member.userId);
readReceiptButton = (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
if (room && !readReceiptButtonDisabled) {
onReadReceiptButton(room);
}
}}
label={_t("user_info|jump_to_rr_button")}
disabled={readReceiptButtonDisabled}
Icon={CheckIcon}
/>
);
if (member instanceof RoomMember && member.roomId && !isSpace) {
const onInsertPillButton = function (): void {
2021-05-24 22:02:50 +01:00
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
userId: member.userId,
timelineRenderingType: TimelineRenderingType.Room,
});
};
insertPillButton = (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
onInsertPillButton();
}}
label={_t("action|mention")}
Icon={MentionIcon}
/>
);
}
2023-03-03 13:34:38 +01:00
if (
member instanceof RoomMember &&
canInvite &&
(member?.membership ?? KnownMembership.Leave) === KnownMembership.Leave &&
2023-03-03 13:34:38 +01:00
shouldShowComponent(UIComponent.InviteUsers)
) {
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
const onInviteUserButton = async (ev: Event): Promise<void> => {
try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
const inviter = new MultiInviter(cli, roomId || "");
await inviter.invite([member.userId]).then(() => {
if (inviter.getCompletionState(member.userId) !== "invited") {
const errorStringFromInviterUtility = inviter.getErrorText(member.userId);
if (errorStringFromInviterUtility) {
throw new Error(errorStringFromInviterUtility);
} else {
throw new UserFriendlyError("slash_command|invite_failed", {
user: member.userId,
roomId,
cause: undefined,
});
}
}
});
} catch (err) {
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
2023-01-11 10:46:35 +00:00
2022-06-14 17:51:51 +01:00
Modal.createDialog(ErrorDialog, {
title: _t("invite|failed_title"),
2023-01-11 10:46:35 +00:00
description,
});
}
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoInviteButton", ev);
};
inviteUserButton = (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
onInviteUserButton(ev);
}}
label={_t("action|invite")}
Icon={InviteIcon}
/>
);
}
}
const shareUserButton = (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
onShareUserClick();
}}
label={_t("user_info|share_button")}
Icon={ShareIcon}
/>
);
const directMessageButton =
isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : <MessageButton member={member} />;
2019-11-12 16:40:07 +01:00
return (
<Container>
{children}
{directMessageButton}
{inviteUserButton}
{readReceiptButton}
{shareUserButton}
{insertPillButton}
</Container>
);
2019-12-17 17:26:12 +00:00
};
export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("user_info|demote_self_confirm_title"),
description: (
<div>
{isSpace
? _t("user_info|demote_self_confirm_description_space")
: _t("user_info|demote_self_confirm_room")}
</div>
),
button: _t("user_info|demote_button"),
});
const [confirmed] = await finished;
return !!confirmed;
};
export const Container: React.FC<{
children: ReactNode;
className?: string;
}> = ({ children, className }) => {
const classes = classNames("mx_UserInfo_container", className);
return <div className={classes}>{children}</div>;
};
export interface IPowerLevelsContent {
2020-09-29 10:10:32 +01:00
events?: Record<string, number>;
// eslint-disable-next-line camelcase
users_default?: number;
// eslint-disable-next-line camelcase
events_default?: number;
// eslint-disable-next-line camelcase
state_default?: number;
ban?: number;
kick?: number;
redact?: number;
}
export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent): boolean => {
if (!powerLevelContent || !member) return false;
const levelToSend =
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default;
2023-01-11 10:46:35 +00:00
// levelToSend could be undefined as .events_default is optional. Coercing in this case using
// Number() would always return false, so this preserves behaviour
// FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
// the member has a negative powerlevel, this will give an incorrect result.
if (levelToSend === undefined) return false;
return member.powerLevel < levelToSend;
};
2023-01-11 10:46:35 +00:00
export const getPowerLevels = (room: Room): IPowerLevelsContent =>
room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
2021-06-08 16:33:06 +01:00
export const useRoomPowerLevels = (cli: MatrixClient, room: Room): IPowerLevelsContent => {
2021-06-08 16:33:06 +01:00
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
2021-04-13 10:39:09 +01:00
const update = useCallback(
(ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
2021-06-08 16:33:06 +01:00
setPowerLevels(getPowerLevels(room));
},
[room],
);
useTypedEventEmitter(cli, RoomStateEvent.Events, update);
useEffect(() => {
update();
return () => {
setPowerLevels({});
};
}, [update]);
return powerLevels;
};
const IgnoreToggleButton: React.FC<{
member: User | RoomMember;
}> = ({ member }) => {
const cli = useContext(MatrixClientContext);
const unignore = useCallback(() => {
const ignoredUsers = cli.getIgnoredUsers();
const index = ignoredUsers.indexOf(member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
cli.setIgnoredUsers(ignoredUsers);
}, [cli, member]);
const ignore = useCallback(async () => {
const name = (member instanceof User ? member.displayName : member.name) || member.userId;
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("user_info|ignore_confirm_title", { user: name }),
description: <div>{_t("user_info|ignore_confirm_description")}</div>,
button: _t("action|ignore"),
});
const [confirmed] = await finished;
if (confirmed) {
const ignoredUsers = cli.getIgnoredUsers();
ignoredUsers.push(member.userId);
cli.setIgnoredUsers(ignoredUsers);
}
}, [cli, member]);
// Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
// Recheck if the user or client changes
useEffect(() => {
setIsIgnored(cli.isUserIgnored(member.userId));
}, [cli, member.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback(
2024-08-05 08:59:27 +01:00
(ev: MatrixEvent) => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(member.userId));
}
},
[cli, member.userId],
);
useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler);
return (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
if (isIgnored) {
unignore();
} else {
ignore();
}
}}
label={isIgnored ? _t("user_info|unignore_button") : _t("user_info|ignore_button")}
kind="critical"
Icon={BlockIcon}
/>
2019-12-17 17:26:12 +00:00
);
};
const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
};
export interface IRoomPermissions {
2020-09-29 10:10:32 +01:00
modifyLevelMax: number;
canEdit: boolean;
canInvite: boolean;
}
function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
2020-09-29 10:10:32 +01:00
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
modifyLevelMax: -1,
canEdit: false,
canInvite: false,
});
const updateRoomPermissions = useCallback(() => {
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
if (!powerLevels) return;
2023-01-11 10:46:35 +00:00
const me = room.getMember(cli.getUserId() || "");
if (!me) return;
const them = user;
const isMe = me.userId === them.userId;
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
let modifyLevelMax = -1;
if (canAffectUser) {
const editPowerLevel = powerLevels.events?.[EventType.RoomPowerLevels] ?? powerLevels.state_default ?? 50;
if (me.powerLevel >= editPowerLevel) {
modifyLevelMax = me.powerLevel;
}
}
setRoomPermissions({
canInvite: me.powerLevel >= (powerLevels.invite ?? 0),
canEdit: modifyLevelMax >= 0,
modifyLevelMax,
});
}, [cli, user, room]);
2022-02-08 13:14:52 +01:00
useTypedEventEmitter(cli, RoomStateEvent.Update, updateRoomPermissions);
useEffect(() => {
updateRoomPermissions();
return () => {
setRoomPermissions({
2020-09-29 10:10:32 +01:00
modifyLevelMax: -1,
canEdit: false,
canInvite: false,
});
};
}, [updateRoomPermissions]);
return roomPermissions;
}
async function getUserDeviceInfo(
userId: string,
cli: MatrixClient,
downloadUncached = false,
): Promise<Device[] | undefined> {
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], downloadUncached);
const devicesMap = userDeviceMap?.get(userId);
if (!devicesMap) return;
return Array.from(devicesMap.values());
}
export const useDevices = (userId: string): IDevice[] | undefined | null => {
2019-12-17 17:26:12 +00:00
const cli = useContext(MatrixClientContext);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
2023-01-11 10:46:35 +00:00
const [devices, setDevices] = useState<undefined | null | IDevice[]>(undefined);
// Download device lists
useEffect(() => {
setDevices(undefined);
let cancelled = false;
async function downloadDeviceList(): Promise<void> {
try {
const devices = await getUserDeviceInfo(userId, cli, true);
if (cancelled || !devices) {
// we got cancelled - presumably a different user now
return;
}
disambiguateDevices(devices);
setDevices(devices);
2024-10-16 16:43:07 +01:00
} catch {
setDevices(null);
}
}
2020-09-29 10:10:32 +01:00
downloadDeviceList();
// Handle being unmounted
return () => {
cancelled = true;
};
}, [cli, userId]);
// Listen to changes
useEffect(() => {
let cancel = false;
const updateDevices = async (): Promise<void> => {
const newDevices = await getUserDeviceInfo(userId, cli);
if (cancel || !newDevices) return;
setDevices(newDevices);
};
const onDevicesUpdated = (users: string[]): void => {
if (!users.includes(userId)) return;
updateDevices();
};
const onUserTrustStatusChanged = (_userId: string, trustLevel: UserVerificationStatus): void => {
if (_userId !== userId) return;
updateDevices();
};
cli.on(CryptoEvent.DevicesUpdated, onDevicesUpdated);
cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener(CryptoEvent.DevicesUpdated, onDevicesUpdated);
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
};
}, [cli, userId]);
return devices;
};
2020-09-29 10:10:32 +01:00
const BasicUserInfo: React.FC<{
room: Room;
member: User | RoomMember;
}> = ({ room, member }) => {
const cli = useContext(MatrixClientContext);
const powerLevels = useRoomPowerLevels(cli, room);
// Load whether or not we are a Synapse Admin
const isSynapseAdmin = useIsSynapseAdmin(cli);
// Count of how many operations are currently in progress, if > 0 then show a Spinner
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const startUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount + 1);
}, [pendingUpdateCount]);
const stopUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount - 1);
}, [pendingUpdateCount]);
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
const onSynapseDeactivate = useCallback(async () => {
2022-06-14 17:51:51 +01:00
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("user_info|deactivate_confirm_title"),
description: <div>{_t("user_info|deactivate_confirm_description")}</div>,
button: _t("user_info|deactivate_confirm_action"),
danger: true,
});
const [accepted] = await finished;
if (!accepted) return;
try {
await cli.deactivateSynapseUser(member.userId);
} catch (err) {
2021-10-15 16:30:53 +02:00
logger.error("Failed to deactivate user");
logger.error(err);
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
2023-01-11 10:46:35 +00:00
2022-06-14 17:51:51 +01:00
Modal.createDialog(ErrorDialog, {
title: _t("user_info|error_deactivate"),
2023-01-11 10:46:35 +00:00
description,
});
}
}, [cli, member.userId]);
let synapseDeactivateButton;
let spinner;
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
// someone does figure out how to bypass this check the worst that happens is an error.
if (isSynapseAdmin && member.userId.endsWith(`:${cli.getDomain()}`)) {
synapseDeactivateButton = (
<MenuItem
role="button"
onSelect={async (ev) => {
ev.preventDefault();
onSynapseDeactivate();
}}
label={_t("user_info|deactivate_confirm_action")}
kind="critical"
Icon={DeleteIcon}
/>
);
}
let memberDetails;
let adminToolsContainer;
if (room && (member as RoomMember).roomId) {
// hide the Roles section for DMs as it doesn't make sense there
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
memberDetails = (
<PowerLevelSection user={member as RoomMember} room={room} roomPermissions={roomPermissions} />
);
}
adminToolsContainer = (
<UserInfoAdminToolsContainer
powerLevels={powerLevels}
member={member as RoomMember}
room={room}
isUpdating={pendingUpdateCount > 0}
startUpdating={startUpdating}
stopUpdating={stopUpdating}
>
{synapseDeactivateButton}
</UserInfoAdminToolsContainer>
);
} else if (synapseDeactivateButton) {
adminToolsContainer = <Container>{synapseDeactivateButton}</Container>;
}
if (pendingUpdateCount > 0) {
2021-05-20 17:31:10 -04:00
spinner = <Spinner />;
}
const isMe = member.userId === cli.getUserId();
return (
<React.Fragment>
<UserOptionsSection
canInvite={roomPermissions.canInvite}
member={member as RoomMember}
isSpace={room?.isSpaceRoom()}
>
{memberDetails}
</UserOptionsSection>
{adminToolsContainer}
{!isMe && (
<Container>
<IgnoreToggleButton member={member} />
</Container>
)}
{spinner}
</React.Fragment>
);
};
export type Member = User | RoomMember;
2020-09-29 10:10:32 +01:00
interface IProps {
user: Member;
room?: Room;
2024-11-25 17:43:09 +00:00
phase: RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel;
2020-09-29 10:10:32 +01:00
onClose(): void;
2021-05-25 12:13:16 +01:00
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
2020-09-29 10:10:32 +01:00
}
const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => {
2019-12-17 17:26:12 +00:00
const cli = useContext(MatrixClientContext);
2020-01-07 12:58:24 +00:00
// fetch latest room member if we have a room, so we don't show historical information, falling back to user
const member = useMemo(() => (room ? room.getMember(user.userId) || user : user), [room, user]);
2019-11-13 12:09:20 +01:00
const isRoomEncrypted = useIsEncrypted(cli, room);
const devices = useDevices(user.userId) ?? [];
const classes = ["mx_UserInfo"];
2023-01-11 10:46:35 +00:00
let cardState: IRightPanelCardState = {};
// We have no previousPhase for when viewing a UserInfo without a Room at this time
2021-01-30 10:08:38 +01:00
if (room && phase === RightPanelPhases.EncryptionPanel) {
2022-01-05 16:14:44 +01:00
cardState = { member };
2021-01-30 10:08:38 +01:00
}
const onEncryptionPanelClose = (): void => {
RightPanelStore.instance.popCard();
2021-06-29 13:11:58 +01:00
};
2021-01-30 10:08:38 +01:00
let content: JSX.Element | undefined;
switch (phase) {
case RightPanelPhases.MemberInfo:
content = <BasicUserInfo room={room as Room} member={member as User} />;
break;
case RightPanelPhases.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
2020-09-29 10:10:32 +01:00
<EncryptionPanel
{...(props as React.ComponentProps<typeof EncryptionPanel>)}
2021-06-18 16:21:46 +01:00
member={member as User | RoomMember}
2021-01-30 10:08:38 +01:00
onClose={onEncryptionPanelClose}
2023-01-11 10:46:35 +00:00
isRoomEncrypted={Boolean(isRoomEncrypted)}
2020-09-29 10:10:32 +01:00
/>
);
break;
}
let closeLabel: string | undefined;
if (phase === RightPanelPhases.EncryptionPanel) {
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
if (verificationRequest && verificationRequest.pending) {
closeLabel = _t("action|cancel");
}
}
2022-02-08 13:14:52 +01:00
const header = (
<>
<UserInfoHeaderView
hideVerificationSection={phase === RightPanelPhases.EncryptionPanel}
member={member}
devices={devices}
roomId={room?.roomId}
/>
2022-02-08 13:14:52 +01:00
</>
);
2024-07-09 17:06:50 +05:30
return (
<BaseCard
className={classes.join(" ")}
2024-07-17 13:54:35 +01:00
header={_t("common|profile")}
onClose={onClose}
closeLabel={closeLabel}
2022-01-05 16:14:44 +01:00
cardState={cardState}
onBack={(ev: ButtonEvent) => {
if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) {
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev);
}
}}
>
2023-10-20 14:30:37 +01:00
{header}
2020-09-08 10:19:51 +01:00
{content}
</BaseCard>
);
2019-12-17 17:26:12 +00:00
};
export default UserInfo;