From 2d27f5679a5d87aac50de7158ea6d26bfee35ce1 Mon Sep 17 00:00:00 2001 From: catinshoe <> Date: Tue, 6 Jun 2023 06:06:00 +0200 Subject: [PATCH] add support for long threads to infinite scrolling --- public/js/infiniteScroll.js | 189 +++++++++++++++++++++++++++++------- 1 file changed, 156 insertions(+), 33 deletions(-) diff --git a/public/js/infiniteScroll.js b/public/js/infiniteScroll.js index 9939c03fe..1f6cde30a 100644 --- a/public/js/infiniteScroll.js +++ b/public/js/infiniteScroll.js @@ -1,11 +1,13 @@ // @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 // SPDX-License-Identifier: AGPL-3.0-only +const LOADING_TEXT = "Loading..."; + function insertBeforeLast(node, elem) { node.insertBefore(elem, node.childNodes[node.childNodes.length - 2]); } function getLoadMore(doc) { - return doc.querySelector('.show-more:not(.timeline-item)'); + return doc.querySelector(".show-more:not(.timeline-item)"); } function isDuplicate(item, itemClass) { @@ -15,52 +17,173 @@ function isDuplicate(item, itemClass) { return document.querySelector(itemClass + " .tweet-link[href='" + href + "']") != null; } -window.onload = function() { +function addScrollToURL(href) { + const url = new URL(href); + url.searchParams.append("scroll", "true"); + return url.toString(); +} + +function fetchAndParse(url) { + return fetch(url) + .then(function (response) { + return response.text(); + }) + .then(function (html) { + var parser = new DOMParser(); + return parser.parseFromString(html, "text/html"); + }); +} + +window.onload = function () { const url = window.location.pathname; const isTweet = url.indexOf("/status/") !== -1; + const isIncompleteThread = + isTweet && document.querySelector(".timeline-item.more-replies") != null; + const containerClass = isTweet ? ".replies" : ".timeline"; - const itemClass = containerClass + ' > div:not(.top-ref)'; + const itemClass = containerClass + " > div:not(.top-ref)"; var html = document.querySelector("html"); - var container = document.querySelector(containerClass); + var mainContainer = document.querySelector(containerClass); var loading = false; - window.addEventListener('scroll', function() { - if (loading) return; - if (html.scrollTop + html.clientHeight >= html.scrollHeight - 3000) { - loading = true; - var loadMore = getLoadMore(document); - if (loadMore == null) return; + function catchErrors(err) { + console.warn("Something went wrong.", err); + loading = true; + } + + function appendLoadedReplies(loadMore) { + return function (doc) { + loadMore.remove(); + + for (var item of doc.querySelectorAll(itemClass)) { + if (item.className == "timeline-item show-more") continue; + if (isDuplicate(item, itemClass)) continue; + if (isTweet) mainContainer.appendChild(item); + else insertBeforeLast(mainContainer, item); + } + + loading = false; + const newLoadMore = getLoadMore(doc); + if (newLoadMore == null) return; + if (isTweet) mainContainer.appendChild(newLoadMore); + else insertBeforeLast(mainContainer, newLoadMore); + }; + } - loadMore.children[0].text = "Loading..."; + var scrollListener = null; + if (!isIncompleteThread) { + scrollListener = (e) => { + if (loading) return; - var url = new URL(loadMore.children[0].href); - url.searchParams.append('scroll', 'true'); + if (html.scrollTop + html.clientHeight >= html.scrollHeight - 3000) { + loading = true; + var loadMore = getLoadMore(document); + if (loadMore == null) return; + + loadMore.children[0].text = LOADING_TEXT; - fetch(url.toString()).then(function (response) { - return response.text(); - }).then(function (html) { - var parser = new DOMParser(); - var doc = parser.parseFromString(html, 'text/html'); + const fetchUrl = addScrollToURL(loadMore.children[0].href); + fetchAndParse(fetchUrl) + .then(appendLoadedReplies(loadMore)) + .catch(catchErrors); + } + }; + } else { + function getEarlierReplies(doc) { + return doc.querySelector(".timeline-item.more-replies.earlier-replies"); + } + + function getLaterReplies(doc) { + return doc.querySelector(".after-tweet > .timeline-item.more-replies"); + } + + function prependLoadedThread(loadMore) { + return function (doc) { loadMore.remove(); - for (var item of doc.querySelectorAll(itemClass)) { - if (item.className == "timeline-item show-more") continue; - if (isDuplicate(item, itemClass)) continue; - if (isTweet) container.appendChild(item); - else insertBeforeLast(container, item); - } + const targetSelector = ".before-tweet.thread-line"; + const threadContainer = document.querySelector(targetSelector); + const earlierReplies = doc.querySelector(targetSelector); + for (var i = earlierReplies.children.length - 1; i >= 0; i--) { + threadContainer.insertBefore( + earlierReplies.children[i], + threadContainer.children[0] + ); + } loading = false; - const newLoadMore = getLoadMore(doc); - if (newLoadMore == null) return; - if (isTweet) container.appendChild(newLoadMore); - else insertBeforeLast(container, newLoadMore); - }).catch(function (err) { - console.warn('Something went wrong.', err); - loading = true; - }); + }; } - }); + + function appendLoadedThread(loadMore) { + return function (doc) { + const targetSelector = ".after-tweet.thread-line"; + const threadContainer = document.querySelector(targetSelector); + + const laterReplies = doc.querySelector(targetSelector); + while (laterReplies && laterReplies.firstChild) { + threadContainer.appendChild(laterReplies.firstChild); + } + + const finalReply = threadContainer.lastElementChild; + if (finalReply.classList.contains("thread-last")) { + fetchAndParse(finalReply.children[0].href).then(function (lastDoc) { + loadMore.remove(); + const anyResponses = lastDoc.querySelector(".replies"); + anyResponses && + insertBeforeLast( + threadContainer.parentElement.parentElement, + anyResponses + ); + loading = false; + }); + } else { + loadMore.remove(); + loading = false; + } + }; + } + + scrollListener = (e) => { + if (loading) return; + + if (html.scrollTop <= html.clientHeight) { + var loadMore = getEarlierReplies(document); + if (loadMore == null) return; + loading = true; + + loadMore.children[0].text = LOADING_TEXT; + + fetchAndParse(loadMore.children[0].href) + .then(prependLoadedThread(loadMore)) + .catch(catchErrors); + } else if (html.scrollTop + html.clientHeight >= html.scrollHeight - 3000) { + var loadMore = getLaterReplies(document); + if (loadMore != null) { + loading = true; + + loadMore.children[0].text = LOADING_TEXT; + + fetchAndParse(loadMore.children[0].href) + .then(appendLoadedThread(loadMore)) + .catch(catchErrors); + } else { + loadMore = getLoadMore(document); + if (loadMore == null) return; + loading = true; + + loadMore.children[0].text = LOADING_TEXT; + + mainContainer = document.querySelector(containerClass); + fetchAndParse(loadMore.children[0].href) + .then(appendLoadedReplies(loadMore)) + .catch(catchErrors); + } + } + }; + } + + window.addEventListener("scroll", scrollListener); }; // @license-end