mirror of https://github.com/vector-im/riot-web
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/_GeneralSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/_HelpSettingsTab.scss";
|
@import "./views/settings/tabs/_HelpSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/_PreferencesSettingsTab.scss";
|
@import "./views/settings/tabs/_PreferencesSettingsTab.scss";
|
||||||
|
@import "./views/settings/tabs/_SecuritySettingsTab.scss";
|
||||||
@import "./views/settings/tabs/_SettingsTab.scss";
|
@import "./views/settings/tabs/_SettingsTab.scss";
|
||||||
@import "./views/settings/tabs/_VoiceSettingsTab.scss";
|
@import "./views/settings/tabs/_VoiceSettingsTab.scss";
|
||||||
@import "./views/voip/_CallView.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;
|
font-family: $font-family-semibold;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_subsectionText {
|
.mx_SettingsTab_subsectionText {
|
||||||
color: $settings-subsection-fg-color;
|
color: $settings-subsection-fg-color;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
margin: 0;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0 100px 0 0; // Align with the rest of the view
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_section .mx_SettingsFlag {
|
.mx_SettingsTab_section .mx_SettingsFlag {
|
||||||
|
@ -54,3 +54,9 @@ limitations under the License.
|
||||||
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {
|
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {
|
||||||
float: right;
|
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 dis from '../../../dispatcher';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
|
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
|
||||||
|
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab";
|
||||||
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
|
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
|
||||||
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
|
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
|
||||||
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
|
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
|
||||||
|
@ -75,7 +76,7 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
_td("Security & Privacy"),
|
_td("Security & Privacy"),
|
||||||
"mx_UserSettingsDialog_securityIcon",
|
"mx_UserSettingsDialog_securityIcon",
|
||||||
<div>Security Test</div>,
|
<SecuritySettingsTab />,
|
||||||
));
|
));
|
||||||
if (SettingsStore.getLabsFeatures().length > 0) {
|
if (SettingsStore.getLabsFeatures().length > 0) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
|
|
|
@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||||
{uploadStatus}
|
{uploadStatus}
|
||||||
<div>{backupSigStatuses}</div><br />
|
<div>{backupSigStatuses}</div><br />
|
||||||
<br />
|
<br />
|
||||||
<AccessibleButton className="mx_UserSettings_button"
|
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||||
onClick={this._restoreBackup}>
|
|
||||||
{ _t("Restore backup") }
|
{ _t("Restore backup") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton className="mx_UserSettings_button danger"
|
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||||
onClick={this._deleteBackup}>
|
|
||||||
{ _t("Delete backup") }
|
{ _t("Delete backup") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
return <div>
|
return <div>
|
||||||
{_t("No backup is present")}<br /><br />
|
{_t("No backup is present")}<br /><br />
|
||||||
<AccessibleButton className="mx_UserSettings_button"
|
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
|
||||||
onClick={this._startNewBackup}>
|
|
||||||
{ _t("Start a new backup") }
|
{ _t("Start a new backup") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</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",
|
"Room list": "Room list",
|
||||||
"Timeline": "Timeline",
|
"Timeline": "Timeline",
|
||||||
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
|
"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",
|
"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",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"No devices with registered encryption keys": "No devices with registered encryption keys",
|
||||||
"Devices": "Devices",
|
|
||||||
"Unignore": "Unignore",
|
|
||||||
"Ignore": "Ignore",
|
"Ignore": "Ignore",
|
||||||
"Jump to read receipt": "Jump to read receipt",
|
"Jump to read receipt": "Jump to read receipt",
|
||||||
"Mention": "Mention",
|
"Mention": "Mention",
|
||||||
|
@ -1074,7 +1087,6 @@
|
||||||
"Room contains unknown devices": "Room contains unknown devices",
|
"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.",
|
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
|
||||||
"Unknown devices": "Unknown devices",
|
"Unknown devices": "Unknown devices",
|
||||||
"Security & Privacy": "Security & Privacy",
|
|
||||||
"Visit old settings": "Visit old settings",
|
"Visit old settings": "Visit old settings",
|
||||||
"Unable to load backup status": "Unable to load backup status",
|
"Unable to load backup status": "Unable to load backup status",
|
||||||
"Unable to restore backup": "Unable to restore backup",
|
"Unable to restore backup": "Unable to restore backup",
|
||||||
|
@ -1305,23 +1317,14 @@
|
||||||
"Interface Language": "Interface Language",
|
"Interface Language": "Interface Language",
|
||||||
"User Interface": "User Interface",
|
"User Interface": "User Interface",
|
||||||
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
||||||
"<not supported>": "<not supported>",
|
|
||||||
"Import E2E room keys": "Import E2E room keys",
|
|
||||||
"Key Backup": "Key Backup",
|
"Key Backup": "Key Backup",
|
||||||
"Cryptography": "Cryptography",
|
|
||||||
"Device ID:": "Device ID:",
|
|
||||||
"Device key:": "Device key:",
|
|
||||||
"Ignored Users": "Ignored Users",
|
"Ignored Users": "Ignored Users",
|
||||||
"Submit Debug Logs": "Submit Debug Logs",
|
"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",
|
"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",
|
"Use with caution": "Use with caution",
|
||||||
"Deactivate my account": "Deactivate my account",
|
"Deactivate my account": "Deactivate my account",
|
||||||
"Clear Cache": "Clear Cache",
|
"Clear Cache": "Clear Cache",
|
||||||
"Updates": "Updates",
|
"Updates": "Updates",
|
||||||
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
|
||||||
"Bulk Options": "Bulk Options",
|
"Bulk Options": "Bulk Options",
|
||||||
"Desktop specific": "Desktop specific",
|
"Desktop specific": "Desktop specific",
|
||||||
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
|
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
|
||||||
|
|
Loading…
Reference in New Issue