nodebb-plugin-ezoic-infinite 1.5.80 → 1.5.82

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.80",
3
+ "version": "1.5.82",
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
@@ -704,9 +704,10 @@ function globalGapFixInit() {
704
704
  }
705
705
 
706
706
  function pruneOrphanWraps(kindClass, items) {
707
- // Message ads should remain stable (desktop + mobile). Pruning them can make it look like
708
- // ads vanish on scroll and can reduce fill on long topics.
709
- if (kindClass === 'ezoic-ad-message') return 0;
707
+ // Topic pages can be virtualized (posts removed from DOM as you scroll).
708
+ // When that happens, previously-inserted ad wraps may become "orphan" nodes with no
709
+ // nearby post containers, which leads to ads clustering together when scrolling back up.
710
+ // We prune only *true* orphans that are far offscreen to keep the UI stable.
710
711
  if (!items || !items.length) return 0;
711
712
  const itemSet = new Set(items);
712
713
  const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
@@ -737,7 +738,10 @@ function globalGapFixInit() {
737
738
  if (wrap.getAttribute('data-ezoic-pin') === '1') return;
738
739
  } catch (e) {}
739
740
 
740
- if (isFilled(wrap)) return; // never prune filled ads
741
+ // For message/topic pages we may prune filled or empty orphans if they are far away,
742
+ // otherwise consecutive "stacks" can appear when posts are virtualized.
743
+ const isMessage = (kindClass === 'ezoic-ad-message');
744
+ if (!isMessage && isFilled(wrap)) return; // never prune filled ads for non-message lists
741
745
 
742
746
  // Never prune a fresh wrap: it may fill late.
743
747
  try {
@@ -745,10 +749,30 @@ function globalGapFixInit() {
745
749
  if (created && (now() - created) < keepEmptyWrapMs()) return;
746
750
  } catch (e) {}
747
751
 
748
- if (hasNearbyItem(wrap)) return;
752
+ if (hasNearbyItem(wrap)) {
753
+ try { wrap.classList && wrap.classList.remove('ez-orphan-hidden'); wrap.style && (wrap.style.display = ''); } catch (e) {}
754
+ return;
755
+ }
756
+
757
+ // If the anchor item is no longer in the DOM (virtualized), hide the wrap so ads never "stack"
758
+ // back-to-back while scrolling. We'll recycle it when its anchor comes back.
759
+ try { wrap.classList && wrap.classList.add('ez-orphan-hidden'); wrap.style && (wrap.style.display = 'none'); } catch (e) {}
749
760
 
750
- withInternalDomChange(() => releaseWrapNode(wrap));
751
- removed++;
761
+ // For message ads: only release if far offscreen to avoid perceived "vanishing" during fast scroll.
762
+ if (isMessage) {
763
+ try {
764
+ const r = wrap.getBoundingClientRect();
765
+ const vh = Math.max(1, window.innerHeight || 1);
766
+ const farAbove = r.bottom < -vh * 2;
767
+ const farBelow = r.top > vh * 4;
768
+ if (!farAbove && !farBelow) return;
769
+ } catch (e) {
770
+ return;
771
+ }
772
+ }
773
+
774
+ withInternalDomChange(() => releaseWrapNode(wrap));
775
+ removed++;
752
776
  });
753
777
 
754
778
  return removed;
@@ -1013,6 +1037,36 @@ function globalGapFixInit() {
1013
1037
 
1014
1038
  // ---------------- core injection ----------------
1015
1039
 
1040
+ function getItemOrdinal(el, fallbackIndex) {
1041
+ try {
1042
+ if (!el) return fallbackIndex + 1;
1043
+ const di = el.getAttribute('data-index') || (el.dataset && (el.dataset.index || el.dataset.postIndex));
1044
+ if (di !== null && di !== undefined && di !== '' && !isNaN(di)) {
1045
+ const n = parseInt(di, 10);
1046
+ if (Number.isFinite(n) && n >= 0) return n + 1;
1047
+ }
1048
+ const d1 = el.getAttribute('data-idx') || el.getAttribute('data-position') || (el.dataset && (el.dataset.idx || el.dataset.position));
1049
+ if (d1 !== null && d1 !== undefined && d1 !== '' && !isNaN(d1)) {
1050
+ const n = parseInt(d1, 10);
1051
+ if (Number.isFinite(n) && n > 0) return n;
1052
+ }
1053
+ } catch (e) {}
1054
+ return fallbackIndex + 1;
1055
+ }
1056
+
1057
+ function buildOrdinalMap(items) {
1058
+ const map = new Map();
1059
+ let max = 0;
1060
+ for (let i = 0; i < items.length; i++) {
1061
+ const el = items[i];
1062
+ const ord = getItemOrdinal(el, i);
1063
+ map.set(ord, el);
1064
+ if (ord > max) max = ord;
1065
+ }
1066
+ return { map, max };
1067
+ }
1068
+
1069
+
1016
1070
  function computeTargets(count, interval, showFirst) {
1017
1071
  const out = [];
1018
1072
  if (count <= 0) return out;
@@ -1027,14 +1081,15 @@ function globalGapFixInit() {
1027
1081
  function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
1028
1082
  if (!items.length) return 0;
1029
1083
 
1030
- const targets = computeTargets(items.length, interval, showFirst);
1084
+ const { map: ordinalMap, max: maxOrdinal } = buildOrdinalMap(items);
1085
+ const targets = computeTargets(maxOrdinal, interval, showFirst);
1031
1086
  let inserted = 0;
1032
1087
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
1033
1088
 
1034
1089
  for (const afterPos of targets) {
1035
1090
  if (inserted >= maxInserts) break;
1036
-
1037
- const el = items[afterPos - 1];
1091
+ const el = ordinalMap.get(afterPos);
1092
+ if (!el) continue;
1038
1093
  if (!el || !el.isConnected) continue;
1039
1094
  if (isAdjacentAd(el)) continue;
1040
1095
  if (findWrap(kindClass, afterPos)) continue;
@@ -0,0 +1 @@
1
+ hi