diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 87a95fd12d5..b82c63a8428 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -26,6 +26,12 @@ class StatusList extends ImmutablePureComponent {
trackScroll: true,
};
+ state = {
+ isIntersecting: [{ }],
+ }
+
+ statusRefQueue = []
+
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
@@ -42,6 +48,7 @@ class StatusList extends ImmutablePureComponent {
componentDidMount () {
this.attachScrollListener();
+ this.attachIntersectionObserver();
}
componentDidUpdate (prevProps) {
@@ -52,6 +59,39 @@ class StatusList extends ImmutablePureComponent {
componentWillUnmount () {
this.detachScrollListener();
+ this.detachIntersectionObserver();
+ }
+
+ attachIntersectionObserver () {
+ const onIntersection = (entries) => {
+ this.setState(state => {
+ const isIntersecting = { };
+
+ entries.forEach(entry => {
+ const statusId = entry.target.getAttribute('data-id');
+
+ state.isIntersecting[0][statusId] = entry.isIntersecting;
+ });
+
+ return { isIntersecting: [state.isIntersecting[0]] };
+ });
+ };
+
+ const options = {
+ 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();
}
attachScrollListener () {
@@ -66,6 +106,15 @@ 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();
@@ -73,10 +122,11 @@ class StatusList extends ImmutablePureComponent {
render () {
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
+ const isIntersecting = this.state.isIntersecting[0];
- let loadMore = '';
- let scrollableArea = '';
- let unread = '';
+ let loadMore = null;
+ let scrollableArea = null;
+ let unread = null;
if (!isLoading && statusIds.size > 0 && hasMore) {
loadMore =
;
@@ -95,7 +145,7 @@ class StatusList extends ImmutablePureComponent {
{prepend}
{statusIds.map((statusId) => {
- return
;
+ return
;
})}
{loadMore}
diff --git a/app/javascript/mastodon/extra_polyfills.js b/app/javascript/mastodon/extra_polyfills.js
new file mode 100644
index 00000000000..546b693b19b
--- /dev/null
+++ b/app/javascript/mastodon/extra_polyfills.js
@@ -0,0 +1,2 @@
+import 'intersection-observer';
+import 'requestidlecallback';
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index ca6b476e12d..01c28e76851 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -1,9 +1,30 @@
import main from '../mastodon/main';
-if (!window.Intl || !Object.assign || !Number.isNaN ||
- !window.Symbol || !Array.prototype.includes) {
- // load polyfills dynamically
- import('../mastodon/polyfills').then(main).catch(e => {
+const needsBasePolyfills = !(
+ window.Intl &&
+ Object.assign &&
+ Number.isNaN &&
+ window.Symbol &&
+ Array.prototype.includes
+);
+
+const needsExtraPolyfills = !(
+ window.IntersectionObserver &&
+ window.requestIdleCallback
+);
+
+// Latest version of Firefox and Safari do not have IntersectionObserver.
+// Edge does not have requestIdleCallback.
+// This avoids shipping them all the polyfills.
+if (needsBasePolyfills) {
+ Promise.all([
+ import('../mastodon/base_polyfills'),
+ import('../mastodon/extra_polyfills'),
+ ]).then(main).catch(e => {
+ console.error(e); // eslint-disable-line no-console
+ });
+} else if (needsExtraPolyfills) {
+ import('../mastodon/extra_polyfills').then(main).catch(e => {
console.error(e); // eslint-disable-line no-console
});
} else {
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 88045976145..3971a85bc48 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -554,6 +554,14 @@
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
+ @keyframes fade {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ opacity: 1;
+ animation: fade 0.3s linear;
+
&.status-direct {
background: lighten($ui-base-color, 8%);
diff --git a/package.json b/package.json
index 993570ce017..995ad60945b 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"glob": "^7.1.1",
"http-link-header": "^0.8.0",
"immutable": "^3.8.1",
+ "intersection-observer": "^0.2.1",
"intl": "^1.2.5",
"is-nan": "^1.2.1",
"js-yaml": "^3.8.3",
@@ -92,6 +93,7 @@
"redux": "^3.6.0",
"redux-immutable": "^3.1.0",
"redux-thunk": "^2.2.0",
+ "requestidlecallback": "^0.3.0",
"reselect": "^2.5.4",
"rimraf": "^2.6.1",
"sass-loader": "^6.0.3",
diff --git a/yarn.lock b/yarn.lock
index 3c19450b631..e84c045daed 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3341,6 +3341,10 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+intersection-observer@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.2.1.tgz#cb55175f4eebef6436d957a7d1774d39a9248e5e"
+
intl:
version "1.2.5"
resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde"
@@ -5832,6 +5836,10 @@ request@2, request@2.x, request@^2.74.0, request@^2.79.0:
tunnel-agent "~0.4.1"
uuid "^3.0.0"
+requestidlecallback@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/requestidlecallback/-/requestidlecallback-0.3.0.tgz#6fb74e0733f90df3faa4838f9f6a2a5f9b742ac5"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"