nodebb-plugin-ezoic-infinite 1.6.57 → 1.6.58

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.58",
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
@@ -7,10 +7,10 @@
7
7
 
8
8
  let config = null;
9
9
  let usedIds = new Set();
10
- let isEzoicEnabled = false; // Flag crucial pour corriger vos logs d'erreur
11
10
  let scheduleTimer = null;
11
+ let isEzoicEnabled = false;
12
12
 
13
- // --- SÉLECTEURS ---
13
+ // --- SÉLECTEURS COMPATIBLES HARMONY ---
14
14
  function getTopicList() {
15
15
  return document.querySelector('[component="topic/list"], [component="category"], [component="categories"]');
16
16
  }
@@ -19,26 +19,26 @@
19
19
  return document.querySelector('[component="topic"], [component="post/list"]');
20
20
  }
21
21
 
22
- // --- LOGIQUE EZOIC ---
22
+ // --- GESTION DU SDK EZOIC (SÉCURISÉE) ---
23
23
  function callEzoic(idsToDefine) {
24
24
  if (!window.ezstandalone || !idsToDefine.length) return;
25
25
 
26
26
  try {
27
- // 1. Définir les placeholders
27
+ // 1. On définit les nouveaux placeholders
28
28
  window.ezstandalone.define(idsToDefine);
29
29
 
30
- // 2. Logique Enable vs Refresh (Correction de vos erreurs log)
30
+ // 2. On gère le cycle de vie (Correction de tes erreurs de log)
31
31
  if (!isEzoicEnabled && !window.ezstandalone.enabled) {
32
32
  window.ezstandalone.enable();
33
33
  window.ezstandalone.display();
34
- isEzoicEnabled = true;
35
- console.log('[Ezoic] First Enable & Display');
34
+ isEzoicEnabled = true;
36
35
  } else {
37
- // On attend un cycle pour s'assurer qu'Ezoic est prêt pour le refresh
36
+ // Si déjà activé, on attend un cycle CPU pour refresh proprement
38
37
  setTimeout(() => {
39
- window.ezstandalone.refresh();
40
- console.log('[Ezoic] Refreshing existing instance');
41
- }, 100);
38
+ if (window.ezstandalone.enabled) {
39
+ window.ezstandalone.refresh();
40
+ }
41
+ }, 200);
42
42
  }
43
43
  } catch (e) {
44
44
  console.warn('[Ezoic] SDK Error:', e);
@@ -58,22 +58,22 @@
58
58
  return null;
59
59
  }
60
60
 
61
- // --- INJECTION ---
61
+ // --- LOGIQUE D'INJECTION ---
62
62
  function inject() {
63
63
  if (!config || config.excluded) return;
64
64
  const newIds = [];
65
-
66
- // A. Topics / Categories
67
65
  const ul = getTopicList();
66
+
67
+ // A. LISTE DES SUJETS
68
68
  if (ul && config.enableBetweenAds) {
69
- // Nettoyage anti-pileup (remontée)
69
+ // Nettoyage des blocs vides qui pourraient traîner
70
70
  ul.querySelectorAll('.' + WRAP_CLASS).forEach(w => {
71
71
  if (!w.previousElementSibling || w.previousElementSibling.tagName !== 'LI') w.remove();
72
72
  });
73
73
 
74
74
  const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
75
75
 
76
- // First Ad
76
+ // Pub n°1 (Après le premier sujet)
77
77
  if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
78
78
  const id = getNextId(config.placeholderIds);
79
79
  if (id) {
@@ -82,7 +82,7 @@
82
82
  }
83
83
  }
84
84
 
85
- // Intervals
85
+ // Intervalles
86
86
  const interval = parseInt(config.intervalPosts, 10) || 5;
87
87
  items.forEach((li, idx) => {
88
88
  if ((idx + 1) % interval === 0 && idx > 0) {
@@ -97,7 +97,7 @@
97
97
  });
98
98
  }
99
99
 
100
- // B. Messages (Posts)
100
+ // B. LISTE DES MESSAGES (Sujets ouverts)
101
101
  const postList = getPostList();
102
102
  if (postList && config.enableMessageAds) {
103
103
  const posts = Array.from(postList.querySelectorAll('[component="post"]'));
@@ -116,6 +116,7 @@
116
116
  }
117
117
 
118
118
  function insertAd(target, id, isPinned, extraClass = '') {
119
+ if (!target) return;
119
120
  const wrap = document.createElement('div');
120
121
  wrap.className = `${WRAP_CLASS} ezoic-ad-between ${extraClass}`;
121
122
  if (isPinned) wrap.setAttribute(PINNED_ATTR, 'true');
@@ -123,7 +124,7 @@
123
124
  target.after(wrap);
124
125
  }
125
126
 
126
- // --- INIT ---
127
+ // --- INITIALISATION ---
127
128
  async function init() {
128
129
  try {
129
130
  const res = await fetch('/api/plugins/ezoic-infinite/config');
@@ -131,6 +132,7 @@
131
132
 
132
133
  inject();
133
134
 
135
+ // MutationObserver pour détecter le scroll infini
134
136
  const observer = new MutationObserver((mutations) => {
135
137
  if (mutations.some(m => m.addedNodes.length > 0)) {
136
138
  if (scheduleTimer) clearTimeout(scheduleTimer);
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
  }