Refactor the search stuff in RoomView

* factor out the call to MatrixClient.search to a separate _getSearchBatch (so
  that we can reuse it for paginated results in a bit)
* Don't group cross-room searches by room - just display them in timeline
  order.
This commit is contained in:
Richard van der Hoff 2015-12-18 11:11:41 +00:00 committed by review.rocks
parent 9931ef1971
commit 4b271a332e
2 changed files with 109 additions and 102 deletions

View file

@ -490,75 +490,65 @@ module.exports = React.createClass({
}, },
onSearch: function(term, scope) { onSearch: function(term, scope) {
var filter; this.setState({
if (scope === "Room") { searchTerm: term,
filter = { searchScope: scope,
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( searchResults: [],
rooms: [ searchHighlights: [],
this.props.roomId searchCount: null,
]
};
}
var self = this;
self.setState({
searchInProgress: true
}); });
MatrixClientPeg.get().search({ this._getSearchBatch(term, scope);
body: { },
search_categories: {
room_events: {
search_term: term,
filter: filter,
order_by: "recent",
include_state: true,
groupings: {
group_by: [
{
key: "room_id"
}
]
},
event_context: {
before_limit: 1,
after_limit: 1,
include_profile: true,
}
}
}
}
}).then(function(data) {
if (!self.state.searching || term !== self.refs.search_bar.refs.search_term.value) { // fire off a request for a batch of search results
_getSearchBatch: function(term, scope) {
this.setState({
searchInProgress: true,
});
// make sure that we don't end up merging results from
// different searches by keeping a unique id.
//
// todo: should cancel any previous search requests.
var searchId = this.searchId = new Date().getTime();
var self = this;
MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope) })
.then(function(data) {
if (!self.state.searching || self.searchId != searchId) {
console.error("Discarding stale search results"); console.error("Discarding stale search results");
return; return;
} }
// for debugging: var results = data.search_categories.room_events;
// data.search_categories.room_events.highlights = ["hello", "everybody"];
var highlights; // postgres on synapse returns us precise details of the
if (data.search_categories.room_events.highlights && // strings which actually got matched for highlighting.
data.search_categories.room_events.highlights.length > 0)
{ // combine the highlight list with our existing list; build an object
// postgres on synapse returns us precise details of the // to avoid O(N^2) fail
// strings which actually got matched for highlighting. var highlights = {};
// for overlapping highlights, favour longer (more specific) terms first results.highlights.forEach(function(hl) { highlights[hl] = 1; });
highlights = data.search_categories.room_events.highlights self.state.searchHighlights.forEach(function(hl) { highlights[hl] = 1; });
.sort(function(a, b) { b.length - a.length });
} // turn it back into an ordered list. For overlapping highlights,
else { // favour longer (more specific) terms first
// sqlite doesn't, so just try to highlight the literal search term highlights = Object.keys(highlights).sort(function(a, b) { b.length - a.length });
// sqlite doesn't give us any highlights, so just try to highlight the literal search term
if (highlights.length == 0) {
highlights = [ term ]; highlights = [ term ];
} }
// append the new results to our existing results
var events = self.state.searchResults.concat(results.results);
self.setState({ self.setState({
highlights: highlights, searchHighlights: highlights,
searchTerm: term, searchResults: events,
searchResults: data, searchCount: results.count,
searchScope: scope,
searchCount: data.search_categories.room_events.count,
}); });
}, function(error) { }, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -570,7 +560,35 @@ module.exports = React.createClass({
self.setState({ self.setState({
searchInProgress: false searchInProgress: false
}); });
}); }).done();
},
_getSearchCondition: function(term, scope) {
var filter;
if (scope === "Room") {
filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [
this.props.roomId
]
};
}
return {
search_categories: {
room_events: {
search_term: term,
filter: filter,
order_by: "recent",
event_context: {
before_limit: 1,
after_limit: 1,
include_profile: true,
}
}
}
}
}, },
getEventTiles: function() { getEventTiles: function() {
@ -585,57 +603,44 @@ module.exports = React.createClass({
if (this.state.searchResults) if (this.state.searchResults)
{ {
if (!this.state.searchResults.search_categories.room_events.results || // XXX: todo: merge overlapping results somehow?
!this.state.searchResults.search_categories.room_events.groups) // XXX: why doesn't searching on name work?
{
return ret;
}
// XXX: this dance is foul, due to the results API not directly returning sorted results var lastRoomId;
var results = this.state.searchResults.search_categories.room_events.results;
var roomIdGroups = this.state.searchResults.search_categories.room_events.groups.room_id;
if (Array.isArray(results)) { for (var i = this.state.searchResults.length - 1; i >= 0; i--) {
// Old search API used to return results as a event_id -> result dict, but now var result = this.state.searchResults[i];
// returns a straightforward list. var mxEv = new Matrix.MatrixEvent(result.result);
results = results.reduce(function(prev, curr) {
prev[curr.result.event_id] = curr;
return prev;
}, {});
}
Object.keys(roomIdGroups)
.sort(function(a, b) { roomIdGroups[a].order - roomIdGroups[b].order }) // WHY NOT RETURN AN ORDERED ARRAY?!?!?!
.forEach(function(roomId)
{
// XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work?
if (self.state.searchScope === 'All') { if (self.state.searchScope === 'All') {
ret.push(<li key={ roomId }><h1>Room: { cli.getRoom(roomId).name }</h1></li>); var roomId = result.result.room_id;
if(roomId != lastRoomId) {
ret.push(<li key={mxEv.getId() + "-room"}><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
lastRoomId = roomId;
}
} }
var resultList = roomIdGroups[roomId].results.map(function(eventId) { return results[eventId]; }); var ts1 = result.result.origin_server_ts;
for (var i = resultList.length - 1; i >= 0; i--) { ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
var ts1 = resultList[i].result.origin_server_ts;
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank} if (result.context.events_before[0]) {
var mxEv = new Matrix.MatrixEvent(resultList[i].result); var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]);
if (resultList[i].context.events_before[0]) { if (EventTile.haveTileForEvent(mxEv2)) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_before[0]); ret.push(<li key={mxEv.getId() + "-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
if (EventTile.haveTileForEvent(mxEv2)) {
ret.push(<li key={mxEv.getId() + "-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
}
}
if (EventTile.haveTileForEvent(mxEv)) {
ret.push(<li key={mxEv.getId() + "+0"}><EventTile mxEvent={mxEv} highlights={self.state.highlights}/></li>);
}
if (resultList[i].context.events_after[0]) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_after[0]);
if (EventTile.haveTileForEvent(mxEv2)) {
ret.push(<li key={mxEv.getId() + "+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
}
} }
} }
});
if (EventTile.haveTileForEvent(mxEv)) {
ret.push(<li key={mxEv.getId() + "+0"}><EventTile mxEvent={mxEv} highlights={self.state.searchHighlights}/></li>);
}
if (result.context.events_after[0]) {
var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]);
if (EventTile.haveTileForEvent(mxEv2)) {
ret.push(<li key={mxEv.getId() + "+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
}
}
}
return ret; return ret;
} }

View file

@ -103,7 +103,9 @@ module.exports = React.createClass({
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} /> // <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
var searchStatus; var searchStatus;
if (this.props.searchInfo && this.props.searchInfo.searchTerm) { // don't display the search count until the search completes and
// gives us a non-null searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;({ this.props.searchInfo.searchCount } results)</div>; searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;({ this.props.searchInfo.searchCount } results)</div>;
} }