reimplement typing notif timeline shrinking prevention

instead of setting a min-height on the whole timeline,
track how much height we need to add to prevent shrinking
and set paddingBottom on the container element of the timeline.
pull/21833/head
Bruno Windels 2019-03-20 17:02:53 +01:00
parent 1e372aad47
commit f164a78eaa
4 changed files with 117 additions and 48 deletions

View File

@ -631,12 +631,22 @@ module.exports = React.createClass({
} }
}, },
_onTypingVisible: function() { _onTypingShown: function() {
const scrollPanel = this.refs.scrollPanel; const scrollPanel = this.refs.scrollPanel;
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { // this will make the timeline grow, so checkScroll
// scroll down if at bottom
scrollPanel.checkScroll(); scrollPanel.checkScroll();
scrollPanel.blockShrinking(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
scrollPanel.preventShrinking();
}
},
_onTypingHidden: function() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
// the shrinking prevention once the typing notifs are hidden
scrollPanel.updatePreventShrinking();
} }
}, },
@ -652,15 +662,15 @@ module.exports = React.createClass({
// update the min-height, so once the last // update the min-height, so once the last
// person stops typing, no jumping occurs // person stops typing, no jumping occurs
if (isAtBottom && isTypingVisible) { if (isAtBottom && isTypingVisible) {
scrollPanel.blockShrinking(); scrollPanel.preventShrinking();
} }
} }
}, },
clearTimelineHeight: function() { onTimelineReset: function() {
const scrollPanel = this.refs.scrollPanel; const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.clearBlockShrinking(); scrollPanel.clearPreventShrinking();
} }
}, },
@ -688,7 +698,12 @@ module.exports = React.createClass({
let whoIsTyping; let whoIsTyping;
if (this.props.room) { if (this.props.room) {
whoIsTyping = (<WhoIsTypingTile room={this.props.room} onVisible={this._onTypingVisible} ref="whoIsTyping" />); whoIsTyping = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}
onHidden={this._onTypingHidden}
ref="whoIsTyping" />
);
} }
return ( return (

View File

@ -175,6 +175,7 @@ module.exports = React.createClass({
// //
// This will also re-check the fill state, in case the paginate was inadequate // This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll(); this.checkScroll();
this.updatePreventShrinking();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -192,22 +193,23 @@ module.exports = React.createClass({
onScroll: function(ev) { onScroll: function(ev) {
this._scrollTimeout.restart(); this._scrollTimeout.restart();
this._saveScrollState(); this._saveScrollState();
this._checkBlockShrinking();
this.checkFillState(); this.checkFillState();
this.updatePreventShrinking();
this.props.onScroll(ev); this.props.onScroll(ev);
}, },
onResize: function() { onResize: function() {
this.clearBlockShrinking();
this.checkScroll(); this.checkScroll();
// update preventShrinkingState if present
if (this.preventShrinkingState) {
this.preventShrinking();
}
}, },
// after an update to the contents of the panel, check that the scroll is // after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary. // where it ought to be, and set off pagination requests if necessary.
checkScroll: function() { checkScroll: function() {
this._restoreSavedScrollState(); this._restoreSavedScrollState();
this._checkBlockShrinking();
this.checkFillState(); this.checkFillState();
}, },
@ -718,39 +720,84 @@ module.exports = React.createClass({
}, },
/** /**
* Set the current height as the min height for the message list Mark the bottom offset of the last tile so we can balance it out when
* so the timeline cannot shrink. This is used to avoid anything below it changes, by calling updatePreventShrinking, to keep
* jumping when the typing indicator gets replaced by a smaller message. the same minimum bottom offset, effectively preventing the timeline to shrink.
*/ */
blockShrinking: function() { preventShrinking: function() {
const messageList = this.refs.itemlist; const messageList = this.refs.itemlist;
if (messageList) { const tiles = messageList && messageList.children;
const currentHeight = messageList.clientHeight; if (!messageList) {
messageList.style.minHeight = `${currentHeight}px`; return;
} }
let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) {
const node = tiles[i];
if (node.dataset.scrollTokens) {
lastTileNode = node;
break;
}
}
if (!lastTileNode) {
return;
}
this.clearPreventShrinking();
const offsetFromBottom = messageList.clientHeight - (lastTileNode.offsetTop + lastTileNode.clientHeight);
this.preventShrinkingState = {
offsetFromBottom: offsetFromBottom,
offsetNode: lastTileNode,
};
debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom");
},
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking: function() {
const messageList = this.refs.itemlist;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
debuglog("prevent shrinking cleared");
}, },
/** /**
* Clear the previously set min height update the container padding to balance
the bottom offset of the last tile since
preventShrinking was called.
Clears the prevent-shrinking state ones the offset
from the bottom of the marked tile grows larger than
what it was when marking.
*/ */
clearBlockShrinking: function() { updatePreventShrinking: function() {
const messageList = this.refs.itemlist; if (this.preventShrinkingState) {
if (messageList) {
messageList.style.minHeight = null;
}
},
_checkBlockShrinking: function() {
const sn = this._getScrollNode(); const sn = this._getScrollNode();
const scrollState = this.scrollState; const scrollState = this.scrollState;
if (!scrollState.stuckAtBottom) { const messageList = this.refs.itemlist;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
// if the offsetNode got unmounted, clear
let shouldClear = !offsetNode.parentElement;
// also if 200px from bottom
if (!shouldClear && !scrollState.stuckAtBottom) {
const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight); const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
// only if we've scrolled up 200px from the bottom shouldClear = spaceBelowViewport >= 200;
// should we clear the min-height used by the typing notifications, }
// otherwise we might still see it jump as the whitespace disappears // try updating if not clearing
// when scrolling up from the bottom if (!shouldClear) {
if (spaceBelowViewport >= 200) { const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight);
this.clearBlockShrinking(); const offsetDiff = offsetFromBottom - currentOffset;
if (offsetDiff > 0) {
balanceElement.style.paddingBottom = `${offsetDiff}px`;
if (this.scrollState.stuckAtBottom) {
sn.scrollTop = sn.scrollHeight;
}
debuglog("update prevent shrinking ", offsetDiff, "px from bottom");
} else if (offsetDiff < 0) {
shouldClear = true;
}
}
if (shouldClear) {
this.clearPreventShrinking();
} }
} }
}, },

View File

@ -939,7 +939,7 @@ var TimelinePanel = React.createClass({
// clear the timeline min-height when // clear the timeline min-height when
// (re)loading the timeline // (re)loading the timeline
if (this.refs.messagePanel) { if (this.refs.messagePanel) {
this.refs.messagePanel.clearTimelineHeight(); this.refs.messagePanel.onTimelineReset();
} }
this._reloadEvents(); this._reloadEvents();

View File

@ -29,7 +29,8 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// the room this statusbar is representing. // the room this statusbar is representing.
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
onVisible: PropTypes.func, onShown: PropTypes.func,
onHidden: PropTypes.func,
// Number of names to display in typing indication. E.g. set to 3, will // Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing." // result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number, whoIsTypingLimit: PropTypes.number,
@ -59,11 +60,13 @@ module.exports = React.createClass({
}, },
componentDidUpdate: function(_, prevState) { componentDidUpdate: function(_, prevState) {
if (this.props.onVisible && const wasVisible = this._isVisible(prevState);
!prevState.usersTyping.length && const isVisible = this._isVisible(this.state);
this.state.usersTyping.length if (this.props.onShown && !wasVisible && isVisible) {
) { this.props.onShown();
this.props.onVisible(); }
else if (this.props.onHidden && wasVisible && !isVisible) {
this.props.onHidden();
} }
}, },
@ -77,8 +80,12 @@ module.exports = React.createClass({
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
}, },
_isVisible: function(state) {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
},
isVisible: function() { isVisible: function() {
return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers).length !== 0; return this._isVisible(this.state);
}, },
onRoomTimeline: function(event, room) { onRoomTimeline: function(event, room) {