nodebb-plugin-ezoic-infinite 1.6.57 → 1.6.59

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
@@ -4,50 +4,127 @@ const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
5
  const db = require.main.require('./src/database');
6
6
 
7
+ const SETTINGS_KEY = 'ezoic-infinite';
7
8
  const plugin = {};
8
9
 
9
- // Helper pour garantir des valeurs par défaut
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
+
10
39
  async function getSettings() {
11
- const settings = await meta.settings.get('ezoic-infinite');
12
- return {
13
- enableBetweenAds: settings.enableBetweenAds === 'on',
14
- showFirstTopicAd: settings.showFirstTopicAd === 'on',
15
- placeholderIds: settings.placeholderIds || '',
16
- intervalPosts: parseInt(settings.intervalPosts, 10) || 5,
17
- enableMessageAds: settings.enableMessageAds === 'on',
18
- showFirstMessageAd: settings.showFirstMessageAd === 'on',
19
- messagePlaceholderIds: settings.messagePlaceholderIds || '',
20
- messageIntervalPosts: parseInt(settings.messageIntervalPosts, 10) || 5,
21
- excludedGroups: settings.excludedGroups || [],
22
- };
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));
23
72
  }
24
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
+
25
91
  plugin.init = async ({ router, middleware }) => {
26
- const render = async (req, res) => {
27
- const settings = await getSettings();
28
- const allGroups = await getAllGroups(); // Utilise votre fonction existante
29
- res.render('admin/plugins/ezoic-infinite', { ...settings, allGroups });
30
- };
31
-
32
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
33
- router.get('/api/admin/plugins/ezoic-infinite', render);
34
-
35
- // L'API que le client.js appelle
36
- router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
37
- const settings = await getSettings();
38
- let excluded = false;
39
-
40
- if (req.uid && settings.excludedGroups.length) {
41
- excluded = await groups.isMemberOfAny(req.uid, settings.excludedGroups);
42
- }
43
-
44
- res.json({
45
- ...settings,
46
- excluded
47
- });
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,
48
102
  });
49
- };
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);
50
111
 
51
- // ... gardez vos fonctions getAllGroups et addAdminNavigation ...
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
+ };
52
129
 
53
- module.exports = plugin;
130
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.57",
3
+ "version": "1.6.59",
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
@@ -2,151 +2,86 @@
2
2
  'use strict';
3
3
 
4
4
  const WRAP_CLASS = 'nodebb-ezoic-wrap';
5
- const PINNED_ATTR = 'data-ezoic-pinned';
6
5
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
7
6
 
8
7
  let config = null;
9
8
  let usedIds = new Set();
10
- let isEzoicEnabled = false; // Flag crucial pour corriger vos logs d'erreur
11
- let scheduleTimer = null;
9
+ let isEzoicEnabled = false;
10
+ let isRefreshing = false;
12
11
 
13
- // --- SÉLECTEURS ---
14
- function getTopicList() {
15
- return document.querySelector('[component="topic/list"], [component="category"], [component="categories"]');
16
- }
17
-
18
- function getPostList() {
19
- return document.querySelector('[component="topic"], [component="post/list"]');
20
- }
21
-
22
- // --- LOGIQUE EZOIC ---
23
- function callEzoic(idsToDefine) {
24
- if (!window.ezstandalone || !idsToDefine.length) return;
12
+ // --- LOGIQUE SDK EZOIC ---
13
+ function callEzoic(newIds) {
14
+ if (!window.ezstandalone || !newIds.length) return;
25
15
 
26
16
  try {
27
- // 1. Définir les placeholders
28
- window.ezstandalone.define(idsToDefine);
17
+ window.ezstandalone.define(newIds);
29
18
 
30
- // 2. Logique Enable vs Refresh (Correction de vos erreurs log)
19
+ // Si c'est le TOUT PREMIER appel sur la page
31
20
  if (!isEzoicEnabled && !window.ezstandalone.enabled) {
32
21
  window.ezstandalone.enable();
33
22
  window.ezstandalone.display();
34
- isEzoicEnabled = true;
35
- console.log('[Ezoic] First Enable & Display');
36
- } else {
37
- // On attend un cycle pour s'assurer qu'Ezoic est prêt pour le refresh
23
+ isEzoicEnabled = true;
24
+ }
25
+ // Sinon, on utilise refresh MAIS avec un verrou de sécurité
26
+ else if (isEzoicEnabled && !isRefreshing) {
27
+ isRefreshing = true;
38
28
  setTimeout(() => {
39
29
  window.ezstandalone.refresh();
40
- console.log('[Ezoic] Refreshing existing instance');
41
- }, 100);
30
+ isRefreshing = false;
31
+ }, 500); // Délai de 500ms pour laisser le SDK respirer
42
32
  }
43
- } catch (e) {
44
- console.warn('[Ezoic] SDK Error:', e);
45
- }
46
- }
47
-
48
- function getNextId(poolStr) {
49
- if (!poolStr) return null;
50
- const ids = poolStr.split(/[\s,]+/).filter(Boolean);
51
- for (const id of ids) {
52
- const num = id.replace(/[^\d]/g, '');
53
- if (num && !usedIds.has(num)) {
54
- usedIds.add(num);
55
- return num;
56
- }
57
- }
58
- return null;
33
+ } catch (e) { console.warn('[Ezoic] Error:', e); }
59
34
  }
60
35
 
61
36
  // --- INJECTION ---
62
37
  function inject() {
63
38
  if (!config || config.excluded) return;
64
- const newIds = [];
39
+ const ul = document.querySelector('[component="topic/list"], [component="category"]');
40
+ if (!ul) return;
65
41
 
66
- // A. Topics / Categories
67
- const ul = getTopicList();
68
- if (ul && config.enableBetweenAds) {
69
- // Nettoyage anti-pileup (remontée)
70
- ul.querySelectorAll('.' + WRAP_CLASS).forEach(w => {
71
- if (!w.previousElementSibling || w.previousElementSibling.tagName !== 'LI') w.remove();
72
- });
42
+ const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
43
+ const newIdsFound = [];
44
+ const interval = parseInt(config.intervalPosts, 10) || 5;
73
45
 
74
- const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
75
-
76
- // First Ad
77
- if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
46
+ items.forEach((li, idx) => {
47
+ const pos = idx + 1;
48
+ if (pos % interval === 0 && !li.nextElementSibling?.classList.contains(WRAP_CLASS)) {
78
49
  const id = getNextId(config.placeholderIds);
79
50
  if (id) {
80
- insertAd(items[0], id, true);
81
- newIds.push(parseInt(id, 10));
51
+ const wrap = document.createElement('div');
52
+ wrap.className = WRAP_CLASS;
53
+ wrap.innerHTML = `<div id="${PLACEHOLDER_PREFIX}${id}"></div>`;
54
+ li.after(wrap);
55
+ newIdsFound.push(parseInt(id, 10));
82
56
  }
83
57
  }
58
+ });
84
59
 
85
- // Intervals
86
- const interval = parseInt(config.intervalPosts, 10) || 5;
87
- items.forEach((li, idx) => {
88
- if ((idx + 1) % interval === 0 && idx > 0) {
89
- if (!li.nextElementSibling || !li.nextElementSibling.classList.contains(WRAP_CLASS)) {
90
- const id = getNextId(config.placeholderIds);
91
- if (id) {
92
- insertAd(li, id, false);
93
- newIds.push(parseInt(id, 10));
94
- }
95
- }
96
- }
97
- });
60
+ if (newIdsFound.length > 0) {
61
+ callEzoic(newIdsFound);
98
62
  }
63
+ }
99
64
 
100
- // B. Messages (Posts)
101
- const postList = getPostList();
102
- if (postList && config.enableMessageAds) {
103
- const posts = Array.from(postList.querySelectorAll('[component="post"]'));
104
- if (config.showFirstMessageAd && posts.length > 0 && !postList.querySelector('.ezoic-msg-first')) {
105
- const id = getNextId(config.messagePlaceholderIds);
106
- if (id) {
107
- insertAd(posts[0], id, false, 'ezoic-msg-first');
108
- newIds.push(parseInt(id, 10));
109
- }
65
+ function getNextId(pool) {
66
+ const ids = pool.split(/[\s,]+/).filter(Boolean);
67
+ for (const id of ids) {
68
+ const n = id.trim();
69
+ if (n && !usedIds.has(n)) {
70
+ usedIds.add(n);
71
+ return n;
110
72
  }
111
73
  }
112
-
113
- if (newIds.length > 0) {
114
- callEzoic(newIds);
115
- }
116
- }
117
-
118
- function insertAd(target, id, isPinned, extraClass = '') {
119
- const wrap = document.createElement('div');
120
- wrap.className = `${WRAP_CLASS} ezoic-ad-between ${extraClass}`;
121
- if (isPinned) wrap.setAttribute(PINNED_ATTR, 'true');
122
- wrap.innerHTML = `<div id="${PLACEHOLDER_PREFIX}${id}"></div>`;
123
- target.after(wrap);
74
+ return null;
124
75
  }
125
76
 
126
- // --- INIT ---
127
77
  async function init() {
128
- try {
129
- const res = await fetch('/api/plugins/ezoic-infinite/config');
130
- config = await res.json();
131
-
132
- inject();
78
+ const res = await fetch('/api/plugins/ezoic-infinite/config');
79
+ config = await res.json();
80
+ inject();
133
81
 
134
- const observer = new MutationObserver((mutations) => {
135
- if (mutations.some(m => m.addedNodes.length > 0)) {
136
- if (scheduleTimer) clearTimeout(scheduleTimer);
137
- scheduleTimer = setTimeout(inject, 300);
138
- }
139
- });
140
- observer.observe(document.body, { childList: true, subtree: true });
141
-
142
- if (window.jQuery) {
143
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
144
- setTimeout(inject, 500);
145
- });
146
- }
147
- } catch (e) {}
82
+ // Ecoute du scroll infini NodeBB
83
+ $(window).on('action:infiniteScroll.loaded', () => setTimeout(inject, 100));
148
84
  }
149
85
 
150
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
151
- else init();
86
+ $(document).ready(init);
152
87
  })();
package/public/style.css CHANGED
@@ -1,80 +1,28 @@
1
- /* --- Conteneur principal inséré entre les sujets/messages --- */
1
+ /* Conteneur de pub */
2
2
  .nodebb-ezoic-wrap {
3
- display: block !important;
4
- width: 100% !important;
5
- clear: both !important;
6
- margin: 20px 0 !important;
7
- padding: 0 !important;
8
- /* Important : Empêche les éléments de déborder lors du recyclage du DOM */
9
- overflow: hidden;
10
- /* Améliore les performances de rendu sur NodeBB 4.x */
11
- contain: layout style;
12
- /* Assure une visibilité minimale pour que le SDK Ezoic puisse mesurer l'emplacement */
13
- min-height: 50px;
3
+ display: block !important;
4
+ width: 100% !important;
5
+ clear: both !important;
6
+ margin: 25px 0 !important;
7
+ min-height: 100px; /* Crucial pour que Ezoic détecte l'emplacement */
8
+ text-align: center;
14
9
  }
15
10
 
16
- /* --- Protection spécifique pour la 1ère publicité (Pinned) --- */
11
+ /* Fix spécifique pour le premier sujet */
17
12
  .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
18
- /* Réserve une hauteur pour éviter le "Layout Shift" (saut d'écran) au scroll up */
19
- min-height: 150px;
20
- border-bottom: 1px solid rgba(0,0,0,0.05);
21
- margin-top: 5px !important;
13
+ min-height: 150px;
14
+ border-bottom: 1px solid rgba(0,0,0,0.05);
15
+ margin-top: 0 !important;
22
16
  }
23
17
 
24
- /* --- Le placeholder Ezoic interne --- */
25
- [id^="ezoic-pub-ad-placeholder-"] {
26
- margin: 0 auto !important;
27
- padding: 0 !important;
28
- text-align: center;
29
- min-height: 1px;
30
- line-height: 0;
31
- }
32
-
33
- /* --- Forcer le centrage et la réactivité des pubs Ezoic --- */
34
- .nodebb-ezoic-wrap span.ezoic-ad,
35
- .nodebb-ezoic-wrap .ezoic-ad,
36
- .nodebb-ezoic-wrap iframe {
37
- display: block !important;
38
- margin: 0 auto !important;
39
- max-width: 100% !important;
40
- }
41
-
42
- /* --- Gestion des blocs vides (Anti-Trous blancs) --- */
43
- /* Si Ezoic ne remplit pas l'emplacement ou si le script le vide,
44
- on réduit l'espace pour ne pas casser le design du forum */
18
+ /* Cache les blocs vides pour éviter les trous blancs */
45
19
  .nodebb-ezoic-wrap:empty {
46
- height: 0 !important;
47
- min-height: 0 !important;
48
- margin: 0 !important;
49
- border: none !important;
50
- }
51
-
52
- /* --- Adaptation pour les messages (Topic View) --- */
53
- .ezoic-msg-first {
54
- margin-bottom: 35px !important;
55
- padding-bottom: 15px !important;
56
- border-bottom: 1px solid var(--border-color, #eee);
57
- }
58
-
59
- /* --- Correction pour le mode sombre (NodeBB 4 Harmony) --- */
60
- [data-theme="dark"] .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
61
- border-bottom-color: rgba(255,255,255,0.1);
62
- }
63
-
64
- /* --- Neutralisation des marges Ezoic sur mobile --- */
65
- @media (max-width: 767px) {
66
- .nodebb-ezoic-wrap {
67
- margin: 10px 0 !important;
68
- min-height: 30px;
69
- }
70
- .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
71
- min-height: 100px;
72
- }
20
+ height: 0 !important;
21
+ min-height: 0 !important;
22
+ margin: 0 !important;
73
23
  }
74
24
 
75
- /* --- Sécurité Anti-Sticky ---
76
- Ezoic injecte parfois du 'position: sticky' qui fait "flotter" les pubs
77
- de manière erratique lors du scroll infini. On le neutralise ici. */
25
+ /* Évite que les pubs "volent" au scroll */
78
26
  .nodebb-ezoic-wrap .ezads-sticky-intradiv {
79
- position: static !important;
27
+ position: static !important;
80
28
  }