mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
Merge branch 'develop' into kegan/guest-access
This commit is contained in:
commit
afbb451d4a
4 changed files with 202 additions and 135 deletions
|
@ -43,6 +43,13 @@ var INITIAL_SIZE = 20;
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
|
if (DEBUG_SCROLL) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function () {};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomView',
|
displayName: 'RoomView',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -330,8 +337,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
|
||||||
this.refs.messagePanel.checkFillState();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -346,53 +351,49 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_paginateCompleted: function() {
|
_paginateCompleted: function() {
|
||||||
if (DEBUG_SCROLL) console.log("paginate complete");
|
debuglog("paginate complete");
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({paginating: false});
|
this.setState({paginating: false});
|
||||||
|
|
||||||
// we might not have got enough (or, indeed, any) results from the
|
|
||||||
// pagination request, so give the messagePanel a chance to set off
|
|
||||||
// another.
|
|
||||||
|
|
||||||
if (this.refs.messagePanel) {
|
|
||||||
this.refs.messagePanel.checkFillState();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
if (!backwards || this.state.searchInProgress)
|
if (!backwards)
|
||||||
return;
|
return q(false);
|
||||||
|
|
||||||
if (this.nextSearchBatch) {
|
if (this.state.searchResults.next_batch) {
|
||||||
if (DEBUG_SCROLL) console.log("requesting more search results");
|
debuglog("requesting more search results");
|
||||||
this._getSearchBatch(this.state.searchTerm,
|
var searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
|
||||||
this.state.searchScope);
|
this.state.searchResults);
|
||||||
|
return this._handleSearchResult(searchPromise);
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG_SCROLL) console.log("no more search results");
|
debuglog("no more search results");
|
||||||
|
return q(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
if (!backwards || this.state.paginating)
|
if (!backwards)
|
||||||
return;
|
return q(false);
|
||||||
|
|
||||||
// Either wind back the message cap (if there are enough events in the
|
// Either wind back the message cap (if there are enough events in the
|
||||||
// timeline to do so), or fire off a pagination request.
|
// timeline to do so), or fire off a pagination request.
|
||||||
|
|
||||||
if (this.state.messageCap < this.state.room.timeline.length) {
|
if (this.state.messageCap < this.state.room.timeline.length) {
|
||||||
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
||||||
if (DEBUG_SCROLL) console.log("winding back message cap to", cap);
|
debuglog("winding back message cap to", cap);
|
||||||
this.setState({messageCap: cap});
|
this.setState({messageCap: cap});
|
||||||
|
return q(true);
|
||||||
} else if(this.state.room.oldState.paginationToken) {
|
} else if(this.state.room.oldState.paginationToken) {
|
||||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
var cap = this.state.messageCap + PAGINATE_SIZE;
|
||||||
if (DEBUG_SCROLL) console.log("starting paginate to cap", cap);
|
debuglog("starting paginate to cap", cap);
|
||||||
this.setState({messageCap: cap, paginating: true});
|
this.setState({messageCap: cap, paginating: true});
|
||||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
|
return MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).
|
||||||
|
finally(this._paginateCompleted).then(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -486,10 +487,8 @@ module.exports = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
searchTerm: term,
|
searchTerm: term,
|
||||||
searchScope: scope,
|
searchScope: scope,
|
||||||
searchResults: [],
|
searchResults: {},
|
||||||
searchHighlights: [],
|
searchHighlights: [],
|
||||||
searchCount: null,
|
|
||||||
searchCanPaginate: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// if we already have a search panel, we need to tell it to forget
|
// if we already have a search panel, we need to tell it to forget
|
||||||
|
@ -498,64 +497,68 @@ module.exports = React.createClass({
|
||||||
this.refs.searchResultsPanel.resetScrollState();
|
this.refs.searchResultsPanel.resetScrollState();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nextSearchBatch = null;
|
// make sure that we don't end up showing results from
|
||||||
this._getSearchBatch(term, scope);
|
// an aborted search by keeping a unique id.
|
||||||
|
//
|
||||||
|
// todo: should cancel any previous search requests.
|
||||||
|
this.searchId = new Date().getTime();
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("sending search request");
|
||||||
|
|
||||||
|
var searchPromise = MatrixClientPeg.get().searchRoomEvents({
|
||||||
|
filter: filter,
|
||||||
|
term: term,
|
||||||
|
});
|
||||||
|
this._handleSearchResult(searchPromise).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
// fire off a request for a batch of search results
|
_handleSearchResult: function(searchPromise) {
|
||||||
_getSearchBatch: function(term, scope) {
|
var self = this;
|
||||||
|
|
||||||
|
// keep a record of the current search id, so that if the search terms
|
||||||
|
// change before we get a response, we can ignore the results.
|
||||||
|
var localSearchId = this.searchId;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchInProgress: true,
|
searchInProgress: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// make sure that we don't end up merging results from
|
return searchPromise.then(function(results) {
|
||||||
// different searches by keeping a unique id.
|
debuglog("search complete");
|
||||||
//
|
if (!self.state.searching || self.searchId != localSearchId) {
|
||||||
// todo: should cancel any previous search requests.
|
|
||||||
var searchId = this.searchId = new Date().getTime();
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (DEBUG_SCROLL) console.log("sending search request");
|
|
||||||
MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope),
|
|
||||||
next_batch: this.nextSearchBatch })
|
|
||||||
.then(function(data) {
|
|
||||||
if (DEBUG_SCROLL) console.log("search complete");
|
|
||||||
if (!self.state.searching || self.searchId != searchId) {
|
|
||||||
console.error("Discarding stale search results");
|
console.error("Discarding stale search results");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = data.search_categories.room_events;
|
// postgres on synapse returns us precise details of the strings
|
||||||
|
// which actually got matched for highlighting.
|
||||||
|
//
|
||||||
|
// In either case, we want to highlight the literal search term
|
||||||
|
// whether it was used by the search engine or not.
|
||||||
|
|
||||||
// postgres on synapse returns us precise details of the
|
var highlights = results.highlights;
|
||||||
// strings which actually got matched for highlighting.
|
if (highlights.indexOf(self.state.searchTerm) < 0) {
|
||||||
|
highlights = highlights.concat(self.state.searchTerm);
|
||||||
// combine the highlight list with our existing list; build an object
|
|
||||||
// to avoid O(N^2) fail
|
|
||||||
var highlights = {};
|
|
||||||
results.highlights.forEach(function(hl) { highlights[hl] = 1; });
|
|
||||||
self.state.searchHighlights.forEach(function(hl) { highlights[hl] = 1; });
|
|
||||||
|
|
||||||
// turn it back into an ordered list. For overlapping highlights,
|
|
||||||
// favour longer (more specific) terms first
|
|
||||||
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 ];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append the new results to our existing results
|
// For overlapping highlights,
|
||||||
var events = self.state.searchResults.concat(results.results);
|
// favour longer (more specific) terms first
|
||||||
|
highlights = highlights.sort(function(a, b) { b.length - a.length });
|
||||||
|
|
||||||
self.setState({
|
self.setState({
|
||||||
searchHighlights: highlights,
|
searchHighlights: highlights,
|
||||||
searchResults: events,
|
searchResults: results,
|
||||||
searchCount: results.count,
|
|
||||||
searchCanPaginate: !!(results.next_batch),
|
|
||||||
});
|
});
|
||||||
self.nextSearchBatch = results.next_batch;
|
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
@ -566,51 +569,27 @@ 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getSearchResultTiles: function() {
|
getSearchResultTiles: function() {
|
||||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
|
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
// XXX: todo: merge overlapping results somehow?
|
// XXX: todo: merge overlapping results somehow?
|
||||||
// XXX: why doesn't searching on name work?
|
// XXX: why doesn't searching on name work?
|
||||||
|
|
||||||
|
if (this.state.searchResults.results === undefined) {
|
||||||
|
// awaiting results
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.searchCanPaginate === false) {
|
var ret = [];
|
||||||
if (this.state.searchResults.length == 0) {
|
|
||||||
|
if (!this.state.searchResults.next_batch) {
|
||||||
|
if (this.state.searchResults.results.length == 0) {
|
||||||
ret.push(<li key="search-top-marker">
|
ret.push(<li key="search-top-marker">
|
||||||
<h2 className="mx_RoomView_topMarker">No results</h2>
|
<h2 className="mx_RoomView_topMarker">No results</h2>
|
||||||
</li>
|
</li>
|
||||||
|
@ -625,9 +604,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var lastRoomId;
|
var lastRoomId;
|
||||||
|
|
||||||
for (var i = this.state.searchResults.length - 1; i >= 0; i--) {
|
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
|
||||||
var result = this.state.searchResults[i];
|
var result = this.state.searchResults.results[i];
|
||||||
var mxEv = new Matrix.MatrixEvent(result.result);
|
|
||||||
|
var mxEv = result.context.getEvent();
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
// XXX: can this ever happen? It will make the result count
|
// XXX: can this ever happen? It will make the result count
|
||||||
|
@ -638,29 +618,28 @@ module.exports = React.createClass({
|
||||||
var eventId = mxEv.getId();
|
var eventId = mxEv.getId();
|
||||||
|
|
||||||
if (this.state.searchScope === 'All') {
|
if (this.state.searchScope === 'All') {
|
||||||
var roomId = result.result.room_id;
|
var roomId = mxEv.getRoomId();
|
||||||
if(roomId != lastRoomId) {
|
if(roomId != lastRoomId) {
|
||||||
ret.push(<li key={eventId + "-room"}><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
|
ret.push(<li key={eventId + "-room"}><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
|
||||||
lastRoomId = roomId;
|
lastRoomId = roomId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ts1 = result.result.origin_server_ts;
|
var ts1 = mxEv.getTs();
|
||||||
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
|
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
|
||||||
|
|
||||||
if (result.context.events_before[0]) {
|
var timeline = result.context.getTimeline();
|
||||||
var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]);
|
for (var j = 0; j < timeline.length; j++) {
|
||||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
var ev = timeline[j];
|
||||||
ret.push(<li key={eventId+"-1"} data-scroll-token={eventId+"-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
var highlights;
|
||||||
|
var contextual = (j != result.context.getOurEventIndex());
|
||||||
|
if (!contextual) {
|
||||||
|
highlights = this.state.searchHighlights;
|
||||||
}
|
}
|
||||||
}
|
if (EventTile.haveTileForEvent(ev)) {
|
||||||
|
ret.push(<li key={eventId+"+"+j} data-scroll-token={eventId+"+"+j}>
|
||||||
ret.push(<li key={eventId+"+0"} data-scroll-token={eventId+"+0"}><EventTile mxEvent={mxEv} highlights={this.state.searchHighlights}/></li>);
|
<EventTile mxEvent={ev} contextual={contextual} highlights={highlights} />
|
||||||
|
</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={eventId+"+1"} data-scroll-token={eventId+"+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1296,7 +1275,7 @@ module.exports = React.createClass({
|
||||||
searchInfo = {
|
searchInfo = {
|
||||||
searchTerm : this.state.searchTerm,
|
searchTerm : this.state.searchTerm,
|
||||||
searchScope : this.state.searchScope,
|
searchScope : this.state.searchScope,
|
||||||
searchCount : this.state.searchCount,
|
searchCount : this.state.searchResults.count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,17 @@ limitations under the License.
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
|
if (DEBUG_SCROLL) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function () {};
|
||||||
|
}
|
||||||
|
|
||||||
/* This component implements an intelligent scrolling list.
|
/* This component implements an intelligent scrolling list.
|
||||||
*
|
*
|
||||||
* It wraps a list of <li> children; when items are added to the start or end
|
* It wraps a list of <li> children; when items are added to the start or end
|
||||||
|
@ -51,7 +59,16 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||||
* the user nears the start (backwards = true) or end (backwards =
|
* the user nears the start (backwards = true) or end (backwards =
|
||||||
* false) of the list
|
* false) of the list.
|
||||||
|
*
|
||||||
|
* This should return a promise; no more calls will be made until the
|
||||||
|
* promise completes.
|
||||||
|
*
|
||||||
|
* The promise should resolve to true if there is more data to be
|
||||||
|
* retrieved in this direction (in which case onFillRequest may be
|
||||||
|
* called again immediately), or false if there is no more data in this
|
||||||
|
* directon (at this time) - which will stop the pagination cycle until
|
||||||
|
* the user scrolls again.
|
||||||
*/
|
*/
|
||||||
onFillRequest: React.PropTypes.func,
|
onFillRequest: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -71,25 +88,33 @@ module.exports = React.createClass({
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
stickyBottom: true,
|
stickyBottom: true,
|
||||||
onFillRequest: function(backwards) {},
|
onFillRequest: function(backwards) { return q(false); },
|
||||||
onScroll: function() {},
|
onScroll: function() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._pendingFillRequests = {b: null, f: null};
|
||||||
this.resetScrollState();
|
this.resetScrollState();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.checkFillState();
|
||||||
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
// after adding event tiles, we may need to tweak the scroll (either to
|
// after adding event tiles, we may need to tweak the scroll (either to
|
||||||
// keep at the bottom of the timeline, or to maintain the view after
|
// keep at the bottom of the timeline, or to maintain the view after
|
||||||
// adding events to the top).
|
// adding events to the top).
|
||||||
this._restoreSavedScrollState();
|
this._restoreSavedScrollState();
|
||||||
|
|
||||||
|
// we also re-check the fill state, in case the paginate was inadequate
|
||||||
|
this.checkFillState();
|
||||||
},
|
},
|
||||||
|
|
||||||
onScroll: function(ev) {
|
onScroll: function(ev) {
|
||||||
var sn = this._getScrollNode();
|
var sn = this._getScrollNode();
|
||||||
if (DEBUG_SCROLL) console.log("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
||||||
|
|
||||||
// Sometimes we see attempts to write to scrollTop essentially being
|
// Sometimes we see attempts to write to scrollTop essentially being
|
||||||
// ignored. (Or rather, it is successfully written, but on the next
|
// ignored. (Or rather, it is successfully written, but on the next
|
||||||
|
@ -113,7 +138,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollState = this._calculateScrollState();
|
this.scrollState = this._calculateScrollState();
|
||||||
if (DEBUG_SCROLL) console.log("Saved scroll state", this.scrollState);
|
debuglog("Saved scroll state", this.scrollState);
|
||||||
|
|
||||||
this.props.onScroll(ev);
|
this.props.onScroll(ev);
|
||||||
|
|
||||||
|
@ -135,11 +160,74 @@ module.exports = React.createClass({
|
||||||
checkFillState: function() {
|
checkFillState: function() {
|
||||||
var sn = this._getScrollNode();
|
var sn = this._getScrollNode();
|
||||||
|
|
||||||
|
// if there is less than a screenful of messages above or below the
|
||||||
|
// viewport, try to get some more messages.
|
||||||
|
//
|
||||||
|
// scrollTop is the number of pixels between the top of the content and
|
||||||
|
// the top of the viewport.
|
||||||
|
//
|
||||||
|
// scrollHeight is the total height of the content.
|
||||||
|
//
|
||||||
|
// clientHeight is the height of the viewport (excluding borders,
|
||||||
|
// margins, and scrollbars).
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// .---------. - -
|
||||||
|
// | | | scrollTop |
|
||||||
|
// .-+---------+-. - - |
|
||||||
|
// | | | | | |
|
||||||
|
// | | | | | clientHeight | scrollHeight
|
||||||
|
// | | | | | |
|
||||||
|
// `-+---------+-' - |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// `---------' -
|
||||||
|
//
|
||||||
|
|
||||||
if (sn.scrollTop < sn.clientHeight) {
|
if (sn.scrollTop < sn.clientHeight) {
|
||||||
// there's less than a screenful of messages left - try to get some
|
// need to back-fill
|
||||||
// more messages.
|
this._maybeFill(true);
|
||||||
this.props.onFillRequest(true);
|
|
||||||
}
|
}
|
||||||
|
if (sn.scrollTop > sn.scrollHeight - sn.clientHeight * 2) {
|
||||||
|
// need to forward-fill
|
||||||
|
this._maybeFill(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// check if there is already a pending fill request. If not, set one off.
|
||||||
|
_maybeFill: function(backwards) {
|
||||||
|
var dir = backwards ? 'b' : 'f';
|
||||||
|
if (this._pendingFillRequests[dir]) {
|
||||||
|
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||||
|
|
||||||
|
// onFillRequest can end up calling us recursively (via onScroll
|
||||||
|
// events) so make sure we set this before firing off the call. That
|
||||||
|
// does present the risk that we might not ever actually fire off the
|
||||||
|
// fill request, so wrap it in a try/catch.
|
||||||
|
this._pendingFillRequests[dir] = true;
|
||||||
|
var fillPromise;
|
||||||
|
try {
|
||||||
|
fillPromise = this.props.onFillRequest(backwards);
|
||||||
|
} catch (e) {
|
||||||
|
this._pendingFillRequests[dir] = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
q.finally(fillPromise, () => {
|
||||||
|
debuglog("ScrollPanel: "+dir+" fill complete");
|
||||||
|
this._pendingFillRequests[dir] = false;
|
||||||
|
}).then((hasMoreResults) => {
|
||||||
|
if (hasMoreResults) {
|
||||||
|
// further pagination requests have been disabled until now, so
|
||||||
|
// it's time to check the fill state again in case the pagination
|
||||||
|
// was insufficient.
|
||||||
|
this.checkFillState();
|
||||||
|
}
|
||||||
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
// get the current scroll position of the room, so that it can be
|
// get the current scroll position of the room, so that it can be
|
||||||
|
@ -163,13 +251,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
scrollToTop: function() {
|
scrollToTop: function() {
|
||||||
this._getScrollNode().scrollTop = 0;
|
this._getScrollNode().scrollTop = 0;
|
||||||
if (DEBUG_SCROLL) console.log("Scrolled to top");
|
debuglog("Scrolled to top");
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom: function() {
|
scrollToBottom: function() {
|
||||||
var scrollNode = this._getScrollNode();
|
var scrollNode = this._getScrollNode();
|
||||||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||||
if (DEBUG_SCROLL) console.log("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
||||||
},
|
},
|
||||||
|
|
||||||
// scroll the message list to the node with the given scrollToken. See
|
// scroll the message list to the node with the given scrollToken. See
|
||||||
|
@ -206,10 +294,10 @@ module.exports = React.createClass({
|
||||||
this.recentEventScroll = scrollNode.scrollTop;
|
this.recentEventScroll = scrollNode.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG_SCROLL) {
|
debuglog("Scrolled to token", node.dataset.scrollToken, "+",
|
||||||
console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
|
pixelOffset+":", scrollNode.scrollTop,
|
||||||
console.log("recentEventScroll now "+this.recentEventScroll);
|
"(delta: "+scrollDelta+")");
|
||||||
}
|
debuglog("recentEventScroll now "+this.recentEventScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculateScrollState: function() {
|
_calculateScrollState: function() {
|
||||||
|
|
|
@ -254,7 +254,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onPopulateInvite}>
|
<form onSubmit={this.onPopulateInvite}>
|
||||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
|
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,8 +109,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var searchStatus;
|
var searchStatus;
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a non-null searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) {
|
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
|
||||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue