Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Improved scrolling & pagination #2676

Merged
merged 13 commits into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/components/structures/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -635,9 +635,9 @@ module.exports = React.createClass({
_onTypingVisible: function() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
scrollPanel.blockShrinking();
// scroll down if at bottom
scrollPanel.checkScroll();
scrollPanel.blockShrinking();
}
},

Expand All @@ -648,12 +648,23 @@ module.exports = React.createClass({
const isAtBottom = scrollPanel.isAtBottom();
const whoIsTyping = this.refs.whoIsTyping;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
// update the min-height, so once the last
// person stops typing, no jumping occurs
if (isAtBottom && isTypingVisible) {
scrollPanel.blockShrinking();
}
}
},

clearTimelineHeight: function() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
scrollPanel.clearBlockShrinking();
}
},

onResize: function() {
dis.dispatch({ action: 'timeline_resize' }, true);
},
Expand Down
111 changes: 55 additions & 56 deletions src/components/structures/ScrollPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ module.exports = React.createClass({

componentDidMount: function() {
this.checkScroll();

if (typeof ResizeObserver !== "undefined") {
this._timelineSizeObserver = new ResizeObserver(() => {
this._restoreSavedScrollState();
});
this._timelineSizeObserver.observe(this.refs.itemlist);
}
},

componentDidUpdate: function() {
Expand All @@ -169,10 +176,6 @@ module.exports = React.createClass({
//
// This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll();

if (!this.isAtBottom()) {
this.clearBlockShrinking();
}
},

componentWillUnmount: function() {
Expand All @@ -181,62 +184,39 @@ module.exports = React.createClass({
//
// (We could use isMounted(), but facebook have deprecated that.)
this.unmounted = true;
if (this._timelineSizeObserver) {
this._timelineSizeObserver.disconnect();
this._timelineSizeObserver = null;
}
},

onScroll: function(ev) {
const sn = this._getScrollNode();
debuglog("Scroll event: offset now:", sn.scrollTop,
"_lastSetScroll:", this._lastSetScroll);

// Sometimes we see attempts to write to scrollTop essentially being
// ignored. (Or rather, it is successfully written, but on the next
// scroll event, it's been reset again).
//
// This was observed on Chrome 47, when scrolling using the trackpad in OS
// X Yosemite. Can't reproduce on El Capitan. Our theory is that this is
// due to Chrome not being able to cope with the scroll offset being reset
// while a two-finger drag is in progress.
//
// By way of a workaround, we detect this situation and just keep
// resetting scrollTop until we see the scroll node have the right
// value.
if (this._lastSetScroll !== undefined && sn.scrollTop < this._lastSetScroll-200) {
console.log("Working around vector-im/vector-web#528");
this._restoreSavedScrollState();
return;
}

// If there weren't enough children to fill the viewport, the scroll we
// got might be different to the scroll we wanted; we don't want to
// forget what we wanted, so don't overwrite the saved state unless
// this appears to be a user-initiated scroll.
if (sn.scrollTop != this._lastSetScroll) {
// when scrolling, we don't care about disappearing typing notifs shrinking the timeline
// this might cause the scrollbar to resize in case the max-height was not correct
// but that's better than ending up with a lot of whitespace at the bottom of the timeline.
// we need to above check because when showing the typing notifs, an onScroll event is also triggered
if (!this.isAtBottom()) {
this.clearBlockShrinking();
}

this._saveScrollState();
} else {
debuglog("Ignoring scroll echo");

// only ignore the echo once, otherwise we'll get confused when the
// user scrolls away from, and back to, the autoscroll point.
this._lastSetScroll = undefined;
}

this._checkBlockShrinking();

this.props.onScroll(ev);

this.checkFillState();
},

onResize: function() {
this.props.onResize();
// clear min-height as the height might have changed
this.clearBlockShrinking();
this.checkScroll();
if (this._gemScroll) this._gemScroll.forceUpdate();
},
Expand All @@ -245,6 +225,7 @@ module.exports = React.createClass({
// where it ought to be, and set off pagination requests if necessary.
checkScroll: function() {
this._restoreSavedScrollState();
this._checkBlockShrinking();
this.checkFillState();
},

Expand Down Expand Up @@ -386,8 +367,6 @@ module.exports = React.createClass({
}
this._unfillDebouncer = setTimeout(() => {
this._unfillDebouncer = null;
// if timeline shrinks, min-height should be cleared
this.clearBlockShrinking();
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
}
Expand Down Expand Up @@ -583,9 +562,10 @@ module.exports = React.createClass({
}

const scrollNode = this._getScrollNode();
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const boundingRect = node.getBoundingClientRect();
const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
const scrollBottom = scrollNode.scrollTop + scrollNode.clientHeight;

const nodeBottom = node.offsetTop + node.clientHeight;
const scrollDelta = nodeBottom + pixelOffset - scrollBottom;

debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
Expand All @@ -602,42 +582,46 @@ module.exports = React.createClass({
return;
}

const scrollNode = this._getScrollNode();
const scrollBottom = scrollNode.scrollTop + scrollNode.clientHeight;

const itemlist = this.refs.itemlist;
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const messages = itemlist.children;
let newScrollState = null;
let node = null;
let nodeBottom;

for (let i = messages.length-1; i >= 0; --i) {
const node = messages[i];
if (!node.dataset.scrollTokens) continue;

const boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
};
if (!messages[i].dataset.scrollTokens) {
continue;
}
node = messages[i];

nodeBottom = node.offsetTop + node.clientHeight;
// If the bottom of the panel intersects the ClientRect of node, use this node
// as the scrollToken.
// If this is false for the entire for-loop, we default to the last node
// (which is why newScrollState is set on every iteration).
if (boundingRect.top < wrapperRect.bottom) {
if (nodeBottom >= scrollBottom) {
// Use this node as the scrollToken
break;
}
}
// This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) {
this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState);
} else {

if (!node) {
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
return;
}

debuglog("ScrollPanel: saved scroll state", this.scrollState);
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: scrollBottom - nodeBottom,
};
},

_restoreSavedScrollState: function() {
const scrollState = this.scrollState;
const scrollNode = this._getScrollNode();

if (scrollState.stuckAtBottom) {
this._setScrollTop(Number.MAX_VALUE);
Expand Down Expand Up @@ -717,6 +701,21 @@ module.exports = React.createClass({
}
},

_checkBlockShrinking: function() {
const sn = this._getScrollNode();
const scrollState = this.scrollState;
if (!scrollState.stuckAtBottom) {
const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
// only if we've scrolled up 200px from the bottom
// should we clear the min-height used by the typing notifications,
// otherwise we might still see it jump as the whitespace disappears
// when scrolling up from the bottom
if (spaceBelowViewport >= 200) {
this.clearBlockShrinking();
}
}
},

render: function() {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
// TODO: the classnames on the div and ol could do with being updated to
Expand Down
5 changes: 5 additions & 0 deletions src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@ var TimelinePanel = React.createClass({
{windowLimit: this.props.timelineCap});

const onLoaded = () => {
// clear the timeline min-height when
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such a low line length for comments! 😉

// (re)loading the timeline
if (this.refs.messagePanel) {
this.refs.messagePanel.clearTimelineHeight();
}
this._reloadEvents();

// If we switched away from the room while there were pending
Expand Down