Merge branch 'develop' into bwindels/bacat-scrolling-merged-develop

This commit is contained in:
Bruno Windels 2019-03-27 11:46:28 +01:00
commit adf263c4af
26 changed files with 641 additions and 54 deletions

View File

@ -20,6 +20,10 @@ steps:
# image: "node:10"
- label: ":karma: Tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# webpack loves to gorge itself on resources.
queue: "medium"
command:
# Install chrome
- "echo '--- Installing Chrome'"
@ -43,6 +47,10 @@ steps:
propagate-environment: true
- label: "🔧 Riot Tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# webpack loves to gorge itself on resources.
queue: "medium"
command:
# Install chrome
- "echo '--- Installing Chrome'"

View File

@ -71,6 +71,7 @@
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";

View File

@ -0,0 +1,28 @@
/*
Copyright 2019 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_WidgetOpenIDPermissionsDialog .mx_SettingsFlag {
.mx_ToggleSwitch {
display: inline-block;
vertical-align: middle;
margin-right: 8px;
}
.mx_SettingsFlag_label {
display: inline-block;
vertical-align: middle;
}
}

View File

@ -54,7 +54,7 @@ limitations under the License.
position: fixed;
border: 1px solid $menu-border-color;
border-radius: 4px;
box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6);
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
background-color: $menu-bg-color;
z-index: 2000;
padding: 10px;

View File

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 Travis Ralston
Licensed under the Apache License, Version 2.0 (the 'License');
you may not use this file except in compliance with the License.
@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager';
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
const WIDGET_API_VERSION = '0.0.1'; // Current API version
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
'0.0.1',
'0.0.2',
];
const INBOUND_API_NAME = 'fromWidget';
// Listen for and handle incomming requests using the 'fromWidget' postMessage
// Listen for and handle incoming requests using the 'fromWidget' postMessage
// API and initiate responses
export default class FromWidgetPostMessageApi {
constructor() {
this.widgetMessagingEndpoints = [];
this.widgetListeners = {}; // {action: func[]}
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi {
window.removeEventListener('message', this.onPostMessage);
}
/**
* Adds a listener for a given action
* @param {string} action The action to listen for.
* @param {Function} callbackFn A callback function to be called when the action is
* encountered. Called with two parameters: the interesting request information and
* the raw event received from the postMessage API. The raw event is meant to be used
* for sendResponse and similar functions.
*/
addListener(action, callbackFn) {
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
this.widgetListeners[action].push(callbackFn);
}
/**
* Removes a listener for a given action.
* @param {string} action The action that was subscribed to.
* @param {Function} callbackFn The original callback function that was used to subscribe
* to updates.
*/
removeListener(action, callbackFn) {
if (!this.widgetListeners[action]) return;
const idx = this.widgetListeners[action].indexOf(callbackFn);
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
}
/**
* Register a widget endpoint for trusted postMessage communication
* @param {string} widgetId Unique widget identifier
@ -117,6 +146,13 @@ export default class FromWidgetPostMessageApi {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
}
// Call any listeners we have registered
if (this.widgetListeners[event.data.action]) {
for (const fn of this.widgetListeners[event.data.action]) {
fn(event.data, event);
}
}
// Although the requestId is required, we don't use it. We'll be nice and process the message
// if the property is missing, but with a warning for widget developers.
if (!event.data.requestId) {
@ -164,6 +200,8 @@ export default class FromWidgetPostMessageApi {
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
}
} else if (action === 'get_openid') {
// Handled by caller
} else {
console.warn('Widget postMessage event unhandled');
this.sendError(event, {message: 'The postMessage was unhandled'});

View File

@ -31,7 +31,8 @@ import Modal from './Modal';
import sdk from './index';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg";
import {sendLoginRequest} from "./Login";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -353,6 +354,8 @@ async function _doSetLoggedIn(credentials, clearStorage) {
await _clearStorage();
}
await StorageManager.checkConsistency();
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
if (localStorage) {

View File

@ -103,7 +103,7 @@ class MatrixClientPeg {
} catch (err) {
if (dbType === 'indexeddb') {
console.error('Error starting matrixclient store - falling back to memory store', err);
this.matrixClient.store = new Matrix.MatrixInMemoryStore({
this.matrixClient.store = new Matrix.MemoryStore({
localStorage: global.localStorage,
});
} else {
@ -171,7 +171,7 @@ class MatrixClientPeg {
return matches[1];
}
_createClient(creds: MatrixClientCreds, useIndexedDb) {
_createClient(creds: MatrixClientCreds) {
const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
@ -183,7 +183,7 @@ class MatrixClientPeg {
verificationMethods: [verificationMethods.SAS]
};
this.matrixClient = createMatrixClient(opts, useIndexedDb);
this.matrixClient = createMatrixClient(opts);
// we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high.

View File

@ -124,6 +124,10 @@ class ModalManager {
this.closeAll = this.closeAll.bind(this);
}
hasDialogs() {
return this._priorityModal || this._staticModal || this._modals.length > 0;
}
getOrCreateContainer() {
let container = document.getElementById(DIALOG_CONTAINER_ID);

View File

@ -29,6 +29,7 @@ import * as querystring from "querystring";
import MultiInviter from './utils/MultiInviter';
import { linkifyAndSanitizeHtml } from './HtmlUtils';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import WidgetUtils from "./utils/WidgetUtils";
class Command {
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
@ -431,7 +432,7 @@ export const CommandMap = {
if (!targetRoomId) targetRoomId = roomId;
return success(
cli.leave(targetRoomId).then(function() {
cli.leaveRoomChain(targetRoomId).then(function() {
dis.dispatch({action: 'view_next_room'});
}),
);
@ -606,6 +607,26 @@ export const CommandMap = {
},
}),
addwidget: new Command({
name: 'addwidget',
args: '<url>',
description: _td('Adds a custom widget by URL to the room'),
runFn: function(roomId, args) {
if (!args || (!args.startsWith("https://") && !args.startsWith("http://"))) {
return reject(_t("Please supply a https:// or http:// widget URL"));
}
if (WidgetUtils.canUserModifyWidgets(roomId)) {
const userId = MatrixClientPeg.get().getUserId();
const nowMs = (new Date()).getTime();
const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`);
return success(WidgetUtils.setRoomWidget(
roomId, widgetId, "m.custom", args, "Custom Widget", {}));
} else {
return reject(_t("You cannot modify widgets in this room."));
}
},
}),
// Verify a user, device, and pubkey tuple
verify: new Command({
name: 'verify',

View File

@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,6 +22,11 @@ limitations under the License.
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
import Modal from "./Modal";
import MatrixClientPeg from "./MatrixClientPeg";
import SettingsStore from "./settings/SettingsStore";
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
import WidgetUtils from "./utils/WidgetUtils";
if (!global.mxFromWidgetMessaging) {
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
@ -34,12 +40,14 @@ if (!global.mxToWidgetMessaging) {
const OUTBOUND_API_NAME = 'toWidget';
export default class WidgetMessaging {
constructor(widgetId, widgetUrl, target) {
constructor(widgetId, widgetUrl, isUserWidget, target) {
this.widgetId = widgetId;
this.widgetUrl = widgetUrl;
this.isUserWidget = isUserWidget;
this.target = target;
this.fromWidget = global.mxFromWidgetMessaging;
this.toWidget = global.mxToWidgetMessaging;
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
this.start();
}
@ -109,9 +117,57 @@ export default class WidgetMessaging {
start() {
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
}
stop() {
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
}
async _onOpenIdRequest(ev, rawEv) {
if (ev.widgetId !== this.widgetId) return; // not interesting
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget);
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
return;
}
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
const responseBody = {state: "allowed"};
const credentials = await MatrixClientPeg.get().getOpenIdToken();
Object.assign(responseBody, credentials);
this.fromWidget.sendResponse(rawEv, responseBody);
return;
}
// Confirm that we received the request
this.fromWidget.sendResponse(rawEv, {state: "request"});
// Actually ask for permission to send the user's data
Modal.createTrackedDialog("OpenID widget permissions", '',
WidgetOpenIDPermissionsDialog, {
widgetUrl: this.widgetUrl,
widgetId: this.widgetId,
isUserWidget: this.isUserWidget,
onFinished: async (confirm) => {
const responseBody = {success: confirm};
if (confirm) {
const credentials = await MatrixClientPeg.get().getOpenIdToken();
Object.assign(responseBody, credentials);
}
this.messageToWidget({
api: OUTBOUND_API_NAME,
action: "openid_credentials",
data: responseBody,
}).catch((error) => {
console.error("Failed to send OpenID credentials: ", error);
});
},
},
);
}
}

View File

@ -50,6 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
import TimelineExplosionDialog from "../views/dialogs/TimelineExplosionDialog";
const AutoDiscovery = Matrix.AutoDiscovery;
@ -1067,34 +1068,48 @@ export default React.createClass({
button: _t("Leave"),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
const d = MatrixClientPeg.get().leaveRoomChain(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(() => {
d.then((errors) => {
modal.close();
for (const leftRoomId of Object.keys(errors)) {
const err = errors[leftRoomId];
if (!err) continue;
console.error("Failed to leave room " + leftRoomId + " " + err);
let title = _t("Failed to leave room");
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
title = _t("Can't leave Server Notices room");
message = _t(
"This room is used for important messages from the Homeserver, " +
"so you cannot leave it.",
);
} else if (err && err.message) {
message = err.message;
}
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: title,
description: message,
});
return;
}
if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
// This should only happen if something went seriously wrong with leaving the chain.
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
let title = _t("Failed to leave room");
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
title = _t("Can't leave Server Notices room");
message = _t(
"This room is used for important messages from the Homeserver, " +
"so you cannot leave it.",
);
} else if (err && err.message) {
message = err.message;
}
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: title,
description: message,
title: _t("Failed to leave room"),
description: _t("Unknown error"),
});
});
}
@ -1288,6 +1303,17 @@ export default React.createClass({
return self._loggedInView.child.canResetTimelineInRoom(roomId);
});
cli.on('sync.unexpectedError', function(err) {
if (err.message && err.message.includes("live timeline ") && err.message.includes(" is no longer live ")) {
console.error("Caught timeline explosion - trying to ask user for more information");
if (Modal.hasDialogs()) {
console.warn("User has another dialog open - skipping prompt");
return;
}
Modal.createTrackedDialog('Timeline exploded', '', TimelineExplosionDialog, {});
}
});
cli.on('sync', function(state, prevState, data) {
// LifecycleStore and others cannot directly subscribe to matrix client for
// events because flux only allows store state changes during flux dispatches.

View File

@ -0,0 +1,130 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
// Dev note: this should be a temporary dialog while we work out what is
// actually going on. See https://github.com/vector-im/riot-web/issues/8593
// for more details. This dialog is almost entirely a copy/paste job of
// BugReportDialog.
export default class TimelineExplosionDialog extends React.Component {
static propTypes = {
onFinished: React.PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.state = {
busy: false,
progress: null,
};
}
_onCancel() {
console.log("Reloading without sending logs for timeline explosion");
window.location.reload();
}
_onSubmit = () => {
const userText = "Caught timeline explosion\n\nhttps://github.com/vector-im/riot-web/issues/8593";
this.setState({busy: true, progress: null});
this._sendProgressCallback(_t("Preparing to send logs"));
require(['../../../rageshake/submit-rageshake'], (s) => {
s(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
}).then(() => {
console.log("Logs sent for timeline explosion - reloading Riot");
window.location.reload();
}, (err) => {
console.error("Error sending logs for timeline explosion - reloading anyways.", err);
window.location.reload();
});
});
};
_sendProgressCallback = (progress) => {
this.setState({progress: progress});
};
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
{this.state.progress} ...
<Loader />
</div>
);
}
return (
<BaseDialog className="mx_TimelineExplosionDialog" onFinished={this._onCancel}
title={_t('Error showing you your room')} contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{_t(
"Riot has run into a problem which makes it difficult to show you " +
"your messages right now. Nothing has been lost and reloading the app " +
"should fix this for you. In order to assist us in troubleshooting the " +
"problem, we'd like to take a look at your debug logs. You do not need " +
"to send your logs unless you want to, but we would really appreciate " +
"it if you did. We'd also like to apologize for having to show this " +
"message to you - we hope your debug logs are the key to solving the " +
"issue once and for all. If you'd like more information on the bug you've " +
"accidentally run into, please visit <a>the issue</a>.",
{},
{
'a': (sub) => {
return <a href="https://github.com/vector-im/riot-web/issues/8593"
target="_blank" rel="noopener">{sub}</a>;
},
},
)}
</p>
<p>
{_t(
"Debug logs contain application usage data including your " +
"username, the IDs or aliases of the rooms or groups you " +
"have visited and the usernames of other users. They do " +
"not contain messages.",
)}
</p>
{progress}
</div>
<DialogButtons primaryButton={_t("Send debug logs and reload Riot")}
onPrimaryButtonClick={this._onSubmit}
cancelButton={_t("Reload Riot without sending logs")}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2019 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import WidgetUtils from "../../../utils/WidgetUtils";
export default class WidgetOpenIDPermissionsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
widgetUrl: PropTypes.string.isRequired,
widgetId: PropTypes.string.isRequired,
isUserWidget: PropTypes.bool.isRequired,
};
constructor() {
super();
this.state = {
rememberSelection: false,
};
}
_onAllow = () => {
this._onPermissionSelection(true);
};
_onDeny = () => {
this._onPermissionSelection(false);
};
_onPermissionSelection(allowed) {
if (this.state.rememberSelection) {
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
if (!currentValues.allow) currentValues.allow = [];
if (!currentValues.deny) currentValues.deny = [];
const securityKey = WidgetUtils.getWidgetSecurityKey(
this.props.widgetId,
this.props.widgetUrl,
this.props.isUserWidget);
(allowed ? currentValues.allow : currentValues.deny).push(securityKey);
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
}
this.props.onFinished(allowed);
}
_onRememberSelectionChange = (newVal) => {
this.setState({rememberSelection: newVal});
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("A widget would like to verify your identity")}>
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
<p>
{_t(
"A widget located at %(widgetUrl)s would like to verify your identity. " +
"By allowing this, the widget will be able to verify your user ID, but not " +
"perform actions as you.", {
widgetUrl: this.props.widgetUrl,
},
)}
</p>
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
onChange={this._onRememberSelectionChange}
label={_t("Remember my selection for this widget")} />
</div>
<DialogButtons
primaryButton={_t("Allow")}
onPrimaryButtonClick={this._onAllow}
cancelButton={_t("Deny")}
onCancel={this._onDeny}
/>
</BaseDialog>
);
}
}

View File

@ -131,10 +131,11 @@ export default class NetworkDropdown extends React.Component {
_getMenuOptions() {
const options = [];
const roomDirectory = this.props.config.roomDirectory || {};
let servers = [];
if (this.props.config.roomDirectory.servers) {
servers = servers.concat(this.props.config.roomDirectory.servers);
if (roomDirectory.servers) {
servers = servers.concat(roomDirectory.servers);
}
if (!servers.includes(MatrixClientPeg.getHomeServerName())) {

View File

@ -351,7 +351,8 @@ export default class AppTile extends React.Component {
_setupWidgetMessaging() {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
const widgetMessaging = new WidgetMessaging(
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
@ -447,10 +448,14 @@ export default class AppTile extends React.Component {
}
// Toggle the view state of the apps drawer
dis.dispatch({
action: 'appsDrawer',
show: !this.props.show,
});
if (this.props.userWidget) {
this._onMinimiseClick();
} else {
dis.dispatch({
action: 'appsDrawer',
show: !this.props.show,
});
}
}
_getSafeUrl() {
@ -626,7 +631,7 @@ export default class AppTile extends React.Component {
{ /* Maximise widget */ }
{ showMaximiseButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
title={_t('Minimize apps')}
title={_t('Maximize apps')}
onClick={this._onMinimiseClick}
/> }
{ /* Title */ }

View File

@ -31,15 +31,29 @@ export default class LabelledToggleSwitch extends React.Component {
// Whether or not to disable the toggle switch
disabled: PropTypes.bool,
// True to put the toggle in front of the label
// Default false.
toggleInFront: PropTypes.bool,
};
render() {
// This is a minimal version of a SettingsFlag
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
onChange={this.props.onChange} />;
if (this.props.toggleInFront) {
const temp = firstPart;
firstPart = secondPart;
secondPart = temp;
}
return (
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{this.props.label}</span>
<ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
onChange={this.props.onChange} />
{firstPart}
{secondPart}
</div>
);
}

View File

@ -144,7 +144,7 @@ module.exports = React.createClass({
_launchManageIntegrations: function() {
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
this._scalarClient.connect().done(() => {
this.scalarClient.connect().done(() => {
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
null;

View File

@ -52,7 +52,7 @@ module.exports = React.createClass({
this.props.onHeightChanged,
);
}, (error)=>{
console.error("Failed to get preview for " + this.props.link + " " + error);
console.error("Failed to get URL preview: " + error);
}).done();
},

View File

@ -837,7 +837,7 @@ module.exports = React.createClass({
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications')} />
label={_t('Enable desktop notifications for this device')} />
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange}
@ -845,7 +845,7 @@ module.exports = React.createClass({
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications in web client')} />
label={_t('Enable audible notifications for this device')} />
{ emailNotificationsRows }

View File

@ -197,6 +197,10 @@ export default class SecurityRoomSettingsTab extends React.Component {
});
};
_updateBlacklistDevicesFlag = (checked) => {
MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked);
};
_renderRoomAccess() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
@ -318,6 +322,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
let encryptionSettings = null;
if (isEncrypted) {
encryptionSettings = <SettingsFlag name="blacklistUnverifiedDevices" level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag}
roomId={this.props.roomId} />;
}

View File

@ -235,8 +235,8 @@ export default class HelpUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br />
{_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}<br />
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{_t("Access Token:") + ' '}
<AccessibleButton element="span" onClick={this._showSpoiler}
data-spoiler={MatrixClientPeg.get().getAccessToken()}>

View File

@ -161,6 +161,9 @@
"Define the power level of a user": "Define the power level of a user",
"Deops user with given id": "Deops user with given id",
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
"Please supply a https:// or http:// widget URL": "Please supply a https:// or http:// widget URL",
"You cannot modify widgets in this room.": "You cannot modify widgets in this room.",
"Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
"Unknown (user, device) pair:": "Unknown (user, device) pair:",
"Device already verified!": "Device already verified!",
@ -497,9 +500,9 @@
"Advanced notification settings": "Advanced notification settings",
"There are advanced notifications which are not shown here": "There are advanced notifications which are not shown here",
"You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply",
"Enable desktop notifications": "Enable desktop notifications",
"Enable desktop notifications for this device": "Enable desktop notifications for this device",
"Show message in desktop notification": "Show message in desktop notification",
"Enable audible notifications in web client": "Enable audible notifications in web client",
"Enable audible notifications for this device": "Enable audible notifications for this device",
"Off": "Off",
"On": "On",
"Noisy": "Noisy",
@ -935,6 +938,7 @@
"Failed to remove widget": "Failed to remove widget",
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
"Minimize apps": "Minimize apps",
"Maximize apps": "Maximize apps",
"Reload widget": "Reload widget",
"Popout widget": "Popout widget",
"Picture": "Picture",
@ -1173,11 +1177,19 @@
"Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message",
"COPY": "COPY",
"Error showing you your room": "Error showing you your room",
"Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit <a>the issue</a>.": "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit <a>the issue</a>.",
"Send debug logs and reload Riot": "Send debug logs and reload Riot",
"Reload Riot without sending logs": "Reload Riot without sending logs",
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.",
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.",
"Room contains unknown devices": "Room contains unknown devices",
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
"Unknown devices": "Unknown devices",
"A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget",
"Deny": "Deny",
"Unable to load backup status": "Unable to load backup status",
"Recovery Key Mismatch": "Recovery Key Mismatch",
"Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",

View File

@ -340,6 +340,13 @@ export const SETTINGS = {
displayName: _td('Show developer tools'),
default: false,
},
"widgetOpenIDPermissions": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: {
allow: [],
deny: [],
},
},
"RoomList.orderByImportance": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Order rooms in the room list by most important first instead of most recent'),

101
src/utils/StorageManager.js Normal file
View File

@ -0,0 +1,101 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import Analytics from '../Analytics';
const localStorage = window.localStorage;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
let indexedDB;
try {
indexedDB = window.indexedDB;
} catch (e) {}
// The JS SDK will add a prefix of "matrix-js-sdk:" to the sync store name.
const SYNC_STORE_NAME = "riot-web-sync";
const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
function log(msg) {
console.log(`StorageManager: ${msg}`);
}
function error(msg) {
console.error(`StorageManager: ${msg}`);
}
function track(action) {
Analytics.trackEvent("StorageManager", action);
}
export async function checkConsistency() {
log("Checking storage consistency");
log(`Local storage supported? ${!!localStorage}`);
log(`IndexedDB supported? ${!!indexedDB}`);
let dataInLocalStorage = false;
let dataInCryptoStore = false;
let healthy = true;
if (localStorage) {
dataInLocalStorage = localStorage.length > 0;
log(`Local storage contains data? ${dataInLocalStorage}`);
} else {
healthy = false;
error("Local storage cannot be used on this browser");
track("Local storage disabled");
}
if (indexedDB && localStorage) {
const dataInSyncStore = await Matrix.IndexedDBStore.exists(
indexedDB, SYNC_STORE_NAME,
);
log(`Sync store contains data? ${dataInSyncStore}`);
} else {
healthy = false;
error("Sync store cannot be used on this browser");
track("Sync store disabled");
}
if (indexedDB) {
dataInCryptoStore = await Matrix.IndexedDBCryptoStore.exists(
indexedDB, CRYPTO_STORE_NAME,
);
log(`Crypto store contains data? ${dataInCryptoStore}`);
} else {
healthy = false;
error("Crypto store cannot be used on this browser");
track("Crypto store disabled");
}
if (dataInLocalStorage && !dataInCryptoStore) {
healthy = false;
error(
"Data exists in local storage but not in crypto store. " +
"IndexedDB storage has likely been evicted by the browser!",
);
track("Crypto store evicted");
}
if (healthy) {
log("Storage consistency checks passed");
track("Consistency checks passed");
} else {
error("Storage consistency checks failed");
track("Consistency checks failed");
}
}

View File

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,6 +26,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore';
// before waitFor[Room/User]Widget rejects its promise
const WIDGET_WAIT_TIME = 20000;
import SettingsStore from "../settings/SettingsStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
/**
* Encodes a URI according to a set of template variables. Variables will be
@ -396,4 +398,25 @@ export default class WidgetUtils {
return capWhitelist;
}
static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) {
let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
if (isUserWidget) {
const userWidget = WidgetUtils.getUserWidgetsArray()
.find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl);
if (!userWidget) {
throw new Error("No matching user widget to form security key");
}
widgetLocation = userWidget.sender;
}
if (!widgetLocation) {
throw new Error("Failed to locate where the widget resides");
}
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
}
}

View File

@ -32,27 +32,18 @@ try {
* @param {Object} opts options to pass to Matrix.createClient. This will be
* extended with `sessionStore` and `store` members.
*
* @param {bool} useIndexedDb True to attempt to use indexeddb, or false to force
* use of the memory store. Default: true.
*
* @property {string} indexedDbWorkerScript Optional URL for a web worker script
* for IndexedDB store operations. By default, indexeddb ops are done on
* the main thread.
*
* @returns {MatrixClient} the newly-created MatrixClient
*/
export default function createMatrixClient(opts, useIndexedDb) {
if (useIndexedDb === undefined) useIndexedDb = true;
export default function createMatrixClient(opts) {
const storeOpts = {
useAuthorizationHeader: true,
};
if (localStorage) {
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (indexedDB && localStorage && useIndexedDb) {
if (indexedDB && localStorage) {
storeOpts.store = new Matrix.IndexedDBStore({
indexedDB: indexedDB,
dbName: "riot-web-sync",
@ -61,6 +52,16 @@ export default function createMatrixClient(opts, useIndexedDb) {
});
}
if (localStorage) {
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (indexedDB) {
storeOpts.cryptoStore = new Matrix.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto",
);
}
opts = Object.assign(storeOpts, opts);
return Matrix.createClient(opts);