Merge pull request #2379 from vector-im/dbkr/join_3p_location

Add native joining of 3p networks to room dir
This commit is contained in:
David Baker 2016-10-05 14:00:21 +01:00 committed by GitHub
commit ea38968be9
4 changed files with 265 additions and 39 deletions

View file

@ -52,23 +52,48 @@ module.exports = React.createClass({
return {
publicRooms: [],
loading: true,
filterByNetwork: null,
network: null,
roomServer: null,
filterString: null,
}
},
componentWillMount: function() {
// precompile Regexps
this.networkPatterns = {};
if (this.props.config.networkPatterns) {
for (const network of Object.keys(this.props.config.networkPatterns)) {
this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]);
this.portalRoomPatterns = {};
this.nativePatterns = {};
if (this.props.config.networks) {
for (const network of Object.keys(this.props.config.networks)) {
const network_info = this.props.config.networks[network];
if (network_info.portalRoomPattern) {
this.portalRoomPatterns[network] = new RegExp(network_info.portalRoomPattern);
}
if (network_info.nativePattern) {
this.nativePatterns[network] = new RegExp(network_info.nativePattern);
}
}
}
this.nextBatch = null;
this.filterString = null;
this.filterTimeout = null;
this.scrollPanel = null;
this.protocols = null;
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
this.protocols = response;
}, (err) => {
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
// thing you see when loading the client!
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to get protocol list from Home Server",
description: "The Home Server may be too old to support third party networks",
});
});
// dis.dispatch({
// action: 'ui_opacity',
@ -97,7 +122,7 @@ module.exports = React.createClass({
getMoreRooms: function() {
if (!MatrixClientPeg.get()) return q();
const my_filter_string = this.filterString;
const my_filter_string = this.state.filterString;
const my_server = this.state.roomServer;
// remember the next batch token when we sent the request
// too. If it's changed, appending to the list will corrupt it.
@ -107,10 +132,10 @@ module.exports = React.createClass({
opts.server = my_server;
}
if (this.nextBatch) opts.since = this.nextBatch;
if (this.filterString) opts.filter = { generic_search_term: my_filter_string } ;
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
if (
my_filter_string != this.filterString ||
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
@ -129,7 +154,7 @@ module.exports = React.createClass({
return Boolean(data.next_batch);
}, (err) => {
if (
my_filter_string != this.filterString ||
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
@ -215,7 +240,7 @@ module.exports = React.createClass({
// to clear the list anyway.
publicRooms: [],
roomServer: server,
filterByNetwork: network,
network: network,
}, this.refreshRoomList);
// We also refresh the room list each time even though this
// filtering is client-side. It hopefully won't be client side
@ -232,7 +257,9 @@ module.exports = React.createClass({
},
onFilterChange: function(alias) {
this.filterString = alias || null;
this.setState({
filterString: alias || null,
});
// don't send the request for a little bit,
// no point hammering the server with a
@ -248,17 +275,52 @@ module.exports = React.createClass({
},
onFilterClear: function() {
this.filterString = null;
// update immediately
this.setState({
filterString: null,
}, this.refreshRoomList);
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
// update immediately
this.refreshRoomList();
},
onJoinClick: function(alias) {
this.showRoomAlias(alias);
// If we're on the 'Matrix' network (or all networks),
// just show that rooms alias
if (this.state.network == null || this.state.network == '_matrix') {
this.showRoomAlias(alias);
} else {
// This is a 3rd party protocol. Let's see if we
// can join it
const fields = this._getFieldsForThirdPartyLocation(alias, this.state.network);
if (!fields) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Unable to join network",
description: "Riot does not know how to join a room on this network",
});
return;
}
const protocol = this._protocolForThirdPartyNetwork(this.state.network);
MatrixClientPeg.get().getThirdpartyLocation(protocol, fields).done((resp) => {
if (resp.length > 0 && resp[0].alias) {
this.showRoomAlias(resp[0].alias);
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Room not found",
description: "Couldn't find a matching Matrix room",
});
}
}, (e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Fetching third party location failed",
description: "Unable to look up room ID from server",
});
});
}
},
showRoomAlias: function(alias) {
@ -311,8 +373,8 @@ module.exports = React.createClass({
if (!this.state.publicRooms) return [];
var rooms = this.state.publicRooms.filter((a) => {
if (this.state.filterByNetwork) {
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.filterByNetwork)) return false;
if (this.state.network) {
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false;
}
return true;
@ -382,7 +444,7 @@ module.exports = React.createClass({
* Terrible temporary function that guess what network a public room
* entry is in, until synapse is able to tell us
*/
_isRoomInNetwork(room, server, network) {
_isRoomInNetwork: function(room, server, network) {
// We carve rooms into two categories here. 'portal' rooms are
// rooms created by a user joining a bridge 'portal' alias to
// participate in that room or a foreign network. A room is a
@ -396,7 +458,7 @@ module.exports = React.createClass({
if (room.aliases && room.aliases.length == 1) {
if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
for (const n of this.props.config.serverConfig[server].networks) {
const pat = this.networkPatterns[n];
const pat = this.portalRoomPatterns[n];
if (pat && pat.test(room.aliases[0])) {
roomNetwork = n;
}
@ -406,6 +468,83 @@ module.exports = React.createClass({
return roomNetwork == network;
},
_stringLooksLikeId: function(s, network) {
let pat = /^#[^\s]+:[^\s]/;
if (
network && network != '_matrix' &&
this.nativePatterns[network]
) {
pat = this.nativePatterns[network];
}
return pat.test(s);
},
_protocolForThirdPartyNetwork: function(network) {
if (
this.props.config.networks &&
this.props.config.networks[network] &&
this.props.config.networks[network].protocol
) {
return this.props.config.networks[network].protocol;
}
},
_getFieldsForThirdPartyLocation: function(user_input, network) {
if (!this.props.config.networks || !this.props.config.networks[network]) return null;
const network_info = this.props.config.networks[network];
if (!network_info.protocol) return null;
if (!this.protocols) return null;
let matched_instance;
// Try to find which instance in the 'protocols' response
// matches this network. We look for a matching protocol
// and the existence of a 'domain' field and if present,
// its value.
if (this.protocols[network_info.protocol].instances.length == 1) {
const the_instance = this.protocols[network_info.protocol].instances[0];
// If there's only one instance in this protocol, use it
// as long as it has no domain (which we assume to mean it's
// there is only one possible instance).
if (
(
the_instance.fields.domain === undefined &&
network_info.domain === undefined
) ||
(
the_instance.fields.domain !== undefined &&
the_instance.fields.domain == network_info.domain
)
) {
matched_instance = the_instance;
}
} else if (network_info.domain) {
// otherwise, we look for one with a matching domain.
for (const this_instance of this.protocols[network_info.protocol].instances) {
if (this_instance.fields.domain == network_info.domain) {
matched_instance = this_instance;
}
}
}
if (matched_instance === undefined) return null;
// now make an object with the fields specified by that protocol. We
// require that the values of all but the last field come from the
// instance. The last is the user input.
const required_fields = this.protocols[network_info.protocol].location_fields;
const fields = {};
for (let i = 0; i < required_fields.length - 1; ++i) {
const this_field = required_fields[i];
if (matched_instance.fields[this_field] === undefined) return null;
fields[this_field] = matched_instance.fields[this_field];
}
fields[required_fields[required_fields.length - 1]] = user_input;
return fields;
},
render: function() {
let content;
if (this.state.loading) {
@ -435,6 +574,23 @@ module.exports = React.createClass({
}
}
let placeholder = 'Search for a room';
if (this.state.network === null || this.state.network === '_matrix') {
placeholder = '#example:' + this.state.roomServer;
} else if (
this.props.config.networks &&
this.props.config.networks[this.state.network] &&
this.props.config.networks[this.state.network].example &&
this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
) {
placeholder = this.props.config.networks[this.state.network].example;
}
const showJoinButton = (
this._stringLooksLikeId(this.state.filterString, this.state.network) &&
this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
);
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
@ -446,6 +602,7 @@ module.exports = React.createClass({
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
placeholder={placeholder} showJoinButton={showJoinButton}
/>
<NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
</div>

View file

@ -40,7 +40,7 @@ export default class NetworkDropdown extends React.Component {
this.props.config.serverConfig &&
this.props.config.serverConfig[server] &&
this.props.config.serverConfig[server].networks &&
'_matrix' in this.props.config.serverConfig[server].networks
this.props.config.serverConfig[server].networks.indexOf('_matrix') > -1
) {
defaultNetwork = '_matrix';
}
@ -170,8 +170,22 @@ export default class NetworkDropdown extends React.Component {
icon = <img src="img/network-matrix.svg" width="16" height="16" />;
span_class = 'mx_NetworkDropdown_menu_network';
} else {
name = this.props.config.networkNames[network];
icon = <img src={this.props.config.networkIcons[network]} />;
if (this.props.config.networks[network] === undefined) {
throw new Error(network + ' network missing from config');
}
if (this.props.config.networks[network].name) {
name = this.props.config.networks[network].name;
} else {
name = network;
}
if (this.props.config.networks[network].icon) {
// omit height here so if people define a non-square logo in the config, it
// will keep the aspect when it scales
icon = <img src={this.props.config.networks[network].icon} width="16" />;
} else {
icon = <img src={iconPath} width="16" height="16" />;
}
span_class = 'mx_NetworkDropdown_menu_network';
}
@ -199,6 +213,7 @@ export default class NetworkDropdown extends React.Component {
</div>;
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
/>
} else {
current_value = this._makeMenuOption(