diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 6c6d8700a5..320128d767 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -202,13 +202,19 @@ export default class RightPanel extends React.Component { dis.dispatch({ action: "view_home_page", }); + } else if (this.state.phase === RightPanelPhases.EncryptionPanel && + this.state.verificationRequest && this.state.verificationRequest.pending + ) { + // When the user clicks close on the encryption panel cancel the pending request first if any + this.state.verificationRequest.cancel(); } else { // Otherwise we have got our user from RoomViewStore which means we're being shown // within a room/group, so go back to the member panel if we were in the encryption panel, // or the member list if we were in the member panel... phew. + const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel; dis.dispatch({ action: Action.ViewUser, - member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null, + member: isEncryptionPhase ? this.state.member : null, }); } }; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 8fd51d3715..60b043016b 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -23,7 +23,7 @@ import {Action} from "../../../dispatcher/actions"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import BaseAvatar from "./BaseAvatar"; -interface IProps { +interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember; fallbackUserId?: string; width: number; diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 3e95da1bc1..5927c7c3cc 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -31,6 +31,7 @@ interface IProps { className?: string; withoutScrollContainer?: boolean; previousPhase?: RightPanelPhases; + closeLabel?: string; onClose?(): void; } @@ -47,6 +48,7 @@ export const Group: React.FC = ({ className, title, children }) => }; const BaseCard: React.FC = ({ + closeLabel, onClose, className, header, @@ -68,7 +70,11 @@ const BaseCard: React.FC = ({ let closeButton; if (onClose) { - closeButton = ; + closeButton = ; } if (!withoutScrollContainer) { diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index df52e5cabd..c237a4ade6 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -27,6 +27,9 @@ import * as sdk from "../../../index"; import {_t} from "../../../languageHandler"; import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import dis from "../../../dispatcher/dispatcher"; +import {Action} from "../../../dispatcher/actions"; +import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; @@ -42,7 +45,14 @@ interface IProps { } const EncryptionPanel: React.FC = (props: IProps) => { - const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props; + const { + verificationRequest, + verificationRequestPromise, + member, + onClose, + layout, + isRoomEncrypted, + } = props; const [request, setRequest] = useState(verificationRequest); // state to show a spinner immediately after clicking "start verification", // before we have a request @@ -95,22 +105,6 @@ const EncryptionPanel: React.FC = (props: IProps) => { }, [onClose, request]); useEventEmitter(request, "change", changeHandler); - const onCancel = useCallback(function() { - if (request) { - request.cancel(); - } - }, [request]); - - let cancelButton: JSX.Element; - if (layout !== "dialog" && request && request.pending) { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - cancelButton = (); - } - const onStartVerification = useCallback(async () => { setRequesting(true); const cli = MatrixClientPeg.get(); @@ -118,7 +112,13 @@ const EncryptionPanel: React.FC = (props: IProps) => { const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId); setRequest(verificationRequest_); setPhase(verificationRequest_.phase); - }, [member.userId]); + // Notify the RightPanelStore about this + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.EncryptionPanel, + refireParams: { member, verificationRequest: verificationRequest_ }, + }); + }, [member]); const requested = (!request && isRequesting) || @@ -128,8 +128,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { member.userId === MatrixClientPeg.get().getUserId(); if (!request || requested) { const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe); - return ( - {cancelButton} + return ( = (props: IProps) => { waitingForOtherParty={requested && initiatedByMe} waitingForNetwork={requested && !initiatedByMe} inDialog={layout === "dialog"} /> - ); + ); } else { - return ( - {cancelButton} + return ( = (props: IProps) => { inDialog={layout === "dialog"} phase={phase} /> - ); + ); } }; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.tsx similarity index 85% rename from src/components/views/right_panel/UserInfo.js rename to src/components/views/right_panel/UserInfo.tsx index 8440532b9d..ecb47e9906 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,20 +17,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react'; -import PropTypes from 'prop-types'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; 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 Modal from '../../../Modal'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import {_t} from '../../../languageHandler'; import createRoom, {privateShouldBeEncrypted} from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; -import {EventTimeline} from "matrix-js-sdk"; import RoomViewStore from "../../../stores/RoomViewStore"; import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; @@ -41,13 +43,31 @@ import {textualPowerLevel} from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; -import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; -import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification'; +import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; +import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; import {Action} from "../../../dispatcher/actions"; import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; 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); for (let i = 0; i < devices.length; i++) { 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 userTrust = cli.checkUserTrust(userId); if (!userTrust.isCrossSigningVerified()) { - return userTrust.wasCrossSigningVerified() ? "warning" : "normal"; + return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal; } const anyDeviceUnverified = devices.some(device => { @@ -81,10 +101,10 @@ export const getE2EStatus = (cli, userId, devices) => { const deviceTrust = cli.checkDeviceTrust(userId, deviceId); 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 lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const room = matrixClient.getRoom(roomId); @@ -107,6 +127,7 @@ async function openDMForUser(matrixClient, userId) { const createRoomOptions = { dmUserId: userId, + encryption: undefined, }; 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 () => { if (!canVerify) { return undefined; @@ -142,7 +165,7 @@ function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) { }, [cli, member, canVerify], undefined); } -function DeviceItem({userId, device}) { +function DeviceItem({userId, device}: {userId: string, device: IDevice}) { const cli = useContext(MatrixClientContext); const isMe = userId === cli.getUserId(); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); @@ -169,8 +192,8 @@ function DeviceItem({userId, device}) { }; const deviceName = device.ambiguous ? - (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : - device.getDisplayName(); + (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : + device.getDisplayName(); let trustedLabel = null; if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); @@ -198,8 +221,7 @@ function DeviceItem({userId, device}) { } } -function DevicesSection({devices, userId, loading}) { - const Spinner = sdk.getComponent("elements.Spinner"); +function DevicesSection({devices, userId, loading}: {devices: IDevice[], userId: string, loading: boolean}) { const cli = useContext(MatrixClientContext); const userTrust = cli.checkUserTrust(userId); @@ -210,7 +232,7 @@ function DevicesSection({devices, userId, loading}) { return ; } if (devices === null) { - return _t("Unable to load session list"); + return <>{_t("Unable to load session list")}; } const isMe = userId === cli.getUserId(); 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); let ignoreButton = null; @@ -296,7 +322,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { const isMe = member.userId === cli.getUserId(); const onShareUserClick = () => { - const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { target: member, }); @@ -318,7 +343,10 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { }; ignoreButton = ( - + { isIgnored ? _t("Unignore") : _t("Ignore") } ); @@ -367,7 +395,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { } }); } catch (err) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t('Failed to invite'), description: ((err && err.message) ? err.message : _t("Operation failed")), @@ -413,8 +440,7 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { ); }; -const _warnSelfDemote = async () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); +const warnSelfDemote = async () => { const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { title: _t("Demote yourself?"), description: @@ -430,7 +456,7 @@ const _warnSelfDemote = async () => { return confirmed; }; -const GenericAdminToolsContainer = ({children}) => { +const GenericAdminToolsContainer: React.FC<{}> = ({children}) => { return (

{ _t("Admin Tools") }

@@ -441,7 +467,20 @@ const GenericAdminToolsContainer = ({children}) => { ); }; -const _isMuted = (member, powerLevelContent) => { +interface IPowerLevelsContent { + events?: Record; + // 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; const levelToSend = ( @@ -451,8 +490,8 @@ const _isMuted = (member, powerLevelContent) => { return member.powerLevel < levelToSend; }; -export const useRoomPowerLevels = (cli, room) => { - const [powerLevels, setPowerLevels] = useState({}); +export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { + const [powerLevels, setPowerLevels] = useState({}); const update = useCallback(() => { if (!room) { @@ -479,14 +518,19 @@ export const useRoomPowerLevels = (cli, room) => { return powerLevels; }; -const RoomKickButton = ({member, startUpdating, stopUpdating}) => { +interface IBaseProps { + member: RoomMember; + startUpdating(): void; + stopUpdating(): void; +} + +const RoomKickButton: React.FC = ({member, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext); // check if user can be kicked/disinvited if (member.membership !== "invite" && member.membership !== "join") return null; const onKick = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( 'Confirm User Action Dialog', 'onKick', @@ -509,7 +553,6 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => { // get out of sync if we force setState here! console.log("Kick success"); }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { title: _t("Failed to kick"), @@ -526,7 +569,7 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => { ; }; -const RedactMessagesButton = ({member}) => { +const RedactMessagesButton: React.FC = ({member}) => { const cli = useContext(MatrixClientContext); const onRedactAllMessages = async () => { @@ -554,7 +597,6 @@ const RedactMessagesButton = ({member}) => { const user = member.name; if (count === 0) { - const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { title: _t("No recent messages by %(user)s found", {user}), description: @@ -563,14 +605,14 @@ const RedactMessagesButton = ({member}) => {
, }); } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { title: _t("Remove recent messages by %(user)s", {user}), description:
-

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

-

{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+

{ _t("You are about to remove %(count)s messages by %(user)s. " + + "This cannot be undone. Do you wish to continue?", {count, user}) }

+

{ _t("For a large amount of messages, this might take some time. " + + "Please don't refresh your client in the meantime.") }

, button: _t("Remove %(count)s messages", {count}), }); @@ -603,11 +645,10 @@ const RedactMessagesButton = ({member}) => {
; }; -const BanToggleButton = ({member, startUpdating, stopUpdating}) => { +const BanToggleButton: React.FC = ({member, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext); const onBanOrUnban = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( 'Confirm User Action Dialog', 'onBanOrUnban', @@ -636,7 +677,6 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => { // get out of sync if we force setState here! console.log("Ban success"); }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Ban error: " + err); Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { title: _t("Error"), @@ -661,22 +701,26 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
; }; -const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => { +interface IBaseRoomProps extends IBaseProps { + room: Room; + powerLevels: IPowerLevelsContent; +} + +const MuteToggleButton: React.FC = ({member, room, powerLevels, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext); // Don't show the mute/unmute option if the user is not in the room if (member.membership !== "join") return null; - const isMuted = _isMuted(member, powerLevels); + const muted = isMuted(member, powerLevels); const onMuteToggle = async () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomId = member.roomId; const target = member.userId; // if muting self, warn as it may be irreversible if (target === cli.getUserId()) { try { - if (!(await _warnSelfDemote())) return; + if (!(await warnSelfDemote())) return; } catch (e) { console.error("Failed to warn about self demotion: ", e); return; @@ -692,7 +736,7 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin powerLevels.events_default ); let level; - if (isMuted) { // unmute + if (muted) { // unmute level = levelToSend; } else { // mute level = levelToSend - 1; @@ -718,16 +762,23 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin }; 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 { muteLabel } ; }; -const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => { +const RoomAdminToolsContainer: React.FC = ({ + room, + children, + member, + startUpdating, + stopUpdating, + powerLevels, +}) => { const cli = useContext(MatrixClientContext); let kickButton; let banButton; @@ -786,7 +837,18 @@ const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpd return
; }; -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 [isPrivileged, setIsPrivileged] = useState(false); @@ -814,8 +876,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, }, [groupId, groupMember.userId]); if (isPrivileged) { - const _onKick = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const onKick = async () => { const {finished} = Modal.createDialog(ConfirmUserActionDialog, { matrixClient: cli, groupMember, @@ -836,7 +897,6 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, member: null, }); }).catch((e) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { title: _t('Error'), description: isInvited ? @@ -850,7 +910,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, }; const kickButton = ( - + { isInvited ? _t('Disinvite') : _t('Remove from community') } ); @@ -870,13 +930,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, return
; }; -const GroupMember = PropTypes.shape({ - userId: PropTypes.string.isRequired, - displayname: PropTypes.string, // XXX: GroupMember objects are inconsistent :(( - avatarUrl: PropTypes.string, -}); - -const useIsSynapseAdmin = (cli) => { +const useIsSynapseAdmin = (cli: MatrixClient) => { const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { cli.isSynapseAdministrator().then((isAdmin) => { @@ -888,14 +942,20 @@ const useIsSynapseAdmin = (cli) => { return isAdmin; }; -const useHomeserverSupportsCrossSigning = (cli) => { - return useAsyncMemo(async () => { +const useHomeserverSupportsCrossSigning = (cli: MatrixClient) => { + return useAsyncMemo(async () => { return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); }, [cli], false); }; -function useRoomPermissions(cli, room, user) { - const [roomPermissions, setRoomPermissions] = useState({ +interface IRoomPermissions { + modifyLevelMax: number; + canEdit: boolean; + canInvite: boolean; +} + +function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions { + const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, canEdit: false, @@ -940,7 +1000,7 @@ function useRoomPermissions(cli, room, user) { updateRoomPermissions(); return () => { setRoomPermissions({ - maximalPowerLevel: -1, + modifyLevelMax: -1, canEdit: false, canInvite: false, }); @@ -950,14 +1010,18 @@ function useRoomPermissions(cli, room, user) { 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); if (isEditing) { return ( setEditing(false)} />); } else { - const IconButton = sdk.getComponent('elements.IconButton'); const powerLevelUsersDefault = powerLevels.users_default || 0; const powerLevel = parseInt(user.powerLevel, 10); 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 [isUpdating, setIsUpdating] = useState(false); @@ -994,7 +1063,6 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { // get out of sync if we force setState here! console.log("Power change success"); }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change power level " + err); Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { title: _t("Error"), @@ -1025,12 +1093,10 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { } 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 (myUserId === target) { try { - if (!(await _warnSelfDemote())) return; + if (!(await warnSelfDemote())) return; } catch (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]; - if (parseInt(myPower) === parseInt(powerLevel)) { + if (parseInt(myPower) === powerLevel) { const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { title: _t("Warning!"), description: @@ -1062,12 +1128,9 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - const IconButton = sdk.getComponent('elements.IconButton'); - const Spinner = sdk.getComponent("elements.Spinner"); const buttonOrSpinner = isUpdating ? : ; - const PowerSelector = sdk.getComponent('elements.PowerSelector'); return (
{ ); }; -export const useDevices = (userId) => { +export const useDevices = (userId: string) => { const cli = useContext(MatrixClientContext); // 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; - async function _downloadDeviceList() { + async function downloadDeviceList() { try { await cli.downloadKeys([userId], true); const devices = cli.getStoredDevicesForUser(userId); @@ -1104,13 +1167,13 @@ export const useDevices = (userId) => { return; } - _disambiguateDevices(devices); + disambiguateDevices(devices); setDevices(devices); } catch (err) { setDevices(null); } } - _downloadDeviceList(); + downloadDeviceList(); // Handle being unmounted return () => { @@ -1153,7 +1216,13 @@ export const useDevices = (userId) => { 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 powerLevels = useRoomPowerLevels(cli, room); @@ -1186,7 +1255,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { const roomPermissions = useRoomPermissions(cli, room, member); const onSynapseDeactivate = useCallback(async () => { - const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { title: _t("Deactivate user?"), description: @@ -1207,7 +1275,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { console.error("Failed to deactivate user"); console.error(err); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { title: _t('Failed to deactivate user'), description: ((err && err.message) ? err.message : _t("Operation failed")), @@ -1260,8 +1327,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { } if (pendingUpdateCount > 0) { - const Loader = sdk.getComponent("elements.Spinner"); - spinner = ; + spinner = ; } 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, // to avoid showing a double spinner // We should ask for a design that includes all the different loading states here - const Spinner = sdk.getComponent('elements.Spinner'); verifyButton = ; } } @@ -1351,7 +1416,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { { securitySection } @@ -1362,7 +1426,12 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { ; }; -const UserInfoHeader = ({member, e2eStatus}) => { +type Member = User | RoomMember | GroupMember; + +const UserInfoHeader: React.FC<{ + member: Member; + e2eStatus: E2EStatus; +}> = ({member, e2eStatus}) => { const cli = useContext(MatrixClientContext); const onMemberAvatarClick = useCallback(() => { @@ -1370,7 +1439,6 @@ const UserInfoHeader = ({member, e2eStatus}) => { if (!avatarUrl) return; const httpUrl = cli.mxcUrlToHttp(avatarUrl); - const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, name: member.name, @@ -1379,7 +1447,6 @@ const UserInfoHeader = ({member, e2eStatus}) => { Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); }, [cli, member]); - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const avatarElement = (
@@ -1421,10 +1488,13 @@ const UserInfoHeader = ({member, e2eStatus}) => { let presenceLabel = null; if (showPresence) { - const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); - presenceLabel = ; + presenceLabel = ( + + ); } let statusLabel = null; @@ -1461,7 +1531,32 @@ const UserInfoHeader = ({member, e2eStatus}) => { ; }; -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 { + user: Member; + groupId: void; + room: Room; + phase: RightPanelPhases.EncryptionPanel; + onClose(): void; +} + +type Props = IProps | IPropsWithEncryptionPanel; + +const UserInfo: React.FC = ({ + user, + groupId, + room, + onClose, + phase = RightPanelPhases.RoomMemberInfo, + ...props +}) => { const cli = useContext(MatrixClientContext); // 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 ); @@ -1493,7 +1588,12 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb case RightPanelPhases.EncryptionPanel: classes.push("mx_UserInfo_smallAvatar"); content = ( - + } + member={member} + onClose={onClose} + isRoomEncrypted={isRoomEncrypted} + /> ); break; } @@ -1504,23 +1604,24 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb previousPhase = RightPanelPhases.RoomMemberList; } - const header = ; - return + let closeLabel = undefined; + if (phase === RightPanelPhases.EncryptionPanel) { + const verificationRequest = (props as React.ComponentProps).verificationRequest; + if (verificationRequest && verificationRequest.pending) { + closeLabel = _t("Cancel"); + } + } + + const header = ; + return { content } ; }; -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; diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index 11c7aca7f1..38c70de259 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -18,8 +18,8 @@ import {useState, useEffect, DependencyList} from 'react'; type Fn = () => Promise; -export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T) => { - const [value, setValue] = useState(initialValue); +export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T): T => { + const [value, setValue] = useState(initialValue); useEffect(() => { fn().then(setValue); }, deps); // eslint-disable-line react-hooks/exhaustive-deps