diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index fecf5f1ad7..805749b1cd 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -631,12 +631,22 @@ module.exports = React.createClass({
}
},
- _onTypingVisible: function() {
+ _onTypingShown: function() {
const scrollPanel = this.refs.scrollPanel;
+ // this will make the timeline grow, so checkScroll
+ scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
- // scroll down if at bottom
- scrollPanel.checkScroll();
- scrollPanel.blockShrinking();
+ 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
// person stops typing, no jumping occurs
if (isAtBottom && isTypingVisible) {
- scrollPanel.blockShrinking();
+ scrollPanel.preventShrinking();
}
}
},
- clearTimelineHeight: function() {
+ onTimelineReset: function() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
- scrollPanel.clearBlockShrinking();
+ scrollPanel.clearPreventShrinking();
}
},
@@ -688,7 +698,12 @@ module.exports = React.createClass({
let whoIsTyping;
if (this.props.room) {
- whoIsTyping = ();
+ whoIsTyping = (
+ );
}
return (
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 36cf4dc018..b1add8235f 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -175,6 +175,7 @@ module.exports = React.createClass({
//
// This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll();
+ this.updatePreventShrinking();
},
componentWillUnmount: function() {
@@ -192,22 +193,23 @@ module.exports = React.createClass({
onScroll: function(ev) {
this._scrollTimeout.restart();
this._saveScrollState();
- this._checkBlockShrinking();
this.checkFillState();
-
+ this.updatePreventShrinking();
this.props.onScroll(ev);
},
onResize: function() {
- this.clearBlockShrinking();
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
// where it ought to be, and set off pagination requests if necessary.
checkScroll: function() {
this._restoreSavedScrollState();
- this._checkBlockShrinking();
this.checkFillState();
},
@@ -718,39 +720,84 @@ module.exports = React.createClass({
},
/**
- * Set the current height as the min height for the message list
- * so the timeline cannot shrink. This is used to avoid
- * jumping when the typing indicator gets replaced by a smaller message.
- */
- blockShrinking: function() {
+ Mark the bottom offset of the last tile so we can balance it out when
+ anything below it changes, by calling updatePreventShrinking, to keep
+ the same minimum bottom offset, effectively preventing the timeline to shrink.
+ */
+ preventShrinking: function() {
const messageList = this.refs.itemlist;
- if (messageList) {
- const currentHeight = messageList.clientHeight;
- messageList.style.minHeight = `${currentHeight}px`;
+ const tiles = messageList && messageList.children;
+ if (!messageList) {
+ 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
- */
- clearBlockShrinking: function() {
- const messageList = this.refs.itemlist;
- if (messageList) {
- messageList.style.minHeight = null;
- }
- },
-
- _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();
+ 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.
+ */
+ updatePreventShrinking: function() {
+ if (this.preventShrinkingState) {
+ const sn = this._getScrollNode();
+ const scrollState = this.scrollState;
+ 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);
+ 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();
}
}
},
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index c983f904a0..aba7964a15 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -939,7 +939,7 @@ var TimelinePanel = React.createClass({
// clear the timeline min-height when
// (re)loading the timeline
if (this.refs.messagePanel) {
- this.refs.messagePanel.clearTimelineHeight();
+ this.refs.messagePanel.onTimelineReset();
}
this._reloadEvents();
diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js
index 9dd690f6e5..95cf0717c7 100644
--- a/src/components/views/rooms/WhoIsTypingTile.js
+++ b/src/components/views/rooms/WhoIsTypingTile.js
@@ -29,7 +29,8 @@ module.exports = React.createClass({
propTypes: {
// the room this statusbar is representing.
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
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number,
@@ -59,11 +60,13 @@ module.exports = React.createClass({
},
componentDidUpdate: function(_, prevState) {
- if (this.props.onVisible &&
- !prevState.usersTyping.length &&
- this.state.usersTyping.length
- ) {
- this.props.onVisible();
+ const wasVisible = this._isVisible(prevState);
+ const isVisible = this._isVisible(this.state);
+ if (this.props.onShown && !wasVisible && isVisible) {
+ this.props.onShown();
+ }
+ 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());
},
+ _isVisible: function(state) {
+ return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
+ },
+
isVisible: function() {
- return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers).length !== 0;
+ return this._isVisible(this.state);
},
onRoomTimeline: function(event, room) {