nodebb-plugin-ezoic-infinite 1.4.94 → 1.4.95

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.94",
3
+ "version": "1.4.95",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -8,6 +8,7 @@
8
8
  }, WRAP_CLASS = 'ezoic-ad';
9
9
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
10
10
 
11
+ // Nécessaire pour savoir si on doit appeler destroyPlaceholders avant recyclage.
11
12
  const sessionDefinedIds = new Set();
12
13
 
13
14
  const insertingIds = new Set(), state = {
@@ -105,7 +106,9 @@
105
106
 
106
107
  function destroyPlaceholderIds(ids) {
107
108
  if (!ids || !ids.length) return;
109
+ // Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
108
110
  const filtered = ids.filter((id) => {
111
+ // Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
109
112
  try { return sessionDefinedIds.has(id); } catch (e) { return true; }
110
113
  });
111
114
  if (!filtered.length) return;
@@ -116,11 +119,6 @@
116
119
  window.ezstandalone.destroyPlaceholders(filtered);
117
120
  }
118
121
  } catch (e) {}
119
-
120
- // Recyclage: libérer IDs après 100ms
121
- setTimeout(() => {
122
- filtered.forEach(id => sessionDefinedIds.delete(id));
123
- }, 100);
124
122
  };
125
123
  try {
126
124
  window.ezstandalone = window.ezstandalone || {};
@@ -128,50 +126,10 @@
128
126
  if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
129
127
  else window.ezstandalone.cmd.push(call);
130
128
  } catch (e) {}
131
-
132
- // Recyclage: libérer IDs après 100ms
133
- setTimeout(() => {
134
- filtered.forEach(id => sessionDefinedIds.delete(id));
135
- }, 100);
136
129
  }
137
130
 
138
- // Nettoyer éléments Ezoic invisibles qui créent espace vertical
139
- function cleanupInvisibleEzoicElements() {
140
- try {
141
- document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
142
- const ph = wrapper.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
143
- if (!ph) return;
144
-
145
- // Supprimer TOUS les éléments après le placeholder rempli
146
- // qui créent de l'espace vertical
147
- let found = false;
148
- Array.from(wrapper.children).forEach(child => {
149
- if (child === ph || child.contains(ph)) {
150
- found = true;
151
- return;
152
- }
153
-
154
- // Si élément APRÈS le placeholder
155
- if (found) {
156
- const rect = child.getBoundingClientRect();
157
- const computed = window.getComputedStyle(child);
158
131
 
159
- // Supprimer si:
160
- // 1. Height > 0 mais pas de texte/image visible
161
- // 2. Ou opacity: 0
162
- // 3. Ou visibility: hidden
163
- const hasContent = child.textContent.trim().length > 0 ||
164
- child.querySelector('img, iframe, video');
165
-
166
- if (!hasContent || computed.opacity === '0' || computed.visibility === 'hidden') {
167
- child.remove();
168
- }
169
- }
170
- });
171
- });
172
- } catch (e) {}
173
- }
174
-
132
+ // Nettoyer les wrappers vides (sans pub) pour éviter espaces verticaux
175
133
  function cleanupEmptyWrappers() {
176
134
  try {
177
135
  document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
@@ -182,7 +140,7 @@
182
140
  if (ph.children.length === 0) {
183
141
  wrapper.remove();
184
142
  }
185
- }, 1500);
143
+ }, 3000);
186
144
  }
187
145
  });
188
146
  } catch (e) {}
@@ -259,8 +217,10 @@
259
217
  if (findWrap(kindClass, afterPos)) return null;
260
218
 
261
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
262
221
  if (insertingIds.has(id)) return null;
263
222
 
223
+ // 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
264
224
  const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
265
225
  if (existingPh && existingPh.isConnected) return null;
266
226
 
@@ -273,6 +233,7 @@
273
233
  attachFillObserver(wrap, id);
274
234
  return wrap;
275
235
  } finally {
236
+ // Libérer le lock après 100ms (le temps que le DOM soit stable)
276
237
  setTimeout(() => insertingIds.delete(id), 50);
277
238
  }
278
239
  }
@@ -283,6 +244,8 @@
283
244
  }
284
245
 
285
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.
286
249
  const applyPatch = () => {
287
250
  try {
288
251
  window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
@@ -309,6 +272,7 @@
309
272
  };
310
273
 
311
274
  applyPatch();
275
+ // Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
312
276
  if (!window.__nodebbEzoicPatched) {
313
277
  try {
314
278
  window.ezstandalone = window.ezstandalone || {};
@@ -321,6 +285,7 @@
321
285
  function markFilled(wrap) {
322
286
  try {
323
287
  if (!wrap) return;
288
+ // Disconnect the fill observer first (no need to remove+re-add the attribute)
324
289
  if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
325
290
  wrap.setAttribute('data-ezoic-filled', '1');
326
291
  } catch (e) {}
@@ -336,18 +301,19 @@
336
301
  if (!ph) return;
337
302
  // Already filled?
338
303
  if (ph.childNodes && ph.childNodes.length > 0) {
339
- markFilled(wrap); // Afficher wrapper
340
- sessionDefinedIds.add(id);
304
+ markFilled(wrap);
305
+ state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
341
306
  return;
342
307
  }
343
308
  const obs = new MutationObserver(() => {
344
309
  if (ph.childNodes && ph.childNodes.length > 0) {
345
- markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
346
- try { sessionDefinedIds.add(id); } catch (e) {}
310
+ markFilled(wrap);
311
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
347
312
  try { obs.disconnect(); } catch (e) {}
348
313
  }
349
314
  });
350
315
  obs.observe(ph, { childList: true, subtree: true });
316
+ // Keep a weak reference on the wrapper so we can disconnect on recycle/remove
351
317
  wrap.__ezoicFillObs = obs;
352
318
  } catch (e) {}
353
319
  }
@@ -367,12 +333,15 @@
367
333
  return filled;
368
334
  }
369
335
 
336
+ // Appeler showAds() en batch selon recommandations Ezoic
337
+ // Au lieu de showAds(id1), showAds(id2)... faire showAds(id1, id2, id3...)
370
338
  let batchShowAdsTimer = null;
371
339
  const pendingShowAdsIds = new Set();
372
340
 
373
341
  function scheduleShowAdsBatch(id) {
374
342
  if (!id) return;
375
343
 
344
+ // CRITIQUE: Si cet ID a déjà été défini (sessionDefinedIds), le détruire d'abord
376
345
  if (sessionDefinedIds.has(id)) {
377
346
  try {
378
347
  destroyPlaceholderIds([id]);
@@ -387,6 +356,7 @@
387
356
  // Ajouter à la batch
388
357
  pendingShowAdsIds.add(id);
389
358
 
359
+ // Debounce: attendre 100ms pour collecter tous les IDs
390
360
  clearTimeout(batchShowAdsTimer);
391
361
  batchShowAdsTimer = setTimeout(() => {
392
362
  if (pendingShowAdsIds.size === 0) return;
@@ -410,11 +380,6 @@
410
380
  }
411
381
  });
412
382
  } catch (e) {}
413
-
414
- // CRITIQUE: Nettoyer éléments invisibles APRÈS que pubs soient chargées
415
- setTimeout(() => {
416
- cleanupInvisibleEzoicElements();
417
- }, 800); // 1.5s pour laisser Ezoic charger
418
383
  }, 100);
419
384
  }
420
385
 
@@ -441,15 +406,19 @@
441
406
  const startPageKey = state.pageKey;
442
407
  let attempts = 0;
443
408
  (function waitForPh() {
409
+ // Abort if the user navigated away since this showAds was scheduled
444
410
  if (state.pageKey !== startPageKey) return;
411
+ // Abort if another concurrent call is already handling this id
445
412
  if (state.pendingById.has(id)) return;
446
413
 
447
414
  attempts += 1;
448
415
  const el = document.getElementById(phId);
449
416
  if (el && el.isConnected) {
417
+ // CRITIQUE: Vérifier que le placeholder est VISIBLE
450
418
 
451
419
  // Si on arrive ici, soit visible, soit timeout
452
420
 
421
+ // Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
453
422
  if (doCall()) {
454
423
  state.pendingById.delete(id);
455
424
  return;
@@ -515,6 +484,7 @@
515
484
  const el = items[afterPos - 1];
516
485
  if (!el || !el.isConnected) continue;
517
486
 
487
+ // Prevent adjacent ads (DOM-based, robust against virtualization)
518
488
  if (isAdjacentAd(el) || isPrevAd(el)) {
519
489
  continue;
520
490
  }
@@ -531,14 +501,17 @@
531
501
 
532
502
  let wrap = null;
533
503
  if (pick.recycled && pick.recycled.wrap) {
504
+ // Only destroy if Ezoic has actually defined this placeholder before
534
505
  if (sessionDefinedIds.has(id)) {
535
506
  destroyPlaceholderIds([id]);
536
507
  }
508
+ // Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
537
509
  const oldWrap = pick.recycled.wrap;
538
510
  try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
539
511
  try { oldWrap && oldWrap.remove(); } catch (e) {}
540
512
  wrap = insertAfter(el, id, kindClass, afterPos);
541
513
  if (!wrap) continue;
514
+ // Attendre que le wrapper soit dans le DOM puis appeler showAds
542
515
  setTimeout(() => {
543
516
  callShowAdsWhenReady(id);
544
517
  }, 50);
@@ -552,10 +525,12 @@
552
525
  }
553
526
 
554
527
  liveArr.push({ id, wrap });
528
+ // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
555
529
  if (wrap && (
556
530
  (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
557
531
  )) {
558
532
  try { wrap.remove(); } catch (e) {}
533
+ // Put id back if it was newly consumed (not recycled)
559
534
  if (!(pick.recycled && pick.recycled.wrap)) {
560
535
  try { kindPool.unshift(id); } catch (e) {}
561
536
  usedSet.delete(id);
@@ -572,6 +547,7 @@
572
547
  for (let i = 0; i < ads.length; i++) {
573
548
  const ad = ads[i], prev = ad.previousElementSibling;
574
549
  if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
550
+ // Supprimer le wrapper adjacent au lieu de le cacher
575
551
  try {
576
552
  const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
577
553
  if (ph) {
@@ -592,6 +568,8 @@
592
568
  function cleanup() {
593
569
  destroyUsedPlaceholders();
594
570
 
571
+ // CRITIQUE: Supprimer TOUS les wrappers .ezoic-ad du DOM
572
+ // Sinon ils restent et deviennent "unused" sur la nouvelle page
595
573
  document.querySelectorAll('.ezoic-ad').forEach(el => {
596
574
  try { el.remove(); } catch (e) {}
597
575
  });
@@ -607,14 +585,23 @@
607
585
  state.usedPosts.clear();
608
586
  state.usedCategories.clear();
609
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
610
590
  state.pendingById.clear();
611
591
  state.definedIds.clear();
612
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
613
599
  state.activeTimeouts.forEach(id => {
614
600
  try { clearTimeout(id); } catch (e) {}
615
601
  });
616
602
  state.activeTimeouts.clear();
617
603
 
604
+ // Vider aussi pendingById pour annuler les showAds en attente
618
605
  state.pendingById.clear();
619
606
 
620
607
  if (state.obs) { state.obs.disconnect(); state.obs = null; }
@@ -631,6 +618,7 @@
631
618
  }
632
619
 
633
620
  async function runCore() {
621
+ // Attendre que canInsert soit true (protection race condition navigation)
634
622
  if (!state.canShowAds) {
635
623
  return;
636
624
  }
@@ -673,6 +661,7 @@
673
661
 
674
662
  enforceNoAdjacentAds();
675
663
 
664
+ // If nothing inserted and list isn't in DOM yet (first click), retry a bit
676
665
  let count = 0;
677
666
  if (kind === 'topic') count = getPostContainers().length;
678
667
  else if (kind === 'categoryTopics') count = getTopicItems().length;
@@ -684,9 +673,12 @@
684
673
  }
685
674
 
686
675
  if (inserted >= MAX_INSERTS_PER_RUN) {
676
+ // Plus d'insertions possibles ce cycle, continuer immédiatement
687
677
  setTimeout(arguments[0], 50);
688
678
  } else if (inserted === 0 && count > 0) {
689
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.
690
682
  if (state.poolWaitAttempts < 8) {
691
683
  state.poolWaitAttempts += 1;
692
684
  setTimeout(arguments[0], 50);
@@ -720,11 +712,15 @@
720
712
  state.pageKey = getPageKey();
721
713
  ensureObserver();
722
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
723
718
  state.canShowAds = true;
724
719
  });
725
720
 
726
721
  $(window).on('action:category.loaded.ezoicInfinite', () => {
727
722
  ensureObserver();
723
+ // category.loaded = infinite scroll, Ezoic déjà chargé normalement
728
724
  waitForContentThenRun();
729
725
  });
730
726
  $(window).on('action:topics.loaded.ezoicInfinite', () => {
@@ -754,6 +750,7 @@
754
750
  window.requestAnimationFrame(() => {
755
751
  ticking = false;
756
752
  enforceNoAdjacentAds();
753
+ // Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
757
754
  const now = Date.now();
758
755
  if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
759
756
  state.lastScrollRun = now;
@@ -763,6 +760,7 @@
763
760
  }, { passive: true });
764
761
  }
765
762
 
763
+ // Fonction qui attend que la page ait assez de contenu avant d'insérer les pubs
766
764
  function waitForContentThenRun() {
767
765
  const MIN_WORDS = 250;
768
766
  let attempts = 0;
@@ -793,6 +791,7 @@
793
791
  })();
794
792
  }
795
793
 
794
+ // Fonction qui attend que Ezoic soit vraiment chargé
796
795
  function waitForEzoicThenRun() {
797
796
  let attempts = 0;
798
797
  const maxAttempts = 50; // 50 × 200ms = 10s max
package/public/style.css CHANGED
@@ -1,10 +1,3 @@
1
- .ezoic-ad {
2
- height: auto !important;
3
- padding: 0 !important;
4
- margin: 0 !important;
5
- }
6
-
7
- .ezoic-ad * {
8
- margin: 0 !important;
9
- padding: 0 !important;
10
- }
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;}