diff --git a/res/css/_components.scss b/res/css/_components.scss index 51087eba62..22c9b73dca 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -143,7 +143,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/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/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss new file mode 100644 index 0000000000..e13b1b6802 --- /dev/null +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -0,0 +1,26 @@ +/* +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; + } + + text-align: center; + } +} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index ad6254f57c..9ce524c5ac 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 { @@ -76,6 +81,7 @@ limitations under the License. .mx_UserInfo_avatar > div { max-width: 30vh; margin: 0 auto; + transition: 0.5s; } .mx_UserInfo_avatar > div > div { @@ -105,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 0.5s; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { @@ -204,10 +211,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; @@ -256,11 +262,17 @@ 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; } } + +.mx_UserInfo.mx_UserInfo_smallAvatar { + .mx_UserInfo_avatar > div { + 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 new file mode 100644 index 0000000000..75b469cef9 --- /dev/null +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -0,0 +1,37 @@ +/* +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; + } + + .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/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 { diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 0867cae6f4..4068f72217 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_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. @@ -58,7 +57,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { accountPassword: PropTypes.string, }; - defaultProps = { + static defaultProps = { hasCancel: true, }; @@ -88,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(); @@ -110,9 +111,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, @@ -144,16 +143,26 @@ 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(); - this._bootstrapSecretStorage(); + if (this.state.backupSigStatus.usable) { + this._bootstrapSecretStorage(); + } else { + this._restoreBackup(); + } } _onCopyClick = () => { @@ -221,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, @@ -228,6 +238,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,16 +257,26 @@ 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 = () => { - this.setState({phase: PHASE_OPTOUT_CONFIRM}); + _onSkipSetupClick = () => { + this.setState({phase: PHASE_CONFIRM_SKIP}); } _onSetUpClick = () => { @@ -367,23 +388,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.", - )}
-{_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("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("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 } +Not a member nor request, not sure what to render
; +const EncryptionPanel = ({verificationRequest, member, onClose}) => { + const [request, setRequest] = useState(verificationRequest); + useEffect(() => { + setRequest(verificationRequest); + }, [verificationRequest]); + + const [phase, setPhase] = useState(false); + const changeHandler = useCallback(() => { + // 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:{ text }
+ { verifyButton } + { devicesSection } +{ text }
- { verifyButton } - { devicesSection } -{_t("Verify by comparing unique emoji.")}
+ + { button } +{_t("Ask %(displayName)s to scan your code:", { + displayName: member.displayName || member.name || member.userId, + })}
+ +{_t("If you can't scan the code above, verify by comparing unique emoji.")}
+ + { button } +{_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.
- if (request.requested) { - return (Waiting for {request.otherUserId} to accept ...
{request.otherUserId} is ready, start {verifyButton} or have them scan: {qrCode}
); - } + renderCancelledPhase() { + const {member, request} = this.props; - return ({request.otherUserId} is ready, start {verifyButton}
); - } else if (request.started) { - if (this.state.sasWaitingForOtherParty) { - returnWaiting for {request.otherUserId} to confirm ...
; - } else if (this.state.sasEvent) { - const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); - return (Setting up SAS verification...
); - } - } else if (request.done) { - returnverified {request.otherUserId}!!
; - } else if (request.cancelled) { - returncancelled by {request.cancellingUserId}!
; + 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 ( +{ text }
+ +{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} -