nodebb-plugin-ezoic-infinite 0.8.9 → 0.9.1

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,96 +2,36 @@
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');
6
5
 
7
- const SETTINGS_KEY = 'ezoic-infinite';
8
- const plugin = {};
6
+ const Plugin = {};
9
7
 
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
- const names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
- const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
26
- const data = await groups.getGroupsData(filtered);
27
- // Sort alphabetically for ACP usability
28
- data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
29
- return data;
30
- }
31
- async function getSettings() {
32
- const s = await meta.settings.get(SETTINGS_KEY);
33
- return {
34
- // Between-post ads (simple blocks)
35
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
36
- placeholderIds: (s.placeholderIds || '').trim(),
37
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
38
-
39
- // "Ad message" between replies (looks like a post)
40
- enableMessageAds: parseBool(s.enableMessageAds, false),
41
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
42
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
8
+ Plugin.init = async function (params) {
9
+ const { router, middleware } = params;
43
10
 
44
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
45
- };
46
- }
11
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
12
+ router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
13
+ };
47
14
 
48
- async function isUserExcluded(uid, excludedGroups) {
49
- if (!uid || !excludedGroups.length) return false;
50
- const userGroups = await groups.getUserGroups([uid]);
51
- return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
15
+ async function renderAdmin(req, res) {
16
+ const settings = await meta.settings.get('ezoic-infinite');
17
+ // groups list for exclusions
18
+ const groupList = await groups.getGroups('groups:visible:createtime', 0, -1);
19
+ groupList.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
20
+ res.render('admin/plugins/ezoic-infinite', {
21
+ title: 'Ezoic Infinite',
22
+ settings,
23
+ groups: groupList,
24
+ });
52
25
  }
53
26
 
54
- plugin.addAdminNavigation = async (header) => {
27
+ Plugin.addAdminNavigation = async function (header) {
55
28
  header.plugins = header.plugins || [];
56
29
  header.plugins.push({
57
30
  route: '/plugins/ezoic-infinite',
58
- icon: 'fa-ad',
59
- name: 'Ezoic Infinite Ads'
31
+ icon: 'fa-bullhorn',
32
+ name: 'Ezoic Infinite',
60
33
  });
61
34
  return header;
62
35
  };
63
36
 
64
- plugin.init = async ({ router, middleware }) => {
65
- async function render(req, res) {
66
- const settings = await getSettings();
67
- const allGroups = await getAllGroups();
68
-
69
- res.render('admin/plugins/ezoic-infinite', {
70
- title: 'Ezoic Infinite Ads',
71
- ...settings,
72
- enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
73
- enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
74
- allGroups,
75
- });
76
- }
77
-
78
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
79
- router.get('/api/admin/plugins/ezoic-infinite', render);
80
-
81
- router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
82
- const settings = await getSettings();
83
- const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
84
-
85
- res.json({
86
- excluded,
87
- enableBetweenAds: settings.enableBetweenAds,
88
- placeholderIds: settings.placeholderIds,
89
- intervalPosts: settings.intervalPosts,
90
- enableMessageAds: settings.enableMessageAds,
91
- messagePlaceholderIds: settings.messagePlaceholderIds,
92
- messageIntervalPosts: settings.messageIntervalPosts,
93
- });
94
- });
95
- };
96
-
97
- module.exports = plugin;
37
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.8.9",
4
- "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
3
+ "version": "0.9.1",
4
+ "description": "Ezoic ads injection for NodeBB infinite scroll (topics list + topic posts) using a pool of placeholder IDs.",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -11,9 +11,6 @@
11
11
  "ads",
12
12
  "infinite-scroll"
13
13
  ],
14
- "engines": {
15
- "node": ">=18"
16
- },
17
14
  "nbbpm": {
18
15
  "compatibility": "^4.0.0"
19
16
  }
package/plugin.json CHANGED
@@ -22,8 +22,8 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "templates": "public/templates",
26
25
  "css": [
27
26
  "public/style.css"
28
- ]
27
+ ],
28
+ "templates": "public/templates"
29
29
  }
package/public/admin.js CHANGED
@@ -1,29 +1,51 @@
1
- /* globals ajaxify */
1
+ /* global $, app, socket */
2
2
  'use strict';
3
3
 
4
- (function () {
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
- }
22
- });
4
+ $(document).ready(function () {
5
+ const namespace = 'ezoic-infinite';
6
+
7
+ function load() {
8
+ socket.emit('admin.settings.get', { hash: namespace }, function (err, data) {
9
+ if (err) return;
10
+ data = data || {};
11
+
12
+ const form = $('.ezoic-infinite-settings');
13
+ form.find('[name="enableBetweenAds"]').prop('checked', data.enableBetweenAds === true || data.enableBetweenAds === 'on');
14
+ form.find('[name="intervalTopics"]').val(parseInt(data.intervalTopics, 10) || 6);
15
+ form.find('[name="placeholderIds"]').val(data.placeholderIds || '');
16
+
17
+ form.find('[name="enableMessageAds"]').prop('checked', data.enableMessageAds === true || data.enableMessageAds === 'on');
18
+ form.find('[name="messageIntervalPosts"]').val(parseInt(data.messageIntervalPosts, 10) || 3);
19
+ form.find('[name="messagePlaceholderIds"]').val(data.messagePlaceholderIds || '');
20
+
21
+ const selected = (data.excludedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
22
+ form.find('[name="excludedGroups"] option').each(function () {
23
+ $(this).prop('selected', selected.includes($(this).val()));
23
24
  });
24
25
  });
25
26
  }
26
27
 
27
- $(document).ready(init);
28
- $(window).on('action:ajaxify.end', init);
29
- })();
28
+ function save() {
29
+ const form = $('.ezoic-infinite-settings');
30
+ const payload = {
31
+ enableBetweenAds: form.find('[name="enableBetweenAds"]').is(':checked'),
32
+ intervalTopics: parseInt(form.find('[name="intervalTopics"]').val(), 10) || 6,
33
+ placeholderIds: form.find('[name="placeholderIds"]').val() || '',
34
+ enableMessageAds: form.find('[name="enableMessageAds"]').is(':checked'),
35
+ messageIntervalPosts: parseInt(form.find('[name="messageIntervalPosts"]').val(), 10) || 3,
36
+ messagePlaceholderIds: form.find('[name="messagePlaceholderIds"]').val() || '',
37
+ excludedGroups: (form.find('[name="excludedGroups"]').val() || []).join(','),
38
+ };
39
+
40
+ socket.emit('admin.settings.set', { hash: namespace, values: payload }, function (err) {
41
+ if (err) {
42
+ app.alertError(err.message || err);
43
+ return;
44
+ }
45
+ app.alertSuccess('Settings saved');
46
+ });
47
+ }
48
+
49
+ $('.ezoic-infinite-save').on('click', save);
50
+ load();
51
+ });
package/public/client.js CHANGED
@@ -1,380 +1,331 @@
1
+ /* global $, ajaxify, app */
1
2
  'use strict';
2
3
 
3
- /* globals ajaxify */
4
- window.ezoicInfiniteLoaded = true;
5
-
6
- let cachedConfig;
7
- let lastFetch = 0;
8
- let debounceTimer;
9
-
10
- let inFlight = false;
11
- let rerunRequested = false;
12
-
13
- // Per-page state (keyed by tid/cid)
14
- let pageKey = null;
15
-
16
- // Topic page state: anchor ads to absolute post number (not DOM index)
17
- let seenAfterPostNo = new Set(); // post numbers we've already inserted an ad after
18
- let usedIds = new Set(); // ids currently in DOM
19
- let fifo = []; // [{afterPostNo, id}]
20
-
21
- // Category page state: anchor to absolute topic position if available
22
- let seenAfterTopicPos = new Set(); // topic positions we've inserted after
23
- let fifoCat = []; // [{afterPos, id}]
24
-
25
- function resetState() {
26
- seenAfterPostNo = new Set();
27
- seenAfterTopicPos = new Set();
28
- usedIds = new Set();
29
- fifo = [];
30
- fifoCat = [];
31
- }
32
-
33
- function getPageKey() {
34
- try {
35
- if (ajaxify && ajaxify.data) {
36
- if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
37
- if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
38
- }
39
- } catch (e) {}
40
- return window.location.pathname;
41
- }
42
-
43
- function parsePool(raw) {
44
- if (!raw) return [];
45
- return Array.from(new Set(
46
- String(raw).split(/[\n,;\s]+/)
47
- .map(x => parseInt(x, 10))
48
- .filter(n => Number.isFinite(n) && n > 0)
49
- ));
50
- }
51
-
52
- async function fetchConfig() {
53
- if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
54
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
55
- cachedConfig = await res.json();
56
- lastFetch = Date.now();
57
- return cachedConfig;
58
- }
59
-
60
- function isTopicPage() {
61
- return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
62
- }
63
-
64
- function isCategoryTopicListPage() {
65
- return $('li[component="category/topic"]').length > 0;
66
- }
67
-
68
- function getTopicPosts() {
69
- const $primary = $('[component="post"][data-pid]');
70
- if ($primary.length) return $primary;
71
-
72
- // fallback: top-level with post/content
73
- return $('[data-pid]').filter(function () {
74
- const $el = $(this);
75
- const hasContent = $el.find('[component="post/content"]').length > 0;
76
- const nested = $el.parents('[data-pid]').length > 0;
77
- return hasContent && !nested;
78
- });
79
- }
80
-
81
- function getCategoryTopicItems() {
82
- return $('li[component="category/topic"]');
83
- }
84
-
85
- // If target's parent is UL/OL, wrapper MUST be LI (otherwise browser may move it to top)
86
- function wrapperTagFor($target) {
87
- if (!$target || !$target.length) return 'div';
88
- const parentTag = ($target.parent().prop('tagName') || '').toUpperCase();
89
- if (parentTag === 'UL' || parentTag === 'OL') return 'li';
90
- const selfTag = ($target.prop('tagName') || '').toUpperCase();
91
- if (selfTag === 'LI') return 'li';
92
- return 'div';
93
- }
94
-
95
- function makeWrapperLike($target, classes, innerHtml, attrs) {
96
- const tag = wrapperTagFor($target);
97
- const attrStr = attrs ? ' ' + attrs : '';
98
- if (tag === 'li') {
99
- return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
100
- }
101
- return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
102
- }
4
+ (function () {
5
+ if (window.ezoicInfiniteLoaded) return;
6
+ window.ezoicInfiniteLoaded = true;
7
+
8
+ const SETTINGS_NS = 'ezoic-infinite';
9
+
10
+ let settings = null;
11
+ let pageKey = null;
103
12
 
104
- function cleanupOnNav() {
105
- $('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad-between').remove();
106
- }
13
+ // Separate pools for category/topic
14
+ let usedTopic = new Set();
15
+ let usedCat = new Set();
16
+ let fifoTopic = []; // [{id, afterNo}]
17
+ let fifoCat = []; // [{id, afterPos}]
107
18
 
108
- function pickNextId(pool) {
109
- for (const id of pool) {
110
- if (!usedIds.has(id)) return id;
19
+ function parsePool(text) {
20
+ return String(text || '')
21
+ .split(/\r?\n/)
22
+ .map(s => s.trim())
23
+ .filter(Boolean)
24
+ .map(s => parseInt(s, 10))
25
+ .filter(n => Number.isFinite(n) && n > 0);
111
26
  }
112
- return null;
113
- }
114
-
115
- function removeOldestTopicAd() {
116
- fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
117
- const old = fifo.shift();
118
- if (!old) return false;
119
-
120
- const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
121
- const $el = $(sel);
122
- if ($el.length) $el.remove();
123
-
124
- usedIds.delete(old.id);
125
- // DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
126
- return true;
127
- }
128
-
129
- function removeOldestCategoryAd() {
130
- fifoCat.sort((a, b) => a.afterPos - b.afterPos);
131
- const old = fifoCat.shift();
132
- if (!old) return false;
133
-
134
- const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
135
- const $el = $(sel);
136
- if ($el.length) $el.remove();
137
-
138
- usedIds.delete(old.id);
139
- return true;
140
- }
141
-
142
- function callEzoic(ids) {
143
- // ids optional; if omitted, we will scan DOM for unrendered placeholders
144
- window.ezstandalone = window.ezstandalone || {};
145
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
146
-
147
- const collect = function () {
148
- const list = [];
149
- document.querySelectorAll('.ezoic-ad [id^="ezoic-pub-ad-placeholder-"]').forEach(function (ph) {
150
- const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
151
- const id = parseInt(idStr, 10);
152
- if (!Number.isFinite(id) || id <= 0) return;
153
-
154
- const wrap = ph.closest('.ezoic-ad');
155
- if (!wrap) return;
156
- if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
157
27
 
158
- list.push(id);
159
- wrap.setAttribute('data-ezoic-rendered', '1');
28
+ function loadSettings(cb) {
29
+ socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
30
+ settings = data || {};
31
+ cb && cb();
160
32
  });
161
- // de-dupe
162
- return Array.from(new Set(list));
163
- };
33
+ }
164
34
 
165
- const toShow = (ids && ids.length) ? Array.from(new Set(ids)) : collect();
166
- if (!toShow.length) return;
35
+ function userExcluded() {
36
+ try {
37
+ const raw = (settings && settings.excludedGroups) ? String(settings.excludedGroups) : '';
38
+ if (!raw) return false;
39
+ const excluded = raw.split(',').map(s => s.trim()).filter(Boolean);
40
+ if (!excluded.length) return false;
41
+ const myGroups = (app.user && app.user.groups) ? app.user.groups : [];
42
+ return excluded.some(g => myGroups.includes(g));
43
+ } catch (e) {
44
+ return false;
45
+ }
46
+ }
167
47
 
168
- const run = function () {
48
+ function getPageKey() {
169
49
  try {
170
- if (typeof window.ezstandalone.showAds === 'function') {
171
- window.ezstandalone.showAds.apply(window.ezstandalone, toShow);
172
- return true;
50
+ if (ajaxify && ajaxify.data) {
51
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
52
+ if (ajaxify.data.cid) return 'cid:' + ajaxify.data.cid + ':' + window.location.pathname;
173
53
  }
174
54
  } catch (e) {}
175
- return false;
176
- };
177
-
178
- setupAdAutoHeight();
179
- window.ezstandalone.cmd.push(function () { run(); });
180
-
181
- // Retry only if showAds isn't available yet
182
- let tries = 0;
183
- const maxTries = 6;
184
- const retry = function () {
185
- tries++;
186
- const ok = run();
187
- if (ok) return;
188
- try {
189
- if (typeof window.ezstandalone.showAds === 'function') return;
190
- } catch (e) {}
191
- if (tries < maxTries) setTimeout(retry, 800);
192
- };
55
+ return window.location.pathname;
56
+ }
57
+
58
+ function isTopicPage() {
59
+ try { if (ajaxify && ajaxify.data && ajaxify.data.tid) return true; } catch (e) {}
60
+ return /^\/topic\//.test(window.location.pathname);
61
+ }
62
+
63
+ function isCategoryTopicList() {
64
+ return $('li[component="category/topic"]').length > 0 && !isTopicPage();
65
+ }
66
+
67
+ function cleanupForNewPage() {
68
+ $('.ezoic-ad').remove();
69
+ usedTopic = new Set();
70
+ usedCat = new Set();
71
+ fifoTopic = [];
72
+ fifoCat = [];
73
+ }
193
74
 
194
- try {
195
- if (typeof window.ezstandalone.showAds !== 'function') {
196
- setTimeout(retry, 800);
75
+ function pickNextId(pool, usedSet) {
76
+ for (const id of pool) {
77
+ if (!usedSet.has(id)) return id;
197
78
  }
198
- } catch (e) {
199
- setTimeout(retry, 800);
79
+ return null;
200
80
  }
201
- }
202
-
203
- function getPostNumber($post) {
204
- const di = parseInt($post.attr('data-index'), 10);
205
- if (Number.isFinite(di) && di > 0) return di;
206
-
207
- const txt = ($post.find('a.post-index').first().text() || '').trim();
208
- const m = txt.match(/#\s*(\d+)/);
209
- if (m) return parseInt(m[1], 10);
210
-
211
- return NaN;
212
- }
213
-
214
- function getTopicPos($item) {
215
- const pos = parseInt($item.attr('data-index'), 10);
216
- if (Number.isFinite(pos) && pos >= 0) return pos + 1;
217
- const schemaPos = parseInt($item.find('meta[itemprop="position"]').attr('content'), 10);
218
- if (Number.isFinite(schemaPos) && schemaPos > 0) return schemaPos;
219
- return NaN;
220
- }
221
-
222
- function injectTopicMessageAds($posts, pool, interval) {
223
- const newIds = [];
224
-
225
- $posts.each(function () {
226
- const $p = $(this);
227
- // Never insert after the last real post: it can break NodeBB infinite scroll
228
- if ($p.is($posts.last())) return;
229
- const postNo = getPostNumber($p);
230
- if (!Number.isFinite(postNo) || postNo <= 0) return;
231
-
232
- if (postNo % interval !== 0) return;
233
- if (seenAfterPostNo.has(postNo)) return;
234
-
235
- let id = pickNextId(pool);
236
- if (!id) { return; }
237
-
238
- const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
239
- const html = makeWrapperLike(
240
- $p,
241
- 'ezoic-ad-post ezoic-ad',
242
- inner,
243
- 'data-ezoic-after="' + postNo + '" data-ezoic-id="' + id + '"'
244
- );
245
81
 
246
- $p.after(html);
82
+ function destroyPlaceholder(id) {
83
+ try {
84
+ window.ezstandalone = window.ezstandalone || {};
85
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
86
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
87
+ window.ezstandalone.destroyPlaceholders(id);
88
+ return;
89
+ }
90
+ window.ezstandalone.cmd.push(function () {
91
+ try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
92
+ });
93
+ } catch (e) {}
94
+ }
247
95
 
248
- seenAfterPostNo.add(postNo);
249
- usedIds.add(id);
250
- fifo.push({ afterPostNo: postNo, id: id });
251
- newIds.push(id);
252
- });
96
+ function callEzoic(ids) {
97
+ if (!ids || !ids.length) return;
98
+ try {
99
+ window.ezstandalone = window.ezstandalone || {};
100
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
101
+ const run = function () {
102
+ try {
103
+ if (typeof window.ezstandalone.showAds === 'function') {
104
+ window.ezstandalone.showAds.apply(window.ezstandalone, ids);
105
+ return true;
106
+ }
107
+ } catch (e) {}
108
+ return false;
109
+ };
110
+ window.ezstandalone.cmd.push(function () { run(); });
111
+ // retries in case ez loads late
112
+ let tries = 0;
113
+ const tick = function () {
114
+ tries++;
115
+ if (run() || tries >= 10) return;
116
+ setTimeout(tick, 800);
117
+ };
118
+ setTimeout(tick, 800);
119
+ } catch (e) {}
120
+ }
253
121
 
254
- return newIds;
255
- }
122
+ // Auto height: show wrapper only when placeholder gets children
123
+ function setupAutoHeight() {
124
+ if (window.__ezoicAutoHeight) return;
125
+ window.__ezoicAutoHeight = true;
256
126
 
257
- function injectCategoryBetweenAds($items, pool, interval) {
258
- const newIds = [];
127
+ const mark = function (wrap) {
128
+ if (!wrap) return;
129
+ const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
130
+ if (ph && ph.children && ph.children.length) {
131
+ wrap.classList.add('ezoic-filled');
132
+ }
133
+ };
259
134
 
260
- $items.each(function () {
261
- const $it = $(this);
262
- // Never insert after the last real topic item (keeps NodeBB infinite scroll working)
263
- if ($it.is($items.last())) return;
264
- const pos = getTopicPos($it);
265
- if (!Number.isFinite(pos) || pos <= 0) return;
135
+ const scan = function () {
136
+ document.querySelectorAll('.ezoic-ad').forEach(mark);
137
+ };
266
138
 
267
- if (pos % interval !== 0) return;
268
- if (seenAfterTopicPos.has(pos)) return;
139
+ scan();
140
+ setInterval(scan, 1000);
269
141
 
270
- let id = pickNextId(pool);
271
- if (!id) { return; }
142
+ try {
143
+ const mo = new MutationObserver(scan);
144
+ mo.observe(document.body, { childList: true, subtree: true });
145
+ } catch (e) {}
146
+ }
272
147
 
273
- const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
274
- const html = makeWrapperLike(
275
- $it,
276
- 'ezoic-ad-topic ezoic-ad',
277
- placeholder,
278
- 'data-ezoic-after="' + pos + '" data-ezoic-id="' + id + '"'
148
+ function insertAfter($target, id, kind, afterVal) {
149
+ const wrap = $(
150
+ '<div class="ezoic-ad ezoic-ad-' + kind + '" data-ezoic-id="' + id + '" data-ezoic-after="' + afterVal + '">' +
151
+ '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>' +
152
+ '</div>'
279
153
  );
154
+ $target.after(wrap);
155
+ return wrap;
156
+ }
280
157
 
281
- $it.after(html);
158
+ function recycleTopic($posts) {
159
+ fifoTopic.sort((a,b) => a.afterNo - b.afterNo);
160
+ while (fifoTopic.length) {
161
+ const old = fifoTopic.shift();
162
+ const sel = '.ezoic-ad-topic[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterNo + '"]';
163
+ const $el = $(sel);
164
+ if (!$el.length) continue;
165
+
166
+ // don't recycle if this is right before the sentinel (after last post)
167
+ try {
168
+ const $last = $posts.last();
169
+ if ($last.length && $el.prev().is($last)) {
170
+ fifoTopic.push(old);
171
+ return null;
172
+ }
173
+ } catch (e) {}
282
174
 
283
- seenAfterTopicPos.add(pos);
284
- usedIds.add(id);
285
- fifoCat.push({ afterPos: pos, id: id });
286
- newIds.push(id);
287
- });
175
+ $el.remove();
176
+ usedTopic.delete(old.id);
177
+ destroyPlaceholder(old.id);
178
+ return old.id;
179
+ }
180
+ return null;
181
+ }
288
182
 
289
- return newIds;
290
- }
183
+ function recycleCat($items) {
184
+ fifoCat.sort((a,b) => a.afterPos - b.afterPos);
185
+ while (fifoCat.length) {
186
+ const old = fifoCat.shift();
187
+ const sel = '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.afterPos + '"]';
188
+ const $el = $(sel);
189
+ if (!$el.length) continue;
190
+
191
+ try {
192
+ const $last = $items.last();
193
+ if ($last.length && $el.prev().is($last)) {
194
+ fifoCat.push(old);
195
+ return null;
196
+ }
197
+ } catch (e) {}
291
198
 
292
- async function refreshAds() {
293
- const key = getPageKey();
294
- if (pageKey !== key) {
295
- pageKey = key;
296
- resetState();
297
- cleanupOnNav();
199
+ $el.remove();
200
+ usedCat.delete(old.id);
201
+ destroyPlaceholder(old.id);
202
+ return old.id;
203
+ }
204
+ return null;
298
205
  }
299
206
 
300
- if (inFlight) { rerunRequested = true; return; }
301
- inFlight = true;
207
+ function injectInTopic() {
208
+ if (!(settings && (settings.enableMessageAds === true || settings.enableMessageAds === 'on'))) return;
209
+ const interval = parseInt(settings.messageIntervalPosts, 10) || 3;
210
+ const pool = parsePool(settings.messagePlaceholderIds);
211
+ if (!pool.length) return;
212
+
213
+ const $posts = $('[component="post"][data-pid]');
214
+ if (!$posts.length) return;
302
215
 
303
- try {
304
- const cfg = await fetchConfig();
305
- if (!cfg || cfg.excluded) return;
216
+ const newIds = [];
217
+ $posts.each(function (idx) {
218
+ const postNo = idx + 1; // 1-based within loaded set
219
+ if (postNo % interval !== 0) return;
306
220
 
307
- const betweenPool = parsePool(cfg.placeholderIds);
308
- const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
221
+ // Do not insert after the last post in DOM (to keep infinite scroll sentinel stable)
222
+ if (idx === $posts.length - 1) return;
309
223
 
310
- const messagePool = parsePool(cfg.messagePlaceholderIds);
311
- const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
224
+ const $post = $(this);
225
+ const existing = $post.next('.ezoic-ad-topic');
226
+ if (existing.length) return;
312
227
 
313
- const onTopic = isTopicPage();
314
- const onCategory = !onTopic && isCategoryTopicListPage();
228
+ let id = pickNextId(pool, usedTopic);
229
+ if (!id) {
230
+ id = recycleTopic($posts);
231
+ if (!id) return;
232
+ }
233
+ usedTopic.add(id);
234
+ fifoTopic.push({ id, afterNo: postNo });
235
+
236
+ insertAfter($post, id, 'topic', postNo).addClass('ezoic-ad-topic');
237
+ newIds.push(id);
238
+ });
239
+
240
+ if (newIds.length) callEzoic(newIds);
241
+ }
242
+
243
+ function injectInCategory() {
244
+ if (!(settings && (settings.enableBetweenAds === true || settings.enableBetweenAds === 'on'))) return;
245
+ const interval = parseInt(settings.intervalTopics, 10) || 6;
246
+ const pool = parsePool(settings.placeholderIds);
247
+ if (!pool.length) return;
248
+
249
+ const $items = $('li[component="category/topic"]');
250
+ if (!$items.length) return;
315
251
 
316
252
  const newIds = [];
253
+ $items.each(function (idx) {
254
+ const pos = idx + 1;
255
+ if (pos % interval !== 0) return;
317
256
 
318
- if (onCategory) {
319
- const $items = getCategoryTopicItems();
320
- if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
321
- newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
322
- }
323
- callEzoic(newIds);
324
- callEzoic();
325
- return;
326
- }
257
+ if (idx === $items.length - 1) return;
327
258
 
328
- if (onTopic) {
329
- const $posts = getTopicPosts();
330
- if (cfg.enableMessageAds && messagePool.length && $posts.length) {
331
- newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
259
+ const $li = $(this);
260
+ const existing = $li.next('.ezoic-ad-between');
261
+ if (existing.length) return;
262
+
263
+ let id = pickNextId(pool, usedCat);
264
+ if (!id) {
265
+ id = recycleCat($items);
266
+ if (!id) return;
332
267
  }
333
- callEzoic(newIds);
334
- callEzoic();
268
+ usedCat.add(id);
269
+ fifoCat.push({ id, afterPos: pos });
270
+
271
+ insertAfter($li, id, 'between', pos).addClass('ezoic-ad-between');
272
+ newIds.push(id);
273
+ });
274
+
275
+ if (newIds.length) callEzoic(newIds);
276
+ }
277
+
278
+ function refresh() {
279
+ if (!settings) return;
280
+ if (userExcluded()) return;
281
+
282
+ const key = getPageKey();
283
+ if (pageKey !== key) {
284
+ pageKey = key;
285
+ cleanupForNewPage();
335
286
  }
336
- } finally {
337
- inFlight = false;
338
- if (rerunRequested) {
339
- rerunRequested = false;
340
- setTimeout(refreshAds, 160);
287
+
288
+ setupAutoHeight();
289
+
290
+ if (isTopicPage()) {
291
+ injectInTopic();
292
+ } else if (isCategoryTopicList()) {
293
+ injectInCategory();
341
294
  }
342
295
  }
343
- }
344
-
345
- function debounceRefresh() {
346
- clearTimeout(debounceTimer);
347
- debounceTimer = setTimeout(refreshAds, 220);
348
- }
349
-
350
- $(document).ready(debounceRefresh);
351
- $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
352
- setTimeout(debounceRefresh, 2200);
353
-
354
- // Fallback: some themes/devices don't emit the expected events for infinite scroll.
355
- // Observe DOM additions and trigger a refresh when new posts/topics are appended/prepended.
356
- (function setupEzoicObserver() {
357
- if (window.__ezoicInfiniteObserver) return;
358
- try {
359
- const obs = new MutationObserver(function (mutations) {
360
- for (const m of mutations) {
361
- if (!m.addedNodes || !m.addedNodes.length) continue;
362
- for (const n of m.addedNodes) {
363
- if (!n || n.nodeType !== 1) continue;
364
- // direct match
365
- if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
366
- debounceRefresh();
367
- return;
368
- }
369
- // descendant match
370
- if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
371
- debounceRefresh();
372
- return;
373
- }
374
- }
375
- }
296
+
297
+ // triggers: hard load + ajaxify + infinite scroll events
298
+ function boot() {
299
+ loadSettings(function () {
300
+ refresh();
301
+ // extra delayed refresh for late ez init
302
+ setTimeout(refresh, 1500);
303
+ setTimeout(refresh, 5000);
304
+ setTimeout(refresh, 10000);
376
305
  });
377
- obs.observe(document.body, { childList: true, subtree: true });
378
- window.__ezoicInfiniteObserver = obs;
379
- } catch (e) {}
306
+ }
307
+
308
+ // Hard load
309
+ $(document).ready(boot);
310
+ // Ajaxify nav end
311
+ $(window).on('action:ajaxify.end', boot);
312
+ // Infinite scroll loads (varies by view)
313
+ $(window).on('action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', function () {
314
+ refresh();
315
+ setTimeout(refresh, 800);
316
+ });
317
+
318
+ // Clean only on real navigation start (not infinite loads)
319
+ $(window).on('action:ajaxify.start', function (ev, data) {
320
+ try {
321
+ const url = data && (data.url || data.href);
322
+ if (!url) return;
323
+ const a = document.createElement('a');
324
+ a.href = url;
325
+ if (a.pathname && a.pathname === window.location.pathname) return;
326
+ } catch (e) {}
327
+ pageKey = null;
328
+ cleanupForNewPage();
329
+ });
330
+
380
331
  })();
package/public/style.css CHANGED
@@ -1,2 +1,4 @@
1
- .ezoic-ad-post{margin:0.75rem 0;}
2
- .ezoic-ad-message-inner{padding:0.75rem 0;}
1
+ .ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
2
+ .ezoic-ad:not(.ezoic-filled){display:none !important;}
3
+ .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
4
+ .ezoic-ad .ezoic-ad-inner > div{margin:0;padding:0;}
@@ -1,59 +1,59 @@
1
1
  <div class="acp-page-container">
2
- <h2>Ezoic Infinite Ads</h2>
2
+ <h1 class="mb-3">Ezoic Infinite</h1>
3
3
 
4
- <form class="ezoic-infinite-settings" role="form">
5
- <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
6
-
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>
4
+ <div class="alert alert-info">
5
+ Placeholders format: <code>&lt;div id="ezoic-pub-ad-placeholder-XXX"&gt;&lt;/div&gt;</code>
6
+ </div>
11
7
 
8
+ <form role="form" class="ezoic-infinite-settings">
12
9
  <div class="mb-3">
13
- <label class="form-label" for="placeholderIds">Pool d’IDs 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>
17
-
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">
10
+ <label class="form-label">Exclude groups (ads disabled for members of these groups)</label>
11
+ <select multiple class="form-select" name="excludedGroups">
12
+ <!-- BEGIN groups -->
13
+ <option value="{groups.name}">{groups.name}</option>
14
+ <!-- END groups -->
15
+ </select>
16
+ <div class="form-text">Hold Ctrl/Cmd to select multiple. Groups are sorted alphabetically.</div>
21
17
  </div>
22
18
 
23
19
  <hr/>
24
20
 
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>
21
+ <h3>Between topics in category (topic list)</h3>
27
22
 
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>
23
+ <div class="form-check form-switch mb-2">
24
+ <input class="form-check-input" type="checkbox" name="enableBetweenAds">
25
+ <label class="form-check-label">Enable between-topic ads</label>
31
26
  </div>
32
27
 
33
28
  <div class="mb-3">
34
- <label class="form-label" for="messagePlaceholderIds">Pool d’IDs 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>
29
+ <label class="form-label">Interval (insert after every N topics)</label>
30
+ <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
37
31
  </div>
38
32
 
39
33
  <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">
34
+ <label class="form-label">Placeholder ID pool (one per line)</label>
35
+ <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
42
36
  </div>
43
37
 
44
38
  <hr/>
45
39
 
46
- <h4 class="mt-3">Exclusions</h4>
40
+ <h3>Inside topics (between posts)</h3>
41
+
42
+ <div class="form-check form-switch mb-2">
43
+ <input class="form-check-input" type="checkbox" name="enableMessageAds">
44
+ <label class="form-check-label">Enable between-post ads</label>
45
+ </div>
46
+
47
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>
48
+ <label class="form-label">Interval (insert after every N posts)</label>
49
+ <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
50
+ </div>
51
+
52
+ <div class="mb-3">
53
+ <label class="form-label">Message placeholder ID pool (one per line)</label>
54
+ <textarea class="form-control" name="messagePlaceholderIds" rows="6"></textarea>
55
55
  </div>
56
56
 
57
- <button id="save" class="btn btn-primary">Enregistrer</button>
57
+ <button type="button" class="btn btn-primary ezoic-infinite-save">Save</button>
58
58
  </form>
59
59
  </div>