Merge remote-tracking branch 'origin/develop' into t3chguy/clear_notifications

This commit is contained in:
Travis Ralston 2019-11-27 19:27:52 -07:00
commit 96ffe94876
733 changed files with 50889 additions and 9584 deletions

View file

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
const MatrixClientPeg = require("../../../MatrixClientPeg");
const sdk = require('../../../index');
import createReactClass from 'create-react-class';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'ChangeAvatar',
propTypes: {
initialAvatarUrl: PropTypes.string,
@ -111,7 +112,7 @@ module.exports = React.createClass({
}
});
httpPromise.done(function() {
httpPromise.then(function() {
self.setState({
phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),

View file

@ -16,11 +16,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'ChangeDisplayName',
_getDisplayName: async function() {

View file

@ -17,20 +17,20 @@ limitations under the License.
import Field from "../elements/Field";
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal");
const sdk = require("../../../index");
import dis from "../../../dispatcher";
import Promise from 'bluebird';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
import sessionStore from '../../../stores/SessionStore';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'ChangePassword',
propTypes: {
@ -173,21 +173,16 @@ module.exports = React.createClass({
newPassword: "",
newPasswordConfirm: "",
});
}).done();
});
},
_optionallySetEmail: function() {
const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'),
onFinished: (confirmed) => {
// ignore confirmed, setting an email is optional
deferred.resolve(confirmed);
},
});
return deferred.promise;
return modal.finished.then(([confirmed]) => confirmed);
},
_onExportE2eKeysClicked: function() {

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -51,7 +52,7 @@ export default class DevicesPanel extends React.Component {
}
_loadDevices() {
MatrixClientPeg.get().getDevices().done(
MatrixClientPeg.get().getDevices().then(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});
@ -186,7 +187,7 @@ export default class DevicesPanel extends React.Component {
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<AccessibleButton className="mx_textButton" onClick={this._onDeleteClick}>
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
</AccessibleButton>;
@ -194,11 +195,11 @@ export default class DevicesPanel extends React.Component {
return (
<div className={classes}>
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div>
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
<div className="mx_DevicesPanel_deviceId">{ _t("ID") }</div>
<div className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</div>
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
<div className="mx_DevicesPanel_deviceButtons">
{ this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') }
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
</div>
</div>
{ devices.map(this._renderDevice) }
@ -207,7 +208,6 @@ export default class DevicesPanel extends React.Component {
}
}
DevicesPanel.displayName = 'MemberDeviceInfo';
DevicesPanel.propTypes = {
className: PropTypes.string,
};

View file

@ -81,7 +81,7 @@ export default class DevicesPanelEntry extends React.Component {
{ lastSeen }
</div>
<div className="mx_DevicesPanel_deviceButtons">
<input type="checkbox" onClick={this.onDeviceToggled} checked={this.props.selected} />
<input type="checkbox" onChange={this.onDeviceToggled} checked={this.props.selected} />
</div>
</div>
);

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const React = require("react");
const Notifier = require("../../../Notifier");
const dis = require("../../../dispatcher");
import React from "react";
import createReactClass from 'create-react-class';
import Notifier from "../../../Notifier";
import dis from "../../../dispatcher";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'EnableNotificationsButton',
componentDidMount: function() {

View file

@ -0,0 +1,90 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
export default class IntegrationManager extends React.Component {
static propTypes = {
// false to display an error saying that we couldn't connect to the integration manager
connected: PropTypes.bool.isRequired,
// true to display a loading spinner
loading: PropTypes.bool.isRequired,
// The source URL to load
url: PropTypes.string,
// callback when the manager is dismissed
onFinished: PropTypes.func.isRequired,
};
static defaultProps = {
connected: true,
loading: false,
};
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
}
onKeyDown = (ev) => {
if (ev.keyCode === 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
};
onAction = (payload) => {
if (payload.action === 'close_scalar') {
this.props.onFinished();
}
};
render() {
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className='mx_IntegrationManager_loading'>
<h3>{_t("Connecting to integration manager...")}</h3>
<Spinner />
</div>
);
}
if (!this.props.connected) {
return (
<div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3>
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
</div>
);
}
return <iframe src={this.props.url}></iframe>;
}
}

View file

@ -1,63 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
'use strict';
const React = require('react');
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const dis = require('../../../dispatcher');
module.exports = React.createClass({
displayName: 'IntegrationsManager',
propTypes: {
src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
},
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
// dialog base class somehow, surely...
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
},
onKeyDown: function(ev) {
if (ev.keyCode == 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
},
onAction: function(payload) {
if (payload.action === 'close_scalar') {
this.props.onFinished();
}
},
render: function() {
return (
<iframe src={ this.props.src }></iframe>
);
},
});

View file

@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
} else if (this.state.loading) {
return <Spinner />;
} else if (this.state.backupInfo) {
const EmojiText = sdk.getComponent('elements.EmojiText');
let clientBackupStatus;
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("This device is backing up your keys. ")}<EmojiText></EmojiText></p>
<p> {_t("This device is backing up your keys. ")}</p>
</div>;
} else {
clientBackupStatus = <div>

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import Promise from 'bluebird';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -63,7 +63,7 @@ function portLegacyActions(actions) {
}
}
module.exports = React.createClass({
module.exports = createReactClass({
displayName: 'Notifications',
phases: {
@ -97,7 +97,7 @@ module.exports = React.createClass({
phase: this.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() {
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() {
self._refreshFromServer();
});
},
@ -170,7 +170,7 @@ module.exports = React.createClass({
emailPusher.kind = null;
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
}
emailPusherPromise.done(() => {
emailPusherPromise.then(() => {
this._refreshFromServer();
}, (error) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -274,7 +274,7 @@ module.exports = React.createClass({
}
}
Promise.all(deferreds).done(function() {
Promise.all(deferreds).then(function() {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -343,7 +343,7 @@ module.exports = React.createClass({
}
}
Promise.all(deferreds).done(function(resps) {
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -398,7 +398,7 @@ module.exports = React.createClass({
};
// Then, add the new ones
Promise.all(removeDeferreds).done(function(resps) {
Promise.all(removeDeferreds).then(function(resps) {
const deferreds = [];
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
@ -434,7 +434,7 @@ module.exports = React.createClass({
}
}
Promise.all(deferreds).done(function(resps) {
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
@ -650,7 +650,7 @@ module.exports = React.createClass({
externalContentRules: self.state.externalContentRules,
externalPushRules: self.state.externalPushRules,
});
}).done();
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},

View file

@ -155,7 +155,7 @@ export default class ProfileSettings extends React.Component {
}
return (
<form onSubmit={this._saveProfile} autoComplete={false} noValidate={true}>
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">

View file

@ -0,0 +1,426 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 url from 'url';
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from '../../../index';
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import dis from "../../../dispatcher";
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import {timeout} from "../../../utils/promise";
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
/**
* Check an IS URL is valid, including liveness check
*
* @param {string} u The url to check
* @returns {string} null if url passes all checks, otherwise i18ned error string
*/
async function checkIdentityServerUrl(u) {
const parsedUrl = url.parse(u);
if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS");
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
try {
const response = await fetch(u + '/_matrix/identity/api/v1');
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
return _t("Not a valid Identity Server (status code %(code)s)", {code: response.status});
} else {
return _t("Could not connect to Identity Server");
}
} catch (e) {
return _t("Could not connect to Identity Server");
}
}
export default class SetIdServer extends React.Component {
static propTypes = {
// Whether or not the ID server is missing terms. This affects the text
// shown to the user.
missingTerms: PropTypes.bool,
};
constructor() {
super();
let defaultIdServer = '';
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
// If no ID server is configured but there's one in the config, prepopulate
// the field to help the user.
defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl());
}
this.state = {
defaultIdServer,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
idServer: "",
error: null,
busy: false,
disconnectBusy: false,
checking: false,
};
}
componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
}
onAction = (payload) => {
// We react to changes in the ID server in the event the user is staring at this form
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
this.setState({
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
});
};
_onIdentityServerChanged = (ev) => {
const u = ev.target.value;
this.setState({idServer: u});
};
_getTooltip = () => {
if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div>
<InlineSpinner />
{ _t("Checking server") }
</div>;
} else if (this.state.error) {
return <span className='warning'>{this.state.error}</span>;
} else {
return null;
}
};
_idServerChangeEnabled = () => {
return !!this.state.idServer && !this.state.busy;
};
_saveIdServer = (fullUrl) => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: fullUrl,
});
this.setState({
busy: false,
error: null,
currentClientIdServer: fullUrl,
idServer: '',
});
};
_checkIdServer = async (e) => {
e.preventDefault();
const { idServer, currentClientIdServer } = this.state;
this.setState({busy: true, checking: true, error: null});
const fullUrl = unabbreviateUrl(idServer);
let errStr = await checkIdentityServerUrl(fullUrl);
if (!errStr) {
try {
this.setState({checking: false}); // clear tooltip
// Test the identity server by trying to register with it. This
// may result in a terms of service prompt.
const authClient = new IdentityAuthClient(fullUrl);
await authClient.getAccessToken();
let save = true;
// Double check that the identity server even has terms of service.
const hasTerms = await doesIdentityServerHaveTerms(fullUrl);
if (!hasTerms) {
const [confirmed] = await this._showNoTermsWarning(fullUrl);
save = confirmed;
}
// Show a general warning, possibly with details about any bound
// 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
const [confirmed] = await this._showServerChangeWarning({
title: _t("Change identity server"),
unboundMessage: _t(
"Disconnect from the identity server <current /> and " +
"connect to <new /> instead?", {},
{
current: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
new: sub => <b>{abbreviateUrl(idServer)}</b>,
},
),
button: _t("Continue"),
});
save = confirmed;
}
if (save) {
this._saveIdServer(fullUrl);
}
} catch (e) {
console.error(e);
errStr = _t("Terms of service not accepted or the identity server is invalid.");
}
}
this.setState({
busy: false,
checking: false,
error: errStr,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
});
};
_showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
<div>
<span className="warning">
{_t("The identity server you have chosen does not have any terms of service.")}
</span>
<span>
&nbsp;{_t("Only continue if you trust the owner of the server.")}
</span>
</div>
),
button: _t("Continue"),
});
return finished;
}
_onDisconnectClicked = async () => {
this.setState({disconnectBusy: true});
try {
const [confirmed] = await this._showServerChangeWarning({
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {},
{idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>},
),
button: _t("Disconnect"),
});
if (confirmed) {
this._disconnectIdServer();
}
} finally {
this.setState({disconnectBusy: false});
}
};
async _showServerChangeWarning({ title, unboundMessage, button }) {
const { currentClientIdServer } = this.state;
let threepids = [];
let currentServerReachable = true;
try {
threepids = await timeout(
getThreepidsWithBindStatus(MatrixClientPeg.get()),
Promise.reject(new Error("Timeout attempting to reach identity server")),
REACHABILITY_TIMEOUT,
);
} catch (e) {
currentServerReachable = false;
console.warn(
`Unable to reach identity server at ${currentClientIdServer} to check ` +
`for 3PIDs during IS change flow`,
);
console.warn(e);
}
const boundThreepids = threepids.filter(tp => tp.bound);
let message;
let danger = false;
const messageElements = {
idserver: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
b: sub => <b>{sub}</b>,
};
if (!currentServerReachable) {
message = <div>
<p>{_t(
"You should <b>remove your personal data</b> from identity server " +
"<idserver /> before disconnecting. Unfortunately, identity server " +
"<idserver /> is currently offline or cannot be reached.",
{}, messageElements,
)}</p>
<p>{_t("You should:")}</p>
<ul>
<li>{_t(
"check your browser plugins for anything that might block " +
"the identity server (such as Privacy Badger)",
)}</li>
<li>{_t("contact the administrators of identity server <idserver />", {}, {
idserver: messageElements.idserver,
})}</li>
<li>{_t("wait and try again later")}</li>
</ul>
</div>;
danger = true;
button = _t("Disconnect anyway");
} else if (boundThreepids.length) {
message = <div>
<p>{_t(
"You are still <b>sharing your personal data</b> on the identity " +
"server <idserver />.", {}, messageElements,
)}</p>
<p>{_t(
"We recommend that you remove your email addresses and phone numbers " +
"from the identity server before disconnecting.",
)}</p>
</div>;
danger = true;
button = _t("Disconnect anyway");
} else {
message = unboundMessage;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, {
title,
description: message,
button,
cancelButton: _t("Go back"),
danger,
});
return finished;
}
_disconnectIdServer = () => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: null, // clear
});
let newFieldVal = '';
if (getDefaultIdentityServerUrl()) {
// Prepopulate the client's default so the user at least has some idea of
// a valid value they might enter
newFieldVal = abbreviateUrl(getDefaultIdentityServerUrl());
}
this.setState({
busy: false,
error: null,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
idServer: newFieldVal,
});
};
render() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const Field = sdk.getComponent('elements.Field');
const idServerUrl = this.state.currentClientIdServer;
let sectionTitle;
let bodyText;
if (idServerUrl) {
sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
bodyText = _t(
"You are currently using <server></server> to discover and be discoverable by " +
"existing contacts you know. You can change your identity server below.",
{},
{ server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
);
if (this.props.missingTerms) {
bodyText = _t(
"If you don't want to use <server /> to discover and be discoverable by existing " +
"contacts you know, enter another identity server below.",
{}, {server: sub => <b>{abbreviateUrl(idServerUrl)}</b>},
);
}
} else {
sectionTitle = _t("Identity Server");
bodyText = _t(
"You are not currently using an identity server. " +
"To discover and be discoverable by existing contacts you know, " +
"add one below.",
);
}
let discoSection;
if (idServerUrl) {
let discoButtonContent = _t("Disconnect");
let discoBodyText = _t(
"Disconnecting from your identity server will mean you " +
"won't be discoverable by other users and you won't be " +
"able to invite others by email or phone.",
);
if (this.props.missingTerms) {
discoBodyText = _t(
"Using an identity server is optional. If you choose not to " +
"use an identity server, you won't be discoverable by other users " +
"and you won't be able to invite others by email or phone.",
);
discoButtonContent = _t("Do not use an identity server");
}
if (this.state.disconnectBusy) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
discoButtonContent = <InlineSpinner />;
}
discoSection = <div>
<span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
<AccessibleButton onClick={this._onDisconnectClicked} kind="danger_sm">
{discoButtonContent}
</AccessibleButton>
</div>;
}
return (
<form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this._checkIdServer}>
<span className="mx_SettingsTab_subheading">
{sectionTitle}
</span>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
</span>
<Field label={_t("Enter a new identity server")}
id="mx_SetIdServer_idServer"
type="text"
autoComplete="off"
placeholder={this.state.defaultIdServer}
value={this.state.idServer}
onChange={this._onIdentityServerChanged}
tooltipContent={this._getTooltip()}
tooltipClassName="mx_SetIdServer_tooltip"
disabled={this.state.busy}
flagInvalid={!!this.state.error}
/>
<AccessibleButton type="submit" kind="primary_sm"
onClick={this._checkIdServer}
disabled={!this._idServerChangeEnabled()}
>{_t("Change")}</AccessibleButton>
{discoSection}
</form>
);
}
}

View file

@ -0,0 +1,83 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import sdk from '../../../index';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class SetIntegrationManager extends React.Component {
constructor() {
super();
const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager();
this.state = {
currentManager,
provisioningEnabled: SettingsStore.getValue("integrationProvisioning"),
};
}
onProvisioningToggled = () => {
const current = this.state.provisioningEnabled;
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
console.error("Error changing integration manager provisioning");
console.error(err);
this.setState({provisioningEnabled: current});
});
this.setState({provisioningEnabled: !current});
};
render() {
const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch");
const currentManager = this.state.currentManager;
let managerName;
let bodyText;
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{serverName: currentManager.name},
{ b: sub => <b>{sub}</b> },
);
} else {
bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
}
return (
<div className='mx_SetIntegrationManager'>
<div className="mx_SettingsTab_heading">
<span>{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
<ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
</div>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
<br />
<br />
{_t(
"Integration Managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
</span>
</div>
);
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,14 +17,14 @@ 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");
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";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -86,15 +87,15 @@ export class ExistingEmailAddress extends React.Component {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Are you sure?")}
{_t("Remove %(email)s?", {email: this.props.email.address} )}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("Yes")}
{_t("Remove")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("No")}
{_t("Cancel")}
</AccessibleButton>
</div>
);
@ -102,20 +103,25 @@ export class ExistingEmailAddress extends React.Component {
return (
<div className="mx_ExistingEmailAddress">
<img src={require("../../../../res/img/feather-customised/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>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
</AccessibleButton>
</div>
);
}
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
static propTypes = {
emails: PropTypes.array.isRequired,
onEmailsChange: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
emails: [],
verifying: false,
addTask: null,
continueDisabled: false,
@ -123,16 +129,9 @@ export default class EmailAddresses extends React.Component {
};
}
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)});
const emails = this.props.emails.filter((e) => e !== address);
this.props.onEmailsChange(emails);
};
_onChangeNewEmailAddress = (e) => {
@ -162,7 +161,7 @@ export default class EmailAddresses extends React.Component {
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addEmailAddress(email, true).then(() => {
task.addEmailAddress(email).then(() => {
this.setState({continueDisabled: false});
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
@ -182,17 +181,27 @@ export default class EmailAddresses extends React.Component {
this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.state.newEmailAddress;
this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null,
continueDisabled: false,
verifying: false,
newEmailAddress: "",
});
const emails = [
...this.props.emails,
{ address: email, medium: "email" },
];
this.props.onEmailsChange(emails);
}).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);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t("Click the link in the email you received to verify " +
"and then click continue again."),
});
} else {
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")),
@ -202,7 +211,7 @@ export default class EmailAddresses extends React.Component {
};
render() {
const existingEmailElements = this.state.emails.map((e) => {
const existingEmailElements = this.props.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
});
@ -226,7 +235,7 @@ export default class EmailAddresses extends React.Component {
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete={false}
<form onSubmit={this._onAddClick} autoComplete="off"
noValidate={true} className="mx_EmailAddresses_new">
<Field id="mx_EmailAddressses_newEmailAddress"
type="text"

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,14 +17,14 @@ 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");
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";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -81,15 +82,15 @@ export class ExistingPhoneNumber extends React.Component {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{_t("Are you sure?")}
{_t("Remove %(phone)s?", {phone: this.props.msisdn.address})}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
<AccessibleButton onClick={this._onActuallyRemove} kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("Yes")}
{_t("Remove")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
<AccessibleButton onClick={this._onDontRemove} kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn">
{_t("No")}
{_t("Cancel")}
</AccessibleButton>
</div>
);
@ -97,20 +98,25 @@ export class ExistingPhoneNumber extends React.Component {
return (
<div className="mx_ExistingPhoneNumber">
<img src={require("../../../../res/img/feather-customised/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>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
</AccessibleButton>
</div>
);
}
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
static propTypes = {
msisdns: PropTypes.array.isRequired,
onMsisdnsChange: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
msisdns: [],
verifying: false,
verifyError: false,
verifyMsisdn: "",
@ -122,16 +128,9 @@ export default class PhoneNumbers extends React.Component {
};
}
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)});
const msisdns = this.props.msisdns.filter((e) => e !== address);
this.props.onMsisdnsChange(msisdns);
};
_onChangeNewPhoneNumber = (e) => {
@ -159,7 +158,7 @@ export default class PhoneNumbers extends React.Component {
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => {
task.addMsisdn(phoneCountry, phoneNumber).then((response) => {
this.setState({continueDisabled: false, verifyMsisdn: response.msisdn});
}).catch((err) => {
console.error("Unable to add phone number " + phoneNumber + " " + err);
@ -177,9 +176,9 @@ export default class PhoneNumbers extends React.Component {
this.setState({continueDisabled: true});
const token = this.state.newPhoneNumberCode;
const address = this.state.verifyMsisdn;
this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
addTask: null,
continueDisabled: false,
verifying: false,
@ -188,6 +187,11 @@ export default class PhoneNumbers extends React.Component {
newPhoneNumber: "",
newPhoneNumberCode: "",
});
const msisdns = [
...this.props.msisdns,
{ address, medium: "msisdn" },
];
this.props.onMsisdnsChange(msisdns);
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -208,7 +212,7 @@ export default class PhoneNumbers extends React.Component {
};
render() {
const existingPhoneElements = this.state.msisdns.map((p) => {
const existingPhoneElements = this.props.msisdns.map((p) => {
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
});
@ -223,11 +227,11 @@ export default class PhoneNumbers extends React.Component {
<div>
<div>
{_t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains", { msisdn: msisdn })}
"Please enter the verification code it contains.", { msisdn: msisdn })}
<br />
{this.state.verifyError}
</div>
<form onSubmit={this._onContinueClick} autoComplete={false} noValidate={true}>
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
<Field id="mx_PhoneNumbers_newPhoneNumberCode"
type="text"
label={_t("Verification code")}
@ -256,8 +260,7 @@ export default class PhoneNumbers extends React.Component {
return (
<div className="mx_PhoneNumbers">
{existingPhoneElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_PhoneNumbers_new">
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input">
<Field id="mx_PhoneNumbers_newPhoneNumber"
type="text"
@ -269,8 +272,8 @@ export default class PhoneNumbers extends React.Component {
onChange={this._onChangeNewPhoneNumber}
/>
</div>
{addVerifySection}
</form>
{addVerifySection}
</div>
);
}

View file

@ -0,0 +1,258 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
/*
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.
*/
/*
TODO: Reduce all the copying between account vs. discovery components.
*/
export class EmailAddress extends React.Component {
static propTypes = {
email: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const { bound } = props.email;
this.state = {
verifying: false,
addTask: null,
continueDisabled: false,
bound,
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.email;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email;
try {
if (bind) {
const task = new AddThreepid();
this.setState({
verifying: true,
continueDisabled: true,
addTask: task,
});
await task.bindEmailAddress(address);
this.setState({
continueDisabled: false,
});
} else {
await MatrixClientPeg.get().unbindThreePid(medium, address);
}
this.setState({ bound: bind });
} catch (err) {
console.error(`Unable to ${label} email address ${address} ${err}`);
this.setState({
verifying: false,
continueDisabled: false,
addTask: null,
});
Modal.createTrackedDialog(`Unable to ${label} email address`, '', ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email;
const task = new AddThreepid();
this.setState({
verifying: true,
continueDisabled: true,
addTask: task,
});
try {
await MatrixClientPeg.get().deleteThreePid(medium, address);
if (bind) {
await task.bindEmailAddress(address);
} else {
await task.addEmailAddress(address);
}
this.setState({
continueDisabled: false,
bound: bind,
});
} catch (err) {
console.error(`Unable to ${label} email address ${address} ${err}`);
this.setState({
verifying: false,
continueDisabled: false,
addTask: null,
});
Modal.createTrackedDialog(`Unable to ${label} email address`, '', ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}
onRevokeClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.changeBinding({
bind: false,
label: "revoke",
errorTitle: _t("Unable to revoke sharing for email address"),
});
}
onShareClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.changeBinding({
bind: true,
label: "share",
errorTitle: _t("Unable to share email address"),
});
}
onContinueClick = async (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({ continueDisabled: true });
try {
await this.state.addTask.checkEmailLinkClicked();
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
});
} catch (err) {
this.setState({ continueDisabled: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t("Click the link in the email you received to verify " +
"and then click continue again."),
});
} else {
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 AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const { address } = this.props.email;
const { verifying, bound } = this.state;
let status;
if (verifying) {
status = <span>
{_t("Verify the link in your inbox")}
<AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onContinueClick}
>
{_t("Complete")}
</AccessibleButton>
</span>;
} else if (bound) {
status = <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
</AccessibleButton>;
} else {
status = <AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onShareClick}
>
{_t("Share")}
</AccessibleButton>;
}
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{address}</span>
{status}
</div>
);
}
}
export default class EmailAddresses extends React.Component {
static propTypes = {
emails: PropTypes.array.isRequired,
}
render() {
let content;
if (this.props.emails.length > 0) {
content = this.props.emails.map((e) => {
return <EmailAddress email={e} key={e.address} />;
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added an email above.")}
</span>;
}
return (
<div className="mx_EmailAddresses">
{content}
</div>
);
}
}

View file

@ -0,0 +1,271 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
/*
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 PhoneNumber extends React.Component {
static propTypes = {
msisdn: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const { bound } = props.msisdn;
this.state = {
verifying: false,
verificationCode: "",
addTask: null,
continueDisabled: false,
bound,
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.msisdn;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn;
try {
if (bind) {
const task = new AddThreepid();
this.setState({
verifying: true,
continueDisabled: true,
addTask: task,
});
// XXX: Sydent will accept a number without country code if you add
// a leading plus sign to a number in E.164 format (which the 3PID
// address is), but this goes against the spec.
// See https://github.com/matrix-org/matrix-doc/issues/2222
await task.bindMsisdn(null, `+${address}`);
this.setState({
continueDisabled: false,
});
} else {
await MatrixClientPeg.get().unbindThreePid(medium, address);
}
this.setState({ bound: bind });
} catch (err) {
console.error(`Unable to ${label} phone number ${address} ${err}`);
this.setState({
verifying: false,
continueDisabled: false,
addTask: null,
});
Modal.createTrackedDialog(`Unable to ${label} phone number`, '', ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn;
const task = new AddThreepid();
this.setState({
verifying: true,
continueDisabled: true,
addTask: task,
});
try {
await MatrixClientPeg.get().deleteThreePid(medium, address);
// XXX: Sydent will accept a number without country code if you add
// a leading plus sign to a number in E.164 format (which the 3PID
// address is), but this goes against the spec.
// See https://github.com/matrix-org/matrix-doc/issues/2222
if (bind) {
await task.bindMsisdn(null, `+${address}`);
} else {
await task.addMsisdn(null, `+${address}`);
}
this.setState({
continueDisabled: false,
bound: bind,
});
} catch (err) {
console.error(`Unable to ${label} phone number ${address} ${err}`);
this.setState({
verifying: false,
continueDisabled: false,
addTask: null,
});
Modal.createTrackedDialog(`Unable to ${label} phone number`, '', ErrorDialog, {
title: errorTitle,
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}
onRevokeClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.changeBinding({
bind: false,
label: "revoke",
errorTitle: _t("Unable to revoke sharing for phone number"),
});
}
onShareClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.changeBinding({
bind: true,
label: "share",
errorTitle: _t("Unable to share phone number"),
});
}
onVerificationCodeChange = (e) => {
this.setState({
verificationCode: e.target.value,
});
}
onContinueClick = async (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({ continueDisabled: true });
const token = this.state.verificationCode;
try {
await this.state.addTask.haveMsisdnToken(token);
this.setState({
addTask: null,
continueDisabled: false,
verifying: false,
verifyError: null,
verificationCode: "",
});
} 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")});
}
}
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Field = sdk.getComponent('elements.Field');
const { address } = this.props.msisdn;
const { verifying, bound } = this.state;
let status;
if (verifying) {
status = <span className="mx_ExistingPhoneNumber_verification">
<span>
{_t("Please enter verification code sent via text.")}
<br />
{this.state.verifyError}
</span>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field id="mx_PhoneNumbers_newPhoneNumberCode"
type="text"
label={_t("Verification code")}
autoComplete="off"
disabled={this.state.continueDisabled}
value={this.state.verificationCode}
onChange={this.onVerificationCodeChange}
/>
</form>
</span>;
} else if (bound) {
status = <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
</AccessibleButton>;
} else {
status = <AccessibleButton
className="mx_ExistingPhoneNumber_confirmBtn"
kind="primary_sm"
onClick={this.onShareClick}
>
{_t("Share")}
</AccessibleButton>;
}
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{address}</span>
{status}
</div>
);
}
}
export default class PhoneNumbers extends React.Component {
static propTypes = {
msisdns: PropTypes.array.isRequired,
}
render() {
let content;
if (this.props.msisdns.length > 0) {
content = this.props.msisdns.map((e) => {
return <PhoneNumber msisdn={e} key={e.address} />;
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added a phone number above.")}
</span>;
}
return (
<div className="mx_PhoneNumbers">
{content}
</div>
);
}
}

View file

@ -0,0 +1,178 @@
/*
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 AccessibleButton from "../../../elements/AccessibleButton";
import Notifier from "../../../../../Notifier";
import SettingsStore from '../../../../../settings/SettingsStore';
import { SettingLevel } from '../../../../../settings/SettingsStore';
export default class NotificationsSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
};
constructor() {
super();
this.state = {
currentSound: "default",
uploadedFile: null,
};
}
componentWillMount() {
Notifier.getSoundForRoom(this.props.roomId).then((soundData) => {
if (!soundData) {
return;
}
this.setState({currentSound: soundData.name || soundData.url});
});
}
async _triggerUploader(e) {
e.stopPropagation();
e.preventDefault();
this.refs.soundUpload.click();
}
async _onSoundUploadChanged(e) {
if (!e.target.files || !e.target.files.length) {
this.setState({
uploadedFile: null,
});
return;
}
const file = e.target.files[0];
this.setState({
uploadedFile: file,
});
}
async _onClickSaveSound(e) {
e.stopPropagation();
e.preventDefault();
try {
await this._saveSound();
} catch (ex) {
console.error(
`Unable to save notification sound for ${this.props.roomId}`,
);
console.error(ex);
}
}
async _saveSound() {
if (!this.state.uploadedFile) {
return;
}
let type = this.state.uploadedFile.type;
if (type === "video/ogg") {
// XXX: I've observed browsers allowing users to pick a audio/ogg files,
// and then calling it a video/ogg. This is a lame hack, but man browsers
// suck at detecting mimetypes.
type = "audio/ogg";
}
const url = await MatrixClientPeg.get().uploadContent(
this.state.uploadedFile, {
type,
},
);
await SettingsStore.setValue(
"notificationSound",
this.props.roomId,
SettingLevel.ROOM_ACCOUNT,
{
name: this.state.uploadedFile.name,
type: type,
size: this.state.uploadedFile.size,
url,
},
);
this.setState({
uploadedFile: null,
currentSound: this.state.uploadedFile.name,
});
}
_clearSound(e) {
e.stopPropagation();
e.preventDefault();
SettingsStore.setValue(
"notificationSound",
this.props.roomId,
SettingLevel.ROOM_ACCOUNT,
null,
);
this.setState({
currentSound: "default",
});
}
render() {
let currentUploadedFile = null;
if (this.state.uploadedFile) {
currentUploadedFile = (
<div>
<span>{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code></span>
</div>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Sounds")}</span>
<div>
<span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
{_t("Reset")}
</AccessibleButton>
</div>
<div>
<h3>{_t("Set a new custom sound")}</h3>
<form autoComplete="off" noValidate={true}>
<input ref="soundUpload" className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
</form>
{currentUploadedFile}
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
{_t("Browse")}
</AccessibleButton>
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
{_t("Save")}
</AccessibleButton>
<br />
</div>
</div>
</div>
);
}
}

View file

@ -30,6 +30,8 @@ const plEventsToLabels = {
"m.room.history_visibility": _td("Change history visibility"),
"m.room.power_levels": _td("Change permissions"),
"m.room.topic": _td("Change topic"),
"m.room.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"),
"im.vector.modular.widgets": _td("Modify widgets"),
};
@ -42,6 +44,8 @@ const plEventsToShow = {
"m.room.history_visibility": {isState: true},
"m.room.power_levels": {isState: true},
"m.room.topic": {isState: true},
"m.room.tombstone": {isState: true},
"m.room.encryption": {isState: true},
"im.vector.modular.widgets": {isState: true},
};
@ -59,13 +63,10 @@ export class BannedUser extends React.Component {
member: PropTypes.object.isRequired, // js-sdk RoomMember
by: PropTypes.string.isRequired,
reason: PropTypes.string,
onUnbanned: PropTypes.func.isRequired,
};
_onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).then(() => {
this.props.onUnbanned();
}).catch((err) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
@ -105,6 +106,22 @@ export default class RolesRoomSettingsTab extends React.Component {
roomId: PropTypes.string.isRequired,
};
componentDidMount(): void {
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership.bind(this));
}
componentWillUnmount(): void {
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("RoomState.members", this._onRoomMembership.bind(this));
}
}
_onRoomMembership(event, state, member) {
if (state.roomId !== this.props.roomId) return;
this.forceUpdate();
}
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
@ -144,7 +161,45 @@ export default class RolesRoomSettingsTab extends React.Component {
parentObj[keyPath[keyPath.length - 1]] = value;
}
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent);
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, {
title: _t('Error changing power level requirement'),
description: _t(
"An error occurred changing the room's power level requirements. Ensure you have sufficient " +
"permissions and try again.",
),
});
});
};
_onUserPowerLevelChanged = (value, powerLevelKey) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
let plContent = plEvent ? (plEvent.getContent() || {}) : {};
// Clone the power levels just in case
plContent = Object.assign({}, plContent);
// powerLevelKey should be a user ID
if (!plContent['users']) plContent['users'] = {};
plContent['users'][powerLevelKey] = value;
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Power level change failed', '', ErrorDialog, {
title: _t('Error changing power level'),
description: _t(
"An error occurred changing the user's power level. Ensure you have sufficient " +
"permissions and try again.",
),
});
});
};
render() {
@ -216,15 +271,29 @@ export default class RolesRoomSettingsTab extends React.Component {
const privilegedUsers = [];
const mutedUsers = [];
Object.keys(userLevels).forEach(function(user) {
Object.keys(userLevels).forEach((user) => {
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(
<PowerSelector value={userLevels[user]} disabled={!canChange} label={user} key={user} />,
<PowerSelector
value={userLevels[user]}
disabled={!canChange}
label={user}
key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
onChange={this._onUserPowerLevelChanged}
/>,
);
} else if (userLevels[user] < defaultUserLevel) { // muted
mutedUsers.push(
<PowerSelector value={userLevels[user]} disabled={!canChange} label={user} key={user} />,
<PowerSelector
value={userLevels[user]}
disabled={!canChange}
label={user}
key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
onChange={this._onUserPowerLevelChanged}
/>,
);
}
});
@ -270,7 +339,7 @@ export default class RolesRoomSettingsTab extends React.Component {
return (
<BannedUser key={member.userId} canUnban={canBanUsers}
member={member} reason={banEvent.reason}
by={bannedBy} onUnbanned={this.forceUpdate} />
by={bannedBy} />
);
})}
</ul>
@ -302,6 +371,11 @@ export default class RolesRoomSettingsTab extends React.Component {
</div>;
});
// hide the power level selector for enabling E2EE if it the room is already encrypted
if (client.isRoomEncrypted(this.props.roomId)) {
delete eventsLevels["m.room.encryption"];
}
const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => {
let label = plEventsToLabels[eventType];
if (label) {

View file

@ -1,5 +1,7 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,8 +19,6 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import ProfileSettings from "../../ProfileSettings";
import EmailAddresses from "../../EmailAddresses";
import PhoneNumbers from "../../PhoneNumbers";
import Field from "../../../elements/Field";
import * as languageHandler from "../../../../../languageHandler";
import {SettingLevel} from "../../../../../settings/SettingsStore";
@ -26,19 +26,140 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
const PlatformPeg = require("../../../../../PlatformPeg");
const sdk = require('../../../../..');
const Modal = require("../../../../../Modal");
const dis = require("../../../../../dispatcher");
import PropTypes from "prop-types";
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../..";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher";
import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false,
requiredPolicyInfo: { // This object is passed along to a component for handling
hasTerms: false,
// policiesAndServices, // From the startTermsFlow callback
// agreedUrls, // From the startTermsFlow callback
// resolve, // Promise resolve function for startTermsFlow callback
},
emails: [],
msisdns: [],
};
this.dispatcherRef = dis.register(this._onAction);
}
async componentWillMount() {
const cli = MatrixClientPeg.get();
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
this.setState({serverSupportsSeparateAddAndBind});
this._getThreepidState();
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
this._getThreepidState();
}
};
_onEmailsChange = (emails) => {
this.setState({ emails });
}
_onMsisdnsChange = (msisdns) => {
this.setState({ msisdns });
}
async _getThreepidState() {
const cli = MatrixClientPeg.get();
// Check to see if terms need accepting
this._checkTerms();
// Need to get 3PIDs generally for Account section and possibly also for
// Discovery (assuming we have an IS and terms are agreed).
let threepids = [];
try {
threepids = await getThreepidsWithBindStatus(cli);
} catch (e) {
const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
console.warn(
`Unable to reach identity server at ${idServerUrl} to check ` +
`for 3PIDs bindings in Settings`,
);
console.warn(e);
}
this.setState({ emails: threepids.filter((a) => a.medium === 'email') });
this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') });
}
async _checkTerms() {
if (!this.state.haveIdServer) {
this.setState({idServerHasUnsignedTerms: false});
return;
}
// By starting the terms flow we get the logic for checking which terms the user has signed
// for free. So we might as well use that for our own purposes.
const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
const authClient = new IdentityAuthClient();
const idAccessToken = await authClient.getAccessToken({ check: false });
try {
await startTermsFlow([new Service(
SERVICE_TYPES.IS,
idServerUrl,
idAccessToken,
)], (policiesAndServices, agreedUrls, extraClassNames) => {
return new Promise((resolve, reject) => {
this.setState({
idServerName: abbreviateUrl(idServerUrl),
requiredPolicyInfo: {
hasTerms: true,
policiesAndServices,
agreedUrls,
resolve,
},
});
});
});
// User accepted all terms
this.setState({
requiredPolicyInfo: {
hasTerms: false,
},
});
} catch (e) {
console.warn(
`Unable to reach identity server at ${idServerUrl} to check ` +
`for terms in Settings`,
);
console.warn(e);
}
}
_onLanguageChange = (newLanguage) => {
@ -53,11 +174,29 @@ export default class GeneralUserSettingsTab extends React.Component {
const newTheme = e.target.value;
if (this.state.theme === newTheme) return;
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
// doing getValue in the .catch will still return the value we failed to set,
// so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
dis.dispatch({action: 'recheck_theme'});
this.setState({theme: oldTheme});
});
this.setState({theme: newTheme});
dis.dispatch({action: 'set_theme', value: newTheme});
// The settings watcher doesn't fire until the echo comes back from the
// server, so to make the theme change immediately we need to manually
// do the dispatch now
// XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override
// the value from settings.
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
};
_onUseSystemThemeChanged = (checked) => {
this.setState({useSystemTheme: checked});
dis.dispatch({action: 'recheck_theme'});
}
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
@ -87,7 +226,11 @@ export default class GeneralUserSettingsTab extends React.Component {
};
_onDeactivateClicked = () => {
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {
onFinished: (success) => {
if (success) this.props.closeSettingsFn();
},
});
};
_renderProfileSection() {
@ -101,6 +244,10 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderAccountSection() {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
const Spinner = sdk.getComponent("views.elements.Spinner");
const passwordChangeForm = (
<ChangePassword
className="mx_GeneralUserSettingsTab_changePassword"
@ -110,6 +257,31 @@ export default class GeneralUserSettingsTab extends React.Component {
onFinished={this._onPasswordChanged} />
);
let threepidSection = null;
// For older homeservers without separate 3PID add and bind methods (MSC2290),
// we use a combo add with bind option API which requires an identity server to
// validate 3PID ownership even if we're just adding to the homeserver only.
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses
emails={this.state.emails}
onEmailsChange={this._onEmailsChange}
/>
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this._onMsisdnsChange}
/>
</div>;
} else if (this.state.serverSupportsSeparateAddAndBind === null) {
threepidSection = <Spinner />;
}
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
@ -117,12 +289,7 @@ export default class GeneralUserSettingsTab extends React.Component {
{_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 />
{threepidSection}
</div>
);
}
@ -140,19 +307,79 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const themeWatcher = new ThemeWatcher();
let systemThemeSection;
if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div>
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE}
onChange={this._onUseSystemThemeChanged}
/>
</div>;
}
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option>
<option value="dark">{_t("Dark theme")}</option>
value={this.state.theme} onChange={this._onThemeChange}
disabled={this.state.useSystemTheme}
>
{Object.entries(enumerateThemes()).map(([theme, text]) => {
return <option key={theme} value={theme}>{text}</option>;
})}
</Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div>
);
}
_renderDiscoverySection() {
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
if (this.state.requiredPolicyInfo.hasTerms) {
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
const intro = <span className="mx_SettingsTab_subsectionText">
{_t(
"Agree to the identity server (%(serverName)s) Terms of Service to " +
"allow yourself to be discoverable by email address or phone number.",
{serverName: this.state.idServerName},
)}
</span>;
return (
<div>
<InlineTermsAgreement
policiesAndServicePairs={this.state.requiredPolicyInfo.policiesAndServices}
agreedUrls={this.state.requiredPolicyInfo.agreedUrls}
onFinished={this.state.requiredPolicyInfo.resolve}
introElement={intro}
/>
{ /* has its own heading as it includes the current ID server */ }
<SetIdServer missingTerms={true} />
</div>
);
}
const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses emails={this.state.emails} />
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers msisdns={this.state.msisdns} />
</div> : null;
return (
<div className="mx_SettingsTab_section">
{threepidSection}
{ /* has its own heading as it includes the current ID server */ }
<SetIdServer />
</div>
);
}
_renderManagementSection() {
// TODO: Improve warning text for account deactivation
return (
@ -168,7 +395,24 @@ export default class GeneralUserSettingsTab extends React.Component {
);
}
_renderIntegrationManagerSection() {
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
return (
<div className="mx_SettingsTab_section">
{ /* has its own heading as it includes the current integration manager */ }
<SetIntegrationManager />
</div>
);
}
render() {
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img className='mx_GeneralUserSettingsTab_warningIcon'
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
width="18" height="18" alt={_t("Warning")} />
: null;
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
@ -176,6 +420,10 @@ export default class GeneralUserSettingsTab extends React.Component {
{this._renderAccountSection()}
{this._renderLanguageSection()}
{this._renderThemeSection()}
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
</div>
);

View file

@ -71,8 +71,11 @@ export default class HelpUserSettingsTab extends React.Component {
_onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return;
// Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
// stopping in the middle of the logs.
console.log("Clear cache & reload clicked");
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
};
@ -135,11 +138,27 @@ export default class HelpUserSettingsTab extends React.Component {
<ul>
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
default cover photo</a> is (C)&nbsp;
default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
CC-BY-SA 4.0</a>. No warranties are given.
CC-BY-SA 4.0</a>.
</li>
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
Apache 2.0</a>.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
CC-BY 4.0</a>.
</li>
</ul>
</div>
@ -210,7 +229,7 @@ export default class HelpUserSettingsTab extends React.Component {
</div>
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")}
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>

View file

@ -32,7 +32,7 @@ export class LabsSettingToggle extends React.Component {
};
render() {
const label = _t(SettingsStore.getDisplayName(this.props.featureId));
const label = SettingsStore.getDisplayName(this.props.featureId);
const value = SettingsStore.isFeatureEnabled(this.props.featureId);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} />;
}
@ -52,6 +52,9 @@ export default class LabsUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
{flags}
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
</div>
</div>
);

View file

@ -0,0 +1,329 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 {Mjolnir} from "../../../../../mjolnir/Mjolnir";
import {ListRule} from "../../../../../mjolnir/ListRule";
import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
import Modal from "../../../../../Modal";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
const sdk = require("../../../../..");
export default class MjolnirUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
busy: false,
newPersonalRule: "",
newList: "",
};
}
_onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value});
};
_onNewListChanged = (e) => {
this.setState({newList: e.target.value});
};
_onAddPersonalRule = async (e) => {
e.preventDefault();
e.stopPropagation();
let kind = RULE_SERVER;
if (this.state.newPersonalRule.startsWith("@")) {
kind = RULE_USER;
}
this.setState({busy: true});
try {
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, {
title: _t('Error adding ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
};
_onSubscribeList = async (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({busy: true});
try {
const room = await MatrixClientPeg.get().joinRoom(this.state.newList);
await Mjolnir.sharedInstance().subscribeToList(room.roomId);
this.setState({newList: ""}); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
title: _t('Error subscribing to list'),
description: _t('Please verify the room ID or alias and try again.'),
});
} finally {
this.setState({busy: false});
}
};
async _removePersonalRule(rule: ListRule) {
this.setState({busy: true});
try {
const list = Mjolnir.sharedInstance().getPersonalList();
await list.unbanEntity(rule.kind, rule.entity);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, {
title: _t('Error removing ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
}
async _unsubscribeFromList(list: BanList) {
this.setState({busy: true});
try {
await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId);
await MatrixClientPeg.get().leave(list.roomId);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, {
title: _t('Error unsubscribing from list'),
description: _t('Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
}
_viewListRules(list: BanList) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? room.name : list.roomId;
const renderRules = (rules: ListRule[]) => {
if (rules.length === 0) return <i>{_t("None")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(<li key={rule.kind + rule.entity}><code>{rule.entity}</code></li>);
}
return <ul>{tiles}</ul>;
};
Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", {roomName: name}),
description: (
<div>
<h3>{_t("Server rules")}</h3>
{renderRules(list.serverRules)}
<h3>{_t("User rules")}</h3>
{renderRules(list.userRules)}
</div>
),
button: _t("Close"),
hasCancelButton: false,
});
}
_renderPersonalBanListRules() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(
<li key={rule.entity} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
kind="danger_sm"
onClick={() => this._removePersonalRule(rule)}
disabled={this.state.busy}
>
{_t("Remove")}
</AccessibleButton>&nbsp;
<code>{rule.entity}</code>
</li>,
);
}
return (
<div>
<p>{_t("You are currently ignoring:")}</p>
<ul>{tiles}</ul>
</div>
);
}
_renderSubscribedBanLists() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const personalList = Mjolnir.sharedInstance().getPersonalList();
const lists = Mjolnir.sharedInstance().lists.filter(b => {
return personalList? personalList.roomId !== b.roomId : true;
});
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
const tiles = [];
for (const list of lists) {
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? <span>{room.name} (<code>{list.roomId}</code>)</span> : <code>list.roomId</code>;
tiles.push(
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
kind="danger_sm"
onClick={() => this._unsubscribeFromList(list)}
disabled={this.state.busy}
>
{_t("Unsubscribe")}
</AccessibleButton>&nbsp;
<AccessibleButton
kind="primary_sm"
onClick={() => this._viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
</AccessibleButton>&nbsp;
{name}
</li>,
);
}
return (
<div>
<p>{_t("You are currently subscribed to:")}</p>
<ul>{tiles}</ul>
</div>
);
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
<div className="mx_SettingsTab_section">
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
<br />
{_t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have Riot match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{}, {code: (s) => <code>{s}</code>},
)}<br />
<br />
{_t(
"Ignoring people is done through ban lists which contain rules for " +
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
"that list will be hidden from you.",
)}
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
"Your personal ban list holds all the users/servers you personally don't " +
"want to see messages from. After ignoring your first user/server, a new room " +
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
"the ban list in effect.",
)}
</div>
<div>
{this._renderPersonalBanListRules()}
</div>
<div>
<form onSubmit={this._onAddPersonalRule} autoComplete="off">
<Field
id="mx_MjolnirUserSettingsTab_personalAdd"
type="text"
label={_t("Server or user ID to ignore")}
placeholder={_t("eg: @bot:* or example.org")}
value={this.state.newPersonalRule}
onChange={this._onPersonalRuleChanged}
/>
<AccessibleButton
type="submit"
kind="primary"
onClick={this._onAddPersonalRule}
disabled={this.state.busy}
>
{_t("Ignore")}
</AccessibleButton>
</form>
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("Subscribing to a ban list will cause you to join it!")}</span>
&nbsp;
<span>{_t(
"If this isn't what you want, please use a different tool to ignore users.",
)}</span>
</div>
<div>
{this._renderSubscribedBanLists()}
</div>
<div>
<form onSubmit={this._onSubscribeList} autoComplete="off">
<Field
id="mx_MjolnirUserSettingsTab_subscriptionAdd"
type="text"
label={_t("Room ID or alias of ban list")}
value={this.state.newList}
onChange={this._onNewListChanged}
/>
<AccessibleButton
type="submit"
kind="primary"
onClick={this._onSubscribeList}
disabled={this.state.busy}
>
{_t("Subscribe")}
</AccessibleButton>
</form>
</div>
</div>
</div>
);
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -20,11 +21,12 @@ import {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
const sdk = require("../../../../..");
const PlatformPeg = require("../../../../../PlatformPeg");
import sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
export default class PreferencesUserSettingsTab extends React.Component {
static COMPOSER_SETTINGS = [
'useCiderComposer',
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
'sendTypingNotifications',
@ -42,10 +44,12 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showJoinLeaves',
'showAvatarChanges',
'showDisplaynameChanges',
'showImages',
];
static ROOM_LIST_SETTINGS = [
'RoomList.orderByImportance',
'breadcrumbs',
];
static ADVANCED_SETTINGS = [
@ -63,8 +67,16 @@ export default class PreferencesUserSettingsTab extends React.Component {
this.state = {
autoLaunch: false,
autoLaunchSupported: false,
alwaysShowMenuBar: true,
alwaysShowMenuBarSupported: false,
minimizeToTray: true,
minimizeToTraySupported: false,
autocompleteDelay:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay').toString(10),
readMarkerInViewThresholdMs:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'readMarkerInViewThresholdMs').toString(10),
readMarkerOutOfViewThresholdMs:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'readMarkerOutOfViewThresholdMs').toString(10),
};
}
@ -73,33 +85,59 @@ export default class PreferencesUserSettingsTab extends React.Component {
const autoLaunchSupported = await platform.supportsAutoLaunch();
let autoLaunch = false;
if (autoLaunchSupported) {
autoLaunch = await platform.getAutoLaunchEnabled();
}
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
let alwaysShowMenuBar = true;
if (alwaysShowMenuBarSupported) {
alwaysShowMenuBar = !await platform.getAutoHideMenuBarEnabled();
}
const minimizeToTraySupported = await platform.supportsMinimizeToTray();
let minimizeToTray = true;
if (minimizeToTraySupported) {
minimizeToTray = await platform.getMinimizeToTrayEnabled();
}
this.setState({autoLaunch, autoLaunchSupported, minimizeToTraySupported, minimizeToTray});
this.setState({
autoLaunch,
autoLaunchSupported,
alwaysShowMenuBarSupported,
alwaysShowMenuBar,
minimizeToTraySupported,
minimizeToTray,
});
}
_onAutoLaunchChange = (checked) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
};
_onAlwaysShowMenuBarChange = (checked) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked}));
};
_onMinimizeToTrayChange = (checked) => {
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked}));
};
_onAutocompleteDelayChange = (e) => {
this.setState({autocompleteDelay: e.target.value});
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
};
_onReadMarkerInViewThresholdMs = (e) => {
this.setState({readMarkerInViewThresholdMs: e.target.value});
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
_onReadMarkerOutOfViewThresholdMs = (e) => {
this.setState({readMarkerOutOfViewThresholdMs: e.target.value});
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", 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} />);
@ -108,16 +146,26 @@ export default class PreferencesUserSettingsTab extends React.Component {
render() {
let autoLaunchOption = null;
if (this.state.autoLaunchSupported) {
autoLaunchOption = <LabelledToggleSwitch value={this.state.autoLaunch}
onChange={this._onAutoLaunchChange}
label={_t('Start automatically after system login')} />;
autoLaunchOption = <LabelledToggleSwitch
value={this.state.autoLaunch}
onChange={this._onAutoLaunchChange}
label={_t('Start automatically after system login')} />;
}
let autoHideMenuOption = null;
if (this.state.alwaysShowMenuBarSupported) {
autoHideMenuOption = <LabelledToggleSwitch
value={this.state.alwaysShowMenuBar}
onChange={this._onAlwaysShowMenuBarChange}
label={_t('Always show the window menu bar')} />;
}
let minimizeToTrayOption = null;
if (this.state.minimizeToTraySupported) {
minimizeToTrayOption = <LabelledToggleSwitch value={this.state.minimizeToTray}
onChange={this._onMinimizeToTrayChange}
label={_t('Close button should minimize window to tray')} />;
minimizeToTrayOption = <LabelledToggleSwitch
value={this.state.minimizeToTray}
onChange={this._onMinimizeToTrayChange}
label={_t('Show tray icon and minimize window to it on close')} />;
}
return (
@ -136,10 +184,26 @@ export default class PreferencesUserSettingsTab extends React.Component {
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
{minimizeToTrayOption}
{autoHideMenuOption}
{autoLaunchOption}
<Field id={"autocompleteDelay"} label={_t('Autocomplete delay (ms)')} type='number'
value={SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay')}
onChange={this._onAutocompleteDelayChange} />
<Field
id={"autocompleteDelay"}
label={_t('Autocomplete delay (ms)')}
type='number'
value={this.state.autocompleteDelay}
onChange={this._onAutocompleteDelayChange} />
<Field
id={"readMarkerInViewThresholdMs"}
label={_t('Read Marker lifetime (ms)')}
type='number'
value={this.state.readMarkerInViewThresholdMs}
onChange={this._onReadMarkerInViewThresholdMs} />
<Field
id={"readMarkerOutOfViewThresholdMs"}
label={_t('Read Marker off-screen lifetime (ms)')}
type='number'
value={this.state.readMarkerOutOfViewThresholdMs}
onChange={this._onReadMarkerOutOfViewThresholdMs} />
</div>
</div>
);

View file

@ -22,9 +22,9 @@ 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 "../../../../..";
import {sleep} from "../../../../../utils/promise";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -129,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component {
if (e.errcode === "M_LIMIT_EXCEEDED") {
// Add a delay between each invite change in order to avoid rate
// limiting by the server.
await Promise.delay(e.retry_after_ms || 2500);
await sleep(e.retry_after_ms || 2500);
// Redo last action
i--;
@ -258,6 +258,7 @@ export default class SecurityUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("A device's public name is visible to people you communicate with")}
<DevicesPanel />
</div>
</div>

View file

@ -29,48 +29,64 @@ export default class VoiceUserSettingsTab extends React.Component {
super();
this.state = {
mediaDevices: null,
mediaDevices: false,
activeAudioOutput: null,
activeAudioInput: null,
activeVideoInput: null,
};
}
componentWillMount(): void {
this._refreshMediaDevices();
async componentDidMount() {
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this._refreshMediaDevices();
}
}
_refreshMediaDevices = async (stream) => {
if (stream) {
// kill stream so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
this.setState({
mediaDevices: await CallMediaHandler.getDevices(),
activeAudioOutput: CallMediaHandler.getAudioOutput(),
activeAudioInput: CallMediaHandler.getAudioInput(),
activeVideoInput: CallMediaHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
// so that we don't leave it lingering around with webcam enabled etc
// as here we called gUM to ask user for permission to their device names only
stream.getTracks().forEach((track) => track.stop());
}
};
_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'),
});
},
]);
_requestMediaPermissions = async () => {
let constraints;
let stream;
let error;
try {
constraints = {video: true, audio: true};
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
// user likely doesn't have a webcam,
// we should still allow to select a microphone
if (err.name === "NotFoundError") {
constraints = { audio: true };
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
error = err;
}
} else {
error = err;
}
}
if (error) {
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'),
});
} else {
this._refreshMediaDevices(stream);
}
};
@ -99,8 +115,14 @@ export default class VoiceUserSettingsTab extends React.Component {
MatrixClientPeg.get().setForceTURN(!p2p);
};
_changeFallbackICEServerAllowed = (allow) => {
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
};
_renderDeviceOptions(devices, category) {
return devices.map((d) => <option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
});
}
render() {
@ -183,7 +205,16 @@ export default class VoiceUserSettingsTab extends React.Component {
{microphoneDropdown}
{webcamDropdown}
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
<SettingsFlag name='webRtcAllowPeerToPeer' level={SettingLevel.DEVICE} onChange={this._changeWebRtcMethod} />
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
onChange={this._changeWebRtcMethod}
/>
<SettingsFlag
name='fallbackICEServerAllowed'
level={SettingLevel.DEVICE}
onChange={this._changeFallbackICEServerAllowed}
/>
</div>
</div>
);