diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index a9f9c1b6b..d35642ede 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -32,16 +32,16 @@ class Status extends ImmutablePureComponent {
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
- onRef: PropTypes.func,
- isIntersecting: PropTypes.bool,
me: PropTypes.number,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
+ intersectionObserverWrapper: PropTypes.object,
};
state = {
- isHidden: false,
+ isIntersecting: true, // assume intersecting until told otherwise
+ isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
// Avoid checking props that are functions (and whose equality will always
@@ -59,12 +59,12 @@ class Status extends ImmutablePureComponent {
updateOnStates = []
shouldComponentUpdate (nextProps, nextState) {
- if (nextProps.isIntersecting === false && nextState.isHidden) {
+ if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
- return this.props.isIntersecting !== false || !this.state.isHidden;
- } else if (nextProps.isIntersecting !== false && this.props.isIntersecting === false) {
+ return this.state.isIntersecting || !this.state.isHidden;
+ } else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
@@ -73,21 +73,47 @@ class Status extends ImmutablePureComponent {
return super.shouldComponentUpdate(nextProps, nextState);
}
- componentWillReceiveProps (nextProps) {
- if (nextProps.isIntersecting === false && this.props.isIntersecting !== false) {
- requestIdleCallback(() => this.setState({ isHidden: true }));
- } else {
- this.setState({ isHidden: !nextProps.isIntersecting });
+ componentDidMount () {
+ if (!this.props.intersectionObserverWrapper) {
+ // TODO: enable IntersectionObserver optimization for notification statuses.
+ // These are managed in notifications/index.js rather than status_list.js
+ return;
}
+ this.props.intersectionObserverWrapper.observe(
+ this.props.id,
+ this.node,
+ this.handleIntersection
+ );
}
- handleRef = (node) => {
- if (this.props.onRef) {
- this.props.onRef(node);
-
- if (node && node.children.length !== 0) {
- this.height = node.clientHeight;
+ handleIntersection = (entry) => {
+ // Edge 15 doesn't support isIntersecting, but we can infer it from intersectionRatio
+ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
+ const isIntersecting = entry.intersectionRatio > 0;
+ this.setState((prevState) => {
+ if (prevState.isIntersecting && !isIntersecting) {
+ requestIdleCallback(this.hideIfNotIntersecting);
}
+ return {
+ isIntersecting: isIntersecting,
+ isHidden: false,
+ };
+ });
+ }
+
+ hideIfNotIntersecting = () => {
+ // When the browser gets a chance, test if we're still not intersecting,
+ // and if so, set our isHidden to true to trigger an unrender. The point of
+ // this is to save DOM nodes and avoid using up too much memory.
+ // See: https://github.com/tootsuite/mastodon/issues/2900
+ this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
+ }
+
+
+ handleRef = (node) => {
+ this.node = node;
+ if (node && node.children.length !== 0) {
+ this.height = node.clientHeight;
}
}
@@ -107,14 +133,14 @@ class Status extends ImmutablePureComponent {
render () {
let media = null;
let statusAvatar;
- const { status, account, isIntersecting, onRef, ...other } = this.props;
- const { isHidden } = this.state;
+ const { status, account, ...other } = this.props;
+ const { isIntersecting, isHidden } = this.state;
if (status === null) {
return null;
}
- if (isIntersecting === false && isHidden) {
+ if (!isIntersecting && isHidden) {
return (
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 39f663dfb..9ee3af4d1 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
class StatusList extends ImmutablePureComponent {
@@ -26,12 +27,7 @@ class StatusList extends ImmutablePureComponent {
trackScroll: true,
};
- state = {
- isIntersecting: {},
- intersectionCount: 0,
- }
-
- statusRefQueue = []
+ intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@@ -64,53 +60,14 @@ class StatusList extends ImmutablePureComponent {
}
attachIntersectionObserver () {
- const onIntersection = (entries) => {
- this.setState(state => {
-
- entries.forEach(entry => {
- const statusId = entry.target.getAttribute('data-id');
-
- // Edge 15 doesn't support isIntersecting, but we can infer it from intersectionRatio
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
- state.isIntersecting[statusId] = entry.intersectionRatio > 0;
- });
-
- // isIntersecting is a map of DOM data-id's to booleans (true for
- // intersecting, false for non-intersecting).
- //
- // We always want to return true in shouldComponentUpdate() if
- // this object changes, because onIntersection() is only called if
- // something has changed.
- //
- // Now, we *could* use an immutable map or some other structure to
- // diff the full map, but that would be pointless because the browser
- // has already informed us that something has changed. So we can just
- // use a regular object, which will be diffed by ImmutablePureComponent
- // based on reference equality (i.e. it's always "unchanged") and
- // then we just increment intersectionCount to force a change.
-
- return {
- isIntersecting: state.isIntersecting,
- intersectionCount: state.intersectionCount + 1,
- };
- });
- };
-
- const options = {
+ this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
- };
-
- this.intersectionObserver = new IntersectionObserver(onIntersection, options);
-
- if (this.statusRefQueue.length) {
- this.statusRefQueue.forEach(node => this.intersectionObserver.observe(node));
- this.statusRefQueue = [];
- }
+ });
}
detachIntersectionObserver () {
- this.intersectionObserver.disconnect();
+ this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
@@ -125,15 +82,6 @@ class StatusList extends ImmutablePureComponent {
this.node = c;
}
- handleStatusRef = (node) => {
- if (node && this.intersectionObserver) {
- const statusId = node.getAttribute('data-id');
- this.intersectionObserver.observe(node);
- } else {
- this.statusRefQueue.push(node);
- }
- }
-
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
@@ -141,7 +89,6 @@ class StatusList extends ImmutablePureComponent {
render () {
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
- const { isIntersecting } = this.state;
let loadMore = null;
let scrollableArea = null;
@@ -164,7 +111,7 @@ class StatusList extends ImmutablePureComponent {
{prepend}
{statusIds.map((statusId) => {
- return ;
+ return ;
})}
{loadMore}
diff --git a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
new file mode 100644
index 000000000..0e959f9ae
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
@@ -0,0 +1,48 @@
+// Wrapper for IntersectionObserver in order to make working with it
+// a bit easier. We also follow this performance advice:
+// "If you need to observe multiple elements, it is both possible and
+// advised to observe multiple elements using the same IntersectionObserver
+// instance by calling observe() multiple times."
+// https://developers.google.com/web/updates/2016/04/intersectionobserver
+
+class IntersectionObserverWrapper {
+
+ callbacks = {};
+ observerBacklog = [];
+ observer = null;
+
+ connect (options) {
+ const onIntersection = (entries) => {
+ entries.forEach(entry => {
+ const id = entry.target.getAttribute('data-id');
+ if (this.callbacks[id]) {
+ this.callbacks[id](entry);
+ }
+ });
+ };
+
+ this.observer = new IntersectionObserver(onIntersection, options);
+ this.observerBacklog.forEach(([ id, node, callback ]) => {
+ this.observe(id, node, callback);
+ });
+ this.observerBacklog = null;
+ }
+
+ observe (id, node, callback) {
+ if (!this.observer) {
+ this.observerBacklog.push([ id, node, callback ]);
+ } else {
+ this.callbacks[id] = callback;
+ this.observer.observe(node);
+ }
+ }
+
+ disconnect () {
+ if (this.observer) {
+ this.observer.disconnect();
+ }
+ }
+
+}
+
+export default IntersectionObserverWrapper;