refactor: migrate infinite scroll feature to a new architecture
This CL introduces a new architecture for the features source code.
Bug: twpowertools:176
Change-Id: I9abc4df2fb67f9bb0c9114aaffc6916d34f1b7ff
diff --git a/src/features/infiniteScroll/core/ccInfiniteScroll.js b/src/features/infiniteScroll/core/ccInfiniteScroll.js
new file mode 100644
index 0000000..93e693c
--- /dev/null
+++ b/src/features/infiniteScroll/core/ccInfiniteScroll.js
@@ -0,0 +1,89 @@
+import {getOptions, isOptionEnabled} from '../../../common/optionsUtils.js';
+
+const kInteropLoadMoreClasses = {
+ // New (interop) UI without nested replies
+ 'scTailwindThreadMorebuttonload-all': 'threadall',
+ 'scTailwindThreadMorebuttonload-more': 'thread',
+
+ // New (interop) UI with nested replies
+ 'scTailwindThreadMessagegapload-all': 'threadall',
+ 'scTailwindThreadMessagegapload-more': 'thread',
+};
+const kArtificialScrollingDelay = 3500;
+
+export default class CCInfiniteScroll {
+ constructor() {
+ this.intersectionObserver = null;
+ }
+
+ setUpIntersectionObserver(node, isScrollableContent) {
+ if (this.intersectionObserver === null) {
+ var scrollableContent = isScrollableContent ?
+ node :
+ node.querySelector('.scrollable-content');
+ if (scrollableContent !== null) {
+ let intersectionOptions = {
+ root: scrollableContent,
+ rootMargin: '0px',
+ threshold: 1.0,
+ };
+ this.intersectionObserver = new IntersectionObserver(
+ this.intersectionCallback, intersectionOptions);
+ }
+ }
+ }
+
+ intersectionCallback(entries, observer) {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ console.debug('[infinitescroll] Clicking button: ', entry.target);
+ entry.target.click();
+ }
+ });
+ }
+
+ isPotentiallyArtificialScroll() {
+ return window.location.href.includes('/message/');
+ }
+
+ observeWithPotentialDelay(node) {
+ if (this.intersectionObserver === null) {
+ console.warn(
+ '[infinitescroll] ' +
+ 'The intersectionObserver is not ready yet.');
+ return;
+ }
+
+ if (this.isPotentiallyArtificialScroll()) {
+ window.setTimeout(
+ () => {this.intersectionObserver.observe(node)},
+ kArtificialScrollingDelay);
+ } else {
+ this.intersectionObserver.observe(node);
+ }
+ }
+
+ observeLoadMoreBar(bar) {
+ getOptions(['thread', 'threadall']).then(threadOptions => {
+ if (threadOptions.thread)
+ this.observeWithPotentialDelay(bar.querySelector('.load-more-button'));
+ if (threadOptions.threadall)
+ this.observeWithPotentialDelay(bar.querySelector('.load-all-button'));
+ });
+ }
+
+ observeLoadMoreInteropBtn(btn) {
+ let parentClasses = btn.parentNode?.classList;
+ let feature = null;
+ for (const [c, f] of Object.entries(kInteropLoadMoreClasses)) {
+ if (parentClasses?.contains?.(c)) {
+ feature = f;
+ break;
+ }
+ }
+ if (feature === null) return;
+ isOptionEnabled(feature).then(isEnabled => {
+ if (isEnabled) this.observeWithPotentialDelay(btn);
+ });
+ }
+};
diff --git a/src/features/infiniteScroll/infiniteScroll.feature.ts b/src/features/infiniteScroll/infiniteScroll.feature.ts
new file mode 100644
index 0000000..b10ecfa
--- /dev/null
+++ b/src/features/infiniteScroll/infiniteScroll.feature.ts
@@ -0,0 +1,10 @@
+import Feature from '../../common/architecture/features/Feature';
+import { ConcreteScript } from '../../common/architecture/scripts/Script';
+import CCInfiniteScrollScript from './scripts/ccInfiniteScroll.script';
+
+export default class InfiniteScrollFeature extends Feature {
+ public readonly scripts: ConcreteScript[] = [CCInfiniteScrollScript];
+
+ readonly codename = 'infiniteScroll';
+ readonly relatedOptions = ['list', 'thread', 'threadall'];
+}
diff --git a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts
new file mode 100644
index 0000000..da4f5e6
--- /dev/null
+++ b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler.ts
@@ -0,0 +1,11 @@
+import { NodeMutation } from '../../../common/nodeWatcher/NodeWatcherHandler';
+import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { InfiniteScrollNodeWatcherOptions } from '../scripts/ccInfiniteScroll.script';
+
+export default class CCInfiniteScrollLoadMoreBarHandler extends CssSelectorNodeWatcherScriptHandler<InfiniteScrollNodeWatcherOptions> {
+ cssSelector = '.load-more-bar';
+
+ onMutatedNode({ node }: NodeMutation) {
+ this.options.ccInfiniteScroll.observeLoadMoreBar(node);
+ }
+}
diff --git a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts
new file mode 100644
index 0000000..ee54aff
--- /dev/null
+++ b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler.ts
@@ -0,0 +1,12 @@
+import { NodeMutation } from '../../../common/nodeWatcher/NodeWatcherHandler';
+import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { InfiniteScrollNodeWatcherOptions } from '../scripts/ccInfiniteScroll.script';
+
+export default class CCInfiniteScrollLoadMoreBtnHandler extends CssSelectorNodeWatcherScriptHandler<InfiniteScrollNodeWatcherOptions> {
+ cssSelector =
+ '.scTailwindThreadMorebuttonbutton, .scTailwindThreadMessagegapbutton';
+
+ onMutatedNode({ node }: NodeMutation) {
+ this.options.ccInfiniteScroll.observeLoadMoreInteropBtn(node);
+ }
+}
diff --git a/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollSetUp.handler.ts b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollSetUp.handler.ts
new file mode 100644
index 0000000..60331a0
--- /dev/null
+++ b/src/features/infiniteScroll/nodeWatcherHandlers/ccInfiniteScrollSetUp.handler.ts
@@ -0,0 +1,16 @@
+import { NodeMutation } from '../../../common/nodeWatcher/NodeWatcherHandler';
+import CssSelectorNodeWatcherScriptHandler from '../../../common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler';
+import { InfiniteScrollNodeWatcherOptions } from '../scripts/ccInfiniteScroll.script';
+
+export default class CCInfiniteScrollSetUpHandler extends CssSelectorNodeWatcherScriptHandler<InfiniteScrollNodeWatcherOptions> {
+ cssSelector = 'ec-app, .scrollable-content';
+
+ onMutatedNode({ node }: NodeMutation) {
+ if (!(node instanceof Element)) return;
+ const isScrollableContent = node.classList.contains('scrollable-content');
+ this.options.ccInfiniteScroll.setUpIntersectionObserver(
+ node,
+ isScrollableContent,
+ );
+ }
+}
diff --git a/src/features/infiniteScroll/scripts/ccInfiniteScroll.script.ts b/src/features/infiniteScroll/scripts/ccInfiniteScroll.script.ts
new file mode 100644
index 0000000..d37dc84
--- /dev/null
+++ b/src/features/infiniteScroll/scripts/ccInfiniteScroll.script.ts
@@ -0,0 +1,31 @@
+import {
+ ScriptEnvironment,
+ ScriptPage,
+ ScriptRunPhase,
+} from '../../../common/architecture/scripts/Script';
+import NodeWatcherScript from '../../../common/architecture/scripts/nodeWatcher/NodeWatcherScript';
+import CCInfiniteScroll from '../core/ccInfiniteScroll';
+import CCInfiniteScrollLoadMoreBarHandler from '../nodeWatcherHandlers/ccInfiniteScrollLoadMoreBar.handler';
+import CCInfiniteScrollLoadMoreBtnHandler from '../nodeWatcherHandlers/ccInfiniteScrollLoadMoreBtn.handler';
+import CCInfiniteScrollSetUpHandler from '../nodeWatcherHandlers/ccInfiniteScrollSetUp.handler';
+
+export interface InfiniteScrollNodeWatcherOptions {
+ ccInfiniteScroll: CCInfiniteScroll;
+}
+
+export default class CCInfiniteScrollScript extends NodeWatcherScript<InfiniteScrollNodeWatcherOptions> {
+ page = ScriptPage.CommunityConsole;
+ environment = ScriptEnvironment.ContentScript;
+ runPhase = ScriptRunPhase.Main;
+ handlers = new Map([
+ ['ccInfiniteScrollSetUp', CCInfiniteScrollSetUpHandler],
+ ['ccInfiniteScrollLoadMoreBar', CCInfiniteScrollLoadMoreBarHandler],
+ ['ccInfiniteScrollLoadMoreBtn', CCInfiniteScrollLoadMoreBtnHandler],
+ ]);
+
+ protected optionsFactory(): InfiniteScrollNodeWatcherOptions {
+ return {
+ ccInfiniteScroll: new CCInfiniteScroll(),
+ };
+ }
+}