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.
This commit is contained in:
parent
1e372aad47
commit
f164a78eaa
4 changed files with 117 additions and 48 deletions
|
@ -631,12 +631,22 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onTypingVisible: function() {
|
_onTypingShown: function() {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this.refs.scrollPanel;
|
||||||
|
// this will make the timeline grow, so checkScroll
|
||||||
|
scrollPanel.checkScroll();
|
||||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||||
// scroll down if at bottom
|
scrollPanel.preventShrinking();
|
||||||
scrollPanel.checkScroll();
|
}
|
||||||
scrollPanel.blockShrinking();
|
},
|
||||||
|
|
||||||
|
_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 (
|
||||||
|
|
|
@ -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
|
||||||
clearBlockShrinking: function() {
|
preventShrinking was called.
|
||||||
const messageList = this.refs.itemlist;
|
Clears the prevent-shrinking state ones the offset
|
||||||
if (messageList) {
|
from the bottom of the marked tile grows larger than
|
||||||
messageList.style.minHeight = null;
|
what it was when marking.
|
||||||
}
|
*/
|
||||||
},
|
updatePreventShrinking: function() {
|
||||||
|
if (this.preventShrinkingState) {
|
||||||
_checkBlockShrinking: function() {
|
const sn = this._getScrollNode();
|
||||||
const sn = this._getScrollNode();
|
const scrollState = this.scrollState;
|
||||||
const scrollState = this.scrollState;
|
const messageList = this.refs.itemlist;
|
||||||
if (!scrollState.stuckAtBottom) {
|
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||||
const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
|
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||||
// only if we've scrolled up 200px from the bottom
|
const balanceElement = messageList.parentElement;
|
||||||
// should we clear the min-height used by the typing notifications,
|
// if the offsetNode got unmounted, clear
|
||||||
// otherwise we might still see it jump as the whitespace disappears
|
let shouldClear = !offsetNode.parentElement;
|
||||||
// when scrolling up from the bottom
|
// also if 200px from bottom
|
||||||
if (spaceBelowViewport >= 200) {
|
if (!shouldClear && !scrollState.stuckAtBottom) {
|
||||||
this.clearBlockShrinking();
|
const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
|
||||||
|
shouldClear = spaceBelowViewport >= 200;
|
||||||
|
}
|
||||||
|
// try updating if not clearing
|
||||||
|
if (!shouldClear) {
|
||||||
|
const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight);
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue