Apply a huge part of the decorations and copy
This commit is contained in:
parent
210616c737
commit
7a5e172b88
13 changed files with 586 additions and 418 deletions
|
@ -169,7 +169,6 @@ export default class RightPanel extends React.Component {
|
|||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
@ -187,19 +186,22 @@ export default class RightPanel extends React.Component {
|
|||
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo ||
|
||||
this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
userId={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
|
@ -215,7 +217,7 @@ export default class RightPanel extends React.Component {
|
|||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
userId={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={onClose} />;
|
||||
|
@ -237,19 +239,6 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: this.state.member,
|
||||
});
|
||||
};
|
||||
panel = (
|
||||
<EncryptionPanel
|
||||
member={this.state.member}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
onClose={onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
|
|
|
@ -45,10 +45,11 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
|
||||
_openRequest = () => {
|
||||
const {verificationRequest} = this.props.mxEvent;
|
||||
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {verificationRequest},
|
||||
refireParams: {verificationRequest, member},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,24 +14,58 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
export default class EncryptionInfo extends React.PureComponent {
|
||||
render() {
|
||||
export const PendingActionSpinner = ({text}) => {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <div className="mx_EncryptionInfo_spinner">
|
||||
<Spinner />
|
||||
{ text }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const EncryptionInfo = ({pending, member, onStartVerification}) => {
|
||||
let content;
|
||||
if (pending) {
|
||||
const text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
content = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify User")}</h3>
|
||||
<div>
|
||||
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
|
||||
<p>{_t("For maximum security, do this in person.")}</p>
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onStartVerification}>
|
||||
{_t("Start Verification")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
content = (
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={onStartVerification}>
|
||||
{_t("Start Verification")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Encryption")}</h3>
|
||||
<div>
|
||||
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
|
||||
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify User")}</h3>
|
||||
<div>
|
||||
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
|
||||
<p>{_t("For maximum security, do this in person.")}</p>
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
};
|
||||
EncryptionInfo.propTypes = {
|
||||
member: PropTypes.object.isRequired,
|
||||
onStartVerification: PropTypes.func.isRequired,
|
||||
request: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EncryptionInfo;
|
||||
|
|
|
@ -14,52 +14,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
import EncryptionInfo from "./EncryptionInfo";
|
||||
import VerificationPanel from "./VerificationPanel";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import {UserInfoPane} from "./UserInfo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
||||
export default class EncryptionPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
const EncryptionPanel = ({verificationRequest, member}) => {
|
||||
const [request, setRequest] = useState(verificationRequest);
|
||||
useEffect(() => {
|
||||
setRequest(verificationRequest);
|
||||
}, [verificationRequest]);
|
||||
|
||||
const [pending, setPending] = useState(false);
|
||||
const changeHandler = useCallback(() => {
|
||||
setPending(request && request.requested);
|
||||
}, [request]);
|
||||
useEventEmitter(request, "change", changeHandler);
|
||||
useEffect(changeHandler, [changeHandler]);
|
||||
|
||||
const onStartVerification = useCallback(async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, member.userId);
|
||||
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
|
||||
setRequest(verificationRequest);
|
||||
}, [member.userId]);
|
||||
|
||||
if (!request || pending) {
|
||||
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={pending} />;
|
||||
} else {
|
||||
return <VerificationPanel member={member} request={request} key={request.channel.transactionId} />;
|
||||
}
|
||||
};
|
||||
EncryptionPanel.propTypes = {
|
||||
|
||||
render() {
|
||||
let content;
|
||||
const request = this.props.verificationRequest || this.state.verificationRequest;
|
||||
const {member} = this.props;
|
||||
if (request) {
|
||||
content = <VerificationPanel request={request} key={request.channel.transactionId} />;
|
||||
} else if (member) {
|
||||
content = <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
|
||||
} else {
|
||||
content = <p>Not a member nor request, not sure what to render</p>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UserInfoPane className="mx_UserInfo_smallAvatar" member={member} onClose={this.props.onClose} e2eStatus="">
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("Encryption")}</h3>
|
||||
<div>
|
||||
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
|
||||
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ content }
|
||||
</UserInfoPane>
|
||||
);
|
||||
}
|
||||
|
||||
_onStartVerification = async () => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const {member} = this.props;
|
||||
const roomId = await ensureDMExists(client, member.userId);
|
||||
const verificationRequest = await client.requestVerificationDM(member.userId, roomId);
|
||||
this.setState({verificationRequest});
|
||||
};
|
||||
}
|
||||
export default EncryptionPanel;
|
||||
|
|
|
@ -41,6 +41,7 @@ import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
|||
import {textualPowerLevel} from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -1047,7 +1048,256 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const UserInfoPane = ({children, className, onClose, e2eStatus, member}) => {
|
||||
export const useDevices = (userId) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||
const [devices, setDevices] = useState(undefined);
|
||||
// Download device lists
|
||||
useEffect(() => {
|
||||
setDevices(undefined);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function _downloadDeviceList() {
|
||||
try {
|
||||
await cli.downloadKeys([userId], true);
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
|
||||
if (cancelled) {
|
||||
// we got cancelled - presumably a different user now
|
||||
return;
|
||||
}
|
||||
|
||||
_disambiguateDevices(devices);
|
||||
setDevices(devices);
|
||||
} catch (err) {
|
||||
setDevices(null);
|
||||
}
|
||||
}
|
||||
_downloadDeviceList();
|
||||
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cli, userId]);
|
||||
|
||||
// Listen to changes
|
||||
useEffect(() => {
|
||||
let cancel = false;
|
||||
const onDeviceVerificationChanged = (_userId, device) => {
|
||||
if (_userId === userId) {
|
||||
// no need to re-download the whole thing; just update our copy of the list.
|
||||
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed in future
|
||||
Promise.resolve(cli.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
if (cancel) return;
|
||||
console.log("setDevices 2", devices);
|
||||
setDevices(devices);
|
||||
});
|
||||
}
|
||||
};
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancel = true;
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
};
|
||||
}, [cli, userId]);
|
||||
|
||||
return devices;
|
||||
};
|
||||
|
||||
const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}, [cli, member.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}
|
||||
}, [cli, member.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
title: _t("Deactivate user?"),
|
||||
description:
|
||||
<div>{ _t(
|
||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, " +
|
||||
"they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " +
|
||||
"want to deactivate this user?",
|
||||
) }</div>,
|
||||
button: _t("Deactivate user"),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [accepted] = await finished;
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await cli.deactivateSynapseUser(member.userId);
|
||||
} catch (err) {
|
||||
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")),
|
||||
});
|
||||
}
|
||||
}, [cli, member.userId]);
|
||||
|
||||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
|
||||
if (isSynapseAdmin && member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
||||
synapseDeactivateButton = (
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{_t("Deactivate user")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let adminToolsContainer;
|
||||
if (room && member.roomId) {
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={member}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
{ synapseDeactivateButton }
|
||||
</RoomAdminToolsContainer>
|
||||
);
|
||||
} else if (groupId) {
|
||||
adminToolsContainer = (
|
||||
<GroupAdminToolsSection
|
||||
groupId={groupId}
|
||||
groupMember={member}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
{ synapseDeactivateButton }
|
||||
</GroupAdminToolsSection>
|
||||
);
|
||||
} else if (synapseDeactivateButton) {
|
||||
adminToolsContainer = (
|
||||
<GenericAdminToolsContainer>
|
||||
{ synapseDeactivateButton }
|
||||
</GenericAdminToolsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingUpdateCount > 0) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
||||
}
|
||||
|
||||
const memberDetails = (
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
let text;
|
||||
if (!isRoomEncrypted) {
|
||||
if (!_enableDevices) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
} else {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
userTrust.isCrossSigningVerified() :
|
||||
userTrust.isVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
let verifyButton;
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
verifyButton = (
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={() => verifyUser(member)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let devicesSection;
|
||||
if (isRoomEncrypted) {
|
||||
devicesSection = <DevicesSection
|
||||
loading={devices === undefined}
|
||||
devices={devices}
|
||||
userId={member.userId} />;
|
||||
}
|
||||
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
{ verifyButton }
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
|
||||
return <React.Fragment>
|
||||
{ memberDetails &&
|
||||
<div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
|
||||
<div className="mx_UserInfo_memberDetails">
|
||||
{ memberDetails }
|
||||
</div>
|
||||
</div> }
|
||||
|
||||
{ securitySection }
|
||||
<UserOptionsSection
|
||||
devices={devices}
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member} />
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
||||
{ spinner }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
const UserInfoHeader = ({onClose, member, e2eStatus}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let closeButton;
|
||||
|
@ -1057,6 +1307,38 @@ export const UserInfoPane = ({children, className, onClose, e2eStatus, member})
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}, [cli, member]);
|
||||
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const avatarElement = (
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div>
|
||||
<div>
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let presenceState;
|
||||
let presenceLastActiveAgo;
|
||||
let presenceCurrentlyActive;
|
||||
|
@ -1091,74 +1373,35 @@ export const UserInfoPane = ({children, className, onClose, e2eStatus, member})
|
|||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
||||
}
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}, [cli, member]);
|
||||
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const avatarElement = (
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div>
|
||||
<div>
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let e2eIcon;
|
||||
if (e2eStatus) {
|
||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const displayName = member.name || member.displayname;
|
||||
return <React.Fragment>
|
||||
{ closeButton }
|
||||
{ avatarElement }
|
||||
|
||||
return (
|
||||
<div className={classNames("mx_UserInfo", className)} role="tabpanel">
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
{ closeButton }
|
||||
{ avatarElement }
|
||||
|
||||
<div className="mx_UserInfo_container mx_UserInfo_separator">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div>
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
<div>{ member.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_UserInfo_container mx_UserInfo_separator">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div>
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{ children }
|
||||
</AutoHideScrollbar>
|
||||
<div>{ member.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||
const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.RoomMemberInfo, ...props}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// Load room if we are given a room id and memoize it
|
||||
|
@ -1166,246 +1409,46 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
// 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]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}, [cli, user.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
title: _t("Deactivate user?"),
|
||||
description:
|
||||
<div>{ _t(
|
||||
"Deactivating this user will log them out and prevent them from logging back in. Additionally, " +
|
||||
"they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " +
|
||||
"want to deactivate this user?",
|
||||
) }</div>,
|
||||
button: _t("Deactivate user"),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [accepted] = await finished;
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await cli.deactivateSynapseUser(user.userId);
|
||||
} catch (err) {
|
||||
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")),
|
||||
});
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
|
||||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
|
||||
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
||||
synapseDeactivateButton = (
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{_t("Deactivate user")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let adminToolsContainer;
|
||||
if (room && member.roomId) {
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={member}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
{ synapseDeactivateButton }
|
||||
</RoomAdminToolsContainer>
|
||||
);
|
||||
} else if (groupId) {
|
||||
adminToolsContainer = (
|
||||
<GroupAdminToolsSection
|
||||
groupId={groupId}
|
||||
groupMember={user}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
{ synapseDeactivateButton }
|
||||
</GroupAdminToolsSection>
|
||||
);
|
||||
} else if (synapseDeactivateButton) {
|
||||
adminToolsContainer = (
|
||||
<GenericAdminToolsContainer>
|
||||
{ synapseDeactivateButton }
|
||||
</GenericAdminToolsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingUpdateCount > 0) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
|
||||
}
|
||||
|
||||
const memberDetails = (
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
);
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||
const [devices, setDevices] = useState(undefined);
|
||||
// Download device lists
|
||||
useEffect(() => {
|
||||
setDevices(undefined);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function _downloadDeviceList() {
|
||||
try {
|
||||
await cli.downloadKeys([user.userId], true);
|
||||
const devices = await cli.getStoredDevicesForUser(user.userId);
|
||||
|
||||
if (cancelled) {
|
||||
// we got cancelled - presumably a different user now
|
||||
return;
|
||||
}
|
||||
|
||||
_disambiguateDevices(devices);
|
||||
setDevices(devices);
|
||||
} catch (err) {
|
||||
setDevices(null);
|
||||
}
|
||||
}
|
||||
_downloadDeviceList();
|
||||
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
|
||||
// Listen to changes
|
||||
useEffect(() => {
|
||||
let cancel = false;
|
||||
const onDeviceVerificationChanged = (_userId, device) => {
|
||||
if (_userId === user.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of the list.
|
||||
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed in future
|
||||
Promise.resolve(cli.getStoredDevicesForUser(user.userId)).then((devices) => {
|
||||
if (cancel) return;
|
||||
setDevices(devices);
|
||||
});
|
||||
}
|
||||
};
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancel = true;
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
|
||||
let text;
|
||||
if (!isRoomEncrypted) {
|
||||
if (!_enableDevices) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
} else {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const userTrust = cli.checkUserTrust(user.userId);
|
||||
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
userTrust.isCrossSigningVerified() :
|
||||
userTrust.isVerified();
|
||||
const isMe = user.userId === cli.getUserId();
|
||||
let verifyButton;
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let devicesSection;
|
||||
if (isRoomEncrypted) {
|
||||
devicesSection = <DevicesSection
|
||||
loading={devices === undefined}
|
||||
devices={devices} userId={user.userId} />;
|
||||
}
|
||||
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
{ verifyButton }
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
const devices = useDevices(user.userId);
|
||||
|
||||
let e2eStatus;
|
||||
if (isRoomEncrypted && devices) {
|
||||
e2eStatus = getE2EStatus(cli, user.userId, devices);
|
||||
}
|
||||
|
||||
return <UserInfoPane onClose={onClose} e2eStatus={e2eStatus} member={member}>
|
||||
{ memberDetails &&
|
||||
<div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
|
||||
<div className="mx_UserInfo_memberDetails">
|
||||
{ memberDetails }
|
||||
</div>
|
||||
</div> }
|
||||
const classes = ["mx_UserInfo"];
|
||||
|
||||
{ securitySection }
|
||||
<UserOptionsSection
|
||||
devices={devices}
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member} />
|
||||
let content;
|
||||
switch (phase) {
|
||||
case RIGHT_PANEL_PHASES.RoomMemberInfo:
|
||||
case RIGHT_PANEL_PHASES.GroupMemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
member={member}
|
||||
groupId={groupId}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted} />
|
||||
);
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.EncryptionPanel:
|
||||
classes.push("mx_UserInfo_smallAvatar");
|
||||
content = (
|
||||
<EncryptionPanel {...props} member={member} onClose={onClose} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
{ adminToolsContainer }
|
||||
return (
|
||||
<div className={classes.join(" ")} role="tabpanel">
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />
|
||||
|
||||
{ spinner }
|
||||
</UserInfoPane>;
|
||||
{ content }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserInfo.propTypes = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,8 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
|
||||
export default class VerificationPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -25,46 +28,81 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
this._hasVerifier = !!props.request.verifier;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="mx_UserInfo">
|
||||
renderQRPhase() {
|
||||
const {member} = this.props;
|
||||
// TODO change the button into a spinner when on click
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <React.Fragment>
|
||||
<div className="mx_UserInfo_container">
|
||||
{ this.renderStatus() }
|
||||
<h3>Verify by scanning</h3>
|
||||
<p>{_t("Ask %(displayName)s to scan your code, or <a>open your camera</a> to scan theirs:", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
}, {
|
||||
a: t => <a>{ t }</a>,
|
||||
})}</p>
|
||||
<div>QR Code</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>Verify by emoji</h3>
|
||||
<p>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this._startSAS}>
|
||||
{_t("Verify by emoji")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
renderStatus() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const {request} = this.props;
|
||||
renderVerifiedPhase() {
|
||||
const {member} = this.props;
|
||||
|
||||
if (request.requested) {
|
||||
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
|
||||
} else if (request.ready) {
|
||||
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
|
||||
Verify by emoji
|
||||
</AccessibleButton>;
|
||||
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||
<h3>Verified</h3>
|
||||
<p>{_t("You've successfully verified %(displayName)s!", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
})}</p>
|
||||
<E2EIcon isUser={true} status="verified" size={128} />
|
||||
<p>Verify all users in a room to ensure it's secure.</p>
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this._startSAS}>
|
||||
{_t("Got it")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {member, request} = this.props;
|
||||
|
||||
const displayName = member.displayName || member.name || member.userId;
|
||||
|
||||
if (request.ready) {
|
||||
return this.renderQRPhase();
|
||||
} else if (request.started) {
|
||||
if (this.state.sasWaitingForOtherParty) {
|
||||
return <p>Waiting for {request.otherUserId} to confirm ...</p>;
|
||||
} else if (this.state.sasEvent) {
|
||||
if (this.state.sasEvent) {
|
||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||
return (<div>
|
||||
// TODO implement "mismatch" vs "cancelled"
|
||||
return <div className="mx_UserInfo_container">
|
||||
<h3>Compare emoji</h3>
|
||||
<VerificationShowSas
|
||||
displayName={displayName}
|
||||
sas={this.state.sasEvent.sas}
|
||||
onCancel={this._onSasMismatchesClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
/>
|
||||
</div>);
|
||||
</div>;
|
||||
} else {
|
||||
return (<p>Setting up SAS verification...</p>);
|
||||
}
|
||||
} else if (request.done) {
|
||||
return <p>verified {request.otherUserId}!!</p>;
|
||||
return this.renderVerifiedPhase();
|
||||
} else if (request.cancelled) {
|
||||
// TODO check if this matches target
|
||||
// TODO should this be a MODAL?
|
||||
return <p>cancelled by {request.cancellingUserId}!</p>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_startSAS = async () => {
|
||||
|
@ -79,7 +117,6 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
};
|
||||
|
||||
_onSasMatchesClick = () => {
|
||||
this.setState({sasWaitingForOtherParty: true});
|
||||
this.state.sasEvent.confirm();
|
||||
};
|
||||
|
||||
|
@ -106,7 +143,7 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
request.verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
}
|
||||
this._hasVerifier = !!request.verifier;
|
||||
this.forceUpdate();
|
||||
this.forceUpdate(); // TODO fix this
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -76,10 +76,14 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
}
|
||||
try {
|
||||
await request.accept();
|
||||
const cli = MatrixClientPeg.get();
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {verificationRequest: request},
|
||||
refireParams: {
|
||||
verificationRequest: request,
|
||||
member: cli.getUser(request.otherUserId),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
|
|
|
@ -18,6 +18,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
function capFirst(s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
@ -25,18 +27,26 @@ function capFirst(s) {
|
|||
|
||||
export default class VerificationShowSas extends React.Component {
|
||||
static propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
onDone: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
sas: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pending: false,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
onMatchClick = () => {
|
||||
this.setState({ pending: true });
|
||||
this.props.onDone();
|
||||
};
|
||||
|
||||
render() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let sasDisplay;
|
||||
let sasCaption;
|
||||
if (this.props.sas.emoji) {
|
||||
|
@ -69,26 +79,33 @@ export default class VerificationShowSas extends React.Component {
|
|||
} else {
|
||||
return <div>
|
||||
{_t("Unable to find a supported verification method.")}
|
||||
<DialogButtons
|
||||
primaryButton={_t('Cancel')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.props.onCancel}
|
||||
/>
|
||||
<AccessibleButton kind="primary" onClick={this.props.onCancel} className="mx_UserInfo_verify">
|
||||
{_t('Cancel')}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let confirm;
|
||||
if (this.state.pending) {
|
||||
const {displayName} = this.props;
|
||||
const text = _t("Waiting for %(displayName)s to verify…", {displayName});
|
||||
confirm = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
confirm = <React.Fragment>
|
||||
<AccessibleButton kind="primary" onClick={this.onMatchClick} className="mx_UserInfo_verify">
|
||||
{_t("They match")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this.props.onCancel} className="mx_UserInfo_verify">
|
||||
{_t("They don't match")}
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_VerificationShowSas">
|
||||
<p>{sasCaption}</p>
|
||||
<p>{_t(
|
||||
"For maximum security, we recommend you do this in person or use another " +
|
||||
"trusted means of communication.",
|
||||
)}</p>
|
||||
<p>{_t("For ultimate security, do this in person or use another way to communicate.")}</p>
|
||||
{sasDisplay}
|
||||
<DialogButtons onPrimaryButtonClick={this.props.onDone}
|
||||
primaryButton={_t("Continue")}
|
||||
hasCancel={true}
|
||||
onCancel={this.props.onCancel}
|
||||
/>
|
||||
{confirm}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue