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 579856f880..5633b7ec8f 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -24,8 +24,10 @@
@import "./structures/_ViewSource.scss";
@import "./structures/login/_Login.scss";
@import "./views/avatars/_BaseAvatar.scss";
+@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";
+@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@@ -33,7 +35,6 @@
@import "./views/dialogs/_ChatInviteDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
-@import "./views/dialogs/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss";
@@ -47,6 +48,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/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss
similarity index 74%
rename from res/css/views/dialogs/_CreateKeyBackupDialog.scss
rename to res/css/views/avatars/_MemberStatusMessageAvatar.scss
index a422cf858c..c857b9807b 100644
--- a/res/css/views/dialogs/_CreateKeyBackupDialog.scss
+++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss
@@ -14,12 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_CreateKeyBackupDialog {
- padding-right: 40px;
-}
-
-.mx_CreateKeyBackupDialog_recoveryKey {
- padding: 20px;
- color: $info-plinth-fg-color;
- background-color: $info-plinth-bg-color;
+.mx_MemberStatusMessageAvatar_hasStatus {
+ border: 2px solid $accent-color;
+ border-radius: 40px;
}
diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss
new file mode 100644
index 0000000000..873ad99495
--- /dev/null
+++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss
@@ -0,0 +1,55 @@
+/*
+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_StatusMessageContextMenu_message {
+ display: inline-block;
+ border-radius: 3px 0 0 3px;
+ border: 1px solid $input-border-color;
+ font-size: 13px;
+ padding: 7px 7px 7px 9px;
+ width: 135px;
+ background-color: $primary-bg-color !important;
+}
+
+.mx_StatusMessageContextMenu_submit {
+ display: inline-block;
+}
+
+.mx_StatusMessageContextMenu_submitFaded {
+ opacity: 0.5;
+}
+
+.mx_StatusMessageContextMenu_submit img {
+ vertical-align: middle;
+ margin-left: 8px;
+}
+
+.mx_StatusMessageContextMenu hr {
+ border: 0.5px solid $menu-border-color;
+}
+
+.mx_StatusMessageContextMenu_clearIcon {
+ margin: 5px 15px 5px 5px;
+ vertical-align: middle;
+}
+
+.mx_StatusMessageContextMenu_clear {
+ padding: 2px;
+}
+
+.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear {
+ color: $warning-color;
+}
diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
index 2cb6b11c0c..424ffbd0a8 100644
--- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
+++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
@@ -13,7 +13,11 @@ 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_CreateKeyBackupDialog {
+ padding-right: 40px;
+}
+
.mx_CreateKeyBackupDialog_primaryContainer {
/*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/
padding: 20px
@@ -25,9 +29,13 @@ limitations under the License.
display: block;
}
+.mx_CreateKeyBackupDialog_passPhraseContainer {
+ display: flex;
+ align-items: start;
+}
+
.mx_CreateKeyBackupDialog_passPhraseHelp {
- float: right;
- width: 230px;
+ flex: 1;
height: 85px;
margin-left: 20px;
font-size: 80%;
@@ -38,20 +46,36 @@ limitations under the License.
}
.mx_CreateKeyBackupDialog_passPhraseInput {
+ flex: none;
width: 250px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
+ margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_passPhraseMatch {
- float: right;
+ margin-left: 20px;
}
-.mx_CreateKeyBackupDialog_recoveryKeyButtons {
- float: right;
+.mx_CreateKeyBackupDialog_recoveryKeyHeader {
+ margin-bottom: 1em;
+}
+
+.mx_CreateKeyBackupDialog_recoveryKeyContainer {
+ display: flex;
}
.mx_CreateKeyBackupDialog_recoveryKey {
- width: 300px;
+ width: 262px;
+ padding: 20px;
+ color: $info-plinth-fg-color;
+ background-color: $info-plinth-bg-color;
+ margin-right: 12px;
+}
+
+.mx_CreateKeyBackupDialog_recoveryKeyButtons {
+ flex: 1;
+ display: flex;
+ align-items: center;
}
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/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss
index 031894afde..90d5dc9aa5 100644
--- a/res/css/views/rooms/_EntityTile.scss
+++ b/res/css/views/rooms/_EntityTile.scss
@@ -111,4 +111,12 @@ limitations under the License.
opacity: 0.25;
}
+.mx_EntityTile_subtext {
+ font-size: 11px;
+ opacity: 0.5;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: clip;
+}
+
diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss
index 5d47275efe..2270e83743 100644
--- a/res/css/views/rooms/_MemberInfo.scss
+++ b/res/css/views/rooms/_MemberInfo.scss
@@ -110,3 +110,10 @@ limitations under the License.
margin-left: 8px;
}
+.mx_MemberInfo_statusMessage {
+ font-size: 11px;
+ opacity: 0.5;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: clip;
+}
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index ccd3afe26c..b5ac9aadc6 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -35,7 +35,19 @@ limitations under the License.
.mx_RoomTile_nameContainer {
display: inline-block;
width: 180px;
- height: 24px;
+ vertical-align: middle;
+}
+
+.mx_RoomTile_subtext {
+ display: inline-block;
+ font-size: 11px;
+ padding: 0 0 0 7px;
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: clip;
+ position: relative;
+ bottom: 4px;
}
.mx_RoomTile_avatar_container {
@@ -49,10 +61,14 @@ limitations under the License.
padding-left: 16px;
padding-right: 6px;
width: 24px;
- height: 24px;
vertical-align: middle;
}
+.mx_RoomTile_hasSubtext .mx_RoomTile_avatar {
+ padding-top: 0;
+ vertical-align: super;
+}
+
.mx_RoomTile_dm {
display: block;
position: absolute;
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/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg
new file mode 100644
index 0000000000..3c5392003d
--- /dev/null
+++ b/res/img/icons-checkmark.svg
@@ -0,0 +1,17 @@
+
+
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/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
index 6b115b890f..a097e84cdb 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() {
@@ -231,17 +239,19 @@ export default React.createClass({
{_t("You'll need it if you log out or lose access to this device.")}
-
- {strengthMeter}
- {helpText}
+
+
+
+ {strengthMeter}
+ {helpText}
+
-
- {passPhraseMatch}
-
-
+
+
+
+
+ {passPhraseMatch}
{_t("Make a copy of this Recovery Key and keep it safe.")}
{bodyText}
-
{_t("Your Recovery Key")}
-
-
- {
- // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design
- }
-
-
+
+ {_t("Your Recovery Key")}
-
- {this._keyBackupInfo.recovery_key}
+
+
+ {this._keyBackupInfo.recovery_key}
+
+
+
+
+
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.",
+ )}
{ _t('Who can join this community?') }
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 4517304453..fd95276445 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -1430,6 +1430,11 @@ export default React.createClass({
break;
}
});
+ cli.on("crypto.keyBackupFailed", () => {
+ Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
+ import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
+ );
+ });
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
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/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js
new file mode 100644
index 0000000000..aebd1741b7
--- /dev/null
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.js
@@ -0,0 +1,120 @@
+/*
+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 MatrixClientPeg from '../../../MatrixClientPeg';
+import AccessibleButton from '../elements/AccessibleButton';
+import MemberAvatar from '../avatars/MemberAvatar';
+import classNames from 'classnames';
+import * as ContextualMenu from "../../structures/ContextualMenu";
+import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
+import SettingsStore from "../../../settings/SettingsStore";
+
+export default class MemberStatusMessageAvatar extends React.Component {
+ static propTypes = {
+ member: PropTypes.object.isRequired,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ resizeMethod: PropTypes.string,
+ };
+
+ static defaultProps = {
+ width: 40,
+ height: 40,
+ resizeMethod: 'crop',
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ componentWillMount() {
+ if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
+ throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
+ }
+ }
+
+ componentDidMount() {
+ MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
+
+ if (this.props.member.user) {
+ this.setState({message: this.props.member.user._unstable_statusMessage});
+ } else {
+ this.setState({message: ""});
+ }
+ }
+
+ componentWillUnmount() {
+ if (MatrixClientPeg.get()) {
+ MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
+ }
+ }
+
+ _onRoomStateEvents = (ev, state) => {
+ if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return;
+ if (ev.getType() !== "im.vector.user_status") return;
+ // TODO: We should be relying on `this.props.member.user._unstable_statusMessage`
+ // We don't currently because the js-sdk doesn't emit a specific event for this
+ // change, and we don't want to race it. This should be improved when we rip out
+ // the im.vector.user_status stuff and replace it with a complete solution.
+ this.setState({message: ev.getContent()["status"]});
+ };
+
+ _onClick = (e) => {
+ e.stopPropagation();
+
+ const elementRect = e.target.getBoundingClientRect();
+
+ // The window X and Y offsets are to adjust position when zoomed in to page
+ const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
+ const chevronOffset = 12;
+ let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
+ y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
+
+ ContextualMenu.createMenu(StatusMessageContextMenu, {
+ chevronOffset: chevronOffset,
+ chevronFace: 'bottom',
+ left: x,
+ top: y,
+ menuWidth: 190,
+ user: this.props.member.user,
+ });
+ };
+
+ render() {
+ if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
+ return ;
+ }
+
+ const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
+
+ const classes = classNames({
+ "mx_MemberStatusMessageAvatar": true,
+ "mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
+ });
+
+ return
+
+ ;
+ }
+}
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js
new file mode 100644
index 0000000000..f07220db44
--- /dev/null
+++ b/src/components/views/context_menus/StatusMessageContextMenu.js
@@ -0,0 +1,86 @@
+/*
+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 { _t } from '../../../languageHandler';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import AccessibleButton from '../elements/AccessibleButton';
+import classNames from 'classnames';
+
+export default class StatusMessageContextMenu extends React.Component {
+ static propTypes = {
+ // js-sdk User object. Not required because it might not exist.
+ user: PropTypes.object,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ message: props.user ? props.user._unstable_statusMessage : "",
+ };
+ }
+
+ _onClearClick = async(e) => {
+ await MatrixClientPeg.get()._unstable_setStatusMessage("");
+ this.setState({message: ""});
+ };
+
+ _onSubmit = (e) => {
+ e.preventDefault();
+ MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
+ };
+
+ _onStatusChange = (e) => {
+ this.setState({message: e.target.value});
+ };
+
+ render() {
+ const formSubmitClasses = classNames({
+ "mx_StatusMessageContextMenu_submit": true,
+ "mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded
+ });
+
+ const form = ;
+
+ const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg";
+ const clearButton =
+
+ {_t("Clear status")}
+ ;
+
+ const menuClasses = classNames({
+ "mx_StatusMessageContextMenu": true,
+ "mx_StatusMessageContextMenu_hasStatus": this.state.message,
+ });
+
+ return
+ { form }
+
+ { clearButton }
+
;
+ }
+}
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/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js
index b93678b2ab..3c9414fd88 100644
--- a/src/components/views/dialogs/ChangelogDialog.js
+++ b/src/components/views/dialogs/ChangelogDialog.js
@@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component {
for (let i=0; i {
- if (body == null) return;
+ const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
+ request(url, (err, response, body) => {
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ this.setState({ [REPOS[i]]: response.statusText });
+ return;
+ }
this.setState({[REPOS[i]]: JSON.parse(body).commits});
});
}
@@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
const logs = REPOS.map(repo => {
- if (this.state[repo] == null) return ;
+ let content;
+ if (this.state[repo] == null) {
+ content = ;
+ } else if (typeof this.state[repo] === "string") {
+ content = _t("Unable to load commit detail: %(msg)s", {
+ msg: this.state[repo],
+ });
+ } else {
+ content = this.state[repo].map(this._elementsForCommit);
+ }
return (
{ /* { incomingCallBox } */ }
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/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 00f781ea5b..81fcd963b3 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -250,11 +250,14 @@
"A word by itself is easy to guess": "A word by itself is easy to guess",
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
+ "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
+ "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"Failed to join room": "Failed to join room",
"Message Pinning": "Message Pinning",
+ "Custom user status messages": "Custom user status messages",
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
"Backup of encryption keys to server": "Backup of encryption keys to server",
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
@@ -351,7 +354,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 ",
@@ -887,6 +890,7 @@
"What GitHub issue are these logs for?": "What GitHub issue are these logs for?",
"Notes:": "Notes:",
"Send logs": "Send logs",
+ "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
"Unavailable": "Unavailable",
"Changelog": "Changelog",
"Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one",
@@ -1058,6 +1062,8 @@
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
+ "Set a new status...": "Set a new status...",
+ "Clear status": "Clear status",
"View Community": "View Community",
"Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
@@ -1401,6 +1407,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/settings/Settings.js b/src/settings/Settings.js
index c9a4ecdebe..1cac8559d1 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -83,6 +83,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_custom_status": {
+ isFeature: true,
+ displayName: _td("Custom user status messages"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_lazyloading": {
isFeature: true,
displayName: _td("Increase performance by only loading room members on first view"),
diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js
index e4bbec1637..545686cdb6 100644
--- a/src/utils/PasswordScorer.js
+++ b/src/utils/PasswordScorer.js
@@ -52,6 +52,8 @@ _td("This is similar to a commonly used password");
_td("A word by itself is easy to guess");
_td("Names and surnames by themselves are easy to guess");
_td("Common names and surnames are easy to guess");
+_td("Straight rows of keys are easy to guess");
+_td("Short keyboard patterns are easy to guess");
/**
* Wrapper around zxcvbn password strength estimation