Sort settings tabs into a logical structure

Fixes https://github.com/vector-im/riot-web/issues/8864
This commit is contained in:
Travis Ralston 2019-02-22 10:47:18 -07:00
parent 150c941340
commit bd54a401bc
25 changed files with 190 additions and 190 deletions

View file

@ -0,0 +1,52 @@
/*
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 {_t} from "../../../../../languageHandler";
import {DragDropContext} from "react-beautiful-dnd";
import GroupUserSettings from "../../../groups/GroupUserSettings";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk";
export default class FlairUserSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() {
super();
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
};
}
render() {
return (
<div className="mx_SettingsTab">
<span className="mx_SettingsTab_heading">{_t("Flair")}</span>
<div className="mx_SettingsTab_section">
<DragDropContext>
<GroupUserSettings />
</DragDropContext>
</div>
</div>
);
}
}

View file

@ -0,0 +1,183 @@
/*
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 {_t} from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
import EmailAddresses from "../../EmailAddresses";
import PhoneNumbers from "../../PhoneNumbers";
import Field from "../../../elements/Field";
import * as languageHandler from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore from "../../../../../settings/SettingsStore";
import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
const PlatformPeg = require("../../../../../PlatformPeg");
const sdk = require('../../../../..');
const Modal = require("../../../../../Modal");
const dis = require("../../../../../dispatcher");
export default class GeneralUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
};
}
_onLanguageChange = (newLanguage) => {
if (this.state.language === newLanguage) return;
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({language: newLanguage});
PlatformPeg.get().reload();
};
_onThemeChange = (e) => {
const newTheme = e.target.value;
if (this.state.theme === newTheme) return;
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
this.setState({theme: newTheme});
dis.dispatch({action: 'set_theme', value: newTheme});
};
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
title: _t("Error"),
description: errMsg,
});
};
_onPasswordChanged = () => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
title: _t("Success"),
description: _t(
"Your password was successfully changed. You will not receive " +
"push notifications on other devices until you log back in to them",
) + ".",
});
};
_onDeactivateClicked = () => {
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
};
_renderProfileSection() {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<ProfileSettings />
</div>
);
}
_renderAccountSection() {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const passwordChangeForm = (
<ChangePassword
className="mx_GeneralUserSettingsTab_changePassword"
rowClassName=""
buttonKind="primary"
onError={this._onPasswordChangeError}
onFinished={this._onPasswordChanged} />
);
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<p className="mx_SettingsTab_subsectionText">
{_t("Set a new account password...")}
</p>
{passwordChangeForm}
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
</div>
);
}
_renderLanguageSection() {
// TODO: Convert to new-styled Field
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<LanguageDropdown className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} value={this.state.language} />
</div>
);
}
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option>
<option value="dark">{_t("Dark theme")}</option>
</Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div>
);
}
_renderManagementSection() {
// TODO: Improve warning text for account deactivation
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
<span className="mx_SettingsTab_subsectionText">
{_t("Deactivating your account is a permanent action - be careful!")}
</span>
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
{_t("Deactivate Account")}
</AccessibleButton>
</div>
);
}
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{this._renderThemeSection()}
{this._renderManagementSection()}
</div>
);
}
}

View file

@ -0,0 +1,250 @@
/*
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, getCurrentLanguage} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../../createRoom";
const packageJson = require('../../../../../../package.json');
const Modal = require("../../../../../Modal");
const sdk = require("../../../../..");
const PlatformPeg = require("../../../../../PlatformPeg");
// if this looks like a release, use the 'version' from package.json; else use
// the git sha. Prepend version with v, to look like riot-web version
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
// Simple method to help prettify GH Release Tags and Commit Hashes.
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const ghVersionLabel = function(repo, token='') {
const match = token.match(semVerRegex);
let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
};
export default class HelpUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
vectorVersion: null,
canUpdate: false,
};
}
componentWillMount(): void {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => {
console.error("Error getting vector version: ", e);
});
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => {
console.error("Error getting self updatability: ", e);
});
}
_onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
PlatformPeg.get().reload();
});
};
_onBugReport = (e) => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
_onStartBotChat = (e) => {
this.props.closeSettingsFn();
createRoom({
dmUserId: SdkConfig.get().welcomeUserId,
andView: true,
});
};
_showSpoiler = (event) => {
const target = event.target;
target.innerHTML = target.getAttribute('data-spoiler');
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
_renderLegal() {
const tocLinks = SdkConfig.get().terms_and_conditions_links;
if (!tocLinks) return null;
const legalLinks = [];
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
legalLinks.push(<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noopener" target="_blank">{tocEntry.text}</a>
</div>);
}
return (
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
<div className='mx_SettingsTab_subsectionText'>
{legalLinks}
</div>
</div>
);
}
_renderCredits() {
// Note: This is not translated because it is legal text.
// Also, &nbsp; is ugly but necessary.
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul>
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
default cover photo</a> is (C)&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>&nbsp;
used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
CC-BY-SA 4.0</a>. No warranties are given.
</li>
</ul>
</div>
);
}
render() {
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener' target='_blank'>{sub}</a>,
});
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
faqText = (
<div>
{
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
'bot using the button below.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener'
target='_blank'>{sub}</a>,
})
}
<div>
<AccessibleButton onClick={this._onStartBotChat} kind='primary'>
{_t("Chat with Riot Bot")}
</AccessibleButton>
</div>
</div>
);
}
const reactSdkVersion = REACT_SDK_VERSION !== '<local>'
? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION;
const vectorVersion = this.state.vectorVersion
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown';
let olmVersion = MatrixClientPeg.get().olmVersion;
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
let updateButton = null;
if (this.state.canUpdate) {
const platform = PlatformPeg.get();
updateButton = (
<AccessibleButton onClick={platform.startUpdateCheck} kind='primary'>
{_t('Check for update')}
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
<div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
<div className='mx_SettingsTab_subsectionText'>
{
_t( "If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)
}
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
</div>
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")}
</AccessibleButton>
</div>
</div>
</div>
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
<div className='mx_SettingsTab_subsectionText'>
{faqText}
</div>
</div>
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
{_t("riot-web version:")} {vectorVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{updateButton}
</div>
</div>
{this._renderLegal()}
{this._renderCredits()}
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br />
{_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}<br />
{_t("Access Token:") + ' '}
<AccessibleButton element="span" onClick={this._showSpoiler}
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
&lt;{ _t("click to reveal") }&gt;
</AccessibleButton>
</div>
</div>
</div>
);
}
}

View file

@ -0,0 +1,59 @@
/*
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 {_t} from "../../../../../languageHandler";
import PropTypes from "prop-types";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
const sdk = require("../../../../..");
export class LabsSettingToggle extends React.Component {
static propTypes = {
featureId: PropTypes.string.isRequired,
};
_onChange = async (checked) => {
await SettingsStore.setFeatureEnabled(this.props.featureId, checked);
this.forceUpdate();
};
render() {
const label = _t(SettingsStore.getDisplayName(this.props.featureId));
const value = SettingsStore.isFeatureEnabled(this.props.featureId);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} />;
}
}
export default class LabsUserSettingsTab extends React.Component {
constructor() {
super();
}
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = SettingsStore.getLabsFeatures().map(f => <LabsSettingToggle featureId={f} key={f} />);
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>
<div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
</div>
</div>
);
}
}

View file

@ -0,0 +1,37 @@
/*
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 {_t} from "../../../../../languageHandler";
const sdk = require("../../../../..");
export default class NotificationUserSettingsTab extends React.Component {
constructor() {
super();
}
render() {
const Notifications = sdk.getComponent("views.settings.Notifications");
return (
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<Notifications />
</div>
</div>
);
}
}

View file

@ -0,0 +1,117 @@
/*
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 {_t} from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
const sdk = require("../../../../..");
const PlatformPeg = require("../../../../../PlatformPeg");
export default class PreferencesUserSettingsTab extends React.Component {
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
'sendTypingNotifications',
];
static TIMELINE_SETTINGS = [
'autoplayGifsAndVideos',
'urlPreviewsEnabled',
'TextualBody.enableBigEmoji',
'showReadReceipts',
'showTwelveHourTimestamps',
'alwaysShowTimestamps',
'showRedactions',
'enableSyntaxHighlightLanguageDetection',
'showJoinLeaves',
'showAvatarChanges',
'showDisplaynameChanges',
];
static ADVANCED_SETTINGS = [
'alwaysShowEncryptionIcons',
'Pill.shouldShowPillAvatar',
'TagPanel.enableTagPanel',
'promptBeforeInviteUnknownUsers',
// Start automatically after startup (electron-only)
// Autocomplete delay (niche text box)
];
constructor() {
super();
this.state = {
autoLaunch: false,
autoLaunchSupported: false,
};
}
async componentWillMount(): void {
const autoLaunchSupported = await PlatformPeg.get().supportsAutoLaunch();
let autoLaunch = false;
if (autoLaunchSupported) {
autoLaunch = await PlatformPeg.get().getAutoLaunchEnabled();
}
this.setState({autoLaunch, autoLaunchSupported});
}
_onAutoLaunchChange = (checked) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
};
_onAutocompleteDelayChange = (e) => {
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
};
_renderGroup(settingIds) {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return settingIds.map(i => <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />);
}
render() {
let autoLaunchOption = null;
if (this.state.autoLaunchSupported) {
autoLaunchOption = <LabelledToggleSwitch value={this.state.autoLaunch}
onChange={this._onAutoLaunchChange}
label={_t('Start automatically after system login')} />;
}
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
{autoLaunchOption}
<Field id={"autocompleteDelay"} label={_t('Autocomplete delay (ms)')} type='number'
value={SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay')}
onChange={this._onAutocompleteDelayChange} />
</div>
</div>
);
}
}

View file

@ -0,0 +1,239 @@
/*
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 {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 "../../../../..";
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_SecurityUserSettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
{_t('Unignore')}
</AccessibleButton>
<span>{this.props.userId}</span>
</div>
);
}
}
export default class SecurityUserSettingsTab 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_SecurityUserSettingsTab_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_SecurityUserSettingsTab_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');
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
const 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_SecurityUserSettingsTab">
<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

@ -0,0 +1,182 @@
/*
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 {_t} from "../../../../../languageHandler";
import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {SettingLevel} from "../../../../../settings/SettingsStore";
const Modal = require("../../../../../Modal");
const sdk = require("../../../../..");
const MatrixClientPeg = require("../../../../../MatrixClientPeg");
export default class VoiceUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
mediaDevices: null,
activeAudioOutput: null,
activeAudioInput: null,
activeVideoInput: null,
};
}
componentWillMount(): void {
this._refreshMediaDevices();
}
_refreshMediaDevices = async (stream) => {
if (stream) {
// kill stream so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
this.setState({
mediaDevices: await CallMediaHandler.getDevices(),
activeAudioOutput: CallMediaHandler.getAudioOutput(),
activeAudioInput: CallMediaHandler.getAudioInput(),
activeVideoInput: CallMediaHandler.getVideoInput(),
});
};
_requestMediaPermissions = () => {
const getUserMedia = (
window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
);
if (getUserMedia) {
return getUserMedia.apply(window.navigator, [
{ video: true, audio: true },
this._refreshMediaDevices,
function() {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
});
},
]);
}
};
_setAudioOutput = (e) => {
CallMediaHandler.setAudioOutput(e.target.value);
};
_setAudioInput = (e) => {
CallMediaHandler.setAudioInput(e.target.value);
};
_setVideoInput = (e) => {
CallMediaHandler.setVideoInput(e.target.value);
};
_changeWebRtcMethod = (p2p) => {
MatrixClientPeg.get().setForceTURN(!p2p);
};
_renderDeviceOptions(devices, category) {
return devices.map((d) => <option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
}
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
let requestButton = null;
let speakerDropdown = null;
let microphoneDropdown = null;
let webcamDropdown = null;
if (this.state.mediaDevices === false) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
const defaultOption = {
deviceId: '',
label: _t('Default Device'),
};
const getDefaultDevice = (devices) => {
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift(defaultOption);
return '';
} else {
return 'default';
}
};
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = (
<Field element="select" label={_t("Audio Output")} id="audioOutput"
value={this.state.activeAudioOutput || defaultDevice}
onChange={this._setAudioOutput}>
{this._renderDeviceOptions(audioOutputs, 'audioOutput')}
</Field>
);
}
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = (
<Field element="select" label={_t("Microphone")} id="audioInput"
value={this.state.activeAudioInput || defaultDevice}
onChange={this._setAudioInput}>
{this._renderDeviceOptions(audioInputs, 'audioInput')}
</Field>
);
}
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = (
<Field element="select" label={_t("Camera")} id="videoInput"
value={this.state.activeVideoInput || defaultDevice}
onChange={this._setVideoInput}>
{this._renderDeviceOptions(videoInputs, 'videoInput')}
</Field>
);
}
}
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section">
{requestButton}
{speakerDropdown}
{microphoneDropdown}
{webcamDropdown}
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag name='webRtcAllowPeerToPeer' level={SettingLevel.DEVICE} onChange={this._changeWebRtcMethod} />
</div>
</div>
);
}
}