Merge pull request #2499 from matrix-org/travis/usettings/tab/security
Implement the "Security & Privacy" tab of new user settingspull/21833/head
						commit
						41bc2a3d0c
					
				|  | @ -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"; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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>    | ||||
|                 <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>; | ||||
|  |  | |||
|  | @ -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.")} | ||||
|                           | ||||
|                         {_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> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -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.", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston