From 9e4d8e7dfee11e151144ac47d4bbc9cbdccba6e0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 May 2019 15:00:26 +0200 Subject: [PATCH 01/40] document settingDefaults --- docs/settings.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/settings.md b/docs/settings.md index 1ba8981f84..30818625d2 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -51,6 +51,17 @@ Settings are the different options a user may set or experience in the applicati } ``` +Settings that support the config level can be set in the config file under the `settingDefaults` key (note that the "theme" setting is special cased to the `default_theme` in the config file): +``` +{ + ... + settingDefaults: { + settingName: true + }, + ... +} +``` + ### Getting values for a setting After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always From 984f9ea4aabbac033ee70470713ddff948b41ba3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 27 Mar 2020 13:44:32 -0600 Subject: [PATCH 02/40] Fix fallback auth link to act and look like a link --- .../views/auth/InteractiveAuthEntryComponents.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index aaf8c88440..db73467ff7 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -598,7 +598,10 @@ export const FallbackAuthEntry = createReactClass({ } }, - _onShowFallbackClick: function() { + _onShowFallbackClick: function(e) { + e.preventDefault(); + e.stopPropagation(); + const url = this.props.matrixClient.getFallbackAuthUrl( this.props.loginType, this.props.authSessionId, @@ -627,7 +630,7 @@ export const FallbackAuthEntry = createReactClass({ } return (
- { _t("Start authentication") } + { _t("Start authentication") } {errorSection}
); From 1e30bdb73956e0d103b5d5168645aa635cca6b33 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 27 Mar 2020 14:39:59 -0600 Subject: [PATCH 03/40] Early proof of concept for SSO UIA It works well enough to start doing design. --- .../auth/InteractiveAuthEntryComponents.js | 66 ++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index db73467ff7..9f0d5f1534 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -1,7 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -565,6 +565,67 @@ export const MsisdnAuthEntry = createReactClass({ }, }); +export class SSOAuthEntry extends React.Component { + static propTypes = { + matrixClient: PropTypes.object.isRequired, + authSessionId: PropTypes.string.isRequired, + loginType: PropTypes.string.isRequired, + submitAuthDict: PropTypes.func.isRequired, + errorText: PropTypes.string, + }; + + static LOGIN_TYPE = "m.login.sso"; + static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; + + static STAGE_PREAUTH = 1; // button to start SSO + static STAGE_POSTAUTH = 2; // button to confirm SSO completed + + constructor(props) { + super(props); + + this.state = { + // We actually send the user through fallback auth so we don't have to + // deal with a redirect back to us, losing application context. + ssoUrl: props.matrixClient.getFallbackAuthUrl( + this.props.loginType, + this.props.authSessionId, + ), + stage: SSOAuthEntry.STAGE_PREAUTH, + }; + } + + onStartAuthClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Note: We don't use PlatformPeg's startSsoAuth functions because we almost + // certainly will need to open the thing in a new tab to avoid loosing application + // context. + + window.open(e.target.href, '_blank'); + this.setState({stage: SSOAuthEntry.STAGE_POSTAUTH}); + }; + + onConfirmClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + this.props.submitAuthDict({}); + }; + + render () { + if (this.state.stage === SSOAuthEntry.STAGE_PREAUTH) { + return + {_t("Single Sign On")} + ; + } else { + return + {_t("Continue")} + ; + } + } +} + export const FallbackAuthEntry = createReactClass({ displayName: 'FallbackAuthEntry', @@ -643,11 +704,12 @@ const AuthEntryComponents = [ EmailIdentityAuthEntry, MsisdnAuthEntry, TermsAuthEntry, + SSOAuthEntry, ]; export default function getEntryComponentForLoginType(loginType) { for (const c of AuthEntryComponents) { - if (c.LOGIN_TYPE == loginType) { + if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { return c; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6e195aa16..ed89068f91 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1831,6 +1831,7 @@ "Please enter the code it contains:": "Please enter the code it contains:", "Code": "Code", "Submit": "Submit", + "Single Sign On": "Single Sign On", "Start authentication": "Start authentication", "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", From ffa75ef48c47a0d75caeb1490bb1d4066ede2728 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2020 20:03:46 -0600 Subject: [PATCH 04/40] Wire up all the dialog parts for SSO, using device deletion as a POC --- .../auth/_InteractiveAuthEntryComponents.scss | 11 ++ res/css/views/elements/_AccessibleButton.scss | 17 ++- src/components/structures/InteractiveAuth.js | 29 ++++- .../auth/InteractiveAuthEntryComponents.js | 100 ++++++++++++++---- .../views/dialogs/InteractiveAuthDialog.js | 68 +++++++++++- src/components/views/settings/DevicesPanel.js | 19 ++++ src/i18n/strings/en_EN.json | 9 +- 7 files changed, 224 insertions(+), 29 deletions(-) diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 85007aeecb..05cddf2c48 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -60,3 +60,14 @@ limitations under the License. .mx_InteractiveAuthEntryComponents_passwordSection { width: 300px; } + +.mx_InteractiveAuthEntryComponents_sso_buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 20px; + + .mx_AccessibleButton { + margin-left: 5px; + } +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index b87071745d..0c48a07ccf 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -36,6 +36,13 @@ limitations under the License. font-weight: 600; } +.mx_AccessibleButton_kind_primary_outline { + color: $button-primary-bg-color; + background-color: $button-secondary-bg-color; + border: 1px solid $button-primary-bg-color; + font-weight: 600; +} + .mx_AccessibleButton_kind_secondary { color: $accent-color; font-weight: 600; @@ -60,7 +67,15 @@ limitations under the License. background-color: $button-danger-bg-color; } -.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_danger_outline { + color: $button-danger-bg-color; + background-color: $button-secondary-bg-color; + border: 1px solid $button-danger-bg-color; +} + +.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, +mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled +{ color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index f4adb5751f..2492bf79a0 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -1,6 +1,6 @@ /* Copyright 2017 Vector Creations Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom import * as sdk from '../../index'; +export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); + export default createReactClass({ displayName: 'InteractiveAuth', @@ -47,7 +49,7 @@ export default createReactClass({ // @param {bool} status True if the operation requiring // auth was completed sucessfully, false if canceled. // @param {object} result The result of the authenticated call - // if successful, otherwise the error object + // if successful, otherwise the error object. // @param {object} extra Additional information about the UI Auth // process: // * emailSid {string} If email auth was performed, the sid of @@ -75,6 +77,15 @@ export default createReactClass({ // is managed by some other party and should not be managed by // the component itself. continueIsManaged: PropTypes.bool, + + // Called when the stage changes, or the stage's phase changes. First + // argument is the stage, second is the phase. Some stages do not have + // phases and will be counted as 0 (numeric). + onStagePhaseChange: PropTypes.func, + + // continueText and continueKind are passed straight through to the AuthEntryComponent. + continueText: PropTypes.string, + continueKind: PropTypes.string, }, getInitialState: function() { @@ -204,6 +215,16 @@ export default createReactClass({ this._authLogic.submitAuthDict(authData); }, + _onPhaseChange: function(newPhase) { + if (this.props.onStagePhaseChange) { + this.props.onStagePhaseChange(this.state.authStage, newPhase || 0); + } + }, + + _onStageCancel: function() { + this.props.onAuthFinished(false, ERROR_USER_CANCELLED); + }, + _renderCurrentStage: function() { const stage = this.state.authStage; if (!stage) { @@ -232,6 +253,10 @@ export default createReactClass({ fail={this._onAuthStageFailed} setEmailSid={this._setEmailSid} showContinue={!this.props.continueIsManaged} + onPhaseChange={this._onPhaseChange} + continueText={this.props.continueText} + continueKind={this.props.continueKind} + onCancel={this._onStageCancel} /> ); }, diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 26a293724a..d262a5525d 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -25,6 +25,7 @@ import classnames from 'classnames'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; +import AccessibleButton from "../elements/AccessibleButton"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore"; * session to be failed and the process to go back to the start. * setEmailSid: m.login.email.identity only: a function to be called with the * email sid after a token is requested. + * onPhaseChange: A function which is called when the stage's phase changes. If + * the stage has no phases, call this with DEFAULT_PHASE. Takes + * one argument, the phase, and is always defined/required. + * continueText: For stages which have a continue button, the text to use. + * continueKind: For stages which have a continue button, the style of button to + * use. For example, 'danger' or 'primary'. + * onCancel A function with no arguments which is called by the stage if the + * user knowingly cancelled/dismissed the authentication attempt. * * Each component may also provide the following functions (beyond the standard React ones): * focus: set the input focus appropriately in the form. */ +export const DEFAULT_PHASE = 0; + export const PasswordAuthEntry = createReactClass({ displayName: 'PasswordAuthEntry', @@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({ // is the auth logic currently waiting for something to // happen? busy: PropTypes.bool, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, getInitialState: function() { @@ -175,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({ stageParams: PropTypes.object.isRequired, errorText: PropTypes.string, busy: PropTypes.bool, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, _onCaptchaResponse: function(response) { @@ -236,6 +257,11 @@ export const TermsAuthEntry = createReactClass({ errorText: PropTypes.string, busy: PropTypes.bool, showContinue: PropTypes.bool, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, componentWillMount: function() { @@ -378,6 +404,11 @@ export const EmailIdentityAuthEntry = createReactClass({ stageState: PropTypes.object.isRequired, fail: PropTypes.func.isRequired, setEmailSid: PropTypes.func.isRequired, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, getInitialState: function() { @@ -420,6 +451,11 @@ export const MsisdnAuthEntry = createReactClass({ clientSecret: PropTypes.func, submitAuthDict: PropTypes.func.isRequired, matrixClient: PropTypes.object, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, getInitialState: function() { @@ -571,13 +607,17 @@ export class SSOAuthEntry extends React.Component { loginType: PropTypes.string.isRequired, submitAuthDict: PropTypes.func.isRequired, errorText: PropTypes.string, + onPhaseChange: PropTypes.func.isRequired, + continueText: PropTypes.string, + continueKind: PropTypes.string, + onCancel: PropTypes.func, }; static LOGIN_TYPE = "m.login.sso"; static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; - static STAGE_PREAUTH = 1; // button to start SSO - static STAGE_POSTAUTH = 2; // button to confirm SSO completed + static PHASE_PREAUTH = 1; // button to start SSO + static PHASE_POSTAUTH = 2; // button to confirm SSO completed constructor(props) { super(props); @@ -589,39 +629,56 @@ export class SSOAuthEntry extends React.Component { this.props.loginType, this.props.authSessionId, ), - stage: SSOAuthEntry.STAGE_PREAUTH, + phase: SSOAuthEntry.PHASE_PREAUTH, }; } - onStartAuthClick = (e) => { - e.preventDefault(); - e.stopPropagation(); + componentDidMount(): void { + this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH); + } + onStartAuthClick = () => { // Note: We don't use PlatformPeg's startSsoAuth functions because we almost // certainly will need to open the thing in a new tab to avoid loosing application // context. - window.open(e.target.href, '_blank'); - this.setState({stage: SSOAuthEntry.STAGE_POSTAUTH}); + window.open(this.state.ssoUrl, '_blank'); + this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); + this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); }; - onConfirmClick = (e) => { - e.preventDefault(); - e.stopPropagation(); - + onConfirmClick = () => { this.props.submitAuthDict({}); }; render () { - if (this.state.stage === SSOAuthEntry.STAGE_PREAUTH) { - return - {_t("Single Sign On")} - ; + let continueButton = null; + const cancelButton = ( + {_t("Cancel")} + ); + if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) { + continueButton = ( + {this.props.continueText || _t("Single Sign On")} + ); } else { - return - {_t("Continue")} - ; + continueButton = ( + {this.props.continueText || _t("Confirm")} + ); } + + return
+ {cancelButton} + {continueButton} +
; } } @@ -634,6 +691,11 @@ export const FallbackAuthEntry = createReactClass({ loginType: PropTypes.string.isRequired, submitAuthDict: PropTypes.func.isRequired, errorText: PropTypes.string, + onPhaseChange: PropTypes.func.isRequired, + }, + + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); }, componentWillMount: function() { diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index ff9f55cb74..c070cc6589 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +24,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; +import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; export default createReactClass({ displayName: 'InteractiveAuthDialog', @@ -44,12 +46,36 @@ export default createReactClass({ onFinished: PropTypes.func.isRequired, + // Optional title and body to show when not showing a particular stage title: PropTypes.string, + body: PropTypes.string, + + // Optional title and body pairs for particular stages and phases within + // those stages. Object structure/example is: + // { + // "org.example.stage_type": { + // 1: { + // "body": "This is a body for phase 1" of org.example.stage_type, + // "title": "Title for phase 1 of org.example.stage_type" + // }, + // 2: { + // "body": "This is a body for phase 2 of org.example.stage_type", + // "title": "Title for phase 2 of org.example.stage_type" + // "continueText": "Confirm identity with Example Auth", + // "continueKind": "danger" + // } + // } + // } + aestheticsForStagePhases: PropTypes.object, }, getInitialState: function() { return { authError: null, + + // See _onUpdateStagePhase() + uiaStage: null, + uiaStagePhase: null, }; }, @@ -57,12 +83,22 @@ export default createReactClass({ if (success) { this.props.onFinished(true, result); } else { - this.setState({ - authError: result, - }); + if (result === ERROR_USER_CANCELLED) { + this.props.onFinished(false, null); + } else { + this.setState({ + authError: result, + }); + } } }, + _onUpdateStagePhase: function(newStage, newPhase) { + console.log({newStage, newPhase}); + // We copy the stage and stage phase params into state for title selection in render() + this.setState({uiaStage: newStage, uiaStagePhase: newPhase}); + }, + _onDismissClick: function() { this.props.onFinished(false); }, @@ -71,6 +107,23 @@ export default createReactClass({ const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + // Let's pick a title, body, and other params text that we'll show to the user. The order + // is most specific first, so stagePhase > our props > defaults. + + let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication')); + let body = this.state.authError ? null : this.props.body; + let continueText = null; + let continueKind = null; + if (!this.state.authError && this.props.aestheticsForStagePhases) { + if (this.props.aestheticsForStagePhases[this.state.uiaStage]) { + const aesthetics = this.props.aestheticsForStagePhases[this.state.uiaStage][this.state.uiaStagePhase]; + if (aesthetics && aesthetics.title) title = aesthetics.title; + if (aesthetics && aesthetics.body) body = aesthetics.body; + if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText; + if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind; + } + } + let content; if (this.state.authError) { content = ( @@ -88,11 +141,16 @@ export default createReactClass({ } else { content = (
-
); @@ -101,7 +159,7 @@ export default createReactClass({ return ( { content } diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 2120801a81..61e7724a6f 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -23,6 +23,7 @@ import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; export default class DevicesPanel extends React.Component { constructor(props) { @@ -123,11 +124,29 @@ export default class DevicesPanel extends React.Component { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm deleting these sessions"), + body: _t("Click the button below to confirm deleting these sessions."), + continueText: _t("Delete sessions"), + continueKind: "danger", + }, + }; Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, { title: _t("Authentication"), matrixClient: MatrixClientPeg.get(), authData: error.data, makeRequest: this._makeDeleteRequest.bind(this), + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }); }).catch((e) => { console.error("Error deleting sessions", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e867623cc9..ecc2698f6d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,6 +598,12 @@ "up to date": "up to date", "Your homeserver does not support session management.": "Your homeserver does not support session management.", "Unable to load session list": "Unable to load session list", + "Use Single Sign On to continue": "Use Single Sign On to continue", + "Confirm deleting these sessions by using Single Sign On to prove your identity.": "Confirm deleting these sessions by using Single Sign On to prove your identity.", + "Single Sign On": "Single Sign On", + "Confirm deleting these sessions": "Confirm deleting these sessions", + "Click the button below to confirm deleting these sessions.": "Click the button below to confirm deleting these sessions.", + "Delete sessions": "Delete sessions", "Authentication": "Authentication", "Delete %(count)s sessions|other": "Delete %(count)s sessions", "Delete %(count)s sessions|one": "Delete %(count)s session", @@ -1831,7 +1837,7 @@ "Please enter the code it contains:": "Please enter the code it contains:", "Code": "Code", "Submit": "Submit", - "Single Sign On": "Single Sign On", + "Confirm": "Confirm", "Start authentication": "Start authentication", "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", @@ -1862,7 +1868,6 @@ "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only", "Enter username": "Enter username", "Email (optional)": "Email (optional)", - "Confirm": "Confirm", "Phone (optional)": "Phone (optional)", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on ": "Create your Matrix account on ", From 6112d92f809c952fe80520a93b0f98d0993057d3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2020 20:11:23 -0600 Subject: [PATCH 05/40] Remove debugging --- src/components/views/dialogs/InteractiveAuthDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index c070cc6589..af5dc5108c 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -94,7 +94,6 @@ export default createReactClass({ }, _onUpdateStagePhase: function(newStage, newPhase) { - console.log({newStage, newPhase}); // We copy the stage and stage phase params into state for title selection in render() this.setState({uiaStage: newStage, uiaStagePhase: newPhase}); }, From de7952015086e8e5d3bab204fd23665def9435c2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2020 20:18:52 -0600 Subject: [PATCH 06/40] Add SSO dialog copy for 3PID adding (email/phone) --- src/AddThreepid.js | 38 +++++++++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 12 +++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 7a3250d0ca..1fc492ef85 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -21,6 +21,7 @@ import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; import IdentityAuthClient from './IdentityAuthClient'; +import {SSOAuthEntry} from "./components/views/auth/InteractiveAuthEntryComponents"; function getIdServerDomain() { return MatrixClientPeg.get().idBaseUrl.split("://")[1]; @@ -188,11 +189,30 @@ export default class AddThreepid { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm adding this email address by using Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm adding email"), + body: _t("Click the button below to confirm adding this email address."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; const { finished } = Modal.createTrackedDialog('Add Email', '', InteractiveAuthDialog, { title: _t("Add Email Address"), matrixClient: MatrixClientPeg.get(), authData: e.data, makeRequest: this._makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }); return finished; } @@ -285,11 +305,29 @@ export default class AddThreepid { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm adding this phone number by using Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm adding phone number"), + body: _t("Click the button below to confirm adding this phone number."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; const { finished } = Modal.createTrackedDialog('Add MSISDN', '', InteractiveAuthDialog, { title: _t("Add Phone Number"), matrixClient: MatrixClientPeg.get(), authData: e.data, makeRequest: this._makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }); return finished; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ecc2698f6d..572268f90d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1,8 +1,17 @@ { "This email address is already in use": "This email address is already in use", "This phone number is already in use": "This phone number is already in use", + "Use Single Sign On to continue": "Use Single Sign On to continue", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity.", + "Single Sign On": "Single Sign On", + "Confirm adding email": "Confirm adding email", + "Click the button below to confirm adding this email address.": "Click the button below to confirm adding this email address.", + "Confirm": "Confirm", "Add Email Address": "Add Email Address", "Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirm adding this phone number by using Single Sign On to prove your identity.", + "Confirm adding phone number": "Confirm adding phone number", + "Click the button below to confirm adding this phone number.": "Click the button below to confirm adding this phone number.", "Add Phone Number": "Add Phone Number", "The platform you're on": "The platform you're on", "The version of Riot": "The version of Riot", @@ -598,9 +607,7 @@ "up to date": "up to date", "Your homeserver does not support session management.": "Your homeserver does not support session management.", "Unable to load session list": "Unable to load session list", - "Use Single Sign On to continue": "Use Single Sign On to continue", "Confirm deleting these sessions by using Single Sign On to prove your identity.": "Confirm deleting these sessions by using Single Sign On to prove your identity.", - "Single Sign On": "Single Sign On", "Confirm deleting these sessions": "Confirm deleting these sessions", "Click the button below to confirm deleting these sessions.": "Click the button below to confirm deleting these sessions.", "Delete sessions": "Delete sessions", @@ -1837,7 +1844,6 @@ "Please enter the code it contains:": "Please enter the code it contains:", "Code": "Code", "Submit": "Submit", - "Confirm": "Confirm", "Start authentication": "Start authentication", "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", From 8bc86deaaaf93928c722a077bd17ece980a0821d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2020 20:23:34 -0600 Subject: [PATCH 07/40] Appease the style linter --- res/css/views/elements/_AccessibleButton.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0c48a07ccf..de39525588 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -48,7 +48,8 @@ limitations under the License. font-weight: 600; } -.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled, +.mx_AccessibleButton_kind_primary_outline.mx_AccessibleButton_disabled { opacity: 0.4; } @@ -74,8 +75,7 @@ limitations under the License. } .mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, -mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled -{ +.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } From d899576578b6b2bfc66ceef7a8b13c857c8857f3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Mar 2020 20:24:53 -0600 Subject: [PATCH 08/40] Appease the linter --- src/AddThreepid.js | 6 ++++-- src/components/views/auth/InteractiveAuthEntryComponents.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 1fc492ef85..f06f7c187d 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -193,7 +193,8 @@ export default class AddThreepid { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), - body: _t("Confirm adding this email address by using Single Sign On to prove your identity."), + body: _t("Confirm adding this email address by using " + + "Single Sign On to prove your identity."), continueText: _t("Single Sign On"), continueKind: "primary", }, @@ -308,7 +309,8 @@ export default class AddThreepid { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), - body: _t("Confirm adding this phone number by using Single Sign On to prove your identity."), + body: _t("Confirm adding this phone number by using " + + "Single Sign On to prove your identity."), continueText: _t("Single Sign On"), continueKind: "primary", }, diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d262a5525d..a59b1354b6 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -651,7 +651,7 @@ export class SSOAuthEntry extends React.Component { this.props.submitAuthDict({}); }; - render () { + render() { let continueButton = null; const cancelButton = ( Date: Tue, 31 Mar 2020 08:29:42 -0600 Subject: [PATCH 09/40] Update src/components/views/auth/InteractiveAuthEntryComponents.js Co-Authored-By: David Baker --- src/components/views/auth/InteractiveAuthEntryComponents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index a59b1354b6..14a8791cda 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -639,7 +639,7 @@ export class SSOAuthEntry extends React.Component { onStartAuthClick = () => { // Note: We don't use PlatformPeg's startSsoAuth functions because we almost - // certainly will need to open the thing in a new tab to avoid loosing application + // certainly will need to open the thing in a new tab to avoid losing application // context. window.open(this.state.ssoUrl, '_blank'); From 64c11c35650fe37a6cbf7edb115f11383a91ef89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 31 Mar 2020 09:26:17 -0600 Subject: [PATCH 10/40] Move ssoUrl to a class property --- .../auth/InteractiveAuthEntryComponents.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 14a8791cda..4e2f444844 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -619,16 +619,19 @@ export class SSOAuthEntry extends React.Component { static PHASE_PREAUTH = 1; // button to start SSO static PHASE_POSTAUTH = 2; // button to confirm SSO completed + _ssoUrl: string; + constructor(props) { super(props); + // We actually send the user through fallback auth so we don't have to + // deal with a redirect back to us, losing application context. + this._ssoUrl = props.matrixClient.getFallbackAuthUrl( + this.props.loginType, + this.props.authSessionId, + ); + this.state = { - // We actually send the user through fallback auth so we don't have to - // deal with a redirect back to us, losing application context. - ssoUrl: props.matrixClient.getFallbackAuthUrl( - this.props.loginType, - this.props.authSessionId, - ), phase: SSOAuthEntry.PHASE_PREAUTH, }; } @@ -642,7 +645,7 @@ export class SSOAuthEntry extends React.Component { // certainly will need to open the thing in a new tab to avoid losing application // context. - window.open(this.state.ssoUrl, '_blank'); + window.open(this._ssoUrl, '_blank'); this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); }; From c86d75693bbd34513b8b431961a15272bbfe9c12 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 31 Mar 2020 14:05:56 -0600 Subject: [PATCH 11/40] Fix a number of minor code quality issues Most of these are complaints from my IDE. Discovered by going through nearly every file looking for React warnings. --- .../views/dialogs/EncryptedEventDialog.js | 2 +- .../views/dialogs/eventindex/ManageEventIndexDialog.js | 10 +++------- src/components/structures/GroupView.js | 1 - src/components/structures/auth/SoftLogout.js | 2 +- src/components/views/dialogs/RoomSettingsDialog.js | 2 +- src/components/views/elements/AppTile.js | 2 +- src/components/views/elements/LanguageDropdown.js | 4 ++-- src/components/views/elements/Pill.js | 2 +- src/components/views/rooms/AppsDrawer.js | 2 +- src/components/views/rooms/RoomRecoveryReminder.js | 1 - test/components/views/rooms/RoomSettings-test.js | 2 +- 11 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index b602cf60fe..3e5c74335f 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -79,7 +79,7 @@ export default createReactClass({ }, onDeviceVerificationChanged: function(userId, device) { - if (userId == this.props.event.getSender()) { + if (userId === this.props.event.getSender()) { this.refreshDevice().then((dev) => { this.setState({ device: dev }); }); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index 3d7249b5a1..4ac9bf52a3 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -30,7 +30,7 @@ import EventIndexPeg from "../../../../indexing/EventIndexPeg"; export default class ManageEventIndexDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, - } + }; constructor(props) { super(props); @@ -126,16 +126,12 @@ export default class ManageEventIndexDialog extends React.Component { import("./DisableEventIndexDialog"), null, null, /* priority = */ false, /* static = */ true, ); - } - - _onDone = () => { - this.props.onFinished(true); - } + }; _onCrawlerSleepTimeChange = (e) => { this.setState({crawlerSleepTime: e.target.value}); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); - } + }; render() { let crawlerState; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 524694fe95..af205f7e1b 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -433,7 +433,6 @@ export default createReactClass({ this._matrixClient = MatrixClientPeg.get(); this._matrixClient.on("Group.myMembership", this._onGroupMyMembership); - this._changeAvatarComponent = null; this._initGroupStore(this.props.groupId, true); this._dispatcherRef = dis.register(this._onAction); diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 287f7e5605..08ab7e8a61 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -54,7 +54,7 @@ export default class SoftLogout extends React.Component { this.state = { loginView: LOGIN_VIEW.LOADING, - keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) + keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) busy: false, password: "", diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 76faf60eef..5d369862b8 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -41,7 +41,7 @@ export default class RoomSettingsDialog extends React.Component { } componentWillUnmount() { - dis.unregister(this._dispatcherRef); + if (this._dispatcherRef) dis.unregister(this._dispatcherRef); } _onAction = (payload) => { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 0a8bf7443b..d74d8fa485 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -161,7 +161,7 @@ export default class AppTile extends React.Component { componentWillUnmount() { // Widget action listeners - dis.unregister(this.dispatcherRef); + if (this.dispatcherRef) dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index cb4e2e4da6..d2546644a0 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -24,8 +24,8 @@ import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; function languageMatchesSearchQuery(query, language) { - if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; - if (language.value.toUpperCase() == query.toUpperCase()) return true; + if (language.label.toUpperCase().includes(query.toUpperCase())) return true; + if (language.value.toUpperCase() === query.toUpperCase()) return true; return false; } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 5f143a06a6..627c51967a 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -158,7 +158,7 @@ const Pill = createReactClass({ componentWillMount() { this._unmounted = false; this._matrixClient = MatrixClientPeg.get(); - this.componentWillReceiveProps(this.props); + this.componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves. }, componentWillUnmount() { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index f81a5630a4..56c7476060 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -71,7 +71,7 @@ export default createReactClass({ MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); } WidgetEchoStore.removeListener('update', this._updateApps); - dis.unregister(this.dispatcherRef); + if (this.dispatcherRef) dis.unregister(this.dispatcherRef); }, componentWillReceiveProps(newProps) { diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 50521cdd37..4fe1e0bc54 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -61,7 +61,6 @@ export default class RoomRecoveryReminder extends React.PureComponent { loading: false, error: e, }); - return; } } diff --git a/test/components/views/rooms/RoomSettings-test.js b/test/components/views/rooms/RoomSettings-test.js index 870d7f0aab..5e21f729d0 100644 --- a/test/components/views/rooms/RoomSettings-test.js +++ b/test/components/views/rooms/RoomSettings-test.js @@ -134,7 +134,7 @@ describe.skip('RoomSettings', () => { }); }); - // XXX: Can't test this because we `getRoomDirectoryVisibility` in `componentWillMount` + // XXX: Can't test this because we `getRoomDirectoryVisibility` in `componentDidMount` xit('should set room directory publicity when set to true', (done) => { const isRoomPublished = true; roomSettings.setState({ From 3f99332f4b985fbd23a437e2ac06e23d8d96d5d7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 31 Mar 2020 14:14:17 -0600 Subject: [PATCH 12/40] Use componentDidMount in place of componentWillMount where possible This fixes a common React warning we see. Most of these components should be using constructors instead, however componentDidMount is just as good (and doesn't require converting most of these). Conversion to classes will be done in a later stage of React warning fixes. For https://github.com/vector-im/riot-web/issues/12877 --- src/AsyncWrapper.js | 2 +- .../views/dialogs/EncryptedEventDialog.js | 2 +- .../views/dialogs/eventindex/ManageEventIndexDialog.js | 2 +- src/components/structures/CustomRoomTagPanel.js | 2 +- src/components/structures/EmbeddedPage.js | 2 +- src/components/structures/GroupView.js | 2 +- src/components/structures/LeftPanel.js | 2 +- src/components/structures/MatrixChat.js | 4 +--- src/components/structures/MyGroups.js | 2 +- src/components/structures/RightPanel.js | 2 +- src/components/structures/RoomDirectory.js | 4 +--- src/components/structures/RoomStatusBar.js | 2 +- src/components/structures/TagPanel.js | 2 +- src/components/structures/UserView.js | 2 +- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 2 +- src/components/structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 2 +- src/components/views/auth/CountryDropdown.js | 2 +- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- src/components/views/avatars/MemberStatusMessageAvatar.js | 2 +- src/components/views/context_menus/MessageContextMenu.js | 2 +- src/components/views/context_menus/RoomTileContextMenu.js | 2 +- .../views/context_menus/StatusMessageContextMenu.js | 2 +- src/components/views/dialogs/ConfirmUserActionDialog.js | 2 +- src/components/views/dialogs/RoomSettingsDialog.js | 2 +- src/components/views/dialogs/RoomUpgradeDialog.js | 2 +- src/components/views/dialogs/SetPasswordDialog.js | 4 ++-- src/components/views/dialogs/ShareDialog.js | 2 +- src/components/views/dialogs/UnknownDeviceDialog.js | 2 +- src/components/views/elements/AppTile.js | 4 +--- src/components/views/elements/DeviceVerifyButtons.js | 2 +- src/components/views/elements/EditableTextContainer.js | 2 +- src/components/views/elements/LanguageDropdown.js | 2 +- src/components/views/elements/PersistentApp.js | 2 +- src/components/views/elements/Pill.js | 2 +- src/components/views/elements/PowerSelector.js | 2 +- src/components/views/elements/ReplyThread.js | 2 +- src/components/views/elements/TintableSvg.js | 6 ++---- src/components/views/groups/GroupMemberInfo.js | 2 +- src/components/views/groups/GroupMemberList.js | 2 +- src/components/views/groups/GroupPublicityToggle.js | 2 +- src/components/views/groups/GroupRoomInfo.js | 2 +- src/components/views/groups/GroupRoomList.js | 2 +- src/components/views/groups/GroupTile.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MImageBody.js | 8 +++----- src/components/views/messages/SenderProfile.js | 2 +- src/components/views/right_panel/HeaderButtons.js | 2 +- src/components/views/rooms/AppsDrawer.js | 5 +---- src/components/views/rooms/EditMessageComposer.js | 2 +- src/components/views/rooms/ForwardMessage.js | 4 +--- src/components/views/rooms/MemberInfo.js | 4 +--- src/components/views/rooms/MemberList.js | 2 +- src/components/views/rooms/RoomBreadcrumbs.js | 2 +- src/components/views/rooms/RoomNameEditor.js | 2 +- src/components/views/rooms/RoomPreviewBar.js | 2 +- src/components/views/rooms/RoomRecoveryReminder.js | 2 +- src/components/views/rooms/RoomTopicEditor.js | 2 +- src/components/views/rooms/RoomUpgradeWarningBar.js | 2 +- src/components/views/rooms/ThirdPartyMemberInfo.js | 2 +- src/components/views/rooms/WhoIsTypingTile.js | 2 +- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 2 +- src/components/views/settings/EventIndexPanel.js | 2 +- src/components/views/settings/KeyBackupPanel.js | 2 +- src/components/views/settings/Notifications.js | 2 +- .../views/settings/tabs/user/HelpUserSettingsTab.js | 2 +- .../settings/tabs/user/PreferencesUserSettingsTab.js | 2 +- src/components/views/voip/CallPreview.js | 2 +- 70 files changed, 74 insertions(+), 91 deletions(-) diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js index b7b81688e1..05054cf63a 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.js @@ -38,7 +38,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index b602cf60fe..1aed623010 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -37,7 +37,7 @@ export default createReactClass({ return { device: null }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; const client = MatrixClientPeg.get(); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index 3d7249b5a1..ef4f2672d4 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -82,7 +82,7 @@ export default class ManageEventIndexDialog extends React.Component { } } - async componentWillMount(): void { + async componentDidMount(): void { let eventIndexSize = 0; let crawlingRoomsCount = 0; let roomCount = 0; diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index e8ff6e814e..6e392ea505 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component { }; } - componentWillMount() { + componentDidMount() { this._tagStoreToken = CustomRoomTagStore.addListener(() => { this.setState({tags: CustomRoomTagStore.getSortedTags()}); }); diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index a0a95ac6f1..0aababf030 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -58,7 +58,7 @@ export default class EmbeddedPage extends React.PureComponent { return sanitizeHtml(_t(s)); } - componentWillMount() { + componentDidMount() { this._unmounted = false; if (!this.props.url) { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 524694fe95..843f1401c4 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -428,7 +428,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._matrixClient = MatrixClientPeg.get(); this._matrixClient.on("Group.myMembership", this._onGroupMyMembership); diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f5e0bca67e..bbeaeb10e0 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -44,7 +44,7 @@ const LeftPanel = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.focusedElement = null; this._breadcrumbsWatcherRef = SettingsStore.watchSetting( diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 258972d18d..b0d08b7673 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -221,7 +221,7 @@ export default createReactClass({ return {serverConfig: props}; }, - componentWillMount: function() { + componentDidMount: function() { SdkConfig.put(this.props.config); // Used by _viewRoom before getting state from sync @@ -261,9 +261,7 @@ export default createReactClass({ this._accountPassword = null; this._accountPasswordTimer = null; - }, - componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); this._themeWatcher = new ThemeWatcher(); this._themeWatcher.start(); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index f1209b7b9e..f179cab6ad 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -38,7 +38,7 @@ export default createReactClass({ contextType: MatrixClientContext, }, - componentWillMount: function() { + componentDidMount: function() { this._fetch(); }, diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 8d25116827..5e556419cc 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -108,7 +108,7 @@ export default class RightPanel extends React.Component { } } - componentWillMount() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 664aaaf21f..eabf097950 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -56,7 +56,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this.nextBatch = null; this.filterTimeout = null; @@ -89,9 +89,7 @@ export default createReactClass({ ), }); }); - }, - componentDidMount: function() { this.refreshRoomList(); }, diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 13b73ec02b..639f38a119 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -96,7 +96,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index f1a39d6fcf..6642cce098 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -44,7 +44,7 @@ const TagPanel = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.unmounted = false; this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("sync", this._onClientSync); diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index 94159a1da4..c4fba137cc 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -35,7 +35,7 @@ export default class UserView extends React.Component { this.state = {}; } - componentWillMount() { + componentDidMount() { if (this.props.userId) { this._loadProfileInfo(); } diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index c849edf260..dc3f4a0a2b 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -69,7 +69,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.reset = null; this._checkServerLiveliness(this.props.serverConfig); }, diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index bfabc34a62..817472552a 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -113,7 +113,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; // map from login step type to a function which will render a control diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 8eef8dce11..687ab9a195 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -37,7 +37,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { // There is some assymetry between ChangeDisplayName and ChangeAvatar, // as ChangeDisplayName will auto-get the name but ChangeAvatar expects // the URL to be passed to you (because it's also used for room avatars). diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 7c6a3ea56f..45d775059c 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -120,7 +120,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._replaceClient(); }, diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index 63dc9d1ada..14a974668b 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -60,7 +60,7 @@ export default class CountryDropdown extends React.Component { }; } - componentWillMount() { + componentDidMount() { if (!this.props.value) { // If no value is given, we start with the default // country selected, but our parent component diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index e731b4cc01..04f7dc109b 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -429,7 +429,7 @@ export const MsisdnAuthEntry = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._submitUrl = null; this._sid = null; this._msisdn = null; diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 54f11e8e91..eef3f86d9a 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component { this._button = createRef(); } - componentWillMount() { + componentDidMount() { if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 4fc6dd58cc..452489aa65 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -61,7 +61,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); this._checkPermissions(); }, diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 2d8dec29c7..d281656bbe 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -82,7 +82,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; }, diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index d5cba45956..5e6f06dd5d 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component { }; } - componentWillMount() { + componentDidMount() { const { user } = this.props; if (!user) { return; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 14910fbf6d..d7b86d43d5 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -55,7 +55,7 @@ export default createReactClass({ askReason: false, }), - componentWillMount: function() { + componentDidMount: function() { this._reasonField = null; }, diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 76faf60eef..96523d1e94 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -36,7 +36,7 @@ export default class RoomSettingsDialog extends React.Component { onFinished: PropTypes.func.isRequired, }; - componentWillMount() { + componentDidMount() { this._dispatcherRef = dis.register(this._onAction); } diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index dc734718d5..c45d82303b 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -30,7 +30,7 @@ export default createReactClass({ onFinished: PropTypes.func.isRequired, }, - componentWillMount: async function() { + componentDidMount: async function() { const recommended = await this.props.room.getRecommendedVersion(); this._targetVersion = recommended.version; this.setState({busy: false}); diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index c48690bb48..fcc6e67656 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -75,8 +75,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { - console.info('SetPasswordDialog component will mount'); + componentDidMount: function() { + console.info('SetPasswordDialog component did mount'); }, _onPasswordChanged: function(res) { diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js index b42a88ceac..cf4735f608 100644 --- a/src/components/views/dialogs/ShareDialog.js +++ b/src/components/views/dialogs/ShareDialog.js @@ -121,7 +121,7 @@ export default class ShareDialog extends React.Component { }); } - componentWillMount() { + componentDidMount() { if (this.props.target instanceof Room) { const permalinkCreator = new RoomPermalinkCreator(this.props.target); permalinkCreator.load(); diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 69ebb72a6f..4cad13b047 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -87,7 +87,7 @@ export default createReactClass({ onSend: PropTypes.func.isRequired, }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged); }, diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 0a8bf7443b..539eba489c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -147,14 +147,12 @@ export default class AppTile extends React.Component { return false; } - componentWillMount() { + componentDidMount() { // Only fetch IM token on mount if we're showing and have permission to load if (this.props.show && this.state.hasPermissionToLoad) { this.setScalarToken(); } - } - componentDidMount() { // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index b4d78bb9bc..7328d50328 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -38,7 +38,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { const cli = MatrixClientPeg.get(); cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); }, diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 57e1b3d2cd..bbc5560557 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -42,7 +42,7 @@ export default class EditableTextContainer extends React.Component { this._onValueChanged = this._onValueChanged.bind(this); } - componentWillMount() { + componentDidMount() { if (this.props.getInitialValue === undefined) { // use whatever was given in the initialValue property. return; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index cb4e2e4da6..465176778c 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -40,7 +40,7 @@ export default class LanguageDropdown extends React.Component { }; } - componentWillMount() { + componentDidMount() { languageHandler.getAllLanguagesFromJson().then((langs) => { langs.sort(function(a, b) { if (a.label < b.label) return -1; diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index a807ed3b93..ee7bc46e74 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -33,7 +33,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate); }, diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 5f143a06a6..8eda4b30b5 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -155,7 +155,7 @@ const Pill = createReactClass({ this.setState({resourceId, pillType, member, group, room}); }, - componentWillMount() { + componentDidMount() { this._unmounted = false; this._matrixClient = MatrixClientPeg.get(); this.componentWillReceiveProps(this.props); diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index eff14979a9..8d17c2be57 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -62,7 +62,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._initStateFromProps(this.props); }, diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 0e1f230e57..eae2d13f8a 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -184,7 +184,7 @@ export default class ReplyThread extends React.Component { ref={ref} permalinkCreator={permalinkCreator} />; } - componentWillMount() { + componentDidMount() { this.unmounted = false; this.room = this.context.getRoom(this.props.parentEv.getRoomId()); this.room.on("Room.redaction", this.onRoomRedaction); diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index 3e0e41f411..66625c7b87 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -36,11 +36,9 @@ const TintableSvg = createReactClass({ idSequence: 0, }, - componentWillMount: function() { - this.fixups = []; - }, - componentDidMount: function() { + this.fixups = []; + this.id = TintableSvg.idSequence++; TintableSvg.mounts[this.id] = this; }, diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index f70c769ad7..95517ef548 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -49,7 +49,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._initGroupStore(this.props.groupId); }, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 2853e70afa..ca374d1309 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -47,7 +47,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._initGroupStore(this.props.groupId); }, diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index 602f9036d8..81f0f469ef 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -36,7 +36,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._initGroupStore(this.props.groupId); }, diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index 91d84be4d1..789396e719 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -47,7 +47,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._initGroupStore(this.props.groupId); }, diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index dee304e1f6..5c3e1587db 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -39,7 +39,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._initGroupStore(this.props.groupId); }, diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 12e2427e69..b845a83c2a 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -55,7 +55,7 @@ const GroupTile = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => { this.setState({profile}); }).catch((err) => { diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index a65d23bed7..8f57eccd05 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -34,7 +34,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.context.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 64f2611caf..ad238a728e 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -67,11 +67,6 @@ export default class MImageBody extends React.Component { this._image = createRef(); } - componentWillMount() { - this.unmounted = false; - this.context.on('sync', this.onClientSync); - } - // FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too! onClientSync(syncState, prevState) { if (this.unmounted) return; @@ -258,6 +253,9 @@ export default class MImageBody extends React.Component { } componentDidMount() { + this.unmounted = false; + this.context.on('sync', this.onClientSync); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index f7da1029e1..bed93b68c3 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -42,7 +42,7 @@ export default createReactClass({ }; }, - componentWillMount() { + componentDidMount() { this.unmounted = false; this._updateRelatedGroups(); diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index dbcae4529a..03b03218ee 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -40,7 +40,7 @@ export default class HeaderButtons extends React.Component { }; } - componentWillMount() { + componentDidMount() { this._storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this)); this._dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses } diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index f81a5630a4..508b149322 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -55,13 +55,10 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { ScalarMessaging.startListening(); MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); WidgetEchoStore.on('update', this._updateApps); - }, - - componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); }, diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 1d01f68551..d6ea0ec5f0 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -223,7 +223,7 @@ export default class EditMessageComposer extends React.Component { this.props.editState.setEditorState(caret, parts); } - componentWillMount() { + componentDidMount() { this._createEditorModel(); } diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index 78466f7cb5..a3c00598a7 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -30,14 +30,12 @@ export default createReactClass({ onCancelClick: PropTypes.func.isRequired, }, - componentWillMount: function() { + componentDidMount: function() { dis.dispatch({ action: 'panel_disable', middleDisabled: true, }); - }, - componentDidMount: function() { document.addEventListener('keydown', this._onKeyDown); }, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 7c187bd17a..f83e183b85 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -79,7 +79,7 @@ export default createReactClass({ contextType: MatrixClientContext, }, - componentWillMount: function() { + componentDidMount: function() { this._cancelDeviceList = null; const cli = this.context; @@ -98,9 +98,7 @@ export default createReactClass({ cli.on("accountData", this.onAccountData); this._checkIgnoreState(); - }, - componentDidMount: function() { this._updateStateForNewMember(this.props.member); }, diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index edd2f52496..dfa7e856b4 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -49,7 +49,7 @@ export default createReactClass({ } }, - componentWillMount: function() { + componentDidMount: function() { this._mounted = true; const cli = MatrixClientPeg.get(); if (cli.hasLazyLoadMembersEnabled()) { diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index f9408d3259..ad8a5cfef9 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -49,7 +49,7 @@ export default class RoomBreadcrumbs extends React.Component { this._scroller = createRef(); } - componentWillMount() { + componentDidMount() { this._dispatcherRef = dis.register(this.onAction); const storedRooms = SettingsStore.getValue("breadcrumb_rooms"); diff --git a/src/components/views/rooms/RoomNameEditor.js b/src/components/views/rooms/RoomNameEditor.js index b65d89ee6f..ab355b1f9e 100644 --- a/src/components/views/rooms/RoomNameEditor.js +++ b/src/components/views/rooms/RoomNameEditor.js @@ -34,7 +34,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { const room = this.props.room; const name = room.currentState.getStateEvents('m.room.name', ''); const myId = MatrixClientPeg.get().credentials.userId; diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 4ff5dd5198..85c41f30ec 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -97,7 +97,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._checkInvitedEmail(); }, diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 50521cdd37..3678193af2 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -44,7 +44,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { }; } - componentWillMount() { + componentDidMount() { this._loadBackupStatus(); } diff --git a/src/components/views/rooms/RoomTopicEditor.js b/src/components/views/rooms/RoomTopicEditor.js index 5decce02a9..0adc0ff1d6 100644 --- a/src/components/views/rooms/RoomTopicEditor.js +++ b/src/components/views/rooms/RoomTopicEditor.js @@ -33,7 +33,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { const room = this.props.room; const topic = room.currentState.getStateEvents('m.room.topic', ''); this.setState({ diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index 1240900e28..7d5bc89034 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -31,7 +31,7 @@ export default createReactClass({ recommendation: PropTypes.object.isRequired, }, - componentWillMount: function() { + componentDidMount: function() { const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", ""); this.setState({upgraded: tombstone && tombstone.getContent().replacement_room}); diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js index f8d9069ca6..3e6ed16aa4 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.js +++ b/src/components/views/rooms/ThirdPartyMemberInfo.js @@ -51,7 +51,7 @@ export default class ThirdPartyMemberInfo extends React.Component { }; } - componentWillMount(): void { + componentDidMount(): void { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); } diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index 9683d00e51..b56a75582f 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -54,7 +54,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); }, diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 5b6b6ae3de..76644d38bc 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -55,7 +55,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); }, diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 6607458b40..7c88573e9c 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -78,7 +78,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._sessionStore = sessionStore; this._sessionStoreToken = this._sessionStore.addListener( this._setStateFromSessionStore, diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index 203a7ee46e..c9c6a5ec4f 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -63,7 +63,7 @@ export default class EventIndexPanel extends React.Component { } } - async componentWillMount(): void { + async componentDidMount(): void { this.updateState(); } diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 27fdb2cb56..9d60ed1188 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -38,7 +38,7 @@ export default class KeyBackupPanel extends React.PureComponent { }; } - componentWillMount() { + componentDidMount() { this._checkKeyBackupStatus(); MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 1f4bde6eab..a3173f18bb 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -87,7 +87,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._refreshFromServer(); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 9a2db8113e..146d841d58 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -40,7 +40,7 @@ export default class HelpUserSettingsTab extends React.Component { }; } - componentWillMount(): void { + componentDidMount(): void { PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => { console.error("Error getting vector version: ", e); }); diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index bdcb25dd51..bdb2a9ffc4 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -81,7 +81,7 @@ export default class PreferencesUserSettingsTab extends React.Component { }; } - async componentWillMount(): void { + async componentDidMount(): void { const platform = PlatformPeg.get(); const autoLaunchSupported = await platform.supportsAutoLaunch(); diff --git a/src/components/views/voip/CallPreview.js b/src/components/views/voip/CallPreview.js index 57bf35a719..049dd8a3c6 100644 --- a/src/components/views/voip/CallPreview.js +++ b/src/components/views/voip/CallPreview.js @@ -40,7 +40,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this.dispatcherRef = dis.register(this._onAction); }, From 538147f7fa492e0a21262c6b5d97b7e65da9f4af Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Apr 2020 10:00:33 +0100 Subject: [PATCH 13/40] Better support for widgets overriding their URLs Move the URL processing into AppTile so that the widget can have a URL used for embedding in the page and a separate one for popping out into a browser. --- src/components/views/elements/AppTile.js | 172 ++++++++++++------ .../views/elements/PersistentApp.js | 8 +- src/components/views/rooms/AppsDrawer.js | 6 +- src/components/views/rooms/Stickerpicker.js | 13 +- src/utils/AutoDiscoveryUtils.js | 9 +- src/utils/WidgetUtils.js | 58 ------ 6 files changed, 135 insertions(+), 131 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 0a8bf7443b..1eb4e5bf90 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -2,6 +2,7 @@ Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -41,12 +42,30 @@ import PersistedElement from "./PersistedElement"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; +/** + * Does template substitution on a URL (or any string). Variables will be + * passed through encodeURIComponent. + * @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'. + * @param {Object} variables The key/value pairs to replace the template + * variables with. E.g. { '$bar': 'baz' }. + * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + */ +function uriFromTemplate(uriTemplate, variables) { + let out = uriTemplate; + for (const [key, val] of Object.entries(variables)) { + out = out.replace( + '$' + key, encodeURIComponent(val), + ); + } + return out; +} + export default class AppTile extends React.Component { constructor(props) { super(props); // The key used for PersistedElement - this._persistKey = 'widget_' + this.props.id; + this._persistKey = 'widget_' + this.props.app.id; this.state = this._getNewState(props); @@ -78,7 +97,7 @@ export default class AppTile extends React.Component { // This is a function to make the impact of calling SettingsStore slightly less const hasPermissionToLoad = () => { const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); - return !!currentlyAllowedWidgets[newProps.eventId]; + return !!currentlyAllowedWidgets[newProps.app.eventId]; }; const PersistedElement = sdk.getComponent("elements.PersistedElement"); @@ -86,7 +105,7 @@ export default class AppTile extends React.Component { initialising: true, // True while we are mangling the widget URL // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), - widgetUrl: this._addWurlParams(newProps.url), + widgetUrl: this._addWurlParams(newProps.app.url), // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), @@ -103,7 +122,7 @@ export default class AppTile extends React.Component { * @return {Boolean} True if capability supported */ _hasCapability(capability) { - return ActiveWidgetStore.widgetHasCapability(this.props.id, capability); + return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability); } /** @@ -125,7 +144,7 @@ export default class AppTile extends React.Component { const params = qs.parse(u.query); // Append widget ID to query parameters - params.widgetId = this.props.id; + params.widgetId = this.props.app.id; // Append current / parent URL, minus the hash because that will change when // we view a different room (ie. may change for persistent widgets) params.parentUrl = window.location.href.split('#', 2)[0]; @@ -137,11 +156,11 @@ export default class AppTile extends React.Component { isMixedContent() { const parentContentProtocol = window.location.protocol; - const u = url.parse(this.props.url); + const u = url.parse(this.props.app.url); const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", - parentContentProtocol, childContentProtocol, window.location, this.props.url); + parentContentProtocol, childContentProtocol, window.location, this.props.app.url); return true; } return false; @@ -164,8 +183,8 @@ export default class AppTile extends React.Component { dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container - if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { - ActiveWidgetStore.destroyPersistentWidget(this.props.id); + if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); } @@ -176,11 +195,11 @@ export default class AppTile extends React.Component { * Component initialisation is only complete when this function has resolved */ setScalarToken() { - if (!WidgetUtils.isScalarUrl(this.props.url)) { + if (!WidgetUtils.isScalarUrl(this.props.app.url)) { console.warn('Non-scalar widget, not setting scalar token!', url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -191,7 +210,7 @@ export default class AppTile extends React.Component { console.warn("No integration manager - not setting scalar token", url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -204,7 +223,7 @@ export default class AppTile extends React.Component { console.warn('Non-scalar manager, not setting scalar token!', url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -217,7 +236,7 @@ export default class AppTile extends React.Component { this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; - const u = url.parse(this._addWurlParams(this.props.url)); + const u = url.parse(this._addWurlParams(this.props.app.url)); const params = qs.parse(u.query); if (!params.scalar_token) { params.scalar_token = encodeURIComponent(token); @@ -246,7 +265,7 @@ export default class AppTile extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.url !== this.props.url) { + if (nextProps.app.url !== this.props.app.url) { this._getNewState(nextProps); // Fetch IM token for new URL if we're showing and have permission to load if (this.props.show && this.state.hasPermissionToLoad) { @@ -280,7 +299,7 @@ export default class AppTile extends React.Component { } _onEditClick() { - console.log("Edit widget ID ", this.props.id); + console.log("Edit widget ID ", this.props.app.id); if (this.props.onEditClick) { this.props.onEditClick(); } else { @@ -289,13 +308,13 @@ export default class AppTile extends React.Component { IntegrationManagers.sharedInstance().openAll( this.props.room, 'type_' + this.props.type, - this.props.id, + this.props.app.id, ); } else { IntegrationManagers.sharedInstance().getPrimaryManager().open( this.props.room, 'type_' + this.props.type, - this.props.id, + this.props.app.id, ); } } @@ -303,7 +322,7 @@ export default class AppTile extends React.Component { _onSnapshotClick() { console.warn("Requesting widget snapshot"); - ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot() + ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot() .catch((err) => { console.error("Failed to get screenshot", err); }) @@ -351,7 +370,7 @@ export default class AppTile extends React.Component { WidgetUtils.setRoomWidget( this.props.room.roomId, - this.props.id, + this.props.app.id, ).catch((e) => { console.error('Failed to delete widget', e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -369,7 +388,7 @@ export default class AppTile extends React.Component { } _onRevokeClicked() { - console.info("Revoke widget permissions - %s", this.props.id); + console.info("Revoke widget permissions - %s", this.props.app.id); this._revokeWidgetPermission(); } @@ -380,10 +399,10 @@ export default class AppTile extends React.Component { // Destroy the old widget messaging before starting it back up again. Some widgets // have startup routines that run when they are loaded, so we just need to reinitialize // the messaging for them. - ActiveWidgetStore.delWidgetMessaging(this.props.id); + ActiveWidgetStore.delWidgetMessaging(this.props.app.id); this._setupWidgetMessaging(); - ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId); + ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId); this.setState({loading: false}); } @@ -391,10 +410,10 @@ export default class AppTile extends React.Component { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. const widgetMessaging = new WidgetMessaging( - this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow); - ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); + this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow); + ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { - console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); + console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities); requestedCapabilities = requestedCapabilities || []; // Allow whitelisted capabilities @@ -406,7 +425,7 @@ export default class AppTile extends React.Component { }, this.props.whitelistCapabilities); if (requestedWhitelistCapabilies.length > 0 ) { - console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` + + console.warn(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` + requestedWhitelistCapabilies, ); } @@ -414,7 +433,7 @@ export default class AppTile extends React.Component { // TODO -- Add UI to warn about and optionally allow requested capabilities - ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies); + ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies); if (this.props.onCapabilityRequest) { this.props.onCapabilityRequest(requestedCapabilities); @@ -422,16 +441,16 @@ export default class AppTile extends React.Component { // We only tell Jitsi widgets that we're ready because they're realistically the only ones // using this custom extension to the widget API. - if (this.props.type === 'jitsi') { + if (this.props.app.type === 'jitsi') { widgetMessaging.flagReadyToContinue(); } }).catch((err) => { - console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err); + console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err); }); } _onAction(payload) { - if (payload.widgetId === this.props.id) { + if (payload.widgetId === this.props.app.id) { switch (payload.action) { case 'm.sticker': if (this._hasCapability('m.sticker')) { @@ -460,9 +479,9 @@ export default class AppTile extends React.Component { _grantWidgetPermission() { const roomId = this.props.room.roomId; - console.info("Granting permission for widget to load: " + this.props.eventId); + console.info("Granting permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); - current[this.props.eventId] = true; + current[this.props.app.eventId] = true; SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { this.setState({hasPermissionToLoad: true}); @@ -476,14 +495,14 @@ export default class AppTile extends React.Component { _revokeWidgetPermission() { const roomId = this.props.room.roomId; - console.info("Revoking permission for widget to load: " + this.props.eventId); + console.info("Revoking permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); - current[this.props.eventId] = false; + current[this.props.app.eventId] = false; SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { this.setState({hasPermissionToLoad: false}); // Force the widget to be non-persistent (able to be deleted/forgotten) - ActiveWidgetStore.destroyPersistentWidget(this.props.id); + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); }).catch(err => { @@ -494,8 +513,8 @@ export default class AppTile extends React.Component { formatAppTileName() { let appTileName = "No name"; - if (this.props.name && this.props.name.trim()) { - appTileName = this.props.name.trim(); + if (this.props.app.name && this.props.app.name.trim()) { + appTileName = this.props.app.name.trim(); } return appTileName; } @@ -519,6 +538,60 @@ export default class AppTile extends React.Component { } } + /** + * Replace the widget template variables in a url with their values + * + * @returns {string} url with temlate variables replaced + */ + _templatedUrl(u) { + const myUserId = MatrixClientPeg.get().credentials.userId; + const myUser = MatrixClientPeg.get().getUser(myUserId); + const vars = Object.assign({ + domain: "jitsi.riot.im", // v1 widgets have this hardcoded + }, this.props.app.data, { + 'matrix_user_id': myUserId, + 'matrix_room_id': this.props.room.roomId, + 'matrix_display_name': myUser ? myUser.displayName : myUserId, + 'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '', + + // TODO: Namespace themes through some standard + 'theme': SettingsStore.getValue("theme"), + }); + + if (vars.conferenceId === undefined) { + // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets + const parsedUrl = new URL(this.props.app.url); + vars.conferenceId = parsedUrl.searchParams.get("confId"); + } + + return uriFromTemplate(u, vars); + } + + /** + * Get the URL used in the iframe + * In cases where we supply our own UI for a widget, this is an internal + * URL different to the one used if the widget is popped out to a separate + * tab / browser + * + * @returns {string} url + */ + _getRenderedUrl() { + let url; + + if (this.props.app.type === 'jitsi') { + console.log("Replacing Jitsi widget URL with local wrapper"); + url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); + url = this._addWurlParams(url); + } else { + url = this._getSafeUrl(); + } + return this._templatedUrl(url); + } + + _getPopoutUrl() { + return this._templatedUrl(this._getSafeUrl()); + } + _getSafeUrl() { const parsedWidgetUrl = url.parse(this.state.widgetUrl, true); if (ENABLE_REACT_PERF) { @@ -526,13 +599,7 @@ export default class AppTile extends React.Component { parsedWidgetUrl.query.react_perf = true; } let safeWidgetUrl = ''; - if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || ( - // Check if the widget URL is a Jitsi widget in Electron - parsedWidgetUrl.protocol === 'vector:' - && parsedWidgetUrl.host === 'vector' - && parsedWidgetUrl.pathname === '/webapp/jitsi.html' - && this.props.type === 'jitsi' - )) { + if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) { safeWidgetUrl = url.format(parsedWidgetUrl); } return safeWidgetUrl; @@ -562,9 +629,9 @@ export default class AppTile extends React.Component { _onPopoutWidgetClick() { // Using Object.assign workaround as the following opens in a new window instead of a new tab. - // window.open(this._getSafeUrl(), '_blank', 'noopener=yes'); + // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), - { target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click(); + { target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click(); } _onReloadWidgetClick() { @@ -641,7 +708,7 @@ export default class AppTile extends React.Component {