Phase 1, split out UserInfo into a generic Pane, use for EncInfo

pull/21833/head
Michael Telatynski 2020-01-24 11:45:39 +00:00
parent 0078c2f099
commit 210616c737
6 changed files with 191 additions and 133 deletions

View File

@ -49,12 +49,17 @@ limitations under the License.
} }
.mx_UserInfo_container { .mx_UserInfo_container {
padding: 0 16px 16px 16px; padding: 8px 16px;
}
.mx_UserInfo_separator {
border-bottom: 1px solid lightgray; border-bottom: 1px solid lightgray;
} }
.mx_UserInfo_memberDetailsContainer { .mx_UserInfo_memberDetailsContainer {
padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 8px;
} }
.mx_RoomTile_nameContainer { .mx_RoomTile_nameContainer {
@ -204,10 +209,9 @@ limitations under the License.
padding-bottom: 16px; padding-bottom: 16px;
} }
.mx_UserInfo_scrollContainer .mx_UserInfo_container { .mx_UserInfo_scrollContainer:not(.mx_UserInfo_separator) {
padding-top: 16px; padding-top: 16px;
padding-bottom: 0; padding-bottom: 0;
border-bottom: none;
> :not(h3) { > :not(h3) {
margin-left: 8px; margin-left: 8px;
@ -264,3 +268,10 @@ limitations under the License.
margin: 16px 0; margin: 16px 0;
} }
} }
.mx_UserInfo.mx_UserInfo_smallAvatar {
.mx_UserInfo_avatar > div {
max-width: 72px;
margin: 0 auto;
}
}

View File

@ -238,7 +238,18 @@ export default class RightPanel extends React.Component {
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />; panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) { } else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />; 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", { const classes = classNames("mx_RightPanel", "mx_fadable", {

View File

@ -21,11 +21,17 @@ import {_t} from "../../../languageHandler";
export default class EncryptionInfo extends React.PureComponent { export default class EncryptionInfo extends React.PureComponent {
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container"> return (
<h3>{_t("Verify User")}</h3> <div className="mx_UserInfo_container">
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p> <h3>{_t("Verify User")}</h3>
<p>{_t("For maximum security, do this in person.")}</p> <div>
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton> <p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
</div></div>); <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>
);
} }
} }

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,6 +19,8 @@ import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel"; import VerificationPanel from "./VerificationPanel";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom"; import {ensureDMExists} from "../../../createRoom";
import {UserInfoPane} from "./UserInfo";
import {_t} from "../../../languageHandler";
export default class EncryptionPanel extends React.PureComponent { export default class EncryptionPanel extends React.PureComponent {
constructor(props) { constructor(props) {
@ -27,15 +29,30 @@ export default class EncryptionPanel extends React.PureComponent {
} }
render() { render() {
let content;
const request = this.props.verificationRequest || this.state.verificationRequest; const request = this.props.verificationRequest || this.state.verificationRequest;
const {member} = this.props; const {member} = this.props;
if (request) { if (request) {
return <VerificationPanel request={request} key={request.channel.transactionId} />; content = <VerificationPanel request={request} key={request.channel.transactionId} />;
} else if (member) { } else if (member) {
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />; content = <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
} else { } else {
return <p>Not a member nor request, not sure what to render</p>; 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 () => { _onStartVerification = async () => {

View File

@ -59,7 +59,7 @@ const _disambiguateDevices = (devices) => {
} }
}; };
const _getE2EStatus = (cli, userId, devices) => { export const getE2EStatus = (cli, userId, devices) => {
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
return hasUnverifiedDevice ? "warning" : "verified"; return hasUnverifiedDevice ? "warning" : "verified";
@ -1047,6 +1047,117 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
); );
}; };
export const UserInfoPane = ({children, className, onClose, e2eStatus, member}) => {
const cli = useContext(MatrixClientContext);
let closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
<div />
</AccessibleButton>;
}
let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
let statusMessage;
if (member instanceof RoomMember && member.user) {
presenceState = member.user.presence;
presenceLastActiveAgo = member.user.lastActiveAgo;
presenceCurrentlyActive = member.user.currentlyActive;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage;
}
}
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[cli.baseUrl];
}
let presenceLabel = null;
if (showPresence) {
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />;
}
let statusLabel = null;
if (statusMessage) {
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 (
<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>
{ children }
</AutoHideScrollbar>
</div>
);
};
const UserInfo = ({user, groupId, roomId, onClose}) => { const UserInfo = ({user, groupId, roomId, onClose}) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -1117,20 +1228,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
} }
}, [cli, user.userId]); }, [cli, user.userId]);
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]);
let synapseDeactivateButton; let synapseDeactivateButton;
let spinner; let spinner;
@ -1180,68 +1277,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />; spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
} }
const displayName = member.name || member.displayname;
let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
let statusMessage;
if (member instanceof RoomMember && member.user) {
presenceState = member.user.presence;
presenceLastActiveAgo = member.user.lastActiveAgo;
presenceCurrentlyActive = member.user.currentlyActive;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage;
}
}
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[cli.baseUrl];
}
let presenceLabel = null;
if (showPresence) {
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />;
}
let statusLabel = null;
if (statusMessage) {
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
}
// const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
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 closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
<div />
</AccessibleButton>;
}
const memberDetails = ( const memberDetails = (
<PowerLevelSection <PowerLevelSection
powerLevels={powerLevels} powerLevels={powerLevels}
@ -1347,53 +1382,30 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
</div> </div>
); );
let e2eIcon; let e2eStatus;
if (isRoomEncrypted && devices) { if (isRoomEncrypted && devices) {
const e2eStatus = _getE2EStatus(cli, user.userId, devices); e2eStatus = getE2EStatus(cli, user.userId, devices);
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
} }
return ( return <UserInfoPane onClose={onClose} e2eStatus={e2eStatus} member={member}>
<div className="mx_UserInfo" role="tabpanel"> { memberDetails &&
<AutoHideScrollbar className="mx_UserInfo_scrollContainer"> <div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
{ closeButton } <div className="mx_UserInfo_memberDetails">
{ avatarElement } { memberDetails }
</div>
</div> }
<div className="mx_UserInfo_container"> { securitySection }
<div className="mx_UserInfo_profile"> <UserOptionsSection
<div> devices={devices}
<h2 aria-label={displayName}> canInvite={roomPermissions.canInvite}
{ e2eIcon } isIgnored={isIgnored}
{ displayName } member={member} />
</h2>
</div>
<div>{ user.userId }</div>
<div className="mx_UserInfo_profileStatus">
{presenceLabel}
{statusLabel}
</div>
</div>
</div>
{ memberDetails && <div className="mx_UserInfo_container mx_UserInfo_memberDetailsContainer"> { adminToolsContainer }
<div className="mx_UserInfo_memberDetails">
{ memberDetails }
</div>
</div> }
{ securitySection } { spinner }
<UserOptionsSection </UserInfoPane>;
devices={devices}
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member} />
{ adminToolsContainer }
{ spinner }
</AutoHideScrollbar>
</div>
);
}; };
UserInfo.propTypes = { UserInfo.propTypes = {

View File

@ -1128,6 +1128,8 @@
"For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.", "For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.",
"For maximum security, do this in person.": "For maximum security, do this in person.", "For maximum security, do this in person.": "For maximum security, do this in person.",
"Start Verification": "Start Verification", "Start Verification": "Start Verification",
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
"Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Your messages are secured and only you and the recipient have the unique keys to unlock them.",
"Members": "Members", "Members": "Members",
"Files": "Files", "Files": "Files",
"Trusted": "Trusted", "Trusted": "Trusted",
@ -1144,7 +1146,6 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s", "<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
"Security": "Security", "Security": "Security",
"Sunday": "Sunday", "Sunday": "Sunday",
"Monday": "Monday", "Monday": "Monday",