nodebb-plugin-ezoic-infinite 1.6.65 → 1.6.67

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +25 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.65",
3
+ "version": "1.6.67",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -79,17 +79,12 @@
79
79
  } catch (e) {}
80
80
  }
81
81
 
82
- // ─── CMP/TCF stability ──────────────────────────────────────────────────────
83
- function ensureTcfApiLocator() {
84
- try {
85
- if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
86
- if (document.getElementById('__tcfapiLocator')) return;
87
- const f = document.createElement('iframe');
88
- f.style.display = 'none';
89
- f.id = f.name = '__tcfapiLocator';
90
- (document.body || document.documentElement).appendChild(f);
91
- } catch (e) {}
92
- }
82
+ // ensureTcfApiLocator intentionally removed:
83
+ // Creating our own __tcfapiLocator iframe conflicts with the CMP's own locator
84
+ // management and causes "Cannot read properties of null (reading 'postMessage')"
85
+ // errors when the CMP tries to postMessage to an iframe we created then navigated away.
86
+ // The CMP (Ezoic/IAB TCF) handles its own locator iframe lifecycle.
87
+ function ensureTcfApiLocator() { /* no-op */ }
93
88
 
94
89
  // ─── Ezoic min-height tightener ─────────────────────────────────────────────
95
90
  // Ezoic injects `min-height:400px !important` on nested wrappers via inline
@@ -468,10 +463,8 @@
468
463
  // ─── Orphan / cluster management ────────────────────────────────────────────
469
464
  function pruneOrphanWraps(kindClass, items) {
470
465
  if (!items || !items.length) return 0;
471
- const itemSet = new Set(items);
472
- const isMessage = kindClass === 'ezoic-ad-message';
473
- // Between/category ads are NEVER fully released — only hidden/shown.
474
- // Releasing frees IDs into the pool → re-injection at wrong positions.
466
+ const itemSet = new Set(items);
467
+ const isMessage = kindClass === 'ezoic-ad-message';
475
468
  const allowRelease = isMessage;
476
469
  let removed = 0;
477
470
 
@@ -495,19 +488,26 @@
495
488
 
496
489
  document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
497
490
  if (wrap.getAttribute('data-ezoic-pin') === '1') return;
498
- const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
499
- if (created && (now() - created) < keepEmptyWrapMs()) return;
500
491
 
501
492
  if (hasNearbyItem(wrap)) {
493
+ // Anchor is in the DOM → always restore visibility, regardless of age.
502
494
  try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
503
495
  return;
504
496
  }
505
497
 
506
- // Anchor gone → hide.
498
+ // Anchor is NOT in the DOM always hide immediately, regardless of age.
499
+ // KEY FIX: the previous code skipped this for fresh wraps (< keepEmptyWrapMs).
500
+ // That meant a wrap whose anchor topic was virtualized away within the first
501
+ // 60-120s would stay visible and float to the top of the list.
507
502
  try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
503
+
504
+ // Release (message-ads only): only after the freshness window, and only
505
+ // when far off-screen — we still want late-filling ads to have a chance.
508
506
  if (!allowRelease) return;
509
507
 
510
- // Message-ads only: release when far off-screen.
508
+ const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
509
+ if (created && (now() - created) < keepEmptyWrapMs()) return;
510
+
511
511
  try {
512
512
  const r = wrap.getBoundingClientRect();
513
513
  const vh = Math.max(1, window.innerHeight || 1);
@@ -909,23 +909,18 @@
909
909
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
910
910
  let inserted = 0;
911
911
 
912
- // Viewport top — we never inject after an element that is fully above this.
913
- // This is the definitive guard against NodeBB loading content above the fold
914
- // and our scanner immediately injecting ads on those newly-loaded top items.
915
- // We use a generous negative margin so items just barely scrolled above still
916
- // get ads injected (they'll be visible when the user scrolls back a little).
917
- const viewportSafeTop = -(window.innerHeight || 800) * 0.5;
918
-
919
912
  for (const afterPos of targets) {
920
913
  if (inserted >= maxInserts) break;
921
914
  const el = ordinalMap.get(afterPos);
922
915
  if (!el || !el.isConnected) continue;
923
916
 
924
- // Skip elements that are well above the viewport.
925
- // getBoundingClientRect is cheap on modern engines (no forced layout).
917
+ // Never inject after an element that is fully above the viewport top.
918
+ // This is the definitive guard: NodeBB loads items above the fold when the
919
+ // user scrolls up. Those items have rect.bottom < 0 at the moment of load.
920
+ // Any negative margin ("a little above is ok") still causes the pile-up
921
+ // because NodeBB loads a whole batch just above — we must use exactly 0.
926
922
  try {
927
- const rect = el.getBoundingClientRect();
928
- if (rect.bottom < viewportSafeTop) continue;
923
+ if (el.getBoundingClientRect().bottom < 0) continue;
929
924
  } catch (e) {}
930
925
 
931
926
  if (isAdjacentAd(el)) continue;