Merge pull request #2499 from matrix-org/travis/usettings/tab/security

Implement the "Security & Privacy" tab of new user settings
This commit is contained in:
Travis Ralston 2019-01-25 09:44:00 -07:00 committed by GitHub
commit 41bc2a3d0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 324 additions and 21 deletions

View file

@ -139,6 +139,7 @@
@import "./views/settings/tabs/_GeneralSettingsTab.scss";
@import "./views/settings/tabs/_HelpSettingsTab.scss";
@import "./views/settings/tabs/_PreferencesSettingsTab.scss";
@import "./views/settings/tabs/_SecuritySettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/_VoiceSettingsTab.scss";
@import "./views/voip/_CallView.scss";

View file

@ -0,0 +1,53 @@
/*
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.
*/
.mx_SecuritySettingsTab .mx_DevicesPanel {
// Normally the panel is 880px, however this can easily overflow the container.
// TODO: Fix the table to not be squishy
width: auto;
max-width: 880px;
}
.mx_SecuritySettingsTab_deviceInfo {
display: table;
padding-left: 0;
}
.mx_SecuritySettingsTab_deviceInfo > li {
display: table-row;
}
.mx_SecuritySettingsTab_deviceInfo > li > label,
.mx_SecuritySettingsTab_deviceInfo > li > span {
display: table-cell;
padding-right: 1em;
}
.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton {
margin-right: 10px;
}
.mx_SecuritySettingsTab_importExportButtons {
margin-bottom: 15px;
}
.mx_SecuritySettingsTab_ignoredUser {
margin-bottom: 5px;
}
.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton {
margin-right: 10px;
}

View file

@ -26,15 +26,15 @@ limitations under the License.
font-family: $font-family-semibold;
color: $primary-fg-color;
margin-bottom: 10px;
margin-top: 10px;
margin-top: 12px;
}
.mx_SettingsTab_subsectionText {
color: $settings-subsection-fg-color;
font-size: 12px;
padding-bottom: 12px;
margin: 0;
display: block;
margin: 0 100px 0 0; // Align with the rest of the view
}
.mx_SettingsTab_section .mx_SettingsFlag {
@ -54,3 +54,9 @@ limitations under the License.
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {
float: right;
}
.mx_SettingsTab_linkBtn {
cursor: pointer;
color: $accent-color;
word-break: break-all;
}

View file

@ -23,6 +23,7 @@ import GeneralSettingsTab from "../settings/tabs/GeneralSettingsTab";
import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab";
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
@ -75,7 +76,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<div>Security Test</div>,
<SecuritySettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(

View file

@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent {
{uploadStatus}
<div>{backupSigStatuses}</div><br />
<br />
<AccessibleButton className="mx_UserSettings_button"
onClick={this._restoreBackup}>
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") }
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._deleteBackup}>
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete backup") }
</AccessibleButton>
</div>;
} else {
return <div>
{_t("No backup is present")}<br /><br />
<AccessibleButton className="mx_UserSettings_button"
onClick={this._startNewBackup}>
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
{ _t("Start a new backup") }
</AccessibleButton>
</div>;

View file

@ -0,0 +1,242 @@
/*
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 PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../utils/FormattingUtils";
import AccessibleButton from "../../elements/AccessibleButton";
import Analytics from "../../../../Analytics";
import Promise from "bluebird";
import Modal from "../../../../Modal";
import sdk from "../../../../index";
export class IgnoredUser extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
onUnignored: PropTypes.func.isRequired,
};
_onUnignoreClicked = (e) => {
this.props.onUnignored(this.props.userId);
};
render() {
return (
<div className='mx_SecuritySettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
{_t('Unignore')}
</AccessibleButton>
<span>{this.props.userId}</span>
</div>
);
}
}
export default class SecuritySettingsTab extends React.Component {
constructor() {
super();
this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
rejectingInvites: false,
};
}
_updateBlacklistDevicesFlag = (checked) => {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
};
_updateAnalytics = (checked) => {
checked ? Analytics.enable() : Analytics.disable();
};
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onUserUnignored = async (userId) => {
// Don't use this.state to get the ignored user list as it might be
// ever so slightly outdated. Instead, prefer to get a fresh list and
// update that.
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) {
ignoredUsers.splice(index, 1);
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers);
}
this.setState({ignoredUsers});
};
_onRejectAllInvitesClicked = (rooms, ev) => {
this.setState({
rejectingInvites: true,
});
// reject the invites
const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId).catch((e) => {
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
});
});
Promise.all(promises).then(() => {
this.setState({
rejectingInvites: false,
});
});
};
_renderCurrentDeviceInfo() {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
let importExportButtons = null;
if (client.isCryptoEnabled()) {
importExportButtons = (
<div className='mx_SecuritySettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
</AccessibleButton>
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
</AccessibleButton>
</div>
);
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<ul className='mx_SettingsTab_subsectionText mx_SecuritySettingsTab_deviceInfo'>
<li>
<label>{_t("Device ID:")}</label>
<span><code>{deviceId}</code></span>
</li>
<li>
<label>{_t("Device key:")}</label>
<span><code><b>{identityKey}</b></code></span>
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
</div>
);
}
_renderIgnoredUsers() {
if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null;
const userIds = this.state.ignoredUserIds
.map((u) => <IgnoredUser userId={u} onUnignored={this._onUserUnignored} key={u} />);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
<div className='mx_SettingsTab_subsectionText'>
{userIds}
</div>
</div>
);
}
_renderRejectInvites() {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
});
if (invitedRooms.length === 0) {
return null;
}
const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
<AccessibleButton onClick={onClick} kind='danger' disabled={this.state.rejectingInvites}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton>
</div>
);
}
render() {
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
let keyBackup = null;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
keyBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
<div className='mx_SettingsTab_subsectionText'>
<KeyBackupPanel />
</div>
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecuritySettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span>
<div className='mx_SettingsTab_subsectionText'>
<DevicesPanel />
</div>
</div>
{keyBackup}
{this._renderCurrentDeviceInfo()}
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Riot collects anonymous analytics to allow us to improve the application.")}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
{this._renderIgnoredUsers()}
{this._renderRejectInvites()}
</div>
);
}
}

View file

@ -469,6 +469,21 @@
"Room list": "Room list",
"Timeline": "Timeline",
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
"Unignore": "Unignore",
"<not supported>": "<not supported>",
"Import E2E room keys": "Import E2E room keys",
"Cryptography": "Cryptography",
"Device ID:": "Device ID:",
"Device key:": "Device key:",
"Ignored users": "Ignored users",
"Bulk options": "Bulk options",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Key backup": "Key backup",
"Security & Privacy": "Security & Privacy",
"Devices": "Devices",
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
"No media permissions": "No media permissions",
"You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
@ -529,8 +544,6 @@
"Failed to change power level": "Failed to change power level",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"No devices with registered encryption keys": "No devices with registered encryption keys",
"Devices": "Devices",
"Unignore": "Unignore",
"Ignore": "Ignore",
"Jump to read receipt": "Jump to read receipt",
"Mention": "Mention",
@ -1074,7 +1087,6 @@
"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",
"Security & Privacy": "Security & Privacy",
"Visit old settings": "Visit old settings",
"Unable to load backup status": "Unable to load backup status",
"Unable to restore backup": "Unable to restore backup",
@ -1305,23 +1317,14 @@
"Interface Language": "Interface Language",
"User Interface": "User Interface",
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
"<not supported>": "<not supported>",
"Import E2E room keys": "Import E2E room keys",
"Key Backup": "Key Backup",
"Cryptography": "Cryptography",
"Device ID:": "Device ID:",
"Device key:": "Device key:",
"Ignored Users": "Ignored Users",
"Submit Debug Logs": "Submit Debug Logs",
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
"These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways",
"Use with caution": "Use with caution",
"Deactivate my account": "Deactivate my account",
"Clear Cache": "Clear Cache",
"Updates": "Updates",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Bulk Options": "Bulk Options",
"Desktop specific": "Desktop specific",
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",