nodebb-plugin-ezoic-infinite 1.7.41 → 1.7.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -15,9 +15,11 @@ function normalizeExcludedGroups(value) {
15
15
  // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
16
  const s = String(value).trim();
17
17
  if (s.startsWith('[')) {
18
- try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
18
+ try {
19
+ const parsed = JSON.parse(s);
20
+ if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
21
+ } catch (_) {}
19
22
  }
20
- // Fallback : séparation par virgule
21
23
  return s.split(',').map(v => v.trim()).filter(Boolean);
22
24
  }
23
25
 
@@ -28,7 +30,15 @@ function parseBool(v, def = false) {
28
30
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
29
31
  }
30
32
 
33
+ // ── Cache groupes (fix #7 : getAllGroups sans cache sur page ACP) ───────────
34
+
35
+ let _groupsCache = null;
36
+ let _groupsCacheAt = 0;
37
+ const GROUPS_TTL = 60_000; // 1 minute
38
+
31
39
  async function getAllGroups() {
40
+ const now = Date.now();
41
+ if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
32
42
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
43
  if (!names || !names.length) {
34
44
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
@@ -37,7 +47,9 @@ async function getAllGroups() {
37
47
  const data = await groups.getGroupsData(filtered);
38
48
  const valid = data.filter(g => g && g.name);
39
49
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
40
- return valid;
50
+ _groupsCache = valid;
51
+ _groupsCacheAt = now;
52
+ return _groupsCache;
41
53
  }
42
54
 
43
55
  // ── Settings cache (30s TTL) ────────────────────────────────────────────────
@@ -88,7 +100,10 @@ ezstandalone.cmd = ezstandalone.cmd || [];
88
100
  // ── Hooks ──────────────────────────────────────────────────────────────────
89
101
 
90
102
  plugin.onSettingsSet = function (data) {
91
- if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
103
+ if (data && data.hash === SETTINGS_KEY) {
104
+ _settingsCache = null;
105
+ _groupsCache = null; // invalider aussi le cache groupes
106
+ }
92
107
  };
93
108
 
94
109
  plugin.addAdminNavigation = async (header) => {
@@ -101,11 +116,11 @@ plugin.addAdminNavigation = async (header) => {
101
116
  * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
102
117
  *
103
118
  * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
104
- * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
105
- * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
106
- * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
107
- * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
108
- * tout en préservant ce dernier.
119
+ * (render.js : templateValues.customHTML = meta.config.customHTML).
120
+ * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData.
121
+ * On préfixe customHTML pour passer AVANT le customHTML admin tout en le préservant.
122
+ *
123
+ * Fix #3 : erreurs loggées côté serveur plutôt qu'avalées silencieusement.
109
124
  */
110
125
  plugin.injectEzoicHead = async (data) => {
111
126
  try {
@@ -113,17 +128,18 @@ plugin.injectEzoicHead = async (data) => {
113
128
  const uid = data.req?.uid ?? 0;
114
129
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
130
  if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
131
  data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
118
132
  }
119
- } catch (_) {}
133
+ } catch (err) {
134
+ // Log l'erreur mais ne pas planter le rendu de la page
135
+ console.error('[ezoic-infinite] injectEzoicHead error:', err.message || err);
136
+ }
120
137
  return data;
121
138
  };
122
139
 
123
140
  plugin.init = async ({ router, middleware }) => {
124
141
  async function render(req, res) {
125
- const settings = await getSettings();
126
- const allGroups = await getAllGroups();
142
+ const [settings, allGroups] = await Promise.all([getSettings(), getAllGroups()]);
127
143
  res.render('admin/plugins/ezoic-infinite', {
128
144
  title: 'Ezoic Infinite Ads',
129
145
  ...settings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.41",
3
+ "version": "1.7.43",
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
@@ -1,68 +1,30 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v36
2
+ * NodeBB Ezoic Infinite Ads — client.js v51
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
6
6
  * v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
7
- *
8
- * v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
9
- * la position dans le batch courant.
10
- *
7
+ * v19 Intervalle global basé sur l'ordinal absolu (data-index).
11
8
  * v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
12
- * Fix fatal catégories : data-cid au lieu de data-index inexistant.
13
- * IO fixe (une instance, jamais recréée).
14
- * Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
15
- *
16
- * v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
17
- *
18
- * v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
19
- *
20
- * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB posts).
21
- *
9
+ * IO fixe (une instance, jamais recréée). Fix TCF locator.
10
+ * v25 Fix scroll-up / virtualisation NodeBB.
11
+ * v26 Suppression recyclage d'id (causait réinjection en haut).
12
+ * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation posts).
22
13
  * v28 decluster supprimé. Wraps persistants pendant la session.
23
- *
24
- * v32 Retour anchorAttr = data-index pour ezoic-ad-between.
25
- * data-tid peut être absent clés invalides wraps empilés.
26
- * pruneOrphansBetween réactivé uniquement pour topics de catégorie :
27
- * NodeBB NE virtualise PAS les topics dans une liste de catégorie,
28
- * les ancres (data-index) restent en DOM prune safe et nécessaire
29
- * pour éviter l'empilement après scroll long.
30
- * Toujours désactivé pour les posts : NodeBB virtualise les posts
31
- * hors-viewport faux-orphelins bug réinjection en haut.
32
- *
33
- * v34 moveDistantWrap voir v38.
34
- *
35
- * v48 Reload page si utilisateur exclu après connexion (plus propre que
36
- * retirer les scripts du DOM — le head sera re-rendu sans Ezoic).
37
- *
38
- * v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
39
- *
40
- * v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
41
- *
42
- * v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
43
- *
44
- * v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
45
- * Séquence : destroy → 300ms → define → 300ms → displayMore.
46
- * Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
47
- *
48
- * v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
49
- * sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
50
- * déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
51
- * break propre dans injectBetween. muteConsole : ajout warnings refresh.
52
- *
53
- * v36 Optimisations chemin critique (scroll → injectBetween) :
54
- * – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
55
- * sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
56
- * dropWrap et cleanup.
57
- * – wrapIsLive allégé : pour les voisins immédiats on vérifie les
58
- * attributs du nœud lui-même sans querySelector global.
59
- * – MutationObserver : matches() vérifié avant querySelector() pour
60
- * court-circuiter les sous-arbres entiers ajoutés par NodeBB.
61
- *
62
- * v35 Revue complète prod-ready :
63
- * – initPools protégé contre ré-initialisation inutile (S.poolsReady).
64
- * – muteConsole élargit à "No valid placeholders for loadMore".
65
- * – Commentaires et historique nettoyés.
14
+ * v32 pruneOrphansBetween réactivé uniquement pour topics de catégorie.
15
+ * NodeBB NE virtualise PAS les topics → prune safe.
16
+ * Toujours désactivé pour posts (virtualisationfaux-orphelins).
17
+ * v35 initPools protégé (S.poolsReady). muteConsole élargi.
18
+ * v36 S.wrapByKey Map O(1). wrapIsLive allégé. MutationObserver optimisé.
19
+ * v38 ez.refresh() interdit supprimé. Pool épuisé break propre.
20
+ * v40 Recyclage via destroyPlaceholders+define+displayMore (délais 300ms).
21
+ * v43 Seuil recyclage -1vh + unobserve avant déplacement.
22
+ * v49 Fix normalizeExcludedGroups : JSON.parse du tableau NodeBB.
23
+ * v50 bindLoginCheck() supprimé (NodeBB recharge après login).
24
+ * v51 #2 fetchConfig() : backoff 10s sur échec réseau (évite spam API).
25
+ * #4 recycleAndMove : re-observe le placeholder après displayMore.
26
+ * #5 ensureTcfLocator : MutationObserver global disconnecté au cleanup.
27
+ * #6 getIO() recrée l'observer si le type d'écran change (resize).
66
28
  */
67
29
  (function nbbEzoicInfinite() {
68
30
  'use strict';
@@ -126,6 +88,7 @@
126
88
  pending: [], // ids en attente de slot inflight
127
89
  pendingSet: new Set(),
128
90
  wrapByKey: new Map(), // anchorKey → wrap DOM node
91
+ tcfObs: null, // MutationObserver TCF locator
129
92
  runQueued: false,
130
93
  burstActive: false,
131
94
  burstDeadline: 0,
@@ -148,12 +111,17 @@
148
111
 
149
112
  // ── Config ─────────────────────────────────────────────────────────────────
150
113
 
114
+ // Fix #2 : backoff 10s sur échec pour éviter de spammer l'API
115
+ // si le réseau est lent ou la route indisponible.
116
+ let _cfgErrorUntil = 0;
151
117
  async function fetchConfig() {
152
118
  if (S.cfg) return S.cfg;
119
+ if (Date.now() < _cfgErrorUntil) return null;
153
120
  try {
154
121
  const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
155
- if (r.ok) S.cfg = await r.json();
156
- } catch (_) {}
122
+ if (r.ok) { S.cfg = await r.json(); }
123
+ else { _cfgErrorUntil = Date.now() + 10_000; }
124
+ } catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
157
125
  return S.cfg;
158
126
  }
159
127
 
@@ -355,7 +323,15 @@
355
323
  // Délais requis : destroyPlaceholders est asynchrone en interne
356
324
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
357
325
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
358
- const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
326
+ // Fix #4 : re-observer le ph après displayMore pour déclencher scheduleEmptyCheck
327
+ // si la pub ne charge pas (détection wrap vide).
328
+ const doDisplay = () => {
329
+ try { ez.displayMore([id]); } catch (_) {}
330
+ observePh(id);
331
+ const t = ts();
332
+ try { best.setAttribute(A_SHOWN, String(t)); } catch (_) {}
333
+ scheduleEmptyCheck(id, t);
334
+ };
359
335
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
360
336
 
361
337
  return { id, wrap: best };
@@ -485,8 +461,15 @@
485
461
 
486
462
  // ── IntersectionObserver & Show ────────────────────────────────────────────
487
463
 
464
+ // Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
465
+ // isMobile() est évalué à chaque appel pour détecter un changement.
466
+ let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
488
467
  function getIO() {
489
- if (S.io) return S.io;
468
+ const mobile = isMobile();
469
+ if (S.io && _ioMobile === mobile) return S.io;
470
+ // Type d'écran changé ou première création : (re)créer l'observer
471
+ if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
472
+ _ioMobile = mobile;
490
473
  try {
491
474
  S.io = new IntersectionObserver(entries => {
492
475
  for (const e of entries) {
@@ -495,7 +478,7 @@
495
478
  const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
496
479
  if (Number.isFinite(id) && id > 0) enqueueShow(id);
497
480
  }
498
- }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
481
+ }, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
499
482
  } catch (_) { S.io = null; }
500
483
  return S.io;
501
484
  }
@@ -701,6 +684,8 @@
701
684
  S.pendingSet.clear();
702
685
  S.burstActive = false;
703
686
  S.runQueued = false;
687
+ if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
688
+ if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
704
689
  }
705
690
 
706
691
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -757,9 +742,10 @@
757
742
  (document.body || document.documentElement).appendChild(f);
758
743
  };
759
744
  inject();
760
- if (!window.__nbbTcfObs) {
761
- window.__nbbTcfObs = new MutationObserver(inject);
762
- window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
745
+ // Fix #5 : ref stockée dans S pour pouvoir déconnecter au cleanup
746
+ if (!S.tcfObs) {
747
+ S.tcfObs = new MutationObserver(inject);
748
+ S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
763
749
  }
764
750
  } catch (_) {}
765
751
  }
@@ -818,36 +804,6 @@
818
804
  } catch (_) {}
819
805
  }
820
806
 
821
- /**
822
- * Retire les scripts Ezoic du DOM si l'utilisateur vient de se connecter
823
- * et appartient à un groupe exclu.
824
- *
825
- * NodeBB ne recharge pas la page après login — les scripts injectés en <head>
826
- * restent en DOM. On détecte la connexion en comparant l'uid avant/après
827
- * chaque action:ajaxify.end, puis on vérifie /api/plugins/ezoic-infinite/config.
828
- * Si excluded:true, on retire les 4 scripts Ezoic du DOM et on arrête le plugin.
829
- */
830
- function bindLoginCheck() {
831
- const $ = window.jQuery;
832
- if (!$) return;
833
- let prevUid = window.app?.user?.uid ?? 0;
834
-
835
- $(window).on('action:ajaxify.end.nbbEzoicLogin', async () => {
836
- const uid = window.app?.user?.uid ?? 0;
837
- if (uid === prevUid || uid === 0) { prevUid = uid; return; }
838
- prevUid = uid;
839
- // L'utilisateur vient de se connecter — vérifier s'il est exclu
840
- try {
841
- const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
842
- if (!r.ok) return;
843
- const cfg = await r.json();
844
- if (!cfg.excluded) return;
845
- // Exclu : recharger la page — le <head> sera re-rendu sans les scripts Ezoic
846
- window.location.reload();
847
- } catch (_) {}
848
- });
849
- }
850
-
851
807
  function bindScroll() {
852
808
  let ticking = false;
853
809
  window.addEventListener('scroll', () => {
@@ -855,6 +811,12 @@
855
811
  ticking = true;
856
812
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
857
813
  }, { passive: true });
814
+ // Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
815
+ let resizeTimer = 0;
816
+ window.addEventListener('resize', () => {
817
+ clearTimeout(resizeTimer);
818
+ resizeTimer = setTimeout(() => { getIO(); }, 500);
819
+ }, { passive: true });
858
820
  }
859
821
 
860
822
  // ── Boot ───────────────────────────────────────────────────────────────────
@@ -868,7 +830,6 @@
868
830
  ensureDomObserver();
869
831
  bindNodeBB();
870
832
  bindScroll();
871
- bindLoginCheck();
872
833
  blockedUntil = 0;
873
834
  requestBurst();
874
835