Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add native joining of 3p networks to room dir #2379

Merged
merged 10 commits into from
Oct 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,32 @@ You can configure the app by copying `vector/config.sample.json` to
addresses) to matrix IDs: see http://matrix.org/docs/spec/identity_service/unstable.html
for more details. Currently the only public matrix identity servers are https://matrix.org
and https://vector.im. In future identity servers will be decentralised.

1. `roomDirectory`: config for the public room directory. This section encodes behaviour
on the room directory screen for filtering the list by server / network type and joining
third party networks. This config section will disappear once APIs are available to
get this information for home servers. This section is optional.
1. `roomDirectory.servers`: List of other Home Servers' directories to include in the drop
down list. Optional.
1. `roomDirectory.serverConfig`: Config for each server in `roomDirectory.servers`. Optional.
1. `roomDirectory.serverConfig.<server_name>.networks`: List of networks (named
in `roomDirectory.networks`) to include for this server. Optional.
1. `roomDirectory.networks`: config for each network type. Optional.
1. `roomDirectory.<network_type>.name`: Human-readable name for the network. Required.
1. `roomDirectory.<network_type>.protocol`: Protocol as given by the server in
`/_matrix/client/unstable/thirdparty/protocols` response. Required to be able to join
this type of third party network.
1. `roomDirectory.<network_type>.domain`: Domain as given by the server in
`/_matrix/client/unstable/thirdparty/protocols` response, if present. Required to be
able to join this type of third party network, if present in `thirdparty/protocols`.
1. `roomDirectory.<network_type>.portalRoomPattern`: Regular expression matching aliases
for portal rooms to locations on this network. Required.
1. `roomDirectory.<network_type>.icon`: URL to an icon to be displayed for this network. Required.
1. `roomDirectory.<network_type>.example`: Textual example of a location on this network,
eg. '#channel' for an IRC network. Optional.
1. `roomDirectory.<network_type>.nativePattern`: Regular expression that matches a
valid location on this network. This is used as a hint to the user to indicate
when a valid location has been entered so it's not necessary for this to be
exactly correct. Optional.

Running as a Desktop app
========================
Expand Down
197 changes: 177 additions & 20 deletions src/components/structures/RoomDirectory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can we be sure that we don't need to do the 3rd party dance if we are showing all networks?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can we be sure that we don't need to do the 3rd party dance if we are showing all networks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're showing all networks, the user input is just treated as a matrix room ID: we don't do 3rd party network stuff since there'd be no obvious 3rd party network to choose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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) {
Expand All @@ -430,6 +569,23 @@ module.exports = React.createClass({
</ScrollPanel>;
}

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');
Expand All @@ -441,6 +597,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>
Expand Down
21 changes: 18 additions & 3 deletions src/components/views/directory/NetworkDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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(
Expand Down
Loading