nodebb-plugin-ezoic-infinite 1.6.56 → 1.6.57

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,127 +4,50 @@ 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';
8
7
  const plugin = {};
9
8
 
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
-
9
+ // Helper pour garantir des valeurs par défaut
39
10
  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));
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
+ };
72
23
  }
73
24
 
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
25
  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,
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
+ });
102
48
  });
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
49
  };
129
50
 
130
- module.exports = plugin;
51
+ // ... gardez vos fonctions getAllGroups et addAdminNavigation ...
52
+
53
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.56",
3
+ "version": "1.6.57",
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,25 +1,50 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
- // --- 1. VARIABLES ET CONFIG ---
5
4
  const WRAP_CLASS = 'nodebb-ezoic-wrap';
6
5
  const PINNED_ATTR = 'data-ezoic-pinned';
7
6
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
8
7
 
9
8
  let config = null;
10
9
  let usedIds = new Set();
10
+ let isEzoicEnabled = false; // Flag crucial pour corriger vos logs d'erreur
11
11
  let scheduleTimer = null;
12
12
 
13
- // --- 2. SÉLECTEURS DE THÈMES ---
13
+ // --- SÉLECTEURS ---
14
14
  function getTopicList() {
15
- return document.querySelector('.category [component="category"], .categories [component="categories"], [component="topic/list"], .topic-list');
15
+ return document.querySelector('[component="topic/list"], [component="category"], [component="categories"]');
16
16
  }
17
17
 
18
18
  function getPostList() {
19
19
  return document.querySelector('[component="topic"], [component="post/list"]');
20
20
  }
21
21
 
22
- // --- 3. LOGIQUE DES POOLS D'ID ---
22
+ // --- LOGIQUE EZOIC ---
23
+ function callEzoic(idsToDefine) {
24
+ if (!window.ezstandalone || !idsToDefine.length) return;
25
+
26
+ try {
27
+ // 1. Définir les placeholders
28
+ window.ezstandalone.define(idsToDefine);
29
+
30
+ // 2. Logique Enable vs Refresh (Correction de vos erreurs log)
31
+ if (!isEzoicEnabled && !window.ezstandalone.enabled) {
32
+ window.ezstandalone.enable();
33
+ 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
38
+ setTimeout(() => {
39
+ window.ezstandalone.refresh();
40
+ console.log('[Ezoic] Refreshing existing instance');
41
+ }, 100);
42
+ }
43
+ } catch (e) {
44
+ console.warn('[Ezoic] SDK Error:', e);
45
+ }
46
+ }
47
+
23
48
  function getNextId(poolStr) {
24
49
  if (!poolStr) return null;
25
50
  const ids = poolStr.split(/[\s,]+/).filter(Boolean);
@@ -33,131 +58,93 @@
33
58
  return null;
34
59
  }
35
60
 
36
- // --- 4. APPEL EZOIC (REFRESH & SHOW) ---
37
- function callEzoic(id) {
38
- if (window.ezstandalone) {
39
- try {
40
- const nid = parseInt(id, 10);
41
- window.ezstandalone.define(nid);
42
- if (!window.ezstandalone.enabled) {
43
- window.ezstandalone.enable();
44
- window.ezstandalone.display();
45
- } else {
46
- window.ezstandalone.refresh();
47
- }
48
- } catch (e) { console.warn('[ezoic] refresh error', id, e); }
49
- }
50
- }
51
-
52
- // --- 5. INJECTION SÉCURISÉE (ANTI-PILEUP) ---
61
+ // --- INJECTION ---
53
62
  function inject() {
54
63
  if (!config || config.excluded) return;
64
+ const newIds = [];
55
65
 
56
- // A. LISTE DES SUJETS (Topics)
66
+ // A. Topics / Categories
57
67
  const ul = getTopicList();
58
68
  if (ul && config.enableBetweenAds) {
59
- // Nettoyage des pubs orphelines (celles qui n'ont plus de LI au dessus à cause du scroll up)
60
- const wraps = ul.querySelectorAll('.' + WRAP_CLASS);
61
- wraps.forEach(w => {
69
+ // Nettoyage anti-pileup (remontée)
70
+ ul.querySelectorAll('.' + WRAP_CLASS).forEach(w => {
62
71
  if (!w.previousElementSibling || w.previousElementSibling.tagName !== 'LI') w.remove();
63
72
  });
64
73
 
65
74
  const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
66
- if (items.length > 0) {
67
- // Pub n°1 (Pinned)
68
- if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
69
- insertAd(items[0], getNextId(config.placeholderIds), true);
75
+
76
+ // First Ad
77
+ if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
78
+ const id = getNextId(config.placeholderIds);
79
+ if (id) {
80
+ insertAd(items[0], id, true);
81
+ newIds.push(parseInt(id, 10));
70
82
  }
83
+ }
71
84
 
72
- // Pubs d'intervalles
73
- const interval = parseInt(config.intervalPosts, 10) || 5;
74
- items.forEach((li, idx) => {
75
- const pos = idx + 1;
76
- if (pos > 1 && pos % interval === 0) {
77
- // Vérifie si ce LI a déjà une pub juste après lui pour éviter les doublons
78
- const next = li.nextElementSibling;
79
- if (!next || !next.classList.contains(WRAP_CLASS)) {
80
- insertAd(li, getNextId(config.placeholderIds), false);
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));
81
94
  }
82
95
  }
83
- });
84
- }
96
+ }
97
+ });
85
98
  }
86
99
 
87
- // B. LISTE DES MESSAGES (Posts)
100
+ // B. Messages (Posts)
88
101
  const postList = getPostList();
89
102
  if (postList && config.enableMessageAds) {
90
103
  const posts = Array.from(postList.querySelectorAll('[component="post"]'));
91
104
  if (config.showFirstMessageAd && posts.length > 0 && !postList.querySelector('.ezoic-msg-first')) {
92
- insertAd(posts[0], getNextId(config.messagePlaceholderIds), false, 'ezoic-msg-first');
93
- }
94
-
95
- const msgInterval = parseInt(config.messageIntervalPosts, 10) || 5;
96
- if (msgInterval > 0) {
97
- posts.forEach((post, idx) => {
98
- const pPos = idx + 1;
99
- if (pPos > 1 && pPos % msgInterval === 0) {
100
- if (!post.nextElementSibling || !post.nextElementSibling.classList.contains(WRAP_CLASS)) {
101
- insertAd(post, getNextId(config.messagePlaceholderIds), false);
102
- }
103
- }
104
- });
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
+ }
105
110
  }
106
111
  }
112
+
113
+ if (newIds.length > 0) {
114
+ callEzoic(newIds);
115
+ }
107
116
  }
108
117
 
109
118
  function insertAd(target, id, isPinned, extraClass = '') {
110
- if (!target || !id) return;
111
119
  const wrap = document.createElement('div');
112
120
  wrap.className = `${WRAP_CLASS} ezoic-ad-between ${extraClass}`;
113
121
  if (isPinned) wrap.setAttribute(PINNED_ATTR, 'true');
114
-
115
- const placeholder = document.createElement('div');
116
- placeholder.id = PLACEHOLDER_PREFIX + id;
117
- wrap.appendChild(placeholder);
118
-
122
+ wrap.innerHTML = `<div id="${PLACEHOLDER_PREFIX}${id}"></div>`;
119
123
  target.after(wrap);
120
-
121
- // On laisse le DOM respirer 50ms avant d'appeler Ezoic
122
- setTimeout(() => callEzoic(id), 50);
123
124
  }
124
125
 
125
- // --- 6. INITIALISATION ET HOOKS ---
126
- async function fetchConfig() {
126
+ // --- INIT ---
127
+ async function init() {
127
128
  try {
128
129
  const res = await fetch('/api/plugins/ezoic-infinite/config');
129
130
  config = await res.json();
130
- } catch (e) { console.error('[ezoic] config fetch failed'); }
131
- }
132
-
133
- function schedule() {
134
- if (scheduleTimer) clearTimeout(scheduleTimer);
135
- scheduleTimer = setTimeout(inject, 250);
136
- }
131
+
132
+ inject();
137
133
 
138
- function init() {
139
- fetchConfig().then(() => {
140
- schedule();
141
-
142
- // MutationObserver pour le scroll infini
143
- const body = document.body;
144
- const mo = new MutationObserver((mutations) => {
145
- for (let m of mutations) {
146
- if (m.addedNodes.length > 0) {
147
- schedule();
148
- break;
149
- }
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);
150
138
  }
151
139
  });
152
- mo.observe(body, { childList: true, subtree: true });
153
- });
140
+ observer.observe(document.body, { childList: true, subtree: true });
154
141
 
155
- // Compatibilité jQuery/NodeBB Events
156
- if (window.jQuery) {
157
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
158
- setTimeout(schedule, 500);
159
- });
160
- }
142
+ if (window.jQuery) {
143
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
144
+ setTimeout(inject, 500);
145
+ });
146
+ }
147
+ } catch (e) {}
161
148
  }
162
149
 
163
150
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
package/public/style.css CHANGED
@@ -1,23 +1,24 @@
1
- /* --- Conteneur principal de la publicité --- */
1
+ /* --- Conteneur principal inséré entre les sujets/messages --- */
2
2
  .nodebb-ezoic-wrap {
3
3
  display: block !important;
4
4
  width: 100% !important;
5
5
  clear: both !important;
6
6
  margin: 20px 0 !important;
7
7
  padding: 0 !important;
8
- /* Empêche les débordements visuels lors du recyclage du DOM */
8
+ /* Important : Empêche les éléments de déborder lors du recyclage du DOM */
9
9
  overflow: hidden;
10
10
  /* Améliore les performances de rendu sur NodeBB 4.x */
11
11
  contain: layout style;
12
+ /* Assure une visibilité minimale pour que le SDK Ezoic puisse mesurer l'emplacement */
13
+ min-height: 50px;
12
14
  }
13
15
 
14
- /* --- Protection du premier emplacement (Pinned) --- */
16
+ /* --- Protection spécifique pour la 1ère publicité (Pinned) --- */
15
17
  .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
16
- /* On réserve une hauteur minimale pour éviter que le contenu
17
- ne saute violemment quand la pub charge au scroll up */
18
- min-height: 120px;
18
+ /* Réserve une hauteur pour éviter le "Layout Shift" (saut d'écran) au scroll up */
19
+ min-height: 150px;
19
20
  border-bottom: 1px solid rgba(0,0,0,0.05);
20
- margin-top: 0 !important;
21
+ margin-top: 5px !important;
21
22
  }
22
23
 
23
24
  /* --- Le placeholder Ezoic interne --- */
@@ -29,39 +30,51 @@
29
30
  line-height: 0;
30
31
  }
31
32
 
32
- /* --- Nettoyage des éléments Ezoic internes --- */
33
+ /* --- Forcer le centrage et la réactivité des pubs Ezoic --- */
33
34
  .nodebb-ezoic-wrap span.ezoic-ad,
34
- .nodebb-ezoic-wrap .ezoic-ad {
35
+ .nodebb-ezoic-wrap .ezoic-ad,
36
+ .nodebb-ezoic-wrap iframe {
35
37
  display: block !important;
36
38
  margin: 0 auto !important;
37
- /* Force Ezoic à respecter la largeur du flux forum */
38
- max-width: 100% !important;
39
+ max-width: 100% !important;
39
40
  }
40
41
 
41
- /* --- Gestion des blocs vides (Anti-Holes) --- */
42
- /* Si Ezoic ne renvoie rien ou si le bloc est vide,
43
- on réduit l'espace pour ne pas avoir de gros trous blancs */
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 */
44
45
  .nodebb-ezoic-wrap:empty {
45
46
  height: 0 !important;
46
47
  min-height: 0 !important;
47
48
  margin: 0 !important;
49
+ border: none !important;
48
50
  }
49
51
 
50
- /* --- Adaptation spécifique pour les Messages (Sujets ouverts) --- */
52
+ /* --- Adaptation pour les messages (Topic View) --- */
51
53
  .ezoic-msg-first {
52
- margin-bottom: 30px !important;
54
+ margin-bottom: 35px !important;
55
+ padding-bottom: 15px !important;
53
56
  border-bottom: 1px solid var(--border-color, #eee);
54
- padding-bottom: 10px !important;
55
57
  }
56
58
 
57
- /* --- Support du mode sombre (NodeBB 4 Harmony) --- */
59
+ /* --- Correction pour le mode sombre (NodeBB 4 Harmony) --- */
58
60
  [data-theme="dark"] .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
59
61
  border-bottom-color: rgba(255,255,255,0.1);
60
62
  }
61
63
 
62
- /* --- Neutralisation des marges forcées par Ezoic sur mobile --- */
64
+ /* --- Neutralisation des marges Ezoic sur mobile --- */
63
65
  @media (max-width: 767px) {
64
66
  .nodebb-ezoic-wrap {
65
67
  margin: 10px 0 !important;
68
+ min-height: 30px;
66
69
  }
70
+ .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
71
+ min-height: 100px;
72
+ }
73
+ }
74
+
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. */
78
+ .nodebb-ezoic-wrap .ezads-sticky-intradiv {
79
+ position: static !important;
67
80
  }