nodebb-plugin-ezoic-infinite 1.0.5 → 1.0.7

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
@@ -2,58 +2,99 @@
2
2
 
3
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
6
 
6
- const Plugin = {};
7
+ const SETTINGS_KEY = 'ezoic-infinite';
8
+ const plugin = {};
7
9
 
8
- Plugin.init = async function ({ router, middleware }) {
9
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
10
- router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
11
- };
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
+ }
12
15
 
13
- async function renderAdmin(req, res) {
14
- const settings = await meta.settings.get('ezoic-infinite');
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
+ }
15
22
 
16
- let groupNames = [];
17
- const candidates = ['groups:createtime', 'groups:visible:createtime', 'groups:alphabetical', 'groups:visible:alphabetical'];
18
- for (const set of candidates) {
19
- try {
20
- groupNames = await groups.getGroupsFromSet(set, 0, -1);
21
- if (Array.isArray(groupNames) && groupNames.length) break;
22
- } catch (e) {}
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);
23
27
  }
28
+ const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
+ const data = await groups.getGroupsData(filtered);
30
+ // Sort alphabetically for ACP usability
31
+ data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
32
+ return data;
33
+ }
34
+ async function getSettings() {
35
+ const s = await meta.settings.get(SETTINGS_KEY);
36
+ return {
37
+ // Between-post ads (simple blocks)
38
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
+ placeholderIds: (s.placeholderIds || '').trim(),
40
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
24
41
 
25
- let groupList = [];
26
- try {
27
- groupList = await groups.getGroupsData(groupNames);
28
- } catch (e) {
29
- groupList = (groupNames || []).map((name) => ({ name }));
30
- }
42
+ // "Ad message" between replies (looks like a post)
43
+ enableMessageAds: parseBool(s.enableMessageAds, false),
44
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
45
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
31
46
 
32
- groupList = (groupList || [])
33
- .filter(g => g && g.name)
34
- .sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr', { sensitivity: 'base' }));
47
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
48
+ };
49
+ }
35
50
 
36
- res.render('admin/plugins/ezoic-infinite', {
37
- title: 'Ezoic - Publicités Infinite Scroll',
38
- settings,
39
- groups: groupList,
40
- });
51
+ async function isUserExcluded(uid, excludedGroups) {
52
+ if (!uid || !excludedGroups.length) return false;
53
+ const userGroups = await groups.getUserGroups([uid]);
54
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
41
55
  }
42
56
 
43
- Plugin.addAdminNavigation = async function (header) {
57
+ plugin.addAdminNavigation = async (header) => {
44
58
  header.plugins = header.plugins || [];
45
59
  header.plugins.push({
46
60
  route: '/plugins/ezoic-infinite',
47
- icon: 'fa-bullhorn',
48
- name: 'Ezoic Infinite',
61
+ icon: 'fa-ad',
62
+ name: 'Ezoic Infinite Ads'
49
63
  });
50
64
  return header;
51
65
  };
52
66
 
53
- Plugin.addConfig = async function (config) {
54
- const settings = await meta.settings.get('ezoic-infinite');
55
- config.ezoicInfinite = settings || {};
56
- return config;
67
+ plugin.init = async ({ router, middleware }) => {
68
+ async function render(req, res) {
69
+ const settings = await getSettings();
70
+ const allGroups = await getAllGroups();
71
+
72
+ res.render('admin/plugins/ezoic-infinite', {
73
+ title: 'Ezoic Infinite Ads',
74
+ ...settings,
75
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
76
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
77
+ allGroups,
78
+ });
79
+ }
80
+
81
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
82
+ router.get('/api/admin/plugins/ezoic-infinite', render);
83
+
84
+ router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
85
+ const settings = await getSettings();
86
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
87
+
88
+ res.json({
89
+ excluded,
90
+ enableBetweenAds: settings.enableBetweenAds,
91
+ placeholderIds: settings.placeholderIds,
92
+ intervalPosts: settings.intervalPosts,
93
+ enableMessageAds: settings.enableMessageAds,
94
+ messagePlaceholderIds: settings.messagePlaceholderIds,
95
+ messageIntervalPosts: settings.messageIntervalPosts,
96
+ });
97
+ });
57
98
  };
58
99
 
59
- module.exports = Plugin;
100
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.0.5",
4
- "description": "Ezoic ads injection for NodeBB 4.x (ACP stable like 0.9.5 + injection logic like 0.9.7).",
3
+ "version": "1.0.7",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -11,6 +11,9 @@
11
11
  "ads",
12
12
  "infinite-scroll"
13
13
  ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
14
17
  "nbbpm": {
15
18
  "compatibility": "^4.0.0"
16
19
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-ezoic-infinite",
3
- "name": "Ezoic Infinite",
4
- "description": "Ezoic ads injection (topics list + topic posts) with pool recycling for infinite scroll.",
3
+ "name": "NodeBB Ezoic Infinite Ads",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "library": "./library.js",
6
6
  "hooks": [
7
7
  {
@@ -11,10 +11,6 @@
11
11
  {
12
12
  "hook": "filter:admin.header.build",
13
13
  "method": "addAdminNavigation"
14
- },
15
- {
16
- "hook": "filter:config.get",
17
- "method": "addConfig"
18
14
  }
19
15
  ],
20
16
  "staticDirs": {
@@ -26,8 +22,8 @@
26
22
  "scripts": [
27
23
  "public/client.js"
28
24
  ],
25
+ "templates": "public/templates",
29
26
  "css": [
30
27
  "public/style.css"
31
- ],
32
- "templates": "public/templates"
28
+ ]
33
29
  }
package/public/admin.js CHANGED
@@ -1,59 +1,29 @@
1
- /* global $, app, socket */
1
+ /* globals ajaxify */
2
2
  'use strict';
3
3
 
4
4
  (function () {
5
- if (window.ezoicInfiniteAdminLoaded) return;
6
- window.ezoicInfiniteAdminLoaded = true;
7
-
8
- $(document).ready(function () {
9
- const namespace = 'ezoic-infinite';
10
-
11
- function load() {
12
- socket.emit('admin.settings.get', { hash: namespace }, function (err, data) {
13
- if (err) return;
14
- data = data || {};
15
-
16
- const form = $('.ezoic-infinite-settings');
17
-
18
- form.find('[name="enableBetweenAds"]').prop('checked', data.enableBetweenAds === true || data.enableBetweenAds === 'on');
19
- form.find('[name="intervalTopics"]').val(parseInt(data.intervalTopics, 10) || 6);
20
- form.find('[name="placeholderIds"]').val(data.placeholderIds || '');
21
-
22
- form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
23
- form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
24
- form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
25
-
26
- const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
27
- form.find('[name="excludedGroups"] option').each(function () {
28
- $(this).prop('selected', selected.includes($(this).val()));
5
+ function init() {
6
+ const $form = $('.ezoic-infinite-settings');
7
+ if (!$form.length) return;
8
+
9
+ require(['settings', 'alerts'], function (Settings, alerts) {
10
+ Settings.load('ezoic-infinite', $form);
11
+
12
+ $('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
13
+ e.preventDefault();
14
+
15
+ Settings.save('ezoic-infinite', $form, function () {
16
+ // Toast vert (NodeBB core)
17
+ if (alerts && typeof alerts.success === 'function') {
18
+ alerts.success('Enregistré');
19
+ } else if (window.app && typeof window.app.alertSuccess === 'function') {
20
+ window.app.alertSuccess('Enregistré');
21
+ }
29
22
  });
30
23
  });
31
- }
32
-
33
- function save() {
34
- const form = $('.ezoic-infinite-settings');
35
- const payload = {
36
- enableBetweenAds: form.find('[name="enableBetweenAds"]').is(':checked'),
37
- intervalTopics: parseInt(form.find('[name="intervalTopics"]').val(), 10) || 6,
38
- placeholderIds: form.find('[name="placeholderIds"]').val() || '',
39
-
40
- enableMessageAds: form.find('[name="enableMessageAds"]').is(':checked'),
41
- messageIntervalPosts: parseInt(form.find('[name="messageIntervalPosts"]').val(), 10) || 3,
42
- messagePlaceholderIds: form.find('[name="messagePlaceholderIds"]').val() || '',
43
-
44
- excludedGroups: (form.find('[name="excludedGroups"]').val() || []).join(','),
45
- };
46
-
47
- socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
48
- if (err) {
49
- app.alertError(err.message || err);
50
- return;
51
- }
52
- app.alertSuccess('Paramètres enregistrés');
53
- });
54
- }
24
+ });
25
+ }
55
26
 
56
- $(document).on('click', '.ezoic-infinite-save', save);
57
- load();
58
- });
27
+ $(document).ready(init);
28
+ $(window).on('action:ajaxify.end', init);
59
29
  })();
package/public/client.js CHANGED
@@ -1,45 +1,26 @@
1
- /* global $, ajaxify, app */
2
1
  'use strict';
3
2
 
3
+ /* globals ajaxify */
4
4
  (function () {
5
5
  if (window.ezoicInfiniteLoaded) return;
6
6
  window.ezoicInfiniteLoaded = true;
7
7
 
8
- function getSettings() {
9
- return (window.config && window.config.ezoicInfinite) ? window.config.ezoicInfinite : {};
10
- }
8
+ let cachedConfig;
9
+ let lastFetch = 0;
10
+ let debounceTimer;
11
+
12
+ let inFlight = false;
13
+ let rerunRequested = false;
11
14
 
12
- let settings = getSettings();
15
+ // per page state
13
16
  let pageKey = null;
14
17
 
15
- let usedBetween = new Set();
16
- let usedMessage = new Set();
18
+ // separate pools/state
19
+ let usedBetween = new Set(); // between topics list
20
+ let usedMessage = new Set(); // between replies
17
21
  let fifoBetween = [];
18
22
  let fifoMessage = [];
19
23
 
20
- let refreshInFlight = false;
21
- let refreshQueued = false;
22
-
23
- function parsePool(text) {
24
- return String(text || '')
25
- .split(/\r?\n/)
26
- .map(s => s.trim())
27
- .filter(Boolean)
28
- .map(s => parseInt(s, 10))
29
- .filter(n => Number.isFinite(n) && n > 0);
30
- }
31
-
32
- function userExcluded() {
33
- try {
34
- const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
35
- if (!raw) return false;
36
- const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
37
- if (!excluded.length) return false;
38
- const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
39
- return excluded.some(g => myGroups.includes(g));
40
- } catch (e) { return false; }
41
- }
42
-
43
24
  function getPageKey() {
44
25
  try {
45
26
  if (ajaxify && ajaxify.data) {
@@ -56,11 +37,31 @@
56
37
  }
57
38
 
58
39
  function isCategoryTopicList() {
59
- return $('li[component="category/topic"]').length > 0 && !isTopicPage();
40
+ return document.querySelectorAll('li[component="category/topic"]').length > 0 && !isTopicPage();
41
+ }
42
+
43
+ function parsePool(raw) {
44
+ if (!raw) return [];
45
+ // accept newline, comma, space, semicolon
46
+ const arr = String(raw).split(/[\n,;\s]+/)
47
+ .map(x => parseInt(x, 10))
48
+ .filter(n => Number.isFinite(n) && n > 0);
49
+ // unique while preserving order
50
+ return Array.from(new Set(arr));
51
+ }
52
+
53
+ async function fetchConfig() {
54
+ if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
55
+ const base = (window.config && window.config.relative_path) ? window.config.relative_path : '';
56
+ const res = await fetch(base + '/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
57
+ const json = await res.json();
58
+ cachedConfig = json;
59
+ lastFetch = Date.now();
60
+ return json;
60
61
  }
61
62
 
62
63
  function cleanupForNewPage() {
63
- $('.ezoic-ad').remove();
64
+ document.querySelectorAll('.ezoic-ad').forEach(el => el.remove());
64
65
  usedBetween = new Set();
65
66
  usedMessage = new Set();
66
67
  fifoBetween = [];
@@ -85,15 +86,12 @@
85
86
  const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
86
87
  if (!existing) return;
87
88
  const wrap = existing.closest('.ezoic-ad');
88
- if (wrap) {
89
- try { $(wrap).remove(); } catch (e) { wrap.remove(); }
90
- } else {
91
- existing.remove();
92
- }
89
+ if (wrap) wrap.remove();
90
+ else existing.remove();
93
91
  destroyPlaceholder(id);
94
92
  }
95
93
 
96
- function callEzoicSingle(id) {
94
+ function callShowAdsSingle(id) {
97
95
  if (!id) return;
98
96
 
99
97
  const now = Date.now();
@@ -102,30 +100,27 @@
102
100
  if (now - last < 1200) return;
103
101
  window.__ezoicLastSingle[id] = now;
104
102
 
105
- try {
106
- window.ezstandalone = window.ezstandalone || {};
107
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
108
-
109
- const run = function () {
110
- try {
111
- if (typeof window.ezstandalone.showAds === 'function') {
112
- window.ezstandalone.showAds(id);
113
- return true;
114
- }
115
- } catch (e) {}
116
- return false;
117
- };
118
-
119
- window.ezstandalone.cmd.push(function () { run(); });
120
-
121
- let tries = 0;
122
- const tick = function () {
123
- tries++;
124
- if (run() || tries >= 8) return;
125
- setTimeout(tick, 800);
126
- };
103
+ window.ezstandalone = window.ezstandalone || {};
104
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
105
+
106
+ const run = function () {
107
+ try {
108
+ if (typeof window.ezstandalone.showAds === 'function') {
109
+ window.ezstandalone.showAds(id);
110
+ return true;
111
+ }
112
+ } catch (e) {}
113
+ return false;
114
+ };
115
+
116
+ window.ezstandalone.cmd.push(function () { run(); });
117
+
118
+ let tries = 0;
119
+ (function tick() {
120
+ tries++;
121
+ if (run() || tries >= 8) return;
127
122
  setTimeout(tick, 800);
128
- } catch (e) {}
123
+ })();
129
124
  }
130
125
 
131
126
  function pickNextId(pool, usedSet) {
@@ -133,14 +128,13 @@
133
128
  return null;
134
129
  }
135
130
 
136
- function recycle(fifo, usedSet, selectorFn) {
131
+ function recycle(fifo, usedSet, selector) {
137
132
  fifo.sort((a, b) => a.after - b.after);
138
133
  while (fifo.length) {
139
134
  const old = fifo.shift();
140
- const $el = selectorFn(old);
141
- if (!$el.length) continue;
142
-
143
- $el.remove();
135
+ const el = document.querySelector(selector(old));
136
+ if (!el) continue;
137
+ el.remove();
144
138
  usedSet.delete(old.id);
145
139
  destroyPlaceholder(old.id);
146
140
  return old.id;
@@ -148,93 +142,81 @@
148
142
  return null;
149
143
  }
150
144
 
151
- function insertAfter($target, id, cls, afterVal) {
145
+ function insertAfter(targetEl, id, cls, afterVal) {
152
146
  ensureUniquePlaceholder(id);
153
- const wrap = $(
154
- '<div class="ezoic-ad ' + cls + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
155
- '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
156
- '</div>'
157
- );
158
- $target.after(wrap);
147
+ const wrap = document.createElement('div');
148
+ wrap.className = 'ezoic-ad ' + cls;
149
+ wrap.setAttribute('data-ezoic-id', String(id));
150
+ wrap.setAttribute('data-ezoic-after', String(afterVal));
151
+ wrap.innerHTML = '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
152
+ targetEl.insertAdjacentElement('afterend', wrap);
159
153
  }
160
154
 
161
- function injectBetweenTopics() {
162
- if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
163
-
164
- const interval = parseInt(settings.intervalTopics, 10) || 6;
165
- const pool = parsePool(settings.placeholderIds);
155
+ function injectBetweenTopics(cfg) {
156
+ if (!cfg.enableBetweenAds) return;
157
+ const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
158
+ const pool = parsePool(cfg.placeholderIds);
166
159
  if (!pool.length) return;
167
160
 
168
- const $items = $('li[component="category/topic"]');
169
- if (!$items.length) return;
161
+ const items = Array.from(document.querySelectorAll('li[component="category/topic"]'));
162
+ if (!items.length) return;
170
163
 
171
- $items.each(function (idx) {
164
+ items.forEach((li, idx) => {
172
165
  const pos = idx + 1;
173
166
  if (pos % interval !== 0) return;
174
- if (idx === $items.length - 1) return;
167
+ if (idx === items.length - 1) return;
175
168
 
176
- const $li = $(this);
177
- if ($li.next('.ezoic-ad-between').length) return;
169
+ const next = li.nextElementSibling;
170
+ if (next && next.classList && next.classList.contains('ezoic-ad-between')) return;
178
171
 
179
172
  let id = pickNextId(pool, usedBetween);
180
173
  if (!id) {
181
- id = recycle(
182
- fifoBetween, usedBetween,
183
- (old) => $('.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]')
184
- );
185
- if (!id) return;
174
+ id = recycle(fifoBetween, usedBetween, (old) => '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
175
+ if (!id) return; // pool empty and nothing recyclable => stop
186
176
  }
187
177
 
188
178
  usedBetween.add(id);
189
179
  fifoBetween.push({ id, after: pos });
190
180
 
191
- insertAfter($li, id, 'ezoic-ad-between', pos);
192
- callEzoicSingle(id);
181
+ insertAfter(li, id, 'ezoic-ad-between', pos);
182
+ callShowAdsSingle(id);
193
183
  });
194
184
  }
195
185
 
196
- function injectBetweenMessages() {
197
- if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
198
-
199
- const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
200
- const pool = parsePool(settings.messagePlaceholderIds);
186
+ function injectBetweenMessages(cfg) {
187
+ if (!cfg.enableMessageAds) return;
188
+ const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
189
+ const pool = parsePool(cfg.messagePlaceholderIds);
201
190
  if (!pool.length) return;
202
191
 
203
- const $posts = $('[component="post"][data-pid]');
204
- if (!$posts.length) return;
192
+ const posts = Array.from(document.querySelectorAll('[component="post"][data-pid]'));
193
+ if (!posts.length) return;
205
194
 
206
- $posts.each(function (idx) {
207
- const postNo = idx + 1;
208
- if (postNo % interval !== 0) return;
209
- if (idx === $posts.length - 1) return;
195
+ posts.forEach((post, idx) => {
196
+ const no = idx + 1;
197
+ if (no % interval !== 0) return;
198
+ if (idx === posts.length - 1) return;
210
199
 
211
- const $post = $(this);
212
- if ($post.next('.ezoic-ad-message').length) return;
200
+ const next = post.nextElementSibling;
201
+ if (next && next.classList && next.classList.contains('ezoic-ad-message')) return;
213
202
 
214
203
  let id = pickNextId(pool, usedMessage);
215
204
  if (!id) {
216
- id = recycle(
217
- fifoMessage, usedMessage,
218
- (old) => $('.ezoic-ad-message[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]')
219
- );
205
+ id = recycle(fifoMessage, usedMessage, (old) => '.ezoic-ad-message[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
220
206
  if (!id) return;
221
207
  }
222
208
 
223
209
  usedMessage.add(id);
224
- fifoMessage.push({ id, after: postNo });
210
+ fifoMessage.push({ id, after: no });
225
211
 
226
- insertAfter($post, id, 'ezoic-ad-message', postNo);
227
- callEzoicSingle(id);
212
+ insertAfter(post, id, 'ezoic-ad-message', no);
213
+ callShowAdsSingle(id);
228
214
  });
229
215
  }
230
216
 
231
- function refresh() {
232
- settings = getSettings();
233
- if (!settings) return;
234
- if (userExcluded()) return;
235
-
236
- if (refreshInFlight) { refreshQueued = true; return; }
237
- refreshInFlight = true;
217
+ async function run() {
218
+ if (inFlight) { rerunRequested = true; return; }
219
+ inFlight = true;
238
220
 
239
221
  try {
240
222
  const key = getPageKey();
@@ -243,29 +225,56 @@
243
225
  cleanupForNewPage();
244
226
  }
245
227
 
246
- if (isTopicPage()) injectBetweenMessages();
247
- else if (isCategoryTopicList()) injectBetweenTopics();
228
+ const cfg = await fetchConfig();
229
+ if (!cfg || cfg.excluded) return;
230
+
231
+ if (isTopicPage()) injectBetweenMessages(cfg);
232
+ else if (isCategoryTopicList()) injectBetweenTopics(cfg);
233
+ } catch (e) {
234
+ // silent
248
235
  } finally {
249
- refreshInFlight = false;
250
- if (refreshQueued) { refreshQueued = false; setTimeout(refresh, 50); }
236
+ inFlight = false;
237
+ if (rerunRequested) {
238
+ rerunRequested = false;
239
+ setTimeout(run, 50);
240
+ }
251
241
  }
252
242
  }
253
243
 
254
- function boot() {
255
- refresh();
256
- setTimeout(refresh, 1200);
244
+ function scheduleRun() {
245
+ clearTimeout(debounceTimer);
246
+ debounceTimer = setTimeout(run, 150);
247
+ }
248
+
249
+ function bindNodeBBEvents() {
250
+ // NodeBB triggers these events through jQuery on window
251
+ if (window.jQuery) {
252
+ const $w = window.jQuery(window);
253
+ $w.off('.ezoicInfinite');
254
+ $w.on('action:ajaxify.end.ezoicInfinite', scheduleRun);
255
+ $w.on('action:posts.loaded.ezoicInfinite', scheduleRun);
256
+ $w.on('action:topic.loaded.ezoicInfinite', scheduleRun);
257
+ $w.on('action:topics.loaded.ezoicInfinite', scheduleRun);
258
+ $w.on('action:category.loaded.ezoicInfinite', scheduleRun);
259
+ $w.on('action:ajaxify.start.ezoicInfinite', function () {
260
+ pageKey = null;
261
+ cleanupForNewPage();
262
+ });
263
+ }
257
264
  }
258
265
 
259
- $(document).ready(boot);
260
- $(window).on('action:ajaxify.end', boot);
266
+ bindNodeBBEvents();
261
267
 
262
- $(window).on('action:posts.loaded action:topics.loaded action:topic.loaded action:category.loaded', function () {
263
- refresh();
264
- setTimeout(refresh, 600);
265
- });
268
+ // Run immediately, so it works on first ajaxify navigation too
269
+ run();
270
+ setTimeout(run, 1200);
266
271
 
267
- $(window).on('action:ajaxify.start', function () {
268
- pageKey = null;
269
- cleanupForNewPage();
270
- });
272
+ // Also run on hard-refresh initial load
273
+ if (document.readyState === 'loading') {
274
+ document.addEventListener('DOMContentLoaded', function () {
275
+ bindNodeBBEvents();
276
+ run();
277
+ setTimeout(run, 1200);
278
+ });
279
+ }
271
280
  })();
@@ -1,78 +1,59 @@
1
- <div class="row">
2
- <div class="col-lg-9">
3
- <div class="card">
4
- <div class="card-header">
5
- <strong>Ezoic Infinite</strong>
6
- </div>
7
- <div class="card-body">
8
- <form class="ezoic-infinite-settings">
9
- <div class="mb-3">
10
- <label class="form-label">Groupes exclus</label>
11
- <select multiple class="form-control" name="excludedGroups" size="10">
12
- <!-- BEGIN groups -->
13
- <option value="{groups.name}">{groups.name}</option>
14
- <!-- END groups -->
15
- </select>
16
- <p class="form-text">Aucune publicité ne sera affichée pour ces groupes.</p>
17
- </div>
1
+ <div class="acp-page-container">
2
+ <h2>Ezoic - Publicités Infinite Scroll Ads</h2>
18
3
 
19
- <hr />
4
+ <form class="ezoic-infinite-settings" role="form">
5
+ <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
20
6
 
21
- <h5>Publicités entre les topics (liste des sujets)</h5>
22
-
23
- <div class="form-check mb-3">
24
- <input class="form-check-input" type="checkbox" name="enableBetweenAds" id="enableBetweenAds">
25
- <label class="form-check-label" for="enableBetweenAds">Activer</label>
26
- </div>
27
-
28
- <div class="mb-3">
29
- <label class="form-label">Intervalle (après chaque N topics)</label>
30
- <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
31
- </div>
7
+ <div class="form-check mb-3">
8
+ <input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
9
+ <label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
10
+ </div>
32
11
 
33
- <div class="mb-3">
34
- <label class="form-label">Pool d'IDs placeholder (un par ligne)</label>
35
- <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
36
- </div>
12
+ <div class="mb-3">
13
+ <label class="form-label" for="placeholderIds">Pool dIDs Ezoic (entre posts)</label>
14
+ <textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
15
+ <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</p>
16
+ </div>
37
17
 
38
- <hr />
18
+ <div class="mb-3">
19
+ <label class="form-label" for="intervalPosts">Afficher une pub tous les N posts</label>
20
+ <input type="number" id="intervalPosts" name="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
21
+ </div>
39
22
 
40
- <h5>Publicités entre les messages (dans un topic)</h5>
23
+ <hr/>
41
24
 
42
- <div class="form-check mb-3">
43
- <input class="form-check-input" type="checkbox" name="enableMessageAds" id="enableMessageAds">
44
- <label class="form-check-label" for="enableMessageAds">Activer</label>
45
- </div>
25
+ <h4 class="mt-3">Pubs “message” entre les réponses</h4>
26
+ <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
46
27
 
47
- <div class="mb-3">
48
- <label class="form-label">Intervalle (après chaque N messages)</label>
49
- <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
50
- </div>
28
+ <div class="form-check mb-3">
29
+ <input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
30
+ <label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
31
+ </div>
51
32
 
52
- <div class="mb-3">
53
- <label class="form-label">Pool d'IDs placeholder messages (un par ligne)</label>
54
- <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
55
- </div>
33
+ <div class="mb-3">
34
+ <label class="form-label" for="messagePlaceholderIds">Pool dIDs Ezoic (message)</label>
35
+ <textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
36
+ <p class="form-text">Pool séparé recommandé pour éviter la réutilisation d’IDs. IMPORTANT : ne réutilise pas les mêmes IDs dans les deux pools.</p>
37
+ </div>
56
38
 
57
- <button type="button" class="btn btn-primary ezoic-infinite-save">
58
- <i class="fa fa-save"></i> Enregistrer
59
- </button>
60
- </form>
61
- </div>
39
+ <div class="mb-3">
40
+ <label class="form-label" for="messageIntervalPosts">Afficher un “message pub” tous les N messages</label>
41
+ <input type="number" id="messageIntervalPosts" name="messageIntervalPosts" class="form-control" value="{messageIntervalPosts}" min="1">
62
42
  </div>
63
- </div>
64
43
 
65
- <div class="col-lg-3">
66
- <div class="card">
67
- <div class="card-header">
68
- <strong>Aide</strong>
69
- </div>
70
- <div class="card-body">
71
- <p>Placeholder Ezoic :</p>
72
- <pre class="mb-0">&lt;div id="ezoic-pub-ad-placeholder-123"&gt;&lt;/div&gt;</pre>
73
- <hr />
74
- <p class="mb-0">Les IDs doivent exister côté Ezoic (ad units configurées).</p>
75
- </div>
44
+ <hr/>
45
+
46
+ <h4 class="mt-3">Exclusions</h4>
47
+ <div class="mb-3">
48
+ <label class="form-label" for="excludedGroups">Groupes exclus</label>
49
+ <select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
50
+ <!-- BEGIN allGroups -->
51
+ <option value="{allGroups.name}">{allGroups.name}</option>
52
+ <!-- END allGroups -->
53
+ </select>
54
+ <p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
76
55
  </div>
77
- </div>
56
+
57
+ <button id="save" class="btn btn-primary">Enregistrer</button>
58
+ </form>
78
59
  </div>
package/README.md DELETED
@@ -1,5 +0,0 @@
1
- nodebb-plugin-ezoic-infinite v1.0.5
2
-
3
- - ACP kept in the same spirit as v0.9.5 (server-rendered groups + admin.settings.get/set)
4
- - Injection logic matches v0.9.7: showAds called one placeholder at a time, separate pools, FIFO recycling.
5
- - Frontend settings are read from window.config.ezoicInfinite (filter:config.get), no admin endpoints used on frontend.