Merge remote-tracking branch 'origin/experimental' into dbkr/sas

This commit is contained in:
David Baker 2019-01-28 09:36:39 +00:00
commit b4f02844a8
90 changed files with 3135 additions and 1228 deletions

View file

@ -26,7 +26,7 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_AuthFooter">
<a href="https://matrix.org">{ _t("powered by Matrix") }</a>
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
</div>
);
},

View file

@ -25,10 +25,12 @@ module.exports = React.createClass({
render: function() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
return (
<div className="mx_AuthHeader">
<AuthHeaderLogo />
<LanguageSelector />
</div>
);
},

View file

@ -62,7 +62,7 @@ module.exports = React.createClass({
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
const protocol = global.location.protocol;
if (protocol === "file:") {
if (protocol === "vector:") {
const warning = document.createElement('div');
// XXX: fix hardcoded app URL. Better solutions include:
// * jumping straight to a hosted captcha page (but we don't support that yet)
@ -71,7 +71,11 @@ module.exports = React.createClass({
ReactDOM.render(_t(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
{},
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
{
'a': (sub) => {
return <a target="_blank" rel="noopener" href='https://riot.im/app'>{ sub }</a>;
},
}), warning);
this.refs.recaptchaContainer.appendChild(warning);
} else {
const scriptTag = document.createElement('script');

View file

@ -21,7 +21,7 @@ import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
const COUNTRIES_BY_ISO2 = new Object(null);
const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) {
COUNTRIES_BY_ISO2[c.iso2] = c;
}

View file

@ -294,7 +294,7 @@ export const TermsAuthEntry = React.createClass({
_trySubmit: function() {
let allChecked = true;
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
const checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
}
@ -440,7 +440,6 @@ export const MsisdnAuthEntry = React.createClass({
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
submitAuthDict: PropTypes.func,
},
getInitialState: function() {

View file

@ -0,0 +1,39 @@
/*
Copyright 2018 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 SdkConfig from "../../../SdkConfig";
import {getCurrentLanguage} from "../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import PlatformPeg from "../../../PlatformPeg";
import sdk from '../../../index';
import React from 'react';
function onChange(newLang) {
if (getCurrentLanguage() !== newLang) {
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload();
}
}
export default function LanguageSelector() {
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <LanguageDropdown className="mx_Auth_language"
onOptionChange={onChange}
value={getCurrentLanguage()}
/>;
}

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects';
import {fieldInputIncorrect} from '../../../UiEffects';
import SdkConfig from '../../../SdkConfig';
/**
@ -71,7 +71,7 @@ class PasswordLogin extends React.Component {
componentWillReceiveProps(nextProps) {
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
fieldInputIncorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
}
}
@ -245,11 +245,16 @@ class PasswordLogin extends React.Component {
let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
{ _t('Forgot your password?') }
</a>
);
forgotPasswordJsx = <span>
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
a: sub => <a className="mx_Login_forgot"
onClick={this.props.onForgotPasswordClick}
href="#"
>
{sub}
</a>,
})}
</span>;
}
let matrixIdText = _t('Matrix ID');

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { field_input_incorrect } from '../../../UiEffects';
import { fieldInputIncorrect } from '../../../UiEffects';
import sdk from '../../../index';
import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
@ -28,7 +28,6 @@ import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
const FIELD_PHONE_NUMBER = 'field_phone_number';
const FIELD_USERNAME = 'field_username';
const FIELD_PASSWORD = 'field_password';
@ -47,17 +46,6 @@ module.exports = React.createClass({
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
teamsConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: PropTypes.string,
teams: PropTypes.arrayOf(PropTypes.shape({
// The displayed name of the team
"name": PropTypes.string,
// The domain of team email addresses
"domain": PropTypes.string,
})).required,
}),
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
@ -76,7 +64,6 @@ module.exports = React.createClass({
getInitialState: function() {
return {
fieldValid: {},
selectedTeam: null,
// The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry,
};
@ -139,8 +126,7 @@ module.exports = React.createClass({
},
/**
* Returns true if all fields were valid last time
* they were validated.
* @returns {boolean} true if all fields were valid last time they were validated.
*/
allFieldsValid: function() {
const keys = Object.keys(this.state.fieldValid);
@ -152,103 +138,84 @@ module.exports = React.createClass({
return true;
},
_isUniEmail: function(email) {
return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org');
},
validateField: function(field_id) {
validateField: function(fieldID) {
const pwd1 = this.refs.password.value.trim();
const pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) {
case FIELD_EMAIL:
switch (fieldID) {
case FIELD_EMAIL: {
const email = this.refs.email.value;
if (this.props.teamsConfig && this._isUniEmail(email)) {
const matchingTeam = this.props.teamsConfig.teams.find(
(team) => {
return email.split('@').pop() === team.domain;
},
) || null;
this.setState({
selectedTeam: matchingTeam,
showSupportEmail: !matchingTeam,
});
this.props.onTeamSelected(matchingTeam);
} else {
this.props.onTeamSelected(null);
this.setState({
selectedTeam: null,
showSupportEmail: false,
});
}
const emailValid = email === '' || Email.looksValid(email);
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_EMAIL");
} else this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL");
} else this.markFieldValid(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_PHONE_NUMBER:
}
case FIELD_PHONE_NUMBER: {
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
} else this.markFieldValid(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break;
case FIELD_USERNAME:
}
case FIELD_USERNAME: {
const username = this.refs.username.value.trim();
if (!SAFE_LOCALPART_REGEX.test(username)) {
this.markFieldValid(
field_id,
fieldID,
false,
"RegistrationForm.ERR_USERNAME_INVALID",
);
} else if (username == '') {
this.markFieldValid(
field_id,
fieldID,
false,
"RegistrationForm.ERR_USERNAME_BLANK",
);
} else {
this.markFieldValid(field_id, true);
this.markFieldValid(fieldID, true);
}
break;
}
case FIELD_PASSWORD:
if (pwd1 == '') {
this.markFieldValid(
field_id,
fieldID,
false,
"RegistrationForm.ERR_PASSWORD_MISSING",
);
} else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid(
field_id,
fieldID,
false,
"RegistrationForm.ERR_PASSWORD_LENGTH",
);
} else {
this.markFieldValid(field_id, true);
this.markFieldValid(fieldID, true);
}
break;
case FIELD_PASSWORD_CONFIRM:
this.markFieldValid(
field_id, pwd1 == pwd2,
fieldID, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH",
);
break;
}
},
markFieldValid: function(field_id, val, error_code) {
markFieldValid: function(fieldID, val, errorCode) {
const fieldValid = this.state.fieldValid;
fieldValid[field_id] = val;
fieldValid[fieldID] = val;
this.setState({fieldValid: fieldValid});
if (!val) {
field_input_incorrect(this.fieldElementById(field_id));
this.props.onError(error_code);
fieldInputIncorrect(this.fieldElementById(fieldID));
this.props.onError(errorCode);
}
},
fieldElementById(field_id) {
switch (field_id) {
fieldElementById(fieldID) {
switch (fieldID) {
case FIELD_EMAIL:
return this.refs.email;
case FIELD_PHONE_NUMBER:
@ -262,9 +229,9 @@ module.exports = React.createClass({
}
},
_classForField: function(field_id, ...baseClasses) {
_classForField: function(fieldID, ...baseClasses) {
let cls = baseClasses.join(' ');
if (this.state.fieldValid[field_id] === false) {
if (this.state.fieldValid[fieldID] === false) {
if (cls) cls += ' ';
cls += 'error';
}
@ -289,7 +256,9 @@ module.exports = React.createClass({
render: function() {
const self = this;
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? _t("Email address") : _t("Email address (optional)");
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
_t("Email address") :
_t("Email address (optional)");
const emailSection = (
<div>
@ -301,32 +270,13 @@ module.exports = React.createClass({
value={self.state.email} />
</div>
);
let belowEmailSection;
if (this.props.teamsConfig) {
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
belowEmailSection = (
<p className="mx_Login_support">
Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{ this.props.teamsConfig.supportEmail }
</a>&nbsp;
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
</p>
);
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
{ _t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name}) }
</p>
);
}
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? _t("Mobile phone number") : _t("Mobile phone number (optional)");
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ?
_t("Mobile phone number") :
_t("Mobile phone number (optional)");
phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
@ -361,7 +311,6 @@ module.exports = React.createClass({
<div>
<form onSubmit={this.onSubmit}>
{ emailSection }
{ belowEmailSection }
{ phoneSection }
<input type="text" ref="username"
placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}

View file

@ -88,8 +88,8 @@ module.exports = React.createClass({
},
onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
this.setState({hs_url: ev.target.value}, () => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
let hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
this.props.onServerConfigChange({
@ -101,8 +101,8 @@ module.exports = React.createClass({
},
onIdentityServerChanged: function(ev) {
this.setState({is_url: ev.target.value}, function() {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
this.setState({is_url: ev.target.value}, () => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
let isUrl = this.state.is_url.trim().replace(/\/$/, "");
if (isUrl === "") isUrl = this.props.defaultIsUrl;
this.props.onServerConfigChange({

View file

@ -138,7 +138,9 @@ export default class LogoutDialog extends React.Component {
// once you can restorew a backup by verifying a device
description={_t(
"When signing in again, you can access encrypted chat history by " +
"restoring your key backup. You'll need your recovery key.",
"restoring your key backup. You'll need your recovery passphrase " +
"or, if you didn't set a recovery passphrase, your recovery key " +
"(that you downloaded).",
)}
button={_t("Sign out")}
onFinished={this._onFinished}

View file

@ -0,0 +1,90 @@
/*
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 {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import dis from '../../../dispatcher';
// TODO: Ditch this whole component
export class TempTab extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
componentDidMount(): void {
dis.dispatch({action: "open_old_room_settings"});
this.props.onClose();
}
render() {
return <div>Hello World</div>;
}
}
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_getTabs() {
const tabs = [];
tabs.push(new Tab(
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
<div>General Test</div>,
));
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
<div>Security Test</div>,
));
tabs.push(new Tab(
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<div>Roles Test</div>,
));
tabs.push(new Tab(
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<div>Advanced Test</div>,
));
tabs.push(new Tab(
_td("Visit old settings"),
"mx_RoomSettingsDialog_warningIcon",
<TempTab onClose={this.props.onFinished} />,
));
return tabs;
}
render() {
return (
<div className="mx_RoomSettingsDialog">
<div className="mx_SettingsDialog_header">
{_t("Settings")}
<span className="mx_SettingsDialog_close">
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
</span>
</div>
<TabbedView tabs={this._getTabs()} />
</div>
);
}
}

View file

@ -193,9 +193,6 @@ export default React.createClass({
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
@ -203,7 +200,6 @@ export default React.createClass({
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},

View file

@ -116,9 +116,8 @@ export default React.createClass({
<ChangePassword
className="mx_SetPasswordDialog_change_password"
rowClassName=""
rowLabelClassName=""
rowInputClassName=""
buttonClassName="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
buttonClassNames="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
buttonKind="primary"
confirm={false}
autoFocusNewPasswordInput={true}
shouldAskForEmail={true}

View file

@ -21,6 +21,13 @@ import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
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";
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab";
// TODO: Ditch this whole component
export class TempTab extends React.Component {
@ -44,52 +51,61 @@ export default class UserSettingsDialog extends React.Component {
};
_getTabs() {
return [
new Tab(
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralSettingsTab />,
),
new Tab(
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<div>Notifications Test</div>,
),
new Tab(
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<div>Preferences Test</div>,
),
new Tab(
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<div>Voice Test</div>,
),
new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<div>Security Test</div>,
),
new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<div>Help Test</div>,
),
new Tab(
_td("Visit old settings"),
"mx_UserSettingsDialog_helpIcon",
<TempTab onClose={this.props.onFinished} />,
),
];
const tabs = [];
tabs.push(new Tab(
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralSettingsTab />,
));
tabs.push(new Tab(
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<NotificationSettingsTab />,
));
tabs.push(new Tab(
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesSettingsTab />,
));
tabs.push(new Tab(
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceSettingsTab />,
));
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecuritySettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsSettingsTab />,
));
}
tabs.push(new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<HelpSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
_td("Visit old settings"),
"mx_UserSettingsDialog_helpIcon",
<TempTab onClose={this.props.onFinished} />,
));
return tabs;
}
render() {
return (
<div className="mx_UserSettingsDialog">
<div className="mx_UserSettingsDialog_header">
<div className="mx_SettingsDialog_header">
{_t("Settings")}
<span className="mx_UserSettingsDialog_close">
<AccessibleButton className="mx_UserSettingsDialog_closeIcon" onClick={this.props.onFinished} />
<span className="mx_SettingsDialog_close">
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
</span>
</div>
<TabbedView tabs={this._getTabs()} />

View file

@ -42,7 +42,9 @@ export default React.createClass({
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
if (cli) {
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {

View file

@ -25,12 +25,24 @@ export default class Field extends React.PureComponent {
type: PropTypes.string,
// The field's label string.
label: PropTypes.string,
// The field's placeholder string.
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
// The type of field to create. Defaults to "input". Should be "input" or "select".
// To define options for a select, use <Field><option ... /></Field>
element: PropTypes.string,
// All other props pass through to the <input>.
};
get value() {
if (!this.refs.fieldInput) return null;
return this.refs.fieldInput.value;
}
set value(newValue) {
if (!this.refs.fieldInput) {
throw new Error("No field input reference");
}
this.refs.fieldInput.value = newValue;
}
render() {
@ -42,6 +54,8 @@ export default class Field extends React.PureComponent {
// Set some defaults for the element
extraProps.type = extraProps.type || "text";
extraProps.ref = "fieldInput";
extraProps.placeholder = extraProps.placeholder || extraProps.label;
const element = this.props.element || "input";
const fieldInput = React.createElement(element, extraProps, this.props.children);

View file

@ -0,0 +1,42 @@
/*
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 ToggleSwitch from "./ToggleSwitch";
export default class LabelledToggleSwitch extends React.Component {
static propTypes = {
// The value for the toggle switch
value: PropTypes.bool.isRequired,
// The function to call when the value changes
onChange: PropTypes.func.isRequired,
// The translated label for the switch
label: PropTypes.string.isRequired,
};
render() {
// This is a minimal version of a SettingsFlag
return (
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{this.props.label}</span>
<ToggleSwitch checked={this.props.value} onChange={this.props.onChange} />
</div>
);
}
}

View file

@ -18,6 +18,7 @@ import React from "react";
import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
import ToggleSwitch from "./ToggleSwitch";
module.exports = React.createClass({
displayName: 'SettingsFlag',
@ -29,10 +30,6 @@ module.exports = React.createClass({
onChange: PropTypes.func,
isExplicit: PropTypes.bool,
manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: PropTypes.string,
value: PropTypes.any, // the value for the radio button
},
getInitialState: function() {
@ -46,13 +43,12 @@ module.exports = React.createClass({
};
},
onChange: function(e) {
if (this.props.group && !e.target.checked) return;
onChange: function(checked) {
if (this.props.group && !checked) return;
const newState = this.props.group ? this.props.value : e.target.checked;
if (!this.props.manualSave) this.save(newState);
else this.setState({ value: newState });
if (this.props.onChange) this.props.onChange(newState);
if (!this.props.manualSave) this.save(checked);
else this.setState({ value: checked });
if (this.props.onChange) this.props.onChange(checked);
},
save: function(val = undefined) {
@ -78,34 +74,11 @@ module.exports = React.createClass({
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label);
// We generate a relatively complex ID to avoid conflicts
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
let checkbox = (
<input id={id}
type="checkbox"
defaultChecked={value}
onChange={this.onChange}
disabled={!canChange}
/>
);
if (this.props.group) {
checkbox = (
<input id={id}
type="radio"
name={this.props.group}
value={this.props.value}
checked={value === this.props.value}
onChange={this.onChange}
disabled={!canChange}
/>
);
}
return (
<label>
{ checkbox }
{ label }
</label>
<div className="mx_SettingsFlag">
<span className="mx_SettingsFlag_label">{label}</span>
<ToggleSwitch checked={value} onChange={this.onChange} disabled={!canChange} />
</div>
);
},
});

View file

@ -0,0 +1,66 @@
/*
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 classNames from "classnames";
export default class ToggleSwitch extends React.Component {
static propTypes = {
// Whether or not this toggle is in the 'on' position. Default false (off).
checked: PropTypes.bool,
// Whether or not the user can interact with the switch
disabled: PropTypes.bool,
// Called when the checked state changes. First argument will be the new state.
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
checked: props.checked || false, // default false
};
}
_onClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.disabled) return;
const newState = !this.state.checked;
this.setState({checked: newState});
if (this.props.onChange) {
this.props.onChange(newState);
}
};
render() {
const classes = classNames({
"mx_ToggleSwitch": true,
"mx_ToggleSwitch_on": this.state.checked,
"mx_ToggleSwitch_enabled": !this.props.disabled,
});
return (
<div className={classes} onClick={this._onClick}>
<div className="mx_ToggleSwitch_ball" />
</div>
);
}
}

View file

@ -68,15 +68,12 @@ export default React.createClass({
text = _t("You're not currently a member of any communities.");
}
return <div>
<h3>{ _t('Flair') }</h3>
<div className="mx_UserSettings_section">
<p>
{ text }
</p>
return (
<div>
<p className="mx_SettingsTab_subsectionText">{ text }</p>
{ scrollbox }
</div>
</div>;
);
},
render() {

View file

@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component {
// till show_right_panel, just without the fromHeader flag
// as that would hide the right panel again
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
}
this.setState({
phase: payload.phase,

View file

@ -62,7 +62,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
if (sig.device && !sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
@ -133,6 +133,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let body;
let primaryCaption = _t("Set up");
if (this.state.error) {
body = <div className="error">
{_t("Unable to load key backup status")}
@ -140,10 +141,20 @@ export default class RoomRecoveryReminder extends React.PureComponent {
} else if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified.
body = _t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, set up Secure Message Recovery.",
);
body = <div>
<p>{_t(
"Secure Message Recovery has been set up on another device: <deviceName></deviceName>",
{},
{
deviceName: () => <i>{this.state.unverifiedDevice.unsigned.device_display_name}</i>,
},
)}</p>
<p>{_t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, verify that device now.",
)}</p>
</div>;
primaryCaption = _t("Verify device");
} else {
// The default case assumes that a key backup doesn't exist for this account.
// (This component doesn't currently check that itself.)
@ -167,7 +178,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_button"
onClick={this.onSetupClick}>
{ _t("Set up") }
{primaryCaption}
</AccessibleButton>
</div>
</div>

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2018-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.
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Field from "../elements/Field";
const React = require('react');
import PropTypes from 'prop-types';
const MatrixClientPeg = require("../../../MatrixClientPeg");
@ -35,9 +37,8 @@ module.exports = React.createClass({
onError: PropTypes.func,
onCheckPassword: PropTypes.func,
rowClassName: PropTypes.string,
rowLabelClassName: PropTypes.string,
rowInputClassName: PropTypes.string,
buttonClassName: PropTypes.string,
buttonKind: PropTypes.string,
confirm: PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: PropTypes.bool,
@ -114,7 +115,11 @@ module.exports = React.createClass({
'making encrypted chat history unreadable, unless you first export your room keys ' +
'and re-import them afterwards. ' +
'In future this will be improved.',
) } (<a href="https://github.com/vector-im/riot-web/issues/2671">https://github.com/vector-im/riot-web/issues/2671</a>)
) }
{' '}
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noopener">
https://github.com/vector-im/riot-web/issues/2671
</a>
</div>,
button: _t("Continue"),
extraButtons: [
@ -203,21 +208,18 @@ module.exports = React.createClass({
},
render: function() {
// TODO: Live validation on `new pw == confirm pw`
const rowClassName = this.props.rowClassName;
const rowLabelClassName = this.props.rowLabelClassName;
const rowInputClassName = this.props.rowInputClassName;
const buttonClassName = this.props.buttonClassName;
let currentPassword = null;
if (!this.state.cachedPassword) {
currentPassword = <div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">{ _t('Current password') }</label>
currentPassword = (
<div className={rowClassName}>
<Field id="passwordold" type="password" ref="old_input" label={_t('Current password')} />
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
</div>
</div>;
);
}
switch (this.state.phase) {
@ -228,24 +230,13 @@ module.exports = React.createClass({
<form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword }
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password1">{ passwordLabel }</label>
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
<Field id="password1" type="password" ref="new_input" label={passwordLabel}
autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password2">{ _t('Confirm password') }</label>
</div>
<div className={rowInputClassName}>
<input id="password2" type="password" ref="confirm_input" />
</div>
<Field id="password2" type="password" ref="confirm_input" label={_t("Confirm password")} />
</div>
<AccessibleButton className={buttonClassName}
onClick={this.onClickChange}
element="button">
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
{ _t('Change Password') }
</AccessibleButton>
</form>

View file

@ -0,0 +1,231 @@
/*
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 MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import * as Email from "../../../email";
import AddThreepid from "../../../AddThreepid";
const sdk = require('../../../index');
const Modal = require("../../../Modal");
/*
TODO: Improve the UX for everything in here.
It's very much placeholder, but it gets the job done. The old way of handling
email addresses in user settings was to use dialogs to communicate state, however
due to our dialog system overriding dialogs (causing unmounts) this creates problems
for a sane UX. For instance, the user could easily end up entering an email address
and receive a dialog to verify the address, which then causes the component here
to forget what it was doing and ultimately fail. Dialogs are still used in some
places to communicate errors - these should be replaced with inline validation when
that is available.
*/
export class ExistingEmailAddress extends React.Component {
static propTypes = {
email: PropTypes.object.isRequired,
onRemoved: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
return this.props.onRemoved(this.props.email);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
render() {
if (this.state.verifyRemove) {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_ExistingEmailAddress">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_ExistingEmailAddress_delete" alt={_t("Remove")} />
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
</div>
);
}
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
this.state = {
emails: [],
verifying: false,
addTask: null,
continueDisabled: false,
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')});
});
}
_onRemoved = (address) => {
this.setState({emails: this.state.emails.filter((e) => e !== address)});
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.refs.newEmailAddress) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const email = this.refs.newEmailAddress.value;
// TODO: Inline field validation
if (!Email.looksValid(email)) {
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addEmailAddress(email, true).then(() => {
this.setState({continueDisabled: false});
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
_onContinueClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.refs.newEmailAddress.value;
this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null,
continueDisabled: false,
verifying: false,
});
this.refs.newEmailAddress.value = "";
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
};
render() {
const existingEmailElements = this.state.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
});
let addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
addButton = (
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_EmailAddresses_new">
<Field id="newEmailAddress" ref="newEmailAddress" label={_t("Email Address")}
type="text" autoComplete="off" disabled={this.state.verifying} />
{addButton}
</form>
</div>
);
}
}

View file

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

View file

@ -28,6 +28,8 @@ import {
PushRuleVectorState,
ContentRules,
} from '../../../notifications';
import * as SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
@ -70,19 +72,6 @@ module.exports = React.createClass({
ERROR: "ERROR", // There was an error
},
propTypes: {
// The array of threepids from the JS SDK (required for email notifications)
threepids: React.PropTypes.array.isRequired,
// The brand string set when creating an email pusher
brand: React.PropTypes.string,
},
getDefaultProps: function() {
return {
threepids: [],
};
},
getInitialState: function() {
return {
phase: this.phases.LOADING,
@ -94,6 +83,7 @@ module.exports = React.createClass({
},
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
threepids: [], // used for email notifications
};
},
@ -101,42 +91,42 @@ module.exports = React.createClass({
this._refreshFromServer();
},
onEnableNotificationsChange: function(event) {
onEnableNotificationsChange: function(checked) {
const self = this;
this.setState({
phase: this.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() {
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() {
self._refreshFromServer();
});
},
onEnableDesktopNotificationsChange: function(event) {
onEnableDesktopNotificationsChange: function(checked) {
SettingsStore.setValue(
"notificationsEnabled", null,
SettingLevel.DEVICE,
event.target.checked,
checked,
).finally(() => {
this.forceUpdate();
});
},
onEnableDesktopNotificationBodyChange: function(event) {
onEnableDesktopNotificationBodyChange: function(checked) {
SettingsStore.setValue(
"notificationBodyEnabled", null,
SettingLevel.DEVICE,
event.target.checked,
checked,
).finally(() => {
this.forceUpdate();
});
},
onEnableAudioNotificationsChange: function(event) {
onEnableAudioNotificationsChange: function(checked) {
SettingsStore.setValue(
"audioNotificationsEnabled", null,
SettingLevel.DEVICE,
event.target.checked,
checked,
).finally(() => {
this.forceUpdate();
});
@ -146,7 +136,7 @@ module.exports = React.createClass({
let emailPusherPromise;
if (event.target.checked) {
const data = {};
data['brand'] = this.props.brand || 'Riot';
data['brand'] = SdkConfig.get().brand || 'Riot';
emailPusherPromise = UserSettingsStore.addEmailPusher(address, data);
} else {
const emailPusher = UserSettingsStore.getEmailPusher(this.state.pushers, address);
@ -434,7 +424,6 @@ module.exports = React.createClass({
// Check if any legacy im.vector rules need to be ported to the new API
// for overriding the actions of default rules.
_portRulesToNewAPI: function(rulesets) {
const self = this;
const needsUpdate = [];
const cli = MatrixClientPeg.get();
@ -633,6 +622,8 @@ module.exports = React.createClass({
externalPushRules: self.state.externalPushRules,
});
}).done();
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},
_updatePushRuleActions: function(rule, actions, enabled) {
@ -691,27 +682,25 @@ module.exports = React.createClass({
return rows;
},
hasEmailPusher: function(pushers, address) {
if (pushers === undefined) {
return false;
}
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return true;
}
}
return false;
},
emailNotificationsRow: function(address, label) {
return (<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableEmailNotifications_{address}"
ref="enableEmailNotifications_{address}"
type="checkbox"
checked={ UserSettingsStore.hasEmailPusher(this.state.pushers, address) }
onChange={ this.onEnableEmailNotificationsChange.bind(this, address) }
/>
</div>
<div className="mx_UserNotifSettings_labelCell">
<label htmlFor="enableEmailNotifications_{address}">
{label}
</label>
</div>
</div>);
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} />;
},
render: function() {
const self = this;
let spinner;
if (this.state.phase === this.phases.LOADING) {
const Loader = sdk.getComponent("elements.Spinner");
@ -720,23 +709,9 @@ module.exports = React.createClass({
let masterPushRuleDiv;
if (this.state.masterPushRule) {
masterPushRuleDiv = (
<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableNotifications"
ref="enableNotifications"
type="checkbox"
checked={ !this.state.masterPushRule.enabled }
onChange={ this.onEnableNotificationsChange }
/>
</div>
<div className="mx_UserNotifSettings_labelCell">
<label htmlFor="enableNotifications">
{ _t('Enable notifications for this account') }
</label>
</div>
</div>
);
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')}/>;
}
// When enabled, the master rule inhibits all existing rules
@ -747,17 +722,17 @@ module.exports = React.createClass({
{masterPushRuleDiv}
<div className="mx_UserSettings_notifTable">
{ _t('All notifications are currently disabled for all targets.') }.
{ _t('All notifications are currently disabled for all targets.') }
</div>
</div>
);
}
const emailThreepids = this.props.threepids.filter((tp) => tp.medium === "email");
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
let emailNotificationsRow;
if (emailThreepids.length === 0) {
emailNotificationsRow = <div>
{ _t('Add an email address above to configure email notifications') }
{ _t('Add an email address to configure email notifications') }
</div>;
} else {
// This only supports the first email address in your profile for now
@ -788,7 +763,7 @@ module.exports = React.createClass({
let devicesSection;
if (this.state.pushers === undefined) {
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
} else if (this.state.pushers.length == 0) {
} else if (this.state.pushers.length === 0) {
devicesSection = null;
} else {
// TODO: It would be great to be able to delete pushers from here too,
@ -836,50 +811,17 @@ module.exports = React.createClass({
{ spinner }
<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableDesktopNotifications"
ref="enableDesktopNotifications"
type="checkbox"
checked={ SettingsStore.getValue("notificationsEnabled") }
onChange={ this.onEnableDesktopNotificationsChange } />
</div>
<div className="mx_UserNotifSettings_labelCell">
<label htmlFor="enableDesktopNotifications">
{ _t('Enable desktop notifications') }
</label>
</div>
</div>
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications')} />
<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableDesktopNotificationBody"
ref="enableDesktopNotificationBody"
type="checkbox"
checked={ SettingsStore.getValue("notificationBodyEnabled") }
onChange={ this.onEnableDesktopNotificationBodyChange } />
</div>
<div className="mx_UserNotifSettings_labelCell">
<label htmlFor="enableDesktopNotificationBody">
{ _t('Show message in desktop notification') }
</label>
</div>
</div>
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange}
label={_t('Show message in desktop notification')} />
<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableDesktopAudioNotifications"
ref="enableDesktopAudioNotifications"
type="checkbox"
checked={ SettingsStore.getValue("audioNotificationsEnabled") }
onChange={ this.onEnableAudioNotificationsChange } />
</div>
<div className="mx_UserNotifSettings_labelCell">
<label htmlFor="enableDesktopAudioNotifications">
{ _t('Enable audible notifications in web client') }
</label>
</div>
</div>
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications in web client')} />
{ emailNotificationsRow }

View file

@ -0,0 +1,247 @@
/*
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 MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import AddThreepid from "../../../AddThreepid";
import CountryDropdown from "../auth/CountryDropdown";
const sdk = require('../../../index');
const Modal = require("../../../Modal");
/*
TODO: Improve the UX for everything in here.
This is a copy/paste of EmailAddresses, mostly.
*/
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
export class ExistingPhoneNumber extends React.Component {
static propTypes = {
msisdn: PropTypes.object.isRequired,
onRemoved: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
return this.props.onRemoved(this.props.msisdn);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
render() {
if (this.state.verifyRemove) {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_ExistingPhoneNumber">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_ExistingPhoneNumber_delete" alt={_t("Remove")} />
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
</div>
);
}
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
this.state = {
msisdns: [],
verifying: false,
verifyError: false,
verifyMsisdn: "",
addTask: null,
continueDisabled: false,
phoneCountry: "",
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({msisdns: addresses.threepids.filter((a) => a.medium === 'msisdn')});
});
}
_onRemoved = (address) => {
this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)});
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.refs.newPhoneNumber) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const phoneNumber = this.refs.newPhoneNumber.value;
const phoneCountry = this.state.phoneCountry;
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => {
this.setState({continueDisabled: false, verifyMsisdn: response.msisdn});
}).catch((err) => {
console.error("Unable to add phone number " + phoneNumber + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
title: _t("Error"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
_onContinueClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
const token = this.refs.newPhoneNumberCode.value;
this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
addTask: null,
continueDisabled: false,
verifying: false,
verifyMsisdn: "",
verifyError: null,
});
this.refs.newPhoneNumber.value = "";
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify phone number: " + err);
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
title: _t("Unable to verify phone number."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
} else {
this.setState({verifyError: _t("Incorrect verification code")});
}
});
};
_onCountryChanged = (e) => {
this.setState({phoneCountry: e.iso2});
};
render() {
const existingPhoneElements = this.state.msisdns.map((p) => {
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
});
let addVerifySection = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
const msisdn = this.state.verifyMsisdn;
addVerifySection = (
<div>
<div>
{_t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains", {msisdn: msisdn})}
<br />
{this.state.verifyError}
</div>
<form onSubmit={this._onContinueClick} autoComplete={false} noValidate={true}>
<Field id="newPhoneNumberCode" ref="newPhoneNumberCode" label={_t("Verification code")}
type="text" autoComplete="off" disabled={this.state.continueDisabled} />
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</form>
</div>
);
}
return (
<div className="mx_PhoneNumbers">
{existingPhoneElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input">
<CountryDropdown onOptionChange={this._onCountryChanged}
className="mx_PhoneNumbers_country"
value={this.state.phoneCountry}
disabled={this.state.verifying}
isSmall={true}
/>
<Field id="newPhoneNumber" ref="newPhoneNumber" label={_t("Phone Number")}
type="text" autoComplete="off" disabled={this.state.verifying} />
</div>
{addVerifySection}
</form>
</div>
);
}
}

View file

@ -0,0 +1,155 @@
/*
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 MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import classNames from 'classnames';
export default class ProfileSettings extends React.Component {
constructor() {
super();
const client = MatrixClientPeg.get();
const user = client.getUser(client.getUserId());
let avatarUrl = user.avatarUrl;
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
this.state = {
userId: user.userId,
originalDisplayName: user.displayName,
displayName: user.displayName,
originalAvatarUrl: avatarUrl,
avatarUrl: avatarUrl,
avatarFile: null,
enableProfileSave: false,
};
}
_uploadAvatar = (e) => {
e.stopPropagation();
e.preventDefault();
this.refs.avatarUpload.click();
};
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this.setState({enableProfileSave: false});
const client = MatrixClientPeg.get();
const newState = {};
// TODO: What do we do about errors?
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setDisplayName(this.state.displayName);
newState.originalDisplayName = this.state.displayName;
}
if (this.state.avatarFile) {
const uri = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
}
newState.enableProfileSave = true;
this.setState(newState);
};
_onDisplayNameChanged = (e) => {
this.setState({
displayName: e.target.value,
enableProfileSave: true,
});
};
_onAvatarChanged = (e) => {
if (!e.target.files || !e.target.files.length) {
this.setState({
avatarUrl: this.state.originalAvatarUrl,
avatarFile: null,
enableProfileSave: false,
});
return;
}
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev) => {
this.setState({
avatarUrl: ev.target.result,
avatarFile: file,
enableProfileSave: true,
});
};
reader.readAsDataURL(file);
};
render() {
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
let showOverlayAnyways = true;
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
if (this.state.avatarUrl) {
showOverlayAnyways = false;
avatarElement = <img src={this.state.avatarUrl}
alt={_t("Profile picture")} />;
}
const avatarOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
});
const avatarHoverElement = (
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload profile picture")}</span>
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
<div className="mx_ProfileSettings_avatarOverlayImg" />
</div>
</div>
);
return (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<p>{this.state.userId}</p>
<Field id="profileDisplayName" label={_t("Display Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} />
</div>
<div className="mx_ProfileSettings_avatar">
{avatarElement}
{avatarHoverElement}
</div>
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
</form>
);
}
}

View file

@ -17,69 +17,176 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import GroupUserSettings from "../../groups/GroupUserSettings";
import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk";
import { DragDropContext } from 'react-beautiful-dnd';
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('../../../../index');
const Modal = require("../../../../Modal");
const dis = require("../../../../dispatcher");
export default class GeneralSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() {
super();
const client = MatrixClientPeg.get();
this.state = {
userId: client.getUserId(),
displayName: client.getUser(client.getUserId()).displayName,
enableProfileSave: false,
language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
};
}
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
};
}
if (!this.state.enableProfileSave) return;
this.setState({enableProfileSave: false});
_onLanguageChange = (newLanguage) => {
if (this.state.language === newLanguage) return;
// TODO: What do we do about errors?
await MatrixClientPeg.get().setDisplayName(this.state.displayName);
// TODO: Support avatars
this.setState({enableProfileSave: true});
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({language: newLanguage});
PlatformPeg.get().reload();
};
_onDisplayNameChanged = (e) => {
this.setState({
displayName: e.target.value,
enableProfileSave: true,
_onThemeChange = (e) => {
const newTheme = e.target.value;
if (this.state.theme === newTheme) return;
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, 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,
});
};
_renderProfileSection() {
// TODO: Ditch avatar placeholder and use the real thing
const form = (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<div className="mx_GeneralSettingsTab_profile">
<div className="mx_GeneralSettingsTab_profileControls">
<p className="mx_GeneralSettingsTab_profileUsername">{this.state.userId}</p>
<Field id="profileDisplayName" label={_t("Display Name")}
type="text" value={this.state.displayName} autocomplete="off"
onChange={this._onDisplayNameChanged} />
</div>
<div className="mx_GeneralSettingsTab_profileAvatar">
<div />
</div>
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
</form>
);
_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() {
// HACK/TODO: Using DragDropContext feels wrong, but we need it.
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
{form}
<ProfileSettings />
<span className="mx_SettingsTab_subheading">{_t("Flair")}</span>
<DragDropContext>
<GroupUserSettings />
</DragDropContext>
</div>
);
}
_renderAccountSection() {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const passwordChangeForm = (
<ChangePassword
className="mx_GeneralSettingsTab_changePassword"
rowClassName=""
buttonKind="primary"
onError={this._onPasswordChangeError}
onFinished={this._onPasswordChanged} />
);
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_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_GeneralSettingsTab_languageInput"
onOptionChange={this._onLanguageChange} value={this.state.language} />
</div>
);
}
_renderThemeSection() {
// TODO: Re-enable theme selection once the themes actually work
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return (
<div className="mx_SettingsTab_section mx_GeneralSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select" disabled={true}
value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option>
<option value="dark">{_t("Dark theme")}</option>
<option value="dharma">{_t("2018 theme")}</option>
<option value="status">{_t("Status.im 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("Close Account")}
</AccessibleButton>
</div>
);
}
@ -89,6 +196,10 @@ export default class GeneralSettingsTab extends React.Component {
<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,227 @@
/*
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("../../../../index");
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 HelpSettingsTab 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_HelpSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
<div className='mx_SettingsTab_subsectionText'>
{legalLinks}
</div>
</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>,
})
}
<AccessibleButton onClick={this._onStartBotChat} kind='primary'>
{_t("Start a chat with Riot Bot")}
</AccessibleButton>
</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_HelpSettingsTab">
<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_HelpSettingsTab_debugButton'>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
</div>
<div className='mx_HelpSettingsTab_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_HelpSettingsTab_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()}
<div className='mx_SettingsTab_section mx_HelpSettingsTab_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,92 @@
/*
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 MatrixClientPeg from "../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
const Modal = require("../../../../Modal");
const sdk = require("../../../../index");
export class LabsSettingToggle extends React.Component {
static propTypes = {
featureId: PropTypes.string.isRequired,
};
async _onLazyLoadChanging(enabling) {
// don't prevent turning LL off when not supported
if (enabling) {
const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading();
if (!supported) {
await new Promise((resolve) => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: _t("Lazy loading members not supported"),
description:
<div>
{ _t("Lazy loading is not supported by your " +
"current homeserver.") }
</div>,
button: _t("OK"),
onFinished: resolve,
});
});
return false;
}
}
return true;
}
_onChange = async (checked) => {
if (this.props.featureId === "feature_lazyloading") {
const confirmed = await this._onLazyLoadChanging(checked);
if (!confirmed) {
return;
}
}
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 LabsSettingsTab 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("../../../../index");
export default class NotificationSettingsTab extends React.Component {
constructor() {
super();
}
render() {
const Notifications = sdk.getComponent("views.settings.Notifications");
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_section">
<Notifications />
</div>
</div>
);
}
}

View file

@ -0,0 +1,125 @@
/*
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("../../../../index");
const PlatformPeg = require("../../../../PlatformPeg");
export default class PreferencesSettingsTab extends React.Component {
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.dontSuggestEmoji', // TODO: Positive wording
'dontSendTypingNotifications', // TODO: Positive wording
];
static ROOM_LIST_SETTINGS = [
'pinUnreadRooms',
'pinMentionedRooms',
];
static TIMELINE_SETTINGS = [
'autoplayGifsAndVideos',
'urlPreviewsEnabled',
'TextualBody.disableBigEmoji', // TODO: Positive wording
'hideReadReceipts', // TODO: Positive wording
'showTwelveHourTimestamps',
'alwaysShowTimestamps',
'hideRedactions', // TODO: Positive wording ("Show a placeholder for removed messages")
'enableSyntaxHighlightLanguageDetection',
'hideJoinLeaves', // TODO: Positive wording
'hideAvatarChanges', // TODO: Positive wording
'hideDisplaynameChanges', // TODO: Positive wording
];
static ADVANCED_SETTINGS = [
'alwaysShowEncryptionIcons',
'Pill.shouldHidePillAvatar', // TODO: Positive wording
'TagPanel.disableTagPanel', // TODO: Positive wording
'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_PreferencesSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesSettingsTab.COMPOSER_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesSettingsTab.ROOM_LIST_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesSettingsTab.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,242 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../utils/FormattingUtils";
import AccessibleButton from "../../elements/AccessibleButton";
import Analytics from "../../../../Analytics";
import Promise from "bluebird";
import Modal from "../../../../Modal";
import sdk from "../../../../index";
export class IgnoredUser extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
onUnignored: PropTypes.func.isRequired,
};
_onUnignoreClicked = (e) => {
this.props.onUnignored(this.props.userId);
};
render() {
return (
<div className='mx_SecuritySettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
{_t('Unignore')}
</AccessibleButton>
<span>{this.props.userId}</span>
</div>
);
}
}
export default class SecuritySettingsTab extends React.Component {
constructor() {
super();
this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
rejectingInvites: false,
};
}
_updateBlacklistDevicesFlag = (checked) => {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
};
_updateAnalytics = (checked) => {
checked ? Analytics.enable() : Analytics.disable();
};
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onUserUnignored = async (userId) => {
// Don't use this.state to get the ignored user list as it might be
// ever so slightly outdated. Instead, prefer to get a fresh list and
// update that.
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) {
ignoredUsers.splice(index, 1);
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers);
}
this.setState({ignoredUsers});
};
_onRejectAllInvitesClicked = (rooms, ev) => {
this.setState({
rejectingInvites: true,
});
// reject the invites
const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId).catch((e) => {
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
});
});
Promise.all(promises).then(() => {
this.setState({
rejectingInvites: false,
});
});
};
_renderCurrentDeviceInfo() {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
let importExportButtons = null;
if (client.isCryptoEnabled()) {
importExportButtons = (
<div className='mx_SecuritySettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
</AccessibleButton>
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
</AccessibleButton>
</div>
);
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<ul className='mx_SettingsTab_subsectionText mx_SecuritySettingsTab_deviceInfo'>
<li>
<label>{_t("Device ID:")}</label>
<span><code>{deviceId}</code></span>
</li>
<li>
<label>{_t("Device key:")}</label>
<span><code><b>{identityKey}</b></code></span>
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
</div>
);
}
_renderIgnoredUsers() {
if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null;
const userIds = this.state.ignoredUserIds
.map((u) => <IgnoredUser userId={u} onUnignored={this._onUserUnignored} key={u} />);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
<div className='mx_SettingsTab_subsectionText'>
{userIds}
</div>
</div>
);
}
_renderRejectInvites() {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
});
if (invitedRooms.length === 0) {
return null;
}
const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
<AccessibleButton onClick={onClick} kind='danger' disabled={this.state.rejectingInvites}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton>
</div>
);
}
render() {
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
let keyBackup = null;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
keyBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
<div className='mx_SettingsTab_subsectionText'>
<KeyBackupPanel />
</div>
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecuritySettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span>
<div className='mx_SettingsTab_subsectionText'>
<DevicesPanel />
</div>
</div>
{keyBackup}
{this._renderCurrentDeviceInfo()}
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Riot collects anonymous analytics to allow us to improve the application.")}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
{this._renderIgnoredUsers()}
{this._renderRejectInvites()}
</div>
);
}
}

View file

@ -0,0 +1,177 @@
/*
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("../../../../index");
const MatrixClientPeg = require("../../../../MatrixClientPeg");
export default class VoiceSettingsTab extends React.Component {
constructor() {
super();
this.state = {
mediaDevices: null,
activeAudioOutput: null,
activeAudioInput: null,
activeVideoInput: null,
};
}
componentWillMount(): void {
this._refreshMediaDevices();
}
_refreshMediaDevices = async () => {
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);
};
_setForceTurn = (forced) => {
MatrixClientPeg.get().setForceTURN(forced);
};
_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_VoiceSettingsTab_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>
);
}
}
// TODO: Make 'webRtcForceTURN' be positively worded
return (
<div className="mx_SettingsTab mx_VoiceSettingsTab">
<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='webRtcForceTURN' level={SettingLevel.DEVICE} onChange={this._setForceTurn} />
</div>
</div>
);
}
}