Apply a huge part of the decorations and copy

This commit is contained in:
Michael Telatynski 2020-01-24 16:16:46 +00:00
parent 210616c737
commit 7a5e172b88
13 changed files with 586 additions and 418 deletions

View file

@ -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", {

View file

@ -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},
});
};

View file

@ -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;

View file

@ -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;

View file

@ -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 = {

View file

@ -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() {

View file

@ -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);

View file

@ -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>;
}
}