nodebb-plugin-ezoic-infinite 1.7.44 → 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 +13 -29
- package/package.json +1 -1
- package/public/client.js +99 -58
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
|
-
|
|
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
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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 (
|
|
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
|
|
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
package/public/client.js
CHANGED
|
@@ -1,30 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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)
|
|
123
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -378,6 +403,37 @@
|
|
|
378
403
|
} catch (_) {}
|
|
379
404
|
}
|
|
380
405
|
|
|
406
|
+
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
407
|
+
//
|
|
408
|
+
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
409
|
+
//
|
|
410
|
+
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
411
|
+
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
412
|
+
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
413
|
+
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
414
|
+
// liste après un long scroll et bloquent les nouvelles injections.
|
|
415
|
+
//
|
|
416
|
+
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
417
|
+
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
418
|
+
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
419
|
+
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
420
|
+
|
|
421
|
+
function pruneOrphansBetween() {
|
|
422
|
+
const klass = 'ezoic-ad-between';
|
|
423
|
+
const cfg = KIND[klass];
|
|
424
|
+
|
|
425
|
+
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
426
|
+
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
427
|
+
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
428
|
+
|
|
429
|
+
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
430
|
+
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
431
|
+
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
432
|
+
|
|
433
|
+
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
434
|
+
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
435
|
+
});
|
|
436
|
+
}
|
|
381
437
|
|
|
382
438
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
383
439
|
|
|
@@ -430,15 +486,8 @@
|
|
|
430
486
|
|
|
431
487
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
432
488
|
|
|
433
|
-
// Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
|
|
434
|
-
// isMobile() est évalué à chaque appel pour détecter un changement.
|
|
435
|
-
let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
|
|
436
489
|
function getIO() {
|
|
437
|
-
|
|
438
|
-
if (S.io && _ioMobile === mobile) return S.io;
|
|
439
|
-
// Type d'écran changé ou première création : (re)créer l'observer
|
|
440
|
-
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
441
|
-
_ioMobile = mobile;
|
|
490
|
+
if (S.io) return S.io;
|
|
442
491
|
try {
|
|
443
492
|
S.io = new IntersectionObserver(entries => {
|
|
444
493
|
for (const e of entries) {
|
|
@@ -447,7 +496,7 @@
|
|
|
447
496
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
448
497
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
449
498
|
}
|
|
450
|
-
}, { root: null, rootMargin:
|
|
499
|
+
}, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
451
500
|
} catch (_) { S.io = null; }
|
|
452
501
|
return S.io;
|
|
453
502
|
}
|
|
@@ -584,6 +633,7 @@
|
|
|
584
633
|
);
|
|
585
634
|
|
|
586
635
|
if (kind === 'categoryTopics') {
|
|
636
|
+
pruneOrphansBetween();
|
|
587
637
|
return exec(
|
|
588
638
|
'ezoic-ad-between', getTopics,
|
|
589
639
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
@@ -652,8 +702,6 @@
|
|
|
652
702
|
S.pendingSet.clear();
|
|
653
703
|
S.burstActive = false;
|
|
654
704
|
S.runQueued = false;
|
|
655
|
-
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
656
|
-
if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
|
|
657
705
|
}
|
|
658
706
|
|
|
659
707
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
@@ -710,10 +758,9 @@
|
|
|
710
758
|
(document.body || document.documentElement).appendChild(f);
|
|
711
759
|
};
|
|
712
760
|
inject();
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
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 });
|
|
717
764
|
}
|
|
718
765
|
} catch (_) {}
|
|
719
766
|
}
|
|
@@ -779,12 +826,6 @@
|
|
|
779
826
|
ticking = true;
|
|
780
827
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
781
828
|
}, { passive: true });
|
|
782
|
-
// Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
|
|
783
|
-
let resizeTimer = 0;
|
|
784
|
-
window.addEventListener('resize', () => {
|
|
785
|
-
clearTimeout(resizeTimer);
|
|
786
|
-
resizeTimer = setTimeout(() => { getIO(); }, 500);
|
|
787
|
-
}, { passive: true });
|
|
788
829
|
}
|
|
789
830
|
|
|
790
831
|
// ── Boot ───────────────────────────────────────────────────────────────────
|