diff --git a/.eslintrc.js b/.eslintrc.js
index 62d24ea707..971809f851 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -47,6 +47,9 @@ module.exports = {
}],
"react/jsx-key": ["error"],
+ // Components in JSX should always be defined.
+ "react/jsx-no-undef": "error",
+
// Assert no spacing in JSX curly brackets
//
//
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 99025f0e0a..f7c8c8b1c5 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,4 +1,4 @@
Contributing code to The React SDK
==================================
-matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
+matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst
diff --git a/res/css/_common.scss b/res/css/_common.scss
index 11e04f5dc0..97ae5412e1 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -34,7 +34,7 @@ body {
-webkit-font-smoothing: subpixel-antialiased;
}
-div.error, div.warning {
+.error, .warning {
color: $warning-color;
}
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 7271038444..06540a2d3e 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -49,6 +49,7 @@
@import "./views/dialogs/_ShareDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
+@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss";
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
@import "./views/directory/_NetworkDropdown.scss";
@import "./views/elements/_AccessibleButton.scss";
diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss
index b4dff612ed..554aabfcd1 100644
--- a/res/css/structures/_RightPanel.scss
+++ b/res/css/structures/_RightPanel.scss
@@ -55,6 +55,10 @@ limitations under the License.
padding-bottom: 3px;
}
+.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge {
+ color: $warning-color;
+}
+
.mx_RightPanel_headerButton_highlight {
width: 25px;
height: 5px;
diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss
new file mode 100644
index 0000000000..370f82d9ab
--- /dev/null
+++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss
@@ -0,0 +1,41 @@
+/*
+Copyright 2018 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.
+*/
+
+.mx_NewRecoveryMethodDialog .mx_Dialog_title {
+ margin-bottom: 32px;
+}
+
+.mx_NewRecoveryMethodDialog_title {
+ position: relative;
+ padding-left: 45px;
+ padding-bottom: 10px;
+
+ &:before {
+ mask: url("../../../img/e2e/lock-warning.svg");
+ mask-repeat: no-repeat;
+ background-color: $primary-fg-color;
+ content: "";
+ position: absolute;
+ top: -6px;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+}
+
+.mx_NewRecoveryMethodDialog .mx_Dialog_buttons {
+ margin-top: 36px;
+}
diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss
index 4bb42ff114..e4e2d19b42 100644
--- a/res/css/views/rooms/_RoomRecoveryReminder.scss
+++ b/res/css/views/rooms/_RoomRecoveryReminder.scss
@@ -40,4 +40,5 @@ limitations under the License.
.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary {
@mixin mx_DialogButton_secondary;
+ background-color: transparent;
}
diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg
new file mode 100644
index 0000000000..a984ed85a0
--- /dev/null
+++ b/res/img/e2e/lock-warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/BasePlatform.js b/src/BasePlatform.js
index abc9aa0bed..79f0d69e2c 100644
--- a/src/BasePlatform.js
+++ b/src/BasePlatform.js
@@ -3,6 +3,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
+Copyright 2018 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.
@@ -105,11 +106,6 @@ export default class BasePlatform {
return "Not implemented";
}
- isElectron(): boolean { return false; }
-
- setupScreenSharingForIframe() {
- }
-
/**
* Restarts the application, without neccessarily reloading
* any application code
diff --git a/src/Notifier.js b/src/Notifier.js
index 80e8be1084..8550f3bf95 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -289,6 +289,11 @@ const Notifier = {
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
+ dis.dispatch({
+ action: "event_notification",
+ event: ev,
+ room: room,
+ });
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
}
diff --git a/src/Registration.js b/src/Registration.js
index f86c9cc618..98aee3ac83 100644
--- a/src/Registration.js
+++ b/src/Registration.js
@@ -26,6 +26,10 @@ import MatrixClientPeg from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
+// Regex for what a "safe" or "Matrix-looking" localpart would be.
+// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
+export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
+
/**
* Starts either the ILAG or full registration flow, depending
* on what the HS supports
diff --git a/src/Tinter.js b/src/Tinter.js
index d24a4c3e74..9c2afd4fab 100644
--- a/src/Tinter.js
+++ b/src/Tinter.js
@@ -390,7 +390,7 @@ class Tinter {
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.
- calcSvgFixups(svgs) {
+ calcSvgFixups(svgs, forceColors) {
// go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the
@@ -418,13 +418,21 @@ class Tinter {
const tag = tags[j];
for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = this.svgAttrs[k];
- for (let l = 0; l < this.keyHex.length; l++) {
- if (tag.getAttribute(attr) &&
- tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
+ for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please.
+ // We use a different attribute from the one we're setting
+ // because we may also be using forceColors. If we were to
+ // check the keyHex against a forceColors value, it may not
+ // match and therefore not change when we need it to.
+ const valAttrName = "mx-val-" + attr;
+ let attribute = tag.getAttribute(valAttrName);
+ if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original
+ if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) {
fixups.push({
node: tag,
attr: attr,
- index: l,
+ refAttr: valAttrName,
+ index: m,
+ forceColors: forceColors,
});
}
}
@@ -440,7 +448,9 @@ class Tinter {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
- svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
+ const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null;
+ svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]);
+ svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]);
}
if (DEBUG) console.log("applySvgFixups end");
}
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
index 6b115b890f..0db9d0699b 100644
--- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
@@ -92,25 +92,33 @@ export default React.createClass({
});
},
- _createBackup: function() {
+ _createBackup: async function() {
this.setState({
phase: PHASE_BACKINGUP,
error: null,
});
- this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion(
- this._keyBackupInfo,
- ).then((info) => {
- return MatrixClientPeg.get().backupAllGroupSessions(info.version);
- }).then(() => {
+ let info;
+ try {
+ info = await MatrixClientPeg.get().createKeyBackupVersion(
+ this._keyBackupInfo,
+ );
+ await MatrixClientPeg.get().backupAllGroupSessions(info.version);
this.setState({
phase: PHASE_DONE,
});
- }).catch(e => {
+ } catch (e) {
console.log("Error creating key backup", e);
+ // TODO: If creating a version succeeds, but backup fails, should we
+ // delete the version, disable backup, or do nothing? If we just
+ // disable without deleting, we'll enable on next app reload since
+ // it is trusted.
+ if (info) {
+ MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
+ }
this.setState({
error: e,
});
- });
+ }
},
_onCancel: function() {
diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js
new file mode 100644
index 0000000000..e88e0444bc
--- /dev/null
+++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js
@@ -0,0 +1,110 @@
+/*
+Copyright 2018 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 PropTypes from "prop-types";
+import sdk from "../../../../index";
+import MatrixClientPeg from '../../../../MatrixClientPeg';
+import dis from "../../../../dispatcher";
+import { _t } from "../../../../languageHandler";
+import Modal from "../../../../Modal";
+
+export default class NewRecoveryMethodDialog extends React.PureComponent {
+ static propTypes = {
+ onFinished: PropTypes.func.isRequired,
+ }
+
+ onGoToSettingsClick = () => {
+ this.props.onFinished();
+ dis.dispatch({ action: 'view_user_settings' });
+ }
+
+ onSetupClick = async() => {
+ // TODO: Should change to a restore key backup flow that checks the
+ // recovery passphrase while at the same time also cross-signing the
+ // device as well in a single flow. Since we don't have that yet, we'll
+ // look for an unverified device and verify it. Note that this means
+ // we won't restore keys yet; instead we'll only trust the backup for
+ // sending our own new keys to it.
+ let backupSigStatus;
+ try {
+ const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
+ backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
+ } catch (e) {
+ console.log("Unable to fetch key backup status", e);
+ return;
+ }
+
+ let unverifiedDevice;
+ for (const sig of backupSigStatus.sigs) {
+ if (!sig.device.isVerified()) {
+ unverifiedDevice = sig.device;
+ break;
+ }
+ }
+ if (!unverifiedDevice) {
+ console.log("Unable to find a device to verify.");
+ return;
+ }
+
+ const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
+ Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
+ userId: MatrixClientPeg.get().credentials.userId,
+ device: unverifiedDevice,
+ onFinished: this.props.onFinished,
+ });
+ }
+
+ render() {
+ const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
+ const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
+ const title =
+ {_t("New Recovery Method")}
+ ;
+
+ return (
+
+
+
{_t(
+ "A new recovery passphrase and key for Secure " +
+ "Messages has been detected.",
+ )}
+
{_t(
+ "Setting up Secure Messages on this device " +
+ "will re-encrypt this device's message history with " +
+ "the new recovery method.",
+ )}
+
{_t(
+ "If you didn't set the new recovery method, an " +
+ "attacker may be trying to access your account. " +
+ "Change your account password and set a new recovery " +
+ "method immediately in Settings.",
+ )}
-
+
{ this.props.isHighlighted ? : }
;
@@ -76,6 +86,7 @@ HeaderButton.propTypes = {
// The badge to display above the icon
badge: PropTypes.node,
+ badgeHighlight: PropTypes.bool,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
@@ -205,7 +216,10 @@ module.exports = React.createClass({
}, 500),
onAction: function(payload) {
- if (payload.action === "view_user") {
+ if (payload.action === "event_notification") {
+ // Try and re-caclulate any badge counts we might have
+ this.forceUpdate();
+ } else if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
@@ -308,6 +322,14 @@ module.exports = React.createClass({
let headerButtons = [];
if (this.props.roomId) {
+ let notifCountBadge;
+ let notifCount = 0;
+ MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0));
+ if (notifCount > 0) {
+ const title = _t("%(count)s Notifications", {count: formatCount(notifCount)});
+ notifCountBadge =
{ formatCount(notifCount) }
;
+ }
+
headerButtons = [
0}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 6f932d71e1..b9dbe345c5 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -188,9 +188,11 @@ module.exports = React.createClass({
phase: "UserSettings.LOADING", // LOADING, DISPLAY
email_add_pending: false,
vectorVersion: undefined,
+ canSelfUpdate: null,
rejectingInvites: false,
mediaDevices: null,
ignoredUsers: [],
+ autoLaunchEnabled: null,
};
},
@@ -209,6 +211,13 @@ module.exports = React.createClass({
}, (e) => {
console.log("Failed to fetch app version", e);
});
+
+ PlatformPeg.get().canSelfUpdate().then((canUpdate) => {
+ if (this._unmounted) return;
+ this.setState({
+ canSelfUpdate: canUpdate,
+ });
+ });
}
this._refreshMediaDevices();
@@ -227,11 +236,12 @@ module.exports = React.createClass({
});
this._refreshFromServer();
- if (PlatformPeg.get().isElectron()) {
- const {ipcRenderer} = require('electron');
-
- ipcRenderer.on('settings', this._electronSettings);
- ipcRenderer.send('settings_get');
+ if (PlatformPeg.get().supportsAutoLaunch()) {
+ PlatformPeg.get().getAutoLaunchEnabled().then(enabled => {
+ this.setState({
+ autoLaunchEnabled: enabled,
+ });
+ });
}
this.setState({
@@ -262,11 +272,6 @@ module.exports = React.createClass({
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
-
- if (PlatformPeg.get().isElectron()) {
- const {ipcRenderer} = require('electron');
- ipcRenderer.removeListener('settings', this._electronSettings);
- }
},
// `UserSettings` assumes that the client peg will not be null, so give it some
@@ -285,10 +290,6 @@ module.exports = React.createClass({
});
},
- _electronSettings: function(ev, settings) {
- this.setState({ electron_settings: settings });
- },
-
_refreshMediaDevices: function(stream) {
if (stream) {
// kill stream so that we don't leave it lingering around with webcam enabled etc
@@ -967,7 +968,7 @@ module.exports = React.createClass({
_renderCheckUpdate: function() {
const platform = PlatformPeg.get();
- if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
+ if (this.state.canSelfUpdate) {
return
{ _t('Updates') }
@@ -1012,8 +1013,7 @@ module.exports = React.createClass({
},
_renderElectronSettings: function() {
- const settings = this.state.electron_settings;
- if (!settings) return;
+ if (!PlatformPeg.get().supportsAutoLaunch()) return;
// TODO: This should probably be a granular setting, but it only applies to electron
// and ends up being get/set outside of matrix anyways (local system setting).
@@ -1023,7 +1023,7 @@ module.exports = React.createClass({
@@ -1033,8 +1033,11 @@ module.exports = React.createClass({
},
_onAutoLaunchChanged: function(e) {
- const {ipcRenderer} = require('electron');
- ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
+ PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => {
+ this.setState({
+ autoLaunchEnabled: e.target.checked,
+ });
+ });
},
_mapWebRtcDevicesToSpans: function(devices) {
@@ -1393,7 +1396,7 @@ module.exports = React.createClass({
{ this._renderBulkOptions() }
{ this._renderBugReport() }
- { PlatformPeg.get().isElectron() && this._renderElectronSettings() }
+ { this._renderElectronSettings() }
{ this._renderAnalyticsControl() }
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
index 444f391258..559136948a 100644
--- a/src/components/structures/login/ForgotPassword.js
+++ b/src/components/structures/login/ForgotPassword.js
@@ -36,6 +36,14 @@ module.exports = React.createClass({
onLoginClick: PropTypes.func,
onRegisterClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
+
+ // The default server name to use when the user hasn't specified
+ // one. This is used when displaying the defaultHsUrl in the UI.
+ defaultServerName: PropTypes.string,
+
+ // An error passed along from higher up explaining that something
+ // went wrong when finding the defaultHsUrl.
+ defaultServerDiscoveryError: PropTypes.string,
},
getInitialState: function() {
@@ -45,6 +53,7 @@ module.exports = React.createClass({
progress: null,
password: null,
password2: null,
+ errorText: null,
};
},
@@ -81,6 +90,13 @@ module.exports = React.createClass({
onSubmitForm: function(ev) {
ev.preventDefault();
+ // Don't allow the user to register if there's a discovery error
+ // Without this, the user could end up registering on the wrong homeserver.
+ if (this.props.defaultServerDiscoveryError) {
+ this.setState({errorText: this.props.defaultServerDiscoveryError});
+ return;
+ }
+
if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) {
@@ -200,6 +216,12 @@ module.exports = React.createClass({
);
}
+ let errorText = null;
+ const err = this.state.errorText || this.props.defaultServerDiscoveryError;
+ if (err) {
+ errorText =
{ err }
;
+ }
+
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
resetPasswordJsx = (
@@ -230,6 +252,7 @@ module.exports = React.createClass({
{ serverConfigSection }
+ { errorText }
{ _t('Return to login screen') }
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index bd18699dd1..b94a1759cf 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -26,11 +26,17 @@ import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import request from 'browser-request';
+import { AutoDiscovery } from "matrix-js-sdk";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
+// These are used in several places, and come from the js-sdk's autodiscovery
+// stuff. We define them here so that they'll be picked up by i18n.
+_td("Invalid homeserver discovery response");
+_td("Invalid identity server discovery response");
+_td("General failure");
+
/**
* A wire component which glues together login UI components and Login logic
*/
@@ -51,6 +57,14 @@ module.exports = React.createClass({
// different home server without confusing users.
fallbackHsUrl: PropTypes.string,
+ // The default server name to use when the user hasn't specified
+ // one. This is used when displaying the defaultHsUrl in the UI.
+ defaultServerName: PropTypes.string,
+
+ // An error passed along from higher up explaining that something
+ // went wrong when finding the defaultHsUrl.
+ defaultServerDiscoveryError: PropTypes.string,
+
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done.
@@ -80,6 +94,7 @@ module.exports = React.createClass({
discoveredHsUrl: "",
discoveredIsUrl: "",
discoveryError: "",
+ findingHomeserver: false,
};
},
@@ -113,7 +128,7 @@ module.exports = React.createClass({
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver
// discovery went wrong
- if (this.state.discoveryError) return;
+ if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
this.setState({
busy: true,
@@ -285,119 +300,56 @@ module.exports = React.createClass({
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
- this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
+ this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false});
return;
}
+ this.setState({findingHomeserver: true});
try {
- const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`);
- if (!wellknown["m.homeserver"]) {
- console.error("No m.homeserver key in well-known response");
- this.setState({discoveryError: _t("Invalid homeserver discovery response")});
- return;
+ const discovery = await AutoDiscovery.findClientConfig(serverName);
+ const state = discovery["m.homeserver"].state;
+ if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
+ this.setState({
+ discoveredHsUrl: "",
+ discoveredIsUrl: "",
+ discoveryError: discovery["m.homeserver"].error,
+ findingHomeserver: false,
+ });
+ } else if (state === AutoDiscovery.PROMPT) {
+ this.setState({
+ discoveredHsUrl: "",
+ discoveredIsUrl: "",
+ discoveryError: "",
+ findingHomeserver: false,
+ });
+ } else if (state === AutoDiscovery.SUCCESS) {
+ this.setState({
+ discoveredHsUrl: discovery["m.homeserver"].base_url,
+ discoveredIsUrl:
+ discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
+ ? discovery["m.identity_server"].base_url
+ : "",
+ discoveryError: "",
+ findingHomeserver: false,
+ });
+ } else {
+ console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
+ this.setState({
+ discoveredHsUrl: "",
+ discoveredIsUrl: "",
+ discoveryError: _t("Unknown failure discovering homeserver"),
+ findingHomeserver: false,
+ });
}
-
- const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
- if (!hsUrl) {
- console.error("Invalid base_url for m.homeserver");
- this.setState({discoveryError: _t("Invalid homeserver discovery response")});
- return;
- }
-
- console.log("Verifying homeserver URL: " + hsUrl);
- const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`);
- if (!hsVersions["versions"]) {
- console.error("Invalid /versions response");
- this.setState({discoveryError: _t("Invalid homeserver discovery response")});
- return;
- }
-
- let isUrl = "";
- if (wellknown["m.identity_server"]) {
- isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
- if (!isUrl) {
- console.error("Invalid base_url for m.identity_server");
- this.setState({discoveryError: _t("Invalid homeserver discovery response")});
- return;
- }
-
- console.log("Verifying identity server URL: " + isUrl);
- const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
- if (!isResponse) {
- console.error("Invalid /api/v1 response");
- this.setState({discoveryError: _t("Invalid homeserver discovery response")});
- return;
- }
- }
-
- this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""});
} catch (e) {
console.error(e);
- if (e.wkAction) {
- if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") {
- // We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user
- // submit their details to the wrong homeserver. In practice, the custom
- // server options will show up to try and guide the user into entering
- // the required information.
- this.setState({discoveryError: _t("Cannot find homeserver")});
- return;
- } else if (e.wkAction === "IGNORE") {
- // Nothing to discover
- this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
- return;
- }
- }
-
- throw e;
+ this.setState({
+ findingHomeserver: false,
+ discoveryError: _t("Unknown error discovering homeserver"),
+ });
}
},
- _sanitizeWellKnownUrl: function(url) {
- if (!url) return false;
-
- const parser = document.createElement('a');
- parser.href = url;
-
- if (parser.protocol !== "http:" && parser.protocol !== "https:") return false;
- if (!parser.hostname) return false;
-
- const port = parser.port ? `:${parser.port}` : "";
- const path = parser.pathname ? parser.pathname : "";
- let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`;
- if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1);
- return saferUrl;
- },
-
- _getWellKnownObject: function(url) {
- return new Promise(function(resolve, reject) {
- request(
- { method: "GET", url: url },
- (err, response, body) => {
- if (err || response.status < 200 || response.status >= 300) {
- let action = "FAIL_ERROR";
- if (response.status === 404) {
- // We could just resolve with an empty object, but that
- // causes a different series of branches when the m.homeserver
- // bit of the JSON is missing.
- action = "IGNORE";
- }
- reject({err: err, response: response, wkAction: action});
- return;
- }
-
- try {
- resolve(JSON.parse(body));
- } catch (e) {
- console.error(e);
- if (e.name === "SyntaxError") {
- reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"});
- } else throw e;
- }
- },
- );
- });
- },
-
_initLoginLogic: function(hsUrl, isUrl) {
const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
@@ -541,6 +493,8 @@ module.exports = React.createClass({
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl}
+ hsName={this.props.defaultServerName}
+ disableSubmit={this.state.findingHomeserver}
/>
);
},
@@ -559,7 +513,7 @@ module.exports = React.createClass({
const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ?
;
}
}
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index abc52f7b1d..cbe80763a6 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
+import * as Email from "../../../email";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -419,6 +420,10 @@ module.exports = React.createClass({
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (this.props.validAddressTypes.includes(addrType)) {
+ if (addrType === 'email' && !Email.looksValid(query)) {
+ this.setState({searchError: _t("That doesn't look like a valid email address")});
+ return;
+ }
suggestedList.unshift({
addressType: addrType,
address: query,
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 8ec417a59b..3e9052cc34 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -57,8 +57,7 @@ export default React.createClass({
className: PropTypes.string,
// Title for the dialog.
- // (could probably actually be something more complicated than a string if desired)
- title: PropTypes.string.isRequired,
+ title: PropTypes.node.isRequired,
// children should be the content of the dialog
children: PropTypes.node,
diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js
index fb892c4a0a..222a2c35fe 100644
--- a/src/components/views/dialogs/SetMxIdDialog.js
+++ b/src/components/views/dialogs/SetMxIdDialog.js
@@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import { KeyCode } from '../../../Keyboard';
import { _t } from '../../../languageHandler';
+import { SAFE_LOCALPART_REGEX } from '../../../Registration';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
@@ -110,12 +111,11 @@ export default React.createClass({
},
_doUsernameCheck: function() {
- // XXX: SPEC-1
- // Check if username is valid
- // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
- if (encodeURIComponent(this.state.username) !== this.state.username) {
+ // We do a quick check ahead of the username availability API to ensure the
+ // user ID roughly looks okay from a Matrix perspective.
+ if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
this.setState({
- usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
+ usernameError: _t("Only use lower case letters, numbers and '=_-./'"),
});
return Promise.reject();
}
@@ -210,7 +210,6 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
- const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
@@ -230,9 +229,8 @@ export default React.createClass({
});
let usernameIndicator = null;
- let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
- usernameBusyIndicator = ;
+ usernameIndicator =
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 23b24adbb4..f4f929a3c2 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -22,7 +22,6 @@ import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
-import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton';
@@ -49,7 +48,6 @@ export default class AppTile extends React.Component {
this.state = this._getNewState(props);
this._onAction = this._onAction.bind(this);
- this._onMessage = this._onMessage.bind(this);
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
@@ -143,10 +141,6 @@ export default class AppTile extends React.Component {
}
componentDidMount() {
- // Legacy Jitsi widget messaging -- TODO replace this with standard widget
- // postMessaging API
- window.addEventListener('message', this._onMessage, false);
-
// Widget action listeners
this.dispatcherRef = dis.register(this._onAction);
}
@@ -155,9 +149,6 @@ export default class AppTile extends React.Component {
// Widget action listeners
dis.unregister(this.dispatcherRef);
- // Jitsi listener
- window.removeEventListener('message', this._onMessage);
-
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget();
@@ -233,32 +224,6 @@ export default class AppTile extends React.Component {
}
}
- // Legacy Jitsi widget messaging
- // TODO -- This should be replaced with the new widget postMessaging API
- _onMessage(event) {
- if (this.props.type !== 'jitsi') {
- return;
- }
- if (!event.origin) {
- event.origin = event.originalEvent.origin;
- }
-
- const widgetUrlObj = url.parse(this.state.widgetUrl);
- const eventOrigin = url.parse(event.origin);
- if (
- eventOrigin.protocol !== widgetUrlObj.protocol ||
- eventOrigin.host !== widgetUrlObj.host
- ) {
- return;
- }
-
- if (event.data.widgetAction === 'jitsi_iframe_loaded') {
- const iframe = this.refs.appFrame.contentWindow
- .document.querySelector('iframe[id^="jitsiConferenceFrame"]');
- PlatformPeg.get().setupScreenSharingForIframe(iframe);
- }
- }
-
_canUserModify() {
// User widgets should always be modifiable by their creator
if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) {
@@ -544,7 +509,7 @@ export default class AppTile extends React.Component {
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
- const iframeFeatures = "microphone; camera; encrypted-media;";
+ const iframeFeatures = "microphone; camera; encrypted-media; autoplay;";
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js
index e04bf87793..08628c8ca9 100644
--- a/src/components/views/elements/TintableSvg.js
+++ b/src/components/views/elements/TintableSvg.js
@@ -29,6 +29,7 @@ var TintableSvg = React.createClass({
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
+ forceColors: PropTypes.arrayOf(PropTypes.string),
},
statics: {
@@ -50,6 +51,12 @@ var TintableSvg = React.createClass({
delete TintableSvg.mounts[this.id];
},
+ componentDidUpdate: function(prevProps, prevState) {
+ if (prevProps.forceColors !== this.props.forceColors) {
+ this.calcAndApplyFixups(this.refs.svgContainer);
+ }
+ },
+
tint: function() {
// TODO: only bother running this if the global tint settings have changed
// since we loaded!
@@ -57,8 +64,13 @@ var TintableSvg = React.createClass({
},
onLoad: function(event) {
- // console.log("TintableSvg.onLoad for " + this.props.src);
- this.fixups = Tinter.calcSvgFixups([event.target]);
+ this.calcAndApplyFixups(event.target);
+ },
+
+ calcAndApplyFixups: function(target) {
+ if (!target) return;
+ // console.log("TintableSvg.calcAndApplyFixups for " + this.props.src);
+ this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors);
Tinter.applySvgFixups(this.fixups);
},
@@ -71,6 +83,7 @@ var TintableSvg = React.createClass({
height={this.props.height}
onLoad={this.onLoad}
tabIndex="-1"
+ ref="svgContainer"
/>
);
},
diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js
index 6a5577fb62..59d4db379c 100644
--- a/src/components/views/login/PasswordLogin.js
+++ b/src/components/views/login/PasswordLogin.js
@@ -40,6 +40,8 @@ class PasswordLogin extends React.Component {
initialPassword: "",
loginIncorrect: false,
hsDomain: "",
+ hsName: null,
+ disableSubmit: false,
}
constructor(props) {
@@ -250,13 +252,15 @@ class PasswordLogin extends React.Component {
);
}
- let matrixIdText = '';
- if (this.props.hsUrl) {
+ let matrixIdText = _t('Matrix ID');
+ if (this.props.hsName) {
+ matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName});
+ } else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
} catch (e) {
- // pass
+ // ignore
}
}
@@ -288,6 +292,8 @@ class PasswordLogin extends React.Component {
);
}
+ const disableSubmit = this.props.disableSubmit || matrixIdText === '';
+
return (
);
@@ -325,6 +331,8 @@ PasswordLogin.propTypes = {
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
+ hsName: PropTypes.string,
+ disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js
index fe977025ae..137aeada91 100644
--- a/src/components/views/login/RegistrationForm.js
+++ b/src/components/views/login/RegistrationForm.js
@@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
-import SettingsStore from "../../../settings/SettingsStore";
+import { SAFE_LOCALPART_REGEX } from '../../../Registration';
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
@@ -194,9 +194,8 @@ module.exports = React.createClass({
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
case FIELD_USERNAME:
- // XXX: SPEC-1
- var username = this.refs.username.value.trim();
- if (encodeURIComponent(username) != username) {
+ const username = this.refs.username.value.trim();
+ if (!SAFE_LOCALPART_REGEX.test(username)) {
this.markFieldValid(
field_id,
false,
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index de5d3db625..f68670b2f9 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -130,7 +130,7 @@ module.exports = React.createClass({
},
isAliasValid: function(alias) {
- // XXX: FIXME SPEC-1
+ // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias);
},
diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js
index b08f4d0e78..03b98d28a0 100644
--- a/src/components/views/settings/KeyBackupPanel.js
+++ b/src/components/views/settings/KeyBackupPanel.js
@@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component {
}
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
+ const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
const sigStatusSubstitutions = {
validity: sub =>
@@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component {
{sub}
,
- device: sub => {sig.device.getDisplayName()},
+ device: sub => {deviceName},
};
let sigStatus;
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
@@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component {
} else if (sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has a valid signature from " +
- "verified device x",
+ "verified device ",
{}, sigStatusSubstitutions,
);
} else if (sig.valid && !sig.device.isVerified()) {
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index 72ad2943aa..40c43e6b2e 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -483,8 +483,11 @@ module.exports = React.createClass({
// The default push rules displayed by Vector UI
'.m.rule.contains_display_name': 'vector',
'.m.rule.contains_user_name': 'vector',
+ '.m.rule.roomnotif': 'vector',
'.m.rule.room_one_to_one': 'vector',
+ '.m.rule.encrypted_room_one_to_one': 'vector',
'.m.rule.message': 'vector',
+ '.m.rule.encrypted': 'vector',
'.m.rule.invite_for_me': 'vector',
//'.m.rule.member_event': 'vector',
'.m.rule.call': 'vector',
@@ -534,9 +537,12 @@ module.exports = React.createClass({
const vectorRuleIds = [
'.m.rule.contains_display_name',
'.m.rule.contains_user_name',
+ '.m.rule.roomnotif',
'_keywords',
'.m.rule.room_one_to_one',
+ '.m.rule.encrypted_room_one_to_one',
'.m.rule.message',
+ '.m.rule.encrypted',
'.m.rule.invite_for_me',
//'im.vector.rule.member_event',
'.m.rule.call',
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0bd1858b90..f723d54746 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -296,8 +296,11 @@
"Waiting for response from server": "Waiting for response from server",
"Messages containing my display name": "Messages containing my display name",
"Messages containing my user name": "Messages containing my user name",
+ "Messages containing @room": "Messages containing @room",
"Messages in one-to-one chats": "Messages in one-to-one chats",
+ "Encrypted messages in one-to-one chats": "Encrypted messages in one-to-one chats",
"Messages in group chats": "Messages in group chats",
+ "Encrypted messages in group chats": "Encrypted messages in group chats",
"When I'm invited to a room": "When I'm invited to a room",
"Call invitation": "Call invitation",
"Messages sent by bot": "Messages sent by bot",
@@ -349,7 +352,7 @@
"This device is uploading keys to this backup": "This device is uploading keys to this backup",
"This device is not uploading keys to this backup": "This device is not uploading keys to this backup",
"Backup has a valid signature from this device": "Backup has a valid signature from this device",
- "Backup has a valid signature from verified device x": "Backup has a valid signature from verified device x",
+ "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ",
"Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ",
"Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ",
"Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ",
@@ -722,6 +725,7 @@
"User name": "User name",
"Mobile phone number": "Mobile phone number",
"Forgot your password?": "Forgot your password?",
+ "Matrix ID": "Matrix ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Sign in with": "Sign in with",
"Email address": "Email address",
@@ -869,9 +873,9 @@
"And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User",
- "Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
+ "That doesn't look like a valid email address": "That doesn't look like a valid email address",
"You have entered an invalid address.": "You have entered an invalid address.",
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
"Preparing to send logs": "Preparing to send logs",
@@ -982,10 +986,11 @@
"Unable to verify email address.": "Unable to verify email address.",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip",
- "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.",
+ "Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'",
"Username not available": "Username not available",
"Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
"An error occurred: %(error_string)s": "An error occurred: %(error_string)s",
+ "Checking...": "Checking...",
"Username available": "Username available",
"To get started, please pick a username!": "To get started, please pick a username!",
"This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.",
@@ -1116,6 +1121,7 @@
"You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.",
"If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.",
"Login": "Login",
+ "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
"Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@@ -1129,6 +1135,7 @@
"Review terms and conditions": "Review terms and conditions",
"Old cryptography data detected": "Old cryptography data detected",
"Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
+ "Unknown error discovering homeserver": "Unknown error discovering homeserver",
"Logout": "Logout",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!",
@@ -1141,6 +1148,8 @@
"%(count)s Members|other": "%(count)s Members",
"%(count)s Members|one": "%(count)s Member",
"Invite to this room": "Invite to this room",
+ "%(count)s Notifications|other": "%(count)s Notifications",
+ "%(count)s Notifications|one": "%(count)s Notification",
"Files": "Files",
"Notifications": "Notifications",
"Hide panel": "Hide panel",
@@ -1289,6 +1298,9 @@
"Confirm your new password": "Confirm your new password",
"Send Reset Email": "Send Reset Email",
"Create an account": "Create an account",
+ "Invalid homeserver discovery response": "Invalid homeserver discovery response",
+ "Invalid identity server discovery response": "Invalid identity server discovery response",
+ "General failure": "General failure",
"This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
"Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.",
"Incorrect username and/or password.": "Incorrect username and/or password.",
@@ -1296,8 +1308,7 @@
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
"The phone number entered looks invalid": "The phone number entered looks invalid",
- "Invalid homeserver discovery response": "Invalid homeserver discovery response",
- "Cannot find homeserver": "Cannot find homeserver",
+ "Unknown failure discovering homeserver": "Unknown failure discovering homeserver",
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.",
@@ -1393,6 +1404,12 @@
"Retry": "Retry",
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
+ "New Recovery Method": "New Recovery Method",
+ "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.",
+ "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.",
+ "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
+ "Set up Secure Messages": "Set up Secure Messages",
+ "Go to Settings": "Go to Settings",
"Failed to set direct chat tag": "Failed to set direct chat tag",
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js
index 30d6ea5975..15f645d5f7 100644
--- a/src/notifications/StandardActions.js
+++ b/src/notifications/StandardActions.js
@@ -24,6 +24,7 @@ module.exports = {
ACTION_NOTIFY: encodeActions({notify: true}),
ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}),
ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}),
+ ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}),
ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}),
ACTION_DONT_NOTIFY: encodeActions({notify: false}),
ACTION_DISABLED: null,
diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js
index eeb193cb8a..3df2e70774 100644
--- a/src/notifications/VectorPushRulesDefinitions.js
+++ b/src/notifications/VectorPushRulesDefinitions.js
@@ -20,6 +20,7 @@ import { _td } from '../languageHandler';
const StandardActions = require('./StandardActions');
const PushRuleVectorState = require('./PushRuleVectorState');
+const { decodeActions } = require('./NotificationUtils');
class VectorPushRuleDefinition {
constructor(opts) {
@@ -31,13 +32,11 @@ class VectorPushRuleDefinition {
// Translate the rule actions and its enabled value into vector state
ruleToVectorState(rule) {
let enabled = false;
- let actions = null;
if (rule) {
enabled = rule.enabled;
- actions = rule.actions;
}
- for (const stateKey in PushRuleVectorState.states) {
+ for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in
const state = PushRuleVectorState.states[stateKey];
const vectorStateToActions = this.vectorStateToActions[state];
@@ -47,15 +46,21 @@ class VectorPushRuleDefinition {
return state;
}
} else {
- // The actions must match to the ones expected by vector state
- if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) {
+ // The actions must match to the ones expected by vector state.
+ // Use `decodeActions` on both sides to canonicalize things like
+ // value: true vs. unspecified for highlight (which defaults to
+ // true, making them equivalent).
+ if (enabled &&
+ JSON.stringify(decodeActions(rule.actions)) ===
+ JSON.stringify(decodeActions(vectorStateToActions))) {
return state;
}
}
}
- console.error("Cannot translate rule actions into Vector rule state. Rule: " +
- JSON.stringify(rule));
+ console.error(`Cannot translate rule actions into Vector rule state. ` +
+ `Rule: ${JSON.stringify(rule)}, ` +
+ `Expected: ${JSON.stringify(this.vectorStateToActions)}`);
return undefined;
}
}
@@ -86,6 +91,17 @@ module.exports = {
},
}),
+ // Messages containing @room
+ ".m.rule.roomnotif": new VectorPushRuleDefinition({
+ kind: "override",
+ description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js
+ vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
+ on: StandardActions.ACTION_NOTIFY,
+ loud: StandardActions.ACTION_HIGHLIGHT,
+ off: StandardActions.ACTION_DISABLED,
+ },
+ }),
+
// Messages just sent to the user in a 1:1 room
".m.rule.room_one_to_one": new VectorPushRuleDefinition({
kind: "underride",
@@ -97,6 +113,17 @@ module.exports = {
},
}),
+ // Encrypted messages just sent to the user in a 1:1 room
+ ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({
+ kind: "underride",
+ description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
+ vectorStateToActions: {
+ on: StandardActions.ACTION_NOTIFY,
+ loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
+ off: StandardActions.ACTION_DONT_NOTIFY,
+ },
+ }),
+
// Messages just sent to a group chat room
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms.
@@ -110,6 +137,19 @@ module.exports = {
},
}),
+ // Encrypted messages just sent to a group chat room
+ // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
+ // By opposition, all other room messages are from group chat rooms.
+ ".m.rule.encrypted": new VectorPushRuleDefinition({
+ kind: "underride",
+ description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
+ vectorStateToActions: {
+ on: StandardActions.ACTION_NOTIFY,
+ loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
+ off: StandardActions.ACTION_DONT_NOTIFY,
+ },
+ }),
+
// Invitation for the user
".m.rule.invite_for_me": new VectorPushRuleDefinition({
kind: "underride",