diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index c9d3bc732d..df8c850d0d 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -48,6 +48,17 @@ module.exports = React.createClass({
ConferenceHandler: React.PropTypes.any
},
+ /* properties in RoomView objects include:
+ *
+ * savedScrollState: the current scroll position in the backlog. Response
+ * from _calculateScrollState. Updated on scroll events.
+ *
+ * savedSearchScrollState: similar to savedScrollState, but specific to the
+ * search results (we need to preserve savedScrollState when search
+ * results are visible)
+ *
+ * eventNodes: a map from event id to DOM node representing that event
+ */
getInitialState: function() {
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
return {
@@ -207,7 +218,7 @@ module.exports = React.createClass({
if (!toStartOfTimeline &&
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
// update unread count when scrolled up
- if (this.savedScrollState.atBottom) {
+ if (!this.state.searchResults && this.savedScrollState.atBottom) {
currentUnread = 0;
}
else {
@@ -331,9 +342,6 @@ module.exports = React.createClass({
// 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
// adding events to the top).
-
- if (this.state.searchResults) return;
-
this._restoreSavedScrollState();
},
@@ -441,11 +449,16 @@ module.exports = React.createClass({
this.recentEventScroll = undefined;
}
- if (this.refs.messagePanel && !this.state.searchResults) {
- this.savedScrollState = this._calculateScrollState();
- if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
- if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
- this.setState({numUnreadMessages: 0});
+ if (this.refs.messagePanel) {
+ if (this.state.searchResults) {
+ this.savedSearchScrollState = this._calculateScrollState();
+ if (DEBUG_SCROLL) console.log("Saved search scroll state", this.savedSearchScrollState);
+ } else {
+ this.savedScrollState = this._calculateScrollState();
+ if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
+ if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
+ this.setState({numUnreadMessages: 0});
+ }
}
}
if (!this.state.paginating && !this.state.searchInProgress) {
@@ -510,6 +523,7 @@ module.exports = React.createClass({
searchCount: null,
});
+ this.savedSearchScrollState = {atBottom: true};
this.nextSearchBatch = null;
this._getSearchBatch(term, scope);
},
@@ -629,10 +643,18 @@ module.exports = React.createClass({
var result = this.state.searchResults[i];
var mxEv = new Matrix.MatrixEvent(result.result);
+ if (!EventTile.haveTileForEvent(mxEv)) {
+ // XXX: can this ever happen? It will make the result count
+ // not match the displayed count.
+ continue;
+ }
+
+ var eventId = mxEv.getId();
+
if (self.state.searchScope === 'All') {
var roomId = result.result.room_id;
if(roomId != lastRoomId) {
- ret.push(
Room: { cli.getRoom(roomId).name }
);
+ ret.push(Room: { cli.getRoom(roomId).name }
);
lastRoomId = roomId;
}
}
@@ -643,18 +665,16 @@ module.exports = React.createClass({
if (result.context.events_before[0]) {
var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]);
if (EventTile.haveTileForEvent(mxEv2)) {
- ret.push();
+ ret.push();
}
}
- if (EventTile.haveTileForEvent(mxEv)) {
- ret.push();
- }
+ ret.push();
if (result.context.events_after[0]) {
var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]);
if (EventTile.haveTileForEvent(mxEv2)) {
- ret.push();
+ ret.push();
}
}
}
@@ -706,15 +726,17 @@ module.exports = React.createClass({
continuation = false;
}
+ var eventId = mxEv.getId();
ret.unshift(
-
+
+
+
);
if (dateSeparator) {
ret.unshift(dateSeparator);
}
++count;
}
- this.lastEventTileCount = count;
return ret;
},
@@ -884,7 +906,7 @@ module.exports = React.createClass({
},
onCancelClick: function() {
- this.setState(this.getInitialState());
+ this.setState({editingRoomSettings: false});
},
onLeaveClick: function() {
@@ -918,6 +940,13 @@ module.exports = React.createClass({
this.setState({ searching: true });
},
+ onCancelSearchClick: function () {
+ this.setState({
+ searching: false,
+ searchResults: null,
+ });
+ },
+
onConferenceNotificationClick: function() {
dis.dispatch({
action: 'place_call',
@@ -945,12 +974,6 @@ module.exports = React.createClass({
// pixel_offset gives the number of pixels between the bottom of the event
// and the bottom of the container.
scrollToEvent: function(eventId, pixelOffset) {
- var scrollNode = this._getScrollNode();
- if (!scrollNode) return;
-
- var messageWrapper = this.refs.messagePanel;
- if (messageWrapper === undefined) return;
-
var idx = this._indexForEventId(eventId);
if (idx === null) {
// we don't seem to have this event in our timeline. Presumably
@@ -960,7 +983,7 @@ module.exports = React.createClass({
//
// for now, just scroll to the top of the buffer.
console.log("Refusing to scroll to unknown event "+eventId);
- scrollNode.scrollTop = 0;
+ this._getScrollNode().scrollTop = 0;
return;
}
@@ -978,14 +1001,88 @@ module.exports = React.createClass({
this.setState({messageCap: minCap});
}
- var node = this.eventNodes[eventId];
- if (node === null) {
- // getEventTiles should have sorted this out when we set the
- // messageCap, so this is weird.
- console.error("No node for event, even after rolling back messageCap");
+ // the scrollTokens on our DOM nodes are the event IDs, so we can pass
+ // eventId directly into _scrollToToken.
+ this._scrollToToken(eventId, pixelOffset);
+ },
+
+ _restoreSavedScrollState: function() {
+ var scrollState = this.state.searchResults ? this.savedSearchScrollState : this.savedScrollState;
+ if (!scrollState || scrollState.atBottom) {
+ this.scrollToBottom();
+ } else if (scrollState.lastDisplayedScrollToken) {
+ this._scrollToToken(scrollState.lastDisplayedScrollToken,
+ scrollState.pixelOffset);
+ }
+ },
+
+ _calculateScrollState: function() {
+ // we don't save the absolute scroll offset, because that
+ // would be affected by window width, zoom level, amount of scrollback,
+ // etc.
+ //
+ // instead we save an identifier for the last fully-visible message,
+ // and the number of pixels the window was scrolled below it - which
+ // will hopefully be near enough.
+ //
+ // Our scroll implementation is agnostic of the precise contents of the
+ // message list (since it needs to work with both search results and
+ // timelines). 'refs.messageList' is expected to be a DOM node with a
+ // number of children, each of which may have a 'data-scroll-token'
+ // attribute. It is this token which is stored as the
+ // 'lastDisplayedScrollToken'.
+
+ var messageWrapperScroll = this._getScrollNode();
+ // + 1 here to avoid fractional pixel rounding errors
+ var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
+
+ var messageWrapper = this.refs.messagePanel;
+ var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
+ var messages = this.refs.messageList.children;
+
+ for (var i = messages.length-1; i >= 0; --i) {
+ var node = messages[i];
+ if (!node.dataset.scrollToken) continue;
+
+ var boundingRect = node.getBoundingClientRect();
+ if (boundingRect.bottom < wrapperRect.bottom) {
+ return {
+ atBottom: atBottom,
+ lastDisplayedScrollToken: node.dataset.scrollToken,
+ pixelOffset: wrapperRect.bottom - boundingRect.bottom,
+ }
+ }
+ }
+
+ // apparently the entire timeline is below the viewport. Give up.
+ return { atBottom: true };
+ },
+
+ // scroll the message list to the node with the given scrollToken. See
+ // notes in _calculateScrollState on how this works.
+ //
+ // pixel_offset gives the number of pixels between the bottom of the node
+ // and the bottom of the container.
+ _scrollToToken: function(scrollToken, pixelOffset) {
+ /* find the dom node with the right scrolltoken */
+ var node;
+ var messages = this.refs.messageList.children;
+ for (var i = messages.length-1; i >= 0; --i) {
+ var m = messages[i];
+ if (!m.dataset.scrollToken) continue;
+ if (m.dataset.scrollToken == scrollToken) {
+ node = m;
+ break;
+ }
+ }
+
+ if (!node) {
+ console.error("No node with scrollToken '"+scrollToken+"'");
return;
}
+ var scrollNode = this._getScrollNode();
+ var messageWrapper = this.refs.messagePanel;
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
@@ -997,59 +1094,11 @@ module.exports = React.createClass({
}
if (DEBUG_SCROLL) {
- console.log("Scrolled to event", eventId, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
+ console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
console.log("recentEventScroll now "+this.recentEventScroll);
}
},
- _restoreSavedScrollState: function() {
- var scrollState = this.savedScrollState;
- if (scrollState.atBottom) {
- this.scrollToBottom();
- } else if (scrollState.lastDisplayedEvent) {
- this.scrollToEvent(scrollState.lastDisplayedEvent,
- scrollState.pixelOffset);
- }
- },
-
- _calculateScrollState: function() {
- // we don't save the absolute scroll offset, because that
- // would be affected by window width, zoom level, amount of scrollback,
- // etc.
- //
- // instead we save the id of the last fully-visible event, and the
- // number of pixels the window was scrolled below it - which will
- // hopefully be near enough.
- //
- if (this.eventNodes === undefined) return null;
-
- var messageWrapper = this.refs.messagePanel;
- if (messageWrapper === undefined) return null;
- var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
-
- var messageWrapperScroll = this._getScrollNode();
- // + 1 here to avoid fractional pixel rounding errors
- var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
-
- for (var i = this.state.room.timeline.length-1; i >= 0; --i) {
- var ev = this.state.room.timeline[i];
- var node = this.eventNodes[ev.getId()];
- if (!node) continue;
-
- var boundingRect = node.getBoundingClientRect();
- if (boundingRect.bottom < wrapperRect.bottom) {
- return {
- atBottom: atBottom,
- lastDisplayedEvent: ev.getId(),
- pixelOffset: wrapperRect.bottom - boundingRect.bottom,
- }
- }
- }
-
- // apparently the entire timeline is below the viewport. Give up.
- return { atBottom: true };
- },
-
// get the current scroll position of the room, so that it can be
// restored when we switch back to it
getScrollState: function() {
@@ -1057,11 +1106,17 @@ module.exports = React.createClass({
},
restoreScrollState: function(scrollState) {
+ if (!this.refs.messagePanel) return;
+
if(scrollState.atBottom) {
// we were at the bottom before. Ideally we'd scroll to the
// 'read-up-to' mark here.
- } else if (scrollState.lastDisplayedEvent) {
- this.scrollToEvent(scrollState.lastDisplayedEvent,
+ } else if (scrollState.lastDisplayedScrollToken) {
+ // we might need to backfill, so we call scrollToEvent rather than
+ // _scrollToToken here. The scrollTokens on our DOM nodes are the
+ // event IDs, so lastDisplayedScrollToken will be the event ID we need,
+ // and we can pass it directly into scrollToEvent.
+ this.scrollToEvent(scrollState.lastDisplayedScrollToken,
scrollState.pixelOffset);
}
},
@@ -1257,7 +1312,7 @@ module.exports = React.createClass({
aux = ;
}
else if (this.state.searching) {
- aux = ;
+ aux = ;
}
var conferenceCallNotification = null;
@@ -1349,7 +1404,7 @@ module.exports = React.createClass({
-
+
-
{this.getEventTiles()}