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 +29 -13
- package/package.json +1 -1
- package/public/client.js +58 -97
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
|
105
|
-
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
|
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
package/public/client.js
CHANGED
|
@@ -1,68 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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 (virtualisation → faux-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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
|