Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b099746
add stop at caught up preference
marcustyphoon Mar 22, 2025
a9066e3
scroll to carousel itself
marcustyphoon Apr 16, 2025
46bf6e5
test code
marcustyphoon Apr 16, 2025
2929c79
Revert "test code"
marcustyphoon Apr 16, 2025
b8bb1ea
no really this time we are waiting long enough.
marcustyphoon Apr 16, 2025
e187d6f
test code
marcustyphoon Apr 16, 2025
ad58dd0
Revert "test code"
marcustyphoon Apr 16, 2025
7e85a18
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Aug 19, 2025
a4c0062
new method: check every cell
marcustyphoon Aug 20, 2025
bc1993a
fixes
marcustyphoon Aug 20, 2025
ca2b3b2
initial click behavior
marcustyphoon Aug 20, 2025
32ac953
less arcane reliable scroll
marcustyphoon Aug 20, 2025
0703ed8
extract debounce util
marcustyphoon Aug 20, 2025
cda6c45
remove test logging
marcustyphoon Aug 20, 2025
edd4aaf
variable name tweak
marcustyphoon Aug 20, 2025
54e5040
Merge branch 'master' into scroll-to-bottom-caught-up
marcustyphoon Oct 25, 2025
0cffebb
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Nov 3, 2025
bca8f01
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Dec 8, 2025
d47ec62
Merge branch 'master-without-commas' into scroll-to-bottom-caught-up
marcustyphoon Feb 3, 2026
939fbfc
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Feb 3, 2026
5f41c76
format
marcustyphoon Feb 3, 2026
e88991d
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Feb 14, 2026
4f318b1
update new util function
marcustyphoon Feb 14, 2026
3fd457a
fix jsdoc
marcustyphoon Feb 14, 2026
0d37995
Merge remote-tracking branch 'upstream/master' into scroll-to-bottom-…
marcustyphoon Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/features/scroll_to_bottom/feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
49 changes: 48 additions & 1 deletion src/features/scroll_to_bottom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ 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 { cellItem } from '../../utils/react_props.js';
import { debounce } from '../../utils/debounce.js';

const scrollToBottomButtonId = 'xkit-scroll-to-bottom-button';
$(`[id="${scrollToBottomButtonId}"]`).remove();
Expand All @@ -13,6 +16,7 @@ ${keyToCss('notifications')} + div
`;
const knightRiderLoaderSelector = `:is(${loaderSelector}) > ${keyToCss('knightRiderLoader')}`;

let stopAtCaughtUp;
let scrollToBottomButton;
let active = false;

Expand Down Expand Up @@ -42,7 +46,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);
Expand Down Expand Up @@ -88,15 +94,56 @@ const addButtonToPage = async function ([scrollToTopButton]) {
pageModifications.register('*', checkForButtonRemoved);
};

const reliablyScrollToTarget = target => {
const callback = () => {
window.scrollBy({ top: target?.getBoundingClientRect?.()?.top });
debouncedDisconnect();
};
const observer = new ResizeObserver(callback);
const debouncedDisconnect = debounce(() => observer.disconnect(), 500);
observer.observe(document.documentElement);
callback();
};
Comment on lines +98 to +107
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you ever look at a piece of code and go, "okay, that very obviously needs to be refactored to be simpler and shorter" but then see no way to actually do that?


const caughtUpCarouselObjectType = 'followed_tag_carousel_card';

const scrollToCaughtUp = async (addedCells) => {
for (const cell of addedCells || [...document.querySelectorAll(keyToCss('cell'))]) {
const item = await cellItem(cell);
if (item.elements?.some(({ objectType }) => objectType === caughtUpCarouselObjectType)) {
const titleElement = cell?.previousElementSibling;
if (!titleElement) continue;

if (active) {
stopScrolling();
} else {
const titleElementTop = titleElement?.getBoundingClientRect?.()?.top;
const isAboveViewportBottom = titleElementTop !== undefined && titleElementTop < window.innerHeight;
if (isAboveViewportBottom) continue;
}

reliablyScrollToTarget(titleElement);
return true;
}
}
};

const onCellsAdded = addedCells => active && scrollToCaughtUp(addedCells);

export const main = async function () {
({ stopAtCaughtUp } = await getPreferences('scroll_to_bottom'));

pageModifications.register(`button[aria-label="${translate('Scroll to top')}"]`, addButtonToPage);
pageModifications.register(knightRiderLoaderSelector, onLoadersAdded);
stopAtCaughtUp && pageModifications.register(keyToCss(('cell')), onCellsAdded);
};

export const clean = async function () {
pageModifications.unregister(addButtonToPage);
pageModifications.unregister(checkForButtonRemoved);
pageModifications.unregister(onLoadersAdded);
pageModifications.unregister(onCellsAdded);

stopScrolling();
scrollToBottomButton?.remove();
};
14 changes: 14 additions & 0 deletions src/main_world/unbury_cell_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function unburyCellItem () {
const cellElement = this;
const reactKey = Object.keys(cellElement).find(key => key.startsWith('__reactFiber'));
let fiber = cellElement[reactKey];

while (fiber !== null) {
const { item } = fiber.memoizedProps || {};
if (item !== undefined) {
return item;
} else {
fiber = fiber.return;
}
}
}
7 changes: 7 additions & 0 deletions src/utils/debounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const debounce = (func, ms) => {
let timeoutID;
return (...args) => {
clearTimeout(timeoutID);
timeoutID = setTimeout(() => func(...args), ms);
};
};
8 changes: 8 additions & 0 deletions src/utils/react_props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>} - 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<object>} - The notification's buried notification property
Expand Down