From 210616c737bdc93fd421c75ec8e554779f4af7fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 24 Jan 2020 11:45:39 +0000 Subject: [PATCH 01/23] Phase 1, split out UserInfo into a generic Pane, use for EncInfo --- res/css/views/right_panel/_UserInfo.scss | 17 +- src/components/structures/RightPanel.js | 13 +- .../views/right_panel/EncryptionInfo.js | 18 +- .../views/right_panel/EncryptionPanel.js | 25 +- src/components/views/right_panel/UserInfo.js | 248 +++++++++--------- src/i18n/strings/en_EN.json | 3 +- 6 files changed, 191 insertions(+), 133 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index d2d9d12c6d..57ffd4982e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -49,12 +49,17 @@ limitations under the License. } .mx_UserInfo_container { - padding: 0 16px 16px 16px; + padding: 8px 16px; + } + + .mx_UserInfo_separator { border-bottom: 1px solid lightgray; } .mx_UserInfo_memberDetailsContainer { + padding-top: 0; padding-bottom: 0; + margin-bottom: 8px; } .mx_RoomTile_nameContainer { @@ -204,10 +209,9 @@ limitations under the License. padding-bottom: 16px; } - .mx_UserInfo_scrollContainer .mx_UserInfo_container { + .mx_UserInfo_scrollContainer:not(.mx_UserInfo_separator) { padding-top: 16px; padding-bottom: 0; - border-bottom: none; > :not(h3) { margin-left: 8px; @@ -264,3 +268,10 @@ limitations under the License. margin: 16px 0; } } + +.mx_UserInfo.mx_UserInfo_smallAvatar { + .mx_UserInfo_avatar > div { + max-width: 72px; + margin: 0 auto; + } +} diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index dca89d0c35..c01a3709e7 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -238,7 +238,18 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { panel = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) { - panel = ; + const onClose = () => { + dis.dispatch({ + action: "view_user", + member: this.state.member, + }); + }; + panel = ( + + ); } const classes = classNames("mx_RightPanel", "mx_fadable", { diff --git a/src/components/views/right_panel/EncryptionInfo.js b/src/components/views/right_panel/EncryptionInfo.js index 5770e9b086..2d265967ae 100644 --- a/src/components/views/right_panel/EncryptionInfo.js +++ b/src/components/views/right_panel/EncryptionInfo.js @@ -21,11 +21,17 @@ import {_t} from "../../../languageHandler"; export default class EncryptionInfo extends React.PureComponent { render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return (
-

{_t("Verify User")}

-

{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}

-

{_t("For maximum security, do this in person.")}

- {_t("Start Verification")} -
); + return ( +
+

{_t("Verify User")}

+
+

{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}

+

{_t("For maximum security, do this in person.")}

+ + {_t("Start Verification")} + +
+
+ ); } } diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js index 4b3473935a..a1008543e4 100644 --- a/src/components/views/right_panel/EncryptionPanel.js +++ b/src/components/views/right_panel/EncryptionPanel.js @@ -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. @@ -19,6 +19,8 @@ import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {ensureDMExists} from "../../../createRoom"; +import {UserInfoPane} from "./UserInfo"; +import {_t} from "../../../languageHandler"; export default class EncryptionPanel extends React.PureComponent { constructor(props) { @@ -27,15 +29,30 @@ export default class EncryptionPanel extends React.PureComponent { } render() { + let content; const request = this.props.verificationRequest || this.state.verificationRequest; const {member} = this.props; if (request) { - return ; + content = ; } else if (member) { - return ; + content = ; } else { - return

Not a member nor request, not sure what to render

; + content =

Not a member nor request, not sure what to render

; } + + return ( + +
+

{_t("Encryption")}

+
+

{_t("Messages in this room are end-to-end encrypted.")}

+

{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}

+
+
+ + { content } +
+ ); } _onStartVerification = async () => { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 051f92cc9c..a31e9a6ce0 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -59,7 +59,7 @@ const _disambiguateDevices = (devices) => { } }; -const _getE2EStatus = (cli, userId, devices) => { +export const getE2EStatus = (cli, userId, devices) => { if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); 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 = +
+ ; + } + + 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 = ; + } + + let statusLabel = null; + if (statusMessage) { + statusLabel = { statusMessage }; + } + + 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 = ( +
+
+
+ +
+
+
+ ); + + let e2eIcon; + if (e2eStatus) { + e2eIcon = ; + } + + const displayName = member.name || member.displayname; + + return ( +
+ + { closeButton } + { avatarElement } + +
+
+
+

+ { e2eIcon } + { displayName } +

+
+
{ member.userId }
+
+ {presenceLabel} + {statusLabel} +
+
+
+ + { children } +
+
+ ); +}; + const UserInfo = ({user, groupId, roomId, onClose}) => { const cli = useContext(MatrixClientContext); @@ -1117,20 +1228,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { } }, [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 spinner; @@ -1180,68 +1277,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { 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 = ; - } - - let statusLabel = null; - if (statusMessage) { - statusLabel = { statusMessage }; - } - - // const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl; - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - const avatarElement = ( -
-
-
- -
-
-
- ); - - let closeButton; - if (onClose) { - closeButton = -
- ; - } - const memberDetails = ( {
); - let e2eIcon; + let e2eStatus; if (isRoomEncrypted && devices) { - const e2eStatus = _getE2EStatus(cli, user.userId, devices); - e2eIcon = ; + e2eStatus = getE2EStatus(cli, user.userId, devices); } - return ( -
- - { closeButton } - { avatarElement } + return + { memberDetails && +
+
+ { memberDetails } +
+
} -
-
-
-

- { e2eIcon } - { displayName } -

-
-
{ user.userId }
-
- {presenceLabel} - {statusLabel} -
-
-
+ { securitySection } + - { memberDetails &&
-
- { memberDetails } -
-
} + { adminToolsContainer } - { securitySection } - - - { adminToolsContainer } - - { spinner } -
-
- ); + { spinner } + ; }; UserInfo.propTypes = { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 099b64dd49..c99b22f421 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 maximum security, do this in person.": "For maximum security, do this in person.", "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", "Files": "Files", "Trusted": "Trusted", @@ -1144,7 +1146,6 @@ "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "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 end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", "Security": "Security", "Sunday": "Sunday", "Monday": "Monday", From 7a5e172b88c959f94051db1f6d7424cf0ef8108f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 24 Jan 2020 16:16:46 +0000 Subject: [PATCH 02/23] Apply a huge part of the decorations and copy --- res/css/_components.scss | 2 + .../views/right_panel/_EncryptionInfo.scss | 24 + res/css/views/right_panel/_UserInfo.scss | 6 +- .../views/right_panel/_VerificationPanel.scss | 21 + src/components/structures/RightPanel.js | 25 +- .../views/messages/MKeyVerificationRequest.js | 3 +- .../views/right_panel/EncryptionInfo.js | 68 +- .../views/right_panel/EncryptionPanel.js | 72 +- src/components/views/right_panel/UserInfo.js | 619 ++++++++++-------- .../views/right_panel/VerificationPanel.js | 85 ++- .../views/toasts/VerificationRequestToast.js | 6 +- .../views/verification/VerificationShowSas.js | 55 +- src/i18n/strings/en_EN.json | 18 +- 13 files changed, 586 insertions(+), 418 deletions(-) create mode 100644 res/css/views/right_panel/_EncryptionInfo.scss create mode 100644 res/css/views/right_panel/_VerificationPanel.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 60f749de9c..e19050dc7e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -140,7 +140,9 @@ @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_ViewSourceEvent.scss"; +@import "./views/right_panel/_EncryptionInfo.scss"; @import "./views/right_panel/_UserInfo.scss"; +@import "./views/right_panel/_VerificationPanel.scss"; @import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss new file mode 100644 index 0000000000..386eef8e7f --- /dev/null +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -0,0 +1,24 @@ +/* +Copyright 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_UserInfo { + .mx_EncryptionInfo_spinner { + .mx_Spinner { + margin-top: 25px; + margin-bottom: 15px; + } + } +} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 57ffd4982e..8aad5240aa 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -81,6 +81,7 @@ limitations under the License. .mx_UserInfo_avatar > div { max-width: 30vh; margin: 0 auto; + transition: 1s; } .mx_UserInfo_avatar > div > div { @@ -260,11 +261,6 @@ limitations under the License. .mx_UserInfo_verify { display: block; - background-color: $accent-color; - color: $accent-fg-color; - border-radius: 4px; - padding: 7px 1.5em; - text-align: center; margin: 16px 0; } } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss new file mode 100644 index 0000000000..84a82c9a5c --- /dev/null +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -0,0 +1,21 @@ +/* +Copyright 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_UserInfo { + .mx_VerificationPanel_verified_section .mx_E2EIcon { + margin: 0 auto; + } +} diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index c01a3709e7..2188bda4f2 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -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 = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) { panel = ; - } 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 = ; } else { panel = ; @@ -215,7 +217,7 @@ export default class RightPanel extends React.Component { }); }; panel = ; @@ -237,19 +239,6 @@ export default class RightPanel extends React.Component { panel = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) { - const onClose = () => { - dis.dispatch({ - action: "view_user", - member: this.state.member, - }); - }; - panel = ( - - ); } const classes = classNames("mx_RightPanel", "mx_fadable", { diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index ae793556d8..474ff7becb 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -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}, }); }; diff --git a/src/components/views/right_panel/EncryptionInfo.js b/src/components/views/right_panel/EncryptionInfo.js index 2d265967ae..8868c30053 100644 --- a/src/components/views/right_panel/EncryptionInfo.js +++ b/src/components/views/right_panel/EncryptionInfo.js @@ -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
+ + { text } +
; +}; + +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 = ; + } else { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( -
-

{_t("Verify User")}

-
-

{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}

-

{_t("For maximum security, do this in person.")}

- - {_t("Start Verification")} - -
-
+ content = ( + + {_t("Start Verification")} + ); } -} + + return +
+

{_t("Encryption")}

+
+

{_t("Messages in this room are end-to-end encrypted.")}

+

{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}

+
+
+
+

{_t("Verify User")}

+
+

{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}

+

{_t("For maximum security, do this in person.")}

+ { content } +
+
+
; +}; +EncryptionInfo.propTypes = { + member: PropTypes.object.isRequired, + onStartVerification: PropTypes.func.isRequired, + request: PropTypes.object, +}; + +export default EncryptionInfo; diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js index a1008543e4..dfb145f61d 100644 --- a/src/components/views/right_panel/EncryptionPanel.js +++ b/src/components/views/right_panel/EncryptionPanel.js @@ -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 ; + } else { + return ; } +}; +EncryptionPanel.propTypes = { - render() { - let content; - const request = this.props.verificationRequest || this.state.verificationRequest; - const {member} = this.props; - if (request) { - content = ; - } else if (member) { - content = ; - } else { - content =

Not a member nor request, not sure what to render

; - } +}; - return ( - -
-

{_t("Encryption")}

-
-

{_t("Messages in this room are end-to-end encrypted.")}

-

{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}

-
-
- - { content } -
- ); - } - - _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; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a31e9a6ce0..41dbe49cc8 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -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: +
{ _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?", + ) }
, + 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 = ( + + {_t("Deactivate user")} + + ); + } + + let adminToolsContainer; + if (room && member.roomId) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } else if (groupId) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } else if (synapseDeactivateButton) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } + + if (pendingUpdateCount > 0) { + const Loader = sdk.getComponent("elements.Spinner"); + spinner = ; + } + + const memberDetails = ( + + ); + + // 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 = ( + verifyUser(member)}> + {_t("Verify")} + + ); + } + + let devicesSection; + if (isRoomEncrypted) { + devicesSection = ; + } + + const securitySection = ( +
+

{ _t("Security") }

+

{ text }

+ { verifyButton } + { devicesSection } +
+ ); + + return + { memberDetails && +
+
+ { memberDetails } +
+
} + + { securitySection } + + + { adminToolsContainer } + + { spinner } +
; +}; + +const UserInfoHeader = ({onClose, member, e2eStatus}) => { const cli = useContext(MatrixClientContext); let closeButton; @@ -1057,6 +1307,38 @@ export const UserInfoPane = ({children, className, onClose, e2eStatus, member})
; } + 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 = ( +
+
+
+ +
+
+
+ ); + let presenceState; let presenceLastActiveAgo; let presenceCurrentlyActive; @@ -1091,74 +1373,35 @@ export const UserInfoPane = ({children, className, onClose, e2eStatus, member}) statusLabel = { statusMessage }; } - 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 = ( -
-
-
- -
-
-
- ); - let e2eIcon; if (e2eStatus) { e2eIcon = ; } const displayName = member.name || member.displayname; + return + { closeButton } + { avatarElement } - return ( -
- - { closeButton } - { avatarElement } - -
-
-
-

- { e2eIcon } - { displayName } -

-
-
{ member.userId }
-
- {presenceLabel} - {statusLabel} -
-
+
+
+
+

+ { e2eIcon } + { displayName } +

- - { children } - +
{ member.userId }
+
+ {presenceLabel} + {statusLabel} +
+
- ); + ; }; -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: -
{ _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?", - ) }
, - 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 = ( - - {_t("Deactivate user")} - - ); - } - - let adminToolsContainer; - if (room && member.roomId) { - adminToolsContainer = ( - - { synapseDeactivateButton } - - ); - } else if (groupId) { - adminToolsContainer = ( - - { synapseDeactivateButton } - - ); - } else if (synapseDeactivateButton) { - adminToolsContainer = ( - - { synapseDeactivateButton } - - ); - } - - if (pendingUpdateCount > 0) { - const Loader = sdk.getComponent("elements.Spinner"); - spinner = ; - } - - const memberDetails = ( - - ); - 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 = verifyUser(user)}> - {_t("Verify")} - ; - } - - let devicesSection; - if (isRoomEncrypted) { - devicesSection = ; - } - - const securitySection = ( -
-

{ _t("Security") }

-

{ text }

- { verifyButton } - { devicesSection } -
- ); + const devices = useDevices(user.userId); let e2eStatus; if (isRoomEncrypted && devices) { e2eStatus = getE2EStatus(cli, user.userId, devices); } - return - { memberDetails && -
-
- { memberDetails } -
-
} + const classes = ["mx_UserInfo"]; - { securitySection } - + let content; + switch (phase) { + case RIGHT_PANEL_PHASES.RoomMemberInfo: + case RIGHT_PANEL_PHASES.GroupMemberInfo: + content = ( + + ); + break; + case RIGHT_PANEL_PHASES.EncryptionPanel: + classes.push("mx_UserInfo_smallAvatar"); + content = ( + + ); + break; + } - { adminToolsContainer } + return ( +
+ + - { spinner } - ; + { content } + +
+ ); }; UserInfo.propTypes = { diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index 0d28e1568f..a66ecd34fb 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -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
+ renderQRPhase() { + const {member} = this.props; + // TODO change the button into a spinner when on click + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return
- { this.renderStatus() } +

Verify by scanning

+

{_t("Ask %(displayName)s to scan your code, or open your camera to scan theirs:", { + displayName: member.displayName || member.name || member.userId, + }, { + a: t => { t }, + })}

+
QR Code
-
; + +
+

Verify by emoji

+

{_t("If you can't scan the code above, verify by comparing unique emoji.")}

+ + {_t("Verify by emoji")} + +
+ ; } - 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 (

Waiting for {request.otherUserId} to accept ...

); - } else if (request.ready) { - const verifyButton = - Verify by emoji - ; - return (

{request.otherUserId} is ready, start {verifyButton}

); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( +
+

Verified

+

{_t("You've successfully verified %(displayName)s!", { + displayName: member.displayName || member.name || member.userId, + })}

+ +

Verify all users in a room to ensure it's secure.

+ + {_t("Got it")} + +
+ ); + } + + 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

Waiting for {request.otherUserId} to confirm ...

; - } else if (this.state.sasEvent) { + if (this.state.sasEvent) { const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); - return (
+ // TODO implement "mismatch" vs "cancelled" + return
+

Compare emoji

-
); +
; } else { return (

Setting up SAS verification...

); } } else if (request.done) { - return

verified {request.otherUserId}!!

; + return this.renderVerifiedPhase(); } else if (request.cancelled) { + // TODO check if this matches target + // TODO should this be a MODAL? return

cancelled by {request.cancellingUserId}!

; } + 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() { diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index 479a3e3f93..769e0d36d0 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -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); diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index 8f39457689..693a3769a0 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -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
{_t("Unable to find a supported verification method.")} - + + {_t('Cancel')} +
; } + let confirm; + if (this.state.pending) { + const {displayName} = this.props; + const text = _t("Waiting for %(displayName)s to verify…", {displayName}); + confirm = ; + } else { + confirm = + + {_t("They match")} + + + {_t("They don't match")} + + ; + } + return

{sasCaption}

-

{_t( - "For maximum security, we recommend you do this in person or use another " + - "trusted means of communication.", - )}

+

{_t("For ultimate security, do this in person or use another way to communicate.")}

{sasDisplay} - + {confirm}
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c99b22f421..5b131b8750 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -451,7 +451,10 @@ "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", "Unable to find a supported verification method.": "Unable to find a supported verification method.", "Cancel": "Cancel", - "For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", + "Waiting for %(displayName)s to verify…": "Waiting for %(displayName)s to verify…", + "They match": "They match", + "They don't match": "They don't match", + "For ultimate security, do this in person or use another way to communicate.": "For ultimate security, do this in person or use another way to communicate.", "Dog": "Dog", "Cat": "Cat", "Lion": "Lion", @@ -1124,12 +1127,13 @@ "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", - "Verify User": "Verify User", - "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.", + "Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…", "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.", + "Verify User": "Verify User", + "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.", "Members": "Members", "Files": "Files", "Trusted": "Trusted", @@ -1147,6 +1151,11 @@ "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.", "Security": "Security", + "Ask %(displayName)s to scan your code, or open your camera to scan theirs:": "Ask %(displayName)s to scan your code, or open your camera to scan theirs:", + "If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.", + "Verify by emoji": "Verify by emoji", + "You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!", + "Got it": "Got it", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -1426,6 +1435,7 @@ "Verify device": "Verify device", "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", "Verify by comparing a short text string.": "Verify by comparing a short text string.", + "For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", "Begin Verifying": "Begin Verifying", "Waiting for partner to accept...": "Waiting for partner to accept...", "Nothing appearing? Not all clients support interactive verification yet. .": "Nothing appearing? Not all clients support interactive verification yet. .", From b67256c7c2b0b68b4b54274fcbf70b2336a32134 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 24 Jan 2020 16:41:43 +0000 Subject: [PATCH 03/23] post-merge fix --- src/components/structures/RightPanel.js | 4 +-- .../views/right_panel/VerificationPanel.js | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 2188bda4f2..79955c8448 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -196,7 +196,7 @@ export default class RightPanel extends React.Component { }); }; panel = ; diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index cbabb1188a..0fdf16de65 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -19,7 +19,6 @@ import React from 'react'; import * as sdk from '../../../index'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {_t} from "../../../languageHandler"; import E2EIcon from "../rooms/E2EIcon"; @@ -32,9 +31,26 @@ export default class VerificationPanel extends React.PureComponent { } renderQRPhase() { - const {member} = this.props; + const {member, request} = this.props; // type req: VerificationRequest // TODO change the button into a spinner when on click const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + if (!request.requestEvent || !request.requestEvent.getId()) { + // TODO handle this error case + return

request.requestEvent.getId()

; + } + + const qrCodeKeys = [ + [MatrixClientPeg.get().getDeviceId(), MatrixClientPeg.get().getDeviceEd25519Key()], + [MatrixClientPeg.get().getCrossSigningId(), MatrixClientPeg.get().getCrossSigningId()], + ]; + const crossSigningInfo = MatrixClientPeg.get().getStoredCrossSigningForUser(request.otherUserId); + + if (!crossSigningInfo) { + // TODO handle this error case + return

crossSigningInfo

; + } + return

Verify by scanning

@@ -43,12 +59,20 @@ export default class VerificationPanel extends React.PureComponent { }, { a: t => { t }, })}

-
QR Code
+ +

Verify by emoji

{_t("If you can't scan the code above, verify by comparing unique emoji.")}

+ {_t("Verify by emoji")} From 50b491d7e5ede07a2d23e91e0a8cb3ec0c3c7251 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 27 Jan 2020 15:48:19 +0000 Subject: [PATCH 04/23] post merge fix --- .../views/right_panel/VerificationPanel.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index 0fdf16de65..b911824b4e 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -35,21 +35,18 @@ export default class VerificationPanel extends React.PureComponent { // TODO change the button into a spinner when on click const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - if (!request.requestEvent || !request.requestEvent.getId()) { + const cli = MatrixClientPeg.get(); + const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId); + if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) { // TODO handle this error case return

request.requestEvent.getId()

; } + const myKeyId = cli.getCrossSigningId(); const qrCodeKeys = [ - [MatrixClientPeg.get().getDeviceId(), MatrixClientPeg.get().getDeviceEd25519Key()], - [MatrixClientPeg.get().getCrossSigningId(), MatrixClientPeg.get().getCrossSigningId()], + [cli.getDeviceId(), cli.getDeviceEd25519Key()], + [myKeyId, myKeyId], ]; - const crossSigningInfo = MatrixClientPeg.get().getStoredCrossSigningForUser(request.otherUserId); - - if (!crossSigningInfo) { - // TODO handle this error case - return

crossSigningInfo

; - } return
From ca5d4d88a275c93aff94158ae8b6836acd1f08e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 27 Jan 2020 17:17:05 +0000 Subject: [PATCH 05/23] Style the QR code --- res/css/views/right_panel/_UserInfo.scss | 5 +++++ .../views/right_panel/_VerificationPanel.scss | 16 ++++++++++++++ .../elements/crypto/VerificationQRCode.js | 2 +- .../views/right_panel/VerificationPanel.js | 21 ++++++++++--------- src/i18n/strings/en_EN.json | 2 +- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 9db636ae6b..30c71146bf 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -111,6 +111,7 @@ limitations under the License. // override the calculated sizes so that the letter isn't HUGE font-size: 56px !important; width: 100% !important; + transition: font-size 1s; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { @@ -270,4 +271,8 @@ limitations under the License. max-width: 72px; margin: 0 auto; } + + .mx_UserInfo_avatar .mx_BaseAvatar_initial { + font-size: 40px !important; // override the other override because here the avatar is smaller + } } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index 84a82c9a5c..75b469cef9 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -18,4 +18,20 @@ limitations under the License. .mx_VerificationPanel_verified_section .mx_E2EIcon { margin: 0 auto; } + + .mx_VerificationPanel_qrCode { + padding: 4px 4px 0 4px; + background: white; + border-radius: 4px; + width: max-content; + max-width: 100%; + margin: 0 auto; + + canvas { + // override height and width which are set on the element directly + height: auto !important; + width: 100% !important; + max-width: 240px; + } + } } diff --git a/src/components/views/elements/crypto/VerificationQRCode.js b/src/components/views/elements/crypto/VerificationQRCode.js index 1cb5647317..630a06a07c 100644 --- a/src/components/views/elements/crypto/VerificationQRCode.js +++ b/src/components/views/elements/crypto/VerificationQRCode.js @@ -51,6 +51,6 @@ export default class VerificationQRCode extends React.PureComponent { const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`; - return ; + return ; } } diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index b911824b4e..c1e84afc22 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -48,22 +48,23 @@ export default class VerificationPanel extends React.PureComponent { [myKeyId, myKeyId], ]; + // TODO: add way to open camera to scan a QR code return

Verify by scanning

-

{_t("Ask %(displayName)s to scan your code, or open your camera to scan theirs:", { +

{_t("Ask %(displayName)s to scan your code:", { displayName: member.displayName || member.name || member.userId, - }, { - a: t => { t }, })}

- +
+ +
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6297270540..d4512d785d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1156,7 +1156,7 @@ "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.", "Security": "Security", - "Ask %(displayName)s to scan your code, or open your camera to scan theirs:": "Ask %(displayName)s to scan your code, or open your camera to scan theirs:", + "Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:", "If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.", "Verify by emoji": "Verify by emoji", "You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!", From db1d3c091e041b607507fdf145cb41c6a92c46f5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 27 Jan 2020 17:21:31 +0000 Subject: [PATCH 06/23] Error handle the no QR verif case better --- .../views/right_panel/VerificationPanel.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index c1e84afc22..f6a26665d8 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -31,15 +31,22 @@ export default class VerificationPanel extends React.PureComponent { } renderQRPhase() { - const {member, request} = this.props; // type req: VerificationRequest + const {member, request} = this.props; // TODO change the button into a spinner when on click const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const cli = MatrixClientPeg.get(); const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId); if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) { - // TODO handle this error case - return

request.requestEvent.getId()

; + // for whatever reason we can't generate a QR code, offer only SAS Verification + return
+

Verify by emoji

+

{_t("Verify by comparing unique emoji.")}

+ + + {_t("Verify by emoji")} + +
; } const myKeyId = cli.getCrossSigningId(); From 657457c14bc1f49da33ddae8a51f9a7d5ac91cba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 Jan 2020 11:13:09 +0000 Subject: [PATCH 07/23] Apply remainder of ux --- .../views/right_panel/_EncryptionInfo.scss | 2 + src/components/views/dialogs/ErrorDialog.js | 10 +- .../views/right_panel/EncryptionPanel.js | 52 ++++++-- .../views/right_panel/VerificationPanel.js | 112 ++++++++++++------ .../views/verification/VerificationShowSas.js | 1 - src/i18n/strings/en_EN.json | 12 +- 6 files changed, 139 insertions(+), 50 deletions(-) diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss index 386eef8e7f..e13b1b6802 100644 --- a/res/css/views/right_panel/_EncryptionInfo.scss +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -20,5 +20,7 @@ limitations under the License. margin-top: 25px; margin-bottom: 15px; } + + text-align: center; } } diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 15c87990d0..fbc5509457 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -42,6 +42,7 @@ export default createReactClass({ button: PropTypes.string, focus: PropTypes.bool, onFinished: PropTypes.func.isRequired, + headerImage: PropTypes.string, }, getDefaultProps: function() { @@ -56,9 +57,12 @@ export default createReactClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
{ this.props.description || _t('An error has occurred.') } diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js index dfb145f61d..2c4a896624 100644 --- a/src/components/views/right_panel/EncryptionPanel.js +++ b/src/components/views/right_panel/EncryptionPanel.js @@ -21,19 +21,47 @@ import VerificationPanel from "./VerificationPanel"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {ensureDMExists} from "../../../createRoom"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import Modal from "../../../Modal"; +import {PHASE_REQUESTED} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import * as sdk from "../../../index"; +import {_t} from "../../../languageHandler"; -const EncryptionPanel = ({verificationRequest, member}) => { +// cancellation codes which constitute a key mismatch +const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; + +const EncryptionPanel = ({verificationRequest, member, onClose}) => { const [request, setRequest] = useState(verificationRequest); useEffect(() => { setRequest(verificationRequest); }, [verificationRequest]); - const [pending, setPending] = useState(false); + const [phase, setPhase] = useState(false); const changeHandler = useCallback(() => { - setPending(request && request.requested); - }, [request]); + // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card + if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { + headerImage: require("../../../../res/img/e2e/warning.svg"), + title: _t("Your messages are not secure"), + description:
+ {_t("One of the following may be compromised:")} +
    +
  • {_t("Your homeserver")}
  • +
  • {_t("The homeserver the user you’re verifying is connected to")}
  • +
  • {_t("Yours, or the other users’ internet connection")}
  • +
  • {_t("Yours, or the other users’ device")}
  • +
+
, + onFinished: onClose, + }); + return; // don't update phase here as we will be transitioning away from this view shortly + } + + if (request) { + setPhase(request.phase); + } + }, [onClose, request]); useEventEmitter(request, "change", changeHandler); - useEffect(changeHandler, [changeHandler]); const onStartVerification = useCallback(async () => { const cli = MatrixClientPeg.get(); @@ -42,10 +70,18 @@ const EncryptionPanel = ({verificationRequest, member}) => { setRequest(verificationRequest); }, [member.userId]); - if (!request || pending) { - return ; + const requested = request && phase === PHASE_REQUESTED; + if (!request || requested) { + return ; } else { - return ; + return ( + + ); } }; EncryptionPanel.propTypes = { diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index f6a26665d8..6a5516927a 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -22,6 +22,13 @@ import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {_t} from "../../../languageHandler"; import E2EIcon from "../rooms/E2EIcon"; +import { + PHASE_READY, + PHASE_DONE, + PHASE_STARTED, + PHASE_CANCELLED, +} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import Spinner from "../elements/Spinner"; export default class VerificationPanel extends React.PureComponent { constructor(props) { @@ -30,11 +37,22 @@ export default class VerificationPanel extends React.PureComponent { this._hasVerifier = !!props.request.verifier; } - renderQRPhase() { + renderQRPhase(pending) { const {member, request} = this.props; // TODO change the button into a spinner when on click const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let button; + if (pending) { + button = ; + } else { + button = ( + + {_t("Verify by emoji")} + + ); + } + const cli = MatrixClientPeg.get(); const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId); if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) { @@ -43,9 +61,7 @@ export default class VerificationPanel extends React.PureComponent {

Verify by emoji

{_t("Verify by comparing unique emoji.")}

- - {_t("Verify by emoji")} - + { button }
; } @@ -78,9 +94,7 @@ export default class VerificationPanel extends React.PureComponent {

Verify by emoji

{_t("If you can't scan the code above, verify by comparing unique emoji.")}

- - {_t("Verify by emoji")} - + { button }
; } @@ -97,7 +111,36 @@ export default class VerificationPanel extends React.PureComponent { })}

Verify all users in a room to ensure it's secure.

- + + + {_t("Got it")} + +
+ ); + } + + renderCancelledPhase() { + const {member, request} = this.props; + + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let text; + if (request.cancellationCode === "m.timeout") { + text = _t("Verification timed out. Start verification again from their profile."); + } else if (request.cancellingUserId === request.otherUserId) { + text = _t("%(displayName)s cancelled verification. Start verification again from their profile.", { + displayName: member.displayName || member.name || member.userId, + }); + } else { + text = _t("You cancelled verification. Start verification again from their profile."); + } + + return ( +
+

Verification cancelled

+

{ text }

+ + {_t("Got it")}
@@ -105,34 +148,32 @@ export default class VerificationPanel extends React.PureComponent { } render() { - const {member, request} = this.props; + const {member} = this.props; const displayName = member.displayName || member.name || member.userId; - if (request.ready) { - return this.renderQRPhase(); - } else if (request.started) { - if (this.state.sasEvent) { - const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); - // TODO implement "mismatch" vs "cancelled" - return
-

Compare emoji

- -
; - } else { - return (

Setting up SAS verification...

); - } - } else if (request.done) { - return this.renderVerifiedPhase(); - } else if (request.cancelled) { - // TODO check if this matches target - // TODO should this be a MODAL? - return

cancelled by {request.cancellingUserId}!

; + switch (this.props.phase) { + case PHASE_READY: + return this.renderQRPhase(); + case PHASE_STARTED: + if (this.state.sasEvent) { + const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + return
+

Compare emoji

+ +
; + } else { + return this.renderQRPhase(true); // keep showing same phase but with a spinner + } + case PHASE_DONE: + return this.renderVerifiedPhase(); + case PHASE_CANCELLED: + return this.renderCancelledPhase(); } return null; } @@ -143,8 +184,6 @@ export default class VerificationPanel extends React.PureComponent { await verifier.verify(); } catch (err) { console.error(err); - } finally { - this.setState({sasEvent: null}); } }; @@ -153,7 +192,7 @@ export default class VerificationPanel extends React.PureComponent { }; _onSasMismatchesClick = () => { - this.state.sasEvent.cancel(); + this.state.sasEvent.mismatch(); }; _onVerifierShowSas = (sasEvent) => { @@ -175,7 +214,6 @@ export default class VerificationPanel extends React.PureComponent { request.verifier.removeListener('show_sas', this._onVerifierShowSas); } this._hasVerifier = !!request.verifier; - this.forceUpdate(); // TODO fix this }; componentDidMount() { diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index 693a3769a0..08d0dd422d 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -16,7 +16,6 @@ limitations under the License. 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"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d4512d785d..e926d4ff91 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1139,6 +1139,12 @@ "Verify User": "Verify User", "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.", + "Your messages are not secure": "Your messages are not secure", + "One of the following may be compromised:": "One of the following may be compromised:", + "Your homeserver": "Your homeserver", + "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", + "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", + "Yours, or the other users’ device": "Yours, or the other users’ device", "Members": "Members", "Files": "Files", "Trusted": "Trusted", @@ -1156,11 +1162,15 @@ "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.", "Security": "Security", + "Verify by emoji": "Verify by emoji", + "Verify by comparing unique emoji.": "Verify by comparing unique emoji.", "Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:", "If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.", - "Verify by emoji": "Verify by emoji", "You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!", "Got it": "Got it", + "Verification timed out. Start verification again from their profile.": "Verification timed out. Start verification again from their profile.", + "%(displayName)s cancelled verification. Start verification again from their profile.": "%(displayName)s cancelled verification. Start verification again from their profile.", + "You cancelled verification. Start verification again from their profile.": "You cancelled verification. Start verification again from their profile.", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From c93b080434670d83511acf01940779d2bbfc5d39 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 Jan 2020 11:17:53 +0000 Subject: [PATCH 08/23] delint --- .../views/toasts/VerificationRequestToast.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index 1015ed0313..342b7d840c 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -83,14 +83,15 @@ export default class VerificationRequestToast extends React.PureComponent { }); await request.accept(); const cli = MatrixClientPeg.get(); - dis.dispatch({ - action: "set_right_panel_phase", - phase: RIGHT_PANEL_PHASES.EncryptionPanel, - refireParams: { - verificationRequest: request, - member: cli.getUser(request.otherUserId), - }, - });} else if (request.channel.deviceId && request.verifier) { + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: { + verificationRequest: request, + member: cli.getUser(request.otherUserId), + }, + }); + } else if (request.channel.deviceId && request.verifier) { // show to_device verifications in dialog still const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { From 9821e25e0b8aa5b4845897221af99261c1bf0418 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 28 Jan 2020 15:12:07 +0100 Subject: [PATCH 09/23] Tweak styling of unread indicator circle. This makes the element symmetric and reduces the size of the hit target. With the previous styling, the mid-circle glyph was sometimes offset from the center. Furthermore, the large font size increased the hit box of the ::after element, eating into the scroll up arrow button's hit target. It might be a good idea to give the ::after element pointer-events:none so that the scroll up button is clickable in its entirety. Signed-off-by: Markus Stange --- res/css/views/rooms/_TopUnreadMessagesBar.scss | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index 77f19dac1c..505af9691d 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -25,19 +25,16 @@ limitations under the License. } .mx_TopUnreadMessagesBar::after { - content: "·"; + content: ""; position: absolute; top: -8px; left: 11px; - width: 16px; - height: 16px; + width: 4px; + height: 4px; border-radius: 16px; - font-weight: 600; - font-size: 30px; - line-height: 14px; - text-align: center; - color: $secondary-accent-color; - background-color: $accent-color; + overflow: hidden; + background-color: $secondary-accent-color; + border: 6px solid $accent-color; } .mx_TopUnreadMessagesBar_scrollUp { From abfa593791975e48440f32cdb1bbff6f0491dd88 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jan 2020 16:36:07 +0000 Subject: [PATCH 10/23] Make the 'encryption upgrade' flow better Fixes https://github.com/vector-im/riot-web/issues/12086 --- .../CreateSecretStorageDialog.js | 92 ++++++++++--------- .../keybackup/RestoreKeyBackupDialog.js | 2 +- src/i18n/strings/en_EN.json | 3 +- 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 0867cae6f4..69dd13f46e 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -25,15 +25,14 @@ import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; const PHASE_LOADING = 0; -const PHASE_RESTORE_KEY_BACKUP = 1; -const PHASE_MIGRATE = 2; -const PHASE_PASSPHRASE = 3; -const PHASE_PASSPHRASE_CONFIRM = 4; -const PHASE_SHOWKEY = 5; -const PHASE_KEEPITSAFE = 6; -const PHASE_STORING = 7; -const PHASE_DONE = 8; -const PHASE_OPTOUT_CONFIRM = 9; +const PHASE_MIGRATE = 1; +const PHASE_PASSPHRASE = 2; +const PHASE_PASSPHRASE_CONFIRM = 3; +const PHASE_SHOWKEY = 4; +const PHASE_KEEPITSAFE = 5; +const PHASE_STORING = 6; +const PHASE_DONE = 7; +const PHASE_OPTOUT_CONFIRM = 8; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. @@ -58,7 +57,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { accountPassword: PropTypes.string, }; - defaultProps = { + static defaultProps = { hasCancel: true, }; @@ -110,9 +109,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); - const phase = backupInfo ? - (backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) : - PHASE_PASSPHRASE; + const phase = backupInfo ? PHASE_MIGRATE : PHASE_PASSPHRASE; this.setState({ phase, @@ -151,9 +148,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } + _onSkipClick = () => { + // TODO: add confirmation + this.props.onFinished(false); + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); - this._bootstrapSecretStorage(); + if (this.state.backupSigStatus.usable) { + this._bootstrapSecretStorage(); + } else { + this._restoreBackup(); + } } _onCopyClick = () => { @@ -228,6 +234,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ + accountPassword: '', accountPasswordCorrect: false, phase: PHASE_MIGRATE, }); @@ -246,12 +253,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(true); } - _onRestoreKeyBackupClick = () => { + _restoreBackup = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog( + const { finished } = Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, /* priority = */ false, /* static = */ true, ); + + await finished; + await this._fetchBackupInfo(); + if ( + this.state.backupSigStatus.usable && + this.state.canUploadKeysWithPasswordOnly && + this.state.accountPassword + ) { + this._bootstrapSecretStorage(); + } } _onOptOutClick = () => { @@ -367,23 +384,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _renderPhaseRestoreKeyBackup() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
-

{_t( - "Key Backup is enabled on your account but has not been set " + - "up from this session. To set up secret storage, " + - "restore your key backup.", - )}

- - -
; - } - _renderPhaseMigrate() { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. @@ -392,9 +392,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // https://github.com/vector-im/riot-web/issues/11696 const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let authPrompt; - if (this.state.canUploadKeysWithPasswordOnly) { + let nextCaption = _t("Next"); + if (!this.state.backupSigStatus.usable) { + authPrompt =
+
{_t("Restore your key backup to upgrade your encryption")}
+
; + nextCaption = _t("Restore"); + } else if (this.state.canUploadKeysWithPasswordOnly) { authPrompt =
{_t("Enter your account password to confirm the upgrade:")}
{authPrompt}
- + > + + ; } @@ -678,8 +688,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { - case PHASE_RESTORE_KEY_BACKUP: - return _t('Restore your Key Backup'); case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: @@ -722,9 +730,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADING: content = this._renderBusyPhase(); break; - case PHASE_RESTORE_KEY_BACKUP: - content = this._renderPhaseRestoreKeyBackup(); - break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; @@ -763,6 +768,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { title={this._titleForPhase(this.state.phase)} headerImage={headerImage} hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} + fixedWidth={false} >
{content} diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 0c432ba542..4466775d3a 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -39,7 +39,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { showSummary: PropTypes.bool, }; - defaultProps = { + static defaultProps = { showSummary: true, }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 23ca730d97..75aadc02bd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1991,7 +1991,7 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", - "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.", + "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", "Restore": "Restore", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", @@ -2022,7 +2022,6 @@ "Verify other users in their profile.": "Verify other users in their profile.", "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.", "Set up secret storage": "Set up secret storage", - "Restore your Key Backup": "Restore your Key Backup", "Upgrade your encryption": "Upgrade your encryption", "Recovery key": "Recovery key", "Keep it safe": "Keep it safe", From 58df4127d3b0c3b08c776c15ae84ccb215d68247 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 Jan 2020 17:14:48 +0000 Subject: [PATCH 11/23] Update propTypes and remove todo --- src/components/views/right_panel/EncryptionPanel.js | 7 +++++-- src/components/views/right_panel/VerificationPanel.js | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js index 2c4a896624..d45280e29c 100644 --- a/src/components/views/right_panel/EncryptionPanel.js +++ b/src/components/views/right_panel/EncryptionPanel.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from "react"; +import PropTypes from "prop-types"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; @@ -85,7 +86,9 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => { } }; EncryptionPanel.propTypes = { - + member: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + verificationRequest: PropTypes.object, }; export default EncryptionPanel; diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index 6a5516927a..3740c6e49d 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -39,7 +39,6 @@ export default class VerificationPanel extends React.PureComponent { renderQRPhase(pending) { const {member, request} = this.props; - // TODO change the button into a spinner when on click const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let button; From 546acb696dd7c2313ebab2cc6f4a29122c6c4b1f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jan 2020 17:15:50 +0000 Subject: [PATCH 12/23] Add confirmation to skip button Re-using the opt out phase which was unused --- .../CreateSecretStorageDialog.js | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 69dd13f46e..b08b6c5624 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -32,7 +32,7 @@ const PHASE_SHOWKEY = 4; const PHASE_KEEPITSAFE = 5; const PHASE_STORING = 6; const PHASE_DONE = 7; -const PHASE_OPTOUT_CONFIRM = 8; +const PHASE_CONFIRM_SKIP = 8; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. @@ -148,11 +148,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } - _onSkipClick = () => { - // TODO: add confirmation - this.props.onFinished(false); - } - _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -271,8 +266,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onOptOutClick = () => { - this.setState({phase: PHASE_OPTOUT_CONFIRM}); + _onSkipSetupClick = () => { + this.setState({phase: PHASE_CONFIRM_SKIP}); } _onSetUpClick = () => { @@ -496,7 +491,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { disabled={!this._passPhraseIsValid()} > @@ -564,7 +559,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { disabled={this.state.passPhrase !== this.state.passPhraseConfirm} > @@ -669,19 +664,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - _renderPhaseOptOutConfirm() { + _renderPhaseSkipConfirm() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
{_t( - "Without setting up secret storage, you won't be able to restore your " + - "access to encrypted messages or your cross-signing identity for " + - "verifying other devices if you log out or use another device.", + "Without completing security on this device, it won’t have " + + "access to encrypted messages.", )} - - +
; } @@ -694,8 +688,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return _t('Set up encryption'); case PHASE_PASSPHRASE_CONFIRM: return _t('Confirm passphrase'); - case PHASE_OPTOUT_CONFIRM: - return _t('Warning!'); + case PHASE_CONFIRM_SKIP: + return _t('Are you sure?'); case PHASE_SHOWKEY: return _t('Recovery key'); case PHASE_KEEPITSAFE: @@ -751,8 +745,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_DONE: content = this._renderPhaseDone(); break; - case PHASE_OPTOUT_CONFIRM: - content = this._renderPhaseOptOutConfirm(); + case PHASE_CONFIRM_SKIP: + content = this._renderPhaseSkipConfirm(); break; } } From e1e35a53e43f9e8567da0e671d91cab1e640a555 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jan 2020 17:18:09 +0000 Subject: [PATCH 13/23] i18n --- src/i18n/strings/en_EN.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 75aadc02bd..6a884d6f25 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2020,8 +2020,6 @@ "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.", "Verify other users in their profile.": "Verify other users in their profile.", - "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.", - "Set up secret storage": "Set up secret storage", "Upgrade your encryption": "Upgrade your encryption", "Recovery key": "Recovery key", "Keep it safe": "Keep it safe", From 5e1b42b3505adb53652a9efb60bd7eda8df15a52 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jan 2020 17:21:42 +0000 Subject: [PATCH 14/23] unused component --- .../views/dialogs/secretstorage/CreateSecretStorageDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index b08b6c5624..62a80d0cdc 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -387,7 +387,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // https://github.com/vector-im/riot-web/issues/11696 const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let authPrompt; let nextCaption = _t("Next"); From b0028066395547f9912c7438bbf2e3e70a59f2e2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 Jan 2020 17:42:39 +0000 Subject: [PATCH 15/23] Use DialogButtons because reusability :(( --- .../views/verification/VerificationShowSas.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index 08d0dd422d..4a1628e9e6 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -19,6 +19,7 @@ import PropTypes from 'prop-types'; import { _t, _td } from '../../../languageHandler'; import {PendingActionSpinner} from "../right_panel/EncryptionInfo"; import AccessibleButton from "../elements/AccessibleButton"; +import DialogButtons from "../elements/DialogButtons"; function capFirst(s) { return s.charAt(0).toUpperCase() + s.slice(1); @@ -90,14 +91,15 @@ export default class VerificationShowSas extends React.Component { const text = _t("Waiting for %(displayName)s to verify…", {displayName}); confirm = ; } else { - confirm = - - {_t("They match")} - - - {_t("They don't match")} - - ; + // FIXME: stop using DialogButtons here once it this component is only used in the right panel verification + confirm = ; } return
From 6fb8f6eef16ef7fc2f18871d8eceaaf083bdefab Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 28 Jan 2020 18:03:47 +0000 Subject: [PATCH 16/23] Add device IDs in user info tooltips For easier device identification, add the device ID in an HTML tooltip for now. Fixes https://github.com/vector-im/riot-web/issues/12103 --- src/components/views/right_panel/UserInfo.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 051f92cc9c..394f6b5c98 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -183,11 +183,16 @@ function DeviceItem({userId, device}) { (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : device.getDisplayName(); const trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); - return ( -
-
{deviceName}
-
{trustedLabel}
- ); + return ( + +
+
{deviceName}
+
{trustedLabel}
+ + ); } function DevicesSection({devices, userId, loading}) { From 39c8a9d204016ae1bc0209c2658f3a3ef7ec3835 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jan 2020 19:42:09 +0000 Subject: [PATCH 17/23] Start new key backup in security setup flow Part of https://github.com/vector-im/riot-web/issues/11889 --- .../_CreateSecretStorageDialog.scss | 24 +++++++++++++++++++ .../CreateSecretStorageDialog.js | 19 +++++++++++++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 53e82670e1..bbbf3fc1d3 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -15,6 +15,30 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_CreateSecretStorageDialog { + // Why you ask? Because CompleteSecurityBody is 600px so this is the width + // we end up when in there, but when in our own dialog we set our own width + // so need to fix it to something sensible as otherwise we'd end up either + // really wide or really narrow depending on the phase. I bet you wish you + // never asked. + width: 560px; + + .mx_SettingsFlag { + display: flex; + } + + .mx_SettingsFlag_label { + flex: 1 1 0; + min-width: 0; + font-weight: 600; + } + + .mx_ToggleSwitch { + flex: 0 0 auto; + margin-left: 30px; + } +} + .mx_CreateSecretStorageDialog .mx_Dialog_title { /* TODO: Consider setting this for all dialog titles. */ margin-bottom: 1em; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 62a80d0cdc..4068f72217 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -87,6 +87,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // set if we are 'upgrading' encryption (making an SSSS store from // an existing key backup secret). doingUpgrade: null, + // status of the key backup toggle switch + useKeyBackup: true, }; this._fetchBackupInfo(); @@ -141,13 +143,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onKeyBackupStatusChange = () => { - this._fetchBackupInfo(); + if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } + _onUseKeyBackupChange = (enabled) => { + this.setState({ + useKeyBackup: enabled, + }); + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -222,6 +230,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._keyInfo, keyBackupInfo: this.state.backupInfo, + setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, }); this.setState({ phase: PHASE_DONE, @@ -425,7 +434,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={false} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} > - @@ -436,6 +445,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); let strengthMeter; let helpText; @@ -484,6 +494,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
+ + Date: Tue, 28 Jan 2020 19:55:53 +0000 Subject: [PATCH 18/23] onClick to separate line --- src/components/views/right_panel/UserInfo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 394f6b5c98..e20ab07cae 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -185,8 +185,9 @@ function DeviceItem({userId, device}) { const trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); return (
{deviceName}
From 793ff2cccc3b2359bf384df6e4dc938a4e735765 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 28 Jan 2020 15:36:24 -0500 Subject: [PATCH 19/23] Hide pre-join UTDs (#3881) --- src/components/structures/TimelinePanel.js | 94 ++++++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 65fb00c305..e708fad6a4 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -2,7 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2019 New Vector Ltd -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. @@ -146,6 +146,9 @@ const TimelinePanel = createReactClass({ liveEvents: [], timelineLoading: true, // track whether our room timeline is loading + // the index of the first event that is to be shown + firstVisibleEventIndex: 0, + // canBackPaginate == false may mean: // // * we haven't (successfully) loaded the timeline yet, or: @@ -333,11 +336,12 @@ const TimelinePanel = createReactClass({ // We can now paginate in the unpaginated direction const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate'; - const { events, liveEvents } = this._getEvents(); + const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); this.setState({ [canPaginateKey]: true, events, liveEvents, + firstVisibleEventIndex, }); } }, @@ -369,6 +373,11 @@ const TimelinePanel = createReactClass({ return Promise.resolve(false); } + if (backwards && this.state.firstVisibleEventIndex !== 0) { + debuglog("TimelinePanel: won't", dir, "paginate past first visible event"); + return Promise.resolve(false); + } + debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); this.setState({[paginatingKey]: true}); @@ -377,12 +386,13 @@ const TimelinePanel = createReactClass({ debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); - const { events, liveEvents } = this._getEvents(); + const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); const newState = { [paginatingKey]: false, [canPaginateKey]: r, events, liveEvents, + firstVisibleEventIndex, }; // moving the window in this direction may mean that we can now @@ -402,7 +412,11 @@ const TimelinePanel = createReactClass({ // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - resolve(r); + // we can continue paginating in the given direction if: + // - _timelineWindow.paginate says we can + // - we're paginating forwards, or we won't be trying to + // paginate backwards past the first visible event + resolve(r && (!backwards || firstVisibleEventIndex === 0)); }); }); }); @@ -476,12 +490,13 @@ const TimelinePanel = createReactClass({ this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } - const { events, liveEvents } = this._getEvents(); + const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); const lastLiveEvent = liveEvents[liveEvents.length - 1]; const updatedState = { events, liveEvents, + firstVisibleEventIndex, }; let callRMUpdated; @@ -1115,6 +1130,7 @@ const TimelinePanel = createReactClass({ // get the list of events from the timeline window and the pending event list _getEvents: function() { const events = this._timelineWindow.getEvents(); + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); // Hold onto the live events separately. The read receipt and read marker // should use this list, so that they don't advance into pending events. @@ -1128,9 +1144,72 @@ const TimelinePanel = createReactClass({ return { events, liveEvents, + firstVisibleEventIndex, }; }, + /** + * Check for undecryptable messages that were sent while the user was not in + * the room. + * + * @param {Array} events The timeline events to check + * + * @return {Number} The index within `events` of the event after the most recent + * undecryptable event that was sent while the user was not in the room. If no + * such events were found, then it returns 0. + */ + _checkForPreJoinUISI: function(events) { + const room = this.props.timelineSet.room; + + if (events.length === 0 || !room || + !MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { + return 0; + } + + const userId = MatrixClientPeg.get().credentials.userId; + + // get the user's membership at the last event by getting the timeline + // that the event belongs to, and traversing the timeline looking for + // that event, while keeping track of the user's membership + const lastEvent = events[events.length - 1]; + const timeline = room.getTimelineForEvent(lastEvent.getId()); + const userMembershipEvent = + timeline.getState(EventTimeline.FORWARDS).getMember(userId); + let userMembership = userMembershipEvent + ? userMembershipEvent.membership : "leave"; + const timelineEvents = timeline.getEvents(); + for (let i = timelineEvents.length - 1; i >= 0; i--) { + const event = timelineEvents[i]; + if (event.getId() === lastEvent.getId()) { + // found the last event, so we can stop looking through the timeline + break; + } else if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } + } + + // now go through the events that we have and find the first undecryptable + // one that was sent when the user wasn't in the room + for (let i = events.length - 1; i >= 0; i--) { + const event = events[i]; + if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } else if (userMembership === "leave" && + (event.isDecryptionFailure() || event.isBeingDecrypted())) { + // reached an undecryptable message when the user wasn't in + // the room -- don't try to load any more + // Note: for now, we assume that events that are being decrypted are + // not decryptable + return i + 1; + } + } + return 0; + }, + _indexForEventId: function(evId) { for (let i = 0; i < this.state.events.length; ++i) { if (evId == this.state.events[i].getId()) { @@ -1323,6 +1402,9 @@ const TimelinePanel = createReactClass({ this.state.forwardPaginating || ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); + const events = this.state.firstVisibleEventIndex + ? this.state.events.slice(this.state.firstVisibleEventIndex) + : this.state.events; return (