nodebb-plugin-ezoic-infinite 1.4.96 → 1.4.98

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/public/client.js CHANGED
@@ -1,940 +1,604 @@
1
1
  (function () {
2
2
  'use strict';
3
+
4
+ // NodeBB client context
3
5
  const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
6
+
7
+ const WRAP_CLASS = 'ezoic-ad';
8
+ const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
9
+
10
+ // Insert at most N ads per run to keep the UI smooth on infinite scroll
11
+ const MAX_INSERTS_PER_RUN = 3;
12
+
13
+ // Preload before viewport (tune if you want even earlier)
14
+ const PRELOAD_ROOT_MARGIN = '1200px 0px';
15
+
4
16
  const SELECTORS = {
5
- topicItem: 'li[component="category/topic"]',
6
- postItem: '[component="post"][data-pid]',
7
- categoryItem: 'li[component="categories/category"]',
8
- }, WRAP_CLASS = 'ezoic-ad';
9
- const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
10
-
11
- // FLAG GLOBAL: Bloquer Ezoic pendant navigation
17
+ topicItem: 'li[component="category/topic"]',
18
+ postItem: '[component="post"][data-pid]',
19
+ categoryItem: 'li[component="categories/category"]',
20
+ };
21
+
22
+ // Hard block during navigation to avoid “placeholder does not exist” spam
12
23
  let EZOIC_BLOCKED = false;
13
-
14
- // DEBUG: Vérifier que le plugin charge
15
24
 
16
- const sessionDefinedIds = new Set();
25
+ const state = {
26
+ pageKey: null,
27
+ cfg: null,
28
+
29
+ poolTopics: [],
30
+ poolPosts: [],
31
+ poolCategories: [],
17
32
 
18
- const insertingIds = new Set(), state = {
19
- pageKey: null,
20
- cfg: null,
21
- cfgPromise: null,
33
+ usedTopics: new Set(),
34
+ usedPosts: new Set(),
35
+ usedCategories: new Set(),
22
36
 
23
- poolTopics: [],
24
- poolPosts: [],
25
- poolCategories: [],
37
+ // throttle per placeholder id
38
+ lastShowById: new Map(),
26
39
 
27
- usedTopics: new Set(),
28
- usedPosts: new Set(),
29
- usedCategories: new Set(),
40
+ // observers / schedulers
41
+ domObs: null,
42
+ io: null,
43
+ runQueued: false,
30
44
 
31
- lastShowById: new Map(),
32
- pendingById: new Set(),
33
- definedIds: new Set(),
45
+ // hero
46
+ heroDoneForPage: false,
47
+ };
34
48
 
35
- scheduled: false,
36
- timer: null,
49
+ const sessionDefinedIds = new Set();
50
+ const insertingIds = new Set();
37
51
 
38
- obs: null,
39
- activeTimeouts: new Set(),
40
- lastScrollRun: 0, };
52
+ // ---------- small utils ----------
41
53
 
42
54
  function normalizeBool(v) {
43
- return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
55
+ return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
44
56
  }
45
57
 
46
58
  function uniqInts(lines) {
47
- const out = [], seen = new Set();
48
- for (const v of lines) {
49
- const n = parseInt(v, 10);
50
- if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
51
- seen.add(n);
52
- out.push(n);
53
- }
54
- }
55
- return out;
59
+ const out = [];
60
+ const seen = new Set();
61
+ for (const v of lines) {
62
+ const n = parseInt(v, 10);
63
+ if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
64
+ seen.add(n);
65
+ out.push(n);
66
+ }
67
+ }
68
+ return out;
56
69
  }
57
70
 
58
71
  function parsePool(raw) {
59
- if (!raw) return [];
60
- const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
61
- return uniqInts(lines);
72
+ if (!raw) return [];
73
+ const lines = String(raw)
74
+ .split(/\r?\n/)
75
+ .map(s => s.trim())
76
+ .filter(Boolean);
77
+ return uniqInts(lines);
62
78
  }
63
79
 
64
80
  function getPageKey() {
65
- try {
66
- const ax = window.ajaxify;
67
- if (ax && ax.data) {
68
- if (ax.data.tid) return `topic:${ax.data.tid}`;
69
- if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
70
- }
71
- } catch (e) {}
72
- return window.location.pathname;
81
+ try {
82
+ const ax = window.ajaxify;
83
+ if (ax && ax.data) {
84
+ if (ax.data.tid) return `topic:${ax.data.tid}`;
85
+ if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
86
+ }
87
+ } catch (e) {}
88
+ return window.location.pathname;
73
89
  }
74
90
 
75
91
  function getKind() {
76
- const p = window.location.pathname || '';
77
- if (/^\/topic\//.test(p)) return 'topic';
78
- if (/^\/category\//.test(p)) return 'categoryTopics';
79
- if (p === '/' || /^\/categories/.test(p)) return 'categories';
80
- // fallback by DOM
81
- if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
82
- if (document.querySelector(SELECTORS.postItem)) return 'topic';
83
- if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
84
- return 'other';
92
+ const p = window.location.pathname || '';
93
+ if (/^\/topic\//.test(p)) return 'topic';
94
+ if (/^\/category\//.test(p)) return 'categoryTopics';
95
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
96
+
97
+ // fallback by DOM
98
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
99
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
100
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
101
+ return 'other';
85
102
  }
86
103
 
87
104
  function getTopicItems() {
88
- return Array.from(document.querySelectorAll(SELECTORS.topicItem));
105
+ return Array.from(document.querySelectorAll(SELECTORS.topicItem));
89
106
  }
90
107
 
91
108
  function getCategoryItems() {
92
- return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
109
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
93
110
  }
94
111
 
95
112
  function getPostContainers() {
96
- const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
97
- return nodes.filter((el) => {
98
- if (!el || !el.isConnected) return false;
99
- if (!el.querySelector('[component="post/content"]')) return false;
100
- const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
101
- if (parentPost && parentPost !== el) return false;
102
- if (el.getAttribute('component') === 'post/parent') return false;
103
- return true;
104
- });
105
- }
106
-
107
- function safeRect(el) {
108
- try { return el.getBoundingClientRect(); } catch (e) { return null; }
109
- }
110
-
111
- function destroyPlaceholderIds(ids) {
112
- if (!ids || !ids.length) return;
113
- const filtered = ids.filter((id) => {
114
- try { return sessionDefinedIds.has(id); } catch (e) { return true; }
115
- });
116
- if (!filtered.length) return;
117
-
118
- const call = () => {
119
- try {
120
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
121
- window.ezstandalone.destroyPlaceholders(filtered);
122
- }
123
- } catch (e) {}
124
-
125
- // Recyclage: libérer IDs après 100ms
126
- setTimeout(() => {
127
- filtered.forEach(id => sessionDefinedIds.delete(id));
128
- }, 100);
129
- };
130
- try {
131
- window.ezstandalone = window.ezstandalone || {};
132
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
133
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
134
- else window.ezstandalone.cmd.push(call);
135
- } catch (e) {}
136
-
137
- // Recyclage: libérer IDs après 100ms
138
- setTimeout(() => {
139
- filtered.forEach(id => sessionDefinedIds.delete(id));
140
- }, 100);
141
- }
142
-
143
- // Nettoyer éléments Ezoic invisibles qui créent espace vertical
144
- function cleanupInvisibleEzoicElements() {
145
- try {
146
- document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
147
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
148
- if (!ph) return;
149
-
150
- // ULTRA-AGRESSIF: Supprimer TOUT sauf placeholder
151
- Array.from(wrapper.children).forEach(child => {
152
- if (child === ph || child.contains(ph)) return;
153
- if (child.id && child.id.startsWith('ezoic-pub-ad-placeholder-')) return;
154
- child.remove();
155
- });
156
-
157
- // Forcer wrapper collé
158
- wrapper.style.height = 'auto';
159
- wrapper.style.overflow = 'hidden';
160
- wrapper.style.lineHeight = '0';
161
- wrapper.style.fontSize = '0';
162
- wrapper.style.margin = '0';
163
- wrapper.style.padding = '0';
164
-
165
- // CRITIQUE: Forcer suppression margin sur TOUS les .ezoic-ad enfants
166
- // Pour overrider les margin-top/bottom:15px !important d'Ezoic
167
- wrapper.querySelectorAll('.ezoic-ad, span.ezoic-ad').forEach(ezSpan => {
168
- ezSpan.style.setProperty('margin-top', '0', 'important');
169
- ezSpan.style.setProperty('margin-bottom', '0', 'important');
170
- ezSpan.style.setProperty('margin-left', '0', 'important');
171
- ezSpan.style.setProperty('margin-right', '0', 'important');
172
- });
173
-
174
- // Nettoyer aussi DANS le placeholder
175
- if (ph) {
176
- Array.from(ph.children).forEach(child => {
177
- // Garder seulement iframe, ins, img
178
- const tag = child.tagName;
179
- if (tag !== 'IFRAME' && tag !== 'INS' && tag !== 'IMG' && tag !== 'DIV') {
180
- child.remove();
181
- }
182
- });
183
- }
184
- });
185
- } catch (e) {}
186
- }
187
-
188
- // Lancer cleanup périodique toutes les 2s
189
- setInterval(() => {
190
- try { cleanupInvisibleEzoicElements(); } catch (e) {}
191
- }, 2000);
192
-
193
- function cleanupEmptyWrappers() {
194
- try {
195
- document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
196
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
197
- if (ph && ph.children.length === 0) {
198
- // Placeholder vide après 3s = pub non chargée
199
- setTimeout(() => {
200
- if (ph.children.length === 0) {
201
- wrapper.remove();
202
- }
203
- }, 1500);
204
- }
205
- });
206
- } catch (e) {}
207
- }
208
-
209
- function getRecyclable(liveArr) {
210
- const margin = 600;
211
- for (let i = 0; i < liveArr.length; i++) {
212
- const entry = liveArr[i];
213
- if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
214
- const r = safeRect(entry.wrap);
215
- if (r && r.bottom < -margin) {
216
- liveArr.splice(i, 1);
217
- return entry;
218
- }
219
- }
220
- return null;
113
+ const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
114
+ return nodes.filter((el) => {
115
+ if (!el || !el.isConnected) return false;
116
+ if (!el.querySelector('[component="post/content"]')) return false;
117
+ const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
118
+ if (parentPost && parentPost !== el) return false;
119
+ if (el.getAttribute('component') === 'post/parent') return false;
120
+ return true;
121
+ });
122
+ }
123
+
124
+ // ---------- warm-up & patching ----------
125
+
126
+ const _warmLinksDone = new Set();
127
+ function warmUpNetwork() {
128
+ try {
129
+ const head = document.head || document.getElementsByTagName('head')[0];
130
+ if (!head) return;
131
+ const links = [
132
+ ['preconnect', 'https://g.ezoic.net', true],
133
+ ['dns-prefetch', 'https://g.ezoic.net', false],
134
+ ['preconnect', 'https://go.ezoic.net', true],
135
+ ['dns-prefetch', 'https://go.ezoic.net', false],
136
+ ];
137
+ for (const [rel, href, cors] of links) {
138
+ const key = `${rel}|${href}`;
139
+ if (_warmLinksDone.has(key)) continue;
140
+ _warmLinksDone.add(key);
141
+ const link = document.createElement('link');
142
+ link.rel = rel;
143
+ link.href = href;
144
+ if (cors) link.crossOrigin = 'anonymous';
145
+ head.appendChild(link);
146
+ }
147
+ } catch (e) {}
148
+ }
149
+
150
+ // Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
151
+ function patchShowAds() {
152
+ const applyPatch = () => {
153
+ try {
154
+ window.ezstandalone = window.ezstandalone || {};
155
+ const ez = window.ezstandalone;
156
+ if (window.__nodebbEzoicPatched) return;
157
+ if (typeof ez.showAds !== 'function') return;
158
+
159
+ window.__nodebbEzoicPatched = true;
160
+ const orig = ez.showAds;
161
+
162
+ ez.showAds = function (...args) {
163
+ if (EZOIC_BLOCKED) return;
164
+
165
+ let ids = [];
166
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
167
+ else ids = args;
168
+
169
+ const seen = new Set();
170
+ for (const v of ids) {
171
+ const id = parseInt(v, 10);
172
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
173
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
174
+ if (!ph || !ph.isConnected) continue;
175
+ seen.add(id);
176
+ try { orig.call(ez, id); } catch (e) {}
177
+ }
178
+ };
179
+ } catch (e) {}
180
+ };
181
+
182
+ applyPatch();
183
+ if (!window.__nodebbEzoicPatched) {
184
+ try {
185
+ window.ezstandalone = window.ezstandalone || {};
186
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
187
+ window.ezstandalone.cmd.push(applyPatch);
188
+ } catch (e) {}
189
+ }
190
+ }
191
+
192
+ // ---------- config & pools ----------
193
+
194
+ async function fetchConfigOnce() {
195
+ if (state.cfg) return state.cfg;
196
+ try {
197
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
198
+ if (!res.ok) return null;
199
+ state.cfg = await res.json();
200
+ return state.cfg;
201
+ } catch (e) {
202
+ return null;
203
+ }
221
204
  }
222
205
 
223
- function pickId(pool, liveArr) {
224
- if (pool.length) return { id: pool.shift(), recycled: null };
225
- const recycled = getRecyclable(liveArr);
226
- if (recycled) return { id: recycled.id, recycled };
227
- return { id: null, recycled: null };
206
+ function initPools(cfg) {
207
+ if (!cfg) return;
208
+ if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
209
+ if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
210
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
228
211
  }
229
212
 
230
- function resetPlaceholderInWrap(wrap, id) {
231
- try {
232
- if (!wrap) return;
233
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
234
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
235
- const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
236
- if (old) old.remove();
237
- // Remove any leftover markup inside wrapper
238
- wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
239
- const ph = document.createElement('div');
240
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
241
- wrap.appendChild(ph);
242
- } catch (e) {}
243
- }
213
+ // ---------- insertion primitives ----------
244
214
 
245
215
  function isAdjacentAd(target) {
246
- if (!target || !target.nextElementSibling) return false;
247
- const next = target.nextElementSibling;
248
- if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
249
- return false;
250
- }
251
-
252
- function isPrevAd(target) {
253
- if (!target || !target.previousElementSibling) return false;
254
- const prev = target.previousElementSibling;
255
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
256
- return false;
257
- }
258
-
259
- function buildWrap(id, kindClass, afterPos) {
260
- const wrap = document.createElement('div');
261
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
262
- wrap.setAttribute('data-ezoic-after', String(afterPos));
263
- wrap.style.width = '100%';
264
-
265
- const ph = document.createElement('div');
266
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
267
- wrap.appendChild(ph);
268
- return wrap;
216
+ if (!target) return false;
217
+ const next = target.nextElementSibling;
218
+ if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
219
+ const prev = target.previousElementSibling;
220
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
221
+ return false;
222
+ }
223
+
224
+ function buildWrap(target, id, kindClass, afterPos) {
225
+ const tag = (target && target.tagName === 'LI') ? 'li' : 'div';
226
+ const wrap = document.createElement(tag);
227
+ if (tag === 'li') {
228
+ wrap.style.listStyle = 'none';
229
+ // preserve common NodeBB list styling
230
+ if (target && target.classList && target.classList.contains('list-group-item')) wrap.classList.add('list-group-item');
231
+ }
232
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
233
+ if (wrap.tagName === 'LI') {
234
+ wrap.setAttribute('role', 'presentation');
235
+ wrap.setAttribute('aria-hidden', 'true');
236
+ }
237
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
238
+ wrap.style.width = '100%';
239
+
240
+ const ph = document.createElement('div');
241
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
242
+ ph.setAttribute('data-ezoic-id', String(id));
243
+ wrap.appendChild(ph);
244
+
245
+ return wrap;
269
246
  }
270
247
 
271
248
  function findWrap(kindClass, afterPos) {
272
- return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
249
+ return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
273
250
  }
274
251
 
275
252
  function insertAfter(target, id, kindClass, afterPos) {
276
- if (!target || !target.insertAdjacentElement) return null;
277
- if (findWrap(kindClass, afterPos)) return null;
278
-
279
- // CRITICAL: Double-lock pour éviter race conditions sur les doublons
280
- if (insertingIds.has(id)) return null;
281
-
282
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
283
- if (existingPh && existingPh.isConnected) return null;
284
-
285
- // Acquérir le lock
286
- insertingIds.add(id);
287
-
288
- try {
289
- const wrap = buildWrap(id, kindClass, afterPos);
290
- target.insertAdjacentElement('afterend', wrap);
291
- attachFillObserver(wrap, id);
292
- return wrap;
293
- } finally {
294
- setTimeout(() => insertingIds.delete(id), 50);
295
- }
296
- }
297
-
298
- function destroyUsedPlaceholders() {
299
- const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
300
- if (ids.length) destroyPlaceholderIds(ids);
301
- }
302
-
303
- function patchShowAds() {
304
- const applyPatch = () => {
305
- try {
306
- window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
307
- if (window.__nodebbEzoicPatched) return;
308
- if (typeof ez.showAds !== 'function') return;
309
-
310
- window.__nodebbEzoicPatched = true;
311
- const orig = ez.showAds;
312
-
313
- ez.showAds = function (arg) {
314
- // CRITIQUE: Bloquer TOUS appels si navigation en cours
315
- if (EZOIC_BLOCKED) {
316
- return; // Ignorer complètement
317
- }
318
-
319
- // Filtrer IDs dont placeholders n'existent pas
320
- const filterValidIds = (ids) => {
321
- return ids.filter(id => {
322
- const phId = `ezoic-pub-ad-placeholder-${id}`;
323
- const ph = document.getElementById(phId);
324
- return ph && ph.isConnected;
325
- });
326
- };
327
-
328
- if (Array.isArray(arg)) {
329
- const seen = new Set();
330
- for (const v of arg) {
331
- const id = parseInt(v, 10);
332
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
333
-
334
- // Vérifier existence avant appel
335
- const phId = `ezoic-pub-ad-placeholder-${id}`;
336
- const ph = document.getElementById(phId);
337
- if (!ph || !ph.isConnected) continue;
338
-
339
- seen.add(id);
340
- try { orig.call(ez, id); } catch (e) {}
341
- }
342
- return;
343
- }
344
-
345
- // Pour appels simples, vérifier aussi
346
- if (typeof arg === 'number') {
347
- const phId = `ezoic-pub-ad-placeholder-${arg}`;
348
- const ph = document.getElementById(phId);
349
- if (!ph || !ph.isConnected) return;
350
- }
351
-
352
- return orig.apply(ez, arguments);
353
- };
354
- } catch (e) {}
355
- };
356
-
357
- applyPatch();
358
- if (!window.__nodebbEzoicPatched) {
359
- try {
360
- window.ezstandalone = window.ezstandalone || {};
361
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
362
- window.ezstandalone.cmd.push(applyPatch);
363
- } catch (e) {}
364
- }
365
- }
366
-
367
- function markFilled(wrap) {
368
- try {
369
- if (!wrap) return;
370
- if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
371
- wrap.setAttribute('data-ezoic-filled', '1');
372
- } catch (e) {}
373
- }
374
-
375
- function isWrapMarkedFilled(wrap) {
376
- try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
377
- }
378
-
379
- function attachFillObserver(wrap, id) {
380
- try {
381
- const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
382
- if (!ph) return;
383
- // Already filled?
384
- if (ph.childNodes && ph.childNodes.length > 0) {
385
- markFilled(wrap); // Afficher wrapper
386
- sessionDefinedIds.add(id);
387
- return;
388
- }
389
- const obs = new MutationObserver(() => {
390
- if (ph.childNodes && ph.childNodes.length > 0) {
391
- markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
392
- try { sessionDefinedIds.add(id); } catch (e) {}
393
- try { obs.disconnect(); } catch (e) {}
394
- }
395
- });
396
- obs.observe(ph, { childList: true, subtree: true });
397
- wrap.__ezoicFillObs = obs;
398
- } catch (e) {}
399
- }
400
-
401
- function isPlaceholderFilled(id) {
402
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
403
- if (!ph || !ph.isConnected) return false;
404
-
405
- const wrap = ph.parentElement;
406
- if (wrap && isWrapMarkedFilled(wrap)) return true;
407
-
408
- const filled = !!(ph.childNodes && ph.childNodes.length > 0);
409
- if (filled) {
410
- try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
411
- try { markFilled(wrap); } catch (e) {}
412
- }
413
- return filled;
414
- }
415
-
416
- let batchShowAdsTimer = null;
417
- const pendingShowAdsIds = new Set();
418
-
419
- function scheduleShowAdsBatch(id) {
420
- if (!id) return;
421
-
422
- if (sessionDefinedIds.has(id)) {
423
- try {
424
- destroyPlaceholderIds([id]);
425
- sessionDefinedIds.delete(id);
426
- } catch (e) {}
427
- }
428
-
429
- // Throttle: ne pas rappeler le même ID trop vite
430
- const now = Date.now(), last = state.lastShowById.get(id) || 0;
431
- if (now - last < 3500) return;
432
-
433
- // Ajouter à la batch
434
- pendingShowAdsIds.add(id);
435
-
436
- clearTimeout(batchShowAdsTimer);
437
- batchShowAdsTimer = setTimeout(() => {
438
- // CRITIQUE: Vérifier que nous sommes toujours sur la même page
439
- const currentPageKey = getPageKey();
440
- if (state.pageKey && currentPageKey !== state.pageKey) {
441
- pendingShowAdsIds.clear();
442
- return; // Page a changé, annuler
443
- }
444
-
445
- if (pendingShowAdsIds.size === 0) return;
446
-
447
- const idsArray = Array.from(pendingShowAdsIds);
448
- pendingShowAdsIds.clear();
449
-
450
- // CRITIQUE: Vérifier que placeholders existent encore
451
- const validIds = idsArray.filter(id => {
452
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
453
- return ph && ph.isConnected;
454
- });
455
-
456
- if (validIds.length === 0) return;
457
-
458
- // Appeler showAds avec TOUS les IDs en une fois
459
- try {
460
- // CRITIQUE: Re-patcher AVANT chaque appel pour être sûr
461
- patchShowAds();
462
-
463
- window.ezstandalone = window.ezstandalone || {};
464
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
465
- window.ezstandalone.cmd.push(function() {
466
- if (typeof window.ezstandalone.showAds === 'function') {
467
- // Appel batch: showAds(id1, id2, id3...)
468
- window.ezstandalone.showAds(...validIds);
469
- // Tracker tous les IDs
470
- validIds.forEach(id => {
471
- state.lastShowById.set(id, Date.now());
472
- sessionDefinedIds.add(id);
473
- });
474
- }
475
- });
476
- } catch (e) {}
477
-
478
- // CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
479
- setTimeout(() => {
480
- cleanupInvisibleEzoicElements();
481
- }, 1500); // 1.5s pour laisser Ezoic charger
482
- }, 100);
483
- }
484
-
485
- function callShowAdsWhenReady(id) {
486
- if (!id) return;
487
-
488
- const now = Date.now(), last = state.lastShowById.get(id) || 0;
489
- if (now - last < 3500) return;
490
-
491
- const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
492
- try {
493
- window.ezstandalone = window.ezstandalone || {};
494
- if (typeof window.ezstandalone.showAds === 'function') {
495
-
496
- state.lastShowById.set(id, Date.now());
497
- window.ezstandalone.showAds(id);
498
- sessionDefinedIds.add(id);
499
- return true;
500
- }
501
- } catch (e) {}
502
- return false;
503
- };
504
-
505
- const startPageKey = state.pageKey;
506
- let attempts = 0;
507
- (function waitForPh() {
508
- if (state.pageKey !== startPageKey) return;
509
- if (state.pendingById.has(id)) return;
510
-
511
- attempts += 1;
512
- const el = document.getElementById(phId);
513
- if (el && el.isConnected) {
514
-
515
- // Si on arrive ici, soit visible, soit timeout
516
-
517
- if (doCall()) {
518
- state.pendingById.delete(id);
519
- return;
520
- }
521
-
522
- }
523
-
524
- if (attempts < 100) {
525
- const timeoutId = setTimeout(waitForPh, 50);
526
- state.activeTimeouts.add(timeoutId);
527
- }
528
- })();
529
- }
530
-
531
- async function fetchConfig() {
532
- if (state.cfg) return state.cfg;
533
- if (state.cfgPromise) return state.cfgPromise;
534
-
535
- state.cfgPromise = (async () => {
536
- const MAX_TRIES = 3;
537
- let delay = 800;
538
- for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
539
- try {
540
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
541
- if (res.ok) {
542
- state.cfg = await res.json();
543
- return state.cfg;
544
- }
545
- } catch (e) {}
546
- if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
547
- delay *= 2;
548
- }
549
- return null;
550
- })();
551
-
552
- try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
553
- }
554
-
555
- function initPools(cfg) {
556
- if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
557
- if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
558
- if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
559
- }
253
+ if (!target || !target.insertAdjacentElement) return null;
254
+ if (findWrap(kindClass, afterPos)) return null;
255
+ if (insertingIds.has(id)) return null;
256
+
257
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
258
+ if (existingPh && existingPh.isConnected) return null;
259
+
260
+ insertingIds.add(id);
261
+ try {
262
+ const wrap = buildWrap(target, id, kindClass, afterPos);
263
+ target.insertAdjacentElement('afterend', wrap);
264
+ return wrap;
265
+ } finally {
266
+ insertingIds.delete(id);
267
+ }
268
+ }
269
+
270
+ function pickId(pool) {
271
+ return pool.length ? pool.shift() : null;
272
+ }
273
+
274
+ function showAd(id) {
275
+ if (!id || EZOIC_BLOCKED) return;
276
+
277
+ const now = Date.now();
278
+ const last = state.lastShowById.get(id) || 0;
279
+ if (now - last < 1500) return; // basic throttle
280
+
281
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
282
+ if (!ph || !ph.isConnected) return;
283
+
284
+ state.lastShowById.set(id, now);
285
+
286
+ try {
287
+ window.ezstandalone = window.ezstandalone || {};
288
+ const ez = window.ezstandalone;
289
+
290
+ // Fast path
291
+ if (typeof ez.showAds === 'function') {
292
+ ez.showAds(id);
293
+ sessionDefinedIds.add(id);
294
+ return;
295
+ }
296
+
297
+ // Queue once for when Ezoic is ready
298
+ ez.cmd = ez.cmd || [];
299
+ if (!ph.__ezoicQueued) {
300
+ ph.__ezoicQueued = true;
301
+ ez.cmd.push(() => {
302
+ try {
303
+ if (EZOIC_BLOCKED) return;
304
+ const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
305
+ if (!el || !el.isConnected) return;
306
+ window.ezstandalone.showAds(id);
307
+ sessionDefinedIds.add(id);
308
+ } catch (e) {}
309
+ });
310
+ }
311
+ } catch (e) {}
312
+ }
313
+
314
+ // ---------- preload / above-the-fold ----------
315
+
316
+ function ensurePreloadObserver() {
317
+ if (state.io) return state.io;
318
+ try {
319
+ state.io = new IntersectionObserver((entries) => {
320
+ for (const ent of entries) {
321
+ if (!ent.isIntersecting) continue;
322
+ const el = ent.target;
323
+ try { state.io && state.io.unobserve(el); } catch (e) {}
324
+
325
+ const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
326
+ const id = parseInt(idAttr, 10);
327
+ if (Number.isFinite(id) && id > 0) showAd(id);
328
+ }
329
+ }, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
330
+ } catch (e) {
331
+ state.io = null;
332
+ }
333
+ return state.io;
334
+ }
335
+
336
+ function observePlaceholder(id) {
337
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
338
+ if (!ph || !ph.isConnected) return;
339
+ const io = ensurePreloadObserver();
340
+ try { io && io.observe(ph); } catch (e) {}
341
+
342
+ // If already above fold, fire immediately
343
+ try {
344
+ const r = ph.getBoundingClientRect();
345
+ if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
346
+ } catch (e) {}
347
+ }
348
+
349
+ // ---------- insertion logic ----------
560
350
 
561
351
  function computeTargets(count, interval, showFirst) {
562
- const out = [];
563
- if (count <= 0) return out;
564
- if (showFirst) out.push(1);
565
- for (let i = 1; i <= count; i++) {
566
- if (i % interval === 0) out.push(i);
567
- }
568
- return Array.from(new Set(out)).sort((a, b) => a - b);
352
+ const out = [];
353
+ if (count <= 0) return out;
354
+ if (showFirst) out.push(1);
355
+ for (let i = 1; i <= count; i++) {
356
+ if (i % interval === 0) out.push(i);
357
+ }
358
+ return Array.from(new Set(out)).sort((a, b) => a - b);
359
+ }
360
+
361
+ function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
362
+ if (!items.length) return 0;
363
+
364
+ const targets = computeTargets(items.length, interval, showFirst);
365
+ let inserted = 0;
366
+
367
+ for (const afterPos of targets) {
368
+ if (inserted >= MAX_INSERTS_PER_RUN) break;
369
+
370
+ const el = items[afterPos - 1];
371
+ if (!el || !el.isConnected) continue;
372
+ if (isAdjacentAd(el)) continue;
373
+ if (findWrap(kindClass, afterPos)) continue;
374
+
375
+ const id = pickId(pool);
376
+ if (!id) break;
377
+
378
+ usedSet.add(id);
379
+ const wrap = insertAfter(el, id, kindClass, afterPos);
380
+ if (!wrap) {
381
+ usedSet.delete(id);
382
+ pool.unshift(id);
383
+ continue;
384
+ }
385
+
386
+ observePlaceholder(id);
387
+ inserted += 1;
388
+ }
389
+
390
+ return inserted;
391
+ }
392
+
393
+ async function insertHeroAdEarly() {
394
+ if (state.heroDoneForPage) return;
395
+ const cfg = await fetchConfigOnce();
396
+ if (!cfg || cfg.excluded) return;
397
+
398
+ initPools(cfg);
399
+
400
+ const kind = getKind();
401
+ let items = [];
402
+ let pool = null;
403
+ let usedSet = null;
404
+ let kindClass = '';
405
+
406
+ if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
407
+ items = getPostContainers();
408
+ pool = state.poolPosts;
409
+ usedSet = state.usedPosts;
410
+ kindClass = 'ezoic-ad-message';
411
+ } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
412
+ items = getTopicItems();
413
+ pool = state.poolTopics;
414
+ usedSet = state.usedTopics;
415
+ kindClass = 'ezoic-ad-between';
416
+ } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
417
+ items = getCategoryItems();
418
+ pool = state.poolCategories;
419
+ usedSet = state.usedCategories;
420
+ kindClass = 'ezoic-ad-categories';
421
+ } else {
422
+ return;
423
+ }
424
+
425
+ if (!items.length) return;
426
+
427
+ // Insert after the very first item (above-the-fold)
428
+ const afterPos = 1;
429
+ const el = items[afterPos - 1];
430
+ if (!el || !el.isConnected) return;
431
+ if (isAdjacentAd(el)) return;
432
+ if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
433
+
434
+ const id = pickId(pool);
435
+ if (!id) return;
436
+
437
+ usedSet.add(id);
438
+ const wrap = insertAfter(el, id, kindClass, afterPos);
439
+ if (!wrap) {
440
+ usedSet.delete(id);
441
+ pool.unshift(id);
442
+ return;
443
+ }
444
+
445
+ state.heroDoneForPage = true;
446
+ observePlaceholder(id);
569
447
  }
570
448
 
571
- function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
572
- if (!items.length) return 0;
573
- const targets = computeTargets(items.length, interval, showFirst);
574
-
575
- let inserted = 0;
576
- for (const afterPos of targets) {
577
- if (inserted >= MAX_INSERTS_PER_RUN) break;
578
-
579
- const el = items[afterPos - 1];
580
- if (!el || !el.isConnected) continue;
581
-
582
- if (isAdjacentAd(el) || isPrevAd(el)) {
583
- continue;
584
- }
585
-
586
- // Prevent back-to-back at load
587
- const prevWrap = findWrap(kindClass, afterPos - 1);
588
- if (prevWrap) continue;
589
-
590
- if (findWrap(kindClass, afterPos)) continue;
591
-
592
- const pick = pickId(kindPool, []);
593
- const id = pick.id;
594
- if (!id) break;
595
-
596
- let wrap = null;
597
- if (pick.recycled && pick.recycled.wrap) {
598
- if (sessionDefinedIds.has(id)) {
599
- destroyPlaceholderIds([id]);
600
- }
601
- const oldWrap = pick.recycled.wrap;
602
- try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
603
- try { oldWrap && oldWrap.remove(); } catch (e) {}
604
- wrap = insertAfter(el, id, kindClass, afterPos);
605
- if (!wrap) continue;
606
- setTimeout(() => {
607
- callShowAdsWhenReady(id);
608
- }, 50);
609
- } else {
610
- usedSet.add(id);
611
- wrap = insertAfter(el, id, kindClass, afterPos);
612
- if (!wrap) continue;
613
- // Micro-délai pour laisser le DOM se synchroniser
614
- // Appel immédiat au lieu de 10ms delay
615
- callShowAdsWhenReady(id);
449
+ async function runCore() {
450
+ if (EZOIC_BLOCKED) return;
451
+
452
+ patchShowAds();
453
+
454
+ const cfg = await fetchConfigOnce();
455
+ if (!cfg || cfg.excluded) return;
456
+ initPools(cfg);
457
+
458
+ const kind = getKind();
459
+
460
+ if (kind === 'topic') {
461
+ if (normalizeBool(cfg.enableMessageAds)) {
462
+ injectBetween(
463
+ 'ezoic-ad-message',
464
+ getPostContainers(),
465
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
466
+ normalizeBool(cfg.showFirstMessageAd),
467
+ state.poolPosts,
468
+ state.usedPosts
469
+ );
470
+ }
471
+ } else if (kind === 'categoryTopics') {
472
+ if (normalizeBool(cfg.enableBetweenAds)) {
473
+ injectBetween(
474
+ 'ezoic-ad-between',
475
+ getTopicItems(),
476
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
477
+ normalizeBool(cfg.showFirstTopicAd),
478
+ state.poolTopics,
479
+ state.usedTopics
480
+ );
481
+ }
482
+ } else if (kind === 'categories') {
483
+ if (normalizeBool(cfg.enableCategoryAds)) {
484
+ injectBetween(
485
+ 'ezoic-ad-categories',
486
+ getCategoryItems(),
487
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
488
+ normalizeBool(cfg.showFirstCategoryAd),
489
+ state.poolCategories,
490
+ state.usedCategories
491
+ );
492
+ }
493
+ }
616
494
  }
617
495
 
618
- liveArr.push({ id, wrap });
619
- if (wrap && (
620
- (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
621
- )) {
622
- try { wrap.remove(); } catch (e) {}
623
- if (!(pick.recycled && pick.recycled.wrap)) {
624
- try { kindPool.unshift(id); } catch (e) {}
625
- usedSet.delete(id);
626
- }
627
- continue;
628
- }
629
- inserted += 1;
630
- }
631
- return inserted;
496
+ function scheduleRun() {
497
+ if (state.runQueued) return;
498
+ state.runQueued = true;
499
+ window.requestAnimationFrame(() => {
500
+ state.runQueued = false;
501
+ const pk = getPageKey();
502
+ if (state.pageKey && pk !== state.pageKey) return;
503
+ runCore().catch(() => {});
504
+ });
632
505
  }
633
506
 
634
- function enforceNoAdjacentAds() {
635
- const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
636
- for (let i = 0; i < ads.length; i++) {
637
- const ad = ads[i], prev = ad.previousElementSibling;
638
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
639
- try {
640
- const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
641
- if (ph) {
642
- const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
643
- if (Number.isFinite(id) && id > 0) {
644
- // Détruire le placeholder si Ezoic l'a déjà défini
645
- if (sessionDefinedIds.has(id)) {
646
- destroyPlaceholderIds([id]);
647
- }
648
- }
649
- }
650
- ad.remove();
651
- } catch (e) {}
652
- }
653
- }
654
- }
507
+ // ---------- observers / lifecycle ----------
655
508
 
656
509
  function cleanup() {
657
- // DEBUG: Vérifier que cleanup est appelé
658
- // CRITIQUE: BLOQUER Ezoic immédiatement
659
- EZOIC_BLOCKED = true;
660
-
661
- // Détruire TOUS les placeholders Ezoic AVANT de supprimer DOM
662
- const allWrappers = document.querySelectorAll('.ezoic-ad');
663
- const allIds = [];
664
- allWrappers.forEach(wrapper => {
665
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
666
- if (ph) {
667
- const match = ph.id.match(/\d+/);
668
- if (match) allIds.push(parseInt(match[0]));
669
- }
670
- });
671
-
672
- // CRITIQUE: Vider COMPLÈTEMENT sessionDefinedIds
673
- // Pour éviter que d'anciens IDs soient encore en mémoire
674
-
675
- // CRITIQUE: Détruire AUSSI tous les IDs tracés dans state
676
- // Pour annuler les anciens IDs qu'Ezoic a en mémoire
677
- const trackedIds = [
678
- ...Array.from(state.usedTopics),
679
- ...Array.from(state.usedPosts),
680
- ...Array.from(state.usedCategories)
681
- ];
682
-
683
- const allIdsToDestroy = [...new Set([...allIds, ...trackedIds, ...Array.from(sessionDefinedIds)])];
684
- sessionDefinedIds.clear(); // ✅ VIDER TOUT
685
-
686
- if (allIdsToDestroy.length > 0) {
687
- destroyPlaceholderIds(allIdsToDestroy);
688
- }
689
-
690
- // Annuler batch showAds en attente
691
- pendingShowAdsIds.clear();
692
- clearTimeout(batchShowAdsTimer);
510
+ EZOIC_BLOCKED = true;
693
511
 
694
- // Maintenant supprimer DOM
695
- allWrappers.forEach(el => {
696
- try { el.remove(); } catch (e) {}
697
- });
512
+ // remove all wrappers
513
+ try {
514
+ document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
515
+ try { el.remove(); } catch (e) {}
516
+ });
517
+ } catch (e) {}
698
518
 
699
- state.pageKey = getPageKey();
700
- state.cfg = null;
701
- state.cfgPromise = null;
702
-
703
- state.poolTopics = [];
704
- state.poolPosts = [];
705
- state.poolCategories = [];
706
- state.usedTopics.clear();
707
- state.usedPosts.clear();
708
- state.usedCategories.clear();
709
- state.lastShowById.clear();
710
- state.pendingById.clear();
711
- state.definedIds.clear();
712
-
713
- state.activeTimeouts.forEach(id => {
714
- try { clearTimeout(id); } catch (e) {}
715
- });
716
- state.activeTimeouts.clear();
717
-
718
- state.pendingById.clear();
719
-
720
- if (state.obs) { state.obs.disconnect(); state.obs = null; }
721
-
722
- state.scheduled = false;
723
- clearTimeout(state.timer);
724
- state.timer = null;
725
- }
519
+ // reset state
520
+ state.cfg = null;
521
+ state.poolTopics = [];
522
+ state.poolPosts = [];
523
+ state.poolCategories = [];
524
+ state.usedTopics.clear();
525
+ state.usedPosts.clear();
526
+ state.usedCategories.clear();
527
+ state.lastShowById.clear();
528
+ state.heroDoneForPage = false;
726
529
 
727
- function ensureObserver() {
728
- if (state.obs) return;
729
- state.obs = new MutationObserver(() => scheduleRun('mutation'));
730
- try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
731
- }
530
+ sessionDefinedIds.clear();
732
531
 
733
- async function runCore() {
734
- // CRITIQUE: Ne rien insérer si navigation en cours
735
- if (EZOIC_BLOCKED) {
736
- return;
737
- }
738
-
739
- if (!state.canShowAds) {
740
- return;
532
+ // keep observers alive (MutationObserver will re-trigger after navigation)
741
533
  }
742
534
 
743
- patchShowAds();
744
-
745
- const cfg = await fetchConfig();
746
- if (!cfg || cfg.excluded) return;
747
-
748
- initPools(cfg);
749
-
750
- const kind = getKind();
751
- let inserted = 0;
752
-
753
- if (kind === 'topic') {
754
- if (normalizeBool(cfg.enableMessageAds)) {
755
- inserted = injectBetween('ezoic-ad-message', getPostContainers(),
756
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
757
- normalizeBool(cfg.showFirstMessageAd),
758
- state.poolPosts,
759
- state.usedPosts);
760
- }
761
- } else if (kind === 'categoryTopics') {
762
- if (normalizeBool(cfg.enableBetweenAds)) {
763
- inserted = injectBetween('ezoic-ad-between', getTopicItems(),
764
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
765
- normalizeBool(cfg.showFirstTopicAd),
766
- state.poolTopics,
767
- state.usedTopics);
768
- }
769
- } else if (kind === 'categories') {
770
- if (normalizeBool(cfg.enableCategoryAds)) {
771
- inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
772
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
773
- normalizeBool(cfg.showFirstCategoryAd),
774
- state.poolCategories,
775
- state.usedCategories);
535
+ function ensureDomObserver() {
536
+ if (state.domObs) return;
537
+ state.domObs = new MutationObserver(() => {
538
+ if (!EZOIC_BLOCKED) scheduleRun();
539
+ });
540
+ try {
541
+ state.domObs.observe(document.body, { childList: true, subtree: true });
542
+ } catch (e) {}
776
543
  }
777
- }
778
-
779
- enforceNoAdjacentAds();
780
544
 
781
- let count = 0;
782
- if (kind === 'topic') count = getPostContainers().length;
783
- else if (kind === 'categoryTopics') count = getTopicItems().length;
784
- else if (kind === 'categories') count = getCategoryItems().length;
545
+ function bindNodeBB() {
546
+ if (!$) return;
785
547
 
786
- if (count === 0 && 0 < 25) {
787
- setTimeout(arguments[0], 50);
788
- return;
789
- }
548
+ $(window).off('.ezoicInfinite');
790
549
 
791
- if (inserted >= MAX_INSERTS_PER_RUN) {
792
- setTimeout(arguments[0], 50);
793
- } else if (inserted === 0 && count > 0) {
794
- // Pool épuisé ou recyclage pas encore disponible.
795
- if (state.poolWaitAttempts < 8) {
796
- state.poolWaitAttempts += 1;
797
- setTimeout(arguments[0], 50);
798
- } else {
799
- }
800
- } else if (inserted > 0) {
801
- }
802
- }
550
+ $(window).on('action:ajaxify.start.ezoicInfinite', () => {
551
+ cleanup();
552
+ });
803
553
 
804
- function scheduleRun() {
805
- if (state.scheduled) return;
806
- state.scheduled = true;
807
-
808
- clearTimeout(state.timer);
809
- state.timer = setTimeout(() => {
810
- state.scheduled = false;
811
- const pk = getPageKey();
812
- if (state.pageKey && pk !== state.pageKey) return;
813
- runCore().catch(() => {});
814
- }, 50);
815
- }
554
+ $(window).on('action:ajaxify.end.ezoicInfinite', () => {
555
+ state.pageKey = getPageKey();
556
+ EZOIC_BLOCKED = false;
816
557
 
817
- function bind() {
818
- if (!$) return;
558
+ warmUpNetwork();
559
+ patchShowAds();
560
+ ensurePreloadObserver();
561
+ ensureDomObserver();
819
562
 
820
- $(window).off('.ezoicInfinite');
563
+ // Ultra-fast above-the-fold first
564
+ insertHeroAdEarly().catch(() => {});
821
565
 
822
- $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
566
+ // Then normal insertion
567
+ scheduleRun();
568
+ });
823
569
 
824
- $(window).on('action:ajaxify.end.ezoicInfinite', () => {
825
- state.pageKey = getPageKey();
826
-
827
- // Débloquer Ezoic IMMÉDIATEMENT pour la nouvelle page
828
- EZOIC_BLOCKED = false;
829
-
830
- ensureObserver();
831
-
832
- state.canShowAds = true;
833
-
834
- // CRITIQUE: Relancer insertion maintenant que navigation est terminée
835
- scheduleRun();
836
- });
837
-
838
- $(window).on('action:category.loaded.ezoicInfinite', () => {
839
- ensureObserver();
840
- waitForContentThenRun();
841
- });
842
- $(window).on('action:topics.loaded.ezoicInfinite', () => {
843
- ensureObserver();
844
- waitForContentThenRun();
845
- });
846
-
847
- $(window).on('action:topic.loaded.ezoicInfinite', () => {
848
- ensureObserver();
849
- waitForContentThenRun();
850
- });
851
-
852
- $(window).on('action:posts.loaded.ezoicInfinite', () => {
853
- ensureObserver();
854
- // posts.loaded = infinite scroll
855
- waitForContentThenRun();
856
- });
570
+ // Infinite scroll / partial updates
571
+ $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
572
+ if (EZOIC_BLOCKED) return;
573
+ scheduleRun();
574
+ });
857
575
  }
858
576
 
859
577
  function bindScroll() {
860
- if (state.lastScrollRun > 0) return;
861
- state.lastScrollRun = Date.now();
862
- let ticking = false;
863
- window.addEventListener('scroll', () => {
864
- if (ticking) return;
865
- ticking = true;
866
- window.requestAnimationFrame(() => {
867
- ticking = false;
868
- enforceNoAdjacentAds();
869
- const now = Date.now();
870
- if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
871
- state.lastScrollRun = now;
872
- scheduleRun();
873
- }
874
- });
875
- }, { passive: true });
876
- }
877
-
878
- function waitForContentThenRun() {
879
- const MIN_WORDS = 250;
880
- let attempts = 0;
881
- const maxAttempts = 20; // 20 × 200ms = 4s max
882
-
883
- (function check() {
884
- attempts++;
885
-
886
- // Compter les mots sur la page
887
- const text = document.body.innerText || '';
888
- const wordCount = text.split(/\s+/).filter(Boolean).length;
889
-
890
- if (wordCount >= MIN_WORDS) {
891
- // Assez de contenu → lancer l'insertion
892
- scheduleRun();
893
- return;
894
- }
895
-
896
- // Pas assez de contenu
897
- if (attempts >= maxAttempts) {
898
- // Timeout après 4s → tenter quand même
899
- scheduleRun();
900
- return;
901
- }
902
-
903
- // Réessayer dans 200ms
904
- setTimeout(check, 50);
905
- })();
578
+ let ticking = false;
579
+ window.addEventListener('scroll', () => {
580
+ if (ticking) return;
581
+ ticking = true;
582
+ window.requestAnimationFrame(() => {
583
+ ticking = false;
584
+ if (!EZOIC_BLOCKED) scheduleRun();
585
+ });
586
+ }, { passive: true });
906
587
  }
907
588
 
908
- function waitForEzoicThenRun() {
909
- let attempts = 0;
910
- const maxAttempts = 50; // 50 × 200ms = 10s max
589
+ // ---------- boot ----------
911
590
 
912
- (function check() {
913
- attempts++;
914
- // Vérifier si Ezoic est chargé
915
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
916
- // Ezoic est prêt → lancer l'insertion
917
- scheduleRun();
918
- waitForContentThenRun();
919
- return;
920
- }
921
- // Ezoic pas encore prêt
922
- if (attempts >= maxAttempts) {
923
- // Tenter quand même
924
- scheduleRun();
925
- return;
926
- }
927
- // Réessayer dans 200ms
928
- setTimeout(check, 50);
929
- })();
930
- }
591
+ state.pageKey = getPageKey();
592
+ warmUpNetwork();
593
+ patchShowAds();
594
+ ensurePreloadObserver();
595
+ ensureDomObserver();
931
596
 
932
- cleanup();
933
- bind();
597
+ bindNodeBB();
934
598
  bindScroll();
935
- ensureObserver();
936
- state.pageKey = getPageKey();
937
599
 
938
- // Attendre que Ezoic soit chargé avant d'insérer
939
- waitForEzoicThenRun();
940
- })();
600
+ // First paint: try hero + run
601
+ EZOIC_BLOCKED = false;
602
+ insertHeroAdEarly().catch(() => {});
603
+ scheduleRun();
604
+ })();