diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 0111ee31..4c306a7a 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -52,23 +52,41 @@ 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)) { + if (this.props.config.networks[network].portalRoomPattern) { + this.portalRoomPatterns[network] = new RegExp(this.props.config.networks[network].portalRoomPattern); + } + if (this.props.config.networks[network].nativePattern) { + this.nativePatterns[network] = new RegExp(this.props.config.networks[network].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) => { + 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 +115,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 +125,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 (this.state.filterString) 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 +147,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 +233,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 +250,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 +268,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 +366,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 +437,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 +451,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 +461,59 @@ 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) { + const getfields_funcs = { + irc: (user_input, network) => { + if (!this.protocols.irc) return; + const domain = this.props.config.networks[network].domain; + // search through to make sure this is a domain actually + // recognised by the HS + for (const inst of this.protocols.irc.instances) { + if (inst.fields && inst.fields.domain == domain) { + return { + domain: domain, + channel: user_input, + } + } + } + return null; + }, + gitter: (user_input, network) => { + if (!this.protocols.gitter) return; + return { + room: user_input, + } + }, + }; + + const protocol = this._protocolForThirdPartyNetwork(network); + if (getfields_funcs[protocol]) { + return getfields_funcs[protocol](user_input, network); + } + }, + render: function() { let content; if (this.state.loading) { @@ -430,6 +538,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'); @@ -441,6 +566,7 @@ module.exports = React.createClass({ diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js index e6596319..cacea190 100644 --- a/src/components/views/directory/NetworkDropdown.js +++ b/src/components/views/directory/NetworkDropdown.js @@ -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,19 @@ export default class NetworkDropdown extends React.Component { icon = ; span_class = 'mx_NetworkDropdown_menu_network'; } else { - name = this.props.config.networkNames[network]; - icon = ; + if (this.props.config.networks[network]) { + if (this.props.config.networks[network].name) { + name = this.props.config.networks[network].name; + } else { + name = network; + } + if (this.props.config.networks[network].icon) { + icon = ; + } else { + icon = ; + } + } + span_class = 'mx_NetworkDropdown_menu_network'; } @@ -199,6 +210,7 @@ export default class NetworkDropdown extends React.Component { ; current_value = } else { current_value = this._makeMenuOption( diff --git a/vector/config.sample.json b/vector/config.sample.json index ad7cec95..fd4a0b31 100644 --- a/vector/config.sample.json +++ b/vector/config.sample.json @@ -15,24 +15,53 @@ "_matrix", "gitter", "irc:freenode", - "irc:mozilla" + "irc:mozilla", + "irc:snoonet", + "irc:oftc" ] } }, - "networkPatterns": { - "gitter": "#gitter_.*:matrix.org", - "irc:freenode": "#freenode_.*:matrix.org", - "irc:mozilla": "#mozilla_.*:matrix.org" - }, - "networkNames": { - "irc:freenode": "Freenode", - "irc:mozilla": "Mozilla", - "gitter": "Gitter" - }, - "networkIcons": { - "irc:freenode": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "irc:mozilla": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "gitter": "//gitter.im/favicon.ico" + "networks": { + "gitter": { + "protocol": "gitter", + "portalRoomPattern": "#gitter_.*:matrix.org", + "name": "Gitter", + "icon": "//gitter.im/favicon.ico", + "example": "org/community", + "nativePattern": "[^\\s]+/[^\\s]+$" + }, + "irc:freenode": { + "portalRoomPattern": "#freenode_.*:matrix.org", + "name": "Freenode", + "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:mozilla": { + "portalRoomPattern": "#mozilla_.*:matrix.org", + "name": "Mozilla", + "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:snoonet": { + "protocol": "irc", + "domain": "ipv6-irc.snoonet.org", + "portalRoomPattern": "#_snoonet_.*:matrix.org", + "name": "Snoonet", + "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:oftc": { + "protocol": "irc", + "domain": "irc.oftc.net", + "portalRoomPattern": "#_oftc_.*:matrix.org", + "name": "OFTC", + "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + } } } }