From b099746d712f4ff325ea579591fbe365b559e3a4 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 22 Mar 2025 17:58:41 -0400 Subject: [PATCH 01/17] add stop at caught up preference --- src/features/scroll_to_bottom.js | 9 +++++++++ src/features/scroll_to_bottom.json | 7 +++++++ src/features/tweaks/caught_up_line.js | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index e4ddb7dbd7..b6ac24b2c7 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -2,6 +2,8 @@ import { keyToClasses, keyToCss } from '../utils/css_map.js'; import { translate } from '../utils/language_data.js'; import { pageModifications } from '../utils/mutations.js'; import { buildStyle } from '../utils/interface.js'; +import { getPreferences } from '../utils/preferences.js'; +import { borderAttribute, tagChicletCarouselLinkSelector } from './tweaks/caught_up_line.js'; const scrollToBottomButtonId = 'xkit-scroll-to-bottom-button'; $(`[id="${scrollToBottomButtonId}"]`).remove(); @@ -88,15 +90,22 @@ const addButtonToPage = async function ([scrollToTopButton]) { pageModifications.register('*', checkForButtonRemoved); }; +const onTagChicletCarouselItemsAdded = () => stopScrolling(); + export const main = async function () { + const { stopAtCaughtUp } = await getPreferences('scroll_to_bottom'); + pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage); pageModifications.register(knightRiderLoaderSelector, onLoadersAdded); + stopAtCaughtUp && pageModifications.register(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`, onTagChicletCarouselItemsAdded); }; export const clean = async function () { pageModifications.unregister(addButtonToPage); pageModifications.unregister(checkForButtonRemoved); pageModifications.unregister(onLoadersAdded); + pageModifications.unregister(onTagChicletCarouselItemsAdded); + stopScrolling(); scrollToBottomButton?.remove(); }; diff --git a/src/features/scroll_to_bottom.json b/src/features/scroll_to_bottom.json index aaf7f404ab..59a35088cd 100644 --- a/src/features/scroll_to_bottom.json +++ b/src/features/scroll_to_bottom.json @@ -6,5 +6,12 @@ "class_name": "ri-download-fill", "color": "#e8d738", "background_color": "black" + }, + "preferences": { + "stopAtCaughtUp": { + "type": "checkbox", + "label": "Stop scrolling the Following feed when I reach the Changes/Trending/etc. carousel", + "default": true + } } } diff --git a/src/features/tweaks/caught_up_line.js b/src/features/tweaks/caught_up_line.js index 76ed0cd3ed..640e863f44 100644 --- a/src/features/tweaks/caught_up_line.js +++ b/src/features/tweaks/caught_up_line.js @@ -4,7 +4,7 @@ import { pageModifications } from '../../utils/mutations.js'; import { followingTimelineSelector } from '../../utils/timeline_id.js'; const hiddenAttribute = 'data-tweaks-caught-up-line-title'; -const borderAttribute = 'data-tweaks-caught-up-line-border'; +export const borderAttribute = 'data-tweaks-caught-up-line-border'; export const styleElement = buildStyle(` [${hiddenAttribute}] > div { display: none; } @@ -24,7 +24,7 @@ export const styleElement = buildStyle(` `); const listTimelineObjectSelector = keyToCss('listTimelineObject'); -const tagChicletCarouselLinkSelector = `${followingTimelineSelector} ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; +export const tagChicletCarouselLinkSelector = `${followingTimelineSelector} ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; const createCaughtUpLine = tagChicletCarouselItems => tagChicletCarouselItems .map(getTimelineItemWrapper) From a9066e3bccc5e3601dd5e7f84068befd76e401d1 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 15 Apr 2025 21:34:36 -0700 Subject: [PATCH 02/17] scroll to carousel itself --- src/features/scroll_to_bottom.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index b6ac24b2c7..6575b2e062 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -1,7 +1,7 @@ import { keyToClasses, keyToCss } from '../utils/css_map.js'; import { translate } from '../utils/language_data.js'; import { pageModifications } from '../utils/mutations.js'; -import { buildStyle } from '../utils/interface.js'; +import { buildStyle, getTimelineItemWrapper } from '../utils/interface.js'; import { getPreferences } from '../utils/preferences.js'; import { borderAttribute, tagChicletCarouselLinkSelector } from './tweaks/caught_up_line.js'; @@ -90,7 +90,15 @@ const addButtonToPage = async function ([scrollToTopButton]) { pageModifications.register('*', checkForButtonRemoved); }; -const onTagChicletCarouselItemsAdded = () => stopScrolling(); +const onTagChicletCarouselItemsAdded = ([carousel]) => { + if (active) { + stopScrolling(); + + const titleElement = getTimelineItemWrapper(carousel)?.previousElementSibling; + const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; + titleElementTop && window.scrollBy({ top: titleElementTop }); + } +}; export const main = async function () { const { stopAtCaughtUp } = await getPreferences('scroll_to_bottom'); From 46bf6e5acd92d0bfd798be60b00b0b70743fe669 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 15 Apr 2025 21:34:41 -0700 Subject: [PATCH 03/17] test code --- src/features/scroll_to_bottom.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index 6575b2e062..a931956682 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -49,6 +49,11 @@ const startScrolling = () => { active = true; scrollToBottomButton.classList.add(activeClass); scrollToBottom(); + + const temp = document.querySelector(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`); + if (temp) { + onTagChicletCarouselItemsAdded([temp]); + } }; const stopScrolling = () => { From 2929c791cd67e0b9bc51dd39ab8dc073f88d3ea9 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 15 Apr 2025 21:34:45 -0700 Subject: [PATCH 04/17] Revert "test code" This reverts commit 9fe679ec7648cd1f00db86abfe1c16d7236fc1a9. --- src/features/scroll_to_bottom.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index a931956682..6575b2e062 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -49,11 +49,6 @@ const startScrolling = () => { active = true; scrollToBottomButton.classList.add(activeClass); scrollToBottom(); - - const temp = document.querySelector(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`); - if (temp) { - onTagChicletCarouselItemsAdded([temp]); - } }; const stopScrolling = () => { From b8bb1eab03fe0904c270368bf9ce3116875d89b6 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 16 Apr 2025 12:09:22 -0700 Subject: [PATCH 05/17] no really this time we are waiting long enough. does this look fun to you --- src/features/scroll_to_bottom.js | 10 ++++++---- src/utils/interface.js | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index 6575b2e062..d54da84684 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -1,7 +1,7 @@ import { keyToClasses, keyToCss } from '../utils/css_map.js'; import { translate } from '../utils/language_data.js'; import { pageModifications } from '../utils/mutations.js'; -import { buildStyle, getTimelineItemWrapper } from '../utils/interface.js'; +import { buildStyle, getTimelineItemWrapper, waitForScroller } from '../utils/interface.js'; import { getPreferences } from '../utils/preferences.js'; import { borderAttribute, tagChicletCarouselLinkSelector } from './tweaks/caught_up_line.js'; @@ -94,9 +94,11 @@ const onTagChicletCarouselItemsAdded = ([carousel]) => { if (active) { stopScrolling(); - const titleElement = getTimelineItemWrapper(carousel)?.previousElementSibling; - const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; - titleElementTop && window.scrollBy({ top: titleElementTop }); + waitForScroller().then(() => { + const titleElement = getTimelineItemWrapper(carousel)?.previousElementSibling; + const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; + titleElementTop && window.scrollBy({ top: titleElementTop }); + }); } }; diff --git a/src/utils/interface.js b/src/utils/interface.js index 1dfe202fdc..dbd0bb021d 100644 --- a/src/utils/interface.js +++ b/src/utils/interface.js @@ -144,3 +144,24 @@ export const appendWithoutOverflow = (element, target, defaultPosition = 'below' element.style.setProperty('--horizontal-offset', `${preventOverflowTargetRect.right - 15 - elementRect.right}px`); } }; + +export const wait = async ({ ms, msTimes = 1, frames }) => { + if (ms !== undefined) { + for (let i = 0; i < msTimes; i++) { + await new Promise(resolve => setTimeout(resolve, ms)); + } + } + for (let i = 0; i < frames; i++) { + await new Promise(requestAnimationFrame); + } +}; + +/** + * @returns {Promise} - Attempts to resolve after Tumblr's endless scrolling code has stopped updating + */ +export const waitForScroller = () => + wait({ + ms: 100, // matches the debounce delay on tumblr's `scheduleUpdate` virtual scroller function + msTimes: 3, // arbitrary, high-enough-in-practice number + frames: 1 + }); From e187d6fb63beb6bdaa4e0bc809e3adfc5069156e Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 16 Apr 2025 12:09:34 -0700 Subject: [PATCH 06/17] test code --- src/features/scroll_to_bottom.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index d54da84684..7f340bd02b 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -105,6 +105,16 @@ const onTagChicletCarouselItemsAdded = ([carousel]) => { export const main = async function () { const { stopAtCaughtUp } = await getPreferences('scroll_to_bottom'); + pageModifications.register('[tabindex="-1"][data-id]', (postElements) => { + for (const postElement of postElements) { + if (Math.random() < 0.05) { + console.log('simulating scroll stop', getTimelineItemWrapper(postElement)); + onTagChicletCarouselItemsAdded([getTimelineItemWrapper(postElement)]); + break; + } + } + }); + pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage); pageModifications.register(knightRiderLoaderSelector, onLoadersAdded); stopAtCaughtUp && pageModifications.register(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`, onTagChicletCarouselItemsAdded); From ad58dd0cc8d87c63574ee6036cde3ac3b40cb47d Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 16 Apr 2025 12:09:39 -0700 Subject: [PATCH 07/17] Revert "test code" This reverts commit dc967739501af97c15aa279b69d31cbc79722578. --- src/features/scroll_to_bottom.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/features/scroll_to_bottom.js b/src/features/scroll_to_bottom.js index 7f340bd02b..d54da84684 100644 --- a/src/features/scroll_to_bottom.js +++ b/src/features/scroll_to_bottom.js @@ -105,16 +105,6 @@ const onTagChicletCarouselItemsAdded = ([carousel]) => { export const main = async function () { const { stopAtCaughtUp } = await getPreferences('scroll_to_bottom'); - pageModifications.register('[tabindex="-1"][data-id]', (postElements) => { - for (const postElement of postElements) { - if (Math.random() < 0.05) { - console.log('simulating scroll stop', getTimelineItemWrapper(postElement)); - onTagChicletCarouselItemsAdded([getTimelineItemWrapper(postElement)]); - break; - } - } - }); - pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage); pageModifications.register(knightRiderLoaderSelector, onLoadersAdded); stopAtCaughtUp && pageModifications.register(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`, onTagChicletCarouselItemsAdded); From a4c0062453b7f66842e45116f573e994a069eaf4 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Tue, 19 Aug 2025 21:23:24 -0400 Subject: [PATCH 08/17] new method: check every cell --- src/features/scroll_to_bottom/index.js | 32 ++++++++++++++++---------- src/features/tweaks/caught_up_line.js | 4 ++-- src/main_world/unbury_cell_item.js | 14 +++++++++++ src/utils/react_props.js | 8 +++++++ 4 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 src/main_world/unbury_cell_item.js diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index 250f8d5f63..70b3cdc2c9 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -1,9 +1,9 @@ import { keyToClasses, keyToCss } from '../../utils/css_map.js'; import { translate } from '../../utils/language_data.js'; import { pageModifications } from '../../utils/mutations.js'; -import { buildStyle, getTimelineItemWrapper, waitForScroller } from '../../utils/interface.js'; +import { buildStyle, waitForScroller } from '../../utils/interface.js'; import { getPreferences } from '../../utils/preferences.js'; -import { borderAttribute, tagChicletCarouselLinkSelector } from './../tweaks/caught_up_line.js'; +import { cellItem } from '../../utils/react_props.js'; const scrollToBottomButtonId = 'xkit-scroll-to-bottom-button'; $(`[id="${scrollToBottomButtonId}"]`).remove(); @@ -90,15 +90,23 @@ const addButtonToPage = async function ([scrollToTopButton]) { pageModifications.register('*', checkForButtonRemoved); }; -const onTagChicletCarouselItemsAdded = ([carousel]) => { - if (active) { - stopScrolling(); +const caughtUpCarouselObjectType = 'followed_tag_carousel_card'; - waitForScroller().then(() => { - const titleElement = getTimelineItemWrapper(carousel)?.previousElementSibling; - const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; - titleElementTop && window.scrollBy({ top: titleElementTop }); - }); +const onCellsAdded = async cells => { + if (active) { + for (const cell of cells) { + const item = await cellItem(cell); + if (item.elements?.some(({ objectType }) => objectType === caughtUpCarouselObjectType)) { + stopScrolling(); + + waitForScroller().then(() => { + const titleElement = cell?.previousElementSibling; + const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; + titleElementTop && window.scrollBy({ top: titleElementTop }); + }); + return; + } + } } }; @@ -107,14 +115,14 @@ export const main = async function () { pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage); pageModifications.register(knightRiderLoaderSelector, onLoadersAdded); - stopAtCaughtUp && pageModifications.register(`${tagChicletCarouselLinkSelector}, [${borderAttribute}]`, onTagChicletCarouselItemsAdded); + stopAtCaughtUp && pageModifications.register(keyToCss(('cell')), onCellsAdded); }; export const clean = async function () { pageModifications.unregister(addButtonToPage); pageModifications.unregister(checkForButtonRemoved); pageModifications.unregister(onLoadersAdded); - pageModifications.unregister(onTagChicletCarouselItemsAdded); + pageModifications.unregister(onCellsAdded); stopScrolling(); scrollToBottomButton?.remove(); diff --git a/src/features/tweaks/caught_up_line.js b/src/features/tweaks/caught_up_line.js index 640e863f44..76ed0cd3ed 100644 --- a/src/features/tweaks/caught_up_line.js +++ b/src/features/tweaks/caught_up_line.js @@ -4,7 +4,7 @@ import { pageModifications } from '../../utils/mutations.js'; import { followingTimelineSelector } from '../../utils/timeline_id.js'; const hiddenAttribute = 'data-tweaks-caught-up-line-title'; -export const borderAttribute = 'data-tweaks-caught-up-line-border'; +const borderAttribute = 'data-tweaks-caught-up-line-border'; export const styleElement = buildStyle(` [${hiddenAttribute}] > div { display: none; } @@ -24,7 +24,7 @@ export const styleElement = buildStyle(` `); const listTimelineObjectSelector = keyToCss('listTimelineObject'); -export const tagChicletCarouselLinkSelector = `${followingTimelineSelector} ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; +const tagChicletCarouselLinkSelector = `${followingTimelineSelector} ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; const createCaughtUpLine = tagChicletCarouselItems => tagChicletCarouselItems .map(getTimelineItemWrapper) diff --git a/src/main_world/unbury_cell_item.js b/src/main_world/unbury_cell_item.js new file mode 100644 index 0000000000..66e5ea55c4 --- /dev/null +++ b/src/main_world/unbury_cell_item.js @@ -0,0 +1,14 @@ +export default function unburyCellItem () { + const postElement = this; + const reactKey = Object.keys(postElement).find(key => key.startsWith('__reactFiber')); + let fiber = postElement[reactKey]; + + while (fiber !== null) { + const { item } = fiber.memoizedProps || {}; + if (item !== undefined) { + return item; + } else { + fiber = fiber.return; + } + } +} diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 6a0a952de0..d50f66e4ca 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -10,6 +10,14 @@ export const timelineObject = weakMemoize(postElement => inject('/main_world/unbury_timeline_object.js', [], postElement) ); +/** + * @param {Element} cellElement - An on-screen timeline cell + * @returns {Promise} - The post's buried item property + */ +export const cellItem = weakMemoize(cellElement => + inject('/main_world/unbury_cell_item.js', [], cellElement) +); + /** * @param {Element} notificationElement - An on-screen notification * @returns {Promise} - The notification's buried notification property From bc1993a1406712e5a4dcf1c487fe1dc8d2cc4335 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 10:31:38 -0400 Subject: [PATCH 09/17] fixes --- src/main_world/unbury_cell_item.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main_world/unbury_cell_item.js b/src/main_world/unbury_cell_item.js index 66e5ea55c4..1652a465a3 100644 --- a/src/main_world/unbury_cell_item.js +++ b/src/main_world/unbury_cell_item.js @@ -1,7 +1,7 @@ export default function unburyCellItem () { - const postElement = this; - const reactKey = Object.keys(postElement).find(key => key.startsWith('__reactFiber')); - let fiber = postElement[reactKey]; + const cellElement = this; + const reactKey = Object.keys(cellElement).find(key => key.startsWith('__reactFiber')); + let fiber = cellElement[reactKey]; while (fiber !== null) { const { item } = fiber.memoizedProps || {}; From ca2b3b2e804107ca46916ca1baad3c0c4c18f5b4 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 10:31:54 -0400 Subject: [PATCH 10/17] initial click behavior --- src/features/scroll_to_bottom/index.js | 46 +++++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index 70b3cdc2c9..a5646510c2 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -15,6 +15,7 @@ ${keyToCss('notifications')} + div `; const knightRiderLoaderSelector = `:is(${loaderSelector}) > ${keyToCss('knightRiderLoader')}`; +let stopAtCaughtUp; let scrollToBottomButton; let active = false; @@ -44,7 +45,9 @@ const scrollToBottom = () => { }; const observer = new ResizeObserver(scrollToBottom); -const startScrolling = () => { +const startScrolling = async () => { + if (stopAtCaughtUp && await scrollToCaughtUp()) return; + observer.observe(document.documentElement); active = true; scrollToBottomButton.classList.add(activeClass); @@ -92,26 +95,37 @@ const addButtonToPage = async function ([scrollToTopButton]) { const caughtUpCarouselObjectType = 'followed_tag_carousel_card'; -const onCellsAdded = async cells => { - if (active) { - for (const cell of cells) { - const item = await cellItem(cell); - if (item.elements?.some(({ objectType }) => objectType === caughtUpCarouselObjectType)) { - stopScrolling(); - - waitForScroller().then(() => { - const titleElement = cell?.previousElementSibling; - const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; - titleElementTop && window.scrollBy({ top: titleElementTop }); - }); - return; - } +const scrollToCaughtUp = async (addedCells) => { + const isStarting = !active; + for (const cell of isStarting ? [...document.querySelectorAll(keyToCss('cell'))] : addedCells) { + const item = await cellItem(cell); + if (item.elements?.some(({ objectType }) => objectType === caughtUpCarouselObjectType)) { + if (!isStarting) stopScrolling(); + if (!isStarting) await waitForScroller(); + + const titleElement = cell?.previousElementSibling; + const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; + if (!titleElementTop) continue; + + const isAboveViewportBottom = titleElementTop < window.innerHeight; + if (isStarting && isAboveViewportBottom) continue; + + window.scrollBy({ top: titleElementTop }); + console.log( + isStarting + ? 'Scroll to Bottom scrolled down to existing carousel:' + : 'Scroll to Bottom scrolled to newly added carousel:', + titleElement + ); + return true; } } }; +const onCellsAdded = addedCells => active && scrollToCaughtUp(addedCells); + export const main = async function () { - const { stopAtCaughtUp } = await getPreferences('scroll_to_bottom'); + ({ stopAtCaughtUp } = await getPreferences('scroll_to_bottom')); pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage); pageModifications.register(knightRiderLoaderSelector, onLoadersAdded); From 32ac953886d0fc0be71c740b0c18a05a5210f346 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 13:45:40 -0400 Subject: [PATCH 11/17] less arcane reliable scroll --- src/features/scroll_to_bottom/index.js | 50 ++++++++++++++++++-------- src/utils/interface.js | 21 ----------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index a5646510c2..0551c01d36 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -1,7 +1,7 @@ import { keyToClasses, keyToCss } from '../../utils/css_map.js'; import { translate } from '../../utils/language_data.js'; import { pageModifications } from '../../utils/mutations.js'; -import { buildStyle, waitForScroller } from '../../utils/interface.js'; +import { buildStyle } from '../../utils/interface.js'; import { getPreferences } from '../../utils/preferences.js'; import { cellItem } from '../../utils/react_props.js'; @@ -93,28 +93,50 @@ const addButtonToPage = async function ([scrollToTopButton]) { pageModifications.register('*', checkForButtonRemoved); }; +const debounce = (func, ms) => { + let timeoutID; + return (...args) => { + clearTimeout(timeoutID); + timeoutID = setTimeout(() => func(...args), ms); + }; +}; + +const reliablyScrollToTarget = target => { + const callback = () => { + window.scrollBy({ top: target?.getBoundingClientRect?.()?.top }); + debouncedCancel(); + }; + const observer = new ResizeObserver(callback); + const debouncedCancel = debounce(() => { + console.log('disconnected reliablyScrollToTarget observer'); + observer.disconnect(); + }, 500); + observer.observe(document.documentElement); + callback(); +}; + const caughtUpCarouselObjectType = 'followed_tag_carousel_card'; const scrollToCaughtUp = async (addedCells) => { - const isStarting = !active; - for (const cell of isStarting ? [...document.querySelectorAll(keyToCss('cell'))] : addedCells) { + for (const cell of addedCells || [...document.querySelectorAll(keyToCss('cell'))]) { const item = await cellItem(cell); if (item.elements?.some(({ objectType }) => objectType === caughtUpCarouselObjectType)) { - if (!isStarting) stopScrolling(); - if (!isStarting) await waitForScroller(); - const titleElement = cell?.previousElementSibling; - const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; - if (!titleElementTop) continue; + if (!titleElement) continue; - const isAboveViewportBottom = titleElementTop < window.innerHeight; - if (isStarting && isAboveViewportBottom) continue; + if (active) { + stopScrolling(); + } else { + const titleElementTop = titleElement?.getBoundingClientRect?.()?.top; + const isAboveViewportBottom = titleElementTop !== undefined && titleElementTop < window.innerHeight; + if (isAboveViewportBottom) continue; + } - window.scrollBy({ top: titleElementTop }); + reliablyScrollToTarget(titleElement); console.log( - isStarting - ? 'Scroll to Bottom scrolled down to existing carousel:' - : 'Scroll to Bottom scrolled to newly added carousel:', + addedCells + ? 'Scroll to Bottom scrolled to newly added carousel:' + : 'Scroll to Bottom scrolled down to existing carousel:', titleElement ); return true; diff --git a/src/utils/interface.js b/src/utils/interface.js index 85b54a079d..c6e0dc50aa 100644 --- a/src/utils/interface.js +++ b/src/utils/interface.js @@ -146,24 +146,3 @@ export const appendWithoutOverflow = (element, target, defaultPosition = 'below' element.style.setProperty('--horizontal-offset', `${preventOverflowTargetRect.left + 15 - elementRect.left}px`); } }; - -export const wait = async ({ ms, msTimes = 1, frames }) => { - if (ms !== undefined) { - for (let i = 0; i < msTimes; i++) { - await new Promise(resolve => setTimeout(resolve, ms)); - } - } - for (let i = 0; i < frames; i++) { - await new Promise(requestAnimationFrame); - } -}; - -/** - * @returns {Promise} - Attempts to resolve after Tumblr's endless scrolling code has stopped updating - */ -export const waitForScroller = () => - wait({ - ms: 100, // matches the debounce delay on tumblr's `scheduleUpdate` virtual scroller function - msTimes: 3, // arbitrary, high-enough-in-practice number - frames: 1 - }); From 0703ed8ae219e3983c905b658557ed78ca271ce6 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 14:47:28 -0400 Subject: [PATCH 12/17] extract debounce util --- src/features/scroll_to_bottom/index.js | 9 +-------- src/utils/debounce.js | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/utils/debounce.js diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index 0551c01d36..3f122d5b98 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -4,6 +4,7 @@ import { pageModifications } from '../../utils/mutations.js'; import { buildStyle } from '../../utils/interface.js'; import { getPreferences } from '../../utils/preferences.js'; import { cellItem } from '../../utils/react_props.js'; +import { debounce } from '../../utils/debounce.js'; const scrollToBottomButtonId = 'xkit-scroll-to-bottom-button'; $(`[id="${scrollToBottomButtonId}"]`).remove(); @@ -93,14 +94,6 @@ const addButtonToPage = async function ([scrollToTopButton]) { pageModifications.register('*', checkForButtonRemoved); }; -const debounce = (func, ms) => { - let timeoutID; - return (...args) => { - clearTimeout(timeoutID); - timeoutID = setTimeout(() => func(...args), ms); - }; -}; - const reliablyScrollToTarget = target => { const callback = () => { window.scrollBy({ top: target?.getBoundingClientRect?.()?.top }); diff --git a/src/utils/debounce.js b/src/utils/debounce.js new file mode 100644 index 0000000000..7b268af262 --- /dev/null +++ b/src/utils/debounce.js @@ -0,0 +1,7 @@ +export const debounce = (func, ms) => { + let timeoutID; + return (...args) => { + clearTimeout(timeoutID); + timeoutID = setTimeout(() => func(...args), ms); + }; +}; From cda6c455a710a2501930ff25fcc133a065e6399b Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 14:51:19 -0400 Subject: [PATCH 13/17] remove test logging --- src/features/scroll_to_bottom/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index 3f122d5b98..9203d1ad0e 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -100,10 +100,7 @@ const reliablyScrollToTarget = target => { debouncedCancel(); }; const observer = new ResizeObserver(callback); - const debouncedCancel = debounce(() => { - console.log('disconnected reliablyScrollToTarget observer'); - observer.disconnect(); - }, 500); + const debouncedCancel = debounce(() => observer.disconnect(), 500); observer.observe(document.documentElement); callback(); }; @@ -126,12 +123,6 @@ const scrollToCaughtUp = async (addedCells) => { } reliablyScrollToTarget(titleElement); - console.log( - addedCells - ? 'Scroll to Bottom scrolled to newly added carousel:' - : 'Scroll to Bottom scrolled down to existing carousel:', - titleElement - ); return true; } } From edd4aaf05510da4a96301d53408fe9ebda880914 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Wed, 20 Aug 2025 15:01:16 -0400 Subject: [PATCH 14/17] variable name tweak --- src/features/scroll_to_bottom/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/scroll_to_bottom/index.js b/src/features/scroll_to_bottom/index.js index 9203d1ad0e..2a28d471a9 100644 --- a/src/features/scroll_to_bottom/index.js +++ b/src/features/scroll_to_bottom/index.js @@ -97,10 +97,10 @@ const addButtonToPage = async function ([scrollToTopButton]) { const reliablyScrollToTarget = target => { const callback = () => { window.scrollBy({ top: target?.getBoundingClientRect?.()?.top }); - debouncedCancel(); + debouncedDisconnect(); }; const observer = new ResizeObserver(callback); - const debouncedCancel = debounce(() => observer.disconnect(), 500); + const debouncedDisconnect = debounce(() => observer.disconnect(), 500); observer.observe(document.documentElement); callback(); }; From 5f41c7646b54608e82c7391fb76f6be4640d9b30 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Tue, 3 Feb 2026 11:41:48 -0800 Subject: [PATCH 15/17] format --- src/utils/react_props.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 6adcde8525..2d9f92178a 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -17,7 +17,7 @@ export const timelineObject = weakMemoize(postElement => * @returns {Promise} The post's buried item property */ export const cellItem = weakMemoize(cellElement => - inject('/main_world/unbury_cell_item.js', [], cellElement) + inject('/main_world/unbury_cell_item.js', [], cellElement), ); /** From 4f318b1da5bfef951b3b4301416af205c6007d6e Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sat, 14 Feb 2026 12:44:28 -0800 Subject: [PATCH 16/17] update new util function --- src/utils/react_props.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 0208a7642b..4823556413 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -16,9 +16,10 @@ export const timelineObject = postElement => { * @param {Element} cellElement An on-screen timeline cell * @returns {Promise} The post's buried item property */ -export const cellItem = weakMemoize(cellElement => - inject('/main_world/unbury_cell_item.js', [], cellElement), -); +export const cellItem = cellElement => { + cellElement.cellItemPromise ??= inject('/main_world/unbury_cell_item.js', [], cellElement); + return cellElement.cellItemPromise; +}; /** * @param {Element} trailItemElement An on-screen reblog trail item element From 3fd457a4384f3b73e4b041001e36a15c5af92fe3 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sat, 14 Feb 2026 12:44:45 -0800 Subject: [PATCH 17/17] fix jsdoc --- src/utils/react_props.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 4823556413..3971ae6984 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -14,7 +14,7 @@ export const timelineObject = postElement => { /** * @param {Element} cellElement An on-screen timeline cell - * @returns {Promise} The post's buried item property + * @returns {Promise} The element's buried item property */ export const cellItem = cellElement => { cellElement.cellItemPromise ??= inject('/main_world/unbury_cell_item.js', [], cellElement);