mirror of https://github.com/vector-im/riot-web
Merge pull request #49 from matrix-org/rav/preserve_room_scroll
Preserve scroll offset when switching roomspull/21833/head
commit
352865ee9d
|
@ -81,6 +81,9 @@ module.exports = React.createClass({
|
|||
this.startMatrixClient();
|
||||
}
|
||||
this.focusComposer = false;
|
||||
// scrollStateMap is a map from room id to the scroll state returned by
|
||||
// RoomView.getScrollState()
|
||||
this.scrollStateMap = {};
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
window.addEventListener("focus", this.onFocus);
|
||||
if (this.state.logged_in) {
|
||||
|
@ -233,27 +236,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'view_room':
|
||||
this.focusComposer = true;
|
||||
var newState = {
|
||||
currentRoom: payload.room_id,
|
||||
page_type: this.PageTypes.RoomView,
|
||||
};
|
||||
if (this.sdkReady) {
|
||||
// if the SDK is not ready yet, remember what room
|
||||
// we're supposed to be on but don't notify about
|
||||
// the new screen yet (we won't be showing it yet)
|
||||
// The normal case where this happens is navigating
|
||||
// to the room in the URL bar on page load.
|
||||
var presentedId = payload.room_id;
|
||||
var room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
this._viewRoom(payload.room_id);
|
||||
break;
|
||||
case 'view_prev_room':
|
||||
roomIndexDelta = -1;
|
||||
|
@ -270,11 +253,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||
this.focusComposer = true;
|
||||
this.setState({
|
||||
currentRoom: allRooms[roomIndex].roomId
|
||||
});
|
||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
||||
this._viewRoom(allRooms[roomIndex].roomId);
|
||||
break;
|
||||
case 'view_indexed_room':
|
||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||
|
@ -282,11 +261,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
var roomIndex = payload.roomIndex;
|
||||
if (allRooms[roomIndex]) {
|
||||
this.focusComposer = true;
|
||||
this.setState({
|
||||
currentRoom: allRooms[roomIndex].roomId
|
||||
});
|
||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
||||
this._viewRoom(allRooms[roomIndex].roomId);
|
||||
}
|
||||
break;
|
||||
case 'view_room_alias':
|
||||
|
@ -310,21 +285,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'view_user_settings':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.UserSettings,
|
||||
});
|
||||
this._setPage(this.PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.CreateRoom,
|
||||
});
|
||||
this._setPage(this.PageTypes.CreateRoom);
|
||||
this.notifyNewScreen('new');
|
||||
break;
|
||||
case 'view_room_directory':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.RoomDirectory,
|
||||
});
|
||||
this._setPage(this.PageTypes.RoomDirectory);
|
||||
this.notifyNewScreen('directory');
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
|
@ -353,6 +322,58 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_setPage: function(pageType) {
|
||||
// record the scroll state if we're in a room view.
|
||||
this._updateScrollMap();
|
||||
|
||||
this.setState({
|
||||
page_type: this.PageTypes.RoomDirectory,
|
||||
});
|
||||
},
|
||||
|
||||
_viewRoom: function(roomId) {
|
||||
// before we switch room, record the scroll state of the current room
|
||||
this._updateScrollMap();
|
||||
|
||||
this.focusComposer = true;
|
||||
var newState = {
|
||||
currentRoom: roomId,
|
||||
page_type: this.PageTypes.RoomView,
|
||||
};
|
||||
if (this.sdkReady) {
|
||||
// if the SDK is not ready yet, remember what room
|
||||
// we're supposed to be on but don't notify about
|
||||
// the new screen yet (we won't be showing it yet)
|
||||
// The normal case where this happens is navigating
|
||||
// to the room in the URL bar on page load.
|
||||
var presentedId = roomId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
if (this.scrollStateMap[roomId]) {
|
||||
var scrollState = this.scrollStateMap[roomId];
|
||||
this.refs.roomView.restoreScrollState(scrollState);
|
||||
}
|
||||
},
|
||||
|
||||
// update scrollStateMap according to the current scroll state of the
|
||||
// room view.
|
||||
_updateScrollMap: function() {
|
||||
if (!this.refs.roomView) {
|
||||
return;
|
||||
}
|
||||
|
||||
var roomview = this.refs.roomView;
|
||||
var state = roomview.getScrollState();
|
||||
this.scrollStateMap[roomview.props.roomId] = state;
|
||||
},
|
||||
|
||||
onLoggedIn: function(credentials) {
|
||||
console.log("onLoggedIn => %s", credentials.userId);
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
|
@ -621,6 +642,7 @@ module.exports = React.createClass({
|
|||
case this.PageTypes.RoomView:
|
||||
page_element = (
|
||||
<RoomView
|
||||
ref="roomView"
|
||||
roomId={this.state.currentRoom}
|
||||
key={this.state.currentRoom}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
|
|
|
@ -70,7 +70,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
this.atBottom = true;
|
||||
this.savedScrollState = {atBottom: true};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -175,7 +175,7 @@ module.exports = React.createClass({
|
|||
if (!toStartOfTimeline &&
|
||||
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
||||
// update unread count when scrolled up
|
||||
if (this.atBottom) {
|
||||
if (this.savedScrollState.atBottom) {
|
||||
currentUnread = 0;
|
||||
}
|
||||
else {
|
||||
|
@ -183,15 +183,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
||||
numUnreadMessages: currentUnread
|
||||
});
|
||||
|
||||
if (toStartOfTimeline && !this.state.paginating) {
|
||||
this.fillSpace();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
|
@ -285,55 +280,58 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
// 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.refs.messagePanel) return;
|
||||
|
||||
var messageWrapperScroll = this._getScrollNode();
|
||||
|
||||
if (this.state.paginating && !this.waiting_for_paginate) {
|
||||
var heightGained = messageWrapperScroll.scrollHeight - this.oldScrollHeight;
|
||||
messageWrapperScroll.scrollTop += heightGained;
|
||||
this.oldScrollHeight = undefined;
|
||||
if (!this.fillSpace()) {
|
||||
this.setState({paginating: false});
|
||||
}
|
||||
} else if (this.atBottom) {
|
||||
messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight;
|
||||
if (this.state.numUnreadMessages !== 0) {
|
||||
this.setState({numUnreadMessages: 0});
|
||||
}
|
||||
if (this.state.searchResults) return;
|
||||
var scrollState = this.savedScrollState;
|
||||
if (scrollState.atBottom) {
|
||||
this.scrollToBottom();
|
||||
} else if (scrollState.lastDisplayedEvent) {
|
||||
this.scrollToEvent(scrollState.lastDisplayedEvent,
|
||||
scrollState.pixelOffset);
|
||||
}
|
||||
},
|
||||
|
||||
_paginateCompleted: function() {
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
||||
});
|
||||
|
||||
// we might not have got enough results from the pagination
|
||||
// request, so give fillSpace() a chance to set off another.
|
||||
if (!this.fillSpace()) {
|
||||
this.setState({paginating: false});
|
||||
}
|
||||
},
|
||||
|
||||
// check the scroll position, and if we need to, set off a pagination
|
||||
// request.
|
||||
//
|
||||
// returns true if a pagination request was started (or is still in progress)
|
||||
fillSpace: function() {
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (this.state.searchResults) return; // TODO: paginate search results
|
||||
var messageWrapperScroll = this._getScrollNode();
|
||||
if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) {
|
||||
this.setState({paginating: true});
|
||||
// there's less than a screenful of messages left. Either wind back
|
||||
// the message cap (if there are enough events in the timeline to
|
||||
// do so), or fire off a pagination request.
|
||||
|
||||
this.oldScrollHeight = messageWrapperScroll.scrollHeight;
|
||||
|
||||
if (this.state.messageCap < this.state.room.timeline.length) {
|
||||
this.waiting_for_paginate = false;
|
||||
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
||||
this.setState({messageCap: cap, paginating: true});
|
||||
this.setState({messageCap: cap});
|
||||
} else {
|
||||
this.waiting_for_paginate = true;
|
||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
||||
this.setState({messageCap: cap, paginating: true});
|
||||
var self = this;
|
||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(function() {
|
||||
self.waiting_for_paginate = false;
|
||||
if (self.isMounted()) {
|
||||
self.setState({
|
||||
room: MatrixClientPeg.get().getRoom(self.props.roomId)
|
||||
});
|
||||
}
|
||||
// wait and set paginating to false when the component updates
|
||||
});
|
||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
@ -364,13 +362,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onMessageListScroll: function(ev) {
|
||||
if (this.refs.messagePanel) {
|
||||
var messageWrapperScroll = this._getScrollNode();
|
||||
var wasAtBottom = this.atBottom;
|
||||
// + 1 here to avoid fractional pixel rounding errors
|
||||
this.atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
|
||||
if (this.atBottom && !wasAtBottom) {
|
||||
this.forceUpdate(); // remove unread msg count
|
||||
if (this.refs.messagePanel && !this.state.searchResults) {
|
||||
this.savedScrollState = this._calculateScrollState();
|
||||
if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
|
||||
this.setState({numUnreadMessages: 0});
|
||||
}
|
||||
}
|
||||
if (!this.state.paginating) this.fillSpace();
|
||||
|
@ -845,6 +840,111 @@ module.exports = React.createClass({
|
|||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||
},
|
||||
|
||||
// scroll the event view to put the given event at the bottom.
|
||||
//
|
||||
// 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
|
||||
// it's fallen out of scrollback. We ought to backfill until we
|
||||
// find it, but we'd have to be careful we didn't backfill forever
|
||||
// looking for a non-existent event.
|
||||
//
|
||||
// for now, just scroll to the top of the buffer.
|
||||
console.log("Refusing to scroll to unknown event "+eventId);
|
||||
scrollNode.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// we might need to roll back the messagecap (to generate tiles for
|
||||
// older messages). This just means telling getEventTiles to create
|
||||
// tiles for events we already have in our timeline (we already know
|
||||
// the event in question is in our timeline, so we shouldn't need to
|
||||
// backfill).
|
||||
//
|
||||
// we actually wind back slightly further than the event in question,
|
||||
// because we want the event to be at the *bottom* of the container.
|
||||
// Don't roll it back past the timeline we have, though.
|
||||
var minCap = this.state.room.timeline.length - Math.min(idx - INITIAL_SIZE, 0);
|
||||
if (minCap > this.state.messageCap) {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
scrollNode.scrollTop += boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
},
|
||||
|
||||
_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() {
|
||||
return this.savedScrollState;
|
||||
},
|
||||
|
||||
restoreScrollState: function(scrollState) {
|
||||
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,
|
||||
scrollState.pixelOffset);
|
||||
}
|
||||
},
|
||||
|
||||
onResize: function(e) {
|
||||
// It seems flexbox doesn't give us a way to constrain the auxPanel height to have
|
||||
// a minimum of the height of the video element, whilst also capping it from pushing out the page
|
||||
|
|
Loading…
Reference in New Issue