From a11516a984c222f7e307b1695cbf82cb92a248d3 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 16 Sep 2016 17:33:28 +0100
Subject: [PATCH] Make publicrooms use the new paginating API

Also do filtering on the server

WIP: This breaks the network dropdown
---
 src/components/structures/RoomDirectory.js    | 110 +++++++++++-------
 .../vector-web/structures/RoomDirectory.css   |   5 +
 2 files changed, 72 insertions(+), 43 deletions(-)

diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 3d82499687..7a54f6b54a 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,8 @@ module.exports = React.createClass({
                 this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]);
             }
         }
+        this.nextBatch = null;
+        this.filterString = null;
 
         // dis.dispatch({
         //     action: 'ui_opacity',
@@ -73,7 +75,7 @@ module.exports = React.createClass({
     },
 
     componentDidMount: function() {
-        this.getPublicRooms();
+        this.refreshRoomList();
     },
 
     componentWillUnmount: function() {
@@ -84,24 +86,34 @@ 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
-                });
-            }
-            else {
-                self.setState({
-                    publicRooms: data.chunk,
-                    loading: false,
-                });
-            }
+    refreshRoomList: function() {
+        this.nextBatch = null;
+        this.setState({
+            publicRooms: [],
+        });
+        this.getMoreRooms().done();
+    },
+
+    getMoreRooms: function() {
+        const opts = {limit: 20};
+        if (this.nextBatch) opts.since = this.nextBatch;
+        if (this.filterString) opts.filter = { generic_search_term: this.filterString } ;
+        return MatrixClientPeg.get().publicRooms(opts).then((data) => {
+            this.nextBatch = data.next_batch;
+            this.setState((s) => {
+                s.publicRooms.push(...data.chunk);
+                s.loading = false;
+                return s;
+            });
+            return Boolean(data.next_batch);
+        }, (err) => {
+            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 +154,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()
@@ -170,6 +182,23 @@ module.exports = React.createClass({
         });
     },
 
+    onFillRequest: function(backwards) {
+        if (backwards || !this.nextBatch) return q(false);
+
+        return this.getMoreRooms();
+    },
+
+    onFilterChange: function(ev) {
+        const alias = ev.target.value;
+
+        if (ev.key == "Enter") {
+            this.showRoomAlias(alias);
+        } else {
+            this.filterString = alias || null;
+            this.refreshRoomList();
+        }
+    },
+
     showRoomAlias: function(alias) {
         this.showRoom(null, alias);
     },
@@ -214,23 +243,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 +282,7 @@ module.exports = React.createClass({
             var topic = rooms[i].topic || '';
             topic = linkifyString(sanitizeHtml(topic));
 
-            rows.unshift(
+            rows.push(
                 <tr key={ rooms[i].room_id }
                     onClick={self.onRoomClicked.bind(self, rooms[i])}
                     // cancel onMouseDown otherwise shift-clicking highlights text
@@ -289,14 +312,6 @@ module.exports = React.createClass({
         return rows;
     },
 
-    onKeyUp: function(ev) {
-        this.forceUpdate();
-        this.setState({ roomAlias : this.refs.roomAlias.value })
-        if (ev.key == "Enter") {
-            this.showRoomAlias(this.refs.roomAlias.value);
-        }
-    },
-
     /**
      * Terrible temporary function that guess what network a public room
      * entry is in, until synapse is able to tell us
@@ -323,21 +338,30 @@ module.exports = React.createClass({
 
         const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
         const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
+        const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
         return (
             <div className="mx_RoomDirectory">
                 <SimpleRoomHeader title="Directory" />
                 <div className="mx_RoomDirectory_list">
                     <div className="mx_RoomDirectory_listheader">
-                        <input ref="roomAlias" placeholder="Join a room (e.g. #foo:domain.com)" className="mx_RoomDirectory_input" size="64" onKeyUp={ this.onKeyUp }/>
+                        <input type="text" placeholder="Find a room by keyword or room ID (#foo:matrix.org)"
+                            className="mx_RoomDirectory_input" size="64" onChange={this.onFilterChange}
+                        />
                         <NetworkDropdown config={this.props.config} onNetworkChange={this.onNetworkChange} />
                     </div>
-                    <GeminiScrollbar className="mx_RoomDirectory_tableWrapper">
+                    <ScrollPanel
+                        className="mx_RoomDirectory_tableWrapper"
+                        onFillRequest={ this.onFillRequest }
+                        stickyBottom={false}
+                        startAtBottom={false}
+                        onResize={function(){}}
+                    >
                         <table ref="directory_table" className="mx_RoomDirectory_table">
                             <tbody>
-                                { this.getRows(this.state.roomAlias) }
+                                { this.getRows() }
                             </tbody>
                         </table>
-                    </GeminiScrollbar>
+                    </ScrollPanel>
                 </div>
             </div>
         );
diff --git a/src/skins/vector/css/vector-web/structures/RoomDirectory.css b/src/skins/vector/css/vector-web/structures/RoomDirectory.css
index b4a2394593..17954e1c98 100644
--- a/src/skins/vector/css/vector-web/structures/RoomDirectory.css
+++ b/src/skins/vector/css/vector-web/structures/RoomDirectory.css
@@ -46,6 +46,11 @@ limitations under the License.
     -webkit-flex-direction: column;
 }
 
+.mx_RoomDirectory_list .mx_RoomView_messageListWrapper {
+    justify-content: flex-start;
+    -webkit-justify-content: flex-start;
+}
+
 .mx_RoomDirectory_listheader {
     display: table;
     width: 100%;