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

@ -120,7 +120,7 @@ const CategoryRoomList = createReactClass({
});
});
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
render: function() {
@ -297,7 +297,7 @@ const RoleUserList = createReactClass({
});
});
},
});
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
},
render: function() {

View file

@ -81,6 +81,9 @@ const LeftPanel = createReactClass({
if (this.state.searchFilter !== nextState.searchFilter) {
return true;
}
if (this.state.searchExpanded !== nextState.searchExpanded) {
return true;
}
return false;
},
@ -203,12 +206,23 @@ const LeftPanel = createReactClass({
if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'});
}
this.setState({searchExpanded: false});
},
collectRoomList: function(ref) {
this._roomList = ref;
},
_onSearchFocus: function() {
this.setState({searchExpanded: true});
},
_onSearchBlur: function(event) {
if (event.target.value.length === 0) {
this.setState({searchExpanded: false});
}
},
render: function() {
const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
@ -217,6 +231,7 @@ const LeftPanel = createReactClass({
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer;
@ -240,11 +255,23 @@ const LeftPanel = createReactClass({
},
);
let exploreButton;
if (!this.props.collapsed) {
exploreButton = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton>
</div>
);
}
const searchBox = (<SearchBox
enableRoomSearchFocus={true}
placeholder={ _t('Filter room names') }
blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onSearch={ this.onSearch }
onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />);
let breadcrumbs;
@ -258,7 +285,10 @@ const LeftPanel = createReactClass({
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<TopLeftMenuButton collapsed={ this.props.collapsed } />
{ breadcrumbs }
{ searchBox }
<div className="mx_LeftPanel_exploreAndFilterRow">
{ exploreButton }
{ searchBox }
</div>
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList
ref={this.collectRoomList}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
@ -140,6 +141,10 @@ module.exports = createReactClass({
getMoreRooms: function() {
if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({
loading: true,
});
const my_filter_string = this.state.filterString;
const my_server = this.state.roomServer;
// remember the next batch token when we sent the request
@ -321,12 +326,7 @@ module.exports = createReactClass({
}
},
onCreateRoomClicked: function() {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
},
onJoinClick: function(alias) {
onJoinFromSearchClick: function(alias) {
// If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId) {
// If the user specified an alias without a domain, add on whichever server is selected
@ -368,6 +368,39 @@ module.exports = createReactClass({
}
},
onPreviewClick: function(room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: true,
});
},
onViewClick: function(room) {
this.props.onFinished();
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
should_peek: false,
});
},
onJoinClick: function(room) {
this.props.onFinished();
MatrixClientPeg.get().joinRoom(room.room_id);
dis.dispatch({
action: 'view_room',
room_id: room.room_id,
joining: true,
});
},
onCreateRoomClick: function(room) {
this.props.onFinished();
dis.dispatch({action: 'view_create_room'});
},
showRoomAlias: function(alias, autoJoin=false) {
this.showRoom(null, alias, autoJoin);
},
@ -412,74 +445,70 @@ module.exports = createReactClass({
dis.dispatch(payload);
},
getRows: function() {
getRow(room) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
const isGuest = client.isGuest();
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let previewButton;
let joinOrViewButton;
if (!this.state.publicRooms) return [];
const rooms = this.state.publicRooms;
const rows = [];
const self = this;
let guestRead; let guestJoin; let perms;
for (let i = 0; i < rooms.length; i++) {
guestRead = null;
guestJoin = null;
if (rooms[i].world_readable) {
guestRead = (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
);
}
if (rooms[i].guest_can_join) {
guestJoin = (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
);
}
perms = null;
if (guestRead || guestJoin) {
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
}
let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = rooms[i].topic || '';
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
rows.push(
<tr key={ rooms[i].room_id }
onClick={self.onRoomClicked.bind(self, rooms[i])}
// cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={ name } idName={ name }
url={ ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
rooms[i].avatar_url, 24, 24, "crop") } />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic"
onClick={ function(e) { e.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ rooms[i].num_joined_members }
</td>
</tr>,
if (room.world_readable && !hasJoinedRoom) {
previewButton = (
<AccessibleButton kind="secondary" onClick={() => this.onPreviewClick(room)}>{_t("Preview")}</AccessibleButton>
);
}
return rows;
if (hasJoinedRoom) {
joinOrViewButton = (
<AccessibleButton kind="secondary" onClick={() => this.onViewClick(room)}>{_t("View")}</AccessibleButton>
);
} else if (!isGuest || room.guest_can_join) {
joinOrViewButton = (
<AccessibleButton kind="primary" onClick={() => this.onJoinClick(room)}>{_t("Join")}</AccessibleButton>
);
}
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = room.topic || '';
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyAndSanitizeHtml(topic);
const avatarUrl = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
room.avatar_url, 32, 32, "crop",
);
return (
<tr key={ room.room_id }
onClick={() => this.onRoomClicked(room)}
// cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={32} height={32} resizeMethod='crop'
name={ name } idName={ name }
url={ avatarUrl } />
</td>
<td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
<div className="mx_RoomDirectory_topic"
onClick={ (ev) => { ev.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ room.num_joined_members }
</td>
<td className="mx_RoomDirectory_preview">{previewButton}</td>
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
</tr>
);
},
collectScrollPanel: function(element) {
@ -530,20 +559,26 @@ module.exports = createReactClass({
let content;
if (this.state.error) {
content = this.state.error;
} else if (this.state.protocolsLoading || this.state.loading) {
} else if (this.state.protocolsLoading) {
content = <Loader />;
} else {
const rows = this.getRows();
const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
// we still show the scrollpanel, at least for now, because
// otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one
let spinner;
if (this.state.loading) {
spinner = <Loader />;
}
let scrollpanel_content;
if (rows.length == 0) {
if (rows.length === 0 && !this.state.loading) {
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
} else {
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
{ rows }
</tbody>
</table>;
}
@ -555,6 +590,7 @@ module.exports = createReactClass({
startAtBottom={false}
>
{ scrollpanel_content }
{ spinner }
</ScrollPanel>;
}
@ -576,10 +612,9 @@ module.exports = createReactClass({
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
}
let placeholder = _t('Search for a room');
let placeholder = _t('Find a room…');
if (!this.state.instanceId) {
placeholder = _t('Search for a room like #example') + ':' + this.state.roomServer;
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
} else if (instance_expected_field_type) {
placeholder = instance_expected_field_type.placeholder;
}
@ -595,27 +630,31 @@ module.exports = createReactClass({
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder} showJoinButton={showJoinButton}
/>
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
</div>;
}
const createRoomButton = (<AccessibleButton
onClick={this.onCreateRoomClicked}
className="mx_RoomDirectory_createRoom"
>{_t("Create new room")}</AccessibleButton>);
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
{a: sub => {
return (<AccessibleButton
kind="secondary"
onClick={this.onCreateRoomClick}
>{sub}</AccessibleButton>);
}},
);
return (
<BaseDialog
className={'mx_RoomDirectory_dialog'}
hasCancel={true}
onFinished={this.props.onFinished}
headerButton={createRoomButton}
title={_t("Room directory")}
title={_t("Explore rooms")}
>
<div className="mx_RoomDirectory">
<p>{explanation}</p>
<div className="mx_RoomDirectory_list">
{listHeader}
{content}

View file

@ -583,7 +583,7 @@ module.exports = createReactClass({
payload.data.description || payload.data.name);
break;
case 'picture_snapshot':
return ContentMessages.sharedInstance().sendContentListToRoom(
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
);
break;
@ -624,6 +624,11 @@ module.exports = createReactClass({
showApps: payload.show,
});
break;
case 'reply_to_event':
if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) {
this.onCancelSearchClick();
}
break;
}
},

View file

@ -22,6 +22,7 @@ import { KeyCode } from '../../Keyboard';
import dis from '../../dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
module.exports = createReactClass({
displayName: 'SearchBox',
@ -47,6 +48,7 @@ module.exports = createReactClass({
getInitialState: function() {
return {
searchTerm: "",
blurred: true,
};
},
@ -94,7 +96,18 @@ module.exports = createReactClass({
},
_onFocus: function(ev) {
this.setState({blurred: false});
ev.target.select();
if (this.props.onFocus) {
this.props.onFocus(ev);
}
},
_onBlur: function(ev) {
this.setState({blurred: true});
if (this.props.onBlur) {
this.props.onBlur(ev);
}
},
_clearSearch: function(source) {
@ -113,15 +126,21 @@ module.exports = createReactClass({
if (this.props.collapsed) {
return null;
}
const clearButton = this.state.searchTerm.length > 0 ?
const clearButton = !this.state.blurred ?
(<AccessibleButton key="button"
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
</AccessibleButton>) : undefined;
// show a shorter placeholder when blurred, if requested
// this is used for the room filter field that has
// the explore button next to it when blurred
const placeholder = this.state.blurred ?
(this.props.blurredPlaceholder || this.props.placeholder) :
this.props.placeholder;
const className = this.props.className || "";
return (
<div className="mx_SearchBox mx_textinput">
<div className={classNames("mx_SearchBox", "mx_textinput", {"mx_SearchBox_blurred": this.state.blurred})}>
<input
key="searchfield"
type="text"
@ -131,7 +150,8 @@ module.exports = createReactClass({
onFocus={ this._onFocus }
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ this.props.placeholder }
onBlur={this._onBlur}
placeholder={ placeholder }
/>
{ clearButton }
</div>

View file

@ -685,20 +685,26 @@ const TimelinePanel = createReactClass({
}
this.lastRMSentEventId = this.state.readMarkerEventId;
const roomId = this.props.timelineSet.room.roomId;
const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId);
debuglog('TimelinePanel: Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
' hidden:' + hiddenRR,
);
MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId,
this.state.readMarkerEventId,
lastReadEvent, // Could be null, in which case no RR is sent
{hidden: hiddenRR},
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
{hidden: hiddenRR},
).catch((e) => {
console.error(e);
this.lastRRSentEventId = undefined;

View file

@ -93,7 +93,7 @@ module.exports = createReactClass({
// Phase of the overall login dialog.
phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password",
currentFlow: null, // we need to load the flows from the server
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
@ -372,6 +372,7 @@ module.exports = createReactClass({
this.setState({
busy: true,
currentFlow: null, // reset flow
loginIncorrect: false,
});
@ -565,6 +566,13 @@ module.exports = createReactClass({
},
_renderSsoStep: function(url) {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
// XXX: This link does *not* have a target="_blank" because single sign-on relies on
// redirecting the user back to a URI once they're logged in. On the web, this means
// we use the same window and redirect back to riot. On electron, this actually
@ -574,7 +582,12 @@ module.exports = createReactClass({
// user's browser, let them log into their SSO provider, then redirect their browser
// to vector://vector which, of course, will not work.
return (
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
<div>
<SignInToText serverConfig={this.props.serverConfig}
onEditServerDetailsClick={onEditServerDetailsClick} />
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
</div>
);
},

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>
);