Convert UserInfo to Typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-09-29 10:10:32 +01:00
parent 8bf4ef5766
commit f945155d04
3 changed files with 203 additions and 116 deletions

View file

@ -23,7 +23,7 @@ import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import BaseAvatar from "./BaseAvatar"; import BaseAvatar from "./BaseAvatar";
interface IProps { interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember; member: RoomMember;
fallbackUserId?: string; fallbackUserId?: string;
width: number; width: number;

View file

@ -17,20 +17,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import {Group, RoomMember, User, Room} from 'matrix-js-sdk'; import {MatrixClient} from 'matrix-js-sdk/src/client';
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
import {User} from 'matrix-js-sdk/src/models/user';
import {Room} from 'matrix-js-sdk/src/models/room';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as sdk from '../../../index'; import {_t} from '../../../languageHandler';
import { _t } from '../../../languageHandler';
import createRoom, {privateShouldBeEncrypted} from '../../../createRoom'; import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {EventTimeline} from "matrix-js-sdk";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter"; import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore"; import GroupStore from "../../../stores/GroupStore";
@ -41,13 +43,31 @@ import {textualPowerLevel} from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel"; import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification'; import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard"; import BaseCard from "./BaseCard";
import {E2EStatus} from "../../../utils/ShieldUtils";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import IconButton from "../elements/IconButton";
import PowerSelector from "../elements/PowerSelector";
import MemberAvatar from "../avatars/MemberAvatar";
import PresenceLabel from "../rooms/PresenceLabel";
import ShareDialog from "../dialogs/ShareDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
import InfoDialog from "../dialogs/InfoDialog";
const _disambiguateDevices = (devices) => { interface IDevice {
deviceId: string;
ambiguous?: boolean;
getDisplayName(): string;
}
const disambiguateDevices = (devices: IDevice[]) => {
const names = Object.create(null); const names = Object.create(null);
for (let i = 0; i < devices.length; i++) { for (let i = 0; i < devices.length; i++) {
const name = devices[i].getDisplayName(); const name = devices[i].getDisplayName();
@ -64,11 +84,11 @@ const _disambiguateDevices = (devices) => {
} }
}; };
export const getE2EStatus = (cli, userId, devices) => { export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice[]): E2EStatus => {
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const userTrust = cli.checkUserTrust(userId); const userTrust = cli.checkUserTrust(userId);
if (!userTrust.isCrossSigningVerified()) { if (!userTrust.isCrossSigningVerified()) {
return userTrust.wasCrossSigningVerified() ? "warning" : "normal"; return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal;
} }
const anyDeviceUnverified = devices.some(device => { const anyDeviceUnverified = devices.some(device => {
@ -81,10 +101,10 @@ export const getE2EStatus = (cli, userId, devices) => {
const deviceTrust = cli.checkDeviceTrust(userId, deviceId); const deviceTrust = cli.checkDeviceTrust(userId, deviceId);
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified();
}); });
return anyDeviceUnverified ? "warning" : "verified"; return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
}; };
async function openDMForUser(matrixClient, userId) { async function openDMForUser(matrixClient: MatrixClient, userId: string) {
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
const room = matrixClient.getRoom(roomId); const room = matrixClient.getRoom(roomId);
@ -107,6 +127,7 @@ async function openDMForUser(matrixClient, userId) {
const createRoomOptions = { const createRoomOptions = {
dmUserId: userId, dmUserId: userId,
encryption: undefined,
}; };
if (privateShouldBeEncrypted()) { if (privateShouldBeEncrypted()) {
@ -122,10 +143,12 @@ async function openDMForUser(matrixClient, userId) {
} }
} }
createRoom(createRoomOptions); return createRoom(createRoomOptions);
} }
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) { type SetUpdating = (updating: boolean) => void;
function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
return useAsyncMemo(async () => { return useAsyncMemo(async () => {
if (!canVerify) { if (!canVerify) {
return undefined; return undefined;
@ -142,7 +165,7 @@ function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
}, [cli, member, canVerify], undefined); }, [cli, member, canVerify], undefined);
} }
function DeviceItem({userId, device}) { function DeviceItem({userId, device}: {userId: string, device: IDevice}) {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
@ -198,8 +221,7 @@ function DeviceItem({userId, device}) {
} }
} }
function DevicesSection({devices, userId, loading}) { function DevicesSection({devices, userId, loading}: {devices: IDevice[], userId: string, loading: boolean}) {
const Spinner = sdk.getComponent("elements.Spinner");
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const userTrust = cli.checkUserTrust(userId); const userTrust = cli.checkUserTrust(userId);
@ -210,7 +232,7 @@ function DevicesSection({devices, userId, loading}) {
return <Spinner />; return <Spinner />;
} }
if (devices === null) { if (devices === null) {
return _t("Unable to load session list"); return <>{_t("Unable to load session list")}</>;
} }
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId)); const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
@ -285,7 +307,11 @@ function DevicesSection({devices, userId, loading}) {
); );
} }
const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { const UserOptionsSection: React.FC<{
member: RoomMember;
isIgnored: boolean;
canInvite: boolean;
}> = ({member, isIgnored, canInvite}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
let ignoreButton = null; let ignoreButton = null;
@ -296,7 +322,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
const isMe = member.userId === cli.getUserId(); const isMe = member.userId === cli.getUserId();
const onShareUserClick = () => { const onShareUserClick = () => {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
target: member, target: member,
}); });
@ -318,7 +343,10 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
}; };
ignoreButton = ( ignoreButton = (
<AccessibleButton onClick={onIgnoreToggle} className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}> <AccessibleButton
onClick={onIgnoreToggle}
className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}
>
{ isIgnored ? _t("Unignore") : _t("Ignore") } { isIgnored ? _t("Unignore") : _t("Ignore") }
</AccessibleButton> </AccessibleButton>
); );
@ -367,7 +395,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
} }
}); });
} catch (err) { } catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t('Failed to invite'), title: _t('Failed to invite'),
description: ((err && err.message) ? err.message : _t("Operation failed")), description: ((err && err.message) ? err.message : _t("Operation failed")),
@ -413,8 +440,7 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
); );
}; };
const _warnSelfDemote = async () => { const warnSelfDemote = async () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Demote yourself?"), title: _t("Demote yourself?"),
description: description:
@ -430,7 +456,7 @@ const _warnSelfDemote = async () => {
return confirmed; return confirmed;
}; };
const GenericAdminToolsContainer = ({children}) => { const GenericAdminToolsContainer: React.FC<{}> = ({children}) => {
return ( return (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
<h3>{ _t("Admin Tools") }</h3> <h3>{ _t("Admin Tools") }</h3>
@ -441,7 +467,20 @@ const GenericAdminToolsContainer = ({children}) => {
); );
}; };
const _isMuted = (member, powerLevelContent) => { interface IPowerLevelsContent {
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;
}
const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
if (!powerLevelContent || !member) return false; if (!powerLevelContent || !member) return false;
const levelToSend = ( const levelToSend = (
@ -451,8 +490,8 @@ const _isMuted = (member, powerLevelContent) => {
return member.powerLevel < levelToSend; return member.powerLevel < levelToSend;
}; };
export const useRoomPowerLevels = (cli, room) => { export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
const [powerLevels, setPowerLevels] = useState({}); const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
const update = useCallback(() => { const update = useCallback(() => {
if (!room) { if (!room) {
@ -479,14 +518,19 @@ export const useRoomPowerLevels = (cli, room) => {
return powerLevels; return powerLevels;
}; };
const RoomKickButton = ({member, startUpdating, stopUpdating}) => { interface IBaseProps {
member: RoomMember;
startUpdating(): void;
stopUpdating(): void;
}
const RoomKickButton: React.FC<IBaseProps> = ({member, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// check if user can be kicked/disinvited // check if user can be kicked/disinvited
if (member.membership !== "invite" && member.membership !== "join") return null; if (member.membership !== "invite" && member.membership !== "join") return null;
const onKick = async () => { const onKick = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createTrackedDialog( const {finished} = Modal.createTrackedDialog(
'Confirm User Action Dialog', 'Confirm User Action Dialog',
'onKick', 'onKick',
@ -509,7 +553,6 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Kick success"); console.log("Kick success");
}, function(err) { }, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err); console.error("Kick error: " + err);
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
title: _t("Failed to kick"), title: _t("Failed to kick"),
@ -526,7 +569,7 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
</AccessibleButton>; </AccessibleButton>;
}; };
const RedactMessagesButton = ({member}) => { const RedactMessagesButton: React.FC<IBaseProps> = ({member}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const onRedactAllMessages = async () => { const onRedactAllMessages = async () => {
@ -554,7 +597,6 @@ const RedactMessagesButton = ({member}) => {
const user = member.name; const user = member.name;
if (count === 0) { if (count === 0) {
const InfoDialog = sdk.getComponent("dialogs.InfoDialog");
Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, {
title: _t("No recent messages by %(user)s found", {user}), title: _t("No recent messages by %(user)s found", {user}),
description: description:
@ -563,14 +605,14 @@ const RedactMessagesButton = ({member}) => {
</div>, </div>,
}); });
} else { } else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, {
title: _t("Remove recent messages by %(user)s", {user}), title: _t("Remove recent messages by %(user)s", {user}),
description: description:
<div> <div>
<p>{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }</p> <p>{ _t("You are about to remove %(count)s messages by %(user)s. " +
<p>{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }</p> "This cannot be undone. Do you wish to continue?", {count, user}) }</p>
<p>{ _t("For a large amount of messages, this might take some time. " +
"Please don't refresh your client in the meantime.") }</p>
</div>, </div>,
button: _t("Remove %(count)s messages", {count}), button: _t("Remove %(count)s messages", {count}),
}); });
@ -603,11 +645,10 @@ const RedactMessagesButton = ({member}) => {
</AccessibleButton>; </AccessibleButton>;
}; };
const BanToggleButton = ({member, startUpdating, stopUpdating}) => { const BanToggleButton: React.FC<IBaseProps> = ({member, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const onBanOrUnban = async () => { const onBanOrUnban = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createTrackedDialog( const {finished} = Modal.createTrackedDialog(
'Confirm User Action Dialog', 'Confirm User Action Dialog',
'onBanOrUnban', 'onBanOrUnban',
@ -636,7 +677,6 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Ban success"); console.log("Ban success");
}, function(err) { }, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Ban error: " + err); console.error("Ban error: " + err);
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
title: _t("Error"), title: _t("Error"),
@ -661,22 +701,26 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
</AccessibleButton>; </AccessibleButton>;
}; };
const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => { interface IBaseRoomProps extends IBaseProps {
room: Room;
powerLevels: IPowerLevelsContent;
}
const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// Don't show the mute/unmute option if the user is not in the room // Don't show the mute/unmute option if the user is not in the room
if (member.membership !== "join") return null; if (member.membership !== "join") return null;
const isMuted = _isMuted(member, powerLevels); const muted = isMuted(member, powerLevels);
const onMuteToggle = async () => { const onMuteToggle = async () => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = member.roomId; const roomId = member.roomId;
const target = member.userId; const target = member.userId;
// if muting self, warn as it may be irreversible // if muting self, warn as it may be irreversible
if (target === cli.getUserId()) { if (target === cli.getUserId()) {
try { try {
if (!(await _warnSelfDemote())) return; if (!(await warnSelfDemote())) return;
} catch (e) { } catch (e) {
console.error("Failed to warn about self demotion: ", e); console.error("Failed to warn about self demotion: ", e);
return; return;
@ -692,7 +736,7 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin
powerLevels.events_default powerLevels.events_default
); );
let level; let level;
if (isMuted) { // unmute if (muted) { // unmute
level = levelToSend; level = levelToSend;
} else { // mute } else { // mute
level = levelToSend - 1; level = levelToSend - 1;
@ -718,16 +762,23 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin
}; };
const classes = classNames("mx_UserInfo_field", { const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: !isMuted, mx_UserInfo_destructive: !muted,
}); });
const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); const muteLabel = muted ? _t("Unmute") : _t("Mute");
return <AccessibleButton className={classes} onClick={onMuteToggle}> return <AccessibleButton className={classes} onClick={onMuteToggle}>
{ muteLabel } { muteLabel }
</AccessibleButton>; </AccessibleButton>;
}; };
const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => { const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
room,
children,
member,
startUpdating,
stopUpdating,
powerLevels,
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
let kickButton; let kickButton;
let banButton; let banButton;
@ -786,7 +837,18 @@ const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpd
return <div />; return <div />;
}; };
const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, stopUpdating}) => { interface GroupMember {
userId: string;
displayname?: string; // XXX: GroupMember objects are inconsistent :((
avatarUrl?: string;
}
const GroupAdminToolsSection: React.FC<{
groupId: string;
groupMember: GroupMember;
startUpdating(): void;
stopUpdating(): void;
}> = ({children, groupId, groupMember, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const [isPrivileged, setIsPrivileged] = useState(false); const [isPrivileged, setIsPrivileged] = useState(false);
@ -814,8 +876,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
}, [groupId, groupMember.userId]); }, [groupId, groupMember.userId]);
if (isPrivileged) { if (isPrivileged) {
const _onKick = async () => { const onKick = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createDialog(ConfirmUserActionDialog, { const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: cli, matrixClient: cli,
groupMember, groupMember,
@ -836,7 +897,6 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
member: null, member: null,
}); });
}).catch((e) => { }).catch((e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
description: isInvited ? description: isInvited ?
@ -850,7 +910,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
}; };
const kickButton = ( const kickButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}> <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
{ isInvited ? _t('Disinvite') : _t('Remove from community') } { isInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton> </AccessibleButton>
); );
@ -870,13 +930,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
return <div />; return <div />;
}; };
const GroupMember = PropTypes.shape({ const useIsSynapseAdmin = (cli: MatrixClient) => {
userId: PropTypes.string.isRequired,
displayname: PropTypes.string, // XXX: GroupMember objects are inconsistent :((
avatarUrl: PropTypes.string,
});
const useIsSynapseAdmin = (cli) => {
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => { useEffect(() => {
cli.isSynapseAdministrator().then((isAdmin) => { cli.isSynapseAdministrator().then((isAdmin) => {
@ -888,14 +942,20 @@ const useIsSynapseAdmin = (cli) => {
return isAdmin; return isAdmin;
}; };
const useHomeserverSupportsCrossSigning = (cli) => { const useHomeserverSupportsCrossSigning = (cli: MatrixClient) => {
return useAsyncMemo(async () => { return useAsyncMemo<boolean>(async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
}, [cli], false); }, [cli], false);
}; };
function useRoomPermissions(cli, room, user) { interface IRoomPermissions {
const [roomPermissions, setRoomPermissions] = useState({ modifyLevelMax: number;
canEdit: boolean;
canInvite: boolean;
}
function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
modifyLevelMax: -1, modifyLevelMax: -1,
canEdit: false, canEdit: false,
@ -940,7 +1000,7 @@ function useRoomPermissions(cli, room, user) {
updateRoomPermissions(); updateRoomPermissions();
return () => { return () => {
setRoomPermissions({ setRoomPermissions({
maximalPowerLevel: -1, modifyLevelMax: -1,
canEdit: false, canEdit: false,
canInvite: false, canInvite: false,
}); });
@ -950,14 +1010,18 @@ function useRoomPermissions(cli, room, user) {
return roomPermissions; return roomPermissions;
} }
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { const PowerLevelSection: React.FC<{
user: User;
room: Room;
roomPermissions: IRoomPermissions;
powerLevels: IPowerLevelsContent;
}> = ({user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
if (isEditing) { if (isEditing) {
return (<PowerLevelEditor return (<PowerLevelEditor
user={user} room={room} roomPermissions={roomPermissions} user={user} room={room} roomPermissions={roomPermissions}
onFinished={() => setEditing(false)} />); onFinished={() => setEditing(false)} />);
} else { } else {
const IconButton = sdk.getComponent('elements.IconButton');
const powerLevelUsersDefault = powerLevels.users_default || 0; const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = parseInt(user.powerLevel, 10); const powerLevel = parseInt(user.powerLevel, 10);
const modifyButton = roomPermissions.canEdit ? const modifyButton = roomPermissions.canEdit ?
@ -975,7 +1039,12 @@ const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
} }
}; };
const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { const PowerLevelEditor: React.FC<{
user: User;
room: Room;
roomPermissions: IRoomPermissions;
onFinished(): void;
}> = ({user, room, roomPermissions, onFinished}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
@ -994,7 +1063,6 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Power change success"); console.log("Power change success");
}, function(err) { }, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err); console.error("Failed to change power level " + err);
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
title: _t("Error"), title: _t("Error"),
@ -1025,12 +1093,10 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
} }
const myUserId = cli.getUserId(); const myUserId = cli.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse. // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) { if (myUserId === target) {
try { try {
if (!(await _warnSelfDemote())) return; if (!(await warnSelfDemote())) return;
} catch (e) { } catch (e) {
console.error("Failed to warn about self demotion: ", e); console.error("Failed to warn about self demotion: ", e);
} }
@ -1039,7 +1105,7 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
} }
const myPower = powerLevelEvent.getContent().users[myUserId]; const myPower = powerLevelEvent.getContent().users[myUserId];
if (parseInt(myPower) === parseInt(powerLevel)) { if (parseInt(myPower) === powerLevel) {
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
description: description:
@ -1062,12 +1128,9 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
const IconButton = sdk.getComponent('elements.IconButton');
const Spinner = sdk.getComponent("elements.Spinner");
const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> : const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
<IconButton icon="check" onClick={changePowerLevel} />; <IconButton icon="check" onClick={changePowerLevel} />;
const PowerSelector = sdk.getComponent('elements.PowerSelector');
return ( return (
<div className="mx_UserInfo_profileField"> <div className="mx_UserInfo_profileField">
<PowerSelector <PowerSelector
@ -1083,7 +1146,7 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
); );
}; };
export const useDevices = (userId) => { export const useDevices = (userId: string) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices // undefined means yet to be loaded, null means failed to load, otherwise list of devices
@ -1094,7 +1157,7 @@ export const useDevices = (userId) => {
let cancelled = false; let cancelled = false;
async function _downloadDeviceList() { async function downloadDeviceList() {
try { try {
await cli.downloadKeys([userId], true); await cli.downloadKeys([userId], true);
const devices = cli.getStoredDevicesForUser(userId); const devices = cli.getStoredDevicesForUser(userId);
@ -1104,13 +1167,13 @@ export const useDevices = (userId) => {
return; return;
} }
_disambiguateDevices(devices); disambiguateDevices(devices);
setDevices(devices); setDevices(devices);
} catch (err) { } catch (err) {
setDevices(null); setDevices(null);
} }
} }
_downloadDeviceList(); downloadDeviceList();
// Handle being unmounted // Handle being unmounted
return () => { return () => {
@ -1153,7 +1216,13 @@ export const useDevices = (userId) => {
return devices; return devices;
}; };
const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { const BasicUserInfo: React.FC<{
room: Room;
member: User | RoomMember;
groupId: string;
devices: IDevice[];
isRoomEncrypted: boolean;
}> = ({room, member, groupId, devices, isRoomEncrypted}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const powerLevels = useRoomPowerLevels(cli, room); const powerLevels = useRoomPowerLevels(cli, room);
@ -1186,7 +1255,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const roomPermissions = useRoomPermissions(cli, room, member); const roomPermissions = useRoomPermissions(cli, room, member);
const onSynapseDeactivate = useCallback(async () => { const onSynapseDeactivate = useCallback(async () => {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
title: _t("Deactivate user?"), title: _t("Deactivate user?"),
description: description:
@ -1207,7 +1275,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
console.error("Failed to deactivate user"); console.error("Failed to deactivate user");
console.error(err); console.error(err);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
title: _t('Failed to deactivate user'), title: _t('Failed to deactivate user'),
description: ((err && err.message) ? err.message : _t("Operation failed")), description: ((err && err.message) ? err.message : _t("Operation failed")),
@ -1260,8 +1327,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
} }
if (pendingUpdateCount > 0) { if (pendingUpdateCount > 0) {
const Loader = sdk.getComponent("elements.Spinner"); spinner = <Spinner imgClassName="mx_ContextualMenu_spinner" />;
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
} }
let memberDetails; let memberDetails;
@ -1324,7 +1390,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
// HACK: only show a spinner if the device section spinner is not shown, // HACK: only show a spinner if the device section spinner is not shown,
// to avoid showing a double spinner // to avoid showing a double spinner
// We should ask for a design that includes all the different loading states here // We should ask for a design that includes all the different loading states here
const Spinner = sdk.getComponent('elements.Spinner');
verifyButton = <Spinner />; verifyButton = <Spinner />;
} }
} }
@ -1351,7 +1416,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
{ securitySection } { securitySection }
<UserOptionsSection <UserOptionsSection
devices={devices}
canInvite={roomPermissions.canInvite} canInvite={roomPermissions.canInvite}
isIgnored={isIgnored} isIgnored={isIgnored}
member={member} /> member={member} />
@ -1362,7 +1426,12 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
</React.Fragment>; </React.Fragment>;
}; };
const UserInfoHeader = ({member, e2eStatus}) => { type Member = User | RoomMember | GroupMember;
const UserInfoHeader: React.FC<{
member: Member;
e2eStatus: E2EStatus;
}> = ({member, e2eStatus}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const onMemberAvatarClick = useCallback(() => { const onMemberAvatarClick = useCallback(() => {
@ -1370,7 +1439,6 @@ const UserInfoHeader = ({member, e2eStatus}) => {
if (!avatarUrl) return; if (!avatarUrl) return;
const httpUrl = cli.mxcUrlToHttp(avatarUrl); const httpUrl = cli.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: member.name, name: member.name,
@ -1379,7 +1447,6 @@ const UserInfoHeader = ({member, e2eStatus}) => {
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}, [cli, member]); }, [cli, member]);
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const avatarElement = ( const avatarElement = (
<div className="mx_UserInfo_avatar"> <div className="mx_UserInfo_avatar">
<div> <div>
@ -1421,10 +1488,13 @@ const UserInfoHeader = ({member, e2eStatus}) => {
let presenceLabel = null; let presenceLabel = null;
if (showPresence) { if (showPresence) {
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); presenceLabel = (
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo} <PresenceLabel
activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive} currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />; presenceState={presenceState}
/>
);
} }
let statusLabel = null; let statusLabel = null;
@ -1461,7 +1531,32 @@ const UserInfoHeader = ({member, e2eStatus}) => {
</React.Fragment>; </React.Fragment>;
}; };
const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => { interface IProps {
user: Member;
groupId?: string;
room?: Room;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
onClose(): void;
}
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
user: Member;
groupId: void;
room: Room;
phase: RightPanelPhases.EncryptionPanel;
onClose(): void;
}
type Props = IProps | IPropsWithEncryptionPanel;
const UserInfo: React.FC<Props> = ({
user,
groupId,
room,
onClose,
phase = RightPanelPhases.RoomMemberInfo,
...props
}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// fetch latest room member if we have a room, so we don't show historical information, falling back to user // fetch latest room member if we have a room, so we don't show historical information, falling back to user
@ -1485,7 +1580,7 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
<BasicUserInfo <BasicUserInfo
room={room} room={room}
member={member} member={member}
groupId={groupId} groupId={groupId as string}
devices={devices} devices={devices}
isRoomEncrypted={isRoomEncrypted} /> isRoomEncrypted={isRoomEncrypted} />
); );
@ -1493,7 +1588,12 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
case RightPanelPhases.EncryptionPanel: case RightPanelPhases.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar"); classes.push("mx_UserInfo_smallAvatar");
content = ( content = (
<EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} /> <EncryptionPanel
{...props as React.ComponentProps<typeof EncryptionPanel>}
member={member}
onClose={onClose}
isRoomEncrypted={isRoomEncrypted}
/>
); );
break; break;
} }
@ -1510,17 +1610,4 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
</BaseCard>; </BaseCard>;
}; };
UserInfo.propTypes = {
user: PropTypes.oneOfType([
PropTypes.instanceOf(User),
PropTypes.instanceOf(RoomMember),
GroupMember,
]).isRequired,
group: PropTypes.instanceOf(Group),
groupId: PropTypes.string,
room: PropTypes.instanceOf(Room),
onClose: PropTypes.func,
};
export default UserInfo; export default UserInfo;

View file

@ -18,8 +18,8 @@ import {useState, useEffect, DependencyList} from 'react';
type Fn<T> = () => Promise<T>; type Fn<T> = () => Promise<T>;
export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T) => { export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState<T>(initialValue);
useEffect(() => { useEffect(() => {
fn().then(setValue); fn().then(setValue);
}, deps); // eslint-disable-line react-hooks/exhaustive-deps }, deps); // eslint-disable-line react-hooks/exhaustive-deps