From 1d2538a7bcb8240dde72b0ed32fe1ebfbb6ba7f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 15 Jan 2019 18:08:13 +0000 Subject: [PATCH 01/38] First working version of SAS --- src/MatrixClientPeg.js | 2 + src/components/structures/MatrixChat.js | 7 + .../views/dialogs/DeviceVerifyDialog.js | 312 +++++++++++++++--- .../views/dialogs/IncomingSasDialog.js | 218 ++++++++++++ src/i18n/strings/en_EN.json | 21 +- src/settings/Settings.js | 6 + 6 files changed, 517 insertions(+), 49 deletions(-) create mode 100644 src/components/views/dialogs/IncomingSasDialog.js diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 9a77901d2e..0cf67a3551 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -29,6 +29,7 @@ import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; import {phasedRollOutExpiredForUser} from "./PhasedRollOut"; import Modal from './Modal'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; interface MatrixClientCreds { homeserverUrl: string, @@ -184,6 +185,7 @@ class MatrixClientPeg { deviceId: creds.deviceId, timelineSupport: true, forceTURN: SettingsStore.getValue('webRtcForceTURN', false), + verificationMethods: [verificationMethods.SAS] }; this.matrixClient = createMatrixClient(opts, useIndexedDb); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2d1c928494..7167e50fb2 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1277,6 +1277,7 @@ export default React.createClass({ this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); const cli = MatrixClientPeg.get(); + const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); // Allow the JS SDK to reap timeline events. This reduces the amount of // memory consumed as the JS SDK stores multiple distinct copies of room @@ -1476,6 +1477,12 @@ export default React.createClass({ } }); + cli.on("crypto.verification.start", (verifier) => { + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { + verifier, + }); + }); + // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user const colorScheme = SettingsStore.getValue("roomColor"); diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 6bec933389..169dc26c52 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,58 +22,273 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; +import SettingsStore from '../../../settings/SettingsStore'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import {renderSasWaitAccept} from '../../../sas_ui'; -export default function DeviceVerifyDialog(props) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); +const MODE_LEGACY = 'legacy'; +const MODE_SAS = 'sas'; - const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint()); - const body = ( -
-

- { _t("To verify that this device can be trusted, please contact its " + - "owner using some other means (e.g. in person or a phone call) " + - "and ask them whether the key they see in their User Settings " + - "for this device matches the key below:") } -

-
-
    -
  • { props.device.getDisplayName() }
  • -
  • { props.device.deviceId }
  • -
  • { key }
  • -
-
-

- { _t("If it matches, press the verify button below. " + - "If it doesn't, then someone else is intercepting this device " + - "and you probably want to press the blacklist button instead.") } -

-

- { _t("In future this verification process will be more sophisticated.") } -

-
- ); +const PHASE_START = 0; +const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1; +const PHASE_SHOW_SAS = 2; +const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3; +const PHASE_VERIFIED = 4; +const PHASE_CANCELLED = 5; - function onFinished(confirm) { - if (confirm) { - MatrixClientPeg.get().setDeviceVerified( - props.userId, props.device.deviceId, true, - ); - } - props.onFinished(confirm); +export default class DeviceVerifyDialog extends React.Component { + static propTypes = { + userId: PropTypes.string.isRequired, + device: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + this._verifier = null; + this._showSasEvent = null; + this.state = { + phase: PHASE_START, + mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY, + }; } - return ( - - ); + componentWillUnmount() { + if (this._verifier) { + this._verifier.removeListener('show_sas', this._onVerifierShowSas); + this._verifier.cancel('User cancel'); + } + } + + _onSwitchToLegacyClick = () => { + this.setState({mode: MODE_LEGACY}); + } + + _onSwitchToSasClick = () => { + this.setState({mode: MODE_SAS}); + } + + _onCancelClick = () => { + this.props.onFinished(false); + } + + _onLegacyFinished = (confirm) => { + if (confirm) { + MatrixClientPeg.get().setDeviceVerified( + this.props.userId, this.props.device.deviceId, true, + ); + } + this.props.onFinished(confirm); + } + + _onSasRequestClick = () => { + this.setState({ + phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT, + }); + this._verifier = MatrixClientPeg.get().beginKeyVerification( + verificationMethods.SAS, this.props.userId, this.props.device.deviceId, + ); + this._verifier.on('show_sas', this._onVerifierShowSas); + this._verifier.verify().then(() => { + this.setState({phase: PHASE_VERIFIED}); + this._verifier.removeListener('show_sas', this._onVerifierShowSas); + this._verifier = null; + }).catch((e) => { + console.log("Verification failed", e); + this.setState({ + phase: PHASE_CANCELLED, + }); + this._verifier = null; + }); + } + + _onSasMatchesClick = () => { + this._showSasEvent.confirm(); + this.setState({ + phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, + }); + } + + _onVerifiedDoneClick = () => { + this.props.onFinished(true); + } + + _onVerifierShowSas = (e) => { + this._showSasEvent = e; + this.setState({ + phase: PHASE_SHOW_SAS, + }); + } + + _renderSasVerification() { + let body; + switch (this.state.phase) { + case PHASE_START: + body = this._renderSasVerificationPhaseStart(); + break; + case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT: + //body = this._renderSasVerificationPhaseWaitForPartnerToAccept(); + body = renderSasWaitAccept(this.props.userId); + break; + case PHASE_SHOW_SAS: + body = this._renderSasVerificationPhaseShowSas(); + break; + case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: + body = this._renderSasVerificationPhaseWaitForPartnerToConfirm(); + break; + case PHASE_VERIFIED: + body = this._renderSasVerificationPhaseVerified(); + break; + case PHASE_CANCELLED: + body = this._renderSasVerificationPhaseCancelled(); + break; + } + + const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); + return ( + + {body} + + ); + } + + _renderSasVerificationPhaseStart() { + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return ( +
+ + {_t("Use Legacy Verification (for older clients)")} + +

+ { _t("Do clicky clicky button press request verify user send to do.") } +

+ +
+ ); + } + + _renderSasVerificationPhaseShowSas() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Verify this user by confirming the following number appears on their screen" + )}

+

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

+
{this._showSasEvent.sas}
+ +
; + } + + _renderSasVerificationPhaseWaitForPartnerToConfirm() { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return
+ +

{_t( + "Waiting for %(userId)s to confirm...", {userId: this.props.userId}, + )}

+
; + } + + _renderSasVerificationPhaseVerified() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t("Verification complete!")}

+ +
; + } + + _renderSasVerificationPhaseCancelled() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( +
+

{_t( + "%(userId)s cancelled the verification.", {userId: this.props.userId}, + )}

+ +
+ ); + } + + _renderLegacyVerification() { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + + const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint()); + const body = ( +
+ + {_t("Use two-way text verification")} + +

+ { _t("To verify that this device can be trusted, please contact its " + + "owner using some other means (e.g. in person or a phone call) " + + "and ask them whether the key they see in their User Settings " + + "for this device matches the key below:") } +

+
+
    +
  • { this.props.device.getDisplayName() }
  • +
  • { this.props.device.deviceId }
  • +
  • { key }
  • +
+
+

+ { _t("If it matches, press the verify button below. " + + "If it doesn't, then someone else is intercepting this device " + + "and you probably want to press the blacklist button instead.") } +

+

+ { _t("In future this verification process will be more sophisticated.") } +

+
+ ); + + return ( + + ); + } + + render() { + if (this.state.mode === MODE_LEGACY) { + return this._renderLegacyVerification(); + } else { + return
+ {this._renderSasVerification()} +
; + } + } } -DeviceVerifyDialog.propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, -}; diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js new file mode 100644 index 0000000000..732ce2a461 --- /dev/null +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -0,0 +1,218 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; + +const PHASE_START = 0; +const PHASE_SHOW_SAS = 1; +const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2; +const PHASE_VERIFIED = 3; +const PHASE_CANCELLED = 4; + +export default class IncomingSasDialog extends React.Component { + static propTypes = { + verifier: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this._showSasEvent = null; + this.state = { + phase: PHASE_START, + }; + this.props.verifier.on('show_sas', this._onVerifierShowSas); + this.props.verifier.on('cancel', this._onVerifierCancel); + } + + componentWillUnmount() { + if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { + this.props.verifier.cancel('User cancel'); + } + this.props.verifier.removeListener('show_sas', this._onVerifierShowSas); + } + + _onFinished = () => { + this.props.onFinished(this.state.phase === PHASE_VERIFIED); + } + + _onCancelClick = () => { + this.props.onFinished(this.state.phase === PHASE_VERIFIED); + } + + _onContinueClick = () => { + this.setState({phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM}); + this.props.verifier.verify().then(() => { + this.setState({phase: PHASE_VERIFIED}); + }).catch((e) => { + console.log("Verification failed", e); + }); + } + + _onVerifierShowSas = (e) => { + this._showSasEvent = e; + this.setState({ + phase: PHASE_SHOW_SAS, + sas: e.sas, + }); + } + + _onVerifierCancel = (e) => { + this.setState({ + phase: PHASE_CANCELLED, + }); + } + + _onSasMatchesClick = () => { + this._showSasEvent.confirm(); + this.setState({ + phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, + }); + } + + _onVerifiedDoneClick = () => { + this.props.onFinished(true); + } + + _renderPhaseStart() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( +
+

{this.props.verifier.userId}

+

{_t( + "Verify this user to mark them as trusted. " + + "Trusting users gives you extra peace of mind when using " + + "end-to-end encrypted messages." + )}

+

{_t( + // NB. Below wording adjusted to singular 'device' until we have + // cross-signing + "Verifying this user will mark their device as trusted, and " + + "also mark your device as trusted to them" + )}

+ +
+ ); + } + + _renderPhaseShowSas() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Verify this user by confirming the following number appears on their screen" + )}

+

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

+
{this._showSasEvent.sas}
+ +
; + } + + _renderPhaseWaitForPartnerToConfirm() { + const Spinner = sdk.getComponent("views.elements.Spinner"); + + return ( +
+ +

{_t("Waiting for partner to confirm...")}

+
+ ); + } + + _renderPhaseVerified() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( +
+

{_t( + "Verification Complete!" + )}

+ +
+ ); + } + + _renderPhaseCancelled() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( +
+

{_t( + "The other party cancelled the verification." + )}

+ +
+ ); + } + + render() { + console.log("rendering pahse "+this.state.phase); + let body; + switch (this.state.phase) { + case PHASE_START: + body = this._renderPhaseStart(); + break; + case PHASE_SHOW_SAS: + body = this._renderPhaseShowSas(); + break; + case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: + body = this._renderPhaseWaitForPartnerToConfirm(); + break; + case PHASE_VERIFIED: + body = this._renderPhaseVerified(); + break; + case PHASE_CANCELLED: + body = this._renderPhaseCancelled(); + break; + } + + const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); + return ( + + {body} + + ); + } +} + diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0fbed11e20..c26330dda4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -114,6 +114,8 @@ "Failed to invite": "Failed to invite", "Failed to invite users to the room:": "Failed to invite users to the room:", "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", + "Waiting for %(userId)s to accept...": "Waiting for %(userId)s to accept...", + "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", "Unable to create widget.": "Unable to create widget.", @@ -264,6 +266,7 @@ "Backup of encryption keys to server": "Backup of encryption keys to server", "Render simple counters in room header": "Render simple counters in room header", "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", + "Two-way device verification using short text": "Two-way device verification using short text", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Hide removed messages": "Hide removed messages", @@ -938,12 +941,22 @@ "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)", "To continue, please enter your password:": "To continue, please enter your password:", "password": "password", + "Verify device": "Verify device", + "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", + "Do clicky clicky button press request verify user send to do.": "Do clicky clicky button press request verify user send to do.", + "Send Verification Request": "Send Verification Request", + "Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen", + "For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication", + "This Matches": "This Matches", + "Verification complete!": "Verification complete!", + "Done": "Done", + "%(userId)s cancelled the verification.": "%(userId)s cancelled the verification.", + "Use two-way text verification": "Use two-way text verification", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", "Device name": "Device name", "Device key": "Device key", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.", - "Verify device": "Verify device", "I verify that the keys match": "I verify that the keys match", "Back": "Back", "Send Custom Event": "Send Custom Event", @@ -960,6 +973,12 @@ "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", "An error has occurred.": "An error has occurred.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", + "Verifying this user will mark their device as trusted, and also mark your device as trusted to them": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them", + "Waiting for partner to confirm...": "Waiting for partner to confirm...", + "Verification Complete!": "Verification Complete!", + "The other party cancelled the verification.": "The other party cancelled the verification.", + "Incoming Verification Request": "Incoming Verification Request", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Start verification": "Start verification", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 9b46f9406b..9e8cd7a672 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -114,6 +114,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_sas": { + isFeature: true, + displayName: _td("Two-way device verification using short text"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.dontSuggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Disable Emoji suggestions while typing'), From ec2d51cbbbe34c26adaa1ba5072b457022894339 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Jan 2019 16:56:49 +0000 Subject: [PATCH 02/38] SAS verification screen matching design --- res/css/_components.scss | 1 + res/css/views/elements/_HexVerify.scss | 34 ++++++ .../views/dialogs/DeviceVerifyDialog.js | 17 ++- .../views/dialogs/IncomingSasDialog.js | 17 ++- src/components/views/elements/HexVerify.js | 104 ++++++++++++++++++ src/i18n/strings/en_EN.json | 2 +- 6 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 res/css/views/elements/_HexVerify.scss create mode 100644 src/components/views/elements/HexVerify.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 1e2d7ae156..70c2f17e9a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -60,6 +60,7 @@ @import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; +@import "./views/elements/_HexVerify.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_MemberEventListSummary.scss"; diff --git a/res/css/views/elements/_HexVerify.scss b/res/css/views/elements/_HexVerify.scss new file mode 100644 index 0000000000..3f3ee4b7ea --- /dev/null +++ b/res/css/views/elements/_HexVerify.scss @@ -0,0 +1,34 @@ +/* +Copyright 2019 New Vector Ltd. + +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_HexVerify { + text-align: center; +} + +.mx_HexVerify_pair { + display: inline-block; + font-weight: bold; + padding-left: 3px; + padding-right: 3px; +} + +.mx_HexVerify_pair_verified { + color: $accent-color; +} + +.mx_HexVerify_pair:hover{ + color: $accent-color; +} diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 169dc26c52..eabae56e25 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -50,6 +50,7 @@ export default class DeviceVerifyDialog extends React.Component { this.state = { phase: PHASE_START, mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY, + sasVerified: false, }; } @@ -102,6 +103,10 @@ export default class DeviceVerifyDialog extends React.Component { }); } + _onVerifyStateChanged = (newVal) => { + this.setState({sasVerified: newVal}); + } + _onSasMatchesClick = () => { this._showSasEvent.confirm(); this.setState({ @@ -127,7 +132,6 @@ export default class DeviceVerifyDialog extends React.Component { body = this._renderSasVerificationPhaseStart(); break; case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT: - //body = this._renderSasVerificationPhaseWaitForPartnerToAccept(); body = renderSasWaitAccept(this.props.userId); break; case PHASE_SHOW_SAS: @@ -180,6 +184,7 @@ export default class DeviceVerifyDialog extends React.Component { _renderSasVerificationPhaseShowSas() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const HexVerify = sdk.getComponent('views.elements.HexVerify'); return

{_t( "Verify this user by confirming the following number appears on their screen" @@ -188,9 +193,15 @@ export default class DeviceVerifyDialog extends React.Component { "For maximum security, we reccommend you do this in person or use another " + "trusted means of communication" )}

-
{this._showSasEvent.sas}
+ +

{_t( + "To continue, click on each pair to confirm it's correct.", + )}

diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 732ce2a461..947c757f80 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -38,6 +38,7 @@ export default class IncomingSasDialog extends React.Component { this._showSasEvent = null; this.state = { phase: PHASE_START, + sasVerified: false, }; this.props.verifier.on('show_sas', this._onVerifierShowSas); this.props.verifier.on('cancel', this._onVerifierCancel); @@ -81,6 +82,10 @@ export default class IncomingSasDialog extends React.Component { }); } + _onVerifiedStateChange = (newVal) => { + this.setState({sasVerified: newVal}); + } + _onSasMatchesClick = () => { this._showSasEvent.confirm(); this.setState({ @@ -121,6 +126,7 @@ export default class IncomingSasDialog extends React.Component { _renderPhaseShowSas() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const HexVerify = sdk.getComponent('views.elements.HexVerify'); return

{_t( "Verify this user by confirming the following number appears on their screen" @@ -129,9 +135,15 @@ export default class IncomingSasDialog extends React.Component { "For maximum security, we reccommend you do this in person or use another " + "trusted means of communication" )}

-
{this._showSasEvent.sas}
+ +

{_t( + "To continue, click on each pair to confirm it's correct.", + )}

@@ -184,7 +196,6 @@ export default class IncomingSasDialog extends React.Component { } render() { - console.log("rendering pahse "+this.state.phase); let body; switch (this.state.phase) { case PHASE_START: diff --git a/src/components/views/elements/HexVerify.js b/src/components/views/elements/HexVerify.js new file mode 100644 index 0000000000..667857a792 --- /dev/null +++ b/src/components/views/elements/HexVerify.js @@ -0,0 +1,104 @@ +/* +Copyright 2019 New Vector Ltd. + +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. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import { _t } from '../../../languageHandler'; +import classnames from 'classnames'; + +import sdk from '../../../index'; + +class HexVerifyPair extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + index: PropTypes.number, + verified: PropTypes.bool, + onChange: PropTypes.func.isRequired, + } + + _onClick = () => { + this.setState({verified: !this.props.verified}); + this.props.onChange(this.props.index, !this.props.verified); + } + + render() { + const classNames = { + mx_HexVerify_pair: true, + mx_HexVerify_pair_verified: this.props.verified, + }; + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + return + {this.props.text} + + } +} + +/** + * Helps a user verify a hexadecimal code matches one displayed + * elsewhere (eg. on a different device) + */ +export default class HexVerify extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + onVerifiedStateChange: PropTypes.func, + } + + static defaultProps = { + onVerifiedStateChange: function() {}, + } + + constructor(props) { + super(props); + this.state = { + pairsVerified: [], + }; + for (let i = 0; i < props.text.length; i += 2) { + this.state.pairsVerified.push(false); + } + } + + _onPairChange = (index, newVal) => { + const oldVerified = this.state.pairsVerified.reduce((acc, val) => { + return acc && val; + }, true); + const newPairsVerified = this.state.pairsVerified.slice(0); + newPairsVerified[index] = newVal; + const newVerified = newPairsVerified.reduce((acc, val) => { + return acc && val; + }, true); + this.setState({pairsVerified: newPairsVerified}); + if (oldVerified !== newVerified) { + this.props.onVerifiedStateChange(newVerified); + } + } + + render() { + const pairs = []; + + for (let i = 0; i < this.props.text.length / 2; ++i) { + pairs.push(); + } + return
+ {pairs} +
; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c26330dda4..7f61a13a21 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -947,7 +947,7 @@ "Send Verification Request": "Send Verification Request", "Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen", "For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication", - "This Matches": "This Matches", + "To continue, click on each pair to confirm it's correct.": "To continue, click on each pair to confirm it's correct.", "Verification complete!": "Verification complete!", "Done": "Done", "%(userId)s cancelled the verification.": "%(userId)s cancelled the verification.", From 4f2f2f4f4ee57e1d73191d6e64fd9ab39987b70e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Jan 2019 18:24:38 +0000 Subject: [PATCH 03/38] Move common UI bits out to separate components --- .../views/dialogs/DeviceVerifyDialog.js | 65 ++++--------------- .../views/dialogs/IncomingSasDialog.js | 65 +++---------------- .../verification/VerificationCancelled.js | 40 ++++++++++++ .../verification/VerificationComplete.js | 42 ++++++++++++ .../views/verification/VerificationShowSas.js | 65 +++++++++++++++++++ src/i18n/strings/en_EN.json | 25 +++---- 6 files changed, 184 insertions(+), 118 deletions(-) create mode 100644 src/components/views/verification/VerificationCancelled.js create mode 100644 src/components/views/verification/VerificationComplete.js create mode 100644 src/components/views/verification/VerificationShowSas.js diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index eabae56e25..cc5c8c6ef9 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -103,10 +103,6 @@ export default class DeviceVerifyDialog extends React.Component { }); } - _onVerifyStateChanged = (newVal) => { - this.setState({sasVerified: newVal}); - } - _onSasMatchesClick = () => { this._showSasEvent.confirm(); this.setState({ @@ -170,10 +166,16 @@ export default class DeviceVerifyDialog extends React.Component { {_t("Use Legacy Verification (for older clients)")}

- { _t("Do clicky clicky button press request verify user send to do.") } + { _t("Verify by comparing a short text string.") } +

+

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

-

{_t( - "Verify this user by confirming the following number appears on their screen" - )}

-

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

- -

{_t( - "To continue, click on each pair to confirm it's correct.", - )}

- -
; + const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + return } _renderSasVerificationPhaseWaitForPartnerToConfirm() { @@ -219,31 +200,13 @@ export default class DeviceVerifyDialog extends React.Component { } _renderSasVerificationPhaseVerified() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
-

{_t("Verification complete!")}

- -
; + const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); + return } _renderSasVerificationPhaseCancelled() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - - return ( -
-

{_t( - "%(userId)s cancelled the verification.", {userId: this.props.userId}, - )}

- -
- ); + const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); + return } _renderLegacyVerification() { diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 947c757f80..331b4fc67c 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -82,10 +82,6 @@ export default class IncomingSasDialog extends React.Component { }); } - _onVerifiedStateChange = (newVal) => { - this.setState({sasVerified: newVal}); - } - _onSasMatchesClick = () => { this._showSasEvent.confirm(); this.setState({ @@ -125,29 +121,12 @@ export default class IncomingSasDialog extends React.Component { } _renderPhaseShowSas() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const HexVerify = sdk.getComponent('views.elements.HexVerify'); - return
-

{_t( - "Verify this user by confirming the following number appears on their screen" - )}

-

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

- -

{_t( - "To continue, click on each pair to confirm it's correct.", - )}

- -
; + const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + return } _renderPhaseWaitForPartnerToConfirm() { @@ -162,37 +141,13 @@ export default class IncomingSasDialog extends React.Component { } _renderPhaseVerified() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - - return ( -
-

{_t( - "Verification Complete!" - )}

- -
- ); + const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); + return } _renderPhaseCancelled() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - - return ( -
-

{_t( - "The other party cancelled the verification." - )}

- -
- ); + const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); + return } render() { diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js new file mode 100644 index 0000000000..7c08d9eb07 --- /dev/null +++ b/src/components/views/verification/VerificationCancelled.js @@ -0,0 +1,40 @@ +/* +Copyright 2019 Vector Creations Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +export default class VerificationCancelled extends React.Component { + static propTypes = { + onDone: PropTypes.func.isRequired, + } + + render() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "The other party cancelled the verification.", + )}

+ +
; + } +}; diff --git a/src/components/views/verification/VerificationComplete.js b/src/components/views/verification/VerificationComplete.js new file mode 100644 index 0000000000..5153ae6650 --- /dev/null +++ b/src/components/views/verification/VerificationComplete.js @@ -0,0 +1,42 @@ +/* +Copyright 2019 Vector Creations Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +export default class VerificationComplete extends React.Component { + static propTypes = { + onDone: PropTypes.func.isRequired, + } + + render() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t("Verified!")}

+

{_t("You've successfully verified this user.")}

+

{_t( + "Secure messages with this user are end-to-end encrypted and not able to be " + + "read by third parties.", + )}

+ +
; + } +}; diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js new file mode 100644 index 0000000000..b9b5b27ac9 --- /dev/null +++ b/src/components/views/verification/VerificationShowSas.js @@ -0,0 +1,65 @@ +/* +Copyright 2019 Vector Creations Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +export default class VerificationShowSas extends React.Component { + static propTypes = { + onDone: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + sas: PropTypes.string.isRequired, + } + + constructor() { + super(); + this.state = { + sasVerified: false, + }; + } + + _onVerifiedStateChange = (newVal) => { + this.setState({sasVerified: newVal}); + } + + render() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const HexVerify = sdk.getComponent('views.elements.HexVerify'); + return
+

{_t( + "Verify this user by confirming the following number appears on their screen" + )}

+

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

+ +

{_t( + "To continue, click on each pair to confirm it's correct.", + )}

+ +
; + } +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7f61a13a21..8cac02d9bd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -320,6 +320,16 @@ "Incoming call from %(name)s": "Incoming call from %(name)s", "Decline": "Decline", "Accept": "Accept", + "The other party cancelled the verification.": "The other party cancelled the verification.", + "Cancel": "Cancel", + "Verified!": "Verified!", + "You've successfully verified this user.": "You've successfully verified this user.", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", + "Got It": "Got It", + "Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen", + "For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication", + "To continue, click on each pair to confirm it's correct.": "To continue, click on each pair to confirm it's correct.", + "Continue": "Continue", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", "Incorrect verification code": "Incorrect verification code", "Enter Code": "Enter Code", @@ -334,7 +344,6 @@ "Passwords can't be empty": "Passwords can't be empty", "Warning!": "Warning!", "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Continue": "Continue", "Export E2E room keys": "Export E2E room keys", "Do you want to set an email address?": "Do you want to set an email address?", "Current password": "Current password", @@ -648,7 +657,6 @@ "Search…": "Search…", "This Room": "This Room", "All Rooms": "All Rooms", - "Cancel": "Cancel", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -943,14 +951,9 @@ "password": "password", "Verify device": "Verify device", "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", - "Do clicky clicky button press request verify user send to do.": "Do clicky clicky button press request verify user send to do.", - "Send Verification Request": "Send Verification Request", - "Verify this user by confirming the following number appears on their screen": "Verify this user by confirming the following number appears on their screen", - "For maximum security, we reccommend you do this in person or use another trusted means of communication": "For maximum security, we reccommend you do this in person or use another trusted means of communication", - "To continue, click on each pair to confirm it's correct.": "To continue, click on each pair to confirm it's correct.", - "Verification complete!": "Verification complete!", - "Done": "Done", - "%(userId)s cancelled the verification.": "%(userId)s cancelled the verification.", + "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", "Use two-way text verification": "Use two-way text verification", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", "Device name": "Device name", @@ -976,8 +979,6 @@ "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them", "Waiting for partner to confirm...": "Waiting for partner to confirm...", - "Verification Complete!": "Verification Complete!", - "The other party cancelled the verification.": "The other party cancelled the verification.", "Incoming Verification Request": "Incoming Verification Request", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", From 630a6a479bad5e258434b7ec761777cef143d361 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Jan 2019 18:43:40 +0000 Subject: [PATCH 04/38] Lint --- src/components/views/dialogs/DeviceVerifyDialog.js | 10 +++++++--- src/components/views/dialogs/IncomingSasDialog.js | 12 +++++------- src/components/views/elements/HexVerify.js | 7 +++---- .../views/verification/VerificationCancelled.js | 2 +- .../views/verification/VerificationComplete.js | 2 +- .../views/verification/VerificationShowSas.js | 6 +++--- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index cc5c8c6ef9..c2c754c715 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -186,7 +186,11 @@ export default class DeviceVerifyDialog extends React.Component { _renderSasVerificationPhaseShowSas() { const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); - return + return ; } _renderSasVerificationPhaseWaitForPartnerToConfirm() { @@ -201,12 +205,12 @@ export default class DeviceVerifyDialog extends React.Component { _renderSasVerificationPhaseVerified() { const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); - return + return ; } _renderSasVerificationPhaseCancelled() { const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); - return + return ; } _renderLegacyVerification() { diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 331b4fc67c..bad1377457 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -16,10 +16,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; const PHASE_START = 0; const PHASE_SHOW_SAS = 1; @@ -102,13 +100,13 @@ export default class IncomingSasDialog extends React.Component {

{_t( "Verify this user to mark them as trusted. " + "Trusting users gives you extra peace of mind when using " + - "end-to-end encrypted messages." + "end-to-end encrypted messages.", )}

{_t( // NB. Below wording adjusted to singular 'device' until we have // cross-signing "Verifying this user will mark their device as trusted, and " + - "also mark your device as trusted to them" + "also mark your device as trusted to them", )}

+ />; } _renderPhaseWaitForPartnerToConfirm() { @@ -142,12 +140,12 @@ export default class IncomingSasDialog extends React.Component { _renderPhaseVerified() { const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); - return + return ; } _renderPhaseCancelled() { const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); - return + return ; } render() { diff --git a/src/components/views/elements/HexVerify.js b/src/components/views/elements/HexVerify.js index 667857a792..86ead3adc1 100644 --- a/src/components/views/elements/HexVerify.js +++ b/src/components/views/elements/HexVerify.js @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import { _t } from '../../../languageHandler'; import classnames from 'classnames'; import sdk from '../../../index'; @@ -40,15 +39,15 @@ class HexVerifyPair extends React.Component { mx_HexVerify_pair_verified: this.props.verified, }; const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - return {this.props.text} - + ; } } -/** +/* * Helps a user verify a hexadecimal code matches one displayed * elsewhere (eg. on a different device) */ diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js index 7c08d9eb07..b21153f2cc 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.js @@ -37,4 +37,4 @@ export default class VerificationCancelled extends React.Component { />
; } -}; +} diff --git a/src/components/views/verification/VerificationComplete.js b/src/components/views/verification/VerificationComplete.js index 5153ae6650..59f7ff924a 100644 --- a/src/components/views/verification/VerificationComplete.js +++ b/src/components/views/verification/VerificationComplete.js @@ -39,4 +39,4 @@ export default class VerificationComplete extends React.Component { /> ; } -}; +} diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index b9b5b27ac9..60f70e2748 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -42,11 +42,11 @@ export default class VerificationShowSas extends React.Component { const HexVerify = sdk.getComponent('views.elements.HexVerify'); return

{_t( - "Verify this user by confirming the following number appears on their screen" + "Verify this user by confirming the following number appears on their screen", )}

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

; } -}; +} From 60a8d560d1b0e62e67d952338049fe12760a50a3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Sat, 19 Jan 2019 11:54:01 +0000 Subject: [PATCH 05/38] Undo abortive first attempt at factoring out --- src/components/views/dialogs/DeviceVerifyDialog.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index c2c754c715..68a2210814 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -24,7 +24,6 @@ import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; import SettingsStore from '../../../settings/SettingsStore'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; -import {renderSasWaitAccept} from '../../../sas_ui'; const MODE_LEGACY = 'legacy'; const MODE_SAS = 'sas'; @@ -128,7 +127,7 @@ export default class DeviceVerifyDialog extends React.Component { body = this._renderSasVerificationPhaseStart(); break; case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT: - body = renderSasWaitAccept(this.props.userId); + body = this._renderSasVerificationPhaseWaitAccept(); break; case PHASE_SHOW_SAS: body = this._renderSasVerificationPhaseShowSas(); @@ -184,6 +183,17 @@ export default class DeviceVerifyDialog extends React.Component { ); } + _renderSasVerificationPhaseWaitAccept() { + const Spinner = sdk.getComponent("views.elements.Spinner"); + + return ( +
+ +

{_t("Waiting for partner to accept...")}

+
+ ); + } + _renderSasVerificationPhaseShowSas() { const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); return Date: Wed, 23 Jan 2019 18:30:51 +0100 Subject: [PATCH 06/38] WIP to port prototype code --- src/resizer/distributors/roomsublist2.js | 280 +++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 src/resizer/distributors/roomsublist2.js diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js new file mode 100644 index 0000000000..c720152962 --- /dev/null +++ b/src/resizer/distributors/roomsublist2.js @@ -0,0 +1,280 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +const allowWhitespace = true; +const blendOverflow = false; +const handleHeight = 1; + +function log(...params) { + // console.log.apply(console, params); +} + +function clamp(height, min, max) { + //log(`clamping ${height} between ${min} and ${max}`); + if (height > max) return max; + if (height < min) return min; + return height; +} + +export class Layout { + constructor(applyHeight, initialSizes, collapsedState) { + this._applyHeight = applyHeight; + this._sections = []; + this._collapsedState = collapsedState || {}; + this._availableHeight = 0; + // need to store heights by id so it doesn't get + // assigned to wrong section when a section gets added? + this._sectionHeights = initialSizes || {}; + this._originalHeights = []; + this._heights = []; + } + + setAvailableHeight(newSize) { + this._availableHeight = newSize; + // needs more work + this._applyNewSize(); + } + + setCollapsed(id, collapsed) { + this._collapsedState[id] = collapsed; + this._applyNewSize(); + } + // [{id, count}] + setSections(sections) { + this._sections = sections; + this._applyNewSize(); + } + + openHandle(id) { + return new Handle(this, this._getSectionIndex(id)); + } + + _getAvailableHeight() { + const nonCollapsedSectionCount = this._sections.reduce((count, section) => { + const collapsed = this._collapsedState[section.id]; + return count + (collapsed ? 0 : 1); + }); + return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight); + } + + _applyNewSize() { + const height = this._getAvailableHeight(); + const sectionHeights = this._sections.map((section) => { + return this._sectionHeight[section.id] || (height / this._sections.length); + }); + const totalRequestedHeight = sectionHeights.reduce((sum, h) => h + sum, 0); + const ratios = sectionHeights.map((h) => h / totalRequestedHeight); + this._originalHeights = ratios.map((r) => r * height); + this._heights = this._originalHeights.slice(0); + this._relayout(); + } + + _getSectionIndex(id) { + return this._sections.findIndex((s) => s.id === id); + } + + _getMaxHeight(i) { + const section = this._sections[i]; + const collapsed = this._collapsedState[section.id]; + + if (collapsed) { + return this._sectionHeight(0); + } else { + return this._sectionHeight(section.count); + } + } + + _sectionHeight(count) { + return 36 + (count === 0 ? 0 : 4 + (count * 34)); + } + + _getMinHeight(i) { + const section = this._sections[i]; + return this._sectionHeight(Math.min(section.count, 1)); + } + + _applyOverflow(overflow, sections) { + //log("applyOverflow", overflow, sections); + // take the given overflow amount, and applies it to the given sections. + // calls itself recursively until it has distributed all the overflow + // or run out of unclamped sections. + + let unclampedSections = []; + + let overflowPerSection = blendOverflow ? (overflow / sections.length) : overflow; + for (const i of sections) { + newHeight = clamp(this._heights[i] - overflow, this._getMinHeight(i), this._getMaxHeight(i)); + if (newHeight == this._heights[i] - overflow) { + unclampedSections.push(i); + } + overflow -= this._heights[i] - newHeight; + log(`heights[${i}] (${this._heights[i]}) - newHeight (${newHeight}) = ${this._heights[i] - newHeight}`); + + // log(`changing ${this._heights[i]} to ${newHeight}`); + this._heights[i] = newHeight; + + //log(`for section ${i} overflow is ${overflow}`); + + if (!blendOverflow) { + overflowPerSection = overflow; + if (Math.abs(overflow) < 1.0) break; + } + } + + if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) { + // we weren't able to distribute all the overflow so recurse and try again + log("recursing with", overflow, unclampedSections); + overflow = this._applyOverflow(overflow, unclampedSections); + } + + return overflow; + } + + _rebalanceAbove(overflowAbove) { + if (Math.abs(overflowAbove) > 1.0) { + log(`trying to rebalance upstream with ${overflowAbove}`); + let sections = []; + for (let i = anchor - 1; i >= 1; i--) { + sections.push(i); + } + overflowAbove = this._applyOverflow(overflowAbove, sections); + } + return overflowAbove; + } + + _rebalanceBelow(overflowBelow) { + if (Math.abs(overflowBelow) > 1.0) { + log(`trying to rebalance downstream with ${overflowBelow}`); + let sections = []; + for (let i = anchor + 1; i <= this._sections.length; i++) { + sections.push(i); + } + overflowBelow = this._applyOverflow(overflowBelow, sections); + //log(`rebalanced downstream with ${overflowBelow}`); + } + return overflowBelow; + } + + // @param offset the amount the anchor is moved from what is stored in _originalHeights, positive if downwards + // if we're clamped, return the offset we should be clamped at. + _relayout(anchor = 0, offset = 0, clamped = false) { + this._heights = this._originalHeights.slice(0); + // are these the amounts the items above/below shrank/grew and need to be relayouted? + let overflowAbove; + let overflowBelow; + const maxHeight = this._getMaxHeight(anchor); + const minHeight = this._getMinHeight(anchor); + // new height > max ? + if (this._heights[anchor] + offset > maxHeight) { + // we're pulling downwards and clamped + // overflowAbove = minus how much are we above max height? + overflowAbove = (maxHeight - this._heights[anchor]) - offset; + overflowBelow = offset; + log(`pulling downwards clamped at max: ${overflowAbove} ${overflowBelow}`); + } + // new height < min? + else if (this._heights[anchor] + offset < minHeight) { + // we're pulling upwards and clamped + // overflowAbove = ??? (offset is negative here, so - offset will add) + overflowAbove = (minHeight - this._heights[anchor]) - offset; + overflowBelow = offset; + log(`pulling upwards clamped at min: ${overflowAbove} ${overflowBelow}`); + } + else { + overflowAbove = 0; + overflowBelow = offset; + log(`resizing the anchor: ${overflowAbove} ${overflowBelow}`); + } + this._heights[anchor] = clamp(this._heights[anchor] + offset, minHeight, maxHeight); + + // these are reassigned the amount of overflow that could not be rebalanced + // meaning we dragged the handle too far and it can't follow the cursor anymore + overflowAbove = this._rebalanceAbove(overflowAbove); + overflowBelow = this._rebalanceBelow(overflowBelow); + + if (!clamped) { // to avoid risk of infinite recursion + // clamp to avoid overflowing or underflowing the page + if (Math.abs(overflowAbove) > 1.0) { + log(`clamping with overflowAbove ${overflowAbove}`); + // here we do the layout again with offset - the amount of space we took too much + this._relayout(anchor, offset + overflowAbove, true); + return offset + overflowAbove; + } + + if (Math.abs(overflowBelow) > 1.0) { + // here we do the layout again with offset - the amount of space we took too much + log(`clamping with overflowBelow ${overflowBelow}`); + this._relayout(anchor, offset - overflowBelow, true); + return offset - overflowBelow; + } + } + + // apply the heights + for (let i = 0; i < this._sections.length; i++) { + const section = this._sections[i]; + this._applyHeight(section.id, this._heights[i]); + // const roomSubList = document.getElementById(`roomSubList${i}`); + // roomSubList.style.height = `${heights[i]}px`; + } + + return undefined; + } + + _commitHeights() { + this._originalHeights = this._heights; + } +} + +class Handle { + constructor(layout, anchor) { + this._layout = layout; + this._anchor = anchor; + } + + setOffset(offset) { + this._layout._relayout(this._anchor, offset); + } + + finish() { + this._layout._commitHeights(); + } +} + +export class Distributor { + constructor(item, cfg) { + this._item = item; + this._layout = cfg.layout; + this._initialTop; + } + + start() { + this._handle = this._layout.openHandle(this._item.id); + this._initialTop = this._item.getOffset(); + } + + finish() { + this._handle.finish(); + } + + resize() { + // not supported + } + + resizeFromContainerOffset(containerOffset) { + const offset = containerOffset - this._initialTop; + this._handle.setOffset(offset); + } +} From 6e35a7f3d9bb2ddbdfa28c7c809d294b9b17edab Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Jan 2019 17:41:15 +0000 Subject: [PATCH 07/38] Re-do npm run i18n because things got weird --- src/i18n/strings/en_EN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9ab0d6b428..fdff3d4199 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -441,6 +441,7 @@ "Encrypted by an unverified device": "Encrypted by an unverified device", "Unencrypted message": "Unencrypted message", "Please select the destination room for this message": "Please select the destination room for this message", + "Scroll to bottom of page": "Scroll to bottom of page", "Blacklisted": "Blacklisted", "Verified": "Verified", "Unverified": "Unverified", @@ -923,6 +924,8 @@ "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...", + "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", "Use two-way text verification": "Use two-way text verification", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", "Device name": "Device name", @@ -1083,6 +1086,8 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", + "Login": "Login", + "powered by Matrix": "powered by Matrix", "Robot check is currently unavailable on desktop - please use a web browser": "Robot check is currently unavailable on desktop - please use a web browser", "This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot", "Custom Server Options": "Custom Server Options", @@ -1100,7 +1105,6 @@ "Please enter the code it contains:": "Please enter the code it contains:", "Code": "Code", "Start authentication": "Start authentication", - "powered by Matrix": "powered by Matrix", "The email field must not be blank.": "The email field must not be blank.", "The user name field must not be blank.": "The user name field must not be blank.", "The phone number field must not be blank.": "The phone number field must not be blank.", @@ -1173,7 +1177,6 @@ "Couldn't load home page": "Couldn't load home page", "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", - "Login": "Login", "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", @@ -1215,7 +1218,6 @@ "Directory": "Directory", "Search for a room": "Search for a room", "#example": "#example", - "Scroll to bottom of page": "Scroll to bottom of page", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Show devices, send anyway or cancel.": "Show devices, send anyway or cancel.", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", @@ -1227,8 +1229,6 @@ "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Resend message or cancel message now.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", - "%(count)s new messages|other": "%(count)s new messages", - "%(count)s new messages|one": "%(count)s new message", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", From 1092244bbf7f85c3df32b5a736db1c005a3fdcca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Jan 2019 15:43:23 +0100 Subject: [PATCH 08/38] more fixes for updates/resizing --- src/resizer/distributors/roomsublist2.js | 105 ++++++++++++----------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index c720152962..2a815cf3b0 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -const allowWhitespace = true; +import FixedDistributor from "./fixed"; + +// const allowWhitespace = true; const blendOverflow = false; const handleHeight = 1; function log(...params) { - // console.log.apply(console, params); + console.log.apply(console, params); } function clamp(height, min, max) { @@ -53,31 +55,46 @@ export class Layout { this._applyNewSize(); } // [{id, count}] - setSections(sections) { + update(sections, availableHeight) { + if (Number.isFinite(availableHeight)) { + this._availableHeight = availableHeight; + } + + this._sections.forEach((section, i) => { + this._sectionHeights[section.id] = this._originalHeights[i]; + }); + this._sections = sections; this._applyNewSize(); } openHandle(id) { - return new Handle(this, this._getSectionIndex(id)); + const index = this._getSectionIndex(id); + //log(`openHandle resolved ${id} to ${index}`); + return new Handle(this, index); } _getAvailableHeight() { const nonCollapsedSectionCount = this._sections.reduce((count, section) => { const collapsed = this._collapsedState[section.id]; return count + (collapsed ? 0 : 1); - }); + }, 0); return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight); } _applyNewSize() { const height = this._getAvailableHeight(); const sectionHeights = this._sections.map((section) => { - return this._sectionHeight[section.id] || (height / this._sections.length); + return this._sectionHeights[section.id] || (height / this._sections.length); }); const totalRequestedHeight = sectionHeights.reduce((sum, h) => h + sum, 0); const ratios = sectionHeights.map((h) => h / totalRequestedHeight); this._originalHeights = ratios.map((r) => r * height); + // re-assign adjusted heights + this._sections.forEach((section, i) => { + this._sectionHeights[section.id] = this._originalHeights[i]; + }); + log("_applyNewSize", height, this._sections, sectionHeights, ratios, this._originalHeights); this._heights = this._originalHeights.slice(0); this._relayout(); } @@ -87,6 +104,8 @@ export class Layout { } _getMaxHeight(i) { + return 100000; + /* const section = this._sections[i]; const collapsed = this._collapsedState[section.id]; @@ -95,6 +114,7 @@ export class Layout { } else { return this._sectionHeight(section.count); } + */ } _sectionHeight(count) { @@ -103,6 +123,7 @@ export class Layout { _getMinHeight(i) { const section = this._sections[i]; + log("_getMinHeight", i, section); return this._sectionHeight(Math.min(section.count, 1)); } @@ -112,11 +133,11 @@ export class Layout { // calls itself recursively until it has distributed all the overflow // or run out of unclamped sections. - let unclampedSections = []; + const unclampedSections = []; - let overflowPerSection = blendOverflow ? (overflow / sections.length) : overflow; + // let overflowPerSection = blendOverflow ? (overflow / sections.length) : overflow; for (const i of sections) { - newHeight = clamp(this._heights[i] - overflow, this._getMinHeight(i), this._getMaxHeight(i)); + const newHeight = clamp(this._heights[i] - overflow, this._getMinHeight(i), this._getMaxHeight(i)); if (newHeight == this._heights[i] - overflow) { unclampedSections.push(i); } @@ -129,7 +150,7 @@ export class Layout { //log(`for section ${i} overflow is ${overflow}`); if (!blendOverflow) { - overflowPerSection = overflow; + // overflowPerSection = overflow; if (Math.abs(overflow) < 1.0) break; } } @@ -143,11 +164,11 @@ export class Layout { return overflow; } - _rebalanceAbove(overflowAbove) { + _rebalanceAbove(anchor, overflowAbove) { if (Math.abs(overflowAbove) > 1.0) { - log(`trying to rebalance upstream with ${overflowAbove}`); - let sections = []; - for (let i = anchor - 1; i >= 1; i--) { + // log(`trying to rebalance upstream with ${overflowAbove}`); + const sections = []; + for (let i = anchor - 1; i >= 0; i--) { sections.push(i); } overflowAbove = this._applyOverflow(overflowAbove, sections); @@ -155,11 +176,11 @@ export class Layout { return overflowAbove; } - _rebalanceBelow(overflowBelow) { + _rebalanceBelow(anchor, overflowBelow) { if (Math.abs(overflowBelow) > 1.0) { - log(`trying to rebalance downstream with ${overflowBelow}`); - let sections = []; - for (let i = anchor + 1; i <= this._sections.length; i++) { + // log(`trying to rebalance downstream with ${overflowBelow}`); + const sections = []; + for (let i = anchor + 1; i < this._sections.length; i++) { sections.push(i); } overflowBelow = this._applyOverflow(overflowBelow, sections); @@ -183,32 +204,29 @@ export class Layout { // overflowAbove = minus how much are we above max height? overflowAbove = (maxHeight - this._heights[anchor]) - offset; overflowBelow = offset; - log(`pulling downwards clamped at max: ${overflowAbove} ${overflowBelow}`); - } - // new height < min? - else if (this._heights[anchor] + offset < minHeight) { + // log(`pulling downwards clamped at max: ${overflowAbove} ${overflowBelow}`); + } else if (this._heights[anchor] + offset < minHeight) { // new height < min? // we're pulling upwards and clamped // overflowAbove = ??? (offset is negative here, so - offset will add) overflowAbove = (minHeight - this._heights[anchor]) - offset; overflowBelow = offset; - log(`pulling upwards clamped at min: ${overflowAbove} ${overflowBelow}`); - } - else { + // log(`pulling upwards clamped at min: ${overflowAbove} ${overflowBelow}`); + } else { overflowAbove = 0; overflowBelow = offset; - log(`resizing the anchor: ${overflowAbove} ${overflowBelow}`); + // log(`resizing the anchor: ${overflowAbove} ${overflowBelow}`); } this._heights[anchor] = clamp(this._heights[anchor] + offset, minHeight, maxHeight); // these are reassigned the amount of overflow that could not be rebalanced // meaning we dragged the handle too far and it can't follow the cursor anymore - overflowAbove = this._rebalanceAbove(overflowAbove); - overflowBelow = this._rebalanceBelow(overflowBelow); + overflowAbove = this._rebalanceAbove(anchor, overflowAbove); + overflowBelow = this._rebalanceBelow(anchor, overflowBelow); if (!clamped) { // to avoid risk of infinite recursion // clamp to avoid overflowing or underflowing the page if (Math.abs(overflowAbove) > 1.0) { - log(`clamping with overflowAbove ${overflowAbove}`); + // log(`clamping with overflowAbove ${overflowAbove}`); // here we do the layout again with offset - the amount of space we took too much this._relayout(anchor, offset + overflowAbove, true); return offset + overflowAbove; @@ -216,25 +234,26 @@ export class Layout { if (Math.abs(overflowBelow) > 1.0) { // here we do the layout again with offset - the amount of space we took too much - log(`clamping with overflowBelow ${overflowBelow}`); + // log(`clamping with overflowBelow ${overflowBelow}`); this._relayout(anchor, offset - overflowBelow, true); return offset - overflowBelow; } } + log("updating layout, heights are now", this._heights); // apply the heights for (let i = 0; i < this._sections.length; i++) { const section = this._sections[i]; this._applyHeight(section.id, this._heights[i]); - // const roomSubList = document.getElementById(`roomSubList${i}`); - // roomSubList.style.height = `${heights[i]}px`; } return undefined; } _commitHeights() { - this._originalHeights = this._heights; + const heights = this._heights.slice(0); + log("committing heights:", heights); + this._originalHeights = heights; } } @@ -253,28 +272,18 @@ class Handle { } } -export class Distributor { +export class Distributor extends FixedDistributor { constructor(item, cfg) { - this._item = item; - this._layout = cfg.layout; - this._initialTop; - } - - start() { - this._handle = this._layout.openHandle(this._item.id); - this._initialTop = this._item.getOffset(); + super(item); + const layout = cfg.layout; + this._handle = layout.openHandle(item.id); } finish() { this._handle.finish(); } - resize() { - // not supported - } - - resizeFromContainerOffset(containerOffset) { - const offset = containerOffset - this._initialTop; + resize(offset) { this._handle.setOffset(offset); } } From 067a861f807ecc7f160e3fa867fc1d83878cb298 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Jan 2019 15:43:49 +0100 Subject: [PATCH 09/38] integrate layout/distributor with RoomList --- src/components/structures/RoomSubList.js | 12 +++++-- src/components/views/rooms/RoomList.js | 46 +++++++++++++++++------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index cda1c9967e..852dddd063 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -313,6 +313,12 @@ const RoomSubList = React.createClass({ } }, + setHeight: function(height) { + if (this.refs.subList) { + this.refs.subList.style.height = `${height}px`; + } + }, + render: function() { const len = this.props.list.length + this.props.extraTiles.length; if (len) { @@ -322,13 +328,13 @@ const RoomSubList = React.createClass({ "mx_RoomSubList_nonEmpty": len && !this.state.hidden, }); if (this.state.hidden) { - return
+ return
{this._getHeaderJsx()}
; } else { const tiles = this.makeRoomTiles(); tiles.push(...this.props.extraTiles); - return
+ return
{this._getHeaderJsx()} { tiles } @@ -343,7 +349,7 @@ const RoomSubList = React.createClass({ } return ( -
+
{ this._getHeaderJsx() } { content }
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1a714e3a84..647e755456 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -36,7 +36,8 @@ import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; import ResizeHandle from '../elements/ResizeHandle'; -import {Resizer, RoomSubListDistributor} from '../../../resizer' +import {Resizer} from '../../../resizer' +import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -79,6 +80,20 @@ module.exports = React.createClass({ const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {}; + this._layoutSections = []; + + this._layout = new Layout((key, size) => { + const subList = this._subListRefs[key]; + if (subList) { + subList.setHeight(size); + } + this.subListSizes[key] = size; + window.localStorage.setItem("mx_roomlist_sizes", + JSON.stringify(this.subListSizes)); + // update overflow indicators + this._checkSubListsOverflow(); + }, this.subListSizes, this.collapsedState); + return { isLoadingLeftRooms: false, totalRoomCount: null, @@ -166,19 +181,18 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); const cfg = { - onResized: this._onSubListResize, + layout: this._layout, }; - this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg); + this.resizer = new Resizer(this.resizeContainer, Distributor, cfg); this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", reverse: "mx_ResizeHandle_reverse" }); - - // load stored sizes - Object.keys(this.subListSizes).forEach((key) => { - this._restoreSubListSize(key); - }); + this._layout.update( + this._layoutSections, + this.resizeContainer && this.resizeContainer.clientHeight, + ); this._checkSubListsOverflow(); this.resizer.attach(); @@ -194,6 +208,11 @@ module.exports = React.createClass({ }); this._checkSubListsOverflow(); } + this._layout.update( + this._layoutSections, + this.resizeContainer && this.resizeContainer.clientHeight, + ); + // TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed }, onAction: function(payload) { @@ -551,9 +570,7 @@ module.exports = React.createClass({ this.collapsedState[key] = collapsed; window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); // load the persisted size configuration of the expanded sub list - if (!collapsed) { - this._restoreSubListSize(key); - } + this._layout.setCollapsed(key, collapsed); // check overflow, as sub lists sizes have changed // important this happens after calling resize above this._checkSubListsOverflow(); @@ -581,6 +598,7 @@ module.exports = React.createClass({ }, _mapSubListProps: function(subListsProps) { + this._layoutSections = []; const defaultProps = { collapsed: this.props.collapsed, isFiltered: !!this.props.searchFilter, @@ -599,6 +617,7 @@ module.exports = React.createClass({ return subListsProps.reduce((components, props, i) => { props = Object.assign({}, defaultProps, props); const isLast = i === subListsProps.length - 1; + const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); const {key, label, onHeaderClick, ... otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { @@ -608,7 +627,10 @@ module.exports = React.createClass({ } }; const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; - + this._layoutSections.push({ + id: chosenKey, + count: len, + }); let subList = ( Date: Thu, 24 Jan 2019 15:44:16 +0100 Subject: [PATCH 10/38] remove flexbox layout --- res/css/structures/_RoomSubList.scss | 25 ++----------------------- res/css/views/rooms/_RoomList.scss | 9 +++++---- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index faaf1cf462..e403057cd3 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -32,34 +32,13 @@ limitations under the License. */ .mx_RoomSubList { - min-height: 31px; - flex: 0 10000 auto; display: flex; flex-direction: column; } -.mx_RoomSubList.resized-sized { - /* - flex-basis to 0 so sublists - are not shrinking/growing relative - to their content (as would be the case with auto), - as this intervenes with sizing an item exactly - when not available space is available - in the flex container - */ - flex: 1 1 0; -} -.mx_RoomSubList_nonEmpty { - min-height: 74px; - - .mx_AutoHideScrollbar_offset { - padding-bottom: 4px; - } -} - -.mx_RoomSubList_hidden { - flex: none !important; +.mx_RoomSubList_nonEmpty .mx_AutoHideScrollbar_offset { + padding-bottom: 4px; } .mx_RoomSubList_labelContainer { diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 8f78e3bb7a..360966a952 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -17,13 +17,14 @@ limitations under the License. .mx_RoomList { /* take up remaining space below TopLeftMenu */ - flex: 1 1 auto; - /* use flexbox to layout sublists */ - display: flex; - flex-direction: column; + flex: 1; min-height: 0; } +.mx_RoomList .mx_ResizeHandle { + position: relative; +} + .mx_SearchBox { flex: none; } From b230e65e74da43e179b4fd571d2abc903bdbada0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Jan 2019 16:44:36 +0100 Subject: [PATCH 11/38] prevent height doubling when resizing caused by mixing up absolute height with incremental height --- src/resizer/distributors/roomsublist2.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index 2a815cf3b0..2db4db9b80 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -71,7 +71,7 @@ export class Layout { openHandle(id) { const index = this._getSectionIndex(id); //log(`openHandle resolved ${id} to ${index}`); - return new Handle(this, index); + return new Handle(this, index, this._originalHeights[index]); } _getAvailableHeight() { @@ -258,13 +258,14 @@ export class Layout { } class Handle { - constructor(layout, anchor) { + constructor(layout, anchor, height) { this._layout = layout; this._anchor = anchor; + this._initialHeight = height; } - setOffset(offset) { - this._layout._relayout(this._anchor, offset); + setHeight(height) { + this._layout._relayout(this._anchor, height - this._initialHeight); } finish() { @@ -283,7 +284,7 @@ export class Distributor extends FixedDistributor { this._handle.finish(); } - resize(offset) { - this._handle.setOffset(offset); + resize(height) { + this._handle.setHeight(height); } } From 636955daef3c27b67b73b6b20add81528abe4ce1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Jan 2019 16:45:26 +0100 Subject: [PATCH 12/38] use offsetHeight for consistency with sizer --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 647e755456..0c910f6808 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -191,7 +191,7 @@ module.exports = React.createClass({ }); this._layout.update( this._layoutSections, - this.resizeContainer && this.resizeContainer.clientHeight, + this.resizeContainer && this.resizeContainer.offsetHeight, ); this._checkSubListsOverflow(); From 4eb2555fc1c6933360c1e66b27751a89f748e2a1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Jan 2019 18:18:10 +0100 Subject: [PATCH 13/38] initial support for collapsing --- src/resizer/distributors/roomsublist2.js | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index 2db4db9b80..099d533166 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -84,19 +84,25 @@ export class Layout { _applyNewSize() { const height = this._getAvailableHeight(); - const sectionHeights = this._sections.map((section) => { + // we should only scale the section here between min and max size + const requestedHeights = this._sections.map((section) => { return this._sectionHeights[section.id] || (height / this._sections.length); }); - const totalRequestedHeight = sectionHeights.reduce((sum, h) => h + sum, 0); - const ratios = sectionHeights.map((h) => h / totalRequestedHeight); - this._originalHeights = ratios.map((r) => r * height); + const totalRequestedHeight = requestedHeights.reduce((sum, h) => h + sum, 0); + const ratios = requestedHeights.map((h) => h / totalRequestedHeight); + this._originalHeights = ratios.map((r, i) => clamp(r * height, this._getMinHeight(i), this._getMaxHeight(i))); + const totalRequestedHeight2 = requestedHeights.reduce((sum, h) => h + sum, 0); + const overflow = height - totalRequestedHeight2; // re-assign adjusted heights this._sections.forEach((section, i) => { this._sectionHeights[section.id] = this._originalHeights[i]; }); - log("_applyNewSize", height, this._sections, sectionHeights, ratios, this._originalHeights); + log("_applyNewSize", height, this._sections, requestedHeights, ratios, this._originalHeights); this._heights = this._originalHeights.slice(0); this._relayout(); + if (overflow) { + this._applyOverflow(overflow, this._sections.map((_, i) => i)); + } } _getSectionIndex(id) { @@ -104,17 +110,15 @@ export class Layout { } _getMaxHeight(i) { - return 100000; - /* const section = this._sections[i]; const collapsed = this._collapsedState[section.id]; if (collapsed) { return this._sectionHeight(0); } else { + // return 100000; return this._sectionHeight(section.count); } - */ } _sectionHeight(count) { @@ -123,8 +127,10 @@ export class Layout { _getMinHeight(i) { const section = this._sections[i]; - log("_getMinHeight", i, section); - return this._sectionHeight(Math.min(section.count, 1)); + const collapsed = this._collapsedState[section.id]; + const maxItems = collapsed ? 0 : 1; + // log("_getMinHeight", i, section); + return this._sectionHeight(Math.min(section.count, maxItems)); } _applyOverflow(overflow, sections) { From b0a8cbf75fac98146083b28752532d63f2b01964 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 15:47:04 -0700 Subject: [PATCH 14/38] Implement the "Security & Privacy" tab of new user settings --- res/css/_components.scss | 1 + .../settings/tabs/_SecuritySettingsTab.scss | 53 ++++ res/css/views/settings/tabs/_SettingsTab.scss | 10 +- .../views/dialogs/UserSettingsDialog.js | 3 +- .../views/settings/KeyBackupPanel.js | 9 +- .../settings/tabs/SecuritySettingsTab.js | 242 ++++++++++++++++++ src/i18n/strings/en_EN.json | 27 +- 7 files changed, 324 insertions(+), 21 deletions(-) create mode 100644 res/css/views/settings/tabs/_SecuritySettingsTab.scss create mode 100644 src/components/views/settings/tabs/SecuritySettingsTab.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 25b7c9342e..6e644802cc 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -137,6 +137,7 @@ @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/tabs/_GeneralSettingsTab.scss"; @import "./views/settings/tabs/_PreferencesSettingsTab.scss"; +@import "./views/settings/tabs/_SecuritySettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/_VoiceSettingsTab.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/settings/tabs/_SecuritySettingsTab.scss b/res/css/views/settings/tabs/_SecuritySettingsTab.scss new file mode 100644 index 0000000000..2640df1383 --- /dev/null +++ b/res/css/views/settings/tabs/_SecuritySettingsTab.scss @@ -0,0 +1,53 @@ +/* +Copyright 2019 New Vector Ltd + +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_SecuritySettingsTab .mx_DevicesPanel { + // Normally the panel is 880px, however this can easily overflow the container. + // TODO: Fix the table to not be squishy + width: auto; + max-width: 880px; +} + +.mx_SecuritySettingsTab_deviceInfo { + display: table; + padding-left: 0; +} + +.mx_SecuritySettingsTab_deviceInfo > li { + display: table-row; +} + +.mx_SecuritySettingsTab_deviceInfo > li > label, +.mx_SecuritySettingsTab_deviceInfo > li > span { + display: table-cell; + padding-right: 1em; +} + +.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton { + margin-right: 10px; +} + +.mx_SecuritySettingsTab_importExportButtons { + margin-bottom: 15px; +} + +.mx_SecuritySettingsTab_ignoredUser { + margin-bottom: 5px; +} + +.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton { + margin-right: 10px; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 1a26c58c44..17f869b5b0 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -26,15 +26,15 @@ limitations under the License. font-family: $font-family-semibold; color: $primary-fg-color; margin-bottom: 10px; - margin-top: 10px; + margin-top: 12px; } .mx_SettingsTab_subsectionText { color: $settings-subsection-fg-color; font-size: 12px; padding-bottom: 12px; - margin: 0; display: block; + margin: 0 100px 0 0; // Align with the rest of the view } .mx_SettingsTab_section .mx_SettingsFlag { @@ -54,3 +54,9 @@ limitations under the License. .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { float: right; } + +.mx_SettingsTab_linkBtn { + cursor: pointer; + color: $accent-color; + word-break: break-all; +} \ No newline at end of file diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 7b09970f56..addd16bdf0 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -23,6 +23,7 @@ import GeneralSettingsTab from "../settings/tabs/GeneralSettingsTab"; import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import LabsSettingsTab from "../settings/tabs/LabsSettingsTab"; +import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab"; import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab"; import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab"; import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab"; @@ -74,7 +75,7 @@ export default class UserSettingsDialog extends React.Component { tabs.push(new Tab( _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", -
Security Test
, + , )); if (SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 8dc817faad..4c32e3eaf8 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent { {uploadStatus}
{backupSigStatuses}


- + { _t("Restore backup") }     - + { _t("Delete backup") }
; } else { return
{_t("No backup is present")}

- + { _t("Start a new backup") }
; diff --git a/src/components/views/settings/tabs/SecuritySettingsTab.js b/src/components/views/settings/tabs/SecuritySettingsTab.js new file mode 100644 index 0000000000..55bc538a55 --- /dev/null +++ b/src/components/views/settings/tabs/SecuritySettingsTab.js @@ -0,0 +1,242 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../../languageHandler"; +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import * as FormattingUtils from "../../../../utils/FormattingUtils"; +import AccessibleButton from "../../elements/AccessibleButton"; +import Analytics from "../../../../Analytics"; +import Promise from "bluebird"; +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); + +export class IgnoredUser extends React.Component { + static propTypes = { + userId: PropTypes.string.isRequired, + onUnignored: PropTypes.func.isRequired, + }; + + _onUnignoreClicked = (e) => { + this.props.onUnignored(this.props.userId); + }; + + render() { + return ( +
+ + {_t('Unignore')} + + {this.props.userId} +
+ ); + } +} + +export default class SecuritySettingsTab extends React.Component { + constructor() { + super(); + + this.state = { + ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), + rejectingInvites: false, + }; + } + + _updateBlacklistDevicesFlag = (checked) => { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); + }; + + _updateAnalytics = (checked) => { + checked ? Analytics.enable() : Analytics.disable(); + }; + + _onExportE2eKeysClicked = () => { + Modal.createTrackedDialogAsync('Export E2E Keys', '', + import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'), + {matrixClient: MatrixClientPeg.get()}, + ); + }; + + _onImportE2eKeysClicked = () => { + Modal.createTrackedDialogAsync('Import E2E Keys', '', + import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'), + {matrixClient: MatrixClientPeg.get()}, + ); + }; + + _onUserUnignored = async (userId) => { + // Don't use this.state to get the ignored user list as it might be + // ever so slightly outdated. Instead, prefer to get a fresh list and + // update that. + const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); + const index = ignoredUsers.indexOf(userId); + if (index !== -1) { + ignoredUsers.splice(index, 1); + MatrixClientPeg.get().setIgnoredUsers(ignoredUsers); + } + this.setState({ignoredUsers}); + }; + + _onRejectAllInvitesClicked = (rooms, ev) => { + this.setState({ + rejectingInvites: true, + }); + // reject the invites + const promises = rooms.map((room) => { + return MatrixClientPeg.get().leave(room.roomId).catch((e) => { + // purposefully drop errors to the floor: we'll just have a non-zero number on the UI + // after trying to reject all the invites. + }); + }); + Promise.all(promises).then(() => { + this.setState({ + rejectingInvites: false, + }); + }); + }; + + _renderCurrentDeviceInfo() { + const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag'); + + const client = MatrixClientPeg.get(); + const deviceId = client.deviceId; + let identityKey = client.getDeviceEd25519Key(); + if (!identityKey) { + identityKey = _t(""); + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } + + let importExportButtons = null; + if (client.isCryptoEnabled()) { + importExportButtons = ( +
+ + {_t("Export E2E room keys")} + + + {_t("Import E2E room keys")} + +
+ ); + } + + return ( +
+ {_t("Cryptography")} +
    +
  • + + {deviceId} +
  • +
  • + + {identityKey} +
  • +
+ {importExportButtons} + +
+ ); + } + + _renderIgnoredUsers() { + if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null; + + const userIds = this.state.ignoredUserIds + .map((u) => ); + + return ( +
+ {_t('Ignored users')} +
+ {userIds} +
+
+ ); + } + + _renderRejectInvites() { + const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { + return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); + }); + if (invitedRooms.length === 0) { + return null; + } + + const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms); + return ( +
+ {_t('Bulk options')} + + {_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})} + +
+ ); + } + + render() { + const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel'); + const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag'); + + let keyBackup = null; + if (SettingsStore.isFeatureEnabled("feature_keybackup")) { + const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel'); + keyBackup = ( +
+ {_t("Key backup")} +
+ +
+
+ ); + } + + return ( +
+
{_t("Security & Privacy")}
+
+ {_t("Devices")} +
+ +
+
+ {keyBackup} + {this._renderCurrentDeviceInfo()} +
+ {_t("Analytics")} +
+ {_t('Riot collects anonymous analytics to allow us to improve the application.')} +   + {_t('Privacy is important to us, so we don\'t collect any personal or ' + + 'identifiable data for our analytics.')} + + {_t('Learn more about how we use analytics.')} + +
+ +
+ {this._renderIgnoredUsers()} + {this._renderRejectInvites()} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ab4e8b74c7..50539e84dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -450,6 +450,21 @@ "Timeline": "Timeline", "Advanced": "Advanced", "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "Unignore": "Unignore", + "": "", + "Import E2E room keys": "Import E2E room keys", + "Cryptography": "Cryptography", + "Device ID:": "Device ID:", + "Device key:": "Device key:", + "Ignored users": "Ignored users", + "Bulk options": "Bulk options", + "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", + "Key backup": "Key backup", + "Security & Privacy": "Security & Privacy", + "Devices": "Devices", + "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", + "Learn more about how we use analytics.": "Learn more about how we use analytics.", "No media permissions": "No media permissions", "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", @@ -510,8 +525,6 @@ "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "No devices with registered encryption keys": "No devices with registered encryption keys", - "Devices": "Devices", - "Unignore": "Unignore", "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", @@ -1056,7 +1069,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Security & Privacy": "Security & Privacy", "Help & About": "Help & About", "Visit old settings": "Visit old settings", "Unable to load backup status": "Unable to load backup status", @@ -1288,18 +1300,10 @@ "Interface Language": "Interface Language", "User Interface": "User Interface", "Autocomplete Delay (ms):": "Autocomplete Delay (ms):", - "": "", - "Import E2E room keys": "Import E2E room keys", "Key Backup": "Key Backup", - "Cryptography": "Cryptography", - "Device ID:": "Device ID:", - "Device key:": "Device key:", "Ignored Users": "Ignored Users", "Submit Debug Logs": "Submit Debug Logs", "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", - "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", - "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", - "Learn more about how we use analytics.": "Learn more about how we use analytics.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "Use with caution": "Use with caution", "Deactivate my account": "Deactivate my account", @@ -1308,7 +1312,6 @@ "Clear Cache and Reload": "Clear Cache and Reload", "Updates": "Updates", "Check for update": "Check for update", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Bulk Options": "Bulk Options", "Desktop specific": "Desktop specific", "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", From 1f10cda5e43bea4ec7bf92fc9604e39113ba96e8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 17:10:35 -0700 Subject: [PATCH 15/38] Implement the "Help & About" tab of new user settings --- res/css/_components.scss | 1 + .../views/settings/tabs/_HelpSettingsTab.scss | 24 ++ .../views/dialogs/UserSettingsDialog.js | 3 +- .../views/settings/tabs/HelpSettingsTab.js | 227 ++++++++++++++++++ src/i18n/strings/en_EN.json | 34 +-- 5 files changed, 274 insertions(+), 15 deletions(-) create mode 100644 res/css/views/settings/tabs/_HelpSettingsTab.scss create mode 100644 src/components/views/settings/tabs/HelpSettingsTab.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 25b7c9342e..d0068c14ad 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -136,6 +136,7 @@ @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/tabs/_GeneralSettingsTab.scss"; +@import "./views/settings/tabs/_HelpSettingsTab.scss"; @import "./views/settings/tabs/_PreferencesSettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/_VoiceSettingsTab.scss"; diff --git a/res/css/views/settings/tabs/_HelpSettingsTab.scss b/res/css/views/settings/tabs/_HelpSettingsTab.scss new file mode 100644 index 0000000000..a1199b59b6 --- /dev/null +++ b/res/css/views/settings/tabs/_HelpSettingsTab.scss @@ -0,0 +1,24 @@ +/* +Copyright 2019 New Vector Ltd + +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_HelpSettingsTab_debugButton { + margin-bottom: 5px; + margin-top: 5px; +} + +.mx_HelpSettingsTab span.mx_AccessibleButton { + word-break: break-word; +} \ No newline at end of file diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 7b09970f56..321d741743 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -26,6 +26,7 @@ import LabsSettingsTab from "../settings/tabs/LabsSettingsTab"; import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab"; import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab"; import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab"; +import HelpSettingsTab from "../settings/tabs/HelpSettingsTab"; // TODO: Ditch this whole component export class TempTab extends React.Component { @@ -86,7 +87,7 @@ export default class UserSettingsDialog extends React.Component { tabs.push(new Tab( _td("Help & About"), "mx_UserSettingsDialog_helpIcon", -
Help Test
, + , )); tabs.push(new Tab( _td("Visit old settings"), diff --git a/src/components/views/settings/tabs/HelpSettingsTab.js b/src/components/views/settings/tabs/HelpSettingsTab.js new file mode 100644 index 0000000000..c0892cf448 --- /dev/null +++ b/src/components/views/settings/tabs/HelpSettingsTab.js @@ -0,0 +1,227 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t, getCurrentLanguage} from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import AccessibleButton from "../../elements/AccessibleButton"; +import SdkConfig from "../../../../SdkConfig"; +import createRoom from "../../../../createRoom"; +const packageJson = require('../../../../../package.json'); +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); +const PlatformPeg = require("../../../../PlatformPeg"); + +// if this looks like a release, use the 'version' from package.json; else use +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; + +// Simple method to help prettify GH Release Tags and Commit Hashes. +const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; +const ghVersionLabel = function(repo, token='') { + const match = token.match(semVerRegex); + let url; + if (match && match[1]) { // basic semVer string possibly with commit hash + url = (match.length > 1 && match[2]) + ? `https://github.com/${repo}/commit/${match[2]}` + : `https://github.com/${repo}/releases/tag/v${match[1]}`; + } else { + url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; + } + return { token }; +}; + +export default class HelpSettingsTab extends React.Component { + static propTypes = { + closeSettingsFn: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + vectorVersion: null, + canUpdate: false, + }; + } + + componentWillMount(): void { + PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => { + console.error("Error getting vector version: ", e); + }); + PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => { + console.error("Error getting self updatability: ", e); + }); + } + + _onClearCacheAndReload = (e) => { + if (!PlatformPeg.get()) return; + + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().store.deleteAllData().done(() => { + PlatformPeg.get().reload(); + }); + }; + + _onBugReport = (e) => { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + if (!BugReportDialog) { + return; + } + Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); + }; + + _onStartBotChat = (e) => { + this.props.closeSettingsFn(); + createRoom({ + dmUserId: SdkConfig.get().welcomeUserId, + andView: true, + }); + }; + + _showSpoiler = (event) => { + const target = event.target; + target.innerHTML = target.getAttribute('data-spoiler'); + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }; + + _renderLegal() { + const tocLinks = SdkConfig.get().terms_and_conditions_links; + if (!tocLinks) return null; + + const legalLinks = []; + for (const tocEntry of SdkConfig.get().terms_and_conditions_links) { + legalLinks.push(); + } + + return ( +
+ {_t("Legal")} +
+ {legalLinks} +
+
+ ); + } + + render() { + let faqText = _t('For help with using Riot, click here.', {}, { + 'a': (sub) => {sub}, + }); + if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) { + faqText = ( +
+ { + _t('For help with using Riot, click here or start a chat with our ' + + 'bot using the button below.', {}, { + 'a': (sub) => {sub}, + }) + } + + {_t("Start a chat with Riot Bot")} + +
+ ); + } + + const reactSdkVersion = REACT_SDK_VERSION !== '' + ? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) + : REACT_SDK_VERSION; + const vectorVersion = this.state.vectorVersion + ? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion) + : 'unknown'; + + let olmVersion = MatrixClientPeg.get().olmVersion; + olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; + + let updateButton = null; + if (this.state.canUpdate) { + const platform = PlatformPeg.get(); + updateButton = ( + + {_t('Check for update')} + + ); + } + + return ( +
+
{_t("Help & About")}
+
+ {_t('Bug reporting')} +
+ { + _t( "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + ) + } +
+ + {_t("Submit debug logs")} + +
+
+ + {_t("Clear Cache and Reload")} + +
+
+
+
+ {_t("FAQ")} +
+ {faqText} +
+
+
+ {_t("Versions")} +
+ {_t("matrix-react-sdk version:")} {reactSdkVersion}
+ {_t("riot-web version:")} {vectorVersion}
+ {_t("olm version:")} {olmVersion}
+ {updateButton} +
+
+ {this._renderLegal()} +
+ {_t("Advanced")} +
+ {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
+ {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
+ {_t("Access Token:") + ' '} + + <{ _t("click to reveal") }> + +
+
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ab4e8b74c7..94c2528c64 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -439,6 +439,26 @@ "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Close Account": "Close Account", "General": "General", + "Legal": "Legal", + "For help with using Riot, click here.": "For help with using Riot, click here.", + "For help with using Riot, click here or start a chat with our bot using the button below.": "For help with using Riot, click here or start a chat with our bot using the button below.", + "Start a chat with Riot Bot": "Start a chat with Riot Bot", + "Check for update": "Check for update", + "Help & About": "Help & About", + "Bug reporting": "Bug reporting", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", + "Submit debug logs": "Submit debug logs", + "Clear Cache and Reload": "Clear Cache and Reload", + "FAQ": "FAQ", + "Versions": "Versions", + "matrix-react-sdk version:": "matrix-react-sdk version:", + "riot-web version:": "riot-web version:", + "olm version:": "olm version:", + "Advanced": "Advanced", + "Homeserver is": "Homeserver is", + "Identity Server is": "Identity Server is", + "Access Token:": "Access Token:", + "click to reveal": "click to reveal", "Lazy loading members not supported": "Lazy loading members not supported", "Lazy loading is not supported by your current homeserver.": "Lazy loading is not supported by your current homeserver.", "Labs": "Labs", @@ -448,7 +468,6 @@ "Composer": "Composer", "Room list": "Room list", "Timeline": "Timeline", - "Advanced": "Advanced", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "No media permissions": "No media permissions", "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", @@ -916,7 +935,6 @@ "Logs sent": "Logs sent", "Thank you!": "Thank you!", "Failed to send logs: ": "Failed to send logs: ", - "Submit debug logs": "Submit debug logs", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", "Before submitting logs, you must create a GitHub issue to describe your problem.": "Before submitting logs, you must create a GitHub issue to describe your problem.", "What GitHub issue are these logs for?": "What GitHub issue are these logs for?", @@ -1057,7 +1075,6 @@ "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", "Security & Privacy": "Security & Privacy", - "Help & About": "Help & About", "Visit old settings": "Visit old settings", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", @@ -1296,18 +1313,14 @@ "Device key:": "Device key:", "Ignored Users": "Ignored Users", "Submit Debug Logs": "Submit Debug Logs", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "Use with caution": "Use with caution", "Deactivate my account": "Deactivate my account", - "Legal": "Legal", "Clear Cache": "Clear Cache", - "Clear Cache and Reload": "Clear Cache and Reload", "Updates": "Updates", - "Check for update": "Check for update", "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Bulk Options": "Bulk Options", "Desktop specific": "Desktop specific", @@ -1318,13 +1331,6 @@ "Display name": "Display name", "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", "Logged in as:": "Logged in as:", - "Access Token:": "Access Token:", - "click to reveal": "click to reveal", - "Homeserver is": "Homeserver is", - "Identity Server is": "Identity Server is", - "matrix-react-sdk version:": "matrix-react-sdk version:", - "riot-web version:": "riot-web version:", - "olm version:": "olm version:", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", From 5dc75e2d3cb9481b54b029f820e7f87c386fce07 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 19:12:09 -0700 Subject: [PATCH 16/38] Have the settings dialog be fixed in size This also brings out some classes that aren't solely applicable to the user settings and will be applicable to the room settings as well. --- res/css/_components.scss | 1 + res/css/structures/_TabbedView.scss | 1 + res/css/views/dialogs/_SettingsDialog.scss | 70 +++++++++++++++++++ .../views/dialogs/_UserSettingsDialog.scss | 45 ++++-------- src/components/structures/MatrixChat.js | 2 +- .../views/dialogs/UserSettingsDialog.js | 6 +- 6 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 res/css/views/dialogs/_SettingsDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 25b7c9342e..7af28b31c5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -56,6 +56,7 @@ @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; +@import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index d9d66f470a..1118ac7aea 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -25,6 +25,7 @@ limitations under the License. .mx_TabbedView_tabLabels { width: 136px; + max-width: 136px; height: 100%; color: $tab-label-fg-color; } diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss new file mode 100644 index 0000000000..2cc7c08039 --- /dev/null +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -0,0 +1,70 @@ +/* +Copyright 2019 New Vector Ltd. + +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_SettingsDialog { + .mx_Dialog { + max-width: 784px; // 900px - 58px (left padding) - 58px (right padding) + width: 80%; + height: 80%; + border-radius: 4px; + + .mx_TabbedView_tabLabels { + // Force the sidebar to be always visible, letting the rest of the content scroll + position: fixed; + } + + .mx_TabbedView_tabPanel { + max-width: 485px; + margin-left: 206px; // 70px margin + 136px for the sidebar + } + + .mx_SettingsDialog_header { + font-size: 24px; + display: block; + text-align: center; + color: $dialog-title-fg-color; + margin-top: 16px; + margin-bottom: 24px; + padding: 0; + } + + .mx_SettingsDialog_close { + position: absolute; + top: 16px; + right: 25px; + } + + .mx_SettingsDialog_closeIcon { + width: 16px; + height: 16px; + display: inline-block; + } + + .mx_SettingsDialog_closeIcon:before { + mask: url('$(res)/img/feather-icons/cancel.svg'); + background-color: $dialog-close-fg-color; + mask-repeat: no-repeat; + mask-size: 16px; + mask-position: center; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + } +} diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index d7a31d0c99..2849573790 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -1,39 +1,18 @@ -.mx_UserSettingsDialog_header { - font-size: 24px; - display: block; - text-align: center; - color: $dialog-title-fg-color; - margin-top: 16px; - margin-bottom: 24px; - padding: 0; -} +/* +Copyright 2019 New Vector Ltd. -.mx_UserSettingsDialog_close { - position: absolute; - top: 16px; - right: 25px; -} +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 -.mx_UserSettingsDialog_closeIcon { - width: 16px; - height: 16px; - display: inline-block; -} - -.mx_UserSettingsDialog_closeIcon:before { - mask: url('$(res)/img/feather-icons/cancel.svg'); - background-color: $dialog-close-fg-color; - mask-repeat: no-repeat; - mask-size: 16px; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} + 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. +*/ // ICONS // ========================================================== diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d5ad130edd..305ee4ec2f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -613,7 +613,7 @@ export default React.createClass({ case 'view_user_settings': { if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog'); } else { this._setPage(PageTypes.UserSettings); this.notifyNewScreen('settings'); diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 7b09970f56..10fbeae125 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -100,10 +100,10 @@ export default class UserSettingsDialog extends React.Component { render() { return (
-
+
{_t("Settings")} - - + +
From d819095a7620157246ed4dd6e2d2fe5d54b79a6a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 19:36:23 -0700 Subject: [PATCH 17/38] Default a Field's placeholder to the label Fixes https://github.com/vector-im/riot-web/issues/8250 This keeps all fields in line with the design without them having to defining it twice. The option is kept in the first place as some fields might want to override the placeholder to be longer than the label or something. --- src/components/views/elements/Field.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 87cfb70306..774ec1db88 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -25,7 +25,7 @@ export default class Field extends React.PureComponent { type: PropTypes.string, // The field's label string. label: PropTypes.string, - // The field's placeholder string. + // The field's placeholder string. Defaults to the label. placeholder: PropTypes.string, // The type of field to create. Defaults to "input". Should be "input" or "select". // To define options for a select, use @@ -55,6 +55,7 @@ export default class Field extends React.PureComponent { // Set some defaults for the element extraProps.type = extraProps.type || "text"; extraProps.ref = "fieldInput"; + extraProps.placeholder = extraProps.placeholder || extraProps.label; const element = this.props.element || "input"; const fieldInput = React.createElement(element, extraProps, this.props.children); From 9bbbab9d03f20584b3e1f82c70231d4d1a64a96c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 21:24:28 -0700 Subject: [PATCH 18/38] Add simple animations to toggle switches --- res/css/views/elements/_ToggleSwitch.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss index 0955648d50..4076414086 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO: Fancy transitions - .mx_ToggleSwitch { + transition: background-color 0.20s ease-out 0.1s; width: 48px; height: 24px; border-radius: 14px; @@ -33,6 +32,7 @@ limitations under the License. } .mx_ToggleSwitch_ball { + transition: left 0.15s ease-out 0.1s; margin: 2px; width: 20px; height: 20px; @@ -47,5 +47,5 @@ limitations under the License. } .mx_ToggleSwitch_on > .mx_ToggleSwitch_ball { - right: 2px; + left: 23px; // 48px switch - 20px ball - 5px padding = 23px } From 3e69be513992354148d46268bdc27bfd85f9e66d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jan 2019 09:12:45 -0700 Subject: [PATCH 19/38] Regenerate i18n due to conflicts --- src/i18n/strings/en_EN.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3a423b7aa5..10506d8513 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1087,7 +1087,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Security & Privacy": "Security & Privacy", "Visit old settings": "Visit old settings", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", @@ -1321,15 +1320,11 @@ "Key Backup": "Key Backup", "Ignored Users": "Ignored Users", "Submit Debug Logs": "Submit Debug Logs", - "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", - "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", - "Learn more about how we use analytics.": "Learn more about how we use analytics.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "Use with caution": "Use with caution", "Deactivate my account": "Deactivate my account", "Clear Cache": "Clear Cache", "Updates": "Updates", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Bulk Options": "Bulk Options", "Desktop specific": "Desktop specific", "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", From fb36f7abefd9705566867b641d4dd62237978ce5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jan 2019 09:18:00 -0700 Subject: [PATCH 20/38] Fix imports and quotes --- .../views/settings/tabs/SecuritySettingsTab.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/settings/tabs/SecuritySettingsTab.js b/src/components/views/settings/tabs/SecuritySettingsTab.js index 55bc538a55..a2a30a2190 100644 --- a/src/components/views/settings/tabs/SecuritySettingsTab.js +++ b/src/components/views/settings/tabs/SecuritySettingsTab.js @@ -23,8 +23,8 @@ import * as FormattingUtils from "../../../../utils/FormattingUtils"; import AccessibleButton from "../../elements/AccessibleButton"; import Analytics from "../../../../Analytics"; import Promise from "bluebird"; -const Modal = require("../../../../Modal"); -const sdk = require("../../../../index"); +import Modal from "../../../../Modal"; +import sdk from "../../../../index"; export class IgnoredUser extends React.Component { static propTypes = { @@ -223,12 +223,12 @@ export default class SecuritySettingsTab extends React.Component {
{_t("Analytics")}
- {_t('Riot collects anonymous analytics to allow us to improve the application.')} + {_t("Riot collects anonymous analytics to allow us to improve the application.")}   - {_t('Privacy is important to us, so we don\'t collect any personal or ' + - 'identifiable data for our analytics.')} + {_t("Privacy is important to us, so we don't collect any personal or " + + "identifiable data for our analytics.")} - {_t('Learn more about how we use analytics.')} + {_t("Learn more about how we use analytics.")}
Date: Fri, 25 Jan 2019 16:25:15 +0000 Subject: [PATCH 21/38] New text/caption for key backup by verifying device https://github.com/vector-im/riot-web/issues/8072 --- .../views/rooms/RoomRecoveryReminder.js | 22 ++++++++++++++----- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 01447012e6..32bf8ad5d1 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -133,17 +133,29 @@ export default class RoomRecoveryReminder extends React.PureComponent { const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); let body; + let primaryCaption; if (this.state.error) { body =
{_t("Unable to load key backup status")}
; + primaryCaption = _t("Set up"); } else if (this.state.unverifiedDevice) { // A key backup exists for this account, but the creating device is not // verified. - body = _t( - "To view your secure message history and ensure you can view new " + - "messages on future devices, set up Secure Message Recovery.", - ); + body =
+

{_t( + "Secure Message Recovery has been set up on another device: ", + {}, + { + deviceName: () => {this.state.unverifiedDevice.unsigned.device_display_name}, + }, + )}

+

{_t( + "To view your secure message history and ensure you can view new " + + "messages on future devices, verify that device now.", + )}

+
; + primaryCaption = _t("Verify device"); } else { // The default case assumes that a key backup doesn't exist for this account. // (This component doesn't currently check that itself.) @@ -167,7 +179,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { - { _t("Set up") } + {primaryCaption}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 94c2528c64..0bb38010f5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -654,7 +654,8 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", - "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.", + "Secure Message Recovery has been set up on another device: ": "Secure Message Recovery has been set up on another device: ", + "To view your secure message history and ensure you can view new messages on future devices, verify that device now.": "To view your secure message history and ensure you can view new messages on future devices, verify that device now.", "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", "Secure Message Recovery": "Secure Message Recovery", "Don't ask again": "Don't ask again", From 44a5ee3e441df852532f4798b40a0be6d3480733 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 25 Jan 2019 17:30:22 +0000 Subject: [PATCH 22/38] Add fullstop Co-Authored-By: dbkr --- src/components/views/dialogs/IncomingSasDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index bad1377457..2a76e8a904 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -106,7 +106,7 @@ export default class IncomingSasDialog extends React.Component { // NB. Below wording adjusted to singular 'device' until we have // cross-signing "Verifying this user will mark their device as trusted, and " + - "also mark your device as trusted to them", + "also mark your device as trusted to them.", )}

Date: Fri, 25 Jan 2019 17:30:47 +0000 Subject: [PATCH 23/38] Fullstop Co-Authored-By: dbkr --- src/components/views/verification/VerificationShowSas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index 60f70e2748..4023da9679 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -42,7 +42,7 @@ export default class VerificationShowSas extends React.Component { const HexVerify = sdk.getComponent('views.elements.HexVerify'); return

{_t( - "Verify this user by confirming the following number appears on their screen", + "Verify this user by confirming the following number appears on their screen.", )}

{_t( "For maximum security, we reccommend you do this in person or use another " + From 939f8b5591c3e5f8385e77b3ba3b35f7c605e730 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 25 Jan 2019 17:30:59 +0000 Subject: [PATCH 24/38] Fullstop Co-Authored-By: dbkr --- src/components/views/verification/VerificationShowSas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index 4023da9679..cd4ae59b76 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -46,7 +46,7 @@ export default class VerificationShowSas extends React.Component { )}

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

Date: Fri, 25 Jan 2019 17:29:36 +0000 Subject: [PATCH 25/38] Remove outdated paragraph promising better verification --- src/components/views/dialogs/DeviceVerifyDialog.js | 3 --- src/i18n/strings/en_EN.json | 1 - 2 files changed, 4 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 68a2210814..e84010726d 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -253,9 +253,6 @@ export default class DeviceVerifyDialog extends React.Component { "If it doesn't, then someone else is intercepting this device " + "and you probably want to press the blacklist button instead.") }

-

- { _t("In future this verification process will be more sophisticated.") } -

); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fdff3d4199..24e5f27e15 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -931,7 +931,6 @@ "Device name": "Device name", "Device key": "Device key", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", - "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.", "I verify that the keys match": "I verify that the keys match", "Back": "Back", "Send Custom Event": "Send Custom Event", From 8ffeee6a9852a81f4f3ee00de84d3312d798774c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 25 Jan 2019 18:47:34 +0100 Subject: [PATCH 26/38] add matthews new resize algo --- src/resizer/distributors/roomsublist2.js | 79 ++++++++++++------------ 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index 099d533166..36c140887b 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -17,7 +17,6 @@ limitations under the License. import FixedDistributor from "./fixed"; // const allowWhitespace = true; -const blendOverflow = false; const handleHeight = 1; function log(...params) { @@ -59,11 +58,16 @@ export class Layout { if (Number.isFinite(availableHeight)) { this._availableHeight = availableHeight; } - + const totalHeight = this._getAvailableHeight(); this._sections.forEach((section, i) => { - this._sectionHeights[section.id] = this._originalHeights[i]; + this._originalHeights[i] = + this._sectionHeights[section.id] || + clamp( + totalHeight / this._sections.length, + this._getMinHeight(i), + this._getMaxHeight(i), + ); }); - this._sections = sections; this._applyNewSize(); } @@ -83,26 +87,21 @@ export class Layout { } _applyNewSize() { - const height = this._getAvailableHeight(); - // we should only scale the section here between min and max size - const requestedHeights = this._sections.map((section) => { - return this._sectionHeights[section.id] || (height / this._sections.length); - }); - const totalRequestedHeight = requestedHeights.reduce((sum, h) => h + sum, 0); - const ratios = requestedHeights.map((h) => h / totalRequestedHeight); - this._originalHeights = ratios.map((r, i) => clamp(r * height, this._getMinHeight(i), this._getMaxHeight(i))); - const totalRequestedHeight2 = requestedHeights.reduce((sum, h) => h + sum, 0); - const overflow = height - totalRequestedHeight2; - // re-assign adjusted heights + const newHeight = this._getAvailableHeight(); + let currHeight = 0; + const sections = []; + for (let i = 0; i < this._sections.length; i++) { + currHeight += this._originalHeights[i]; + sections.push(i); + } + const offset = newHeight - currHeight; + this._heights = this._originalHeights.slice(0); + this._applyOverflow(-offset, sections, true); + this._applyHeights(); + this._originalHeights = this._heights; this._sections.forEach((section, i) => { this._sectionHeights[section.id] = this._originalHeights[i]; }); - log("_applyNewSize", height, this._sections, requestedHeights, ratios, this._originalHeights); - this._heights = this._originalHeights.slice(0); - this._relayout(); - if (overflow) { - this._applyOverflow(overflow, this._sections.map((_, i) => i)); - } } _getSectionIndex(id) { @@ -116,8 +115,8 @@ export class Layout { if (collapsed) { return this._sectionHeight(0); } else { - // return 100000; - return this._sectionHeight(section.count); + return 100000; + // return this._sectionHeight(section.count); } } @@ -133,7 +132,7 @@ export class Layout { return this._sectionHeight(Math.min(section.count, maxItems)); } - _applyOverflow(overflow, sections) { + _applyOverflow(overflow, sections, blend) { //log("applyOverflow", overflow, sections); // take the given overflow amount, and applies it to the given sections. // calls itself recursively until it has distributed all the overflow @@ -141,22 +140,23 @@ export class Layout { const unclampedSections = []; - // let overflowPerSection = blendOverflow ? (overflow / sections.length) : overflow; + let overflowPerSection = blend ? (overflow / sections.length) : overflow; for (const i of sections) { - const newHeight = clamp(this._heights[i] - overflow, this._getMinHeight(i), this._getMaxHeight(i)); - if (newHeight == this._heights[i] - overflow) { + const newHeight = clamp(this._heights[i] - overflowPerSection, this._getMinHeight(i), this._getMaxHeight(i)); + if (newHeight == this._heights[i] - overflowPerSection) { unclampedSections.push(i); } + // when section is growing, overflow increases? + // 100 -= 200 - 300 + // 100 -= -100 + // 200 overflow -= this._heights[i] - newHeight; - log(`heights[${i}] (${this._heights[i]}) - newHeight (${newHeight}) = ${this._heights[i] - newHeight}`); - - // log(`changing ${this._heights[i]} to ${newHeight}`); + // console.log(`this._heights[${i}] (${this._heights[i]}) - newHeight (${newHeight}) = ${this._heights[i] - newHeight}`); + // console.log(`changing ${this._heights[i]} to ${newHeight}`); this._heights[i] = newHeight; - - //log(`for section ${i} overflow is ${overflow}`); - - if (!blendOverflow) { - // overflowPerSection = overflow; + // console.log(`for section ${i} overflow is ${overflow}`); + if (!blend) { + overflowPerSection = overflow; if (Math.abs(overflow) < 1.0) break; } } @@ -164,7 +164,7 @@ export class Layout { if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) { // we weren't able to distribute all the overflow so recurse and try again log("recursing with", overflow, unclampedSections); - overflow = this._applyOverflow(overflow, unclampedSections); + overflow = this._applyOverflow(overflow, unclampedSections, blend); } return overflow; @@ -246,14 +246,17 @@ export class Layout { } } + this._applyHeights(); + return undefined; + } + + _applyHeights() { log("updating layout, heights are now", this._heights); // apply the heights for (let i = 0; i < this._sections.length; i++) { const section = this._sections[i]; this._applyHeight(section.id, this._heights[i]); } - - return undefined; } _commitHeights() { From cb9ebf8b8b426b88273e469e5562b51bac07d849 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 25 Jan 2019 18:47:57 +0100 Subject: [PATCH 27/38] rerender roomlist when banner gets shown/hidden, to update the layout --- src/components/structures/LeftPanel.js | 1 + src/components/structures/LoggedInView.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index ba0e97366e..a2d08f35c8 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -212,6 +212,7 @@ const LeftPanel = React.createClass({ diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 409988842f..e57e42e0cf 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -553,6 +553,7 @@ const LoggedInView = React.createClass({
From 2d2f9712b5682086c109170fab1331229bb3a06a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 25 Jan 2019 18:48:25 +0100 Subject: [PATCH 28/38] update layout on window resize --- src/components/views/rooms/RoomList.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0c910f6808..3cbd8b0a4b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -196,6 +196,7 @@ module.exports = React.createClass({ this._checkSubListsOverflow(); this.resizer.attach(); + window.addEventListener("resize", this.onWindowResize); this.mounted = true; }, @@ -241,6 +242,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { this.mounted = false; + window.removeEventListener("resize", this.onWindowResize); dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); @@ -270,6 +272,17 @@ module.exports = React.createClass({ this._delayedRefreshRoomList.cancelPendingCall(); }, + onWindowResize: function() { + if (this.mounted && this._layout && this.resizeContainer && + Array.isArray(this._layoutSections) + ) { + this._layout.update( + this._layoutSections, + this.resizeContainer.offsetHeight + ); + } + }, + onRoom: function(room) { this.updateVisibleRooms(); }, From de9b964490857ce747d08d87599be1ddc2a1c2cb Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 25 Jan 2019 17:49:02 +0000 Subject: [PATCH 29/38] Fix NPE in RoomRecoveryReminder Also fix a bug introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2506 where I failed to set the button caption in one case. Fixes https://github.com/vector-im/riot-web/issues/8216 --- src/components/views/rooms/RoomRecoveryReminder.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 32bf8ad5d1..e8c315ecf5 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -62,7 +62,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { let unverifiedDevice; for (const sig of backupSigStatus.sigs) { - if (!sig.device.isVerified()) { + if (sig.device && !sig.device.isVerified()) { unverifiedDevice = sig.device; break; } @@ -133,12 +133,11 @@ export default class RoomRecoveryReminder extends React.PureComponent { const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); let body; - let primaryCaption; + let primaryCaption = _t("Set up"); if (this.state.error) { body =
{_t("Unable to load key backup status")}
; - primaryCaption = _t("Set up"); } else if (this.state.unverifiedDevice) { // A key backup exists for this account, but the creating device is not // verified. From 47c2ca33841d182cb56202cb36b6737b4ea07063 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 25 Jan 2019 18:01:40 +0000 Subject: [PATCH 30/38] Tweak wording on logout warning I further tweaked the wording from Tom's in the bug report to put passphrases first since they are supposed to be the norm (and the correct term is "recovery passphrase"). Fixes https://github.com/vector-im/riot-web/issues/8214 --- src/components/views/dialogs/LogoutDialog.js | 4 +++- src/i18n/strings/en_EN.json | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 8c74b3aff5..1f81472cc0 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -138,7 +138,9 @@ export default class LogoutDialog extends React.Component { // once you can restorew a backup by verifying a device description={_t( "When signing in again, you can access encrypted chat history by " + - "restoring your key backup. You'll need your recovery key.", + "restoring your key backup. You'll need your recovery passphrase " + + "or, if you didn't set a recovery passphrase, your recovery key " + + "(that you downloaded).", )} button={_t("Sign out")} onFinished={this._onFinished} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 13eedf47e0..a71a1b265f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -667,12 +667,13 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", + "Set up": "Set up", "Secure Message Recovery has been set up on another device: ": "Secure Message Recovery has been set up on another device: ", "To view your secure message history and ensure you can view new messages on future devices, verify that device now.": "To view your secure message history and ensure you can view new messages on future devices, verify that device now.", + "Verify device": "Verify device", "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", "Secure Message Recovery": "Secure Message Recovery", "Don't ask again": "Don't ask again", - "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -998,7 +999,6 @@ "Device key": "Device key", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.", - "Verify device": "Verify device", "I verify that the keys match": "I verify that the keys match", "Back": "Back", "Send Custom Event": "Send Custom Event", @@ -1033,7 +1033,7 @@ "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", "Set a Recovery Method": "Set a Recovery Method", "I understand, log out without": "I understand, log out without", - "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.", + "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).", "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", From 535d42684fc586420f506f7a745ab4d2dae19532 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jan 2019 12:51:33 -0700 Subject: [PATCH 31/38] Initial structure for new room settings --- res/css/_components.scss | 1 + res/css/structures/_TabbedView.scss | 6 +- .../views/dialogs/_RoomSettingsDialog.scss | 34 +++++++ res/img/feather-icons/users-sm.svg | 7 ++ res/img/feather-icons/warning-triangle.svg | 5 ++ .../views/dialogs/RoomSettingsDialog.js | 90 +++++++++++++++++++ src/i18n/strings/en_EN.json | 3 +- src/stores/RoomViewStore.js | 11 +++ 8 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 res/css/views/dialogs/_RoomSettingsDialog.scss create mode 100644 res/img/feather-icons/users-sm.svg create mode 100644 res/img/feather-icons/warning-triangle.svg create mode 100644 src/components/views/dialogs/RoomSettingsDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 11baf7fee7..91e6fa8685 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -52,6 +52,7 @@ @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; +@import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 1118ac7aea..d1a448711a 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -24,8 +24,8 @@ limitations under the License. } .mx_TabbedView_tabLabels { - width: 136px; - max-width: 136px; + width: 150px; + max-width: 150px; height: 100%; color: $tab-label-fg-color; } @@ -37,7 +37,7 @@ limitations under the License. border-radius: 3px; font-size: 12px; font-weight: 600; - height: 20px; + min-height: 20px; // use min-height instead of height to allow the label to overflow a bit margin-bottom: 6px; position: relative; } diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss new file mode 100644 index 0000000000..f255bb38f0 --- /dev/null +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -0,0 +1,34 @@ +/* +Copyright 2019 New Vector Ltd. + +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. +*/ + +// ICONS +// ========================================================== + +.mx_RoomSettingsDialog_settingsIcon:before { + mask-image: url('$(res)/img/feather-icons/settings.svg'); +} + +.mx_RoomSettingsDialog_securityIcon:before { + mask-image: url('$(res)/img/feather-icons/lock.svg'); +} + +.mx_RoomSettingsDialog_rolesIcon:before { + mask-image: url('$(res)/img/feather-icons/users-sm.svg'); +} + +.mx_RoomSettingsDialog_warningIcon:before { + mask-image: url('$(res)/img/feather-icons/warning-triangle.svg'); +} diff --git a/res/img/feather-icons/users-sm.svg b/res/img/feather-icons/users-sm.svg new file mode 100644 index 0000000000..6098be38c3 --- /dev/null +++ b/res/img/feather-icons/users-sm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/feather-icons/warning-triangle.svg b/res/img/feather-icons/warning-triangle.svg new file mode 100644 index 0000000000..02196cbf43 --- /dev/null +++ b/res/img/feather-icons/warning-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js new file mode 100644 index 0000000000..ce834d564e --- /dev/null +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -0,0 +1,90 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Tab, TabbedView} from "../../structures/TabbedView"; +import {_t, _td} from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import dis from '../../../dispatcher'; + +// TODO: Ditch this whole component +export class TempTab extends React.Component { + static propTypes = { + onClose: PropTypes.func.isRequired, + }; + + componentDidMount(): void { + dis.dispatch({action: "open_old_room_settings"}); + this.props.onClose(); + } + + render() { + return
Hello World
; + } +} + +export default class UserSettingsDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _getTabs() { + const tabs = []; + + tabs.push(new Tab( + _td("General"), + "mx_RoomSettingsDialog_settingsIcon", +
General Test
, + )); + tabs.push(new Tab( + _td("Security & Privacy"), + "mx_RoomSettingsDialog_securityIcon", +
Security Test
, + )); + tabs.push(new Tab( + _td("Roles & Permissions"), + "mx_RoomSettingsDialog_rolesIcon", +
Roles Test
, + )); + tabs.push(new Tab( + _td("Advanced"), + "mx_RoomSettingsDialog_warningIcon", +
Advanced Test
, + )); + tabs.push(new Tab( + _td("Visit old settings"), + "mx_RoomSettingsDialog_warningIcon", + , + )); + + return tabs; + } + + render() { + return ( +
+
+ {_t("Settings")} + + + +
+ +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a71a1b265f..1d88db5397 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1038,6 +1038,8 @@ "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", "Go back": "Go back", + "Roles & Permissions": "Roles & Permissions", + "Visit old settings": "Visit old settings", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", @@ -1088,7 +1090,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Visit old settings": "Visit old settings", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 9e048e5d8e..036a7c04fc 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -20,6 +20,7 @@ import MatrixClientPeg from '../MatrixClientPeg'; import sdk from '../index'; import Modal from '../Modal'; import { _t } from '../languageHandler'; +import SettingsStore from "../settings/SettingsStore"; const INITIAL_STATE = { // Whether we're joining the currently viewed room (see isJoining()) @@ -119,6 +120,16 @@ class RoomViewStore extends Store { }); break; case 'open_room_settings': + if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) { + const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); + Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {}, 'mx_SettingsDialog'); + } else { + this._setState({ + isEditingSettings: true, + }); + } + break; + case 'open_old_room_settings': this._setState({ isEditingSettings: true, }); From 20b7debcaf87c702e464408746dd7902b40ec52b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 25 Jan 2019 16:10:54 -0600 Subject: [PATCH 32/38] Remove support for team servers --- src/Lifecycle.js | 34 +---- src/RtsClient.js | 104 --------------- src/components/structures/HomePage.js | 49 +++---- src/components/structures/LoggedInView.js | 11 -- src/components/structures/MatrixChat.js | 66 ++-------- src/components/structures/UserSettings.js | 30 ----- .../structures/auth/Registration.js | 124 ++---------------- src/components/views/auth/RegistrationForm.js | 59 --------- src/components/views/dialogs/SetMxIdDialog.js | 4 - src/index.js | 5 - .../structures/auth/Registration-test.js | 105 --------------- .../views/auth/RegistrationForm-test.js | 91 ------------- test/test-utils.js | 14 -- 13 files changed, 41 insertions(+), 655 deletions(-) delete mode 100644 src/RtsClient.js delete mode 100644 test/components/structures/auth/Registration-test.js delete mode 100644 test/components/views/auth/RegistrationForm-test.js diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ed057eb020..54ac605c65 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -27,7 +27,6 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; -import RtsClient from './RtsClient'; import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; @@ -224,7 +223,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // // The plan is to gradually move the localStorage access done here into // SessionStore to avoid bugs where the view becomes out-of-sync with -// localStorage (e.g. teamToken, isGuest etc.) +// localStorage (e.g. isGuest etc.) async function _restoreFromLocalStorage() { if (!localStorage) { return false; @@ -286,15 +285,6 @@ function _handleLoadSessionFailure(e) { }); } -let rtsClient = null; -export function initRtsClient(url) { - if (url) { - rtsClient = new RtsClient(url); - } else { - rtsClient = null; - } -} - /** * Transitions to a logged-in state using the given credentials. * @@ -333,7 +323,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { ); // This is dispatched to indicate that the user is still in the process of logging in - // because `teamPromise` may take some time to resolve, breaking the assumption that + // because async code may take some time to resolve, breaking the assumption that // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // later than MatrixChat might assume. // @@ -347,10 +337,6 @@ async function _doSetLoggedIn(credentials, clearStorage) { Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl); - // Resolves by default - let teamPromise = Promise.resolve(null); - - if (localStorage) { try { _persistCredentialsToLocalStorage(credentials); @@ -367,27 +353,13 @@ async function _doSetLoggedIn(credentials, clearStorage) { } catch (e) { console.warn("Error using local storage: can't persist session!", e); } - - if (rtsClient && !credentials.guest) { - teamPromise = rtsClient.login(credentials.userId).then((body) => { - if (body.team_token) { - localStorage.setItem("mx_team_token", body.team_token); - } - return body.team_token; - }, (err) => { - console.warn(`Failed to get team token on login: ${err}` ); - return null; - }); - } } else { console.warn("No local storage available: can't persist session!"); } MatrixClientPeg.replaceUsingCreds(credentials); - teamPromise.then((teamToken) => { - dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); - }); + dis.dispatch({ action: 'on_logged_in' }); await startMatrixClient(); return MatrixClientPeg.get(); diff --git a/src/RtsClient.js b/src/RtsClient.js deleted file mode 100644 index 493b19599c..0000000000 --- a/src/RtsClient.js +++ /dev/null @@ -1,104 +0,0 @@ -import 'whatwg-fetch'; - -let fetchFunction = fetch; - -function checkStatus(response) { - if (!response.ok) { - return response.text().then((text) => { - throw new Error(text); - }); - } - return response; -} - -function parseJson(response) { - return response.json(); -} - -function encodeQueryParams(params) { - return '?' + Object.keys(params).map((k) => { - return k + '=' + encodeURIComponent(params[k]); - }).join('&'); -} - -const request = (url, opts) => { - if (opts && opts.qs) { - url += encodeQueryParams(opts.qs); - delete opts.qs; - } - if (opts && opts.body) { - if (!opts.headers) { - opts.headers = {}; - } - opts.body = JSON.stringify(opts.body); - opts.headers['Content-Type'] = 'application/json'; - } - return fetchFunction(url, opts) - .then(checkStatus) - .then(parseJson); -}; - - -export default class RtsClient { - constructor(url) { - this._url = url; - } - - getTeamsConfig() { - return request(this._url + '/teams'); - } - - /** - * Track a referral with the Riot Team Server. This should be called once a referred - * user has been successfully registered. - * @param {string} referrer the user ID of one who referred the user to Riot. - * @param {string} sid the sign-up identity server session ID . - * @param {string} clientSecret the sign-up client secret. - * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon - * success. - */ - trackReferral(referrer, sid, clientSecret) { - return request(this._url + '/register', - { - body: { - referrer: referrer, - session_id: sid, - client_secret: clientSecret, - }, - method: 'POST', - }, - ); - } - - getTeam(teamToken) { - return request(this._url + '/teamConfiguration', - { - qs: { - team_token: teamToken, - }, - }, - ); - } - - /** - * Signal to the RTS that a login has occurred and that a user requires their team's - * token. - * @param {string} userId the user ID of the user who is a member of a team. - * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon - * success. - */ - login(userId) { - return request(this._url + '/login', - { - qs: { - user_id: userId, - }, - }, - ); - } - - // allow fetch to be replaced, for testing. - static setFetch(fn) { - fetchFunction = fn; - } -} diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index aa17e63d73..948a07ee59 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -30,11 +30,6 @@ class HomePage extends React.Component { static displayName = 'HomePage'; static propTypes = { - // URL base of the team server. Optional. - teamServerUrl: PropTypes.string, - // Team token. Optional. If set, used to get the static homepage of the team - // associated. If unset, homePageUrl will be used. - teamToken: PropTypes.string, // URL to use as the iFrame src. Defaults to /home.html. homePageUrl: PropTypes.string, }; @@ -56,35 +51,29 @@ class HomePage extends React.Component { componentWillMount() { this._unmounted = false; - if (this.props.teamToken && this.props.teamServerUrl) { - this.setState({ - iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`, - }); - } else { - // we use request() to inline the homepage into the react component - // so that it can inherit CSS and theming easily rather than mess around - // with iframes and trying to synchronise document.stylesheets. + // we use request() to inline the homepage into the react component + // so that it can inherit CSS and theming easily rather than mess around + // with iframes and trying to synchronise document.stylesheets. - const src = this.props.homePageUrl || 'home.html'; + const src = this.props.homePageUrl || 'home.html'; - request( - { method: "GET", url: src }, - (err, response, body) => { - if (this._unmounted) { - return; - } + request( + { method: "GET", url: src }, + (err, response, body) => { + if (this._unmounted) { + return; + } - if (err || response.status < 200 || response.status >= 300) { - console.warn(`Error loading home page: ${err}`); - this.setState({ page: _t("Couldn't load home page") }); - return; - } + if (err || response.status < 200 || response.status >= 300) { + console.warn(`Error loading home page: ${err}`); + this.setState({ page: _t("Couldn't load home page") }); + return; + } - body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); - this.setState({ page: body }); - }, - ); - } + body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); + this.setState({ page: body }); + }, + ); } componentWillUnmount() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 6f2e1f3989..645782a854 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -63,7 +63,6 @@ const LoggedInView = React.createClass({ // transitioned to PWLU) onRegistered: PropTypes.func, collapsedRhs: PropTypes.bool, - teamToken: PropTypes.string, // Used by the RoomView to handle joining rooms viaServers: PropTypes.arrayOf(PropTypes.string), @@ -457,8 +456,6 @@ const LoggedInView = React.createClass({ pageElement = ; break; @@ -475,15 +472,7 @@ const LoggedInView = React.createClass({ case PageTypes.HomePage: { - // If team server config is present, pass the teamServerURL. props.teamToken - // must also be set for the team page to be displayed, otherwise the - // welcomePageUrl is used (which might be undefined). - const teamServerUrl = this.props.config.teamServerConfig ? - this.props.config.teamServerConfig.teamServerURL : null; - pageElement = ; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 305ee4ec2f..6803a7e6d8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -75,8 +75,8 @@ const VIEWS = { // we have valid matrix credentials (either via an explicit login, via the // initial re-animation/guest registration, or via a registration), and are // now setting up a matrixclient to talk to it. This isn't an instant - // process because (a) we need to clear out indexeddb, and (b) we need to - // talk to the team server; while it is going on we show a big spinner. + // process because we need to clear out indexeddb. While it is going on we + // show a big spinner. LOGGING_IN: 5, // we are logged in with an active matrix client. @@ -256,42 +256,6 @@ export default React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // To enable things like riot.im/geektime in a nicer way than rewriting the URL - // and appending a team token query parameter, use the first path segment to - // indicate a team, with "public" team tokens stored in the config teamTokenMap. - let routedTeamToken = null; - if (this.props.config.teamTokenMap) { - const teamName = window.location.pathname.split('/')[1]; - if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) { - routedTeamToken = this.props.config.teamTokenMap[teamName]; - } - } - - // Persist the team token across refreshes using sessionStorage. A new window or - // tab will not persist sessionStorage, but refreshes will. - if (this.props.startingFragmentQueryParams.team_token) { - window.sessionStorage.setItem( - 'mx_team_token', - this.props.startingFragmentQueryParams.team_token, - ); - } - - // Use the locally-stored team token first, then as a fall-back, check to see if - // a referral link was used, which will contain a query parameter `team_token`. - this._teamToken = routedTeamToken || - window.localStorage.getItem('mx_team_token') || - window.sessionStorage.getItem('mx_team_token'); - - // Some users have ended up with "undefined" as their local storage team token, - // treat that as undefined. - if (this._teamToken === "undefined") { - this._teamToken = undefined; - } - - if (this._teamToken) { - console.info(`Team token set to ${this._teamToken}`); - } - // Set up the default URLs (async) if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { this.setState({loadingDefaultHomeserver: true}); @@ -360,9 +324,6 @@ export default React.createClass({ linkifyMatrix.onGroupClick = this.onGroupClick; } - const teamServerConfig = this.props.config.teamServerConfig || {}; - Lifecycle.initRtsClient(teamServerConfig.teamServerURL); - // the first thing to do is to try the token params in the query-string Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { if (loggedIn) { @@ -726,7 +687,7 @@ export default React.createClass({ }); break; case 'on_logged_in': - this._onLoggedIn(payload.teamToken); + this._onLoggedIn(); break; case 'on_logged_out': this._onLoggedOut(); @@ -1196,16 +1157,10 @@ export default React.createClass({ /** * Called when a new logged in session has started - * - * @param {string} teamToken */ - _onLoggedIn: async function(teamToken) { + _onLoggedIn: async function() { this.setStateForNewView({view: VIEWS.LOGGED_IN}); - if (teamToken) { - // A team member has logged in, not a guest - this._teamToken = teamToken; - dis.dispatch({action: 'view_home_page'}); - } else if (this._is_registered) { + if (this._is_registered) { this._is_registered = false; if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { @@ -1261,7 +1216,6 @@ export default React.createClass({ currentRoomId: null, page_type: PageTypes.RoomDirectory, }); - this._teamToken = null; this._setPageSubtitle(); }, @@ -1707,15 +1661,13 @@ export default React.createClass({ onReturnToAppClick: function() { // treat it the same as if the user had completed the login - this._onLoggedIn(null); + this._onLoggedIn(); }, // returns a promise which resolves to the new MatrixClient - onRegistered: function(credentials, teamToken) { - // XXX: These both should be in state or ideally store(s) because we risk not + onRegistered: function(credentials) { + // XXX: This should be in state or ideally store(s) because we risk not // rendering the most up-to-date view of state otherwise. - // teamToken may not be truthy - this._teamToken = teamToken; this._is_registered = true; return Lifecycle.setLoggedIn(credentials); }, @@ -1888,7 +1840,6 @@ export default React.createClass({ onCloseAllSettings={this.onCloseAllSettings} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} - teamToken={this._teamToken} showCookieBar={this.state.showCookieBar} {...this.props} {...this.state} @@ -1929,7 +1880,6 @@ export default React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - teamServerConfig={this.props.config.teamServerConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} makeRegistrationUrl={this._makeRegistrationUrl} diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a3b4623168..f4a2c57aaf 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -167,13 +167,6 @@ module.exports = React.createClass({ onClose: PropTypes.func, // The brand string given when creating email pushers brand: PropTypes.string, - - // The base URL to use in the referral link. Defaults to window.location.origin. - referralBaseUrl: PropTypes.string, - - // Team token for the referral link. If falsy, the referral section will - // not appear - teamToken: PropTypes.string, }, getDefaultProps: function() { @@ -590,27 +583,6 @@ module.exports = React.createClass({ return ; }, - _renderReferral: function() { - const teamToken = this.props.teamToken; - if (!teamToken) { - return null; - } - if (typeof teamToken !== 'string') { - console.warn('Team token not a string'); - return null; - } - const href = (this.props.referralBaseUrl || window.location.origin) + - `/#/register?referrer=${this._me}&team_token=${teamToken}`; - return ( -
-

Referral

-
- { _t("Refer a friend to Riot:") } { href } -
-
- ); - }, - onLanguageChange: function(newLang) { if (this.state.language !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); @@ -1355,8 +1327,6 @@ module.exports = React.createClass({ { this._renderGroupSettings() } - { this._renderReferral() } - { notificationArea } { this._renderUserInterfaceSettings() } diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 8abd4073fe..acc45df86f 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -23,9 +23,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import RegistrationForm from '../../views/auth/RegistrationForm'; -import RtsClient from '../../../RtsClient'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; @@ -48,13 +46,6 @@ module.exports = React.createClass({ brand: PropTypes.string, email: PropTypes.string, referrer: PropTypes.string, - teamServerConfig: PropTypes.shape({ - // Email address to request new teams - supportEmail: PropTypes.string.isRequired, - // URL of the riot-team-server to get team configurations and track referrals - teamServerURL: PropTypes.string.isRequired, - }), - teamSelected: PropTypes.object, // The default server name to use when the user hasn't specified // one. This is used when displaying the defaultHsUrl in the UI. @@ -70,18 +61,11 @@ module.exports = React.createClass({ onLoginClick: PropTypes.func.isRequired, onCancelClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, - - rtsClient: PropTypes.shape({ - getTeamsConfig: PropTypes.func.isRequired, - trackReferral: PropTypes.func.isRequired, - getTeam: PropTypes.func.isRequired, - }), }, getInitialState: function() { return { busy: false, - teamServerBusy: false, errorText: null, // We remember the values entered by the user because // the registration form will be unmounted during the @@ -106,37 +90,7 @@ module.exports = React.createClass({ componentWillMount: function() { this._unmounted = false; - this._replaceClient(); - - if ( - this.props.teamServerConfig && - this.props.teamServerConfig.teamServerURL && - !this._rtsClient - ) { - this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL); - - this.setState({ - teamServerBusy: true, - }); - // GET team configurations including domains, names and icons - this._rtsClient.getTeamsConfig().then((data) => { - const teamsConfig = { - teams: data, - supportEmail: this.props.teamServerConfig.supportEmail, - }; - console.log('Setting teams config to ', teamsConfig); - this.setState({ - teamsConfig: teamsConfig, - teamServerBusy: false, - }); - }, (err) => { - console.error('Error retrieving config for teams', err); - this.setState({ - teamServerBusy: false, - }); - }); - } }, onServerConfigChange: function(config) { @@ -191,7 +145,7 @@ module.exports = React.createClass({ }); }, - _onUIAuthFinished: function(success, response, extra) { + _onUIAuthFinished: async function(success, response, extra) { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? @@ -240,58 +194,15 @@ module.exports = React.createClass({ doingUIAuth: false, }); - // Done regardless of `teamSelected`. People registering with non-team emails - // will just nop. The point of this being we might not have the email address - // that the user registered with at this stage (depending on whether this - // is the client they initiated registration). - let trackPromise = Promise.resolve(null); - if (this._rtsClient && extra.emailSid) { - // Track referral if this.props.referrer set, get team_token in order to - // retrieve team config and see welcome page etc. - trackPromise = this._rtsClient.trackReferral( - this.props.referrer || '', // Default to empty string = not referred - extra.emailSid, - extra.clientSecret, - ).then((data) => { - const teamToken = data.team_token; - // Store for use /w welcome pages - window.localStorage.setItem('mx_team_token', teamToken); - - this._rtsClient.getTeam(teamToken).then((team) => { - console.log( - `User successfully registered with team ${team.name}`, - ); - if (!team.rooms) { - return; - } - // Auto-join rooms - team.rooms.forEach((room) => { - if (room.auto_join && room.room_id) { - console.log(`Auto-joining ${room.room_id}`); - MatrixClientPeg.get().joinRoom(room.room_id); - } - }); - }, (err) => { - console.error('Error getting team config', err); - }); - - return teamToken; - }, (err) => { - console.error('Error tracking referral', err); - }); - } - - trackPromise.then((teamToken) => { - return this.props.onLoggedIn({ - userId: response.user_id, - deviceId: response.device_id, - homeserverUrl: this._matrixClient.getHomeserverUrl(), - identityServerUrl: this._matrixClient.getIdentityServerUrl(), - accessToken: response.access_token, - }, teamToken); - }).then((cli) => { - return this._setupPushers(cli); + const cli = await this.props.onLoggedIn({ + userId: response.user_id, + deviceId: response.device_id, + homeserverUrl: this._matrixClient.getHomeserverUrl(), + identityServerUrl: this._matrixClient.getIdentityServerUrl(), + accessToken: response.access_token, }); + + this._setupPushers(cli); }, _setupPushers: function(matrixClient) { @@ -356,12 +267,6 @@ module.exports = React.createClass({ }); }, - onTeamSelected: function(teamSelected) { - if (!this._unmounted) { - this.setState({ teamSelected }); - } - }, - onLoginClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); @@ -418,7 +323,7 @@ module.exports = React.createClass({ poll={true} /> ); - } else if (this.state.busy || this.state.teamServerBusy || !this.state.flows) { + } else if (this.state.busy || !this.state.flows) { registerBody = ; } else { let serverConfigSection; @@ -443,11 +348,9 @@ module.exports = React.createClass({ defaultPhoneCountry={this.state.formVals.phoneCountry} defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} - teamsConfig={this.state.teamsConfig} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} onRegisterClick={this.onFormSubmit} - onTeamSelected={this.onTeamSelected} flows={this.state.flows} /> { serverConfigSection } @@ -472,12 +375,7 @@ module.exports = React.createClass({ return ( - +

{ _t('Create your account') }

{ registerBody } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 873442e122..b38d8ca361 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -46,17 +46,6 @@ module.exports = React.createClass({ defaultPhoneNumber: PropTypes.string, defaultUsername: PropTypes.string, defaultPassword: PropTypes.string, - teamsConfig: PropTypes.shape({ - // Email address to request new teams - supportEmail: PropTypes.string, - teams: PropTypes.arrayOf(PropTypes.shape({ - // The displayed name of the team - "name": PropTypes.string, - // The domain of team email addresses - "domain": PropTypes.string, - })).required, - }), - minPasswordLength: PropTypes.number, onError: PropTypes.func, onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise @@ -75,7 +64,6 @@ module.exports = React.createClass({ getInitialState: function() { return { fieldValid: {}, - selectedTeam: null, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, }; @@ -150,10 +138,6 @@ module.exports = React.createClass({ return true; }, - _isUniEmail: function(email) { - return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org'); - }, - validateField: function(fieldID) { const pwd1 = this.refs.password.value.trim(); const pwd2 = this.refs.passwordConfirm.value.trim(); @@ -161,24 +145,6 @@ module.exports = React.createClass({ switch (fieldID) { case FIELD_EMAIL: { const email = this.refs.email.value; - if (this.props.teamsConfig && this._isUniEmail(email)) { - const matchingTeam = this.props.teamsConfig.teams.find( - (team) => { - return email.split('@').pop() === team.domain; - }, - ) || null; - this.setState({ - selectedTeam: matchingTeam, - showSupportEmail: !matchingTeam, - }); - this.props.onTeamSelected(matchingTeam); - } else { - this.props.onTeamSelected(null); - this.setState({ - selectedTeam: null, - showSupportEmail: false, - }); - } const emailValid = email === '' || Email.looksValid(email); if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) { this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL"); @@ -304,30 +270,6 @@ module.exports = React.createClass({ value={self.state.email} />
); - let belowEmailSection; - if (this.props.teamsConfig) { - if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { - belowEmailSection = ( -

- Sorry, but your university is not registered with us just yet.  - Email us on  - - { this.props.teamsConfig.supportEmail } -   - to get your university signed up. - Or continue to register with Riot to enjoy our open source platform. -

- ); - } else if (this.state.selectedTeam) { - belowEmailSection = ( -

- {_t("You are registering with %(SelectedTeamName)s", { - SelectedTeamName: this.state.selectedTeam.name, - })} -

- ); - } - } const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); let phoneSection; @@ -369,7 +311,6 @@ module.exports = React.createClass({
{ emailSection } - { belowEmailSection } { phoneSection } , - ); - - res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someEmailSid1234'}); - }); - - it('should NOT track a referral following successful registration of a non-team member', function(done) { - const onLoggedIn = jest.fn(function(creds, teamToken) { - expect(teamToken).toBeFalsy(); - done(); - }); - - const res = ReactTestUtils.renderIntoDocument( - , - ); - - res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someOtherEmailSid11'}); - }); -}); diff --git a/test/components/views/auth/RegistrationForm-test.js b/test/components/views/auth/RegistrationForm-test.js deleted file mode 100644 index 4e7db9a230..0000000000 --- a/test/components/views/auth/RegistrationForm-test.js +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -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. -*/ - -const jest = require('jest-mock'); -const React = require('react'); -const ReactTestUtils = require('react-addons-test-utils'); -const expect = require('expect'); - -const testUtils = require('test-utils'); - -const sdk = require('matrix-react-sdk'); -const RegistrationForm = sdk.getComponent('views.auth.RegistrationForm'); - -const TEAM_CONFIG = { - supportEmail: "support@some.domain", - teams: [ - { name: "The Team Org.", domain: "team.ac.uk" }, - { name: "The Super Team", domain: "superteam.ac.uk" }, - ], -}; - -function doInputEmail(inputEmail, onTeamSelected) { - const res = ReactTestUtils.renderIntoDocument( - , - ); - - const teamInput = res.refs.email; - teamInput.value = inputEmail; - - ReactTestUtils.Simulate.change(teamInput); - ReactTestUtils.Simulate.blur(teamInput); - - return res; -} - -function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) { - const onTeamSelected = jest.fn(); - doInputEmail(inputEmail, onTeamSelected); - - expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam); -} - -function expectSupportFromEmailInput(inputEmail, isSupportShown) { - const onTeamSelected = jest.fn(); - const res = doInputEmail(inputEmail, onTeamSelected); - - expect(res.state.showSupportEmail).toBe(isSupportShown); -} - -describe('RegistrationForm', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should select a team when a team email is entered', function() { - expectTeamSelectedFromEmailInput("member@team.ac.uk", TEAM_CONFIG.teams[0]); - }); - - it('should not select a team when an unrecognised team email is entered', function() { - expectTeamSelectedFromEmailInput("member@someunknownteam.ac.uk", null); - }); - - it('should show support when an unrecognised team email is entered', function() { - expectSupportFromEmailInput("member@someunknownteam.ac.uk", true); - }); - - it('should NOT show support when an unrecognised non-team email is entered', function() { - expectSupportFromEmailInput("someone@yahoo.com", false); - }); -}); diff --git a/test/test-utils.js b/test/test-utils.js index d5bcd9397a..f4f00effbb 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -104,20 +104,6 @@ export function createTestClient() { }; } -export function createTestRtsClient(teamMap, sidMap) { - return { - getTeamsConfig() { - return Promise.resolve(Object.keys(teamMap).map((token) => teamMap[token])); - }, - trackReferral(referrer, emailSid, clientSecret) { - return Promise.resolve({team_token: sidMap[emailSid]}); - }, - getTeam(teamToken) { - return Promise.resolve(teamMap[teamToken]); - }, - }; -} - /** * Create an Event. * @param {Object} opts Values for the event. From d1b78e5b0850f46f07a896b58800147e3e62c063 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 25 Jan 2019 17:54:11 -0600 Subject: [PATCH 33/38] Fix unmount TypeError in `DeviceVerifyButtons` --- src/components/views/elements/DeviceVerifyButtons.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index b88fbdfc06..cfd79529f1 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -42,7 +42,9 @@ export default React.createClass({ componentWillUnmount: function() { const cli = MatrixClientPeg.get(); - cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + if (cli) { + cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + } }, onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) { From 0a5e8e6cfe5535ca66c4098db834a45f51a71ec1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Jan 2019 14:35:04 +0100 Subject: [PATCH 34/38] WIP on persisting height across collapse/expand --- src/components/views/rooms/RoomList.js | 50 +++++++----------------- src/resizer/distributors/roomsublist2.js | 21 +++++++--- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3cbd8b0a4b..f79ca64869 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -87,11 +87,14 @@ module.exports = React.createClass({ if (subList) { subList.setHeight(size); } - this.subListSizes[key] = size; - window.localStorage.setItem("mx_roomlist_sizes", - JSON.stringify(this.subListSizes)); // update overflow indicators this._checkSubListsOverflow(); + // don't store height for collapsed sublists + if(!this.collapsedState[key]) { + this.subListSizes[key] = size; + window.localStorage.setItem("mx_roomlist_sizes", + JSON.stringify(this.subListSizes)); + } }, this.subListSizes, this.collapsedState); return { @@ -161,23 +164,6 @@ module.exports = React.createClass({ this._delayedRefreshRoomListLoopCount = 0; }, - _onSubListResize: function(newSize, id) { - if (!id) { - return; - } - if (typeof newSize === "string") { - newSize = Number.MAX_SAFE_INTEGER; - } - if (newSize === null) { - delete this.subListSizes[id]; - } else { - this.subListSizes[id] = newSize; - } - window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); - // update overflow indicators - this._checkSubListsOverflow(); - }, - componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); const cfg = { @@ -202,13 +188,9 @@ module.exports = React.createClass({ componentDidUpdate: function(prevProps) { this._repositionIncomingCallBox(undefined, false); - if (this.props.searchFilter !== prevProps.searchFilter) { - // restore sizes - Object.keys(this.subListSizes).forEach((key) => { - this._restoreSubListSize(key); - }); - this._checkSubListsOverflow(); - } + // if (this.props.searchFilter !== prevProps.searchFilter) { + // this._checkSubListsOverflow(); + // } this._layout.update( this._layoutSections, this.resizeContainer && this.resizeContainer.clientHeight, @@ -583,20 +565,16 @@ module.exports = React.createClass({ this.collapsedState[key] = collapsed; window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); // load the persisted size configuration of the expanded sub list - this._layout.setCollapsed(key, collapsed); + if (collapsed) { + this._layout.collapseSection(key); + } else { + this._layout.expandSection(key, this.subListSizes[key]); + } // check overflow, as sub lists sizes have changed // important this happens after calling resize above this._checkSubListsOverflow(); }, - _restoreSubListSize(key) { - const size = this.subListSizes[key]; - const handle = this.resizer.forHandleWithId(key); - if (handle) { - handle.resize(size); - } - }, - // check overflow for scroll indicator gradient _checkSubListsOverflow() { Object.values(this._subListRefs).forEach(l => l.checkOverflow()); diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index 36c140887b..bd4c37df84 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -34,11 +34,11 @@ export class Layout { constructor(applyHeight, initialSizes, collapsedState) { this._applyHeight = applyHeight; this._sections = []; - this._collapsedState = collapsedState || {}; + this._collapsedState = Object.assign({}, collapsedState); this._availableHeight = 0; // need to store heights by id so it doesn't get // assigned to wrong section when a section gets added? - this._sectionHeights = initialSizes || {}; + this._sectionHeights = Object.assign({}, initialSizes); this._originalHeights = []; this._heights = []; } @@ -49,10 +49,17 @@ export class Layout { this._applyNewSize(); } - setCollapsed(id, collapsed) { - this._collapsedState[id] = collapsed; + expandSection(id, height) { + this._collapsedState[id] = false; + this._applyNewSize(); + this.openHandle(id).setHeight(height).finish(); + } + + collapseSection(id) { + this._collapsedState[id] = true; this._applyNewSize(); } + // [{id, count}] update(sections, availableHeight) { if (Number.isFinite(availableHeight)) { @@ -98,7 +105,7 @@ export class Layout { this._heights = this._originalHeights.slice(0); this._applyOverflow(-offset, sections, true); this._applyHeights(); - this._originalHeights = this._heights; + this._commitHeights(); this._sections.forEach((section, i) => { this._sectionHeights[section.id] = this._originalHeights[i]; }); @@ -163,7 +170,7 @@ export class Layout { if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) { // we weren't able to distribute all the overflow so recurse and try again - log("recursing with", overflow, unclampedSections); + // log("recursing with", overflow, unclampedSections); overflow = this._applyOverflow(overflow, unclampedSections, blend); } @@ -275,10 +282,12 @@ class Handle { setHeight(height) { this._layout._relayout(this._anchor, height - this._initialHeight); + return this; } finish() { this._layout._commitHeights(); + return this; } } From bfb1031a6bac7b43696d70f55d34366a0178b21d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Jan 2019 14:52:40 +0100 Subject: [PATCH 35/38] unify heights stored by id and index, to avoid them getting out of sync effectively get rid of _originalHeights and calculate the array from the dictionary when needed --- src/resizer/distributors/roomsublist2.js | 38 ++++++++++-------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index bd4c37df84..aede4938fa 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -36,10 +36,9 @@ export class Layout { this._sections = []; this._collapsedState = Object.assign({}, collapsedState); this._availableHeight = 0; - // need to store heights by id so it doesn't get - // assigned to wrong section when a section gets added? + // heights stored by section section id this._sectionHeights = Object.assign({}, initialSizes); - this._originalHeights = []; + // in-progress heights, while dragging. Committed on mouse-up. this._heights = []; } @@ -67,13 +66,13 @@ export class Layout { } const totalHeight = this._getAvailableHeight(); this._sections.forEach((section, i) => { - this._originalHeights[i] = - this._sectionHeights[section.id] || - clamp( + if (!this._sectionHeights.hasOwnProperty(section.id)) { + this._sectionHeights[section.id] = clamp( totalHeight / this._sections.length, this._getMinHeight(i), this._getMaxHeight(i), ); + }; }); this._sections = sections; this._applyNewSize(); @@ -82,7 +81,7 @@ export class Layout { openHandle(id) { const index = this._getSectionIndex(id); //log(`openHandle resolved ${id} to ${index}`); - return new Handle(this, index, this._originalHeights[index]); + return new Handle(this, index, this._sectionHeights[id]); } _getAvailableHeight() { @@ -95,20 +94,15 @@ export class Layout { _applyNewSize() { const newHeight = this._getAvailableHeight(); - let currHeight = 0; - const sections = []; - for (let i = 0; i < this._sections.length; i++) { - currHeight += this._originalHeights[i]; - sections.push(i); - } + const currHeight = this._sections.reduce((sum, section) => { + return sum + this._sectionHeights[section.id]; + }, 0); const offset = newHeight - currHeight; - this._heights = this._originalHeights.slice(0); + this._heights = this._sections.map((section) => this._sectionHeights[section.id]); + const sections = this._sections.map((_, i) => i); this._applyOverflow(-offset, sections, true); this._applyHeights(); this._commitHeights(); - this._sections.forEach((section, i) => { - this._sectionHeights[section.id] = this._originalHeights[i]; - }); } _getSectionIndex(id) { @@ -202,10 +196,10 @@ export class Layout { return overflowBelow; } - // @param offset the amount the anchor is moved from what is stored in _originalHeights, positive if downwards + // @param offset the amount the anchor is moved from what is stored in _sectionHeights, positive if downwards // if we're clamped, return the offset we should be clamped at. _relayout(anchor = 0, offset = 0, clamped = false) { - this._heights = this._originalHeights.slice(0); + this._heights = this._sections.map((section) => this._sectionHeights[section.id]); // are these the amounts the items above/below shrank/grew and need to be relayouted? let overflowAbove; let overflowBelow; @@ -267,9 +261,9 @@ export class Layout { } _commitHeights() { - const heights = this._heights.slice(0); - log("committing heights:", heights); - this._originalHeights = heights; + this._sections.forEach((section, i) => { + this._sectionHeights[section.id] = this._heights[i]; + }); } } From d08216e8570dd6d6c27257345b99d793008cfeb6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Jan 2019 14:56:14 +0100 Subject: [PATCH 36/38] fix lint --- src/resizer/distributors/roomsublist2.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index aede4938fa..e3559c54f3 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -72,7 +72,7 @@ export class Layout { this._getMinHeight(i), this._getMaxHeight(i), ); - }; + } }); this._sections = sections; this._applyNewSize(); @@ -143,7 +143,11 @@ export class Layout { let overflowPerSection = blend ? (overflow / sections.length) : overflow; for (const i of sections) { - const newHeight = clamp(this._heights[i] - overflowPerSection, this._getMinHeight(i), this._getMaxHeight(i)); + const newHeight = clamp( + this._heights[i] - overflowPerSection, + this._getMinHeight(i), + this._getMaxHeight(i), + ); if (newHeight == this._heights[i] - overflowPerSection) { unclampedSections.push(i); } From f103e60d1d14f408fbef248aae18075fa4c68365 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Jan 2019 15:22:05 +0100 Subject: [PATCH 37/38] fix lint - bis --- src/resizer/distributors/roomsublist2.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index e3559c54f3..ca440370c2 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -19,8 +19,8 @@ import FixedDistributor from "./fixed"; // const allowWhitespace = true; const handleHeight = 1; -function log(...params) { - console.log.apply(console, params); +function log() { + // console.log.apply(console, ["LAYOUT: "].concat(params)); } function clamp(height, min, max) { From eaf212dd89baa8e267189716ea9ff86902649322 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Jan 2019 15:28:56 +0100 Subject: [PATCH 38/38] fix lint - bis bis --- src/resizer/distributors/roomsublist2.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resizer/distributors/roomsublist2.js b/src/resizer/distributors/roomsublist2.js index ca440370c2..5d291e1516 100644 --- a/src/resizer/distributors/roomsublist2.js +++ b/src/resizer/distributors/roomsublist2.js @@ -20,7 +20,6 @@ import FixedDistributor from "./fixed"; const handleHeight = 1; function log() { - // console.log.apply(console, ["LAYOUT: "].concat(params)); } function clamp(height, min, max) {