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,
"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"],
}],
}
}

View file

@ -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",

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <input>.
};
@ -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 = <Tooltip
tooltipClassName="mx_Field_tooltip"
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 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 = <div />;
if (ScalarAuthClient.isPossible()) {
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations 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.
@ -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 (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
</div>
);
}
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 (
<div className="mx_RoomAvatarEvent">
@ -75,8 +77,8 @@ module.exports = React.createClass({
{
'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}>
<BaseAvatar width={14} height={14} url={url} name={name} />
onClick={this.onAvatarClick}>
<RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>,
})
}

View file

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

View file

@ -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) {

View file

@ -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() {

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");
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 <div>
<InlineSpinner />
{ _t("Checking Server") }
{ _t("Checking server") }
</div>;
} 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()}
/>
<input className="mx_Dialog_primary"
type="submit" value={_t("Change")}
<AccessibleButton type="submit" kind="primary_sm"
onClick={this._saveIdServer}
disabled={!this._idServerChangeEnabled()}
/>
>{_t("Change")}</AccessibleButton>
{discoSection}
</form>
);

View file

@ -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() ? <div>
const threepidSection = this.state.haveIdServer ? <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
@ -160,8 +175,9 @@ export default class GeneralUserSettingsTab extends React.Component {
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option>
<option value="dark">{_t("Dark theme")}</option>
{Object.entries(THEMES).map(([theme, text]) => {
return <option key={theme} value={theme}>{_t(text)}</option>;
})}
</Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div>

View file

@ -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 <idserver></idserver>?": "Disconnect from the ID Server <idserver></idserver>?",
"Disconnect": "Disconnect",
"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.",
"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.",
"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",

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

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

View file

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