Merge pull request #2241 from vector-im/dbkr/paginate_publicrooms

Paginate Room Directory
pull/2280/head
Matthew Hodgson 2016-09-17 15:46:21 +01:00 committed by GitHub
commit 4720da3f8e
3 changed files with 121 additions and 50 deletions

View File

@ -29,6 +29,7 @@ var linkify = require('linkifyjs');
var linkifyString = require('linkifyjs/string'); var linkifyString = require('linkifyjs/string');
var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix'); var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
var sanitizeHtml = require('sanitize-html'); var sanitizeHtml = require('sanitize-html');
var q = require('q');
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -50,7 +51,6 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
publicRooms: [], publicRooms: [],
roomAlias: '',
loading: true, loading: true,
filterByNetwork: null, filterByNetwork: null,
} }
@ -64,6 +64,10 @@ module.exports = React.createClass({
this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]); this.networkPatterns[network] = new RegExp(this.props.config.networkPatterns[network]);
} }
} }
this.nextBatch = null;
this.filterString = null;
this.filterTimeout = null;
this.scrollPanel = null;
// dis.dispatch({ // dis.dispatch({
// action: 'ui_opacity', // action: 'ui_opacity',
@ -73,7 +77,7 @@ module.exports = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
this.getPublicRooms(); this.refreshRoomList();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -84,24 +88,48 @@ module.exports = React.createClass({
// }); // });
}, },
getPublicRooms: function() { refreshRoomList: function() {
var self = this; this.nextBatch = null;
MatrixClientPeg.get().publicRooms(function (err, data) { this.setState({
if (err) { publicRooms: [],
self.setState({ loading: false }); loading: true,
console.error("Failed to get publicRooms: %s", JSON.stringify(err)); });
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); this.getMoreRooms().done();
Modal.createDialog(ErrorDialog, { },
title: "Failed to get public room list",
description: err.message 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({ this.nextBatch = data.next_batch;
publicRooms: data.chunk, this.setState((s) => {
loading: false, 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); return MatrixClientPeg.get().deleteAlias(alias);
}).done(() => { }).done(() => {
modal.close(); modal.close();
this.getPublicRooms(); this.refreshRoomList();
}, function(err) { }, function(err) {
modal.close(); modal.close();
this.getPublicRooms(); this.refreshRoomList();
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to "+step, title: "Failed to "+step,
description: err.toString() description: err.toString()
@ -167,9 +195,47 @@ module.exports = React.createClass({
onNetworkChange: function(network) { onNetworkChange: function(network) {
this.setState({ this.setState({
filterByNetwork: network, 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) { showRoomAlias: function(alias) {
this.showRoom(null, alias); this.showRoom(null, alias);
}, },
@ -214,23 +280,17 @@ module.exports = React.createClass({
dis.dispatch(payload); dis.dispatch(payload);
}, },
getRows: function(filter) { getRows: function() {
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
if (!this.state.publicRooms) return []; if (!this.state.publicRooms) return [];
var rooms = this.state.publicRooms.filter((a) => { 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.state.filterByNetwork) {
if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false; if (!this._isRoomInNetwork(a, this.state.filterByNetwork)) return false;
} }
return (((a.name && a.name.toLowerCase().search(filter.toLowerCase()) >= 0) || return true;
(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;
}); });
var rows = []; var rows = [];
var self = this; var self = this;
@ -259,7 +319,7 @@ module.exports = React.createClass({
var topic = rooms[i].topic || ''; var topic = rooms[i].topic || '';
topic = linkifyString(sanitizeHtml(topic)); topic = linkifyString(sanitizeHtml(topic));
rows.unshift( rows.push(
<tr key={ rooms[i].room_id } <tr key={ rooms[i].room_id }
onClick={self.onRoomClicked.bind(self, rooms[i])} onClick={self.onRoomClicked.bind(self, rooms[i])}
// cancel onMouseDown otherwise shift-clicking highlights text // cancel onMouseDown otherwise shift-clicking highlights text
@ -289,12 +349,8 @@ module.exports = React.createClass({
return rows; return rows;
}, },
onKeyUp: function(ev) { collectScrollPanel: function(element) {
this.forceUpdate(); this.scrollPanel = element;
this.setState({ roomAlias : this.refs.roomAlias.value })
if (ev.key == "Enter") {
this.showRoomAlias(this.refs.roomAlias.value);
}
}, },
/** /**
@ -312,13 +368,27 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
let content;
if (this.state.loading) { if (this.state.loading) {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
return ( content = <div className="mx_RoomDirectory">
<div className="mx_RoomDirectory"> <Loader />
<Loader /> </div>;
</div> } else {
); const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
content = <ScrollPanel ref={this.collectScrollPanel}
className="mx_RoomDirectory_tableWrapper"
onFillRequest={ this.onFillRequest }
stickyBottom={false}
startAtBottom={false}
onResize={function(){}}
>
<table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows() }
</tbody>
</table>
</ScrollPanel>;
} }
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
@ -328,16 +398,12 @@ module.exports = React.createClass({
<SimpleRoomHeader title="Directory" /> <SimpleRoomHeader title="Directory" />
<div className="mx_RoomDirectory_list"> <div className="mx_RoomDirectory_list">
<div className="mx_RoomDirectory_listheader"> <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} onKeyUp={this.onFilterKeyUp}
/>
<NetworkDropdown config={this.props.config} onNetworkChange={this.onNetworkChange} /> <NetworkDropdown config={this.props.config} onNetworkChange={this.onNetworkChange} />
</div> </div>
<GeminiScrollbar className="mx_RoomDirectory_tableWrapper"> {content}
<table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows(this.state.roomAlias) }
</tbody>
</table>
</GeminiScrollbar>
</div> </div>
</div> </div>
); );

View File

@ -46,6 +46,11 @@ limitations under the License.
-webkit-flex-direction: column; -webkit-flex-direction: column;
} }
.mx_RoomDirectory_list .mx_RoomView_messageListWrapper {
justify-content: flex-start;
-webkit-justify-content: flex-start;
}
.mx_RoomDirectory_listheader { .mx_RoomDirectory_listheader {
display: table; display: table;
width: 100%; width: 100%;

View File

@ -76,7 +76,7 @@ describe('joining a room', function () {
httpBackend.when('GET', '/pushrules').respond(200, {}); httpBackend.when('GET', '/pushrules').respond(200, {});
httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' }); httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' });
httpBackend.when('GET', '/sync').respond(200, {}); httpBackend.when('GET', '/sync').respond(200, {});
httpBackend.when('GET', '/publicRooms').respond(200, {chunk: []}); httpBackend.when('POST', '/publicRooms').respond(200, {chunk: []});
httpBackend.when('GET', '/directory/room/'+encodeURIComponent(ROOM_ALIAS)).respond(200, { room_id: ROOM_ID }); httpBackend.when('GET', '/directory/room/'+encodeURIComponent(ROOM_ALIAS)).respond(200, { room_id: ROOM_ID });
// start with a logged-in client // start with a logged-in client