nodebb-plugin-ezoic-infinite 1.4.92 → 1.4.94
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 +1 -1
- package/public/{public/client.js → client.js} +56 -55
- package/public/style.css +10 -0
- package/public/README.md +0 -15
- package/public/library.js +0 -130
- package/public/package.json +0 -21
- package/public/plugin.json +0 -33
- package/public/public/style.css +0 -3
- /package/public/{public/admin.js → admin.js} +0 -0
- /package/public/{public/templates → templates}/admin/plugins/ezoic-infinite.tpl +0 -0
package/package.json
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
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.
|
|
12
11
|
const sessionDefinedIds = new Set();
|
|
13
12
|
|
|
14
13
|
const insertingIds = new Set(), state = {
|
|
@@ -106,9 +105,7 @@
|
|
|
106
105
|
|
|
107
106
|
function destroyPlaceholderIds(ids) {
|
|
108
107
|
if (!ids || !ids.length) return;
|
|
109
|
-
// Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
|
|
110
108
|
const filtered = ids.filter((id) => {
|
|
111
|
-
// Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
|
|
112
109
|
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
113
110
|
});
|
|
114
111
|
if (!filtered.length) return;
|
|
@@ -119,6 +116,11 @@
|
|
|
119
116
|
window.ezstandalone.destroyPlaceholders(filtered);
|
|
120
117
|
}
|
|
121
118
|
} catch (e) {}
|
|
119
|
+
|
|
120
|
+
// Recyclage: libérer IDs après 100ms
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
123
|
+
}, 100);
|
|
122
124
|
};
|
|
123
125
|
try {
|
|
124
126
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -126,10 +128,50 @@
|
|
|
126
128
|
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
127
129
|
else window.ezstandalone.cmd.push(call);
|
|
128
130
|
} catch (e) {}
|
|
131
|
+
|
|
132
|
+
// Recyclage: libérer IDs après 100ms
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
135
|
+
}, 100);
|
|
129
136
|
}
|
|
130
137
|
|
|
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);
|
|
131
158
|
|
|
132
|
-
//
|
|
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
|
+
|
|
133
175
|
function cleanupEmptyWrappers() {
|
|
134
176
|
try {
|
|
135
177
|
document.querySelectorAll('.ezoic-ad').forEach(wrapper => {
|
|
@@ -140,7 +182,7 @@
|
|
|
140
182
|
if (ph.children.length === 0) {
|
|
141
183
|
wrapper.remove();
|
|
142
184
|
}
|
|
143
|
-
},
|
|
185
|
+
}, 1500);
|
|
144
186
|
}
|
|
145
187
|
});
|
|
146
188
|
} catch (e) {}
|
|
@@ -217,10 +259,8 @@
|
|
|
217
259
|
if (findWrap(kindClass, afterPos)) return null;
|
|
218
260
|
|
|
219
261
|
// 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
|
|
221
262
|
if (insertingIds.has(id)) return null;
|
|
222
263
|
|
|
223
|
-
// 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
|
|
224
264
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
225
265
|
if (existingPh && existingPh.isConnected) return null;
|
|
226
266
|
|
|
@@ -233,7 +273,6 @@
|
|
|
233
273
|
attachFillObserver(wrap, id);
|
|
234
274
|
return wrap;
|
|
235
275
|
} finally {
|
|
236
|
-
// Libérer le lock après 100ms (le temps que le DOM soit stable)
|
|
237
276
|
setTimeout(() => insertingIds.delete(id), 50);
|
|
238
277
|
}
|
|
239
278
|
}
|
|
@@ -244,8 +283,6 @@
|
|
|
244
283
|
}
|
|
245
284
|
|
|
246
285
|
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.
|
|
249
286
|
const applyPatch = () => {
|
|
250
287
|
try {
|
|
251
288
|
window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
|
|
@@ -272,7 +309,6 @@
|
|
|
272
309
|
};
|
|
273
310
|
|
|
274
311
|
applyPatch();
|
|
275
|
-
// Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
|
|
276
312
|
if (!window.__nodebbEzoicPatched) {
|
|
277
313
|
try {
|
|
278
314
|
window.ezstandalone = window.ezstandalone || {};
|
|
@@ -285,7 +321,6 @@
|
|
|
285
321
|
function markFilled(wrap) {
|
|
286
322
|
try {
|
|
287
323
|
if (!wrap) return;
|
|
288
|
-
// Disconnect the fill observer first (no need to remove+re-add the attribute)
|
|
289
324
|
if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; }
|
|
290
325
|
wrap.setAttribute('data-ezoic-filled', '1');
|
|
291
326
|
} catch (e) {}
|
|
@@ -301,19 +336,18 @@
|
|
|
301
336
|
if (!ph) return;
|
|
302
337
|
// Already filled?
|
|
303
338
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
304
|
-
markFilled(wrap);
|
|
305
|
-
|
|
339
|
+
markFilled(wrap); // Afficher wrapper
|
|
340
|
+
sessionDefinedIds.add(id);
|
|
306
341
|
return;
|
|
307
342
|
}
|
|
308
343
|
const obs = new MutationObserver(() => {
|
|
309
344
|
if (ph.childNodes && ph.childNodes.length > 0) {
|
|
310
|
-
markFilled(wrap);
|
|
311
|
-
try {
|
|
345
|
+
markFilled(wrap); // CRITIQUE: Afficher wrapper maintenant
|
|
346
|
+
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
312
347
|
try { obs.disconnect(); } catch (e) {}
|
|
313
348
|
}
|
|
314
349
|
});
|
|
315
350
|
obs.observe(ph, { childList: true, subtree: true });
|
|
316
|
-
// Keep a weak reference on the wrapper so we can disconnect on recycle/remove
|
|
317
351
|
wrap.__ezoicFillObs = obs;
|
|
318
352
|
} catch (e) {}
|
|
319
353
|
}
|
|
@@ -333,15 +367,12 @@
|
|
|
333
367
|
return filled;
|
|
334
368
|
}
|
|
335
369
|
|
|
336
|
-
// Appeler showAds() en batch selon recommandations Ezoic
|
|
337
|
-
// Au lieu de showAds(id1), showAds(id2)... faire showAds(id1, id2, id3...)
|
|
338
370
|
let batchShowAdsTimer = null;
|
|
339
371
|
const pendingShowAdsIds = new Set();
|
|
340
372
|
|
|
341
373
|
function scheduleShowAdsBatch(id) {
|
|
342
374
|
if (!id) return;
|
|
343
375
|
|
|
344
|
-
// CRITIQUE: Si cet ID a déjà été défini (sessionDefinedIds), le détruire d'abord
|
|
345
376
|
if (sessionDefinedIds.has(id)) {
|
|
346
377
|
try {
|
|
347
378
|
destroyPlaceholderIds([id]);
|
|
@@ -356,7 +387,6 @@
|
|
|
356
387
|
// Ajouter à la batch
|
|
357
388
|
pendingShowAdsIds.add(id);
|
|
358
389
|
|
|
359
|
-
// Debounce: attendre 100ms pour collecter tous les IDs
|
|
360
390
|
clearTimeout(batchShowAdsTimer);
|
|
361
391
|
batchShowAdsTimer = setTimeout(() => {
|
|
362
392
|
if (pendingShowAdsIds.size === 0) return;
|
|
@@ -380,6 +410,11 @@
|
|
|
380
410
|
}
|
|
381
411
|
});
|
|
382
412
|
} 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
|
|
383
418
|
}, 100);
|
|
384
419
|
}
|
|
385
420
|
|
|
@@ -406,19 +441,15 @@
|
|
|
406
441
|
const startPageKey = state.pageKey;
|
|
407
442
|
let attempts = 0;
|
|
408
443
|
(function waitForPh() {
|
|
409
|
-
// Abort if the user navigated away since this showAds was scheduled
|
|
410
444
|
if (state.pageKey !== startPageKey) return;
|
|
411
|
-
// Abort if another concurrent call is already handling this id
|
|
412
445
|
if (state.pendingById.has(id)) return;
|
|
413
446
|
|
|
414
447
|
attempts += 1;
|
|
415
448
|
const el = document.getElementById(phId);
|
|
416
449
|
if (el && el.isConnected) {
|
|
417
|
-
// CRITIQUE: Vérifier que le placeholder est VISIBLE
|
|
418
450
|
|
|
419
451
|
// Si on arrive ici, soit visible, soit timeout
|
|
420
452
|
|
|
421
|
-
// Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
|
|
422
453
|
if (doCall()) {
|
|
423
454
|
state.pendingById.delete(id);
|
|
424
455
|
return;
|
|
@@ -484,7 +515,6 @@
|
|
|
484
515
|
const el = items[afterPos - 1];
|
|
485
516
|
if (!el || !el.isConnected) continue;
|
|
486
517
|
|
|
487
|
-
// Prevent adjacent ads (DOM-based, robust against virtualization)
|
|
488
518
|
if (isAdjacentAd(el) || isPrevAd(el)) {
|
|
489
519
|
continue;
|
|
490
520
|
}
|
|
@@ -501,17 +531,14 @@
|
|
|
501
531
|
|
|
502
532
|
let wrap = null;
|
|
503
533
|
if (pick.recycled && pick.recycled.wrap) {
|
|
504
|
-
// Only destroy if Ezoic has actually defined this placeholder before
|
|
505
534
|
if (sessionDefinedIds.has(id)) {
|
|
506
535
|
destroyPlaceholderIds([id]);
|
|
507
536
|
}
|
|
508
|
-
// Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
|
|
509
537
|
const oldWrap = pick.recycled.wrap;
|
|
510
538
|
try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
|
|
511
539
|
try { oldWrap && oldWrap.remove(); } catch (e) {}
|
|
512
540
|
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
513
541
|
if (!wrap) continue;
|
|
514
|
-
// Attendre que le wrapper soit dans le DOM puis appeler showAds
|
|
515
542
|
setTimeout(() => {
|
|
516
543
|
callShowAdsWhenReady(id);
|
|
517
544
|
}, 50);
|
|
@@ -525,12 +552,10 @@
|
|
|
525
552
|
}
|
|
526
553
|
|
|
527
554
|
liveArr.push({ id, wrap });
|
|
528
|
-
// If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
|
|
529
555
|
if (wrap && (
|
|
530
556
|
(wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
|
|
531
557
|
)) {
|
|
532
558
|
try { wrap.remove(); } catch (e) {}
|
|
533
|
-
// Put id back if it was newly consumed (not recycled)
|
|
534
559
|
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
535
560
|
try { kindPool.unshift(id); } catch (e) {}
|
|
536
561
|
usedSet.delete(id);
|
|
@@ -547,7 +572,6 @@
|
|
|
547
572
|
for (let i = 0; i < ads.length; i++) {
|
|
548
573
|
const ad = ads[i], prev = ad.previousElementSibling;
|
|
549
574
|
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
550
|
-
// Supprimer le wrapper adjacent au lieu de le cacher
|
|
551
575
|
try {
|
|
552
576
|
const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
553
577
|
if (ph) {
|
|
@@ -568,8 +592,6 @@
|
|
|
568
592
|
function cleanup() {
|
|
569
593
|
destroyUsedPlaceholders();
|
|
570
594
|
|
|
571
|
-
// CRITIQUE: Supprimer TOUS les wrappers .ezoic-ad du DOM
|
|
572
|
-
// Sinon ils restent et deviennent "unused" sur la nouvelle page
|
|
573
595
|
document.querySelectorAll('.ezoic-ad').forEach(el => {
|
|
574
596
|
try { el.remove(); } catch (e) {}
|
|
575
597
|
});
|
|
@@ -585,23 +607,14 @@
|
|
|
585
607
|
state.usedPosts.clear();
|
|
586
608
|
state.usedCategories.clear();
|
|
587
609
|
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
|
|
590
610
|
state.pendingById.clear();
|
|
591
611
|
state.definedIds.clear();
|
|
592
612
|
|
|
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
|
|
599
613
|
state.activeTimeouts.forEach(id => {
|
|
600
614
|
try { clearTimeout(id); } catch (e) {}
|
|
601
615
|
});
|
|
602
616
|
state.activeTimeouts.clear();
|
|
603
617
|
|
|
604
|
-
// Vider aussi pendingById pour annuler les showAds en attente
|
|
605
618
|
state.pendingById.clear();
|
|
606
619
|
|
|
607
620
|
if (state.obs) { state.obs.disconnect(); state.obs = null; }
|
|
@@ -618,7 +631,6 @@
|
|
|
618
631
|
}
|
|
619
632
|
|
|
620
633
|
async function runCore() {
|
|
621
|
-
// Attendre que canInsert soit true (protection race condition navigation)
|
|
622
634
|
if (!state.canShowAds) {
|
|
623
635
|
return;
|
|
624
636
|
}
|
|
@@ -661,7 +673,6 @@
|
|
|
661
673
|
|
|
662
674
|
enforceNoAdjacentAds();
|
|
663
675
|
|
|
664
|
-
// If nothing inserted and list isn't in DOM yet (first click), retry a bit
|
|
665
676
|
let count = 0;
|
|
666
677
|
if (kind === 'topic') count = getPostContainers().length;
|
|
667
678
|
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
@@ -673,12 +684,9 @@
|
|
|
673
684
|
}
|
|
674
685
|
|
|
675
686
|
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
676
|
-
// Plus d'insertions possibles ce cycle, continuer immédiatement
|
|
677
687
|
setTimeout(arguments[0], 50);
|
|
678
688
|
} else if (inserted === 0 && count > 0) {
|
|
679
689
|
// 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.
|
|
682
690
|
if (state.poolWaitAttempts < 8) {
|
|
683
691
|
state.poolWaitAttempts += 1;
|
|
684
692
|
setTimeout(arguments[0], 50);
|
|
@@ -712,15 +720,11 @@
|
|
|
712
720
|
state.pageKey = getPageKey();
|
|
713
721
|
ensureObserver();
|
|
714
722
|
|
|
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
|
|
718
723
|
state.canShowAds = true;
|
|
719
724
|
});
|
|
720
725
|
|
|
721
726
|
$(window).on('action:category.loaded.ezoicInfinite', () => {
|
|
722
727
|
ensureObserver();
|
|
723
|
-
// category.loaded = infinite scroll, Ezoic déjà chargé normalement
|
|
724
728
|
waitForContentThenRun();
|
|
725
729
|
});
|
|
726
730
|
$(window).on('action:topics.loaded.ezoicInfinite', () => {
|
|
@@ -750,7 +754,6 @@
|
|
|
750
754
|
window.requestAnimationFrame(() => {
|
|
751
755
|
ticking = false;
|
|
752
756
|
enforceNoAdjacentAds();
|
|
753
|
-
// Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
|
|
754
757
|
const now = Date.now();
|
|
755
758
|
if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
|
|
756
759
|
state.lastScrollRun = now;
|
|
@@ -760,7 +763,6 @@
|
|
|
760
763
|
}, { passive: true });
|
|
761
764
|
}
|
|
762
765
|
|
|
763
|
-
// Fonction qui attend que la page ait assez de contenu avant d'insérer les pubs
|
|
764
766
|
function waitForContentThenRun() {
|
|
765
767
|
const MIN_WORDS = 250;
|
|
766
768
|
let attempts = 0;
|
|
@@ -791,7 +793,6 @@
|
|
|
791
793
|
})();
|
|
792
794
|
}
|
|
793
795
|
|
|
794
|
-
// Fonction qui attend que Ezoic soit vraiment chargé
|
|
795
796
|
function waitForEzoicThenRun() {
|
|
796
797
|
let attempts = 0;
|
|
797
798
|
const maxAttempts = 50; // 50 × 200ms = 10s max
|
package/public/style.css
ADDED
package/public/README.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# NodeBB Plugin – Ezoic Infinite (Production)
|
|
2
|
-
|
|
3
|
-
This plugin injects Ezoic placeholders between topics and posts on NodeBB 4.x,
|
|
4
|
-
with full support for infinite scroll.
|
|
5
|
-
|
|
6
|
-
## Key guarantees
|
|
7
|
-
- No duplicate ads back-to-back
|
|
8
|
-
- One showAds call per placeholder
|
|
9
|
-
- Fast reveal (MutationObserver on first child)
|
|
10
|
-
- Safe with ajaxify navigation
|
|
11
|
-
- Works with NodeBB 4.x + Harmony
|
|
12
|
-
|
|
13
|
-
## Notes
|
|
14
|
-
- Placeholders must exist and be selected in Ezoic
|
|
15
|
-
- Use separate ID pools for topics vs messages
|
package/public/library.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const meta = require.main.require('./src/meta');
|
|
4
|
-
const groups = require.main.require('./src/groups');
|
|
5
|
-
const db = require.main.require('./src/database');
|
|
6
|
-
|
|
7
|
-
const SETTINGS_KEY = 'ezoic-infinite';
|
|
8
|
-
const plugin = {};
|
|
9
|
-
|
|
10
|
-
function normalizeExcludedGroups(value) {
|
|
11
|
-
if (!value) return [];
|
|
12
|
-
if (Array.isArray(value)) return value;
|
|
13
|
-
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function parseBool(v, def = false) {
|
|
17
|
-
if (v === undefined || v === null || v === '') return def;
|
|
18
|
-
if (typeof v === 'boolean') return v;
|
|
19
|
-
const s = String(v).toLowerCase();
|
|
20
|
-
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function getAllGroups() {
|
|
24
|
-
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
25
|
-
if (!names || !names.length) {
|
|
26
|
-
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
27
|
-
}
|
|
28
|
-
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
|
-
const data = await groups.getGroupsData(filtered);
|
|
30
|
-
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
31
|
-
const valid = data.filter(g => g && g.name);
|
|
32
|
-
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
33
|
-
return valid;
|
|
34
|
-
}
|
|
35
|
-
let _settingsCache = null;
|
|
36
|
-
let _settingsCacheAt = 0;
|
|
37
|
-
const SETTINGS_TTL = 30000; // 30s
|
|
38
|
-
|
|
39
|
-
async function getSettings() {
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
42
|
-
const s = await meta.settings.get(SETTINGS_KEY);
|
|
43
|
-
_settingsCacheAt = Date.now();
|
|
44
|
-
_settingsCache = {
|
|
45
|
-
// Between-post ads (simple blocks) in category topic list
|
|
46
|
-
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
47
|
-
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
48
|
-
placeholderIds: (s.placeholderIds || '').trim(),
|
|
49
|
-
intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
|
|
50
|
-
|
|
51
|
-
// Home/categories list ads (between categories on / or /categories)
|
|
52
|
-
enableCategoryAds: parseBool(s.enableCategoryAds, false),
|
|
53
|
-
showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
|
|
54
|
-
categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
|
|
55
|
-
intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
|
|
56
|
-
|
|
57
|
-
// "Ad message" between replies (looks like a post)
|
|
58
|
-
enableMessageAds: parseBool(s.enableMessageAds, false),
|
|
59
|
-
showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
|
|
60
|
-
messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
|
|
61
|
-
messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
|
|
62
|
-
|
|
63
|
-
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
64
|
-
};
|
|
65
|
-
return _settingsCache;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function isUserExcluded(uid, excludedGroups) {
|
|
69
|
-
if (!uid || !excludedGroups.length) return false;
|
|
70
|
-
const userGroups = await groups.getUserGroups([uid]);
|
|
71
|
-
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
plugin.onSettingsSet = function (data) {
|
|
75
|
-
// Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
|
|
76
|
-
if (data && data.hash === SETTINGS_KEY) {
|
|
77
|
-
_settingsCache = null;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
plugin.addAdminNavigation = async (header) => {
|
|
82
|
-
header.plugins = header.plugins || [];
|
|
83
|
-
header.plugins.push({
|
|
84
|
-
route: '/plugins/ezoic-infinite',
|
|
85
|
-
icon: 'fa-ad',
|
|
86
|
-
name: 'Ezoic Infinite Ads'
|
|
87
|
-
});
|
|
88
|
-
return header;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
plugin.init = async ({ router, middleware }) => {
|
|
92
|
-
async function render(req, res) {
|
|
93
|
-
const settings = await getSettings();
|
|
94
|
-
const allGroups = await getAllGroups();
|
|
95
|
-
|
|
96
|
-
res.render('admin/plugins/ezoic-infinite', {
|
|
97
|
-
title: 'Ezoic Infinite Ads',
|
|
98
|
-
...settings,
|
|
99
|
-
enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
|
|
100
|
-
enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
|
|
101
|
-
allGroups,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
106
|
-
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
107
|
-
|
|
108
|
-
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
|
-
const settings = await getSettings();
|
|
110
|
-
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
111
|
-
|
|
112
|
-
res.json({
|
|
113
|
-
excluded,
|
|
114
|
-
enableBetweenAds: settings.enableBetweenAds,
|
|
115
|
-
showFirstTopicAd: settings.showFirstTopicAd,
|
|
116
|
-
placeholderIds: settings.placeholderIds,
|
|
117
|
-
intervalPosts: settings.intervalPosts,
|
|
118
|
-
enableCategoryAds: settings.enableCategoryAds,
|
|
119
|
-
showFirstCategoryAd: settings.showFirstCategoryAd,
|
|
120
|
-
categoryPlaceholderIds: settings.categoryPlaceholderIds,
|
|
121
|
-
intervalCategories: settings.intervalCategories,
|
|
122
|
-
enableMessageAds: settings.enableMessageAds,
|
|
123
|
-
showFirstMessageAd: settings.showFirstMessageAd,
|
|
124
|
-
messagePlaceholderIds: settings.messagePlaceholderIds,
|
|
125
|
-
messageIntervalPosts: settings.messageIntervalPosts,
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
module.exports = plugin;
|
package/public/package.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"version": "1.4.8",
|
|
4
|
-
"description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
|
|
5
|
-
"main": "library.js",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"nodebb",
|
|
9
|
-
"nodebb-plugin",
|
|
10
|
-
"ezoic",
|
|
11
|
-
"ads",
|
|
12
|
-
"infinite-scroll"
|
|
13
|
-
],
|
|
14
|
-
"engines": {
|
|
15
|
-
"nodebb": ">=4.0.0"
|
|
16
|
-
},
|
|
17
|
-
"nbbpm": {
|
|
18
|
-
"compatibility": "^4.0.0"
|
|
19
|
-
},
|
|
20
|
-
"private": false
|
|
21
|
-
}
|
package/public/plugin.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "nodebb-plugin-ezoic-infinite",
|
|
3
|
-
"name": "NodeBB Ezoic Infinite Ads",
|
|
4
|
-
"description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
|
|
5
|
-
"library": "./library.js",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"hook": "static:app.load",
|
|
9
|
-
"method": "init"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"hook": "filter:admin.header.build",
|
|
13
|
-
"method": "addAdminNavigation"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"hook": "action:settings.set",
|
|
17
|
-
"method": "onSettingsSet"
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"staticDirs": {
|
|
21
|
-
"public": "public"
|
|
22
|
-
},
|
|
23
|
-
"acpScripts": [
|
|
24
|
-
"public/admin.js"
|
|
25
|
-
],
|
|
26
|
-
"scripts": [
|
|
27
|
-
"public/client.js"
|
|
28
|
-
],
|
|
29
|
-
"templates": "public/templates",
|
|
30
|
-
"css": [
|
|
31
|
-
"public/style.css"
|
|
32
|
-
]
|
|
33
|
-
}
|
package/public/public/style.css
DELETED
|
File without changes
|
|
File without changes
|