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:
commit
6dc69afe67
38 changed files with 975 additions and 393 deletions
|
@ -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() {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
{ 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>
|
||||
<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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
62
src/components/views/auth/SignInToText.js
Normal file
62
src/components/views/auth/SignInToText.js
Normal 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>;
|
||||
}
|
||||
}
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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} />;
|
||||
});
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue