diff --git a/res/css/_components.scss b/res/css/_components.scss index 454418f9b4..11baf7fee7 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/settings/tabs/_SecuritySettingsTab.scss b/res/css/views/settings/tabs/_SecuritySettingsTab.scss new file mode 100644 index 0000000000..2640df1383 --- /dev/null +++ b/res/css/views/settings/tabs/_SecuritySettingsTab.scss @@ -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; +} \ No newline at end of file diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 1a26c58c44..17f869b5b0 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -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; +} \ No newline at end of file diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index fa7311b141..b47b2368f9 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -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", -
Security Test
, + , )); if (SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 8dc817faad..4c32e3eaf8 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent { {uploadStatus}
{backupSigStatuses}


- + { _t("Restore backup") }     - + { _t("Delete backup") } ; } else { return
{_t("No backup is present")}

- + { _t("Start a new backup") }
; diff --git a/src/components/views/settings/tabs/SecuritySettingsTab.js b/src/components/views/settings/tabs/SecuritySettingsTab.js new file mode 100644 index 0000000000..a2a30a2190 --- /dev/null +++ b/src/components/views/settings/tabs/SecuritySettingsTab.js @@ -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 ( +
+ + {_t('Unignore')} + + {this.props.userId} +
+ ); + } +} + +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(""); + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } + + let importExportButtons = null; + if (client.isCryptoEnabled()) { + importExportButtons = ( +
+ + {_t("Export E2E room keys")} + + + {_t("Import E2E room keys")} + +
+ ); + } + + return ( +
+ {_t("Cryptography")} +
    +
  • + + {deviceId} +
  • +
  • + + {identityKey} +
  • +
+ {importExportButtons} + +
+ ); + } + + _renderIgnoredUsers() { + if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null; + + const userIds = this.state.ignoredUserIds + .map((u) => ); + + return ( +
+ {_t('Ignored users')} +
+ {userIds} +
+
+ ); + } + + _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 ( +
+ {_t('Bulk options')} + + {_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})} + +
+ ); + } + + 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 = ( +
+ {_t("Key backup")} +
+ +
+
+ ); + } + + return ( +
+
{_t("Security & Privacy")}
+
+ {_t("Devices")} +
+ +
+
+ {keyBackup} + {this._renderCurrentDeviceInfo()} +
+ {_t("Analytics")} +
+ {_t("Riot collects anonymous analytics to allow us to improve the application.")} +   + {_t("Privacy is important to us, so we don't collect any personal or " + + "identifiable data for our analytics.")} + + {_t("Learn more about how we use analytics.")} + +
+ +
+ {this._renderIgnoredUsers()} + {this._renderRejectInvites()} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 94c2528c64..10506d8513 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -469,6 +469,21 @@ "Room list": "Room list", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "Unignore": "Unignore", + "": "", + "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):", - "": "", - "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.",