nodebb-plugin-ezoic-infinite 1.8.12 → 1.8.13

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/library.js CHANGED
@@ -71,9 +71,8 @@ async function getSettings() {
71
71
 
72
72
  async function isUserExcluded(uid, excludedGroups) {
73
73
  if (!uid || !excludedGroups.length) return false;
74
- const excluded = new Set((excludedGroups || []).map(String));
75
74
  const userGroups = await groups.getUserGroups([uid]);
76
- return (userGroups[0] || []).some(g => excluded.has(String(g.name)));
75
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
77
76
  }
78
77
 
79
78
  // ── Scripts Ezoic ──────────────────────────────────────────────────────────
@@ -114,34 +113,13 @@ plugin.injectEzoicHead = async (data) => {
114
113
  const uid = data.req?.uid ?? 0;
115
114
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
116
115
  if (!excluded) {
117
- const html = data.templateData.customHTML || '';
118
- if (!html.includes('data-nbb-ezoic-head="1"')) {
119
- data.templateData.customHTML = `<meta data-nbb-ezoic-head="1">` + EZOIC_SCRIPTS + html;
120
- }
116
+ // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
+ data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
121
118
  }
122
119
  } catch (_) {}
123
120
  return data;
124
121
  };
125
122
 
126
-
127
- function publicConfigPayload(settings, excluded) {
128
- return {
129
- excluded,
130
- enableBetweenAds: settings.enableBetweenAds,
131
- showFirstTopicAd: settings.showFirstTopicAd,
132
- placeholderIds: settings.placeholderIds,
133
- intervalPosts: settings.intervalPosts,
134
- enableCategoryAds: settings.enableCategoryAds,
135
- showFirstCategoryAd: settings.showFirstCategoryAd,
136
- categoryPlaceholderIds: settings.categoryPlaceholderIds,
137
- intervalCategories: settings.intervalCategories,
138
- enableMessageAds: settings.enableMessageAds,
139
- showFirstMessageAd: settings.showFirstMessageAd,
140
- messagePlaceholderIds: settings.messagePlaceholderIds,
141
- messageIntervalPosts: settings.messageIntervalPosts,
142
- };
143
- }
144
-
145
123
  plugin.init = async ({ router, middleware }) => {
146
124
  async function render(req, res) {
147
125
  const settings = await getSettings();
@@ -161,7 +139,21 @@ plugin.init = async ({ router, middleware }) => {
161
139
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
162
140
  const settings = await getSettings();
163
141
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
164
- res.json(publicConfigPayload(settings, excluded));
142
+ res.json({
143
+ excluded,
144
+ enableBetweenAds: settings.enableBetweenAds,
145
+ showFirstTopicAd: settings.showFirstTopicAd,
146
+ placeholderIds: settings.placeholderIds,
147
+ intervalPosts: settings.intervalPosts,
148
+ enableCategoryAds: settings.enableCategoryAds,
149
+ showFirstCategoryAd: settings.showFirstCategoryAd,
150
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
151
+ intervalCategories: settings.intervalCategories,
152
+ enableMessageAds: settings.enableMessageAds,
153
+ showFirstMessageAd: settings.showFirstMessageAd,
154
+ messagePlaceholderIds: settings.messagePlaceholderIds,
155
+ messageIntervalPosts: settings.messageIntervalPosts,
156
+ });
165
157
  });
166
158
  };
167
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.12",
3
+ "version": "1.8.13",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -18,4 +18,4 @@
18
18
  "compatibility": "^4.0.0"
19
19
  },
20
20
  "private": false
21
- }
21
+ }
package/public/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v52.1
2
+ * NodeBB Ezoic Infinite Ads — client.js v36
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
@@ -32,8 +32,6 @@
32
32
  *
33
33
  * v34 moveDistantWrap — voir v38.
34
34
  *
35
- * v52 Clean architecture interne (modules logiques, cache DOM, bridge Ezoic).
36
- *
37
35
  * v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
38
36
  * après login — filter:middleware.renderHeader re-évalue l'exclusion au
39
37
  * rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
@@ -151,10 +149,6 @@
151
149
  burstDeadline: 0,
152
150
  burstCount: 0,
153
151
  lastBurstTs: 0,
154
- ioViewportMode: isMobile() ? 'm' : 'd',
155
- domRev: 0,
156
- domCache: new Map(),
157
- cacheFrameArmed: false,
158
152
  };
159
153
 
160
154
  let blockedUntil = 0;
@@ -163,81 +157,7 @@
163
157
  const isBlocked = () => ts() < blockedUntil;
164
158
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
165
159
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
166
- const DEBUG = (() => { try { return localStorage.getItem('nbbEzDebug') === '1'; } catch (_) { return false; } })();
167
- const dbg = (...a) => { if (DEBUG) { try { console.debug('[nbb-ezoic]', ...a); } catch (_) {} } };
168
- const isFilled = n => {
169
- try {
170
- if (!n?.querySelector) return false;
171
- if (n.querySelector('iframe, ins, img, video, [data-google-container-id], .ezoic-ad, [id$="__container__"]')) return true;
172
- const r = n.getBoundingClientRect?.();
173
- return !!(r && r.height > 8);
174
- } catch (_) { return false; }
175
- };
176
-
177
- const Arch = {
178
- cache: {
179
- // v53: frame-only micro-cache (safe for NodeBB progressive renders).
180
- // We only cache DOM query results until the next animation frame, and any mutation invalidates immediately.
181
- _armFrameClear() {
182
- if (S.cacheFrameArmed) return;
183
- S.cacheFrameArmed = true;
184
- requestAnimationFrame(() => {
185
- S.cacheFrameArmed = false;
186
- try { S.domCache.clear(); } catch (_) {}
187
- dbg('dom-cache.clear', 'raf');
188
- });
189
- },
190
- invalidate(reason) {
191
- try { S.domRev++; S.domCache.clear(); } catch (_) {}
192
- dbg('dom-cache.invalidate', reason || '');
193
- },
194
- queryAll(key, selector, filterFn) {
195
- const k = `q:${S.domRev}:${key}:${selector}`;
196
- try {
197
- if (S.domCache.has(k)) return S.domCache.get(k);
198
- } catch (_) {}
199
- const arr = Array.from(document.querySelectorAll(selector));
200
- const out = typeof filterFn === 'function' ? arr.filter(Boolean).filter(filterFn) : arr;
201
- try {
202
- this._armFrameClear();
203
- S.domCache.set(k, out);
204
- } catch (_) {}
205
- return out;
206
- },
207
- get(key, buildFn) {
208
- const k = `g:${S.domRev}:${key}`;
209
- try {
210
- if (S.domCache.has(k)) return S.domCache.get(k);
211
- } catch (_) {}
212
- const out = buildFn();
213
- try {
214
- this._armFrameClear();
215
- S.domCache.set(k, out);
216
- } catch (_) {}
217
- return out;
218
- },
219
- },
220
- ezoic: {
221
- ensure() {
222
- window.ezstandalone = window.ezstandalone || {};
223
- return window.ezstandalone;
224
- },
225
- run(fn) {
226
- const ez = this.ensure();
227
- try { return (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(fn) : fn(); } catch (_) {}
228
- },
229
- call(method, ...args) {
230
- const ez = this.ensure();
231
- const fn = ez?.[method];
232
- if (typeof fn !== 'function') return;
233
- try { return fn.apply(ez, args); } catch (_) {}
234
- },
235
- },
236
- page: {
237
- kind() { return Arch.cache.get('kind', getKind); },
238
- key() { return pageKey(); },
239
- },
240
- };
160
+ const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
241
161
 
242
162
  function healFalseEmpty(root = document) {
243
163
  try {
@@ -295,8 +215,7 @@
295
215
 
296
216
  function mutate(fn) {
297
217
  S.mutGuard++;
298
- try { fn(); }
299
- finally { S.mutGuard--; Arch.cache.invalidate('mutate'); }
218
+ try { fn(); } finally { S.mutGuard--; }
300
219
  }
301
220
  function scheduleDestroyFlush() {
302
221
  if (S.destroyBatchTimer) return;
@@ -316,7 +235,11 @@ function flushDestroyBatch() {
316
235
  ids.push(id);
317
236
  }
318
237
  if (ids.length) {
319
- try { Arch.ezoic.run(() => Arch.ezoic.call('destroyPlaceholders', ids)); } catch (_) {}
238
+ try {
239
+ const ez = window.ezstandalone;
240
+ const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
241
+ try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
242
+ } catch (_) {}
320
243
  }
321
244
  if (S.destroyPending.length) scheduleDestroyFlush();
322
245
  }
@@ -332,28 +255,20 @@ function destroyEzoicId(id) {
332
255
  scheduleDestroyFlush();
333
256
  }
334
257
 
335
- function queueDestroyIds(ids) {
336
- for (const raw of (ids || [])) {
337
- const id = parseInt(raw, 10);
338
- if (!Number.isFinite(id) || id <= 0) continue;
339
- if (!S.ezActiveIds.has(id) && !S.ezShownSinceDestroy.has(id)) continue;
340
- if (S.destroyPendingSet.has(id)) continue;
341
- S.destroyPending.push(id);
342
- S.destroyPendingSet.add(id);
343
- S.ezActiveIds.delete(id);
344
- S.ezShownSinceDestroy.delete(id);
345
- }
346
- if (S.destroyPending.length) scheduleDestroyFlush();
347
- }
348
-
349
- function prepareIdsForShow(ids) {
258
+ function destroyBeforeReuse(ids) {
350
259
  const out = [];
260
+ const toDestroy = [];
351
261
  const seen = new Set();
352
262
  for (const raw of (ids || [])) {
353
263
  const id = parseInt(raw, 10);
354
264
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
355
265
  seen.add(id);
356
266
  out.push(id);
267
+ if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
268
+ }
269
+ if (toDestroy.length) {
270
+ try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
271
+ for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
357
272
  }
358
273
  return out;
359
274
  }
@@ -412,7 +327,7 @@ function prepareIdsForShow(ids) {
412
327
  // ── Items DOM ──────────────────────────────────────────────────────────────
413
328
 
414
329
  function getPosts() {
415
- return Arch.cache.queryAll('posts', SEL.post, el => {
330
+ return Array.from(document.querySelectorAll(SEL.post)).filter(el => {
416
331
  if (!el.isConnected) return false;
417
332
  if (!el.querySelector('[component="post/content"]')) return false;
418
333
  const p = el.parentElement?.closest(SEL.post);
@@ -421,8 +336,8 @@ function prepareIdsForShow(ids) {
421
336
  });
422
337
  }
423
338
 
424
- const getTopics = () => Arch.cache.queryAll('topics', SEL.topic);
425
- const getCategories = () => Arch.cache.queryAll('categories', SEL.category);
339
+ const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
340
+ const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
426
341
 
427
342
  // ── Wraps — détection ──────────────────────────────────────────────────────
428
343
 
@@ -523,7 +438,6 @@ function prepareIdsForShow(ids) {
523
438
  S.mountedIds.delete(id);
524
439
  S.pendingSet.delete(id);
525
440
  S.lastShow.delete(id);
526
- queueDestroyIds([id]);
527
441
  S.ezActiveIds.delete(id);
528
442
  }
529
443
  }
@@ -540,7 +454,7 @@ function prepareIdsForShow(ids) {
540
454
  * Priorité : wraps vides d'abord, remplis si nécessaire.
541
455
  */
542
456
  function recycleAndMove(klass, targetEl, newKey) {
543
- const ez = Arch.ezoic.ensure();
457
+ const ez = window.ezstandalone;
544
458
  if (typeof ez?.destroyPlaceholders !== 'function' ||
545
459
  typeof ez?.define !== 'function' ||
546
460
  typeof ez?.displayMore !== 'function') return null;
@@ -612,7 +526,7 @@ function recycleAndMove(klass, targetEl, newKey) {
612
526
  };
613
527
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
614
528
  const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
615
- try { Arch.ezoic.run(doDestroy); } catch (_) {}
529
+ try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
616
530
 
617
531
  return { id, wrap: best };
618
532
  }
@@ -652,7 +566,7 @@ function recycleAndMove(klass, targetEl, newKey) {
652
566
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
653
567
  if (ph instanceof Element) S.io?.unobserve(ph);
654
568
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
655
- if (Number.isFinite(id)) { queueDestroyIds([id]); S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
569
+ if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
656
570
  const key = w.getAttribute(A_ANCHOR);
657
571
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
658
572
  w.remove();
@@ -675,18 +589,19 @@ function recycleAndMove(klass, targetEl, newKey) {
675
589
  // supprimerait les wraps, et provoquerait une réinjection en haut.
676
590
 
677
591
  function pruneOrphansBetween() {
678
- if (S.burstActive || (S.scrollSpeed || 0) > 1800) return;
679
592
  const klass = 'ezoic-ad-between';
680
593
  const cfg = KIND[klass];
681
- const liveAnchors = new Set(Arch.cache.queryAll('prune-between-anchors', cfg.sel).map(el => el.getAttribute(cfg.anchorAttr)).filter(Boolean));
682
594
 
683
595
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
684
596
  const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
685
- if (ts() - created < MIN_PRUNE_AGE_MS) return;
597
+ if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
598
+
686
599
  const key = w.getAttribute(A_ANCHOR) ?? '';
687
- const sid = key.slice(klass.length + 1);
600
+ const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
688
601
  if (!sid) { mutate(() => dropWrap(w)); return; }
689
- if (!liveAnchors.has(sid)) mutate(() => dropWrap(w));
602
+
603
+ const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
604
+ if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
690
605
  });
691
606
  }
692
607
 
@@ -732,8 +647,6 @@ function recycleAndMove(klass, targetEl, newKey) {
732
647
  const w = insertAfter(el, id, klass, key);
733
648
  if (w) { observePh(id); inserted++; }
734
649
  } else {
735
- // Recyclage agressif = source fréquente de churn visuel (surtout sticky/fixed)
736
- if (klass === 'ezoic-ad-message') break;
737
650
  const recycled = recycleAndMove(klass, el, key);
738
651
  if (!recycled) break;
739
652
  inserted++;
@@ -744,31 +657,15 @@ function recycleAndMove(klass, targetEl, newKey) {
744
657
 
745
658
  // ── IntersectionObserver & Show ────────────────────────────────────────────
746
659
 
747
- function ensureIOViewportMode() {
748
- const mode = isMobile() ? 'm' : 'd';
749
- if (S.io && S.ioViewportMode !== mode) {
750
- try { S.io.disconnect(); } catch (_) {}
751
- S.io = null;
752
- }
753
- S.ioViewportMode = mode;
754
- }
755
-
756
660
  function getIO() {
757
- ensureIOViewportMode();
758
661
  if (S.io) return S.io;
759
662
  try {
760
663
  S.io = new IntersectionObserver(entries => {
761
664
  for (const e of entries) {
762
665
  if (!e.isIntersecting) continue;
763
- const target = e.target instanceof Element ? e.target : null;
764
- const id = parseInt(target?.getAttribute('data-ezoic-id'), 10);
765
- if (!Number.isFinite(id) || id <= 0) continue;
766
- const accepted = enqueueShow(id);
767
- if (accepted) {
768
- try { S.io?.unobserve(target); } catch (_) {}
769
- } else if (target?.isConnected) {
770
- setTimeout(() => { try { if (target.isConnected) S.io?.observe(target); } catch (_) {} }, 300);
771
- }
666
+ if (e.target instanceof Element) S.io?.unobserve(e.target);
667
+ const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
668
+ if (Number.isFinite(id) && id > 0) enqueueShow(id);
772
669
  }
773
670
  }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
774
671
  } catch (_) { S.io = null; }
@@ -789,15 +686,12 @@ function recycleAndMove(klass, targetEl, newKey) {
789
686
  }
790
687
 
791
688
  function enqueueShow(id) {
792
- if (!id || isBlocked()) return false;
689
+ if (!id || isBlocked()) return;
793
690
  const n = parseInt(id, 10);
794
- if (!Number.isFinite(n) || n <= 0) return false;
795
- if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
796
- const ph = phEl(n);
797
- if (!ph?.isConnected || isFilled(ph) || !hasSinglePlaceholder(n)) return false;
691
+ if (!Number.isFinite(n) || n <= 0) return;
692
+ if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
798
693
  if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
799
694
  scheduleDrainQueue();
800
- return true;
801
695
  }
802
696
 
803
697
  function scheduleDrainQueue() {
@@ -863,18 +757,19 @@ function startShowBatch(ids) {
863
757
 
864
758
  if (!valid.length) { clearTimeout(timer); return release(); }
865
759
 
866
- const ez = Arch.ezoic.ensure();
760
+ window.ezstandalone = window.ezstandalone || {};
761
+ const ez = window.ezstandalone;
867
762
  const doShow = () => {
868
- const prepared = prepareIdsForShow(valid);
763
+ const prepared = destroyBeforeReuse(valid);
869
764
  if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
870
- try { ez.showAds(...prepared); } catch (_) { dbg('showAds failed', prepared); }
765
+ try { ez.showAds(...prepared); } catch (_) {}
871
766
  for (const id of prepared) {
872
767
  S.ezActiveIds.add(id);
873
768
  S.ezShownSinceDestroy.add(id);
874
769
  }
875
770
  setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
876
771
  };
877
- Arch.ezoic.run(doShow);
772
+ Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
878
773
  } catch (_) { clearTimeout(timer); release(); }
879
774
  });
880
775
  }
@@ -889,7 +784,8 @@ function startShowBatch(ids) {
889
784
  function patchShowAds() {
890
785
  const apply = () => {
891
786
  try {
892
- const ez = Arch.ezoic.ensure();
787
+ window.ezstandalone = window.ezstandalone || {};
788
+ const ez = window.ezstandalone;
893
789
  if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
894
790
  window.__nbbEzPatched = true;
895
791
  const orig = ez.showAds.bind(ez);
@@ -916,7 +812,8 @@ function startShowBatch(ids) {
916
812
  };
917
813
  apply();
918
814
  if (!window.__nbbEzPatched) {
919
- Arch.ezoic.run(apply);
815
+ window.ezstandalone = window.ezstandalone || {};
816
+ (window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
920
817
  }
921
818
  }
922
819
 
@@ -1023,10 +920,8 @@ function startShowBatch(ids) {
1023
920
  S.runQueued = false;
1024
921
  S.sweepQueued = false;
1025
922
  S.scrollSpeed = 0;
1026
- S.cacheFrameArmed = false;
1027
923
  S.lastScrollY = 0;
1028
924
  S.lastScrollTs = 0;
1029
- Arch.cache.invalidate('cleanup');
1030
925
  }
1031
926
 
1032
927
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -1044,14 +939,14 @@ function startShowBatch(ids) {
1044
939
  sawWrapRemoval = true;
1045
940
  }
1046
941
  }
1047
- if (sawWrapRemoval) { Arch.cache.invalidate('wrap-removed'); queueSweepDeadWraps(); }
942
+ if (sawWrapRemoval) queueSweepDeadWraps();
1048
943
  for (const n of m.addedNodes) {
1049
944
  if (n.nodeType !== 1) continue;
1050
945
  try { healFalseEmpty(n); } catch (_) {}
1051
946
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
1052
947
  if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
1053
948
  allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
1054
- Arch.cache.invalidate('dom-added'); requestBurst(); return;
949
+ requestBurst(); return;
1055
950
  }
1056
951
  }
1057
952
  }
@@ -1130,8 +1025,7 @@ function startShowBatch(ids) {
1130
1025
  $(window).off('.nbbEzoic');
1131
1026
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
1132
1027
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
1133
- S.pageKey = Arch.page.key();
1134
- Arch.cache.invalidate('ajaxify.end');
1028
+ S.pageKey = pageKey();
1135
1029
  blockedUntil = 0;
1136
1030
  muteConsole(); ensureTcfLocator(); warmNetwork();
1137
1031
  patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
@@ -1154,17 +1048,6 @@ function startShowBatch(ids) {
1154
1048
  } catch (_) {}
1155
1049
  }
1156
1050
 
1157
- function bindResize() {
1158
- let rT = 0;
1159
- window.addEventListener('resize', () => {
1160
- if (rT) clearTimeout(rT);
1161
- rT = setTimeout(() => {
1162
- ensureIOViewportMode();
1163
- requestBurst();
1164
- }, 120);
1165
- }, { passive: true });
1166
- }
1167
-
1168
1051
  function bindScroll() {
1169
1052
  let ticking = false;
1170
1053
  try {
@@ -1191,29 +1074,16 @@ function startShowBatch(ids) {
1191
1074
 
1192
1075
  // ── Boot ───────────────────────────────────────────────────────────────────
1193
1076
 
1194
- const App = {
1195
- modules: {
1196
- infra: { muteConsole, ensureTcfLocator, warmNetwork, patchShowAds },
1197
- lifecycle: { ensureDomObserver, bindNodeBB, cleanup },
1198
- viewport: { getIO, bindResize, bindScroll },
1199
- scheduler: { requestBurst },
1200
- },
1201
- boot() {
1202
- this.modules.infra.muteConsole();
1203
- this.modules.infra.ensureTcfLocator();
1204
- this.modules.infra.warmNetwork();
1205
- this.modules.infra.patchShowAds();
1206
- this.modules.viewport.getIO();
1207
- this.modules.lifecycle.ensureDomObserver();
1208
- this.modules.lifecycle.bindNodeBB();
1209
- this.modules.viewport.bindResize();
1210
- this.modules.viewport.bindScroll();
1211
- blockedUntil = 0;
1212
- this.modules.scheduler.requestBurst();
1213
- },
1214
- };
1215
-
1216
- S.pageKey = Arch.page.key();
1217
- App.boot();
1077
+ S.pageKey = pageKey();
1078
+ muteConsole();
1079
+ ensureTcfLocator();
1080
+ warmNetwork();
1081
+ patchShowAds();
1082
+ getIO();
1083
+ ensureDomObserver();
1084
+ bindNodeBB();
1085
+ bindScroll();
1086
+ blockedUntil = 0;
1087
+ requestBurst();
1218
1088
 
1219
1089
  })();
package/public/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * NodeBB Ezoic Infinite Ads — style.css (v21)
2
+ * NodeBB Ezoic Infinite Ads — style.css (v20)
3
3
  */
4
4
 
5
5
  /* ── Wrapper ──────────────────────────────────────────────────────────────── */
@@ -8,9 +8,8 @@
8
8
  width: 100%;
9
9
  margin: 0 !important;
10
10
  padding: 0 !important;
11
- overflow: visible;
12
- contain: none;
13
- position: relative;
11
+ overflow: hidden;
12
+ contain: layout style;
14
13
  }
15
14
 
16
15
  /* Placeholder : 1px minimum pour rester visible par l'IntersectionObserver */
@@ -51,7 +50,11 @@
51
50
  position: absolute !important;
52
51
  }
53
52
 
54
- /* Compat sticky/fixed Ezoic : ne pas neutraliser globalement ici. */
53
+ /* Neutralise sticky dans nos wraps (évite l'effet "gliding") */
54
+ .nodebb-ezoic-wrap .ezads-sticky-intradiv {
55
+ position: static !important;
56
+ top: auto !important;
57
+ }
55
58
 
56
59
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
57
60
  .ezoic-ad {