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 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 });
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;
}
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
});
}
else {
self.setState({
publicRooms: data.chunk,
loading: false,
});
}
});
},
@ -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(
<tr key={ rooms[i].room_id }
onClick={self.onRoomClicked.bind(self, rooms[i])}
// cancel onMouseDown otherwise shift-clicking highlights text
@ -289,12 +349,8 @@ 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);
}
collectScrollPanel: function(element) {
this.scrollPanel = element;
},
/**
@ -312,13 +368,27 @@ module.exports = React.createClass({
},
render: function() {
let content;
if (this.state.loading) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<div className="mx_RoomDirectory">
const Loader = sdk.getComponent("elements.Spinner");
content = <div className="mx_RoomDirectory">
<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');
@ -328,16 +398,12 @@ module.exports = React.createClass({
<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} onKeyUp={this.onFilterKeyUp}
/>
<NetworkDropdown config={this.props.config} onNetworkChange={this.onNetworkChange} />
</div>
<GeminiScrollbar className="mx_RoomDirectory_tableWrapper">
<table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows(this.state.roomAlias) }
</tbody>
</table>
</GeminiScrollbar>
{content}
</div>
</div>
);

View File

@ -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%;

View File

@ -76,7 +76,7 @@ describe('joining a room', function () {
httpBackend.when('GET', '/pushrules').respond(200, {});
httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' });
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 });
// start with a logged-in client