Merge remote-tracking branch 'origin/develop' into dbkr/disco_is

This commit is contained in:
David Baker 2019-08-14 10:03:32 +01:00
commit 735c6d73d8
35 changed files with 493 additions and 333 deletions

View file

@ -15,6 +15,9 @@ module.exports = {
"number-leading-zero": null, "number-leading-zero": null,
"selector-list-comma-newline-after": null, "selector-list-comma-newline-after": null,
"at-rule-no-unknown": 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"],
}],
} }
} }

View file

@ -148,7 +148,7 @@
"karma-summary-reporter": "^1.5.1", "karma-summary-reporter": "^1.5.1",
"karma-webpack": "^4.0.0-beta.0", "karma-webpack": "^4.0.0-beta.0",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.1.1", "matrix-react-test-utils": "^0.2.2",
"mocha": "^5.0.5", "mocha": "^5.0.5",
"react-addons-test-utils": "^15.4.0", "react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1", "require-json": "0.0.1",

View file

@ -559,3 +559,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Username_color8 { .mx_Username_color8 {
color: $username-variant8-color; color: $username-variant8-color;
} }
@define-mixin mx_Settings_fullWidthField {
margin-right: 200px;
}

View file

@ -26,6 +26,10 @@ limitations under the License.
height: 4em; height: 4em;
} }
.mx_ProfileSettings_controls .mx_Field {
margin-right: 100px;
}
.mx_ProfileSettings_controls .mx_Field:first-child { .mx_ProfileSettings_controls .mx_Field:first-child {
margin-top: 0; margin-top: 0;
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { .mx_SetIdServer .mx_Field_input {
width: 300px; @mixin mx_Settings_fullWidthField;
} }

View file

@ -16,15 +16,15 @@ limitations under the License.
.mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_changePassword .mx_Field,
.mx_GeneralUserSettingsTab_themeSection .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 { .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child {
margin-top: 0; margin-top: 0;
} }
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses,
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers,
.mx_GeneralUserSettingsTab_languageInput { .mx_GeneralUserSettingsTab_languageInput {
margin-right: 100px; // Align with the other fields on the page @mixin mx_Settings_fullWidthField;
} }

View file

@ -15,5 +15,5 @@ limitations under the License.
*/ */
.mx_PreferencesUserSettingsTab .mx_Field { .mx_PreferencesUserSettingsTab .mx_Field {
margin-right: 100px; // Align with the rest of the controls @mixin mx_Settings_fullWidthField;
} }

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
.mx_VoiceUserSettingsTab .mx_Field { .mx_VoiceUserSettingsTab .mx_Field {
margin-right: 100px; // align with the rest of the fields @mixin mx_Settings_fullWidthField;
} }
.mx_VoiceUserSettingsTab_missingMediaPermissions { .mx_VoiceUserSettingsTab_missingMediaPermissions {

View file

@ -63,7 +63,7 @@ import SdkConfig from './SdkConfig';
import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore'; import WidgetEchoStore from './stores/WidgetEchoStore';
import ScalarAuthClient from './ScalarAuthClient'; import {IntegrationManagers} from "./integrations/IntegrationManagers";
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
@ -348,14 +348,20 @@ async function _startCallApp(roomId, type) {
// the state event in anyway, but the resulting widget would then not // 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 // work for us. Better that the user knows before everyone else in the
// room sees it. // room sees it.
const scalarClient = new ScalarAuthClient(); const managers = IntegrationManagers.sharedInstance();
let haveScalar = false; let haveScalar = true;
try { if (managers.hasManager()) {
await scalarClient.connect(); try {
haveScalar = scalarClient.hasCredentials(); const scalarClient = managers.getPrimaryManager().getScalarClient();
} catch (e) { await scalarClient.connect();
// fall through haveScalar = scalarClient.hasCredentials();
} catch (e) {
// ignore
}
} else {
haveScalar = false;
} }
if (!haveScalar) { if (!haveScalar) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); 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. // URL, but this will at least allow the integration manager to not be hardcoded.
widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString;
} else { } 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 }; const widgetData = { widgetSessionId };

View file

@ -22,7 +22,7 @@ import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore'; import ActiveWidgetStore from './stores/ActiveWidgetStore';
import MatrixClientPeg from "./MatrixClientPeg"; import MatrixClientPeg from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore"; 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 WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [ const SUPPORTED_WIDGET_API_VERSIONS = [
@ -193,11 +193,12 @@ export default class FromWidgetPostMessageApi {
const integType = (data && data.integType) ? data.integType : null; const integType = (data && data.integType) ? data.integType : null;
const integId = (data && data.integId) ? data.integId : null; const integId = (data && data.integId) ? data.integId : null;
showIntegrationsManager({ // TODO: Open the right integration manager for the widget
room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), IntegrationManagers.sharedInstance().getPrimaryManager().open(
screen: 'type_' + integType, MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
integrationId: integId, `type_${integType}`,
}); integId,
);
} else if (action === 'set_always_on_screen') { } else if (action === 'set_always_on_screen') {
// This is a new message: there is no reason to support the deprecated widgetData here // This is a new message: there is no reason to support the deprecated widgetData here
const data = event.data.data; const data = event.data.data;

View file

@ -35,6 +35,7 @@ import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager'; import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import TypingStore from "./stores/TypingStore"; import TypingStore from "./stores/TypingStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
/** /**
* Called at startup, to attempt to build a logged-in Matrix session. It tries * 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(); Presence.start();
} }
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();
if (startSyncing) { if (startSyncing) {
@ -638,6 +640,7 @@ export function stopMatrixClient(unsetClient=true) {
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();
ActiveWidgetStore.stop(); ActiveWidgetStore.stop();
IntegrationManagers.sharedInstance().stopWatching();
if (DMRoomMap.shared()) DMRoomMap.shared().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop();
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {

View file

@ -274,7 +274,7 @@ class ModalManager {
this._reRender(); this._reRender();
return { return {
close: closeDialog, close: closeDialog,
then: (resolve, reject) => onFinishedProm.then(resolve, reject), finished: onFinishedProm,
}; };
} }
@ -285,7 +285,7 @@ class ModalManager {
this._reRender(); this._reRender();
return { return {
close: closeDialog, close: closeDialog,
then: (resolve, reject) => onFinishedProm.then(resolve, reject), finished: onFinishedProm,
}; };
} }

View file

@ -29,20 +29,43 @@ import * as Matrix from 'matrix-js-sdk';
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
class ScalarAuthClient { export default class ScalarAuthClient {
constructor() { constructor(apiUrl, uiUrl) {
this.apiUrl = apiUrl;
this.uiUrl = uiUrl;
this.scalarToken = null; this.scalarToken = null;
// `undefined` to allow `startTermsFlow` to fallback to a default // `undefined` to allow `startTermsFlow` to fallback to a default
// callback if this is unset. // callback if this is unset.
this.termsInteractionCallback = undefined; 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;
} }
/** _writeTokenToStore() {
* Determines if setting up a ScalarAuthClient is even possible window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
* @returns {boolean} true if possible, false otherwise. if (this.isDefaultManager) {
*/ // We remove the old token from storage to migrate upwards. This is safe
static isPossible() { // to do because even if the user switches to /app when this is on /develop
return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; // 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) { setTermsInteractionCallback(callback) {
@ -61,8 +84,7 @@ class ScalarAuthClient {
// Returns a promise that resolves to a scalar_token string // Returns a promise that resolves to a scalar_token string
getScalarToken() { getScalarToken() {
let token = this.scalarToken; const token = this._readToken();
if (!token) token = window.localStorage.getItem("mx_scalar_token");
if (!token) { if (!token) {
return this.registerForToken(); return this.registerForToken();
@ -78,7 +100,7 @@ class ScalarAuthClient {
} }
_getAccountName(token) { _getAccountName(token) {
const url = SdkConfig.get().integrations_rest_url + "/account"; const url = this.apiUrl + "/account";
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
request({ request({
@ -111,7 +133,7 @@ class ScalarAuthClient {
return token; return token;
}).catch((e) => { }).catch((e) => {
if (e instanceof TermsNotSignedError) { 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, // The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the // but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk // 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 // 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 grace period to update their configs, then use the rest url as
// a regular base url. // a regular base url.
const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url); const parsedImRestUrl = url.parse(this.apiUrl);
parsedImRestUrl.path = ''; parsedImRestUrl.path = '';
parsedImRestUrl.pathname = ''; parsedImRestUrl.pathname = '';
return startTermsFlow([new Service( return startTermsFlow([new Service(
@ -147,17 +169,18 @@ class ScalarAuthClient {
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token // Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject); 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) // Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this._checkToken(tokenObject); return this._checkToken(token);
}).then((tokenObject) => { }).then((token) => {
window.localStorage.setItem("mx_scalar_token", tokenObject); this.scalarToken = token;
return tokenObject; this._writeTokenToStore();
return token;
}); });
} }
exchangeForScalarToken(openidTokenObject) { exchangeForScalarToken(openidTokenObject) {
const scalarRestUrl = SdkConfig.get().integrations_rest_url; const scalarRestUrl = this.apiUrl;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
request({ request({
@ -181,7 +204,7 @@ class ScalarAuthClient {
} }
getScalarPageTitle(url) { getScalarPageTitle(url) {
let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
@ -217,7 +240,7 @@ class ScalarAuthClient {
* @return {Promise} Resolves on completion * @return {Promise} Resolves on completion
*/ */
disableWidgetAssets(widgetType, widgetId) { 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); url = this.getStarterLink(url);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request({ request({
@ -246,7 +269,7 @@ class ScalarAuthClient {
getScalarInterfaceUrlForRoom(room, screen, id) { getScalarInterfaceUrlForRoom(room, screen, id) {
const roomId = room.roomId; const roomId = room.roomId;
const roomName = room.name; const roomName = room.name;
let url = SdkConfig.get().integrations_ui_url; let url = this.uiUrl;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId); url += "&room_id=" + encodeURIComponent(roomId);
url += "&room_name=" + encodeURIComponent(roomName); url += "&room_name=" + encodeURIComponent(roomName);
@ -264,5 +287,3 @@ class ScalarAuthClient {
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
} }
} }
module.exports = ScalarAuthClient;

View file

@ -232,13 +232,13 @@ Example:
} }
*/ */
import SdkConfig from './SdkConfig';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk'; import { MatrixEvent } from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import RoomViewStore from './stores/RoomViewStore'; import RoomViewStore from './stores/RoomViewStore';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import {IntegrationManagers} from "./integrations/IntegrationManagers";
function sendResponse(event, res) { function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); 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) // (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
let configUrl; let configUrl;
try { try {
configUrl = new URL(SdkConfig.get().integrations_ui_url); // TODO: Support multiple integration managers
configUrl = new URL(IntegrationManagers.sharedInstance().getPrimaryManager().uiUrl);
} catch (e) { } catch (e) {
// No integrations UI URL, ignore silently. // No integrations UI URL, ignore silently.
return; return;

View file

@ -935,7 +935,7 @@ export default React.createClass({
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
const [shouldCreate, name, noFederate] = await modal; const [shouldCreate, name, noFederate] = await modal.finished;
if (shouldCreate) { if (shouldCreate) {
const createOpts = {}; const createOpts = {};
if (name) createOpts.name = name; if (name) createOpts.name = name;

View file

@ -15,13 +15,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector'; 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'; 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 * This is a variant of ServerConfig with only the HS field and different body
* text that is specific to the Modular case. * text that is specific to the Modular case.
*/ */
export default class ModularServerConfig extends React.PureComponent { export default class ModularServerConfig extends ServerConfig {
static propTypes = { static propTypes = ServerConfig.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);
}
async validateAndApplyServer(hsUrl, isUrl) { async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first // 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); 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() { render() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -69,10 +70,10 @@ module.exports = React.createClass({
fieldValid: {}, fieldValid: {},
// The ISO2 country code selected in the phone number entry // The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry, phoneCountry: this.props.defaultPhoneCountry,
username: "", username: this.props.defaultUsername || "",
email: "", email: this.props.defaultEmail || "",
phoneNumber: "", phoneNumber: this.props.defaultPhoneNumber || "",
password: "", password: this.props.defaultPassword || "",
passwordConfirm: "", passwordConfirm: "",
passwordComplexity: null, passwordComplexity: null,
passwordSafe: false, passwordSafe: false,
@ -90,7 +91,7 @@ module.exports = React.createClass({
} }
const self = this; const self = this;
if (this.state.email == '') { if (this.state.email === '') {
const haveIs = Boolean(this.props.serverConfig.isUrl); const haveIs = Boolean(this.props.serverConfig.isUrl);
let desc; let desc;
@ -455,7 +456,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_EMAIL] = field} ref={field => this[FIELD_EMAIL] = field}
type="text" type="text"
label={emailPlaceholder} label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email} value={this.state.email}
onChange={this.onEmailChange} onChange={this.onEmailChange}
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
@ -469,7 +469,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD] = field} ref={field => this[FIELD_PASSWORD] = field}
type="password" type="password"
label={_t("Password")} label={_t("Password")}
defaultValue={this.props.defaultPassword}
value={this.state.password} value={this.state.password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate} onValidate={this.onPasswordValidate}
@ -483,7 +482,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD_CONFIRM] = field} ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password" type="password"
label={_t("Confirm")} label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
value={this.state.passwordConfirm} value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange} onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate} onValidate={this.onPasswordConfirmValidate}
@ -512,7 +510,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PHONE_NUMBER] = field} ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text" type="text"
label={phoneLabel} label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefix={phoneCountry}
onChange={this.onPhoneNumberChange} onChange={this.onPhoneNumberChange}
@ -528,7 +525,6 @@ module.exports = React.createClass({
type="text" type="text"
autoFocus={true} autoFocus={true}
label={_t("Username")} label={_t("Username")}
defaultValue={this.props.defaultUsername}
value={this.state.username} value={this.state.username}
onChange={this.onUsernameChange} onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate} onValidate={this.onUsernameValidate}

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar'; import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -121,6 +121,10 @@ module.exports = React.createClass({
); );
urls.push(defaultImageUrl); // lowest priority urls.push(defaultImageUrl); // lowest priority
} }
// deduplicate URLs
urls = Array.from(new Set(urls));
return { return {
imageUrls: urls, imageUrls: urls,
defaultImageUrl: defaultImageUrl, defaultImageUrl: defaultImageUrl,

View file

@ -22,7 +22,6 @@ import qs from 'querystring';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging'; import WidgetMessaging from '../../../WidgetMessaging';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -35,7 +34,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames'; import classNames from 'classnames';
import { showIntegrationsManager } from '../../../integrations/integrations'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false; const ENABLE_REACT_PERF = false;
@ -178,9 +177,22 @@ export default class AppTile extends React.Component {
return; 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 // Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) { if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient(); this._scalarClient = managers.getPrimaryManager().getScalarClient();
} }
this._scalarClient.getScalarToken().done((token) => { this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present // 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); const params = qs.parse(u.query);
if (!params.scalar_token) { if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(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.search = undefined;
u.query = params; u.query = params;
} }
@ -251,11 +263,12 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) { if (this.props.onEditClick) {
this.props.onEditClick(); this.props.onEditClick();
} else { } else {
showIntegrationsManager({ // TODO: Open the right manager for the widget
room: this.props.room, IntegrationManagers.sharedInstance().getPrimaryManager().open(
screen: 'type_' + this.props.type, this.props.room,
integrationId: this.props.id, this.props.type,
}); this.props.id,
);
} }
} }

View file

@ -48,7 +48,7 @@ export default class Field extends React.PureComponent {
onValidate: PropTypes.func, onValidate: PropTypes.func,
// If specified, contents will appear as a tooltip on the element and // If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed. // validation feedback tooltips will be suppressed.
tooltip: PropTypes.node, tooltipContent: PropTypes.node,
// All other props pass through to the <input>. // All other props pass through to the <input>.
}; };
@ -137,8 +137,7 @@ export default class Field extends React.PureComponent {
}, VALIDATION_THROTTLE_MS); }, VALIDATION_THROTTLE_MS);
render() { render() {
const { element, prefix, onValidate, children, ...inputProps } = this.props; const { element, prefix, onValidate, children, tooltipContent, ...inputProps } = this.props;
delete inputProps.tooltip; // needs to be removed from props but we don't need it here
const inputElement = element || "input"; const inputElement = element || "input";
@ -170,11 +169,11 @@ export default class Field extends React.PureComponent {
// Handle displaying feedback on validity // Handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
let fieldTooltip; let fieldTooltip;
if (this.props.tooltip || this.state.feedback) { if (tooltipContent || this.state.feedback) {
fieldTooltip = <Tooltip fieldTooltip = <Tooltip
tooltipClassName="mx_Field_tooltip" tooltipClassName="mx_Field_tooltip"
visible={this.state.feedbackVisible} visible={this.state.feedbackVisible}
label={this.props.tooltip || this.state.feedback} label={tooltipContent || this.state.feedback}
/>; />;
} }

View file

@ -18,9 +18,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { showIntegrationsManager } from '../../../integrations/integrations'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
export default class ManageIntegsButton extends React.Component { export default class ManageIntegsButton extends React.Component {
constructor(props) { constructor(props) {
@ -30,12 +29,17 @@ export default class ManageIntegsButton extends React.Component {
onManageIntegrations = (ev) => { onManageIntegrations = (ev) => {
ev.preventDefault(); ev.preventDefault();
showIntegrationsManager({ room: this.props.room }); const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
managers.getPrimaryManager().open(this.props.room);
}
}; };
render() { render() {
let integrationsButton = <div />; let integrationsButton = <div />;
if (ScalarAuthClient.isPossible()) { if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = ( integrationsButton = (
<AccessibleButton <AccessibleButton

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -31,12 +31,21 @@ module.exports = React.createClass({
mxEvent: PropTypes.object.isRequired, mxEvent: PropTypes.object.isRequired,
}, },
onAvatarClick: function(name) { onAvatarClick: function() {
const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url); 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 ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: name, name: text,
}; };
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}, },
@ -44,29 +53,22 @@ module.exports = React.createClass({
render: function() { render: function() {
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
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 : '',
});
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return ( return (
<div className="mx_TextualEvent"> <div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) } { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
</div> </div>
); );
} }
const url = ContentRepo.getHttpUriForMxc( const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
MatrixClientPeg.get().getHomeserverUrl(), // Provide all arguments to RoomAvatar via oobData because the avatar is historic
ev.getContent().url, const oobData = {
Math.ceil(14 * window.devicePixelRatio), avatarUrl: ev.getContent().url,
Math.ceil(14 * window.devicePixelRatio), name: room ? room.name : "",
'crop', };
);
return ( return (
<div className="mx_RoomAvatarEvent"> <div className="mx_RoomAvatarEvent">
@ -75,8 +77,8 @@ module.exports = React.createClass({
{ {
'img': () => 'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar" <AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}> onClick={this.onAvatarClick}>
<BaseAvatar width={14} height={14} url={url} name={name} /> <RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>, </AccessibleButton>,
}) })
} }

View file

@ -25,7 +25,6 @@ import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
import {formatDate} from '../../../DateUtils'; import {formatDate} from '../../../DateUtils';
import sdk from '../../../index'; import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
@ -35,6 +34,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import {host as matrixtoHost} from '../../../matrix-to'; import {host as matrixtoHost} from '../../../matrix-to';
import {pillifyLinks} from '../../../utils/pillify'; import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'TextualBody', 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 // 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. // 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 // Go fetch a scalar token
const scalarClient = new ScalarAuthClient(); const integrationManager = managers.getPrimaryManager();
const scalarClient = integrationManager.getScalarClient();
scalarClient.connect().then(() => { scalarClient.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink); const completeUrl = scalarClient.getStarterLink(starterLink);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const integrationsUrl = SdkConfig.get().integrations_ui_url; const integrationsUrl = integrationManager.uiUrl;
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, { Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
title: _t("Add an Integration"), title: _t("Add an Integration"),
description: description:

View file

@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore"; import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton'; 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 // The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2; const MAX_WIDGETS = 2;
@ -128,10 +128,7 @@ module.exports = React.createClass({
}, },
_launchManageIntegrations: function() { _launchManageIntegrations: function() {
showIntegrationsManager({ IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
room: this.props.room,
screen: 'add_integ',
});
}, },
onClickAddWidget: function(e) { onClickAddWidget: function(e) {

View file

@ -18,13 +18,12 @@ import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile'; import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement"; import PersistedElement from "../elements/PersistedElement";
import { showIntegrationsManager } from '../../../integrations/integrations'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
const widgetType = 'm.stickerpicker'; const widgetType = 'm.stickerpicker';
@ -67,8 +66,9 @@ export default class Stickerpicker extends React.Component {
_acquireScalarClient() { _acquireScalarClient() {
if (this.scalarClient) return Promise.resolve(this.scalarClient); if (this.scalarClient) return Promise.resolve(this.scalarClient);
if (ScalarAuthClient.isPossible()) { // TODO: Pick the right manager for the widget
this.scalarClient = new ScalarAuthClient(); if (IntegrationManagers.sharedInstance().hasManager()) {
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
return this.scalarClient.connect().then(() => { return this.scalarClient.connect().then(() => {
this.forceUpdate(); this.forceUpdate();
return this.scalarClient; return this.scalarClient;
@ -348,11 +348,12 @@ export default class Stickerpicker extends React.Component {
* Launch the integrations manager on the stickers integration page * Launch the integrations manager on the stickers integration page
*/ */
_launchManageIntegrations() { _launchManageIntegrations() {
showIntegrationsManager({ // TODO: Open the right integration manager for the widget
room: this.props.room, IntegrationManagers.sharedInstance().getPrimaryManager().open(
screen: `type_${widgetType}`, this.props.room,
integrationId: this.state.widgetId, `type_${widgetType}`,
}); this.state.widgetId,
);
} }
render() { render() {

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import request from 'browser-request';
import url from 'url'; import url from 'url';
import React from 'react'; import React from 'react';
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import Field from "../elements/Field";
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import dis from "../../../dispatcher";
/** /**
* If a url has no path component, etc. abbreviate it to just the hostname * 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 * 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 * @returns {string} null if url passes all checks, otherwise i18ned error string
*/ */
async function checkIsUrl(isUrl) { async function checkIdentityServerUrl(u) {
const parsedUrl = url.parse(isUrl); const parsedUrl = url.parse(u);
if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); 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 // 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 // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
return new Promise((resolve) => { try {
request( const response = await fetch(u + '/_matrix/identity/api/v1');
// also XXX: we don't really know whether to hit /v1 or /v2 for this: we if (response.ok) {
// probably want a /versions endpoint like the C/S API. return null;
{ method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, } else if (response.status < 200 || response.status >= 300) {
(err, response, body) => { return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status});
if (err) { } else {
resolve(_t("Could not connect to ID Server")); return _t("Could not connect to Identity Server");
} else if (response.status < 200 || response.status >= 300) { }
resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); } catch (e) {
} else { return _t("Could not connect to Identity Server");
resolve(null); }
}
},
);
});
} }
export default class SetIdServer extends React.Component { export default class SetIdServer extends React.Component {
constructor() { constructor() {
super(); super();
let defaultIdServer = abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()); let defaultIdServer = '';
if (!defaultIdServer) { if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) {
defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['idServer']) || ''; // 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 = { this.state = {
@ -115,7 +112,7 @@ export default class SetIdServer extends React.Component {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div> return <div>
<InlineSpinner /> <InlineSpinner />
{ _t("Checking Server") } { _t("Checking server") }
</div>; </div>;
} else if (this.state.error) { } else if (this.state.error) {
return 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; return !!this.state.idServer && !this.state.busy;
}; };
_saveIdServer = async () => { _saveIdServer = async (e) => {
e.preventDefault();
this.setState({busy: true}); this.setState({busy: true});
const fullUrl = unabbreviateUrl(this.state.idServer); const fullUrl = unabbreviateUrl(this.state.idServer);
const errStr = await checkIsUrl(fullUrl); const errStr = await checkIdentityServerUrl(fullUrl);
let newFormValue = this.state.idServer; let newFormValue = this.state.idServer;
if (!errStr) { if (!errStr) {
MatrixClientPeg.get().setIdentityServerUrl(fullUrl); MatrixClientPeg.get().setIdentityServerUrl(fullUrl);
localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_access_token");
localStorage.setItem("mx_is_url", fullUrl); localStorage.setItem("mx_is_url", fullUrl);
dis.dispatch({action: 'id_server_changed'});
newFormValue = ''; newFormValue = '';
} }
this.setState({ this.setState({
@ -184,6 +184,7 @@ export default class SetIdServer extends React.Component {
render() { render() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const Field = sdk.getComponent('elements.Field');
const idServerUrl = this.state.currentClientIdServer; const idServerUrl = this.state.currentClientIdServer;
let sectionTitle; let sectionTitle;
let bodyText; let bodyText;
@ -198,9 +199,9 @@ export default class SetIdServer extends React.Component {
} else { } else {
sectionTitle = _t("Identity Server"); sectionTitle = _t("Identity Server");
bodyText = _t( 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, " + "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" id="mx_SetIdServer_idServer"
type="text" value={this.state.idServer} autoComplete="off" type="text" value={this.state.idServer} autoComplete="off"
onChange={this._onIdentityServerChanged} onChange={this._onIdentityServerChanged}
tooltip={this._getTooltip()} tooltipContent={this._getTooltip()}
/> />
<input className="mx_Dialog_primary" <AccessibleButton type="submit" kind="primary_sm"
type="submit" value={_t("Change")} onClick={this._saveIdServer}
disabled={!this._idServerChangeEnabled()} disabled={!this._idServerChangeEnabled()}
/> >{_t("Change")}</AccessibleButton>
{discoSection} {discoSection}
</form> </form>
); );

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C. 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {THEMES} from "../../../../../themes";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../.."; import sdk from "../../../../..";
@ -43,9 +45,22 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = { this.state = {
language: languageHandler.getCurrentLanguage(), language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), 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) => { _onLanguageChange = (newLanguage) => {
if (this.state.language === newLanguage) return; if (this.state.language === newLanguage) return;
@ -122,7 +137,7 @@ export default class GeneralUserSettingsTab extends React.Component {
onFinished={this._onPasswordChanged} /> onFinished={this._onPasswordChanged} />
); );
const threepidSection = MatrixClientPeg.get().getIdentityServerUrl() ? <div> const threepidSection = this.state.haveIdServer ? <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span> <span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses /> <EmailAddresses />
@ -160,8 +175,9 @@ export default class GeneralUserSettingsTab extends React.Component {
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span> <span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select" <Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}> value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option> {Object.entries(THEMES).map(([theme, text]) => {
<option value="dark">{_t("Dark theme")}</option> return <option key={theme} value={theme}>{_t(text)}</option>;
})}
</Field> </Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div> </div>

View file

@ -251,6 +251,8 @@
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(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 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", "%(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 …", "%(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 …|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 …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
@ -538,16 +540,16 @@
"Display Name": "Display Name", "Display Name": "Display Name",
"Save": "Save", "Save": "Save",
"Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", "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 Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)",
"Not a valid ID Server (status code %(code)s)": "Not a valid ID Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server",
"Checking Server": "Checking Server", "Checking server": "Checking server",
"Disconnect ID Server": "Disconnect ID Server", "Disconnect ID Server": "Disconnect ID Server",
"Disconnect from the ID Server <idserver></idserver>?": "Disconnect from the ID Server <idserver></idserver>?", "Disconnect from the ID Server <idserver></idserver>?": "Disconnect from the ID Server <idserver></idserver>?",
"Disconnect": "Disconnect", "Disconnect": "Disconnect",
"Identity Server (%(server)s)": "Identity Server (%(server)s)", "Identity Server (%(server)s)": "Identity Server (%(server)s)",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.", "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.",
"Identity Server": "Identity Server", "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 wont be discoverable by other users and you wont be able to invite others by email or phone.": "Disconnecting from your identity server will mean you wont be discoverable by other users and you wont be able to invite others by email or phone.", "Disconnecting from your identity server will mean you wont be discoverable by other users and you wont be able to invite others by email or phone.": "Disconnecting from your identity server will mean you wont be discoverable by other users and you wont be able to invite others by email or phone.",
"Change": "Change", "Change": "Change",
"Flair": "Flair", "Flair": "Flair",
@ -561,8 +563,6 @@
"Set a new account password...": "Set a new account password...", "Set a new account password...": "Set a new account password...",
"Language and region": "Language and region", "Language and region": "Language and region",
"Theme": "Theme", "Theme": "Theme",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"Account management": "Account management", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account", "Deactivate Account": "Deactivate Account",

View file

@ -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',
);
}
}

View file

@ -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;

View file

@ -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",
);
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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"; import SettingController from "./SettingController";
import {DEFAULT_THEME, THEMES} from "../../themes";
const SUPPORTED_THEMES = [
"light",
"dark",
];
export default class ThemeController extends SettingController { export default class ThemeController extends SettingController {
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
// Override in case some no longer supported theme is stored here // Override in case some no longer supported theme is stored here
if (!SUPPORTED_THEMES.includes(calculatedValue)) { if (!THEMES[calculatedValue]) {
return "light"; return DEFAULT_THEME;
} }
return null; // no override return null; // no override

24
src/themes.js Normal file
View file

@ -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"),
};

View file

@ -27,6 +27,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore';
const WIDGET_WAIT_TIME = 20000; const WIDGET_WAIT_TIME = 20000;
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers";
/** /**
* Encodes a URI according to a set of template variables. Variables will be * 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; let scalarUrls = SdkConfig.get().integrations_widgets_urls;
if (!scalarUrls || scalarUrls.length === 0) { 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++) { 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"); 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) * Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
* @return {Promise} Resolves on account data updated * @return {Promise} Resolves on account data updated

View file

@ -5061,13 +5061,10 @@ matrix-mock-request@^1.2.3:
bluebird "^3.5.0" bluebird "^3.5.0"
expect "^1.20.2" expect "^1.20.2"
matrix-react-test-utils@^0.1.1: matrix-react-test-utils@^0.2.2:
version "0.1.1" version "0.2.2"
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz#b548844d0ebe338ea1b9c8f16474c30d17c3bdf4" resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha1-tUiETQ6+M46hucjxZHTDDRfDvfQ= integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
dependencies:
react "^15.6.1"
react-dom "^15.6.1"
md5.js@^1.3.4: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
@ -6373,7 +6370,7 @@ react-beautiful-dnd@^4.0.1:
redux-thunk "^2.2.0" redux-thunk "^2.2.0"
reselect "^3.0.1" reselect "^3.0.1"
react-dom@^15.6.0, react-dom@^15.6.1: react-dom@^15.6.0:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=
@ -6436,7 +6433,7 @@ react-redux@^5.0.6:
react-is "^16.6.0" react-is "^16.6.0"
react-lifecycles-compat "^3.0.0" react-lifecycles-compat "^3.0.0"
react@^15.6.0, react@^15.6.1: react@^15.6.0:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=