Files
ThreadNet-Web/apps/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.

262 lines
8.9 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.
*/
2025-10-20 08:13:20 +02:00
import React, { type JSX, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
import classNames from "classnames";
2025-10-20 08:13:20 +02:00
import { type MatrixClient, type RoomMember, type Room, type User, type Device } from "matrix-js-sdk/src/matrix";
2025-02-05 13:25:06 +00:00
import { type UserVerificationStatus, type VerificationRequest, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
2020-09-29 10:10:32 +01:00
import Modal from "../../../Modal";
2025-10-20 08:13:20 +02:00
import { _t } from "../../../languageHandler";
import { type ButtonEvent } from "../elements/AccessibleButton";
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 { 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 QuestionDialog from "../dialogs/QuestionDialog";
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";
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
2025-10-20 08:13:20 +02:00
import { UserInfoBasicView } from "./user_info/UserInfoBasicView";
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;
});
}
}
};
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 interface IRoomPermissions {
2020-09-29 10:10:32 +01:00
modifyLevelMax: number;
canEdit: boolean;
canInvite: boolean;
}
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;
};
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;
onClose(this: void): 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:
2025-10-20 08:13:20 +02:00
content = <UserInfoBasicView 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;