You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Enhancements to search results, and event context implementation
This change adds support to the JDK for processing the results of a room search, as well as back-paginating the results. It treats each search result as a 'context' object, which can itself be backwards or forward-paginated.
This commit is contained in:
committed by
review.rocks
parent
3e4cef89fd
commit
c669d21af7
184
lib/client.js
184
lib/client.js
@@ -12,6 +12,7 @@ var q = require("q");
|
|||||||
var httpApi = require("./http-api");
|
var httpApi = require("./http-api");
|
||||||
var MatrixEvent = require("./models/event").MatrixEvent;
|
var MatrixEvent = require("./models/event").MatrixEvent;
|
||||||
var EventStatus = require("./models/event").EventStatus;
|
var EventStatus = require("./models/event").EventStatus;
|
||||||
|
var SearchResult = require("./models/search-result");
|
||||||
var StubStore = require("./store/stub");
|
var StubStore = require("./store/stub");
|
||||||
var webRtcCall = require("./webrtc/call");
|
var webRtcCall = require("./webrtc/call");
|
||||||
var utils = require("./utils");
|
var utils = require("./utils");
|
||||||
@@ -1901,6 +1902,73 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
|
|||||||
return defer.promise;
|
return defer.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take an EventContext, and back/forward-fill results.
|
||||||
|
*
|
||||||
|
* @param {module:models/event-context.EventContext} eventContext context
|
||||||
|
* object to be updated
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {boolean} opts.backwards true to fill backwards, false to go forwards
|
||||||
|
* @param {boolean} opts.limit number of events to request
|
||||||
|
*
|
||||||
|
* @return {module:client.Promise} Resolves: updated EventContext object
|
||||||
|
* @return {Error} Rejects: with an error response.
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
|
||||||
|
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||||
|
// nicely with HTTP errors.
|
||||||
|
opts = opts || {};
|
||||||
|
var backwards = opts.backwards || false;
|
||||||
|
|
||||||
|
var token = eventContext.getPaginateToken(backwards);
|
||||||
|
if (!token) {
|
||||||
|
// no more results.
|
||||||
|
return q.reject(new Error("No paginate token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = backwards ? 'b' : 'f';
|
||||||
|
var pendingRequest = eventContext._paginateRequests[dir];
|
||||||
|
|
||||||
|
if (pendingRequest) {
|
||||||
|
// already a request in progress - return the existing promise
|
||||||
|
return pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = utils.encodeUri(
|
||||||
|
"/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()}
|
||||||
|
);
|
||||||
|
var params = {
|
||||||
|
from: token,
|
||||||
|
limit: ('limit' in opts) ? opts.limit : 30,
|
||||||
|
dir: dir
|
||||||
|
};
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var promise =
|
||||||
|
self._http.authedRequest(undefined, "GET", path, params
|
||||||
|
).then(function(res) {
|
||||||
|
var token = res.end;
|
||||||
|
if (res.chunk.length === 0) {
|
||||||
|
token = null;
|
||||||
|
} else {
|
||||||
|
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
||||||
|
if (backwards) {
|
||||||
|
// eventContext expects the events in timeline order, but
|
||||||
|
// back-pagination returns them in reverse order.
|
||||||
|
matrixEvents.reverse();
|
||||||
|
}
|
||||||
|
eventContext.addEvents(matrixEvents, backwards);
|
||||||
|
}
|
||||||
|
eventContext.setPaginateToken(token, backwards);
|
||||||
|
return eventContext;
|
||||||
|
}).finally(function() {
|
||||||
|
eventContext._paginateRequests[dir] = null;
|
||||||
|
});
|
||||||
|
eventContext._paginateRequests[dir] = promise;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
// Registration/Login operations
|
// Registration/Login operations
|
||||||
// =============================
|
// =============================
|
||||||
|
|
||||||
@@ -2095,6 +2163,122 @@ MatrixClient.prototype.searchMessageText = function(opts, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a server-side search for room events.
|
||||||
|
*
|
||||||
|
* The returned promise resolves to an object containing the fields:
|
||||||
|
*
|
||||||
|
* * {number} count: estimate of the number of results
|
||||||
|
* * {string} next_batch: token for back-pagination; if undefined, there are
|
||||||
|
* no more results
|
||||||
|
* * {Array} highlights: a list of words to highlight from the stemming
|
||||||
|
* algorithm
|
||||||
|
* * {Array} results: a list of results
|
||||||
|
*
|
||||||
|
* Each entry in the results list is a {module:models/search-result.SearchResult}.
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.term the term to search for
|
||||||
|
* @param {Object} opts.filter a JSON filter object to pass in the request
|
||||||
|
* @return {module:client.Promise} Resolves: result object
|
||||||
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.searchRoomEvents = function(opts) {
|
||||||
|
// TODO: support groups
|
||||||
|
|
||||||
|
var body = {
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
search_term: opts.term,
|
||||||
|
filter: opts.filter,
|
||||||
|
order_by: "recent",
|
||||||
|
event_context: {
|
||||||
|
before_limit: 1,
|
||||||
|
after_limit: 1,
|
||||||
|
include_profile: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var searchResults = {
|
||||||
|
_query: body,
|
||||||
|
results: [],
|
||||||
|
highlights: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.search({body: body}).then(
|
||||||
|
this._processRoomEventsSearch.bind(this, searchResults)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a result from an earlier searchRoomEvents call, and backfill results.
|
||||||
|
*
|
||||||
|
* @param {object} searchResults the results object to be updated
|
||||||
|
* @return {module:client.Promise} Resolves: updated result object
|
||||||
|
* @return {Error} Rejects: with an error response.
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
|
||||||
|
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||||
|
// nicely with HTTP errors.
|
||||||
|
|
||||||
|
if (!searchResults.next_batch) {
|
||||||
|
return q.reject(new Error("Cannot backpaginate event search any further"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchResults.pendingRequest) {
|
||||||
|
// already a request in progress - return the existing promise
|
||||||
|
return searchResults.pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchOpts = {
|
||||||
|
body: searchResults._query,
|
||||||
|
next_batch: searchResults.next_batch,
|
||||||
|
};
|
||||||
|
|
||||||
|
var promise = this.search(searchOpts).then(
|
||||||
|
this._processRoomEventsSearch.bind(this, searchResults)
|
||||||
|
).finally(function() {
|
||||||
|
searchResults.pendingRequest = null;
|
||||||
|
});
|
||||||
|
searchResults.pendingRequest = promise;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
|
||||||
|
* response from the API call and updates the searchResults
|
||||||
|
*
|
||||||
|
* @param {Object} searchResults
|
||||||
|
* @param {Object} response
|
||||||
|
* @return {Object} searchResults
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) {
|
||||||
|
var room_events = response.search_categories.room_events;
|
||||||
|
|
||||||
|
searchResults.count = room_events.count;
|
||||||
|
searchResults.next_batch = room_events.next_batch;
|
||||||
|
|
||||||
|
// combine the highlight list with our existing list; build an object
|
||||||
|
// to avoid O(N^2) fail
|
||||||
|
var highlights = {};
|
||||||
|
room_events.highlights.forEach(function(hl) { highlights[hl] = 1; });
|
||||||
|
searchResults.highlights.forEach(function(hl) { highlights[hl] = 1; });
|
||||||
|
|
||||||
|
// turn it back into a list.
|
||||||
|
searchResults.highlights = Object.keys(highlights);
|
||||||
|
|
||||||
|
// append the new results to our existing results
|
||||||
|
for (var i = 0; i < room_events.results.length; i++) {
|
||||||
|
var sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
|
||||||
|
searchResults.results.push(sr);
|
||||||
|
}
|
||||||
|
return searchResults;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a server-side search.
|
* Perform a server-side search.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
|||||||
104
lib/models/event-context.js
Normal file
104
lib/models/event-context.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/event-context
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new EventContext
|
||||||
|
*
|
||||||
|
* An eventcontext is used for circumstances such as search results, when we
|
||||||
|
* have a particular event of interest, and a bunch of events before and after
|
||||||
|
* it.
|
||||||
|
*
|
||||||
|
* It also stores pagination tokens for going backwards and forwards in the
|
||||||
|
* timeline.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} ourEvent the event at the centre of this context
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function EventContext(ourEvent) {
|
||||||
|
this._timeline = [ourEvent];
|
||||||
|
this._ourEventIndex = 0;
|
||||||
|
this._paginateTokens = {b: null, f: null};
|
||||||
|
|
||||||
|
// this is used by MatrixClient to keep track of active requests
|
||||||
|
this._paginateRequests = {b: null, f: null};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the main event of interest
|
||||||
|
*
|
||||||
|
* This is a convenience function for getTimeline()[getOurEventIndex()].
|
||||||
|
*
|
||||||
|
* @return {MatrixEvent} The event at the centre of this context.
|
||||||
|
*/
|
||||||
|
EventContext.prototype.getEvent = function() {
|
||||||
|
return this._timeline[this._ourEventIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of events in this context
|
||||||
|
*
|
||||||
|
* @return {Array} An array of MatrixEvents
|
||||||
|
*/
|
||||||
|
EventContext.prototype.getTimeline = function() {
|
||||||
|
return this._timeline;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index in the timeline of our event
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
*/
|
||||||
|
EventContext.prototype.getOurEventIndex = function() {
|
||||||
|
return this._ourEventIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pagination token.
|
||||||
|
*
|
||||||
|
* @param {boolean} backwards true to get the pagination token for going
|
||||||
|
* backwards in time
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
EventContext.prototype.getPaginateToken = function(backwards) {
|
||||||
|
return this._paginateTokens[backwards ? 'b' : 'f'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a pagination token.
|
||||||
|
*
|
||||||
|
* Generally this will be used only by the matrix js sdk.
|
||||||
|
*
|
||||||
|
* @param {string} token pagination token
|
||||||
|
* @param {boolean} backwards true to set the pagination token for going
|
||||||
|
* backwards in time
|
||||||
|
*/
|
||||||
|
EventContext.prototype.setPaginateToken = function(token, backwards) {
|
||||||
|
this._paginateTokens[backwards ? 'b' : 'f'] = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add more events to the timeline
|
||||||
|
*
|
||||||
|
* @param {Array} events new events, in timeline order
|
||||||
|
* @param {boolean} atStart true to insert new events at the start
|
||||||
|
*/
|
||||||
|
EventContext.prototype.addEvents = function(events, atStart) {
|
||||||
|
// TODO: should we share logic with Room.addEventsToTimeline?
|
||||||
|
// Should Room even use EventContext?
|
||||||
|
|
||||||
|
if (atStart) {
|
||||||
|
this._timeline = events.concat(this._timeline);
|
||||||
|
this._ourEventIndex += events.length;
|
||||||
|
} else {
|
||||||
|
this._timeline = this._timeline.concat(events);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The EventContext class
|
||||||
|
*/
|
||||||
|
module.exports = EventContext;
|
||||||
51
lib/models/search-result.js
Normal file
51
lib/models/search-result.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/search-result
|
||||||
|
*/
|
||||||
|
|
||||||
|
var EventContext = require("./event-context");
|
||||||
|
var utils = require("../utils");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new SearchResult
|
||||||
|
*
|
||||||
|
* @param {number} rank where this SearchResult ranks in the results
|
||||||
|
* @param {event-context.EventContext} eventContext the matching event and its
|
||||||
|
* context
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function SearchResult(rank, eventContext) {
|
||||||
|
this.rank = rank;
|
||||||
|
this.context = eventContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a SearchResponse from the response to /search
|
||||||
|
* @static
|
||||||
|
* @param {Object} jsonObj
|
||||||
|
* @param {function} eventMapper
|
||||||
|
* @return {SearchResult}
|
||||||
|
*/
|
||||||
|
|
||||||
|
SearchResult.fromJson = function(jsonObj, eventMapper) {
|
||||||
|
var jsonContext = jsonObj.context || {};
|
||||||
|
var events_before = jsonContext.events_before || [];
|
||||||
|
var events_after = jsonContext.events_after || [];
|
||||||
|
|
||||||
|
var context = new EventContext(eventMapper(jsonObj.result));
|
||||||
|
|
||||||
|
context.setPaginateToken(jsonContext.start, true);
|
||||||
|
context.addEvents(utils.map(events_before, eventMapper), true);
|
||||||
|
context.addEvents(utils.map(events_after, eventMapper), false);
|
||||||
|
context.setPaginateToken(jsonContext.end, false);
|
||||||
|
|
||||||
|
return new SearchResult(jsonObj.rank, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SearchResult class
|
||||||
|
*/
|
||||||
|
module.exports = SearchResult;
|
||||||
Reference in New Issue
Block a user