nodebb-plugin-ezoic-infinite 1.5.32 → 1.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/client.js +167 -59
- package/public/style.css +5 -36
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -14,19 +14,79 @@
|
|
|
14
14
|
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
15
15
|
const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
|
|
16
16
|
|
|
17
|
+
// When the user scrolls very fast, temporarily preload more aggressively.
|
|
18
|
+
// This helps ensure ads are already in-flight before the user reaches them.
|
|
19
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '4200px 0px 4200px 0px';
|
|
20
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '2500px 0px 2500px 0px';
|
|
21
|
+
const BOOST_DURATION_MS = 2500;
|
|
22
|
+
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
23
|
+
|
|
17
24
|
const MAX_INFLIGHT_DESKTOP = 4;
|
|
18
25
|
const MAX_INFLIGHT_MOBILE = 3;
|
|
19
26
|
|
|
27
|
+
|
|
28
|
+
// Adaptive performance profile (device/network aware)
|
|
29
|
+
const PERF_DEFAULTS = Object.freeze({
|
|
30
|
+
maxInflightDesktop: MAX_INFLIGHT_DESKTOP,
|
|
31
|
+
maxInflightMobile: MAX_INFLIGHT_MOBILE,
|
|
32
|
+
maxInsertsPerRun: MAX_INSERTS_PER_RUN,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function getPerfProfile() {
|
|
36
|
+
// Cache result for this pageview; recomputed on navigation via cleanup()
|
|
37
|
+
if (state.perfProfile) return state.perfProfile;
|
|
38
|
+
const p = {
|
|
39
|
+
maxInflightDesktop: PERF_DEFAULTS.maxInflightDesktop,
|
|
40
|
+
maxInflightMobile: PERF_DEFAULTS.maxInflightMobile,
|
|
41
|
+
maxInsertsPerRun: PERF_DEFAULTS.maxInsertsPerRun,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const mem = typeof navigator !== 'undefined' ? navigator.deviceMemory : undefined; // GB
|
|
46
|
+
const cores = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined;
|
|
47
|
+
|
|
48
|
+
// Conservative tuning for low-end devices
|
|
49
|
+
if (typeof mem === 'number' && mem > 0 && mem <= 2) {
|
|
50
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
51
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
52
|
+
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
53
|
+
}
|
|
54
|
+
if (typeof cores === 'number' && cores > 0 && cores <= 4) {
|
|
55
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
56
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
57
|
+
p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const conn = typeof navigator !== 'undefined' ? navigator.connection : undefined;
|
|
61
|
+
const eff = conn && conn.effectiveType ? String(conn.effectiveType).toLowerCase() : '';
|
|
62
|
+
// Slow connections: don't ramp concurrency too high (keeps page responsive).
|
|
63
|
+
if (eff.includes('2g')) {
|
|
64
|
+
p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
|
|
65
|
+
p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
|
|
69
|
+
state.perfProfile = p;
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isBoosted() {
|
|
74
|
+
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
20
77
|
function isMobile() {
|
|
21
78
|
try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
|
|
22
79
|
}
|
|
23
80
|
|
|
24
81
|
function getPreloadRootMargin() {
|
|
25
|
-
|
|
82
|
+
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
83
|
+
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
26
84
|
}
|
|
27
85
|
|
|
28
86
|
function getMaxInflight() {
|
|
29
|
-
|
|
87
|
+
const perf = getPerfProfile();
|
|
88
|
+
const base = isMobile() ? perf.maxInflightMobile : perf.maxInflightDesktop;
|
|
89
|
+
return base + (isBoosted() ? 1 : 0);
|
|
30
90
|
}
|
|
31
91
|
|
|
32
92
|
const SELECTORS = {
|
|
@@ -35,6 +95,31 @@
|
|
|
35
95
|
categoryItem: 'li[component="categories/category"]',
|
|
36
96
|
};
|
|
37
97
|
|
|
98
|
+
const RELEVANT_MATCHERS = [
|
|
99
|
+
SELECTORS.postItem,
|
|
100
|
+
SELECTORS.topicItem,
|
|
101
|
+
SELECTORS.categoryItem,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
function mutationHasRelevantAddedNodes(mutations) {
|
|
105
|
+
try {
|
|
106
|
+
for (const m of mutations) {
|
|
107
|
+
if (!m || !m.addedNodes || !m.addedNodes.length) continue;
|
|
108
|
+
for (const n of m.addedNodes) {
|
|
109
|
+
if (!n || n.nodeType !== 1) continue;
|
|
110
|
+
const el = /** @type {Element} */ (n);
|
|
111
|
+
for (const sel of RELEVANT_MATCHERS) {
|
|
112
|
+
if (el.matches && el.matches(sel)) return true;
|
|
113
|
+
if (el.querySelector && el.querySelector(sel)) return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
38
123
|
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
39
124
|
let blockedUntil = 0;
|
|
40
125
|
function isBlocked() {
|
|
@@ -71,35 +156,17 @@
|
|
|
71
156
|
pending: [],
|
|
72
157
|
pendingSet: new Set(),
|
|
73
158
|
|
|
159
|
+
// fast scroll boosting
|
|
160
|
+
scrollBoostUntil: 0,
|
|
161
|
+
lastScrollY: 0,
|
|
162
|
+
lastScrollTs: 0,
|
|
163
|
+
ioMargin: null,
|
|
164
|
+
|
|
74
165
|
// hero)
|
|
75
166
|
heroDoneForPage: false,
|
|
76
167
|
};
|
|
77
168
|
|
|
78
169
|
const insertingIds = new Set();
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function markEmptyWrapper(id) {
|
|
83
|
-
try {
|
|
84
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
85
|
-
if (!ph || !ph.isConnected) return;
|
|
86
|
-
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
87
|
-
if (!wrap) return;
|
|
88
|
-
// If still empty after a delay, collapse it.
|
|
89
|
-
setTimeout(() => {
|
|
90
|
-
try {
|
|
91
|
-
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
92
|
-
if (!ph2 || !ph2.isConnected) return;
|
|
93
|
-
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
94
|
-
if (!w2) return;
|
|
95
|
-
// consider empty if only whitespace and no iframes/ins/img
|
|
96
|
-
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
97
|
-
if (!hasAd) w2.classList.add('is-empty');
|
|
98
|
-
} catch (e) {}
|
|
99
|
-
}, 3500);
|
|
100
|
-
} catch (e) {}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
170
|
// Production build: debug disabled
|
|
104
171
|
function dbg() {}
|
|
105
172
|
|
|
@@ -346,22 +413,6 @@ function withInternalDomChange(fn) {
|
|
|
346
413
|
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
347
414
|
return removed;
|
|
348
415
|
}
|
|
349
|
-
|
|
350
|
-
function refreshEmptyState(id) {
|
|
351
|
-
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
352
|
-
window.setTimeout(() => {
|
|
353
|
-
try {
|
|
354
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
355
|
-
if (!ph || !ph.isConnected) return;
|
|
356
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
357
|
-
if (!wrap) return;
|
|
358
|
-
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
359
|
-
if (hasContent) wrap.classList.remove('is-empty');
|
|
360
|
-
else wrap.classList.add('is-empty');
|
|
361
|
-
} catch (e) {}
|
|
362
|
-
}, 3500);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
416
|
function buildWrap(id, kindClass, afterPos) {
|
|
366
417
|
const wrap = document.createElement('div');
|
|
367
418
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
@@ -432,13 +483,20 @@ function buildWrap(id, kindClass, afterPos) {
|
|
|
432
483
|
// Otherwise remove the earliest one in the document
|
|
433
484
|
if (!victim) victim = wraps[0];
|
|
434
485
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
486
|
+
const id = getWrapIdFromWrap(victim);
|
|
487
|
+
|
|
488
|
+
withInternalDomChange(() => {
|
|
489
|
+
// Unobserve placeholder if still observed
|
|
490
|
+
try {
|
|
491
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
492
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
493
|
+
} catch (e) {}
|
|
494
|
+
|
|
495
|
+
try { if (id) safeDestroyById(id); } catch (e) {}
|
|
496
|
+
|
|
497
|
+
try { victim.remove(); } catch (e) {}
|
|
498
|
+
});
|
|
440
499
|
|
|
441
|
-
victim.remove();
|
|
442
500
|
return true;
|
|
443
501
|
} catch (e) {
|
|
444
502
|
return false;
|
|
@@ -505,14 +563,13 @@ function startShow(id) {
|
|
|
505
563
|
|
|
506
564
|
const doShow = () => {
|
|
507
565
|
try {
|
|
508
|
-
if (state.usedOnce && state.usedOnce.has(id)
|
|
509
|
-
|
|
566
|
+
if (state.usedOnce && state.usedOnce.has(id)) {
|
|
567
|
+
safeDestroyById(id);
|
|
510
568
|
}
|
|
511
569
|
} catch (e) {}
|
|
512
570
|
|
|
513
571
|
try { ez.showAds(id); } catch (e) {}
|
|
514
572
|
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
515
|
-
try { markEmptyWrapper(id); } catch (e) {}
|
|
516
573
|
|
|
517
574
|
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
518
575
|
};
|
|
@@ -532,7 +589,14 @@ function startShow(id) {
|
|
|
532
589
|
// ---------- preload / above-the-fold ----------
|
|
533
590
|
|
|
534
591
|
function ensurePreloadObserver() {
|
|
535
|
-
|
|
592
|
+
const desiredMargin = getPreloadRootMargin();
|
|
593
|
+
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
594
|
+
|
|
595
|
+
// Rebuild IO if margin changed (e.g., scroll boost toggled)
|
|
596
|
+
if (state.io) {
|
|
597
|
+
try { state.io.disconnect(); } catch (e) {}
|
|
598
|
+
state.io = null;
|
|
599
|
+
}
|
|
536
600
|
try {
|
|
537
601
|
state.io = new IntersectionObserver((entries) => {
|
|
538
602
|
for (const ent of entries) {
|
|
@@ -544,10 +608,20 @@ function startShow(id) {
|
|
|
544
608
|
const id = parseInt(idAttr, 10);
|
|
545
609
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
546
610
|
}
|
|
547
|
-
}, { root: null, rootMargin:
|
|
611
|
+
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
612
|
+
state.ioMargin = desiredMargin;
|
|
548
613
|
} catch (e) {
|
|
549
614
|
state.io = null;
|
|
615
|
+
state.ioMargin = null;
|
|
550
616
|
}
|
|
617
|
+
|
|
618
|
+
// If we rebuilt the observer, re-observe existing placeholders so we don't miss them.
|
|
619
|
+
try {
|
|
620
|
+
if (state.io) {
|
|
621
|
+
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
622
|
+
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {}
|
|
551
625
|
return state.io;
|
|
552
626
|
}
|
|
553
627
|
|
|
@@ -560,7 +634,9 @@ function startShow(id) {
|
|
|
560
634
|
// If already above fold, fire immediately
|
|
561
635
|
try {
|
|
562
636
|
const r = ph.getBoundingClientRect();
|
|
563
|
-
|
|
637
|
+
const screens = isBoosted() ? 5.0 : 3.0;
|
|
638
|
+
const minBottom = isBoosted() ? -1500 : -800;
|
|
639
|
+
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
564
640
|
} catch (e) {}
|
|
565
641
|
}
|
|
566
642
|
|
|
@@ -581,9 +657,11 @@ function startShow(id) {
|
|
|
581
657
|
|
|
582
658
|
const targets = computeTargets(items.length, interval, showFirst);
|
|
583
659
|
let inserted = 0;
|
|
660
|
+
const perf = getPerfProfile();
|
|
661
|
+
const maxInserts = perf.maxInsertsPerRun + (isBoosted() ? 1 : 0);
|
|
584
662
|
|
|
585
663
|
for (const afterPos of targets) {
|
|
586
|
-
if (inserted >=
|
|
664
|
+
if (inserted >= maxInserts) break;
|
|
587
665
|
|
|
588
666
|
const el = items[afterPos - 1];
|
|
589
667
|
if (!el || !el.isConnected) continue;
|
|
@@ -749,11 +827,16 @@ function startShow(id) {
|
|
|
749
827
|
|
|
750
828
|
// remove all wrappers
|
|
751
829
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
830
|
+
withInternalDomChange(() => {
|
|
831
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
832
|
+
try { el.remove(); } catch (e) {}
|
|
833
|
+
});
|
|
754
834
|
});
|
|
755
835
|
} catch (e) {}
|
|
756
836
|
|
|
837
|
+
// reset perf profile cache
|
|
838
|
+
state.perfProfile = null;
|
|
839
|
+
|
|
757
840
|
// reset state
|
|
758
841
|
state.cfg = null;
|
|
759
842
|
state.allTopics = [];
|
|
@@ -774,9 +857,11 @@ function startShow(id) {
|
|
|
774
857
|
|
|
775
858
|
function ensureDomObserver() {
|
|
776
859
|
if (state.domObs) return;
|
|
777
|
-
state.domObs = new MutationObserver(() => {
|
|
860
|
+
state.domObs = new MutationObserver((mutations) => {
|
|
778
861
|
if (state.internalDomChange > 0) return;
|
|
779
|
-
if (
|
|
862
|
+
if (isBlocked()) return;
|
|
863
|
+
// Only rescan when NodeBB actually added posts/topics/categories.
|
|
864
|
+
if (mutationHasRelevantAddedNodes(mutations)) scheduleRun();
|
|
780
865
|
});
|
|
781
866
|
try {
|
|
782
867
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -818,6 +903,29 @@ function startShow(id) {
|
|
|
818
903
|
function bindScroll() {
|
|
819
904
|
let ticking = false;
|
|
820
905
|
window.addEventListener('scroll', () => {
|
|
906
|
+
// Detect very fast scrolling and temporarily boost preload/parallelism.
|
|
907
|
+
try {
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
910
|
+
if (state.lastScrollTs) {
|
|
911
|
+
const dt = now - state.lastScrollTs;
|
|
912
|
+
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
913
|
+
if (dt > 0) {
|
|
914
|
+
const speed = dy / dt; // px/ms
|
|
915
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
916
|
+
const wasBoosted = isBoosted();
|
|
917
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, now + BOOST_DURATION_MS);
|
|
918
|
+
if (!wasBoosted) {
|
|
919
|
+
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
920
|
+
ensurePreloadObserver();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
state.lastScrollY = y;
|
|
926
|
+
state.lastScrollTs = now;
|
|
927
|
+
} catch (e) {}
|
|
928
|
+
|
|
821
929
|
if (ticking) return;
|
|
822
930
|
ticking = true;
|
|
823
931
|
window.requestAnimationFrame(() => {
|
package/public/style.css
CHANGED
|
@@ -1,40 +1,9 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Minimal styling for injected Ezoic wrappers.
|
|
2
|
+
Spacing (margins/padding) is intentionally NOT forced here, because it can be
|
|
3
|
+
configured inside Ezoic and may vary by placement/device.
|
|
4
|
+
*/
|
|
5
|
+
|
|
2
6
|
.ezoic-ad {
|
|
3
7
|
display: block;
|
|
4
8
|
width: 100%;
|
|
5
|
-
margin: 0 !important;
|
|
6
|
-
padding: 0 !important;
|
|
7
|
-
overflow: hidden;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
11
|
-
margin: 0 !important;
|
|
12
|
-
padding: 0 !important;
|
|
13
|
-
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
-
.ezoic-ad span.ezoic-ad,
|
|
18
|
-
.ezoic-ad .ezoic-ad {
|
|
19
|
-
margin: 0 !important;
|
|
20
|
-
padding: 0 !important;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
-
.ezoic-ad.is-empty {
|
|
26
|
-
display: block !important;
|
|
27
|
-
margin: 0 !important;
|
|
28
|
-
padding: 0 !important;
|
|
29
|
-
height: 0 !important;
|
|
30
|
-
min-height: 0 !important;
|
|
31
|
-
overflow: hidden !important;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.ezoic-ad {
|
|
35
|
-
min-height: 0 !important;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
39
|
-
min-height: 0 !important;
|
|
40
9
|
}
|