Merge pull request #2766 from matrix-org/bwindels/scrolling

Scroll investigation changes
This commit is contained in:
Bruno Windels 2019-03-11 09:57:13 +00:00 committed by GitHub
commit 99f82a3de9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 60 additions and 72 deletions

View file

@ -387,7 +387,7 @@ module.exports = React.createClass({
ret.push(<MemberEventListSummary key={key} ret.push(<MemberEventListSummary key={key}
events={summarisedEvents} events={summarisedEvents}
onToggle={this._onWidgetLoad} // Update scroll state onToggle={this._onHeightChanged} // Update scroll state
startExpanded={highlightInMels} startExpanded={highlightInMels}
> >
{ eventTiles } { eventTiles }
@ -517,7 +517,7 @@ module.exports = React.createClass({
data-scroll-tokens={scrollToken}> data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
onWidgetLoad={this._onWidgetLoad} onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts} readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap} readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
@ -625,7 +625,7 @@ module.exports = React.createClass({
// once dynamic content in the events load, make the scrollPanel check the // once dynamic content in the events load, make the scrollPanel check the
// scroll offsets. // scroll offsets.
_onWidgetLoad: function() { _onHeightChanged: function() {
const scrollPanel = this.refs.scrollPanel; const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.forceUpdate(); scrollPanel.forceUpdate();

View file

@ -1186,7 +1186,7 @@ module.exports = React.createClass({
// once dynamic content in the search results load, make the scrollPanel check // once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets. // the scroll offsets.
const onWidgetLoad = () => { const onHeightChanged = () => {
const scrollPanel = this.refs.searchResultsPanel; const scrollPanel = this.refs.searchResultsPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.checkScroll(); scrollPanel.checkScroll();
@ -1231,7 +1231,7 @@ module.exports = React.createClass({
searchHighlights={this.state.searchHighlights} searchHighlights={this.state.searchHighlights}
resultLink={resultLink} resultLink={resultLink}
permalinkCreator={this.state.permalinkCreator} permalinkCreator={this.state.permalinkCreator}
onWidgetLoad={onWidgetLoad} />); onHeightChanged={onHeightChanged} />);
} }
return ret; return ret;
}, },

View file

@ -79,26 +79,6 @@ if (DEBUG_SCROLL) {
* offset as normal. * offset as normal.
*/ */
function createTimelineResizeDetector(scrollNode, itemlist, callback) {
if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver(callback);
ro.observe(itemlist);
return ro;
} else if (typeof IntersectionObserver !== "undefined") {
const threshold = [];
for (let i = 0; i <= 1000; ++i) {
threshold.push(i / 1000);
}
const io = new IntersectionObserver(
callback,
{root: scrollNode, threshold},
);
io.observe(itemlist);
return io;
}
}
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ScrollPanel', displayName: 'ScrollPanel',
@ -181,12 +161,6 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this.checkScroll(); this.checkScroll();
this._timelineSizeObserver = createTimelineResizeDetector(
this._getScrollNode(),
this.refs.itemlist,
() => { this._restoreSavedScrollState(); },
);
}, },
componentDidUpdate: function() { componentDidUpdate: function() {
@ -204,10 +178,6 @@ module.exports = React.createClass({
// //
// (We could use isMounted(), but facebook have deprecated that.) // (We could use isMounted(), but facebook have deprecated that.)
this.unmounted = true; this.unmounted = true;
if (this._timelineSizeObserver) {
this._timelineSizeObserver.disconnect();
this._timelineSizeObserver = null;
}
}, },
onScroll: function(ev) { onScroll: function(ev) {
@ -601,16 +571,17 @@ module.exports = React.createClass({
} }
const scrollNode = this._getScrollNode(); const scrollNode = this._getScrollNode();
const scrollBottom = scrollNode.scrollTop + scrollNode.clientHeight; const scrollTop = scrollNode.scrollTop;
const viewportBottom = scrollTop + scrollNode.clientHeight;
const nodeBottom = node.offsetTop + node.clientHeight; const nodeBottom = node.offsetTop + node.clientHeight;
const scrollDelta = nodeBottom + pixelOffset - scrollBottom; const intendedViewportBottom = nodeBottom + pixelOffset;
const scrollDelta = intendedViewportBottom - viewportBottom;
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
if (scrollDelta != 0) { if (scrollDelta !== 0) {
this._setScrollTop(scrollNode.scrollTop + scrollDelta); this._setScrollTop(scrollTop + scrollDelta);
} }
}, },
@ -622,7 +593,7 @@ module.exports = React.createClass({
} }
const scrollNode = this._getScrollNode(); const scrollNode = this._getScrollNode();
const scrollBottom = scrollNode.scrollTop + scrollNode.clientHeight; const viewportBottom = scrollNode.scrollTop + scrollNode.clientHeight;
const itemlist = this.refs.itemlist; const itemlist = this.refs.itemlist;
const messages = itemlist.children; const messages = itemlist.children;
@ -636,7 +607,7 @@ module.exports = React.createClass({
node = messages[i]; node = messages[i];
// break at the first message (coming from the bottom) // break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport. // that has it's offsetTop above the bottom of the viewport.
if (node.offsetTop < scrollBottom) { if (node.offsetTop < viewportBottom) {
// Use this node as the scrollToken // Use this node as the scrollToken
break; break;
} }
@ -652,7 +623,7 @@ module.exports = React.createClass({
this.scrollState = { this.scrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0], trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: scrollBottom - nodeBottom, pixelOffset: viewportBottom - nodeBottom,
}; };
}, },

View file

@ -31,7 +31,7 @@ export default class ReplyThread extends React.Component {
// the latest event in this chain of replies // the latest event in this chain of replies
parentEv: PropTypes.instanceOf(MatrixEvent), parentEv: PropTypes.instanceOf(MatrixEvent),
// called when the ReplyThread contents has changed, including EventTiles thereof // called when the ReplyThread contents has changed, including EventTiles thereof
onWidgetLoad: PropTypes.func.isRequired, onHeightChanged: PropTypes.func.isRequired,
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
}; };
@ -160,11 +160,11 @@ export default class ReplyThread extends React.Component {
}; };
} }
static makeThread(parentEv, onWidgetLoad, permalinkCreator, ref) { static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) {
if (!ReplyThread.getParentEventId(parentEv)) { if (!ReplyThread.getParentEventId(parentEv)) {
return <div />; return <div />;
} }
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} return <ReplyThread parentEv={parentEv} onHeightChanged={onHeightChanged}
ref={ref} permalinkCreator={permalinkCreator} />; ref={ref} permalinkCreator={permalinkCreator} />;
} }
@ -175,7 +175,7 @@ export default class ReplyThread extends React.Component {
} }
componentDidUpdate() { componentDidUpdate() {
this.props.onWidgetLoad(); this.props.onHeightChanged();
} }
componentWillUnmount() { componentWillUnmount() {
@ -295,7 +295,7 @@ export default class ReplyThread extends React.Component {
{ dateSep } { dateSep }
<EventTile mxEvent={ev} <EventTile mxEvent={ev}
tileShape="reply" tileShape="reply"
onWidgetLoad={this.props.onWidgetLoad} onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} /> isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
</blockquote>; </blockquote>;

View file

@ -203,6 +203,17 @@ module.exports = React.createClass({
}; };
}, },
propTypes: {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
/* already decrypted blob */
decryptedBlob: PropTypes.object,
/* called when the download link iframe is shown */
onHeightChanged: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
},
contextTypes: { contextTypes: {
appConfig: PropTypes.object, appConfig: PropTypes.object,
}, },
@ -248,6 +259,12 @@ module.exports = React.createClass({
this.tint(); this.tint();
}, },
componentDidUpdate: function(prevProps, prevState) {
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
this.props.onHeightChanged();
}
},
componentWillUnmount: function() { componentWillUnmount: function() {
// Remove this from the list of mounted components // Remove this from the list of mounted components
delete mounts[this.id]; delete mounts[this.id];

View file

@ -34,7 +34,7 @@ export default class MImageBody extends React.Component {
mxEvent: PropTypes.object.isRequired, mxEvent: PropTypes.object.isRequired,
/* called when the image has loaded */ /* called when the image has loaded */
onWidgetLoad: PropTypes.func.isRequired, onHeightChanged: PropTypes.func.isRequired,
/* the maximum image height to use */ /* the maximum image height to use */
maxImageHeight: PropTypes.number, maxImageHeight: PropTypes.number,
@ -144,7 +144,7 @@ export default class MImageBody extends React.Component {
} }
onImageLoad() { onImageLoad() {
this.props.onWidgetLoad(); this.props.onHeightChanged();
let loadedImageDimensions; let loadedImageDimensions;

View file

@ -33,7 +33,7 @@ module.exports = React.createClass({
mxEvent: PropTypes.object.isRequired, mxEvent: PropTypes.object.isRequired,
/* called when the video has loaded */ /* called when the video has loaded */
onWidgetLoad: PropTypes.func.isRequired, onHeightChanged: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -108,7 +108,7 @@ module.exports = React.createClass({
decryptedThumbnailUrl: thumbnailUrl, decryptedThumbnailUrl: thumbnailUrl,
decryptedBlob: decryptedBlob, decryptedBlob: decryptedBlob,
}); });
this.props.onWidgetLoad(); this.props.onHeightChanged();
}); });
}).catch((err) => { }).catch((err) => {
console.warn("Unable to decrypt attachment: ", err); console.warn("Unable to decrypt attachment: ", err);

View file

@ -37,7 +37,7 @@ module.exports = React.createClass({
showUrlPreview: PropTypes.bool, showUrlPreview: PropTypes.bool,
/* callback called when dynamic content in events are loaded */ /* callback called when dynamic content in events are loaded */
onWidgetLoad: PropTypes.func, onHeightChanged: PropTypes.func,
/* the shape of the tile, used */ /* the shape of the tile, used */
tileShape: PropTypes.string, tileShape: PropTypes.string,
@ -89,6 +89,6 @@ module.exports = React.createClass({
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight} maxImageHeight={this.props.maxImageHeight}
onWidgetLoad={this.props.onWidgetLoad} />; onHeightChanged={this.props.onHeightChanged} />;
}, },
}); });

View file

@ -52,7 +52,7 @@ module.exports = React.createClass({
showUrlPreview: PropTypes.bool, showUrlPreview: PropTypes.bool,
/* callback for when our widget has loaded */ /* callback for when our widget has loaded */
onWidgetLoad: PropTypes.func, onHeightChanged: PropTypes.func,
/* the shape of the tile, used */ /* the shape of the tile, used */
tileShape: PropTypes.string, tileShape: PropTypes.string,
@ -451,7 +451,7 @@ module.exports = React.createClass({
link={link} link={link}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
onCancelClick={this.onCancelClick} onCancelClick={this.onCancelClick}
onWidgetLoad={this.props.onWidgetLoad} />; onHeightChanged={this.props.onHeightChanged} />;
}); });
} }

View file

@ -129,7 +129,7 @@ module.exports = withMatrixClient(React.createClass({
isSelectedEvent: PropTypes.bool, isSelectedEvent: PropTypes.bool,
/* callback called when dynamic content in events are loaded */ /* callback called when dynamic content in events are loaded */
onWidgetLoad: PropTypes.func, onHeightChanged: PropTypes.func,
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */ /* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
readReceipts: PropTypes.arrayOf(React.PropTypes.object), readReceipts: PropTypes.arrayOf(React.PropTypes.object),
@ -165,8 +165,8 @@ module.exports = withMatrixClient(React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
// no-op function because onWidgetLoad is optional yet some sub-components assume its existence // no-op function because onHeightChanged is optional yet some sub-components assume its existence
onWidgetLoad: function() {}, onHeightChanged: function() {},
}; };
}, },
@ -223,7 +223,7 @@ module.exports = withMatrixClient(React.createClass({
*/ */
_onDecrypted: function() { _onDecrypted: function() {
// we need to re-verify the sending device. // we need to re-verify the sending device.
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption // (we call onHeightChanged in _verifyEvent to handle the case where decryption
// has caused a change in size of the event tile) // has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent); this._verifyEvent(this.props.mxEvent);
this.forceUpdate(); this.forceUpdate();
@ -245,7 +245,7 @@ module.exports = withMatrixClient(React.createClass({
verified: verified, verified: verified,
}, () => { }, () => {
// Decryption may have caused a change in size // Decryption may have caused a change in size
this.props.onWidgetLoad(); this.props.onHeightChanged();
}); });
}, },
@ -667,7 +667,7 @@ module.exports = withMatrixClient(React.createClass({
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} /> onHeightChanged={this.props.onHeightChanged} />
</div> </div>
</div> </div>
); );
@ -682,7 +682,7 @@ module.exports = withMatrixClient(React.createClass({
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} /> onHeightChanged={this.props.onHeightChanged} />
</div> </div>
<a <a
className="mx_EventTile_senderDetailsLink" className="mx_EventTile_senderDetailsLink"
@ -704,7 +704,7 @@ module.exports = withMatrixClient(React.createClass({
if (this.props.tileShape === 'reply_preview') { if (this.props.tileShape === 'reply_preview') {
thread = ReplyThread.makeThread( thread = ReplyThread.makeThread(
this.props.mxEvent, this.props.mxEvent,
this.props.onWidgetLoad, this.props.onHeightChanged,
this.props.permalinkCreator, this.props.permalinkCreator,
'replyThread', 'replyThread',
); );
@ -723,7 +723,7 @@ module.exports = withMatrixClient(React.createClass({
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
onWidgetLoad={this.props.onWidgetLoad} onHeightChanged={this.props.onHeightChanged}
showUrlPreview={false} /> showUrlPreview={false} />
</div> </div>
</div> </div>
@ -732,7 +732,7 @@ module.exports = withMatrixClient(React.createClass({
default: { default: {
const thread = ReplyThread.makeThread( const thread = ReplyThread.makeThread(
this.props.mxEvent, this.props.mxEvent,
this.props.onWidgetLoad, this.props.onHeightChanged,
this.props.permalinkCreator, this.props.permalinkCreator,
'replyThread', 'replyThread',
); );
@ -753,7 +753,7 @@ module.exports = withMatrixClient(React.createClass({
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} /> onHeightChanged={this.props.onHeightChanged} />
{ keyRequestInfo } { keyRequestInfo }
{ editButton } { editButton }
</div> </div>

View file

@ -32,7 +32,7 @@ module.exports = React.createClass({
link: PropTypes.string.isRequired, // the URL being previewed link: PropTypes.string.isRequired, // the URL being previewed
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: PropTypes.func, // called when the preview's contents has loaded onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
}, },
getInitialState: function() { getInitialState: function() {
@ -49,7 +49,7 @@ module.exports = React.createClass({
} }
this.setState( this.setState(
{ preview: res }, { preview: res },
this.props.onWidgetLoad, this.props.onHeightChanged,
); );
}, (error)=>{ }, (error)=>{
console.error("Failed to get preview for " + this.props.link + " " + error); console.error("Failed to get preview for " + this.props.link + " " + error);

View file

@ -92,7 +92,7 @@ module.exports = React.createClass({
</span> </span>
<div className="mx_PinnedEventTile_message"> <div className="mx_PinnedEventTile_message">
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150} <MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
onWidgetLoad={() => {}} // we need to give this, apparently onHeightChanged={() => {}} // we need to give this, apparently
/> />
</div> </div>
</div> </div>

View file

@ -33,7 +33,7 @@ module.exports = React.createClass({
// href for the highlights in this result // href for the highlights in this result
resultLink: PropTypes.string, resultLink: PropTypes.string,
onWidgetLoad: PropTypes.func, onHeightChanged: PropTypes.func,
}, },
render: function() { render: function() {
@ -58,7 +58,7 @@ module.exports = React.createClass({
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights} ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink} highlightLink={this.props.resultLink}
onWidgetLoad={this.props.onWidgetLoad} />); onHeightChanged={this.props.onHeightChanged} />);
} }
} }
return ( return (