diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 3d824996873..95e2f71c5b5 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -29,6 +29,7 @@ var linkify = require('linkifyjs'); var linkifyString = require('linkifyjs/string'); var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix'); var sanitizeHtml = require('sanitize-html'); +var q = require('q'); linkifyMatrix(linkify); @@ -50,7 +51,6 @@ module.exports = React.createClass({ getInitialState: function() { return { publicRooms: [], - roomAlias: '', loading: true, filterByNetwork: null, } @@ -64,6 +64,10 @@ module.exports = React.createClass({ this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]); } } + this.nextBatch = null; + this.filterString = null; + this.filterTimeout = null; + this.scrollPanel = null; // dis.dispatch({ // action: 'ui_opacity', @@ -73,7 +77,7 @@ module.exports = React.createClass({ }, componentDidMount: function() { - this.getPublicRooms(); + this.refreshRoomList(); }, componentWillUnmount: function() { @@ -84,24 +88,48 @@ module.exports = React.createClass({ // }); }, - getPublicRooms: function() { - var self = this; - MatrixClientPeg.get().publicRooms(function (err, data) { - if (err) { - self.setState({ loading: false }); - console.error("Failed to get publicRooms: %s", JSON.stringify(err)); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Failed to get public room list", - description: err.message - }); + refreshRoomList: function() { + this.nextBatch = null; + this.setState({ + publicRooms: [], + loading: true, + }); + this.getMoreRooms().done(); + }, + + getMoreRooms: function() { + const my_filter_string = this.filterString; + const opts = {limit: 20}; + 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, + // throw away the result (don't even clear the busy flag + // since we must still have a request in flight) + return; } - else { - self.setState({ - publicRooms: data.chunk, - loading: false, - }); + + this.nextBatch = data.next_batch; + this.setState((s) => { + s.publicRooms.push(...data.chunk); + s.loading = false; + return s; + }); + return Boolean(data.next_batch); + }, (err) => { + if (my_filter_string != this.filterString) { + // as above: we don't care about errors for old + // requests either + return; } + this.setState({ loading: false }); + console.error("Failed to get publicRooms: %s", JSON.stringify(err)); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to get public room list", + description: err.message + }); }); }, @@ -142,10 +170,10 @@ module.exports = React.createClass({ return MatrixClientPeg.get().deleteAlias(alias); }).done(() => { modal.close(); - this.getPublicRooms(); + this.refreshRoomList(); }, function(err) { modal.close(); - this.getPublicRooms(); + this.refreshRoomList(); Modal.createDialog(ErrorDialog, { title: "Failed to "+step, description: err.toString() @@ -167,9 +195,47 @@ module.exports = React.createClass({ onNetworkChange: function(network) { this.setState({ 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(); }); }, + onFillRequest: function(backwards) { + if (backwards || !this.nextBatch) return q(false); + + return this.getMoreRooms(); + }, + + onFilterChange: function(ev) { + const alias = ev.target.value; + + this.filterString = alias || null; + + // don't send the request for a little bit, + // no point hammering the server with a + // request for every keystroke, let the + // user finish typing. + if (this.filterTimeout) { + clearTimeout(this.filterTimeout); + } + this.filterTimeout = setTimeout(() => { + this.filterTimeout = null; + this.refreshRoomList(); + }, 300); + }, + + onFilterKeyUp: function(ev) { + if (ev.key == "Enter") { + this.showRoomAlias(ev.target.value); + } + }, + showRoomAlias: function(alias) { this.showRoom(null, alias); }, @@ -214,23 +280,17 @@ module.exports = React.createClass({ dis.dispatch(payload); }, - getRows: function(filter) { + getRows: function() { var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); if (!this.state.publicRooms) return []; var rooms = this.state.publicRooms.filter((a) => { - // FIXME: if incrementally typing, keep narrowing down the search set - // incrementally rather than starting over each time. if (this.state.filterByNetwork) { if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false; } - return (((a.name && a.name.toLowerCase().search(filter.toLowerCase()) >= 0) || - (a.aliases && a.aliases[0].toLowerCase().search(filter.toLowerCase()) >= 0)) && - a.num_joined_members > 0); - }).sort(function(a,b) { - return a.num_joined_members - b.num_joined_members; + return true; }); var rows = []; var self = this; @@ -259,7 +319,7 @@ module.exports = React.createClass({ var topic = rooms[i].topic || ''; topic = linkifyString(sanitizeHtml(topic)); - rows.unshift( + rows.push(