Merge branches 'develop' and 't3chguy/community_member_invite_IS_text' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/community_member_invite_IS_text

This commit is contained in:
Michael Telatynski 2019-09-12 12:53:32 +01:00
commit 6dc69afe67
38 changed files with 975 additions and 393 deletions

View file

@ -31,6 +31,7 @@ export default class PasswordLogin extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onEditServerDetailsClick: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
@ -257,6 +258,7 @@ export default class PasswordLogin extends React.Component {
render() {
const Field = sdk.getComponent('elements.Field');
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let forgotPasswordJsx;
@ -273,33 +275,6 @@ export default class PasswordLogin extends React.Component {
</span>;
}
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
const pwFieldClass = classNames({
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
@ -342,10 +317,8 @@ export default class PasswordLogin extends React.Component {
return (
<div>
<h3>
{signInToText}
{editLink}
</h3>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
<form onSubmit={this.onSubmitForm}>
{loginType}
{loginField}

View file

@ -0,0 +1,62 @@
/*
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 sdk from "../../../index";
import PropTypes from "prop-types";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
export default class SignInToText extends React.PureComponent {
static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onEditServerDetailsClick: PropTypes.func,
};
render() {
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
let editLink = null;
if (this.props.onEditServerDetailsClick) {
editLink = <a className="mx_AuthBody_editServerDetails"
href="#" onClick={this.props.onEditServerDetailsClick}
>
{_t('Change')}
</a>;
}
return <h3>
{signInToText}
{editLink}
</h3>;
}
}

View file

@ -112,6 +112,10 @@ export default class EditMessageComposer extends React.Component {
super(props, context);
this.model = null;
this._editorRef = null;
this.state = {
saveDisabled: true,
};
}
_setEditorRef = ref => {
@ -160,7 +164,7 @@ export default class EditMessageComposer extends React.Component {
dis.dispatch({action: 'focus_composer'});
}
_isModifiedOrSameAsOld(newContent) {
_isContentModified(newContent) {
// if nothing has changed then bail
const oldContent = this.props.editState.getEvent().getContent();
if (!this._editorRef.isModified() ||
@ -176,16 +180,18 @@ export default class EditMessageComposer extends React.Component {
const editedEvent = this.props.editState.getEvent();
const editContent = createEditContent(this.model, editedEvent);
const newContent = editContent["m.new_content"];
if (!this._isModifiedOrSameAsOld(newContent)) {
return;
}
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.matrixClient.sendMessage(roomId, editContent);
// If content is modified then send an updated event into the room
if (this._isContentModified(newContent)) {
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.matrixClient.sendMessage(roomId, editContent);
}
// close the event editing and focus composer
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
}
};
_cancelPreviousPendingEdit() {
const originalEvent = this.props.editState.getEvent();
@ -240,6 +246,16 @@ export default class EditMessageComposer extends React.Component {
return caretPosition;
}
_onChange = () => {
if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) {
return;
}
this.setState({
saveDisabled: false,
});
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
@ -249,10 +265,13 @@ export default class EditMessageComposer extends React.Component {
room={this._getRoom()}
initialCaret={this.props.editState.getCaret()}
label={_t("Edit message")}
onChange={this._onChange}
/>
<div className="mx_EditMessageComposer_buttons">
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit} disabled={this.state.saveDisabled}>
{_t("Save")}
</AccessibleButton>
</div>
</div>);
}

View file

@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import E2EIcon from "./E2EIcon";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {EventTimeline} from "matrix-js-sdk";
module.exports = createReactClass({
displayName: 'MemberInfo',
@ -64,6 +65,7 @@ module.exports = createReactClass({
mute: false,
modifyLevel: false,
synapseDeactivate: false,
redactMessages: false,
},
muted: false,
isTargetMod: false,
@ -356,6 +358,74 @@ module.exports = createReactClass({
});
},
onRedactAllMessages: async function() {
const {roomId, userId} = this.props.member;
const room = this.context.matrixClient.getRoom(roomId);
if (!room) {
return;
}
let timeline = room.getLiveTimeline();
let eventsToRedact = [];
while (timeline) {
eventsToRedact = timeline.getEvents().reduce((events, event) => {
if (event.getSender() === userId && !event.isRedacted()) {
return events.concat(event);
} else {
return events;
}
}, eventsToRedact);
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
const count = eventsToRedact.length;
const user = this.props.member.name;
if (count === 0) {
const InfoDialog = sdk.getComponent("dialogs.InfoDialog");
Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, {
title: _t("No recent messages by %(user)s found", {user}),
description:
<div>
<p>{ _t("Try scrolling up in the timeline to see if there are any earlier ones.") }</p>
</div>,
});
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const confirmed = await new Promise((resolve) => {
Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, {
title: _t("Remove recent messages by %(user)s", {user}),
description:
<div>
<p>{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }</p>
<p>{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }</p>
</div>,
button: _t("Remove %(count)s messages", {count}),
onFinished: resolve,
});
});
if (!confirmed) {
return;
}
// Submitting a large number of redactions freezes the UI,
// so first yield to allow to rerender after closing the dialog.
await Promise.resolve();
console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`);
await Promise.all(eventsToRedact.map(async event => {
try {
await this.context.matrixClient.redactEvent(roomId, event.getId());
} catch (err) {
// log and swallow errors
console.error("Could not redact", event.getId());
console.error(err);
}
}));
console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`);
}
},
_warnSelfDemote: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
return new Promise((resolve) => {
@ -572,7 +642,10 @@ module.exports = createReactClass({
_calculateOpsPermissions: async function(member) {
const defaultPerms = {
can: {},
can: {
// Calculate permissions for Synapse before doing the PL checks
synapseDeactivate: await this.context.matrixClient.isSynapseAdministrator(),
},
muted: false,
};
const room = this.context.matrixClient.getRoom(member.roomId);
@ -586,9 +659,10 @@ module.exports = createReactClass({
const them = member;
return {
can: await this._calculateCanPermissions(
me, them, powerLevels.getContent(),
),
can: {
...defaultPerms.can,
...await this._calculateCanPermissions(me, them, powerLevels.getContent()),
},
muted: this._isMuted(them, powerLevels.getContent()),
isTargetMod: them.powerLevel > powerLevels.getContent().users_default,
};
@ -602,11 +676,9 @@ module.exports = createReactClass({
mute: false,
modifyLevel: false,
modifyLevelMax: 0,
redactMessages: false,
};
// Calculate permissions for Synapse before doing the PL checks
can.synapseDeactivate = await this.context.matrixClient.isSynapseAdministrator();
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
@ -623,6 +695,7 @@ module.exports = createReactClass({
can.mute = me.powerLevel >= editPowerLevel;
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevelMax = me.powerLevel;
can.redactMessages = me.powerLevel >= powerLevels.redact;
return can;
},
@ -812,6 +885,7 @@ module.exports = createReactClass({
let banButton;
let muteButton;
let giveModButton;
let redactButton;
let synapseDeactivateButton;
let spinner;
@ -892,6 +966,15 @@ module.exports = createReactClass({
</AccessibleButton>
);
}
if (this.state.can.redactMessages) {
redactButton = (
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onRedactAllMessages}>
{ _t("Remove recent messages") }
</AccessibleButton>
);
}
if (this.state.can.ban) {
let label = _t("Ban");
if (this.props.member.membership === 'ban') {
@ -932,7 +1015,7 @@ module.exports = createReactClass({
}
let adminTools;
if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton) {
if (kickButton || banButton || muteButton || giveModButton || synapseDeactivateButton || redactButton) {
adminTools =
<div>
<h3>{ _t("Admin Tools") }</h3>
@ -941,6 +1024,7 @@ module.exports = createReactClass({
{ muteButton }
{ kickButton }
{ banButton }
{ redactButton }
{ giveModButton }
{ synapseDeactivateButton }
</div>

View file

@ -37,18 +37,19 @@ export default class ReplyPreview extends React.Component {
constructor(props, context) {
super(props, context);
this.unmounted = false;
this.state = {
event: null,
event: RoomViewStore.getQuotingEvent(),
};
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate();
}
componentWillUnmount() {
this.unmounted = true;
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
@ -56,6 +57,8 @@ export default class ReplyPreview extends React.Component {
}
_onRoomViewStoreUpdate() {
if (this.unmounted) return;
const event = RoomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });

View file

@ -758,7 +758,7 @@ module.exports = createReactClass({
headerItems: this._getHeaderItems('im.vector.fake.recent'),
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
onAddRoom: () => {dis.dispatch({action: 'view_room_directory'})},
onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
},
];
const tagSubLists = Object.keys(this.state.lists)

View file

@ -22,7 +22,7 @@ import sdk from '../../../index';
import MatrixClientPeg from "../../../MatrixClientPeg";
import Modal from '../../../Modal';
import dis from "../../../dispatcher";
import { getThreepidBindStatus } from '../../../boundThreepids';
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
@ -93,18 +93,11 @@ export default class SetIdServer extends React.Component {
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 the user is trying to change
// it in two places, we'll end up stomping all over their input, but at that point we
// should question our UX which led to them doing that.
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
const fullUrl = MatrixClientPeg.get().getIdentityServerUrl();
let abbr = '';
if (fullUrl) abbr = abbreviateUrl(fullUrl);
this.setState({
currentClientIdServer: fullUrl,
idServer: abbr,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
});
};
@ -137,15 +130,21 @@ export default class SetIdServer extends React.Component {
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: fullUrl,
});
this.setState({idServer: '', busy: false, error: null});
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(this.state.idServer);
const fullUrl = unabbreviateUrl(idServer);
let errStr = await checkIdentityServerUrl(fullUrl);
if (!errStr) {
@ -157,20 +156,49 @@ export default class SetIdServer extends React.Component {
const authClient = new IdentityAuthClient(fullUrl);
await authClient.getAccessToken();
let save = true;
// Double check that the identity server even has terms of service.
const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) {
this._showNoTermsWarning(fullUrl);
return;
let terms;
try {
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
} catch (e) {
console.error(e);
if (e.cors === "rejected" || e.httpStatus === 404) {
terms = null;
} else {
throw e;
}
}
this._saveIdServer(fullUrl);
if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) {
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);
if (e.cors === "rejected" || e.httpStatus === 404) {
this._showNoTermsWarning(fullUrl);
return;
}
errStr = _t("Terms of service not accepted or the identity server is invalid.");
}
}
@ -179,13 +207,12 @@ export default class SetIdServer extends React.Component {
checking: false,
error: errStr,
currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(),
idServer: this.state.idServer,
});
};
_showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
<div>
@ -198,54 +225,67 @@ export default class SetIdServer extends React.Component {
</div>
),
button: _t("Continue"),
onFinished: async (confirmed) => {
if (!confirmed) return;
this._saveIdServer(fullUrl);
},
});
return finished;
}
_onDisconnectClicked = async () => {
this.setState({disconnectBusy: true});
try {
const threepids = await getThreepidBindStatus(MatrixClientPeg.get());
const boundThreepids = threepids.filter(tp => tp.bound);
let message;
if (boundThreepids.length) {
message = _t(
"You are currently sharing email addresses or phone numbers on the identity " +
"server <idserver />. You will need to reconnect to <idserver2 /> to stop " +
"sharing them.", {},
{
idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
// XXX: https://github.com/vector-im/riot-web/issues/9086
idserver2: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
},
);
} else {
message = _t(
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>},
);
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Identity Server Disconnect Warning', '', QuestionDialog, {
title: _t("Disconnect Identity Server"),
description: message,
),
button: _t("Disconnect"),
onFinished: (confirmed) => {
if (confirmed) {
this._disconnectIdServer();
}
},
});
if (confirmed) {
this._disconnectIdServer();
}
} finally {
this.setState({disconnectBusy: false});
}
};
async _showServerChangeWarning({ title, unboundMessage, button }) {
const threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get());
const boundThreepids = threepids.filter(tp => tp.bound);
let message;
let danger = false;
if (boundThreepids.length) {
message = <div>
<p>{_t(
"You are still <b>sharing your personal data</b> on the identity " +
"server <idserver />.", {},
{
idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b>,
b: sub => <b>{sub}</b>,
},
)}</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", {

View file

@ -23,8 +23,8 @@ 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 sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -113,11 +113,15 @@ export class ExistingEmailAddress extends React.Component {
}
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,
@ -125,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) => {
@ -184,12 +181,16 @@ 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') {
@ -204,7 +205,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} />;
});

View file

@ -23,8 +23,8 @@ 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 sdk from '../../../../index';
import Modal from '../../../../Modal';
/*
TODO: Improve the UX for everything in here.
@ -108,11 +108,15 @@ export class ExistingPhoneNumber extends React.Component {
}
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: "",
@ -124,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) => {
@ -181,7 +178,6 @@ export default class PhoneNumbers extends React.Component {
const token = this.state.newPhoneNumberCode;
this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
addTask: null,
continueDisabled: false,
verifying: false,
@ -190,6 +186,11 @@ export default class PhoneNumbers extends React.Component {
newPhoneNumber: "",
newPhoneNumberCode: "",
});
const msisdns = [
...this.props.msisdns,
{ address: this.state.verifyMsisdn, medium: "msisdn" },
];
this.props.onMsisdnsChange(msisdns);
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -210,7 +211,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} />;
});

View file

@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import { getThreepidBindStatus } from '../../../../boundThreepids';
/*
TODO: Improve the UX for everything in here.
@ -59,6 +58,11 @@ export class EmailAddress extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.email;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.email;
@ -187,27 +191,14 @@ export class EmailAddress extends React.Component {
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
this.state = {
loaded: false,
emails: [],
};
}
async componentWillMount() {
const client = MatrixClientPeg.get();
const emails = await getThreepidBindStatus(client, 'email');
this.setState({ emails });
static propTypes = {
emails: PropTypes.array.isRequired,
}
render() {
let content;
if (this.state.emails.length > 0) {
content = this.state.emails.map((e) => {
if (this.props.emails.length > 0) {
content = this.props.emails.map((e) => {
return <EmailAddress email={e} key={e.address} />;
});
} else {

View file

@ -23,7 +23,6 @@ import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from '../../../../index';
import Modal from '../../../../Modal';
import AddThreepid from '../../../../AddThreepid';
import { getThreepidBindStatus } from '../../../../boundThreepids';
/*
TODO: Improve the UX for everything in here.
@ -51,6 +50,11 @@ export class PhoneNumber extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
const { bound } = nextProps.msisdn;
this.setState({ bound });
}
async changeBinding({ bind, label, errorTitle }) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const { medium, address } = this.props.msisdn;
@ -206,27 +210,14 @@ export class PhoneNumber extends React.Component {
}
export default class PhoneNumbers extends React.Component {
constructor() {
super();
this.state = {
loaded: false,
msisdns: [],
};
}
async componentWillMount() {
const client = MatrixClientPeg.get();
const msisdns = await getThreepidBindStatus(client, 'msisdn');
this.setState({ msisdns });
static propTypes = {
msisdns: PropTypes.array.isRequired,
}
render() {
let content;
if (this.state.msisdns.length > 0) {
content = this.state.msisdns.map((e) => {
if (this.props.msisdns.length > 0) {
content = this.props.msisdns.map((e) => {
return <PhoneNumber msisdn={e} key={e.address} />;
});
} else {

View file

@ -148,7 +148,18 @@ 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) => {

View file

@ -37,6 +37,7 @@ 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 = {
@ -58,17 +59,20 @@ export default class GeneralUserSettingsTab extends React.Component {
// agreedUrls, // From the startTermsFlow callback
// resolve, // Promise resolve function for startTermsFlow callback
},
emails: [],
msisdns: [],
};
this.dispatcherRef = dis.register(this._onAction);
}
async componentWillMount() {
const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam();
const cli = MatrixClientPeg.get();
const serverRequiresIdServer = await cli.doesServerRequireIdServerParam();
this.setState({serverRequiresIdServer});
// Check to see if terms need accepting
this._checkTerms();
this._getThreepidState();
}
componentWillUnmount() {
@ -78,10 +82,31 @@ export default class GeneralUserSettingsTab extends React.Component {
_onAction = (payload) => {
if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
this._checkTerms();
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).
const threepids = await getThreepidsWithBindStatus(cli);
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});
@ -91,7 +116,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// 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 authClient = new IdentityAuthClient();
const idAccessToken = await authClient.getAccessToken(/*check=*/false);
const idAccessToken = await authClient.getAccessToken({ check: false });
startTermsFlow([new Service(
SERVICE_TYPES.IS,
MatrixClientPeg.get().getIdentityServerUrl(),
@ -200,10 +225,16 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.haveIdServer || this.state.serverRequiresIdServer === false) {
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<EmailAddresses
emails={this.state.emails}
onEmailsChange={this._onEmailsChange}
/>
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
<PhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this._onMsisdnsChange}
/>
</div>;
} else if (this.state.serverRequiresIdServer === null) {
threepidSection = <Spinner />;
@ -279,10 +310,10 @@ export default class GeneralUserSettingsTab extends React.Component {
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
<EmailAddresses emails={this.state.emails} />
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers />
<PhoneNumbers msisdns={this.state.msisdns} />
</div> : null;
return (

View file

@ -54,6 +54,7 @@ export default class LabsUserSettingsTab extends React.Component {
<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>
);