nodebb-plugin-ezoic-infinite 1.4.91 → 1.4.92

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.4.91",
3
+ "version": "1.4.92",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/library.js CHANGED
@@ -27,13 +27,21 @@ async function getAllGroups() {
27
27
  }
28
28
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
29
  const data = await groups.getGroupsData(filtered);
30
- // Sort alphabetically for ACP usability
31
- data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
32
- return data;
30
+ // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
+ const valid = data.filter(g => g && g.name);
32
+ valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
+ return valid;
33
34
  }
35
+ let _settingsCache = null;
36
+ let _settingsCacheAt = 0;
37
+ const SETTINGS_TTL = 30000; // 30s
38
+
34
39
  async function getSettings() {
40
+ const now = Date.now();
41
+ if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
35
42
  const s = await meta.settings.get(SETTINGS_KEY);
36
- return {
43
+ _settingsCacheAt = Date.now();
44
+ _settingsCache = {
37
45
  // Between-post ads (simple blocks) in category topic list
38
46
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
47
  showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
@@ -54,6 +62,7 @@ async function getSettings() {
54
62
 
55
63
  excludedGroups: normalizeExcludedGroups(s.excludedGroups),
56
64
  };
65
+ return _settingsCache;
57
66
  }
58
67
 
59
68
  async function isUserExcluded(uid, excludedGroups) {
@@ -62,6 +71,13 @@ async function isUserExcluded(uid, excludedGroups) {
62
71
  return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
63
72
  }
64
73
 
74
+ plugin.onSettingsSet = function (data) {
75
+ // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
76
+ if (data && data.hash === SETTINGS_KEY) {
77
+ _settingsCache = null;
78
+ }
79
+ };
80
+
65
81
  plugin.addAdminNavigation = async (header) => {
66
82
  header.plugins = header.plugins || [];
67
83
  header.plugins.push({
@@ -89,7 +105,7 @@ plugin.init = async ({ router, middleware }) => {
89
105
  router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
90
106
  router.get('/api/admin/plugins/ezoic-infinite', render);
91
107
 
92
- router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
108
+ router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
93
109
  const settings = await getSettings();
94
110
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
95
111
 
@@ -11,6 +11,10 @@
11
11
  {
12
12
  "hook": "filter:admin.header.build",
13
13
  "method": "addAdminNavigation"
14
+ },
15
+ {
16
+ "hook": "action:settings.set",
17
+ "method": "onSettingsSet"
14
18
  }
15
19
  ],
16
20
  "staticDirs": {
@@ -13,11 +13,10 @@
13
13
  e.preventDefault();
14
14
 
15
15
  Settings.save('ezoic-infinite', $form, function () {
16
- // Toast vert (NodeBB core)
17
16
  if (alerts && typeof alerts.success === 'function') {
18
- alerts.success('Enregistré');
17
+ alerts.success('[[admin/settings:saved]]');
19
18
  } else if (window.app && typeof window.app.alertSuccess === 'function') {
20
- window.app.alertSuccess('Enregistré');
19
+ window.app.alertSuccess('[[admin/settings:saved]]');
21
20
  }
22
21
  });
23
22
  });
@@ -1,371 +1,827 @@
1
1
  (function () {
2
2
  'use strict';
3
-
4
3
  const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
5
-
6
4
  const SELECTORS = {
7
- topicItem: 'li[component="category/topic"]',
8
- postItem: '[component="post"][data-pid]',
9
- categoryItem: 'li[component="categories/category"]',
10
- };
11
-
12
- const WRAP_CLASS = 'ezoic-ad';
13
- const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
14
- const MAX_INSERTS_PER_RUN = 3;
15
-
16
- const state = {
17
- pageKey: null,
18
- cfg: null,
19
- cfgPromise: null,
20
- poolTopics: [],
21
- poolPosts: [],
22
- poolCategories: [],
23
- usedTopics: new Set(),
24
- usedPosts: new Set(),
25
- usedCategories: new Set(),
26
- lastShowById: new Map(),
27
- canShowAds: false,
28
- scheduled: false,
29
- timer: null,
30
- obs: null,
31
- };
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;
32
10
 
11
+ // Nécessaire pour savoir si on doit appeler destroyPlaceholders avant recyclage.
33
12
  const sessionDefinedIds = new Set();
34
13
 
14
+ const insertingIds = new Set(), state = {
15
+ pageKey: null,
16
+ cfg: null,
17
+ cfgPromise: null,
18
+
19
+ poolTopics: [],
20
+ poolPosts: [],
21
+ poolCategories: [],
22
+
23
+ usedTopics: new Set(),
24
+ usedPosts: new Set(),
25
+ usedCategories: new Set(),
26
+
27
+ lastShowById: new Map(),
28
+ pendingById: new Set(),
29
+ definedIds: new Set(),
30
+
31
+ scheduled: false,
32
+ timer: null,
33
+
34
+ obs: null,
35
+ activeTimeouts: new Set(),
36
+ lastScrollRun: 0, };
37
+
35
38
  function normalizeBool(v) {
36
- return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
39
+ return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
37
40
  }
38
41
 
39
42
  function uniqInts(lines) {
40
- const out = [], seen = new Set();
41
- for (const v of lines) {
42
- const n = parseInt(v, 10);
43
- if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
44
- seen.add(n);
45
- out.push(n);
46
- }
47
- }
48
- return out;
43
+ const out = [], seen = new Set();
44
+ for (const v of lines) {
45
+ const n = parseInt(v, 10);
46
+ if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
47
+ seen.add(n);
48
+ out.push(n);
49
+ }
50
+ }
51
+ return out;
49
52
  }
50
53
 
51
54
  function parsePool(raw) {
52
- if (!raw) return [];
53
- const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
54
- return uniqInts(lines);
55
+ if (!raw) return [];
56
+ const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
57
+ return uniqInts(lines);
55
58
  }
56
59
 
57
60
  function getPageKey() {
58
- try {
59
- const ax = window.ajaxify;
60
- if (ax && ax.data) {
61
- if (ax.data.tid) return `topic:${ax.data.tid}`;
62
- if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
63
- }
64
- } catch (e) {}
65
- return window.location.pathname;
61
+ try {
62
+ const ax = window.ajaxify;
63
+ if (ax && ax.data) {
64
+ if (ax.data.tid) return `topic:${ax.data.tid}`;
65
+ if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
66
+ }
67
+ } catch (e) {}
68
+ return window.location.pathname;
66
69
  }
67
70
 
68
71
  function getKind() {
69
- const p = window.location.pathname || '';
70
- if (/^\/topic\//.test(p)) return 'topic';
71
- if (/^\/category\//.test(p)) return 'categoryTopics';
72
- if (p === '/' || /^\/categories/.test(p)) return 'categories';
73
- return 'other';
72
+ const p = window.location.pathname || '';
73
+ if (/^\/topic\//.test(p)) return 'topic';
74
+ if (/^\/category\//.test(p)) return 'categoryTopics';
75
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
76
+ // fallback by DOM
77
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
78
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
79
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
80
+ return 'other';
74
81
  }
75
82
 
76
83
  function getTopicItems() {
77
- return Array.from(document.querySelectorAll(SELECTORS.topicItem));
84
+ return Array.from(document.querySelectorAll(SELECTORS.topicItem));
85
+ }
86
+
87
+ function getCategoryItems() {
88
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
78
89
  }
79
90
 
80
91
  function getPostContainers() {
81
- return Array.from(document.querySelectorAll(SELECTORS.postItem));
92
+ const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
93
+ return nodes.filter((el) => {
94
+ if (!el || !el.isConnected) return false;
95
+ if (!el.querySelector('[component="post/content"]')) return false;
96
+ const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
97
+ if (parentPost && parentPost !== el) return false;
98
+ if (el.getAttribute('component') === 'post/parent') return false;
99
+ return true;
100
+ });
82
101
  }
83
102
 
84
- function getCategoryItems() {
85
- return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
103
+ function safeRect(el) {
104
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
86
105
  }
87
106
 
88
- async function fetchConfig() {
89
- if (state.cfg) return state.cfg;
90
- if (state.cfgPromise) return state.cfgPromise;
107
+ function destroyPlaceholderIds(ids) {
108
+ if (!ids || !ids.length) return;
109
+ // Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
110
+ const filtered = ids.filter((id) => {
111
+ // Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
112
+ try { return sessionDefinedIds.has(id); } catch (e) { return true; }
113
+ });
114
+ if (!filtered.length) return;
115
+
116
+ const call = () => {
117
+ try {
118
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
119
+ window.ezstandalone.destroyPlaceholders(filtered);
120
+ }
121
+ } catch (e) {}
122
+ };
123
+ try {
124
+ window.ezstandalone = window.ezstandalone || {};
125
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
126
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
127
+ else window.ezstandalone.cmd.push(call);
128
+ } catch (e) {}
129
+ }
91
130
 
92
- state.cfgPromise = (async () => {
93
- try {
94
- const res = await fetch('/api/ezoic-infinite/config');
95
- if (!res.ok) return null;
96
- const cfg = await res.json();
97
- state.cfg = cfg;
98
- return cfg;
99
- } catch (e) {
100
- return null;
101
- }
102
- })();
131
+
132
+ // Nettoyer les wrappers vides (sans pub) pour éviter espaces verticaux
133
+ function cleanupEmptyWrappers() {
134
+ try {
135
+ document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
136
+ const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
137
+ if (ph && ph.children.length === 0) {
138
+ // Placeholder vide après 3s = pub non chargée
139
+ setTimeout(() => {
140
+ if (ph.children.length === 0) {
141
+ wrapper.remove();
142
+ }
143
+ }, 3000);
144
+ }
145
+ });
146
+ } catch (e) {}
147
+ }
103
148
 
104
- return state.cfgPromise;
149
+ function getRecyclable(liveArr) {
150
+ const margin = 600;
151
+ for (let i = 0; i < liveArr.length; i++) {
152
+ const entry = liveArr[i];
153
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
154
+ const r = safeRect(entry.wrap);
155
+ if (r && r.bottom < -margin) {
156
+ liveArr.splice(i, 1);
157
+ return entry;
158
+ }
159
+ }
160
+ return null;
105
161
  }
106
162
 
107
- function initPools(cfg) {
108
- if (!state.poolTopics.length) state.poolTopics = parsePool(cfg.poolTopics);
109
- if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.poolPosts);
110
- if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.poolCategories);
163
+ function pickId(pool, liveArr) {
164
+ if (pool.length) return { id: pool.shift(), recycled: null };
165
+ const recycled = getRecyclable(liveArr);
166
+ if (recycled) return { id: recycled.id, recycled };
167
+ return { id: null, recycled: null };
111
168
  }
112
169
 
113
- function destroyPlaceholderIds(ids) {
114
- if (!ids || !ids.length) return;
115
- const filtered = ids.filter(id => Number.isFinite(id) && id > 0);
116
- if (!filtered.length) return;
117
-
118
- try {
119
- window.ezstandalone = window.ezstandalone || {};
120
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
121
-
122
- const call = () => {
123
- if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
124
- window.ezstandalone.destroyPlaceholders(filtered);
125
- }
126
- };
127
-
128
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
129
- call();
130
- } else {
131
- window.ezstandalone.cmd.push(call);
132
- }
133
-
134
- // Recyclage: libérer après 100ms
135
- setTimeout(() => {
136
- filtered.forEach(id => sessionDefinedIds.delete(id));
137
- }, 100);
138
- } catch (e) {}
170
+ function resetPlaceholderInWrap(wrap, id) {
171
+ try {
172
+ if (!wrap) return;
173
+ try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
174
+ if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
175
+ const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
176
+ if (old) old.remove();
177
+ // Remove any leftover markup inside wrapper
178
+ wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
179
+ const ph = document.createElement('div');
180
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
181
+ wrap.appendChild(ph);
182
+ } catch (e) {}
139
183
  }
140
184
 
141
- function buildWrap(id, kindClass, afterPos) {
142
- const wrap = document.createElement('div');
143
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
144
- wrap.setAttribute('data-ezoic-after', String(afterPos));
145
- wrap.style.width = '100%';
185
+ function isAdjacentAd(target) {
186
+ if (!target || !target.nextElementSibling) return false;
187
+ const next = target.nextElementSibling;
188
+ if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
189
+ return false;
190
+ }
191
+
192
+ function isPrevAd(target) {
193
+ if (!target || !target.previousElementSibling) return false;
194
+ const prev = target.previousElementSibling;
195
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
196
+ return false;
197
+ }
146
198
 
147
- const ph = document.createElement('div');
148
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
149
- wrap.appendChild(ph);
150
- return wrap;
199
+ function buildWrap(id, kindClass, afterPos) {
200
+ const wrap = document.createElement('div');
201
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
202
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
203
+ wrap.style.width = '100%';
204
+
205
+ const ph = document.createElement('div');
206
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
207
+ wrap.appendChild(ph);
208
+ return wrap;
151
209
  }
152
210
 
153
211
  function findWrap(kindClass, afterPos) {
154
- return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
212
+ return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
155
213
  }
156
214
 
157
215
  function insertAfter(target, id, kindClass, afterPos) {
158
- if (!target || !target.insertAdjacentElement) return null;
159
- if (findWrap(kindClass, afterPos)) return null;
216
+ if (!target || !target.insertAdjacentElement) return null;
217
+ if (findWrap(kindClass, afterPos)) return null;
218
+
219
+ // CRITICAL: Double-lock pour éviter race conditions sur les doublons
220
+ // 1. Vérifier qu'aucun autre thread n'est en train d'insérer cet ID
221
+ if (insertingIds.has(id)) return null;
222
+
223
+ // 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
224
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
225
+ if (existingPh && existingPh.isConnected) return null;
226
+
227
+ // Acquérir le lock
228
+ insertingIds.add(id);
229
+
230
+ try {
231
+ const wrap = buildWrap(id, kindClass, afterPos);
232
+ target.insertAdjacentElement('afterend', wrap);
233
+ attachFillObserver(wrap, id);
234
+ return wrap;
235
+ } finally {
236
+ // Libérer le lock après 100ms (le temps que le DOM soit stable)
237
+ setTimeout(() => insertingIds.delete(id), 50);
238
+ }
239
+ }
160
240
 
161
- try {
162
- const wrap = buildWrap(id, kindClass, afterPos);
163
- target.insertAdjacentElement('afterend', wrap);
164
- return wrap;
165
- } catch (e) {
166
- return null;
167
- }
241
+ function destroyUsedPlaceholders() {
242
+ const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
243
+ if (ids.length) destroyPlaceholderIds(ids);
168
244
  }
169
245
 
170
- function pickId(pool) {
171
- if (!pool || !pool.length) return null;
172
- return pool.shift();
246
+ function patchShowAds() {
247
+ // Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
248
+ // Also ensures the patch is applied even if Ezoic loads after our script.
249
+ const applyPatch = () => {
250
+ try {
251
+ window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
252
+ if (window.__nodebbEzoicPatched) return;
253
+ if (typeof ez.showAds !== 'function') return;
254
+
255
+ window.__nodebbEzoicPatched = true;
256
+ const orig = ez.showAds;
257
+
258
+ ez.showAds = function (arg) {
259
+ if (Array.isArray(arg)) {
260
+ const seen = new Set();
261
+ for (const v of arg) {
262
+ const id = parseInt(v, 10);
263
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
264
+ seen.add(id);
265
+ try { orig.call(ez, id); } catch (e) {}
266
+ }
267
+ return;
173
268
  }
269
+ return orig.apply(ez, arguments);
270
+ };
271
+ } catch (e) {}
272
+ };
174
273
 
175
- function callShowAds(id) {
176
- if (!id) return;
177
-
178
- try {
179
- window.ezstandalone = window.ezstandalone || {};
180
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
181
-
182
- window.ezstandalone.cmd.push(function() {
183
- if (typeof window.ezstandalone.showAds === 'function') {
184
- window.ezstandalone.showAds(id);
185
- sessionDefinedIds.add(id);
186
- }
187
- });
188
- } catch (e) {}
274
+ applyPatch();
275
+ // Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
276
+ if (!window.__nodebbEzoicPatched) {
277
+ try {
278
+ window.ezstandalone = window.ezstandalone || {};
279
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
280
+ window.ezstandalone.cmd.push(applyPatch);
281
+ } catch (e) {}
282
+ }
189
283
  }
190
284
 
191
- function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
192
- if (!items || items.length === 0) return 0;
285
+ function markFilled(wrap) {
286
+ try {
287
+ if (!wrap) return;
288
+ // Disconnect the fill observer first (no need to remove+re-add the attribute)
289
+ if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
290
+ wrap.setAttribute('data-ezoic-filled', '1');
291
+ } catch (e) {}
292
+ }
193
293
 
194
- let inserted = 0;
195
- const targets = [];
294
+ function isWrapMarkedFilled(wrap) {
295
+ try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
296
+ }
196
297
 
197
- for (let i = 0; i < items.length; i++) {
198
- const afterPos = i + 1;
199
- if (afterPos === 1 && !showFirst) continue;
200
- if (afterPos % interval !== (showFirst ? 1 : 0)) continue;
201
- targets.push(afterPos);
202
- }
298
+ function attachFillObserver(wrap, id) {
299
+ try {
300
+ const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
301
+ if (!ph) return;
302
+ // Already filled?
303
+ if (ph.childNodes && ph.childNodes.length > 0) {
304
+ markFilled(wrap);
305
+ state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
306
+ return;
307
+ }
308
+ const obs = new MutationObserver(() => {
309
+ if (ph.childNodes && ph.childNodes.length > 0) {
310
+ markFilled(wrap);
311
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
312
+ try { obs.disconnect(); } catch (e) {}
313
+ }
314
+ });
315
+ obs.observe(ph, { childList: true, subtree: true });
316
+ // Keep a weak reference on the wrapper so we can disconnect on recycle/remove
317
+ wrap.__ezoicFillObs = obs;
318
+ } catch (e) {}
319
+ }
203
320
 
204
- for (const afterPos of targets) {
205
- if (inserted >= MAX_INSERTS_PER_RUN) break;
321
+ function isPlaceholderFilled(id) {
322
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
323
+ if (!ph || !ph.isConnected) return false;
206
324
 
207
- const el = items[afterPos - 1];
208
- if (!el || !el.isConnected) continue;
209
- if (findWrap(kindClass, afterPos)) continue;
325
+ const wrap = ph.parentElement;
326
+ if (wrap && isWrapMarkedFilled(wrap)) return true;
210
327
 
211
- const id = pickId(pool);
212
- if (!id) break;
328
+ const filled = !!(ph.childNodes && ph.childNodes.length > 0);
329
+ if (filled) {
330
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
331
+ try { markFilled(wrap); } catch (e) {}
332
+ }
333
+ return filled;
334
+ }
213
335
 
214
- const wrap = insertAfter(el, id, kindClass, afterPos);
215
- if (!wrap) continue;
336
+ // Appeler showAds() en batch selon recommandations Ezoic
337
+ // Au lieu de showAds(id1), showAds(id2)... faire showAds(id1, id2, id3...)
338
+ let batchShowAdsTimer = null;
339
+ const pendingShowAdsIds = new Set();
216
340
 
217
- usedSet.add(id);
218
- callShowAds(id);
219
- inserted += 1;
220
- }
341
+ function scheduleShowAdsBatch(id) {
342
+ if (!id) return;
221
343
 
222
- return inserted;
344
+ // CRITIQUE: Si cet ID a déjà été défini (sessionDefinedIds), le détruire d'abord
345
+ if (sessionDefinedIds.has(id)) {
346
+ try {
347
+ destroyPlaceholderIds([id]);
348
+ sessionDefinedIds.delete(id);
349
+ } catch (e) {}
223
350
  }
224
351
 
225
- async function runCore() {
226
- if (!state.canShowAds) return;
227
-
228
- const cfg = await fetchConfig();
229
- if (!cfg || cfg.excluded) return;
230
-
231
- initPools(cfg);
232
-
233
- const kind = getKind();
234
- let inserted = 0;
235
-
236
- if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
237
- inserted = injectBetween(
238
- 'ezoic-ad-message',
239
- getPostContainers(),
240
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
241
- normalizeBool(cfg.showFirstMessageAd),
242
- state.poolPosts,
243
- state.usedPosts
244
- );
245
- } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
246
- inserted = injectBetween(
247
- 'ezoic-ad-between',
248
- getTopicItems(),
249
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
250
- normalizeBool(cfg.showFirstTopicAd),
251
- state.poolTopics,
252
- state.usedTopics
253
- );
254
- } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
255
- inserted = injectBetween(
256
- 'ezoic-ad-category',
257
- getCategoryItems(),
258
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 6),
259
- normalizeBool(cfg.showFirstCategoryAd),
260
- state.poolCategories,
261
- state.usedCategories
262
- );
263
- }
352
+ // Throttle: ne pas rappeler le même ID trop vite
353
+ const now = Date.now(), last = state.lastShowById.get(id) || 0;
354
+ if (now - last < 3500) return;
355
+
356
+ // Ajouter à la batch
357
+ pendingShowAdsIds.add(id);
358
+
359
+ // Debounce: attendre 100ms pour collecter tous les IDs
360
+ clearTimeout(batchShowAdsTimer);
361
+ batchShowAdsTimer = setTimeout(() => {
362
+ if (pendingShowAdsIds.size === 0) return;
363
+
364
+ const idsArray = Array.from(pendingShowAdsIds);
365
+ pendingShowAdsIds.clear();
366
+
367
+ // Appeler showAds avec TOUS les IDs en une fois
368
+ try {
369
+ window.ezstandalone = window.ezstandalone || {};
370
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
371
+ window.ezstandalone.cmd.push(function() {
372
+ if (typeof window.ezstandalone.showAds === 'function') {
373
+ // Appel batch: showAds(id1, id2, id3...)
374
+ window.ezstandalone.showAds(...idsArray);
375
+ // Tracker tous les IDs
376
+ idsArray.forEach(id => {
377
+ state.lastShowById.set(id, Date.now());
378
+ sessionDefinedIds.add(id);
379
+ });
380
+ }
381
+ });
382
+ } catch (e) {}
383
+ }, 100);
264
384
  }
265
385
 
266
- function scheduleRun() {
267
- if (state.scheduled) return;
268
- state.scheduled = true;
386
+ function callShowAdsWhenReady(id) {
387
+ if (!id) return;
388
+
389
+ const now = Date.now(), last = state.lastShowById.get(id) || 0;
390
+ if (now - last < 3500) return;
391
+
392
+ const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
393
+ try {
394
+ window.ezstandalone = window.ezstandalone || {};
395
+ if (typeof window.ezstandalone.showAds === 'function') {
269
396
 
270
- clearTimeout(state.timer);
271
- state.timer = setTimeout(() => {
272
- state.scheduled = false;
273
- const pk = getPageKey();
274
- if (state.pageKey && pk !== state.pageKey) return;
275
- runCore().catch(() => {});
276
- }, 50);
397
+ state.lastShowById.set(id, Date.now());
398
+ window.ezstandalone.showAds(id);
399
+ sessionDefinedIds.add(id);
400
+ return true;
401
+ }
402
+ } catch (e) {}
403
+ return false;
404
+ };
405
+
406
+ const startPageKey = state.pageKey;
407
+ let attempts = 0;
408
+ (function waitForPh() {
409
+ // Abort if the user navigated away since this showAds was scheduled
410
+ if (state.pageKey !== startPageKey) return;
411
+ // Abort if another concurrent call is already handling this id
412
+ if (state.pendingById.has(id)) return;
413
+
414
+ attempts += 1;
415
+ const el = document.getElementById(phId);
416
+ if (el && el.isConnected) {
417
+ // CRITIQUE: Vérifier que le placeholder est VISIBLE
418
+
419
+ // Si on arrive ici, soit visible, soit timeout
420
+
421
+ // Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
422
+ if (doCall()) {
423
+ state.pendingById.delete(id);
424
+ return;
425
+ }
426
+
427
+ }
428
+
429
+ if (attempts < 100) {
430
+ const timeoutId = setTimeout(waitForPh, 50);
431
+ state.activeTimeouts.add(timeoutId);
432
+ }
433
+ })();
434
+ }
435
+
436
+ async function fetchConfig() {
437
+ if (state.cfg) return state.cfg;
438
+ if (state.cfgPromise) return state.cfgPromise;
439
+
440
+ state.cfgPromise = (async () => {
441
+ const MAX_TRIES = 3;
442
+ let delay = 800;
443
+ for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
444
+ try {
445
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
446
+ if (res.ok) {
447
+ state.cfg = await res.json();
448
+ return state.cfg;
449
+ }
450
+ } catch (e) {}
451
+ if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
452
+ delay *= 2;
453
+ }
454
+ return null;
455
+ })();
456
+
457
+ try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
458
+ }
459
+
460
+ function initPools(cfg) {
461
+ if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
462
+ if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
463
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
464
+ }
465
+
466
+ function computeTargets(count, interval, showFirst) {
467
+ const out = [];
468
+ if (count <= 0) return out;
469
+ if (showFirst) out.push(1);
470
+ for (let i = 1; i <= count; i++) {
471
+ if (i % interval === 0) out.push(i);
472
+ }
473
+ return Array.from(new Set(out)).sort((a, b) => a - b);
474
+ }
475
+
476
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
477
+ if (!items.length) return 0;
478
+ const targets = computeTargets(items.length, interval, showFirst);
479
+
480
+ let inserted = 0;
481
+ for (const afterPos of targets) {
482
+ if (inserted >= MAX_INSERTS_PER_RUN) break;
483
+
484
+ const el = items[afterPos - 1];
485
+ if (!el || !el.isConnected) continue;
486
+
487
+ // Prevent adjacent ads (DOM-based, robust against virtualization)
488
+ if (isAdjacentAd(el) || isPrevAd(el)) {
489
+ continue;
490
+ }
491
+
492
+ // Prevent back-to-back at load
493
+ const prevWrap = findWrap(kindClass, afterPos - 1);
494
+ if (prevWrap) continue;
495
+
496
+ if (findWrap(kindClass, afterPos)) continue;
497
+
498
+ const pick = pickId(kindPool, []);
499
+ const id = pick.id;
500
+ if (!id) break;
501
+
502
+ let wrap = null;
503
+ if (pick.recycled && pick.recycled.wrap) {
504
+ // Only destroy if Ezoic has actually defined this placeholder before
505
+ if (sessionDefinedIds.has(id)) {
506
+ destroyPlaceholderIds([id]);
507
+ }
508
+ // Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
509
+ const oldWrap = pick.recycled.wrap;
510
+ try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
511
+ try { oldWrap && oldWrap.remove(); } catch (e) {}
512
+ wrap = insertAfter(el, id, kindClass, afterPos);
513
+ if (!wrap) continue;
514
+ // Attendre que le wrapper soit dans le DOM puis appeler showAds
515
+ setTimeout(() => {
516
+ callShowAdsWhenReady(id);
517
+ }, 50);
518
+ } else {
519
+ usedSet.add(id);
520
+ wrap = insertAfter(el, id, kindClass, afterPos);
521
+ if (!wrap) continue;
522
+ // Micro-délai pour laisser le DOM se synchroniser
523
+ // Appel immédiat au lieu de 10ms delay
524
+ callShowAdsWhenReady(id);
525
+ }
526
+
527
+ liveArr.push({ id, wrap });
528
+ // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
529
+ if (wrap && (
530
+ (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
531
+ )) {
532
+ try { wrap.remove(); } catch (e) {}
533
+ // Put id back if it was newly consumed (not recycled)
534
+ if (!(pick.recycled && pick.recycled.wrap)) {
535
+ try { kindPool.unshift(id); } catch (e) {}
536
+ usedSet.delete(id);
537
+ }
538
+ continue;
539
+ }
540
+ inserted += 1;
541
+ }
542
+ return inserted;
543
+ }
544
+
545
+ function enforceNoAdjacentAds() {
546
+ const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
547
+ for (let i = 0; i < ads.length; i++) {
548
+ const ad = ads[i], prev = ad.previousElementSibling;
549
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
550
+ // Supprimer le wrapper adjacent au lieu de le cacher
551
+ try {
552
+ const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
553
+ if (ph) {
554
+ const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
555
+ if (Number.isFinite(id) && id > 0) {
556
+ // Détruire le placeholder si Ezoic l'a déjà défini
557
+ if (sessionDefinedIds.has(id)) {
558
+ destroyPlaceholderIds([id]);
559
+ }
560
+ }
561
+ }
562
+ ad.remove();
563
+ } catch (e) {}
564
+ }
565
+ }
277
566
  }
278
567
 
279
568
  function cleanup() {
280
- const allIds = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
281
- if (allIds.length) destroyPlaceholderIds(allIds);
282
-
283
- document.querySelectorAll('.ezoic-ad').forEach(el => {
284
- try { el.remove(); } catch (e) {}
285
- });
286
-
287
- state.pageKey = getPageKey();
288
- state.cfg = null;
289
- state.cfgPromise = null;
290
- state.poolTopics = [];
291
- state.poolPosts = [];
292
- state.poolCategories = [];
293
- state.usedTopics.clear();
294
- state.usedPosts.clear();
295
- state.usedCategories.clear();
296
- state.lastShowById.clear();
297
- sessionDefinedIds.clear();
298
-
299
- if (state.obs) {
300
- state.obs.disconnect();
301
- state.obs = null;
302
- }
303
-
304
- state.scheduled = false;
305
- clearTimeout(state.timer);
306
- state.timer = null;
569
+ destroyUsedPlaceholders();
570
+
571
+ // CRITIQUE: Supprimer TOUS les wrappers .ezoic-ad du DOM
572
+ // Sinon ils restent et deviennent "unused" sur la nouvelle page
573
+ document.querySelectorAll('.ezoic-ad').forEach(el => {
574
+ try { el.remove(); } catch (e) {}
575
+ });
576
+
577
+ state.pageKey = getPageKey();
578
+ state.cfg = null;
579
+ state.cfgPromise = null;
580
+
581
+ state.poolTopics = [];
582
+ state.poolPosts = [];
583
+ state.poolCategories = [];
584
+ state.usedTopics.clear();
585
+ state.usedPosts.clear();
586
+ state.usedCategories.clear();
587
+ state.lastShowById.clear();
588
+ // CRITIQUE: Vider pendingById pour annuler tous les showAds en cours
589
+ // Sinon Ezoic essaie d'accéder aux placeholders pendant que NodeBB vide le DOM
590
+ state.pendingById.clear();
591
+ state.definedIds.clear();
592
+
593
+ // NE PAS supprimer les wrappers Ezoic ici - ils seront supprimés naturellement
594
+ // quand NodeBB vide le DOM lors de la navigation ajaxify
595
+ // Les supprimer manuellement cause des problèmes avec l'état interne d'Ezoic
596
+
597
+ // CRITIQUE: Annuler TOUS les timeouts en cours pour éviter que les anciens
598
+ // showAds() continuent à s'exécuter après la navigation
599
+ state.activeTimeouts.forEach(id => {
600
+ try { clearTimeout(id); } catch (e) {}
601
+ });
602
+ state.activeTimeouts.clear();
603
+
604
+ // Vider aussi pendingById pour annuler les showAds en attente
605
+ state.pendingById.clear();
606
+
607
+ if (state.obs) { state.obs.disconnect(); state.obs = null; }
608
+
609
+ state.scheduled = false;
610
+ clearTimeout(state.timer);
611
+ state.timer = null;
307
612
  }
308
613
 
309
614
  function ensureObserver() {
310
- if (state.obs) return;
311
- state.obs = new MutationObserver(() => scheduleRun());
312
- try {
313
- state.obs.observe(document.body, { childList: true, subtree: true });
314
- } catch (e) {}
615
+ if (state.obs) return;
616
+ state.obs = new MutationObserver(() => scheduleRun('mutation'));
617
+ try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
315
618
  }
316
619
 
317
- function waitForContentThenRun() {
318
- const kind = getKind();
319
- let selector = SELECTORS.postItem;
320
- if (kind === 'categoryTopics') selector = SELECTORS.topicItem;
321
- else if (kind === 'categories') selector = SELECTORS.categoryItem;
620
+ async function runCore() {
621
+ // Attendre que canInsert soit true (protection race condition navigation)
622
+ if (!state.canShowAds) {
623
+ return;
624
+ }
625
+
626
+ patchShowAds();
322
627
 
323
- const check = () => {
324
- if (document.querySelector(selector)) {
325
- scheduleRun();
326
- } else {
327
- setTimeout(check, 200);
328
- }
329
- };
330
- check();
628
+ const cfg = await fetchConfig();
629
+ if (!cfg || cfg.excluded) return;
630
+
631
+ initPools(cfg);
632
+
633
+ const kind = getKind();
634
+ let inserted = 0;
635
+
636
+ if (kind === 'topic') {
637
+ if (normalizeBool(cfg.enableMessageAds)) {
638
+ inserted = injectBetween('ezoic-ad-message', getPostContainers(),
639
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
640
+ normalizeBool(cfg.showFirstMessageAd),
641
+ state.poolPosts,
642
+ state.usedPosts);
643
+ }
644
+ } else if (kind === 'categoryTopics') {
645
+ if (normalizeBool(cfg.enableBetweenAds)) {
646
+ inserted = injectBetween('ezoic-ad-between', getTopicItems(),
647
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
648
+ normalizeBool(cfg.showFirstTopicAd),
649
+ state.poolTopics,
650
+ state.usedTopics);
651
+ }
652
+ } else if (kind === 'categories') {
653
+ if (normalizeBool(cfg.enableCategoryAds)) {
654
+ inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
655
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
656
+ normalizeBool(cfg.showFirstCategoryAd),
657
+ state.poolCategories,
658
+ state.usedCategories);
659
+ }
331
660
  }
332
661
 
333
- function bind() {
334
- if (!$) return;
335
-
336
- $(window).off('.ezoicInfinite');
337
- $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
338
- $(window).on('action:ajaxify.end.ezoicInfinite', () => {
339
- state.pageKey = getPageKey();
340
- ensureObserver();
341
- state.canShowAds = true;
342
- scheduleRun();
343
- });
344
-
345
- $(window).on('action:category.loaded.ezoicInfinite', () => {
346
- ensureObserver();
347
- waitForContentThenRun();
348
- });
349
-
350
- $(window).on('action:topics.loaded.ezoicInfinite', () => {
351
- ensureObserver();
352
- waitForContentThenRun();
353
- });
354
- }
355
-
356
- function init() {
357
- state.pageKey = getPageKey();
358
- state.canShowAds = true;
359
- bind();
360
- ensureObserver();
361
- waitForContentThenRun();
362
- }
363
-
364
- if ($ && $(document).ready) {
365
- $(document).ready(init);
366
- } else if (document.readyState === 'loading') {
367
- document.addEventListener('DOMContentLoaded', init);
662
+ enforceNoAdjacentAds();
663
+
664
+ // If nothing inserted and list isn't in DOM yet (first click), retry a bit
665
+ let count = 0;
666
+ if (kind === 'topic') count = getPostContainers().length;
667
+ else if (kind === 'categoryTopics') count = getTopicItems().length;
668
+ else if (kind === 'categories') count = getCategoryItems().length;
669
+
670
+ if (count === 0 && 0 < 25) {
671
+ setTimeout(arguments[0], 50);
672
+ return;
673
+ }
674
+
675
+ if (inserted >= MAX_INSERTS_PER_RUN) {
676
+ // Plus d'insertions possibles ce cycle, continuer immédiatement
677
+ setTimeout(arguments[0], 50);
678
+ } else if (inserted === 0 && count > 0) {
679
+ // Pool épuisé ou recyclage pas encore disponible.
680
+ // Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
681
+ // le temps de défiler hors écran et devenir recyclables.
682
+ if (state.poolWaitAttempts < 8) {
683
+ state.poolWaitAttempts += 1;
684
+ setTimeout(arguments[0], 50);
368
685
  } else {
369
- init();
370
686
  }
371
- })();
687
+ } else if (inserted > 0) {
688
+ }
689
+ }
690
+
691
+ function scheduleRun() {
692
+ if (state.scheduled) return;
693
+ state.scheduled = true;
694
+
695
+ clearTimeout(state.timer);
696
+ state.timer = setTimeout(() => {
697
+ state.scheduled = false;
698
+ const pk = getPageKey();
699
+ if (state.pageKey && pk !== state.pageKey) return;
700
+ runCore().catch(() => {});
701
+ }, 50);
702
+ }
703
+
704
+ function bind() {
705
+ if (!$) return;
706
+
707
+ $(window).off('.ezoicInfinite');
708
+
709
+ $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
710
+
711
+ $(window).on('action:ajaxify.end.ezoicInfinite', () => {
712
+ state.pageKey = getPageKey();
713
+ ensureObserver();
714
+
715
+ // CRITIQUE: Attendre 300ms avant de permettre l'insertion de nouveaux placeholders
716
+ // pour laisser les anciens showAds() (en cours) se terminer ou échouer proprement
717
+ // Sinon race condition: NodeBB vide le DOM pendant que Ezoic essaie d'accéder aux placeholders
718
+ state.canShowAds = true;
719
+ });
720
+
721
+ $(window).on('action:category.loaded.ezoicInfinite', () => {
722
+ ensureObserver();
723
+ // category.loaded = infinite scroll, Ezoic déjà chargé normalement
724
+ waitForContentThenRun();
725
+ });
726
+ $(window).on('action:topics.loaded.ezoicInfinite', () => {
727
+ ensureObserver();
728
+ waitForContentThenRun();
729
+ });
730
+
731
+ $(window).on('action:topic.loaded.ezoicInfinite', () => {
732
+ ensureObserver();
733
+ waitForContentThenRun();
734
+ });
735
+
736
+ $(window).on('action:posts.loaded.ezoicInfinite', () => {
737
+ ensureObserver();
738
+ // posts.loaded = infinite scroll
739
+ waitForContentThenRun();
740
+ });
741
+ }
742
+
743
+ function bindScroll() {
744
+ if (state.lastScrollRun > 0) return;
745
+ state.lastScrollRun = Date.now();
746
+ let ticking = false;
747
+ window.addEventListener('scroll', () => {
748
+ if (ticking) return;
749
+ ticking = true;
750
+ window.requestAnimationFrame(() => {
751
+ ticking = false;
752
+ enforceNoAdjacentAds();
753
+ // Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
754
+ const now = Date.now();
755
+ if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
756
+ state.lastScrollRun = now;
757
+ scheduleRun();
758
+ }
759
+ });
760
+ }, { passive: true });
761
+ }
762
+
763
+ // Fonction qui attend que la page ait assez de contenu avant d'insérer les pubs
764
+ function waitForContentThenRun() {
765
+ const MIN_WORDS = 250;
766
+ let attempts = 0;
767
+ const maxAttempts = 20; // 20 × 200ms = 4s max
768
+
769
+ (function check() {
770
+ attempts++;
771
+
772
+ // Compter les mots sur la page
773
+ const text = document.body.innerText || '';
774
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
775
+
776
+ if (wordCount >= MIN_WORDS) {
777
+ // Assez de contenu → lancer l'insertion
778
+ scheduleRun();
779
+ return;
780
+ }
781
+
782
+ // Pas assez de contenu
783
+ if (attempts >= maxAttempts) {
784
+ // Timeout après 4s → tenter quand même
785
+ scheduleRun();
786
+ return;
787
+ }
788
+
789
+ // Réessayer dans 200ms
790
+ setTimeout(check, 50);
791
+ })();
792
+ }
793
+
794
+ // Fonction qui attend que Ezoic soit vraiment chargé
795
+ function waitForEzoicThenRun() {
796
+ let attempts = 0;
797
+ const maxAttempts = 50; // 50 × 200ms = 10s max
798
+
799
+ (function check() {
800
+ attempts++;
801
+ // Vérifier si Ezoic est chargé
802
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
803
+ // Ezoic est prêt → lancer l'insertion
804
+ scheduleRun();
805
+ waitForContentThenRun();
806
+ return;
807
+ }
808
+ // Ezoic pas encore prêt
809
+ if (attempts >= maxAttempts) {
810
+ // Tenter quand même
811
+ scheduleRun();
812
+ return;
813
+ }
814
+ // Réessayer dans 200ms
815
+ setTimeout(check, 50);
816
+ })();
817
+ }
818
+
819
+ cleanup();
820
+ bind();
821
+ bindScroll();
822
+ ensureObserver();
823
+ state.pageKey = getPageKey();
824
+
825
+ // Attendre que Ezoic soit chargé avant d'insérer
826
+ waitForEzoicThenRun();
827
+ })();
@@ -1,11 +1,3 @@
1
- .ezoic-ad {
2
- padding: 0 !important;
3
- margin: 0 !important;
4
- min-height: 0 !important;
5
- min-width: 0 !important;
6
- }
7
-
8
- .ezoic-ad * {
9
- min-height: 0 !important;
10
- min-width: 0 !important;
11
- }
1
+ .ezoic-ad{height:auto !important; padding:0 !important; margin: 0.25rem 0;}
2
+ .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
3
+ .ezoic-ad .ezoic-ad-inner > div{padding:0;margin:0;}