nodebb-plugin-ezoic-infinite 1.7.43 → 1.7.45

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,11 +15,9 @@ 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 {
19
- const parsed = JSON.parse(s);
20
- if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
21
- } catch (_) {}
18
+ try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
22
19
  }
20
+ // Fallback : séparation par virgule
23
21
  return s.split(',').map(v => v.trim()).filter(Boolean);
24
22
  }
25
23
 
@@ -30,15 +28,7 @@ function parseBool(v, def = false) {
30
28
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
31
29
  }
32
30
 
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
-
39
31
  async function getAllGroups() {
40
- const now = Date.now();
41
- if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
42
32
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
43
33
  if (!names || !names.length) {
44
34
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
@@ -47,9 +37,7 @@ async function getAllGroups() {
47
37
  const data = await groups.getGroupsData(filtered);
48
38
  const valid = data.filter(g => g && g.name);
49
39
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
50
- _groupsCache = valid;
51
- _groupsCacheAt = now;
52
- return _groupsCache;
40
+ return valid;
53
41
  }
54
42
 
55
43
  // ── Settings cache (30s TTL) ────────────────────────────────────────────────
@@ -100,10 +88,7 @@ ezstandalone.cmd = ezstandalone.cmd || [];
100
88
  // ── Hooks ──────────────────────────────────────────────────────────────────
101
89
 
102
90
  plugin.onSettingsSet = function (data) {
103
- if (data && data.hash === SETTINGS_KEY) {
104
- _settingsCache = null;
105
- _groupsCache = null; // invalider aussi le cache groupes
106
- }
91
+ if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
107
92
  };
108
93
 
109
94
  plugin.addAdminNavigation = async (header) => {
@@ -116,11 +101,11 @@ plugin.addAdminNavigation = async (header) => {
116
101
  * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
117
102
  *
118
103
  * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
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.
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.
124
109
  */
125
110
  plugin.injectEzoicHead = async (data) => {
126
111
  try {
@@ -128,18 +113,17 @@ plugin.injectEzoicHead = async (data) => {
128
113
  const uid = data.req?.uid ?? 0;
129
114
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
130
115
  if (!excluded) {
116
+ // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
131
117
  data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
132
118
  }
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
- }
119
+ } catch (_) {}
137
120
  return data;
138
121
  };
139
122
 
140
123
  plugin.init = async ({ router, middleware }) => {
141
124
  async function render(req, res) {
142
- const [settings, allGroups] = await Promise.all([getSettings(), getAllGroups()]);
125
+ const settings = await getSettings();
126
+ const allGroups = await getAllGroups();
143
127
  res.render('admin/plugins/ezoic-infinite', {
144
128
  title: 'Ezoic Infinite Ads',
145
129
  ...settings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.43",
3
+ "version": "1.7.45",
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,30 +1,69 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v51
2
+ * NodeBB Ezoic Infinite Ads — client.js v36
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
- * v19 Intervalle global basé sur l'ordinal absolu (data-index).
7
+ *
8
+ * v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
9
+ * la position dans le batch courant.
10
+ *
8
11
  * v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
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).
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
+ *
13
22
  * v28 decluster supprimé. Wraps persistants pendant la session.
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).
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
+ * v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
36
+ * après login filter:middleware.renderHeader re-évalue l'exclusion au
37
+ * rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
38
+ *
39
+ * v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
40
+ *
41
+ * v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
42
+ *
43
+ * v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
44
+ *
45
+ * v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
46
+ * Séquence : destroy → 300ms → define → 300ms → displayMore.
47
+ * Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
48
+ *
49
+ * v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
50
+ * sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
51
+ * déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
52
+ * break propre dans injectBetween. muteConsole : ajout warnings refresh.
53
+ *
54
+ * v36 Optimisations chemin critique (scroll → injectBetween) :
55
+ * – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
56
+ * sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
57
+ * dropWrap et cleanup.
58
+ * – wrapIsLive allégé : pour les voisins immédiats on vérifie les
59
+ * attributs du nœud lui-même sans querySelector global.
60
+ * – MutationObserver : matches() vérifié avant querySelector() pour
61
+ * court-circuiter les sous-arbres entiers ajoutés par NodeBB.
62
+ *
63
+ * v35 Revue complète prod-ready :
64
+ * – initPools protégé contre ré-initialisation inutile (S.poolsReady).
65
+ * – muteConsole élargit à "No valid placeholders for loadMore".
66
+ * – Commentaires et historique nettoyés.
28
67
  */
29
68
  (function nbbEzoicInfinite() {
30
69
  'use strict';
@@ -88,7 +127,6 @@
88
127
  pending: [], // ids en attente de slot inflight
89
128
  pendingSet: new Set(),
90
129
  wrapByKey: new Map(), // anchorKey → wrap DOM node
91
- tcfObs: null, // MutationObserver TCF locator
92
130
  runQueued: false,
93
131
  burstActive: false,
94
132
  burstDeadline: 0,
@@ -111,17 +149,12 @@
111
149
 
112
150
  // ── Config ─────────────────────────────────────────────────────────────────
113
151
 
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;
117
152
  async function fetchConfig() {
118
153
  if (S.cfg) return S.cfg;
119
- if (Date.now() < _cfgErrorUntil) return null;
120
154
  try {
121
155
  const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
122
- if (r.ok) { S.cfg = await r.json(); }
123
- else { _cfgErrorUntil = Date.now() + 10_000; }
124
- } catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
156
+ if (r.ok) S.cfg = await r.json();
157
+ } catch (_) {}
125
158
  return S.cfg;
126
159
  }
127
160
 
@@ -323,15 +356,7 @@
323
356
  // Délais requis : destroyPlaceholders est asynchrone en interne
324
357
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
325
358
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
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
+ const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
335
360
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
336
361
 
337
362
  return { id, wrap: best };
@@ -461,15 +486,8 @@
461
486
 
462
487
  // ── IntersectionObserver & Show ────────────────────────────────────────────
463
488
 
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éé
467
489
  function getIO() {
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
+ if (S.io) return S.io;
473
491
  try {
474
492
  S.io = new IntersectionObserver(entries => {
475
493
  for (const e of entries) {
@@ -478,7 +496,7 @@
478
496
  const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
479
497
  if (Number.isFinite(id) && id > 0) enqueueShow(id);
480
498
  }
481
- }, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
499
+ }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
482
500
  } catch (_) { S.io = null; }
483
501
  return S.io;
484
502
  }
@@ -684,8 +702,6 @@
684
702
  S.pendingSet.clear();
685
703
  S.burstActive = false;
686
704
  S.runQueued = false;
687
- if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
688
- if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
689
705
  }
690
706
 
691
707
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -742,10 +758,9 @@
742
758
  (document.body || document.documentElement).appendChild(f);
743
759
  };
744
760
  inject();
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 });
761
+ if (!window.__nbbTcfObs) {
762
+ window.__nbbTcfObs = new MutationObserver(inject);
763
+ window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
749
764
  }
750
765
  } catch (_) {}
751
766
  }
@@ -811,12 +826,6 @@
811
826
  ticking = true;
812
827
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
813
828
  }, { 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 });
820
829
  }
821
830
 
822
831
  // ── Boot ───────────────────────────────────────────────────────────────────