nodebb-plugin-ezoic-infinite 1.5.74 → 1.5.76

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 +141 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.74",
3
+ "version": "1.5.76",
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
@@ -38,6 +38,46 @@
38
38
  // Production build: debug disabled
39
39
  function dbg() {}
40
40
 
41
+ // Ezoic (and some partner scripts) can be very noisy in console on SPA/Ajaxify setups.
42
+ // These warnings are not actionable for end-users and can flood the console.
43
+ // We selectively silence the known spam patterns while keeping other warnings intact.
44
+ function muteNoisyConsole() {
45
+ try {
46
+ if (window.__nodebbEzoicConsoleMuted) return;
47
+ window.__nodebbEzoicConsoleMuted = true;
48
+
49
+ const shouldMute = (args) => {
50
+ try {
51
+ if (!args || !args.length) return false;
52
+ const s0 = typeof args[0] === 'string' ? args[0] : '';
53
+ // Duplicate placeholder definition spam (common when reusing ids in SPA/Ajaxify).
54
+ if (s0.includes('[EzoicAds JS]: Placeholder Id') && s0.includes('has already been defined')) return true;
55
+ // Ezoic debugger iframe spam.
56
+ if (s0.includes('Debugger iframe already exists')) return true;
57
+ // Missing placeholder spam (we already guard showAds; Ezoic still logs sometimes).
58
+ if (s0.includes('HTML element with id ezoic-pub-ad-placeholder-') && s0.includes('does not exist')) return true;
59
+ return false;
60
+ } catch (e) {
61
+ return false;
62
+ }
63
+ };
64
+
65
+ const wrap = (method) => {
66
+ const orig = console[method];
67
+ if (typeof orig !== 'function') return;
68
+ console[method] = function (...args) {
69
+ if (shouldMute(args)) return;
70
+ return orig.apply(console, args);
71
+ };
72
+ };
73
+
74
+ wrap('log');
75
+ wrap('info');
76
+ wrap('warn');
77
+ wrap('error');
78
+ } catch (e) {}
79
+ }
80
+
41
81
 
42
82
 
43
83
  function isFilledNode(node) {
@@ -145,19 +185,88 @@ function tightenEzoicMinHeight(wrap) {
145
185
  function watchWrapForFill(wrap) {
146
186
  try {
147
187
  if (!wrap || wrap.__ezFillObs) return;
148
- const obs = new MutationObserver(() => {
188
+
189
+ // Ezoic can (re)apply inline styles after fill; keep tightening for a short window.
190
+ const start = now();
191
+ const tightenBurst = () => {
192
+ try { tightenEzoicMinHeight(wrap); } catch (e) {}
193
+ if (now() - start < 6000) {
194
+ setTimeout(tightenBurst, 350);
195
+ }
196
+ };
197
+
198
+ const obs = new MutationObserver((muts) => {
199
+ // If anything that looks like ad content appears, treat as filled.
149
200
  if (isFilledNode(wrap)) {
150
201
  wrap.classList.remove('is-empty');
151
- tightenEzoicMinHeight(wrap);
202
+ tightenBurst();
203
+ }
204
+
205
+ // If Ezoic changes inline style on descendants (min-height:400!important), tighten again.
206
+ for (const m of muts) {
207
+ if (m.type === 'attributes' && m.attributeName === 'style') {
208
+ try { tightenEzoicMinHeight(wrap); } catch (e) {}
209
+ break;
210
+ }
211
+ }
212
+
213
+ // Disconnect only after the burst window to avoid missing late style rewrites.
214
+ if (now() - start > 7000) {
152
215
  try { obs.disconnect(); } catch (e) {}
153
216
  wrap.__ezFillObs = null;
154
217
  }
155
218
  });
156
- obs.observe(wrap, { childList: true, subtree: true });
219
+
220
+ obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
157
221
  wrap.__ezFillObs = obs;
158
222
  } catch (e) {}
159
223
  }
160
224
 
225
+ // Global safety net: sometimes Ezoic swaps nodes in ways that bypass our per-wrap observers.
226
+ // When we see an Ezoic container with min-height:400!important inside posts/topics, shrink it.
227
+ function globalGapFixInit() {
228
+ try {
229
+ if (window.__nodebbEzoicGapFix) return;
230
+ window.__nodebbEzoicGapFix = true;
231
+
232
+ const inPostArea = (el) => {
233
+ try {
234
+ return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
235
+ } catch (e) { return false; }
236
+ };
237
+
238
+ const maybeFix = (root) => {
239
+ if (!root || !root.querySelectorAll) return;
240
+ const nodes = root.querySelectorAll('.ezoic-ad[style*="min-height"], .ezoic-ad-adaptive[style*="min-height"]');
241
+ nodes.forEach((n) => {
242
+ const st = (n.getAttribute('style') || '').toLowerCase();
243
+ if (!st.includes('min-height:400')) return;
244
+ if (!inPostArea(n)) return;
245
+ try {
246
+ const tmpWrap = n.closest('.' + WRAP_CLASS) || n.parentElement;
247
+ tightenEzoicMinHeight(tmpWrap || n);
248
+ } catch (e) {}
249
+ });
250
+ };
251
+
252
+ requestAnimationFrame(() => maybeFix(document));
253
+
254
+ const obs = new MutationObserver((muts) => {
255
+ for (const m of muts) {
256
+ const t = m.target;
257
+ if (m.type === 'attributes') {
258
+ maybeFix(t && t.nodeType === 1 ? t : t && t.parentElement);
259
+ } else if (m.addedNodes && m.addedNodes.length) {
260
+ m.addedNodes.forEach((n) => {
261
+ if (n && n.nodeType === 1) maybeFix(n);
262
+ });
263
+ }
264
+ }
265
+ });
266
+ obs.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
267
+ } catch (e) {}
268
+ }
269
+
161
270
  // ---------------- state ----------------
162
271
 
163
272
  const state = {
@@ -477,6 +586,10 @@ function watchWrapForFill(wrap) {
477
586
  wrap.setAttribute('data-ezoic-after', String(afterPos));
478
587
  wrap.setAttribute('data-ezoic-wrapid', String(id));
479
588
  wrap.setAttribute('data-created', String(now()));
589
+ // "Pinned" placements (after the first item) should remain stable.
590
+ if (afterPos === 1) {
591
+ wrap.setAttribute('data-ezoic-pin', '1');
592
+ }
480
593
  wrap.style.width = '100%';
481
594
 
482
595
  if (createPlaceholder) {
@@ -535,8 +648,9 @@ function watchWrapForFill(wrap) {
535
648
  }
536
649
 
537
650
  function pruneOrphanWraps(kindClass, items) {
538
- // On mobile topics/messages, keep wraps alive longer to avoid 'disappearing' ads.
539
- if (kindClass === 'ezoic-ad-message' && isMobile()) return 0;
651
+ // Message ads should remain stable (desktop + mobile). Pruning them can make it look like
652
+ // ads vanish on scroll and can reduce fill on long topics.
653
+ if (kindClass === 'ezoic-ad-message') return 0;
540
654
  if (!items || !items.length) return 0;
541
655
  const itemSet = new Set(items);
542
656
  const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
@@ -562,6 +676,11 @@ function watchWrapForFill(wrap) {
562
676
  };
563
677
 
564
678
  wraps.forEach((wrap) => {
679
+ // Never prune pinned placements.
680
+ try {
681
+ if (wrap.getAttribute('data-ezoic-pin') === '1') return;
682
+ } catch (e) {}
683
+
565
684
  if (isFilled(wrap)) return; // never prune filled ads
566
685
 
567
686
  // Never prune a fresh wrap: it may fill late.
@@ -601,9 +720,19 @@ function watchWrapForFill(wrap) {
601
720
 
602
721
  let removed = 0;
603
722
  for (const w of wraps) {
723
+ // Never decluster pinned placements.
724
+ try {
725
+ if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
726
+ } catch (e) {}
727
+
604
728
  let prev = w.previousElementSibling;
605
729
  for (let i = 0; i < 3 && prev; i++) {
606
730
  if (isWrap(prev)) {
731
+ // If the previous wrap is pinned, keep this one (spacing is intentional).
732
+ try {
733
+ if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
734
+ } catch (e) {}
735
+
607
736
  // Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
608
737
  // Only decluster when at least one is filled, or when the newer one is stale.
609
738
  const prevFilled = isFilled(prev);
@@ -951,6 +1080,10 @@ function watchWrapForFill(wrap) {
951
1080
 
952
1081
  state.heroDoneForPage = true;
953
1082
  observePlaceholder(id);
1083
+ // Hero placement is expected to be visible right away (after first item),
1084
+ // so kick a fill request immediately instead of waiting only for IO callbacks.
1085
+ enqueueShow(id);
1086
+ startShowQueue();
954
1087
  }
955
1088
 
956
1089
  // ---------------- scheduler ----------------
@@ -1085,8 +1218,10 @@ function watchWrapForFill(wrap) {
1085
1218
  state.pageKey = getPageKey();
1086
1219
  blockedUntil = 0;
1087
1220
 
1221
+ muteNoisyConsole();
1088
1222
  warmUpNetwork();
1089
1223
  patchShowAds();
1224
+ globalGapFixInit();
1090
1225
  ensurePreloadObserver();
1091
1226
  ensureDomObserver();
1092
1227
 
@@ -1138,6 +1273,7 @@ function watchWrapForFill(wrap) {
1138
1273
  // ---------------- boot ----------------
1139
1274
 
1140
1275
  state.pageKey = getPageKey();
1276
+ muteNoisyConsole();
1141
1277
  warmUpNetwork();
1142
1278
  patchShowAds();
1143
1279
  ensurePreloadObserver();