nodebb-plugin-ezoic-infinite 1.5.75 → 1.5.77

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 +94 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.75",
3
+ "version": "1.5.77",
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) {
@@ -95,11 +135,8 @@ function tightenEzoicMinHeight(wrap) {
95
135
  }
96
136
  if (!maxBottom) return;
97
137
 
98
- // Account for the Ezoic badge (reportline) which is absolutely positioned.
99
- const report = refNode.querySelector('.reportline');
100
- if (report && report.offsetHeight) {
101
- maxBottom += report.offsetHeight;
102
- }
138
+ // NOTE: Do NOT add the Ezoic badge (reportline) height here.
139
+ // It is absolutely positioned and should not reserve layout space.
103
140
 
104
141
  const h = Math.max(1, Math.ceil(maxBottom));
105
142
 
@@ -189,6 +226,9 @@ function globalGapFixInit() {
189
226
  if (window.__nodebbEzoicGapFix) return;
190
227
  window.__nodebbEzoicGapFix = true;
191
228
 
229
+ // Observe only the main content area to minimize overhead.
230
+ const root = document.getElementById('content') || document.querySelector('[component="content"], #panel') || document.body;
231
+
192
232
  const inPostArea = (el) => {
193
233
  try {
194
234
  return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
@@ -209,21 +249,37 @@ function globalGapFixInit() {
209
249
  });
210
250
  };
211
251
 
212
- requestAnimationFrame(() => maybeFix(document));
252
+ requestAnimationFrame(() => maybeFix(root));
253
+
254
+ // Batch DOM mutation processing into a single rAF to avoid doing work per mutation.
255
+ const pending = new Set();
256
+ let scheduled = false;
257
+ const scheduleFlush = () => {
258
+ if (scheduled) return;
259
+ scheduled = true;
260
+ requestAnimationFrame(() => {
261
+ scheduled = false;
262
+ pending.forEach((n) => {
263
+ try { maybeFix(n); } catch (e) {}
264
+ });
265
+ pending.clear();
266
+ });
267
+ };
213
268
 
214
269
  const obs = new MutationObserver((muts) => {
215
270
  for (const m of muts) {
216
- const t = m.target;
217
271
  if (m.type === 'attributes') {
218
- maybeFix(t && t.nodeType === 1 ? t : t && t.parentElement);
272
+ const t = m.target && m.target.nodeType === 1 ? m.target : m.target && m.target.parentElement;
273
+ if (t) pending.add(t);
219
274
  } else if (m.addedNodes && m.addedNodes.length) {
220
275
  m.addedNodes.forEach((n) => {
221
- if (n && n.nodeType === 1) maybeFix(n);
276
+ if (n && n.nodeType === 1) pending.add(n);
222
277
  });
223
278
  }
224
279
  }
280
+ if (pending.size) scheduleFlush();
225
281
  });
226
- obs.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
282
+ obs.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
227
283
  } catch (e) {}
228
284
  }
229
285
 
@@ -546,6 +602,10 @@ function globalGapFixInit() {
546
602
  wrap.setAttribute('data-ezoic-after', String(afterPos));
547
603
  wrap.setAttribute('data-ezoic-wrapid', String(id));
548
604
  wrap.setAttribute('data-created', String(now()));
605
+ // "Pinned" placements (after the first item) should remain stable.
606
+ if (afterPos === 1) {
607
+ wrap.setAttribute('data-ezoic-pin', '1');
608
+ }
549
609
  wrap.style.width = '100%';
550
610
 
551
611
  if (createPlaceholder) {
@@ -604,8 +664,9 @@ function globalGapFixInit() {
604
664
  }
605
665
 
606
666
  function pruneOrphanWraps(kindClass, items) {
607
- // On mobile topics/messages, keep wraps alive longer to avoid 'disappearing' ads.
608
- if (kindClass === 'ezoic-ad-message' && isMobile()) return 0;
667
+ // Message ads should remain stable (desktop + mobile). Pruning them can make it look like
668
+ // ads vanish on scroll and can reduce fill on long topics.
669
+ if (kindClass === 'ezoic-ad-message') return 0;
609
670
  if (!items || !items.length) return 0;
610
671
  const itemSet = new Set(items);
611
672
  const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
@@ -631,6 +692,11 @@ function globalGapFixInit() {
631
692
  };
632
693
 
633
694
  wraps.forEach((wrap) => {
695
+ // Never prune pinned placements.
696
+ try {
697
+ if (wrap.getAttribute('data-ezoic-pin') === '1') return;
698
+ } catch (e) {}
699
+
634
700
  if (isFilled(wrap)) return; // never prune filled ads
635
701
 
636
702
  // Never prune a fresh wrap: it may fill late.
@@ -670,9 +736,19 @@ function globalGapFixInit() {
670
736
 
671
737
  let removed = 0;
672
738
  for (const w of wraps) {
739
+ // Never decluster pinned placements.
740
+ try {
741
+ if (w.getAttribute && w.getAttribute('data-ezoic-pin') === '1') continue;
742
+ } catch (e) {}
743
+
673
744
  let prev = w.previousElementSibling;
674
745
  for (let i = 0; i < 3 && prev; i++) {
675
746
  if (isWrap(prev)) {
747
+ // If the previous wrap is pinned, keep this one (spacing is intentional).
748
+ try {
749
+ if (prev.getAttribute && prev.getAttribute('data-ezoic-pin') === '1') break;
750
+ } catch (e) {}
751
+
676
752
  // Don't decluster two "fresh" empty wraps — it can reduce fill on slow auctions.
677
753
  // Only decluster when at least one is filled, or when the newer one is stale.
678
754
  const prevFilled = isFilled(prev);
@@ -1020,6 +1096,10 @@ function globalGapFixInit() {
1020
1096
 
1021
1097
  state.heroDoneForPage = true;
1022
1098
  observePlaceholder(id);
1099
+ // Hero placement is expected to be visible right away (after first item),
1100
+ // so kick a fill request immediately instead of waiting only for IO callbacks.
1101
+ enqueueShow(id);
1102
+ startShowQueue();
1023
1103
  }
1024
1104
 
1025
1105
  // ---------------- scheduler ----------------
@@ -1154,6 +1234,7 @@ function globalGapFixInit() {
1154
1234
  state.pageKey = getPageKey();
1155
1235
  blockedUntil = 0;
1156
1236
 
1237
+ muteNoisyConsole();
1157
1238
  warmUpNetwork();
1158
1239
  patchShowAds();
1159
1240
  globalGapFixInit();
@@ -1208,6 +1289,7 @@ function globalGapFixInit() {
1208
1289
  // ---------------- boot ----------------
1209
1290
 
1210
1291
  state.pageKey = getPageKey();
1292
+ muteNoisyConsole();
1211
1293
  warmUpNetwork();
1212
1294
  patchShowAds();
1213
1295
  ensurePreloadObserver();