diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 90afff5c..28136e15 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -53,6 +53,7 @@ module.exports = React.createClass({ publicRooms: [], loading: true, filterByNetwork: null, + roomServer: null, } }, @@ -76,10 +77,6 @@ module.exports = React.createClass({ // }); }, - componentDidMount: function() { - this.refreshRoomList(); - }, - componentWillUnmount: function() { // dis.dispatch({ // action: 'ui_opacity', @@ -101,12 +98,16 @@ module.exports = React.createClass({ if (!MatrixClientPeg.get()) return q(); const my_filter_string = this.filterString; + const my_server = this.state.roomServer; const opts = {limit: 20}; + if (my_server != MatrixClientPeg.getHomeServerName()) { + opts.server = my_server; + } if (this.nextBatch) opts.since = this.nextBatch; if (this.filterString) opts.filter = { generic_search_term: my_filter_string } ; return MatrixClientPeg.get().publicRooms(opts).then((data) => { - if (my_filter_string != this.filterString) { - // if the filter has changed since this request was sent, + if (my_filter_string != this.filterString || my_server != this.state.roomServer) { + // if the filter or server has changed since this request was sent, // throw away the result (don't even clear the busy flag // since we must still have a request in flight) return; @@ -120,7 +121,7 @@ module.exports = React.createClass({ }); return Boolean(data.next_batch); }, (err) => { - if (my_filter_string != this.filterString) { + if (my_filter_string != this.filterString || my_server != this.state.roomServer) { // as above: we don't care about errors for old // requests either return; @@ -194,18 +195,23 @@ module.exports = React.createClass({ } }, - onNetworkChange: function(network) { + onOptionChange: function(server, network) { + // clear next batch so we don't try to load more rooms + this.nextBatch = null; this.setState({ + // Clear the public rooms out here otherwise we needlessly + // spend time filtering lots of rooms when we're about to + // to clear the list anyway. + publicRooms: [], + roomServer: server, filterByNetwork: network, - }, () => { - // we just filtered out a bunch of rooms, so check to see if - // we need to fill up the scrollpanel again - // NB. Because we filter the results, the HS can keep giving - // us more rooms and we'll keep requesting more if none match - // the filter, which is pretty terrible. We need a way - // to filter by network on the server. - if (this.scrollPanel) this.scrollPanel.checkFillState(); - }); + }, this.refreshRoomList); + // We also refresh the room list each time even though this + // filtering is client-side. It hopefully won't be client side + // for very long, and we may have fetched a thousand rooms to + // find the five gitter ones, at which point we do not want + // to render all those rooms when switching back to 'all networks'. + // Easiest to just blow away the state & re-fetch. }, onFillRequest: function(backwards) { @@ -295,7 +301,7 @@ module.exports = React.createClass({ var rooms = this.state.publicRooms.filter((a) => { if (this.state.filterByNetwork) { - if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false; + if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.filterByNetwork)) return false; } return true; @@ -365,14 +371,28 @@ 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, network) { - if (room.aliases && this.networkPatterns[network]) { - for (const alias of room.aliases) { - if (this.networkPatterns[network].test(alias)) return true; + _isRoomInNetwork(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 + // portal room if it has exactly one alias and that alias matches + // a pattern defined in the config. Its network is the key + // of the pattern that it matches. + // All other rooms are considered 'native matrix' rooms, and + // go into the special '_matrix' network. + + let roomNetwork = '_matrix'; + 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]; + if (pat && pat.test(room.aliases[0])) { + roomNetwork = n; + } + } } } - - return false; + return roomNetwork == network; }, render: function() { @@ -411,7 +431,7 @@ module.exports = React.createClass({ className="mx_RoomDirectory_searchbox" onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick} /> - + {content} diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js index e1de4ffe..48d5152f 100644 --- a/src/components/views/directory/NetworkDropdown.js +++ b/src/components/views/directory/NetworkDropdown.js @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg'; export default class NetworkDropdown extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.dropdownRootElement = null; this.ignoreEvent = null; @@ -26,12 +27,27 @@ export default class NetworkDropdown extends React.Component { this.onInputClick = this.onInputClick.bind(this); this.onRootClick = this.onRootClick.bind(this); this.onDocumentClick = this.onDocumentClick.bind(this); - this.onNetworkClick = this.onNetworkClick.bind(this); + this.onMenuOptionClick = this.onMenuOptionClick.bind(this); + this.onInputKeyUp = this.onInputKeyUp.bind(this); this.collectRoot = this.collectRoot.bind(this); + this.collectInputTextBox = this.collectInputTextBox.bind(this); + + this.inputTextBox = null; + + let defaultNetwork = null; + if ( + this.props.config.serverConfig && + this.props.config.serverConfig[server] && + this.props.config.serverConfig[server].networks && + '_matrix' in this.props.config.serverConfig[server].networks + ) { + defaultNetwork = '_matrix'; + } this.state = { expanded: false, - selectedNetwork: null, + selectedServer: MatrixClientPeg.getHomeServerName(), + selectedNetwork: defaultNetwork, }; } @@ -39,12 +55,21 @@ export default class NetworkDropdown extends React.Component { // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else document.addEventListener('click', this.onDocumentClick, false); + + // fire this now so the defaults can be set up + this.props.onOptionChange(this.state.selectedServer, this.state.selectedNetwork); } componentWillUnmount() { document.removeEventListener('click', this.onDocumentClick, false); } + componentDidUpdate() { + if (this.state.expanded && this.inputTextBox) { + this.inputTextBox.focus(); + } + } + onDocumentClick(ev) { // Close the dropdown if the user clicks anywhere that isn't // within our root element @@ -72,12 +97,24 @@ export default class NetworkDropdown extends React.Component { ev.preventDefault(); } - onNetworkClick(network, ev) { + onMenuOptionClick(server, network, ev) { this.setState({ expanded: false, + selectedServer: server, selectedNetwork: network, }); - this.props.onNetworkChange(network); + this.props.onOptionChange(server, network); + } + + onInputKeyUp(e) { + if (e.key == 'Enter') { + this.setState({ + expanded: false, + selectedServer: e.target.value, + selectedNetwork: null, + }); + this.props.onOptionChange(e.target.value, null); + } } collectRoot(e) { @@ -90,41 +127,82 @@ export default class NetworkDropdown extends React.Component { this.dropdownRootElement = e; } - _optionForNetwork(network, wire_onclick) { + collectInputTextBox(e) { + this.inputTextBox = e; + } + + _getMenuOptions() { + const options = []; + + let servers = []; + if (this.props.config.servers) { + servers = servers.concat(this.props.config.servers); + } + + if (servers.indexOf(MatrixClientPeg.getHomeServerName()) == -1) { + servers.unshift(MatrixClientPeg.getHomeServerName()); + } + + for (const server of servers) { + options.push(this._makeMenuOption(server, null)); + if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) { + for (const network of this.props.config.serverConfig[server].networks) { + options.push(this._makeMenuOption(server, network)); + } + } + } + + return options; + } + + _makeMenuOption(server, network, wire_onclick) { if (wire_onclick === undefined) wire_onclick = true; let icon; let name; let span_class; if (network === null) { - name = 'All networks'; + name = server; span_class = 'mx_NetworkDropdown_menu_all'; + } else if (network == '_matrix') { + name = 'Matrix'; + icon = ; + span_class = 'mx_NetworkDropdown_menu_network'; } else { name = this.props.config.networkNames[network]; icon = ; span_class = 'mx_NetworkDropdown_menu_network'; } - const click_handler = wire_onclick ? this.onNetworkClick.bind(this, network) : null; + const click_handler = wire_onclick ? this.onMenuOptionClick.bind(this, server, network) : null; - return
+ let key = server; + if (network !== null) { + key += '_' + network; + } + + return
{icon} {name}
; } render() { - const current_value = this._optionForNetwork(this.state.selectedNetwork, false); + let current_value; let menu; if (this.state.expanded) { - const menu_options = [this._optionForNetwork(null)]; - for (const network of this.props.config.networks) { - menu_options.push(this._optionForNetwork(network)); - } + const menu_options = this._getMenuOptions(); menu =
{menu_options}
; + current_value = + } else { + current_value = this._makeMenuOption( + this.state.selectedServer, this.state.selectedNetwork, false + ); } return
@@ -138,7 +216,7 @@ export default class NetworkDropdown extends React.Component { } NetworkDropdown.propTypes = { - onNetworkChange: React.PropTypes.func.isRequired, + onOptionChange: React.PropTypes.func.isRequired, config: React.PropTypes.object, }; diff --git a/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css b/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css index cc152802..b500ffa2 100644 --- a/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css +++ b/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css @@ -52,6 +52,10 @@ limitations under the License. vertical-align: middle; } +input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus { + border: 0; +} + .mx_NetworkDropdown_menu { position: absolute; left: -1px; diff --git a/src/skins/vector/img/network-matrix.svg b/src/skins/vector/img/network-matrix.svg new file mode 100644 index 00000000..bb8278ae --- /dev/null +++ b/src/skins/vector/img/network-matrix.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/vector/config.sample.json b/vector/config.sample.json index e4fd3469..ad7cec95 100644 --- a/vector/config.sample.json +++ b/vector/config.sample.json @@ -6,30 +6,30 @@ "integrations_rest_url": "http://localhost:5050", "enableLabs": true, "roomDirectory": { - "networks": [ - "matrix:example_com", - "matrix:matrix_org", - "gitter", - "irc:freenode", - "irc:mozilla" + "servers": [ + "matrix.org" ], + "serverConfig": { + "matrix.org": { + "networks": [ + "_matrix", + "gitter", + "irc:freenode", + "irc:mozilla" + ] + } + }, "networkPatterns": { - "matrix:example_com": "#.*:example.com", - "matrix:matrix_org": "#.*:matrix.org", "gitter": "#gitter_.*:matrix.org", "irc:freenode": "#freenode_.*:matrix.org", "irc:mozilla": "#mozilla_.*:matrix.org" }, "networkNames": { - "matrix:example_com": "example.com", - "matrix:matrix_org": "matrix.org", "irc:freenode": "Freenode", "irc:mozilla": "Mozilla", "gitter": "Gitter" }, "networkIcons": { - "matrix:example_com": "//matrix.org/favicon.ico", - "matrix:matrix_org": "//matrix.org/favicon.ico", "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"