diff --git a/.stylelintrc.js b/.stylelintrc.js index 97e1ec8023..f028c76cc0 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -15,6 +15,9 @@ module.exports = { "number-leading-zero": null, "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true, + "scss/at-rule-no-unknown": [true, { + // https://github.com/vector-im/riot-web/issues/10544 + "ignoreAtRules": ["define-mixin"], + }], } } diff --git a/package.json b/package.json index 8e1a1fa668..ffd701a233 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "karma-summary-reporter": "^1.5.1", "karma-webpack": "^4.0.0-beta.0", "matrix-mock-request": "^1.2.3", - "matrix-react-test-utils": "^0.1.1", + "matrix-react-test-utils": "^0.2.2", "mocha": "^5.0.5", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", diff --git a/res/css/_common.scss b/res/css/_common.scss index 517ced43fb..1b7c8ec938 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -559,3 +559,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Username_color8 { color: $username-variant8-color; } + +@define-mixin mx_Settings_fullWidthField { + margin-right: 200px; +} diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 3e97a0ff6d..afac75986f 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -26,6 +26,10 @@ limitations under the License. height: 4em; } +.mx_ProfileSettings_controls .mx_Field { + margin-right: 100px; +} + .mx_ProfileSettings_controls .mx_Field:first-child { margin-top: 0; } diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss index c6fcfc8af5..55ad6eef02 100644 --- a/res/css/views/settings/_SetIdServer.scss +++ b/res/css/views/settings/_SetIdServer.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,5 +15,5 @@ limitations under the License. */ .mx_SetIdServer .mx_Field_input { - width: 300px; + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 091c98ffb8..16467165cf 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -16,15 +16,15 @@ limitations under the License. .mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_themeSection .mx_Field { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child { margin-top: 0; } -.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, -.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, +.mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, +.mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index b3430f47af..d003e175d9 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_PreferencesUserSettingsTab .mx_Field { - margin-right: 100px; // Align with the rest of the controls + @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index 36c8cfd896..69d57bdba1 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_VoiceUserSettingsTab .mx_Field { - margin-right: 100px; // align with the rest of the fields + @mixin mx_Settings_fullWidthField; } .mx_VoiceUserSettingsTab_missingMediaPermissions { diff --git a/src/CallHandler.js b/src/CallHandler.js index 5b58400ae6..40a8d426f8 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -63,7 +63,7 @@ import SdkConfig from './SdkConfig'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import ScalarAuthClient from './ScalarAuthClient'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; global.mxCalls = { //room_id: MatrixCall @@ -348,14 +348,20 @@ async function _startCallApp(roomId, type) { // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. - const scalarClient = new ScalarAuthClient(); - let haveScalar = false; - try { - await scalarClient.connect(); - haveScalar = scalarClient.hasCredentials(); - } catch (e) { - // fall through + const managers = IntegrationManagers.sharedInstance(); + let haveScalar = true; + if (managers.hasManager()) { + try { + const scalarClient = managers.getPrimaryManager().getScalarClient(); + await scalarClient.connect(); + haveScalar = scalarClient.hasCredentials(); + } catch (e) { + // ignore + } + } else { + haveScalar = false; } + if (!haveScalar) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -421,7 +427,8 @@ async function _startCallApp(roomId, type) { // URL, but this will at least allow the integration manager to not be hardcoded. widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; } else { - widgetUrl = SdkConfig.get().integrations_rest_url + '/widgets/jitsi.html?' + queryString; + const apiUrl = IntegrationManagers.sharedInstance().getPrimaryManager().apiUrl; + widgetUrl = apiUrl + '/widgets/jitsi.html?' + queryString; } const widgetData = { widgetSessionId }; diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index d34e3d8ed0..b2bd579b74 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -22,7 +22,7 @@ import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import MatrixClientPeg from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; -import { showIntegrationsManager } from './integrations/integrations'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -193,11 +193,12 @@ export default class FromWidgetPostMessageApi { const integType = (data && data.integType) ? data.integType : null; const integId = (data && data.integId) ? data.integId : null; - showIntegrationsManager({ - room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - screen: 'type_' + integType, - integrationId: integId, - }); + // TODO: Open the right integration manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + `type_${integType}`, + integId, + ); } else if (action === 'set_always_on_screen') { // This is a new message: there is no reason to support the deprecated widgetData here const data = event.data.data; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0ddb7e9aae..c03a958840 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -35,6 +35,7 @@ import { sendLoginRequest } from "./Login"; import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -580,6 +581,7 @@ async function startMatrixClient(startSyncing=true) { Presence.start(); } DMRoomMap.makeShared().start(); + IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); if (startSyncing) { @@ -638,6 +640,7 @@ export function stopMatrixClient(unsetClient=true) { TypingStore.sharedInstance().reset(); Presence.stop(); ActiveWidgetStore.stop(); + IntegrationManagers.sharedInstance().stopWatching(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { diff --git a/src/Modal.js b/src/Modal.js index 96be445ab1..26c9da8bbb 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -274,7 +274,7 @@ class ModalManager { this._reRender(); return { close: closeDialog, - then: (resolve, reject) => onFinishedProm.then(resolve, reject), + finished: onFinishedProm, }; } @@ -285,7 +285,7 @@ class ModalManager { this._reRender(); return { close: closeDialog, - then: (resolve, reject) => onFinishedProm.then(resolve, reject), + finished: onFinishedProm, }; } diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index c268fbe3fb..3623d47f8e 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -29,20 +29,43 @@ import * as Matrix from 'matrix-js-sdk'; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; -class ScalarAuthClient { - constructor() { +export default class ScalarAuthClient { + constructor(apiUrl, uiUrl) { + this.apiUrl = apiUrl; + this.uiUrl = uiUrl; this.scalarToken = null; // `undefined` to allow `startTermsFlow` to fallback to a default // callback if this is unset. this.termsInteractionCallback = undefined; + + // We try and store the token on a per-manager basis, but need a fallback + // for the default manager. + const configApiUrl = SdkConfig.get()['integrations_rest_url']; + const configUiUrl = SdkConfig.get()['integrations_ui_url']; + this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - /** - * Determines if setting up a ScalarAuthClient is even possible - * @returns {boolean} true if possible, false otherwise. - */ - static isPossible() { - return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; + _writeTokenToStore() { + window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); + if (this.isDefaultManager) { + // We remove the old token from storage to migrate upwards. This is safe + // to do because even if the user switches to /app when this is on /develop + // they'll at worst register for a new token. + window.localStorage.removeItem("mx_scalar_token"); // no-op when not present + } + } + + _readTokenFromStore() { + let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); + if (!token && this.isDefaultManager) { + token = window.localStorage.getItem("mx_scalar_token"); + } + return token; + } + + _readToken() { + if (this.scalarToken) return this.scalarToken; + return this._readTokenFromStore(); } setTermsInteractionCallback(callback) { @@ -61,8 +84,7 @@ class ScalarAuthClient { // Returns a promise that resolves to a scalar_token string getScalarToken() { - let token = this.scalarToken; - if (!token) token = window.localStorage.getItem("mx_scalar_token"); + const token = this._readToken(); if (!token) { return this.registerForToken(); @@ -78,7 +100,7 @@ class ScalarAuthClient { } _getAccountName(token) { - const url = SdkConfig.get().integrations_rest_url + "/account"; + const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { request({ @@ -111,7 +133,7 @@ class ScalarAuthClient { return token; }).catch((e) => { if (e instanceof TermsNotSignedError) { - console.log("Integrations manager requires new terms to be agreed to"); + console.log("Integration manager requires new terms to be agreed to"); // The terms endpoints are new and so live on standard _matrix prefixes, // but IM rest urls are currently configured with paths, so remove the // path from the base URL before passing it to the js-sdk @@ -126,7 +148,7 @@ class ScalarAuthClient { // Once we've fully transitioned to _matrix URLs, we can give people // a grace period to update their configs, then use the rest url as // a regular base url. - const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url); + const parsedImRestUrl = url.parse(this.apiUrl); parsedImRestUrl.path = ''; parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( @@ -147,17 +169,18 @@ class ScalarAuthClient { return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(tokenObject); - }).then((tokenObject) => { + }).then((token) => { // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(tokenObject); - }).then((tokenObject) => { - window.localStorage.setItem("mx_scalar_token", tokenObject); - return tokenObject; + return this._checkToken(token); + }).then((token) => { + this.scalarToken = token; + this._writeTokenToStore(); + return token; }); } exchangeForScalarToken(openidTokenObject) { - const scalarRestUrl = SdkConfig.get().integrations_rest_url; + const scalarRestUrl = this.apiUrl; return new Promise(function(resolve, reject) { request({ @@ -181,7 +204,7 @@ class ScalarAuthClient { } getScalarPageTitle(url) { - let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; + let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); @@ -217,7 +240,7 @@ class ScalarAuthClient { * @return {Promise} Resolves on completion */ disableWidgetAssets(widgetType, widgetId) { - let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state'; + let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); return new Promise((resolve, reject) => { request({ @@ -246,7 +269,7 @@ class ScalarAuthClient { getScalarInterfaceUrlForRoom(room, screen, id) { const roomId = room.roomId; const roomName = room.name; - let url = SdkConfig.get().integrations_ui_url; + let url = this.uiUrl; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); url += "&room_name=" + encodeURIComponent(roomName); @@ -264,5 +287,3 @@ class ScalarAuthClient { return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } - -module.exports = ScalarAuthClient; diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 8b87650929..0d61755519 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -232,13 +232,13 @@ Example: } */ -import SdkConfig from './SdkConfig'; import MatrixClientPeg from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; import dis from './dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; +import {IntegrationManagers} from "./integrations/IntegrationManagers"; function sendResponse(event, res) { const data = JSON.parse(JSON.stringify(event.data)); @@ -548,7 +548,8 @@ const onMessage = function(event) { // (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) let configUrl; try { - configUrl = new URL(SdkConfig.get().integrations_ui_url); + // TODO: Support multiple integration managers + configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl); } catch (e) { // No integrations UI URL, ignore silently. return; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index deef8488f4..b8903076c7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -935,7 +935,7 @@ export default React.createClass({ const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); - const [shouldCreate, name, noFederate] = await modal; + const [shouldCreate, name, noFederate] = await modal.finished; if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index b5af58adf1..ff8d88f738 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -15,13 +15,13 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import * as ServerType from '../../views/auth/ServerTypeSelector'; +import ServerConfig from "./ServerConfig"; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; @@ -33,49 +33,8 @@ const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_ * This is a variant of ServerConfig with only the HS field and different body * text that is specific to the Modular case. */ -export default class ModularServerConfig extends React.PureComponent { - static propTypes = { - onServerConfigChange: PropTypes.func, - - // The current configuration that the user is expecting to change. - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - - delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - - // Called after the component calls onServerConfigChange - onAfterSubmit: PropTypes.func, - - // Optional text for the submit button. If falsey, no button will be shown. - submitText: PropTypes.string, - - // Optional class for the submit button. Only applies if the submit button - // is to be rendered. - submitClass: PropTypes.string, - }; - - static defaultProps = { - onServerConfigChange: function() {}, - customHsUrl: "", - delayTimeMs: 0, - }; - - constructor(props) { - super(props); - - this.state = { - busy: false, - errorText: "", - hsUrl: props.serverConfig.hsUrl, - isUrl: props.serverConfig.isUrl, - }; - } - - componentWillReceiveProps(newProps) { - if (newProps.serverConfig.hsUrl === this.state.hsUrl && - newProps.serverConfig.isUrl === this.state.isUrl) return; - - this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); - } +export default class ModularServerConfig extends ServerConfig { + static propTypes = ServerConfig.propTypes; async validateAndApplyServer(hsUrl, isUrl) { // Always try and use the defaults first @@ -120,35 +79,6 @@ export default class ModularServerConfig extends React.PureComponent { return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } - onHomeserverBlur = (ev) => { - this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.validateServer(); - }); - }; - - onHomeserverChange = (ev) => { - const hsUrl = ev.target.value; - this.setState({ hsUrl }); - }; - - onSubmit = async (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - const result = await this.validateServer(); - if (!result) return; // Do not continue. - - if (this.props.onAfterSubmit) { - this.props.onAfterSubmit(); - } - }; - - _waitThenInvoke(existingTimeoutId, fn) { - if (existingTimeoutId) { - clearTimeout(existingTimeoutId); - } - return setTimeout(fn.bind(this), this.props.delayTimeMs); - } - render() { const Field = sdk.getComponent('elements.Field'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index cd3dab12ac..f3b9640e16 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -69,10 +70,10 @@ module.exports = React.createClass({ fieldValid: {}, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, - username: "", - email: "", - phoneNumber: "", - password: "", + username: this.props.defaultUsername || "", + email: this.props.defaultEmail || "", + phoneNumber: this.props.defaultPhoneNumber || "", + password: this.props.defaultPassword || "", passwordConfirm: "", passwordComplexity: null, passwordSafe: false, @@ -90,7 +91,7 @@ module.exports = React.createClass({ } const self = this; - if (this.state.email == '') { + if (this.state.email === '') { const haveIs = Boolean(this.props.serverConfig.isUrl); let desc; @@ -455,7 +456,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_EMAIL] = field} type="text" label={emailPlaceholder} - defaultValue={this.props.defaultEmail} value={this.state.email} onChange={this.onEmailChange} onValidate={this.onEmailValidate} @@ -469,7 +469,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PASSWORD] = field} type="password" label={_t("Password")} - defaultValue={this.props.defaultPassword} value={this.state.password} onChange={this.onPasswordChange} onValidate={this.onPasswordValidate} @@ -483,7 +482,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PASSWORD_CONFIRM] = field} type="password" label={_t("Confirm")} - defaultValue={this.props.defaultPassword} value={this.state.passwordConfirm} onChange={this.onPasswordConfirmChange} onValidate={this.onPasswordConfirmValidate} @@ -512,7 +510,6 @@ module.exports = React.createClass({ ref={field => this[FIELD_PHONE_NUMBER] = field} type="text" label={phoneLabel} - defaultValue={this.props.defaultPhoneNumber} value={this.state.phoneNumber} prefix={phoneCountry} onChange={this.onPhoneNumberChange} @@ -528,7 +525,6 @@ module.exports = React.createClass({ type="text" autoFocus={true} label={_t("Username")} - defaultValue={this.props.defaultUsername} value={this.state.username} onChange={this.onUsernameChange} onValidate={this.onUsernameValidate} diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 80f5c43d0c..afc6faa18d 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +20,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; -import sdk from '../../../index'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; @@ -121,6 +121,10 @@ module.exports = React.createClass({ ); urls.push(defaultImageUrl); // lowest priority } + + // deduplicate URLs + urls = Array.from(new Set(urls)); + return { imageUrls: urls, defaultImageUrl: defaultImageUrl, diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 38830d78f2..82953cf52e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import AccessibleButton from './AccessibleButton'; import Modal from '../../../Modal'; @@ -35,7 +34,7 @@ import WidgetUtils from '../../../utils/WidgetUtils'; import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -178,9 +177,22 @@ export default class AppTile extends React.Component { return; } + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + console.warn("No integration manager - not setting scalar token", url); + this.setState({ + error: null, + widgetUrl: this._addWurlParams(this.props.url), + initialising: false, + }); + return; + } + + // TODO: Pick the right manager for the widget + // Fetch the token before loading the iframe as we need it to mangle the URL if (!this._scalarClient) { - this._scalarClient = new ScalarAuthClient(); + this._scalarClient = managers.getPrimaryManager().getScalarClient(); } this._scalarClient.getScalarToken().done((token) => { // Append scalar_token as a query param if not already present @@ -189,7 +201,7 @@ export default class AppTile extends React.Component { const params = qs.parse(u.query); if (!params.scalar_token) { params.scalar_token = encodeURIComponent(token); - // u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options + // u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options u.search = undefined; u.query = params; } @@ -251,11 +263,12 @@ export default class AppTile extends React.Component { if (this.props.onEditClick) { this.props.onEditClick(); } else { - showIntegrationsManager({ - room: this.props.room, - screen: 'type_' + this.props.type, - integrationId: this.props.id, - }); + // TODO: Open the right manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + this.props.type, + this.props.id, + ); } } diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 084ec1bd6a..8272b36639 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -48,7 +48,7 @@ export default class Field extends React.PureComponent { onValidate: PropTypes.func, // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltip: PropTypes.node, + tooltipContent: PropTypes.node, // All other props pass through to the . }; @@ -137,8 +137,7 @@ export default class Field extends React.PureComponent { }, VALIDATION_THROTTLE_MS); render() { - const { element, prefix, onValidate, children, ...inputProps } = this.props; - delete inputProps.tooltip; // needs to be removed from props but we don't need it here + const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props; const inputElement = element || "input"; @@ -170,11 +169,11 @@ export default class Field extends React.PureComponent { // Handle displaying feedback on validity const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; - if (this.props.tooltip || this.state.feedback) { + if (tooltipContent || this.state.feedback) { fieldTooltip = ; } diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index f5b6d75d6c..ca7391329f 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -18,9 +18,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import { _t } from '../../../languageHandler'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -30,12 +29,17 @@ export default class ManageIntegsButton extends React.Component { onManageIntegrations = (ev) => { ev.preventDefault(); - showIntegrationsManager({ room: this.props.room }); + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + managers.openNoManagerDialog(); + } else { + managers.getPrimaryManager().open(this.props.room); + } }; render() { let integrationsButton =
; - if (ScalarAuthClient.isPossible()) { + if (IntegrationManagers.sharedInstance().hasManager()) { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +18,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import { ContentRepo } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -31,12 +31,21 @@ module.exports = React.createClass({ mxEvent: PropTypes.object.isRequired, }, - onAvatarClick: function(name) { - const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url); + onAvatarClick: function() { + const cli = MatrixClientPeg.get(); + const ev = this.props.mxEvent; + const httpUrl = cli.mxcUrlToHttp(ev.getContent().url); + + const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { + senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(), + roomName: room ? room.name : '', + }); + const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, - name: name, + name: text, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); }, @@ -44,29 +53,22 @@ module.exports = React.createClass({ render: function() { const ev = this.props.mxEvent; const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { - senderDisplayName: senderDisplayName, - roomName: room ? room.name : '', - }); + const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { return (
- { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) } + { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
); } - const url = ContentRepo.getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - ev.getContent().url, - Math.ceil(14 * window.devicePixelRatio), - Math.ceil(14 * window.devicePixelRatio), - 'crop', - ); + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + // Provide all arguments to RoomAvatar via oobData because the avatar is historic + const oobData = { + avatarUrl: ev.getContent().url, + name: room ? room.name : "", + }; return (
@@ -75,8 +77,8 @@ module.exports = React.createClass({ { 'img': () => - + onClick={this.onAvatarClick}> + , }) } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index bfa4860160..492c95ba1b 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -25,7 +25,6 @@ import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; import {formatDate} from '../../../DateUtils'; import sdk from '../../../index'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from '../../../Modal'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; @@ -35,6 +34,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; import {host as matrixtoHost} from '../../../matrix-to'; import {pillifyLinks} from '../../../utils/pillify'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; module.exports = React.createClass({ displayName: 'TextualBody', @@ -318,12 +318,19 @@ module.exports = React.createClass({ // which requires the user to click through and THEN we can open the link in a new tab because // the window.open command occurs in the same stack frame as the onClick callback. + const managers = IntegrationManagers.sharedInstance(); + if (!managers.hasManager()) { + managers.openNoManagerDialog(); + return; + } + // Go fetch a scalar token - const scalarClient = new ScalarAuthClient(); + const integrationManager = managers.getPrimaryManager(); + const scalarClient = integrationManager.getScalarClient(); scalarClient.connect().then(() => { const completeUrl = scalarClient.getStarterLink(starterLink); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const integrationsUrl = SdkConfig.get().integrations_ui_url; + const integrationsUrl = integrationManager.uiUrl; Modal.createTrackedDialog('Add an integration', '', QuestionDialog, { title: _t("Add an Integration"), description: diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 2e9d3e5071..4d2c1e0380 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler'; import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import AccessibleButton from '../elements/AccessibleButton'; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; // The maximum number of widgets that can be added in a room const MAX_WIDGETS = 2; @@ -128,10 +128,7 @@ module.exports = React.createClass({ }, _launchManageIntegrations: function() { - showIntegrationsManager({ - room: this.props.room, - screen: 'add_integ', - }); + IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); }, onClickAddWidget: function(e) { diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 6c48351992..2d3508c404 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -18,13 +18,12 @@ import {_t, _td} from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; import WidgetUtils from '../../../utils/WidgetUtils'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import PersistedElement from "../elements/PersistedElement"; -import { showIntegrationsManager } from '../../../integrations/integrations'; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; const widgetType = 'm.stickerpicker'; @@ -67,8 +66,9 @@ export default class Stickerpicker extends React.Component { _acquireScalarClient() { if (this.scalarClient) return Promise.resolve(this.scalarClient); - if (ScalarAuthClient.isPossible()) { - this.scalarClient = new ScalarAuthClient(); + // TODO: Pick the right manager for the widget + if (IntegrationManagers.sharedInstance().hasManager()) { + this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient(); return this.scalarClient.connect().then(() => { this.forceUpdate(); return this.scalarClient; @@ -348,11 +348,12 @@ export default class Stickerpicker extends React.Component { * Launch the integrations manager on the stickers integration page */ _launchManageIntegrations() { - showIntegrationsManager({ - room: this.props.room, - screen: `type_${widgetType}`, - integrationId: this.state.widgetId, - }); + // TODO: Open the right integration manager for the widget + IntegrationManagers.sharedInstance().getPrimaryManager().open( + this.props.room, + `type_${widgetType}`, + this.state.widgetId, + ); } render() { diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 12726ac234..96382d9cb4 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import request from 'browser-request'; import url from 'url'; import React from 'react'; import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; import SdkConfig from "../../../SdkConfig"; -import Field from "../elements/Field"; import Modal from '../../../Modal'; +import dis from "../../../dispatcher"; /** * If a url has no path component, etc. abbreviate it to just the hostname @@ -59,41 +58,39 @@ function unabbreviateUrl(u) { /** * Check an IS URL is valid, including liveness check * - * @param {string} isUrl The url to check + * @param {string} u The url to check * @returns {string} null if url passes all checks, otherwise i18ned error string */ -async function checkIsUrl(isUrl) { - const parsedUrl = url.parse(isUrl); +async function checkIdentityServerUrl(u) { + const parsedUrl = url.parse(u); if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it - return new Promise((resolve) => { - request( - // also XXX: we don't really know whether to hit /v1 or /v2 for this: we - // probably want a /versions endpoint like the C/S API. - { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, - (err, response, body) => { - if (err) { - resolve(_t("Could not connect to ID Server")); - } else if (response.status < 200 || response.status >= 300) { - resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); - } else { - resolve(null); - } - }, - ); - }); + try { + const response = await fetch(u + '/_matrix/identity/api/v1'); + if (response.ok) { + return null; + } else if (response.status < 200 || response.status >= 300) { + return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status}); + } else { + return _t("Could not connect to Identity Server"); + } + } catch (e) { + return _t("Could not connect to Identity Server"); + } } export default class SetIdServer extends React.Component { constructor() { super(); - let defaultIdServer = abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()); - if (!defaultIdServer) { - defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['idServer']) || ''; + let defaultIdServer = ''; + if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { + // If no ID server is configured but there's one in the config, prepopulate + // the field to help the user. + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -115,7 +112,7 @@ export default class SetIdServer extends React.Component { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
- { _t("Checking Server") } + { _t("Checking server") }
; } else if (this.state.error) { return this.state.error; @@ -128,18 +125,21 @@ export default class SetIdServer extends React.Component { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = async () => { + _saveIdServer = async (e) => { + e.preventDefault(); + this.setState({busy: true}); const fullUrl = unabbreviateUrl(this.state.idServer); - const errStr = await checkIsUrl(fullUrl); + const errStr = await checkIdentityServerUrl(fullUrl); let newFormValue = this.state.idServer; if (!errStr) { MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); localStorage.setItem("mx_is_url", fullUrl); + dis.dispatch({action: 'id_server_changed'}); newFormValue = ''; } this.setState({ @@ -184,6 +184,7 @@ export default class SetIdServer extends React.Component { render() { const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + const Field = sdk.getComponent('elements.Field'); const idServerUrl = this.state.currentClientIdServer; let sectionTitle; let bodyText; @@ -198,9 +199,9 @@ export default class SetIdServer extends React.Component { } else { sectionTitle = _t("Identity Server"); bodyText = _t( - "You are not currently using an Identity Server. " + + "You are not currently using an identity server. " + "To discover and be discoverable by existing contacts you know, " + - "add one below", + "add one below.", ); } @@ -230,12 +231,12 @@ export default class SetIdServer extends React.Component { id="mx_SetIdServer_idServer" type="text" value={this.state.idServer} autoComplete="off" onChange={this._onIdentityServerChanged} - tooltip={this._getTooltip()} + tooltipContent={this._getTooltip()} /> - + >{_t("Change")} {discoSection} ); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 4c0ebef3f3..b3c7aadd7b 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -1,6 +1,7 @@ /* Copyright 2019 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,6 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; +import {THEMES} from "../../../../../themes"; import PlatformPeg from "../../../../../PlatformPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; @@ -43,9 +45,22 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), }; + + this.dispatcherRef = dis.register(this._onAction); } + componentWillUnmount() { + dis.unregister(this.dispatcherRef); + } + + _onAction = (payload) => { + if (payload.action === 'id_server_changed') { + this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); + } + }; + _onLanguageChange = (newLanguage) => { if (this.state.language === newLanguage) return; @@ -122,7 +137,7 @@ export default class GeneralUserSettingsTab extends React.Component { onFinished={this._onPasswordChanged} /> ); - const threepidSection = MatrixClientPeg.get().getIdentityServerUrl() ?
+ const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} @@ -160,8 +175,9 @@ export default class GeneralUserSettingsTab extends React.Component { {_t("Theme")} - - + {Object.entries(THEMES).map(([theme, text]) => { + return ; + })}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 577048bd18..3d5ee588ef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -251,6 +251,8 @@ "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", + "Light theme": "Light theme", + "Dark theme": "Dark theme", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", @@ -538,16 +540,16 @@ "Display Name": "Display Name", "Save": "Save", "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", - "Could not connect to ID Server": "Could not connect to ID Server", - "Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", - "Checking Server": "Checking Server", + "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", + "Could not connect to Identity Server": "Could not connect to Identity Server", + "Checking server": "Checking server", "Disconnect ID Server": "Disconnect ID Server", "Disconnect from the ID Server ?": "Disconnect from the ID Server ?", "Disconnect": "Disconnect", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "Identity Server": "Identity Server", - "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.", "Change": "Change", "Flair": "Flair", @@ -561,8 +563,6 @@ "Set a new account password...": "Set a new account password...", "Language and region": "Language and region", "Theme": "Theme", - "Light theme": "Light theme", - "Dark theme": "Dark theme", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js new file mode 100644 index 0000000000..4d0181f017 --- /dev/null +++ b/src/integrations/IntegrationManagerInstance.js @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import ScalarAuthClient from "../ScalarAuthClient"; +import sdk from "../index"; +import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; +import type {Room} from "matrix-js-sdk"; +import Modal from '../Modal'; + +export class IntegrationManagerInstance { + apiUrl: string; + uiUrl: string; + + constructor(apiUrl: string, uiUrl: string) { + this.apiUrl = apiUrl; + this.uiUrl = uiUrl; + + // Per the spec: UI URL is optional. + if (!this.uiUrl) this.uiUrl = this.apiUrl; + } + + getScalarClient(): ScalarAuthClient { + return new ScalarAuthClient(this.apiUrl, this.uiUrl); + } + + async open(room: Room = null, screen: string = null, integrationId: string = null): void { + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const dialog = Modal.createTrackedDialog( + 'Integration Manager', '', IntegrationsManager, + {loading: true}, 'mx_IntegrationsManager', + ); + + const client = this.getScalarClient(); + client.setTermsInteractionCallback((policyInfo, agreedUrls) => { + // To avoid visual glitching of two modals stacking briefly, we customise the + // terms dialog sizing when it will appear for the integrations manager so that + // it gets the same basic size as the IM's own modal. + return dialogTermsInteractionCallback( + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + ); + }); + + const newProps = {}; + try { + await client.connect(); + if (!client.hasCredentials()) { + newProps["connected"] = false; + } else { + newProps["url"] = client.getScalarInterfaceUrlForRoom(room, screen, integrationId); + } + } catch (e) { + if (e instanceof TermsNotSignedError) { + dialog.close(); + return; + } + + console.error(e); + newProps["connected"] = false; + } + + // Close the old dialog and open a new one + dialog.close(); + Modal.createTrackedDialog( + 'Integration Manager', '', IntegrationsManager, + newProps, 'mx_IntegrationsManager', + ); + } +} diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js new file mode 100644 index 0000000000..9c9a1fa228 --- /dev/null +++ b/src/integrations/IntegrationManagers.js @@ -0,0 +1,113 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SdkConfig from '../SdkConfig'; +import sdk from "../index"; +import Modal from '../Modal'; +import {IntegrationManagerInstance} from "./IntegrationManagerInstance"; +import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; +import WidgetUtils from "../utils/WidgetUtils"; +import MatrixClientPeg from "../MatrixClientPeg"; + +export class IntegrationManagers { + static _instance; + + static sharedInstance(): IntegrationManagers { + if (!IntegrationManagers._instance) { + IntegrationManagers._instance = new IntegrationManagers(); + } + return IntegrationManagers._instance; + } + + _managers: IntegrationManagerInstance[] = []; + _client: MatrixClient; + + constructor() { + this._compileManagers(); + } + + startWatching(): void { + this.stopWatching(); + this._client = MatrixClientPeg.get(); + this._client.on("accountData", this._onAccountData.bind(this)); + this._compileManagers(); + } + + stopWatching(): void { + if (!this._client) return; + this._client.removeListener("accountData", this._onAccountData.bind(this)); + } + + _compileManagers() { + this._managers = []; + this._setupConfiguredManager(); + this._setupAccountManagers(); + } + + _setupConfiguredManager() { + const apiUrl = SdkConfig.get()['integrations_rest_url']; + const uiUrl = SdkConfig.get()['integrations_ui_url']; + + if (apiUrl && uiUrl) { + this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + } + } + + _setupAccountManagers() { + if (!this._client || !this._client.getUserId()) return; // not logged in + const widgets = WidgetUtils.getIntegrationManagerWidgets(); + widgets.forEach(w => { + const data = w.content['data']; + if (!data) return; + + const uiUrl = w.content['url']; + const apiUrl = data['api_url']; + if (!apiUrl || !uiUrl) return; + + this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + }); + } + + _onAccountData(ev: MatrixEvent): void { + if (ev.getType() === 'm.widgets') { + this._compileManagers(); + } + } + + hasManager(): boolean { + return this._managers.length > 0; + } + + getPrimaryManager(): IntegrationManagerInstance { + if (this.hasManager()) { + return this._managers[this._managers.length - 1]; + } else { + return null; + } + } + + openNoManagerDialog(): void { + // TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken. + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + Modal.createTrackedDialog( + "Integration Manager", "None", IntegrationsManager, + {configured: false}, 'mx_IntegrationsManager', + ); + } +} + +// For debugging +global.mxIntegrationManagers = IntegrationManagers; diff --git a/src/integrations/integrations.js b/src/integrations/integrations.js deleted file mode 100644 index dad6cbf3e8..0000000000 --- a/src/integrations/integrations.js +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import sdk from "../index"; -import ScalarAuthClient from '../ScalarAuthClient'; -import Modal from '../Modal'; -import { TermsNotSignedError, dialogTermsInteractionCallback } from '../Terms'; - -export async function showIntegrationsManager(opts) { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - - let props = {}; - if (ScalarAuthClient.isPossible()) { - props.loading = true; - } else { - props.configured = false; - } - - const close = Modal.createTrackedDialog( - 'Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager", - ).close; - - if (!ScalarAuthClient.isPossible()) { - return; - } - - const scalarClient = new ScalarAuthClient(); - scalarClient.setTermsInteractionCallback(integrationsTermsInteractionCallback); - try { - await scalarClient.connect(); - if (!scalarClient.hasCredentials()) { - props = { connected: false }; - } else { - props = { - url: scalarClient.getScalarInterfaceUrlForRoom( - opts.room, - opts.screen, - opts.integrationId, - ), - }; - } - } catch (err) { - if (err instanceof TermsNotSignedError) { - // user canceled terms dialog, so just cancel the action - close(); - return; - } - console.error(err); - props = { connected: false }; - } - close(); - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager"); -} - -/* - * To avoid visual glitching of two modals stacking briefly, we customise the - * terms dialog sizing when it will appear for the integrations manager so that - * it gets the same basic size as the IM's own modal. - */ -function integrationsTermsInteractionCallback(policiesAndServicePairs, agreedUrls) { - return dialogTermsInteractionCallback( - policiesAndServicePairs, - agreedUrls, - "mx_TermsDialog_forIntegrationsManager", - ); -} diff --git a/src/settings/controllers/ThemeController.js b/src/settings/controllers/ThemeController.js index 615fc4c192..da20521873 100644 --- a/src/settings/controllers/ThemeController.js +++ b/src/settings/controllers/ThemeController.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,17 +16,13 @@ limitations under the License. */ import SettingController from "./SettingController"; - -const SUPPORTED_THEMES = [ - "light", - "dark", -]; +import {DEFAULT_THEME, THEMES} from "../../themes"; export default class ThemeController extends SettingController { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { // Override in case some no longer supported theme is stored here - if (!SUPPORTED_THEMES.includes(calculatedValue)) { - return "light"; + if (!THEMES[calculatedValue]) { + return DEFAULT_THEME; } return null; // no override diff --git a/src/themes.js b/src/themes.js new file mode 100644 index 0000000000..1896333844 --- /dev/null +++ b/src/themes.js @@ -0,0 +1,24 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {_td} from "./languageHandler"; + +export const DEFAULT_THEME = "light"; + +export const THEMES = { + "light": _td("Light theme"), + "dark": _td("Dark theme"), +}; diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 41a241c905..1e47554914 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -27,6 +27,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore'; const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; +import {IntegrationManagers} from "../integrations/IntegrationManagers"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -102,7 +103,8 @@ export default class WidgetUtils { let scalarUrls = SdkConfig.get().integrations_widgets_urls; if (!scalarUrls || scalarUrls.length === 0) { - scalarUrls = [SdkConfig.get().integrations_rest_url]; + const defaultManager = IntegrationManagers.sharedInstance().getPrimaryManager(); + if (defaultManager) scalarUrls = [defaultManager.apiUrl]; } for (let i = 0; i < scalarUrls.length; i++) { @@ -338,6 +340,17 @@ export default class WidgetUtils { return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker"); } + /** + * Get all integration manager widgets for this user. + * @returns {Object[]} An array of integration manager user widgets. + */ + static getIntegrationManagerWidgets() { + const widgets = WidgetUtils.getUserWidgetsArray(); + // We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. + const imTypes = ["m.integration_manager", "im.vector.integration_manager"]; + return widgets.filter(w => w.content && imTypes.includes(w.content.type)); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated diff --git a/yarn.lock b/yarn.lock index f6ae81d6e9..b9341b2a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5061,13 +5061,10 @@ matrix-mock-request@^1.2.3: bluebird "^3.5.0" expect "^1.20.2" -matrix-react-test-utils@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz#b548844d0ebe338ea1b9c8f16474c30d17c3bdf4" - integrity sha1-tUiETQ6+M46hucjxZHTDDRfDvfQ= - dependencies: - react "^15.6.1" - react-dom "^15.6.1" +matrix-react-test-utils@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" + integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== md5.js@^1.3.4: version "1.3.5" @@ -6373,7 +6370,7 @@ react-beautiful-dnd@^4.0.1: redux-thunk "^2.2.0" reselect "^3.0.1" -react-dom@^15.6.0, react-dom@^15.6.1: +react-dom@^15.6.0: version "15.6.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= @@ -6436,7 +6433,7 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react@^15.6.0, react@^15.6.1: +react@^15.6.0: version "15.6.2" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=