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
parent
1e372aad47
commit
f164a78eaa
|
@ -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 (
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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…
Reference in New Issue