2019-10-07 16:52:50 +01:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2020-01-30 15:59:03 +00:00
|
|
|
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
|
2019-10-07 16:52:50 +01:00
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
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.
|
2019-10-07 16:52:50 +01:00
|
|
|
*/
|
|
|
|
|
|
2025-10-20 08:13:20 +02:00
|
|
|
import React, { type JSX, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
2019-10-07 16:52:50 +01:00
|
|
|
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
|
|
|
|
2019-10-07 16:52:50 +01:00
|
|
|
import Modal from "../../../Modal";
|
2025-10-20 08:13:20 +02:00
|
|
|
import { _t } from "../../../languageHandler";
|
2025-02-10 16:52:58 +05:30
|
|
|
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";
|
2020-01-24 16:16:46 +00:00
|
|
|
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";
|
2022-02-09 14:42:08 +00:00
|
|
|
import PosthogTrackers from "../../../PosthogTrackers";
|
2025-07-21 14:04:50 +02:00
|
|
|
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
|
2025-10-20 08:13:20 +02:00
|
|
|
import { UserInfoBasicView } from "./user_info/UserInfoBasicView";
|
2025-02-04 14:35:48 +05:30
|
|
|
|
2023-04-26 12:23:32 +02:00
|
|
|
export interface IDevice extends Device {
|
2020-09-29 10:10:32 +01:00
|
|
|
ambiguous?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
export const disambiguateDevices = (devices: IDevice[]): void => {
|
2019-10-07 16:52:50 +01:00
|
|
|
const names = Object.create(null);
|
|
|
|
|
for (let i = 0; i < devices.length; i++) {
|
2023-04-26 12:23:32 +02:00
|
|
|
const name = devices[i].displayName ?? "";
|
2019-10-07 16:52:50 +01:00
|
|
|
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) => {
|
2019-10-07 16:52:50 +01:00
|
|
|
devices[j].ambiguous = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-07-11 13:53:33 +01:00
|
|
|
export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
2023-02-28 10:31:48 +00:00
|
|
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
2023-09-29 08:49:26 +01:00
|
|
|
title: _t("user_info|demote_self_confirm_title"),
|
2019-10-07 16:52:50 +01:00
|
|
|
description: (
|
|
|
|
|
<div>
|
2021-03-02 09:51:11 +00:00
|
|
|
{isSpace
|
2023-09-29 08:49:26 +01:00
|
|
|
? _t("user_info|demote_self_confirm_description_space")
|
|
|
|
|
: _t("user_info|demote_self_confirm_room")}
|
2019-10-07 16:52:50 +01:00
|
|
|
</div>
|
|
|
|
|
),
|
2023-09-29 08:49:26 +01:00
|
|
|
button: _t("user_info|demote_button"),
|
2019-10-07 16:52:50 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [confirmed] = await finished;
|
2023-03-14 11:09:35 +00:00
|
|
|
return !!confirmed;
|
2019-10-07 16:52:50 +01:00
|
|
|
};
|
|
|
|
|
|
2025-07-21 14:04:50 +02:00
|
|
|
export const Container: React.FC<{
|
2023-03-08 13:28:07 +00:00
|
|
|
children: ReactNode;
|
2025-02-04 14:35:48 +05:30
|
|
|
className?: string;
|
|
|
|
|
}> = ({ children, className }) => {
|
|
|
|
|
const classes = classNames("mx_UserInfo_container", className);
|
|
|
|
|
return <div className={classes}>{children}</div>;
|
2019-10-07 16:52:50 +01:00
|
|
|
};
|
|
|
|
|
|
2025-06-13 09:08:29 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 15:26:37 +02:00
|
|
|
export interface IRoomPermissions {
|
2020-09-29 10:10:32 +01:00
|
|
|
modifyLevelMax: number;
|
|
|
|
|
canEdit: boolean;
|
|
|
|
|
canInvite: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-26 12:23:32 +02:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
export const useDevices = (userId: string): IDevice[] | undefined | null => {
|
2019-12-17 17:26:12 +00:00
|
|
|
const cli = useContext(MatrixClientContext);
|
2019-11-14 16:59:09 +01:00
|
|
|
|
2020-01-24 16:16:46 +00:00
|
|
|
// 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);
|
2020-01-24 16:16:46 +00:00
|
|
|
// Download device lists
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setDevices(undefined);
|
2019-11-14 16:59:09 +01:00
|
|
|
|
2020-01-24 16:16:46 +00:00
|
|
|
let cancelled = false;
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
async function downloadDeviceList(): Promise<void> {
|
2020-01-24 16:16:46 +00:00
|
|
|
try {
|
2023-04-26 12:23:32 +02:00
|
|
|
const devices = await getUserDeviceInfo(userId, cli, true);
|
2020-01-24 16:16:46 +00:00
|
|
|
|
2023-04-26 12:23:32 +02:00
|
|
|
if (cancelled || !devices) {
|
2020-01-24 16:16:46 +00:00
|
|
|
// we got cancelled - presumably a different user now
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 10:31:48 +00:00
|
|
|
disambiguateDevices(devices);
|
|
|
|
|
setDevices(devices);
|
2024-10-16 16:43:07 +01:00
|
|
|
} catch {
|
2020-01-24 16:16:46 +00:00
|
|
|
setDevices(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-29 10:10:32 +01:00
|
|
|
downloadDeviceList();
|
2020-01-24 16:16:46 +00:00
|
|
|
|
|
|
|
|
// Handle being unmounted
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, [cli, userId]);
|
|
|
|
|
|
|
|
|
|
// Listen to changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancel = false;
|
2023-01-12 13:25:14 +00:00
|
|
|
const updateDevices = async (): Promise<void> => {
|
2023-04-26 12:23:32 +02:00
|
|
|
const newDevices = await getUserDeviceInfo(userId, cli);
|
|
|
|
|
if (cancel || !newDevices) return;
|
2023-02-28 10:31:48 +00:00
|
|
|
setDevices(newDevices);
|
2020-01-24 16:16:46 +00:00
|
|
|
};
|
2023-01-12 13:25:14 +00:00
|
|
|
const onDevicesUpdated = (users: string[]): void => {
|
2020-01-29 21:55:27 +00:00
|
|
|
if (!users.includes(userId)) return;
|
|
|
|
|
updateDevices();
|
|
|
|
|
};
|
2024-07-04 16:50:07 +01:00
|
|
|
const onUserTrustStatusChanged = (_userId: string, trustLevel: UserVerificationStatus): void => {
|
2020-01-29 21:55:27 +00:00
|
|
|
if (_userId !== userId) return;
|
|
|
|
|
updateDevices();
|
|
|
|
|
};
|
2022-02-22 12:18:08 +00:00
|
|
|
cli.on(CryptoEvent.DevicesUpdated, onDevicesUpdated);
|
|
|
|
|
cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
|
2020-01-24 16:16:46 +00:00
|
|
|
// Handle being unmounted
|
|
|
|
|
return () => {
|
|
|
|
|
cancel = true;
|
2022-02-22 12:18:08 +00:00
|
|
|
cli.removeListener(CryptoEvent.DevicesUpdated, onDevicesUpdated);
|
|
|
|
|
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
|
2020-01-24 16:16:46 +00:00
|
|
|
};
|
|
|
|
|
}, [cli, userId]);
|
|
|
|
|
|
|
|
|
|
return devices;
|
|
|
|
|
};
|
|
|
|
|
|
2022-03-22 17:07:37 -06:00
|
|
|
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;
|
2026-02-23 15:37:58 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2024-11-25 17:29:32 +00: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-23 16:09:32 +00:00
|
|
|
|
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-14 16:59:09 +01:00
|
|
|
|
2019-11-13 12:09:20 +01:00
|
|
|
const isRoomEncrypted = useIsEncrypted(cli, room);
|
2023-03-22 12:15:26 +00:00
|
|
|
const devices = useDevices(user.userId) ?? [];
|
2019-11-13 15:59:49 +01:00
|
|
|
|
2020-01-24 16:16:46 +00:00
|
|
|
const classes = ["mx_UserInfo"];
|
2020-01-23 16:09:32 +00:00
|
|
|
|
2023-01-11 10:46:35 +00:00
|
|
|
let cardState: IRightPanelCardState = {};
|
2022-03-22 17:07:37 -06:00
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
const onEncryptionPanelClose = (): void => {
|
2022-01-05 17:25:41 +01:00
|
|
|
RightPanelStore.instance.popCard();
|
2021-06-29 13:11:58 +01:00
|
|
|
};
|
2021-01-30 10:08:38 +01:00
|
|
|
|
2023-02-28 10:31:48 +00:00
|
|
|
let content: JSX.Element | undefined;
|
2020-01-24 16:16:46 +00:00
|
|
|
switch (phase) {
|
2024-11-25 17:29:32 +00:00
|
|
|
case RightPanelPhases.MemberInfo:
|
2025-10-20 08:13:20 +02:00
|
|
|
content = <UserInfoBasicView room={room as Room} member={member as User} />;
|
2020-01-24 16:16:46 +00:00
|
|
|
break;
|
2020-07-18 16:38:20 +05:30
|
|
|
case RightPanelPhases.EncryptionPanel:
|
2020-01-24 16:16:46 +00:00
|
|
|
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
|
|
|
/>
|
2020-01-24 16:16:46 +00:00
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-10-07 16:52:50 +01:00
|
|
|
|
2023-03-14 11:09:35 +00:00
|
|
|
let closeLabel: string | undefined;
|
2020-09-29 10:11:04 +01:00
|
|
|
if (phase === RightPanelPhases.EncryptionPanel) {
|
|
|
|
|
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
|
|
|
|
|
if (verificationRequest && verificationRequest.pending) {
|
2023-08-23 11:57:22 +01:00
|
|
|
closeLabel = _t("action|cancel");
|
2020-09-29 10:11:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-08 13:14:52 +01:00
|
|
|
const header = (
|
|
|
|
|
<>
|
2025-07-21 14:04:50 +02:00
|
|
|
<UserInfoHeaderView
|
2025-02-10 16:52:58 +05:30
|
|
|
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
|
|
|
|
2020-09-29 10:11:04 +01:00
|
|
|
return (
|
|
|
|
|
<BaseCard
|
|
|
|
|
className={classes.join(" ")}
|
2024-07-17 13:54:35 +01:00
|
|
|
header={_t("common|profile")}
|
2020-09-29 10:11:04 +01:00
|
|
|
onClose={onClose}
|
|
|
|
|
closeLabel={closeLabel}
|
2022-01-05 16:14:44 +01:00
|
|
|
cardState={cardState}
|
2022-02-09 14:42:08 +00:00
|
|
|
onBack={(ev: ButtonEvent) => {
|
2024-11-25 17:29:32 +00:00
|
|
|
if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) {
|
2022-02-09 14:42:08 +00:00
|
|
|
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2020-09-29 10:11:04 +01:00
|
|
|
>
|
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
|
|
|
};
|
2019-10-07 16:52:50 +01:00
|
|
|
|
|
|
|
|
export default UserInfo;
|