nodebb-plugin-ezoic-infinite 1.6.63 → 1.6.65

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 +23 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.63",
3
+ "version": "1.6.65",
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
@@ -401,6 +401,7 @@
401
401
  }
402
402
 
403
403
  // ─── Insertion primitives ───────────────────────────────────────────────────
404
+
404
405
  function buildWrap(id, kindClass, afterPos, createPlaceholder) {
405
406
  const wrap = document.createElement('div');
406
407
  wrap.className = WRAP_CLASS + ' ' + kindClass;
@@ -469,23 +470,16 @@
469
470
  if (!items || !items.length) return 0;
470
471
  const itemSet = new Set(items);
471
472
  const isMessage = kindClass === 'ezoic-ad-message';
472
- // Between-ads and category-ads are NEVER fully released — only hidden/shown.
473
- // Releasing them frees their placeholder IDs back into the pool. When the
474
- // anchor topics return (scroll-up / NodeBB re-render), injectBetween would
475
- // re-insert those IDs, but DOM ordinals have shifted → all wraps pile at top.
476
- // Keeping the wrap node in DOM at its original position means findWrap() will
477
- // always find it and injectBetween() will never create a duplicate.
473
+ // Between/category ads are NEVER fully released — only hidden/shown.
474
+ // Releasing frees IDs into the pool re-injection at wrong positions.
478
475
  const allowRelease = isMessage;
479
476
  let removed = 0;
480
477
 
481
478
  const hasNearbyItem = (wrap) => {
482
- // If the wrap is inside a li.nodebb-ezoic-host, we must check the HOST's
483
- // siblings, not the wrap's own siblings (the wrap has no siblings inside host).
479
+ // If wrap is inside a li.nodebb-ezoic-host, check the HOST's siblings.
484
480
  const pivot = (wrap.parentElement && wrap.parentElement.classList &&
485
481
  wrap.parentElement.classList.contains(HOST_CLASS))
486
- ? wrap.parentElement
487
- : wrap;
488
-
482
+ ? wrap.parentElement : wrap;
489
483
  let prev = pivot.previousElementSibling;
490
484
  for (let i = 0; i < 14 && prev; i++) {
491
485
  if (itemSet.has(prev)) return true;
@@ -500,31 +494,24 @@
500
494
  };
501
495
 
502
496
  document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
503
- // Never touch pinned placements.
504
497
  if (wrap.getAttribute('data-ezoic-pin') === '1') return;
505
-
506
- // Never prune very fresh wraps (slow auction / CMP fills).
507
498
  const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
508
499
  if (created && (now() - created) < keepEmptyWrapMs()) return;
509
500
 
510
501
  if (hasNearbyItem(wrap)) {
511
- // Anchor back in DOM → restore visibility.
512
502
  try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
513
503
  return;
514
504
  }
515
505
 
516
- // Anchor is gone (virtualized) → hide so ads don't stack visually.
506
+ // Anchor gone → hide.
517
507
  try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
518
-
519
- // For between/category ads: stop here — never release the node.
520
- // The wrap stays in DOM (hidden) at its correct data-ezoic-after position.
521
508
  if (!allowRelease) return;
522
509
 
523
- // For message-ads only: fully release when far off-screen.
510
+ // Message-ads only: release when far off-screen.
524
511
  try {
525
512
  const r = wrap.getBoundingClientRect();
526
513
  const vh = Math.max(1, window.innerHeight || 1);
527
- if (r.bottom > -vh * 2 && r.top < vh * 4) return; // still near viewport
514
+ if (r.bottom > -vh * 2 && r.top < vh * 4) return;
528
515
  } catch (e) { return; }
529
516
 
530
517
  withInternalDomChange(() => releaseWrapNode(wrap));
@@ -922,10 +909,25 @@
922
909
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
923
910
  let inserted = 0;
924
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
+
925
919
  for (const afterPos of targets) {
926
920
  if (inserted >= maxInserts) break;
927
921
  const el = ordinalMap.get(afterPos);
928
922
  if (!el || !el.isConnected) continue;
923
+
924
+ // Skip elements that are well above the viewport.
925
+ // getBoundingClientRect is cheap on modern engines (no forced layout).
926
+ try {
927
+ const rect = el.getBoundingClientRect();
928
+ if (rect.bottom < viewportSafeTop) continue;
929
+ } catch (e) {}
930
+
929
931
  if (isAdjacentAd(el)) continue;
930
932
  if (findWrap(kindClass, afterPos)) continue;
931
933