nodebb-plugin-ezoic-infinite 1.7.35 → 1.7.37

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
@@ -1,12 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const meta = require.main.require('./src/meta');
3
+ const meta = require.main.require('./src/meta');
4
4
  const groups = require.main.require('./src/groups');
5
- const db = require.main.require('./src/database');
5
+ const db = require.main.require('./src/database');
6
6
 
7
7
  const SETTINGS_KEY = 'ezoic-infinite';
8
8
  const plugin = {};
9
9
 
10
+ // ── Helpers ────────────────────────────────────────────────────────────────
11
+
10
12
  function normalizeExcludedGroups(value) {
11
13
  if (!value) return [];
12
14
  if (Array.isArray(value)) return value;
@@ -27,14 +29,16 @@ async function getAllGroups() {
27
29
  }
28
30
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
31
  const data = await groups.getGroupsData(filtered);
30
- // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
32
  const valid = data.filter(g => g && g.name);
32
33
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
34
  return valid;
34
35
  }
35
- let _settingsCache = null;
36
+
37
+ // ── Settings cache (30s TTL) ────────────────────────────────────────────────
38
+
39
+ let _settingsCache = null;
36
40
  let _settingsCacheAt = 0;
37
- const SETTINGS_TTL = 30000; // 30s
41
+ const SETTINGS_TTL = 30_000;
38
42
 
39
43
  async function getSettings() {
40
44
  const now = Date.now();
@@ -42,25 +46,22 @@ async function getSettings() {
42
46
  const s = await meta.settings.get(SETTINGS_KEY);
43
47
  _settingsCacheAt = Date.now();
44
48
  _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),
49
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
50
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
51
+ placeholderIds: (s.placeholderIds || '').trim(),
52
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
53
+
54
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
55
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
54
56
  categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
55
- intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
57
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
56
58
 
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),
59
+ enableMessageAds: parseBool(s.enableMessageAds, false),
60
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
61
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
62
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
62
63
 
63
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
64
65
  };
65
66
  return _settingsCache;
66
67
  }
@@ -71,58 +72,87 @@ async function isUserExcluded(uid, excludedGroups) {
71
72
  return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
72
73
  }
73
74
 
75
+ // ── Scripts Ezoic à injecter dans le <head> ────────────────────────────────
76
+ //
77
+ // Injectés uniquement pour les utilisateurs non-exclus.
78
+ // Recommandation Ezoic : charger ces scripts en <head> plutôt qu'en widget
79
+ // pour garantir le chargement avant le contenu de la page.
80
+
81
+ const EZOIC_HEAD_HTML = `<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>
82
+ <script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>
83
+ <script async src="//www.ezojs.com/ezoic/sa.min.js"></script>
84
+ <script>
85
+ window.ezstandalone = window.ezstandalone || {};
86
+ ezstandalone.cmd = ezstandalone.cmd || [];
87
+ </script>`;
88
+
89
+ // ── Hooks ──────────────────────────────────────────────────────────────────
90
+
74
91
  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
- }
92
+ if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
79
93
  };
80
94
 
81
95
  plugin.addAdminNavigation = async (header) => {
82
96
  header.plugins = header.plugins || [];
83
97
  header.plugins.push({
84
98
  route: '/plugins/ezoic-infinite',
85
- icon: 'fa-ad',
86
- name: 'Ezoic Infinite Ads'
99
+ icon: 'fa-ad',
100
+ name: 'Ezoic Infinite Ads',
87
101
  });
88
102
  return header;
89
103
  };
90
104
 
105
+ /**
106
+ * Injecte les scripts Ezoic dans le <head> pour les utilisateurs non-exclus.
107
+ * Hook : filter:middleware.renderHeader
108
+ * NodeBB appelle ce hook avant de rendre le header — templateData.headHTML
109
+ * est concaténé dans le <head> de chaque page.
110
+ */
111
+ plugin.injectEzoicHead = async (data) => {
112
+ try {
113
+ const settings = await getSettings();
114
+ const uid = data.templateData?.uid ?? data.req?.uid ?? 0;
115
+ const excluded = await isUserExcluded(uid, settings.excludedGroups);
116
+ if (!excluded) {
117
+ data.templateData.headHTML = (data.templateData.headHTML || '') + EZOIC_HEAD_HTML;
118
+ }
119
+ } catch (_) {}
120
+ return data;
121
+ };
122
+
91
123
  plugin.init = async ({ router, middleware }) => {
92
124
  async function render(req, res) {
93
- const settings = await getSettings();
125
+ const settings = await getSettings();
94
126
  const allGroups = await getAllGroups();
95
-
96
127
  res.render('admin/plugins/ezoic-infinite', {
97
128
  title: 'Ezoic Infinite Ads',
98
129
  ...settings,
99
130
  enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
100
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
131
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
101
132
  allGroups,
102
133
  });
103
134
  }
104
135
 
105
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
136
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
106
137
  router.get('/api/admin/plugins/ezoic-infinite', render);
107
138
 
108
139
  router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
109
140
  const settings = await getSettings();
110
141
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
111
-
112
142
  res.json({
113
143
  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,
144
+ enableBetweenAds: settings.enableBetweenAds,
145
+ showFirstTopicAd: settings.showFirstTopicAd,
146
+ placeholderIds: settings.placeholderIds,
147
+ intervalPosts: settings.intervalPosts,
148
+ enableCategoryAds: settings.enableCategoryAds,
149
+ showFirstCategoryAd: settings.showFirstCategoryAd,
120
150
  categoryPlaceholderIds: settings.categoryPlaceholderIds,
121
- intervalCategories: settings.intervalCategories,
122
- enableMessageAds: settings.enableMessageAds,
123
- showFirstMessageAd: settings.showFirstMessageAd,
124
- messagePlaceholderIds: settings.messagePlaceholderIds,
125
- messageIntervalPosts: settings.messageIntervalPosts,
151
+ intervalCategories: settings.intervalCategories,
152
+ enableMessageAds: settings.enableMessageAds,
153
+ showFirstMessageAd: settings.showFirstMessageAd,
154
+ messagePlaceholderIds: settings.messagePlaceholderIds,
155
+ messageIntervalPosts: settings.messageIntervalPosts,
126
156
  });
127
157
  });
128
158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.35",
3
+ "version": "1.7.37",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -15,6 +15,10 @@
15
15
  {
16
16
  "hook": "action:settings.set",
17
17
  "method": "onSettingsSet"
18
+ },
19
+ {
20
+ "hook": "filter:middleware.renderHeader",
21
+ "method": "injectEzoicHead"
18
22
  }
19
23
  ],
20
24
  "staticDirs": {
@@ -30,4 +34,4 @@
30
34
  "css": [
31
35
  "public/style.css"
32
36
  ]
33
- }
37
+ }
package/public/client.js CHANGED
@@ -32,10 +32,12 @@
32
32
  *
33
33
  * v34 moveDistantWrap — voir v38.
34
34
  *
35
- * v42 Seuil de recyclage aligné sur IO_MARGIN par device :
36
- * desktop -(2500 + vh), mobile -(3500 + vh).
37
- * Un wrap dans la zone IO_MARGIN peut être re-observé après recyclage
38
- * conflit. On recycle uniquement au-delà de la marge d'observation.
35
+ * v43 Seuil de recyclage abaissé à -vh (hors viewport visible).
36
+ * recycleAndMove appelle unobserve(ph) avant de déplacer le wrap,
37
+ * ce qui neutralise l'IO sur ce nœud plus de risque de showAds
38
+ * parasite après recyclage. Plus de wraps éligibles = moins de pool sec.
39
+ *
40
+ * v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
39
41
  *
40
42
  * v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
41
43
  *
@@ -310,11 +312,10 @@
310
312
  typeof ez?.define !== 'function' ||
311
313
  typeof ez?.displayMore !== 'function') return null;
312
314
 
313
- const vh = window.innerHeight || 800;
314
- // Seuil : au-delà de l'IO_MARGIN + 1vh pour être sûr que l'observer
315
- // ne peut plus déclencher de showAds sur ce wrap après recyclage.
316
- const margin = isMobile() ? 3500 : 2500;
317
- const threshold = -(margin + vh);
315
+ const vh = window.innerHeight || 800;
316
+ // Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
317
+ // après pour neutraliser l'IO — plus de showAds parasite possible.
318
+ const threshold = -vh;
318
319
  let bestEmpty = null, bestEmptyBottom = Infinity;
319
320
  let bestFilled = null, bestFilledBottom = Infinity;
320
321
 
@@ -336,6 +337,9 @@
336
337
  if (!Number.isFinite(id)) return null;
337
338
 
338
339
  const oldKey = best.getAttribute(A_ANCHOR);
340
+ // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
341
+ // parasite si le nœud était encore dans la zone IO_MARGIN.
342
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
339
343
  mutate(() => {
340
344
  best.setAttribute(A_ANCHOR, newKey);
341
345
  best.setAttribute(A_CREATED, String(ts()));