Preserve scroll offset when switching rooms
When we change rooms, save the scroll offset, and restore the scroll when we switch back. Hopefully this fixes https://github.com/vector-im/vector-web/issues/80.
This commit is contained in:
parent
072130466c
commit
00656fc1dc
2 changed files with 140 additions and 31 deletions
|
@ -80,6 +80,9 @@ module.exports = React.createClass({
|
||||||
this.startMatrixClient();
|
this.startMatrixClient();
|
||||||
}
|
}
|
||||||
this.focusComposer = false;
|
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);
|
document.addEventListener("keydown", this.onKeyDown);
|
||||||
window.addEventListener("focus", this.onFocus);
|
window.addEventListener("focus", this.onFocus);
|
||||||
if (this.state.logged_in) {
|
if (this.state.logged_in) {
|
||||||
|
@ -202,27 +205,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
this.focusComposer = true;
|
this._viewRoom(payload.room_id);
|
||||||
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);
|
|
||||||
break;
|
break;
|
||||||
case 'view_prev_room':
|
case 'view_prev_room':
|
||||||
roomIndexDelta = -1;
|
roomIndexDelta = -1;
|
||||||
|
@ -239,11 +222,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||||
this.focusComposer = true;
|
this._viewRoom(allRooms[roomIndex].roomId);
|
||||||
this.setState({
|
|
||||||
currentRoom: allRooms[roomIndex].roomId
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
|
||||||
break;
|
break;
|
||||||
case 'view_indexed_room':
|
case 'view_indexed_room':
|
||||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||||
|
@ -251,11 +230,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
var roomIndex = payload.roomIndex;
|
var roomIndex = payload.roomIndex;
|
||||||
if (allRooms[roomIndex]) {
|
if (allRooms[roomIndex]) {
|
||||||
this.focusComposer = true;
|
this._viewRoom(allRooms[roomIndex].roomId);
|
||||||
this.setState({
|
|
||||||
currentRoom: allRooms[roomIndex].roomId
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'view_room_alias':
|
case 'view_room_alias':
|
||||||
|
@ -322,6 +297,49 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_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) {
|
onLoggedIn: function(credentials) {
|
||||||
console.log("onLoggedIn => %s", credentials.userId);
|
console.log("onLoggedIn => %s", credentials.userId);
|
||||||
MatrixClientPeg.replaceUsingAccessToken(
|
MatrixClientPeg.replaceUsingAccessToken(
|
||||||
|
@ -590,6 +608,7 @@ module.exports = React.createClass({
|
||||||
case this.PageTypes.RoomView:
|
case this.PageTypes.RoomView:
|
||||||
page_element = (
|
page_element = (
|
||||||
<RoomView
|
<RoomView
|
||||||
|
ref="roomview"
|
||||||
roomId={this.state.currentRoom}
|
roomId={this.state.currentRoom}
|
||||||
key={this.state.currentRoom}
|
key={this.state.currentRoom}
|
||||||
ConferenceHandler={this.props.ConferenceHandler} />
|
ConferenceHandler={this.props.ConferenceHandler} />
|
||||||
|
|
|
@ -803,6 +803,96 @@ module.exports = React.createClass({
|
||||||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
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(event_id, pixel_offset) {
|
||||||
|
var scrollNode = this._getScrollNode();
|
||||||
|
if (!scrollNode) return;
|
||||||
|
|
||||||
|
var messageWrapper = this.refs.messagePanel;
|
||||||
|
if (messageWrapper === undefined) return;
|
||||||
|
|
||||||
|
var idx = this._indexForEventId(event_id);
|
||||||
|
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 "+event_id);
|
||||||
|
scrollNode.scrollTop = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we might need to roll back the messagecap (to generate tiles for
|
||||||
|
// older messages). 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[event_id];
|
||||||
|
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 + pixel_offset - wrapperRect.bottom;
|
||||||
|
},
|
||||||
|
|
||||||
|
// get the current scroll position of the room, so that it can be
|
||||||
|
// restored when we switch back to it
|
||||||
|
getScrollState: 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();
|
||||||
|
|
||||||
|
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: this.atBottom,
|
||||||
|
lastDisplayedEvent: ev.getId(),
|
||||||
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apparently the entire timeline is below the viewport. Give up.
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
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.lastDisplayed) {
|
||||||
|
this.scrollToEvent(scrollState.lastDisplayedEvent,
|
||||||
|
scrollState.pixelOffset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
|
|
Loading…
Reference in a new issue