nodebb-plugin-ezoic-infinite 0.9.0 → 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.9.0",
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/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,443 +1,331 @@
1
+ /* global $, ajaxify, app */
1
2
  'use strict';
2
- /* globals ajaxify */
3
-
4
- window.ezoicInfiniteLoaded = true;
5
-
6
- let cachedConfig = null;
7
- let lastFetch = 0;
8
- let inFlight = false;
9
- let rerunRequested = false;
10
- let debounceTimer = null;
11
-
12
- let pageKey = null;
13
-
14
- // Separate state per "mode" so category and topic don't leak ids
15
- let usedBetween = new Set();
16
- let usedMessages = new Set();
17
-
18
- let seenBetweenAfter = new Set(); // category: after absolute topic position
19
- let fifoBetween = []; // [{afterPos, id}]
20
- let seenMsgAfter = new Set(); // topic: after absolute post number
21
- let fifoMsg = []; // [{afterPostNo, id}]
22
-
23
- // Destroy spam guard
24
- window.__ezoicLastDestroy = window.__ezoicLastDestroy || {};
25
- window.__ezoicRecycling = false;
26
-
27
- // ---------- Config ----------
28
- async function fetchConfig() {
29
- const now = Date.now();
30
- if (cachedConfig && (now - lastFetch) < 5000) return cachedConfig;
31
- lastFetch = now;
32
-
33
- try {
34
- const res = await fetch('/api/admin/settings/ezoic-infinite', { credentials: 'same-origin' });
35
- const data = await res.json();
36
- cachedConfig = {
37
- excluded: !!data.excluded,
38
- enableBetweenAds: data.enableBetweenAds !== false,
39
- placeholderIds: String(data.placeholderIds || '').trim(),
40
- intervalPosts: parseInt(data.intervalPosts, 10) || 6,
41
-
42
- enableMessageAds: data.enableMessageAds !== false,
43
- messagePlaceholderIds: String(data.messagePlaceholderIds || '').trim(),
44
- messageIntervalPosts: parseInt(data.messageIntervalPosts, 10) || 3,
45
- };
46
- return cachedConfig;
47
- } catch (e) {
48
- return cachedConfig || {
49
- excluded: false,
50
- enableBetweenAds: true,
51
- placeholderIds: '',
52
- intervalPosts: 6,
53
- enableMessageAds: true,
54
- messagePlaceholderIds: '',
55
- messageIntervalPosts: 3,
56
- };
3
+
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;
12
+
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}]
18
+
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);
26
+ }
27
+
28
+ function loadSettings(cb) {
29
+ socket.emit('admin.settings.get', { hash: SETTINGS_NS }, function (err, data) {
30
+ settings = data || {};
31
+ cb && cb();
32
+ });
57
33
  }
58
- }
59
-
60
- function parsePool(text) {
61
- return String(text || '')
62
- .split(/\r?\n|,|;/g)
63
- .map(s => s.trim())
64
- .filter(Boolean)
65
- .map(s => parseInt(s, 10))
66
- .filter(n => Number.isFinite(n) && n > 0);
67
- }
68
-
69
- // ---------- Page detection ----------
70
- function isTopicPage() {
71
- try {
72
- if (ajaxify && ajaxify.data && ajaxify.data.tid) return true;
73
- } catch (e) {}
74
- return /^\/topic\//.test(window.location.pathname);
75
- }
76
-
77
- function isCategoryTopicListPage() {
78
- return document.querySelectorAll('li[component="category/topic"]').length > 0;
79
- }
80
-
81
- function getPageKey() {
82
- try {
83
- if (ajaxify && ajaxify.data) {
84
- if (ajaxify.data.tid) return `topic:${ajaxify.data.tid}`;
85
- if (ajaxify.data.cid) return `category:${ajaxify.data.cid}`;
34
+
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;
86
45
  }
87
- } catch (e) {}
88
- return `path:${window.location.pathname}`;
89
- }
90
-
91
- function resetState() {
92
- usedBetween = new Set();
93
- usedMessages = new Set();
94
- seenBetweenAfter = new Set();
95
- seenMsgAfter = new Set();
96
- fifoBetween = [];
97
- fifoMsg = [];
98
- window.__ezoicLastShowKey = null;
99
- window.__ezoicLastShowAt = 0;
100
- }
101
-
102
- function cleanupOnNav() {
103
- document.querySelectorAll('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad').forEach(el => el.remove());
104
- }
105
-
106
- // ---------- Ezoic helpers ----------
107
- function destroyEzoicId(id) {
108
- const now = Date.now();
109
- if (window.__ezoicLastDestroy[id] && (now - window.__ezoicLastDestroy[id]) < 2000) return;
110
- window.__ezoicLastDestroy[id] = now;
111
-
112
- try {
113
- window.ezstandalone = window.ezstandalone || {};
114
- if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
115
- window.ezstandalone.destroyPlaceholders(id);
116
- return;
46
+ }
47
+
48
+ function getPageKey() {
49
+ try {
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;
53
+ }
54
+ } catch (e) {}
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
+ }
74
+
75
+ function pickNextId(pool, usedSet) {
76
+ for (const id of pool) {
77
+ if (!usedSet.has(id)) return id;
117
78
  }
118
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
119
- window.ezstandalone.cmd.push(function () {
120
- try {
121
- if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
122
- window.ezstandalone.destroyPlaceholders(id);
123
- }
124
- } catch (e) {}
125
- });
126
- } catch (e) {}
127
- }
128
-
129
- // Auto-height: hide until filled
130
- function setupAdAutoHeight() {
131
- if (window.__ezoicAutoHeightAttached) return;
132
- window.__ezoicAutoHeightAttached = true;
133
-
134
- const attach = function () {
135
- document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
136
- if (!wrap.classList.contains('ezoic-filled')) {
137
- wrap.style.display = 'none';
79
+ return null;
80
+ }
81
+
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;
138
89
  }
90
+ window.ezstandalone.cmd.push(function () {
91
+ try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
92
+ });
93
+ } catch (e) {}
94
+ }
95
+
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
+ }
121
+
122
+ // Auto height: show wrapper only when placeholder gets children
123
+ function setupAutoHeight() {
124
+ if (window.__ezoicAutoHeight) return;
125
+ window.__ezoicAutoHeight = true;
126
+
127
+ const mark = function (wrap) {
128
+ if (!wrap) return;
139
129
  const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
140
130
  if (ph && ph.children && ph.children.length) {
141
131
  wrap.classList.add('ezoic-filled');
142
- wrap.style.display = '';
143
132
  }
144
- });
145
- };
146
-
147
- attach();
148
- setTimeout(attach, 500);
149
- setTimeout(attach, 1500);
150
- setTimeout(attach, 3000);
151
- setInterval(attach, 1000);
152
- }
153
-
154
- function callEzoic(ids) {
155
- window.ezstandalone = window.ezstandalone || {};
156
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
157
-
158
- const uniq = Array.from(new Set(ids || []));
159
- if (!uniq.length) return;
160
-
161
- // De-dupe rapid duplicates
162
- const key = uniq.join(',');
163
- const now = Date.now();
164
- if (window.__ezoicLastShowKey === key && (now - (window.__ezoicLastShowAt || 0)) < 1200) return;
165
- window.__ezoicLastShowKey = key;
166
- window.__ezoicLastShowAt = now;
167
-
168
- const run = function () {
133
+ };
134
+
135
+ const scan = function () {
136
+ document.querySelectorAll('.ezoic-ad').forEach(mark);
137
+ };
138
+
139
+ scan();
140
+ setInterval(scan, 1000);
141
+
169
142
  try {
170
- if (typeof window.ezstandalone.showAds === 'function') {
171
- window.ezstandalone.showAds.apply(window.ezstandalone, uniq);
172
- return true;
173
- }
143
+ const mo = new MutationObserver(scan);
144
+ mo.observe(document.body, { childList: true, subtree: true });
174
145
  } catch (e) {}
175
- return false;
176
- };
146
+ }
177
147
 
178
- setupAdAutoHeight();
179
- window.ezstandalone.cmd.push(function () { run(); });
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>'
153
+ );
154
+ $target.after(wrap);
155
+ return wrap;
156
+ }
180
157
 
181
- // Retry only if showAds isn't available yet
182
- try {
183
- if (typeof window.ezstandalone.showAds !== 'function') {
184
- let tries = 0;
185
- const retry = function () {
186
- tries++;
187
- if (run()) return;
188
- if (tries < 6) setTimeout(retry, 800);
189
- };
190
- setTimeout(retry, 800);
191
- }
192
- } catch (e) {}
193
- }
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;
194
165
 
195
- // ---------- Pool logic ----------
196
- function pickNextId(pool, usedSet) {
197
- for (const id of pool) {
198
- if (!usedSet.has(id)) return id;
199
- }
200
- return null;
201
- }
202
-
203
- function recycleOldest(fifo, usedSet, selector, avoidAfterNode) {
204
- if (window.__ezoicRecycling) return null;
205
- window.__ezoicRecycling = true;
206
- try {
207
- fifo.sort((a, b) => (a.after - b.after));
208
- while (fifo.length) {
209
- const old = fifo.shift();
210
- const el = document.querySelector(selector(old));
211
- if (!el) continue;
212
-
213
- // Don't recycle if it is right after the last real item (protect sentinel)
166
+ // don't recycle if this is right before the sentinel (after last post)
214
167
  try {
215
- if (avoidAfterNode && el.previousElementSibling === avoidAfterNode) {
216
- fifo.push(old);
168
+ const $last = $posts.last();
169
+ if ($last.length && $el.prev().is($last)) {
170
+ fifoTopic.push(old);
217
171
  return null;
218
172
  }
219
173
  } catch (e) {}
220
174
 
221
- el.remove();
222
- usedSet.delete(old.id);
223
- destroyEzoicId(old.id);
175
+ $el.remove();
176
+ usedTopic.delete(old.id);
177
+ destroyPlaceholder(old.id);
224
178
  return old.id;
225
179
  }
226
180
  return null;
227
- } finally {
228
- window.__ezoicRecycling = false;
229
181
  }
230
- }
231
-
232
- // ---------- Injection ----------
233
- function injectBetweenAds(config) {
234
- if (!config.enableBetweenAds) return;
235
- const pool = parsePool(config.placeholderIds);
236
- if (!pool.length) return;
237
-
238
- const interval = Math.max(1, config.intervalPosts);
239
- const items = Array.from(document.querySelectorAll('li[component="category/topic"]'));
240
- if (!items.length) return;
241
-
242
- const newIds = [];
243
- const lastItem = items[items.length - 1];
244
-
245
- for (let i = 0; i < items.length; i++) {
246
- const pos = i + 1; // absolute position in loaded list
247
- if (pos % interval !== 0) continue;
248
- if (seenBetweenAfter.has(pos)) continue;
249
-
250
- let id = pickNextId(pool, usedBetween);
251
- if (!id) {
252
- // recycle oldest
253
- id = recycleOldest(
254
- fifoBetween,
255
- usedBetween,
256
- (o) => `.ezoic-ad-topic[data-ezoic-after="${o.after}"][data-ezoic-id="${o.id}"]`,
257
- lastItem
258
- );
259
- if (!id) break;
260
- }
261
182
 
262
- const anchor = items[i];
263
- const wrap = document.createElement('li');
264
- wrap.className = 'ezoic-ad ezoic-ad-topic';
265
- wrap.setAttribute('data-ezoic-after', String(pos));
266
- wrap.setAttribute('data-ezoic-id', String(id));
267
- wrap.innerHTML = `<div id="ezoic-pub-ad-placeholder-${id}"></div>`;
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;
268
190
 
269
- anchor.insertAdjacentElement('afterend', wrap);
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) {}
270
198
 
271
- seenBetweenAfter.add(pos);
272
- usedBetween.add(id);
273
- fifoBetween.push({ after: pos, id });
274
- newIds.push(id);
199
+ $el.remove();
200
+ usedCat.delete(old.id);
201
+ destroyPlaceholder(old.id);
202
+ return old.id;
203
+ }
204
+ return null;
275
205
  }
276
206
 
277
- if (newIds.length) callEzoic(newIds);
278
- }
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;
279
212
 
280
- function getPostNumberFromPost(postEl) {
281
- // Prefer post-index text (#72)
282
- try {
283
- const idx = postEl.querySelector('a.post-index');
284
- if (idx) {
285
- const n = parseInt(String(idx.textContent || '').replace('#', '').trim(), 10);
286
- if (Number.isFinite(n)) return n;
287
- }
288
- } catch (e) {}
289
-
290
- // Fallback: pid-based ordering not reliable, but DOM order is ok for interval within loaded window.
291
- return null;
292
- }
293
-
294
- function injectMessageAds(config) {
295
- if (!config.enableMessageAds) return;
296
- const pool = parsePool(config.messagePlaceholderIds);
297
- if (!pool.length) return;
298
-
299
- const interval = Math.max(1, config.messageIntervalPosts);
300
- const posts = Array.from(document.querySelectorAll('[component="post"][data-pid]'));
301
- if (!posts.length) return;
302
-
303
- const newIds = [];
304
- const lastPost = posts[posts.length - 1];
305
-
306
- // Determine absolute post numbers if available; else use DOM position
307
- let numbers = posts.map((p, i) => ({ el: p, no: getPostNumberFromPost(p) || (i + 1) }));
308
- // Ensure strictly increasing by DOM
309
- numbers = numbers.map((x, i) => ({ el: x.el, no: x.no || (i + 1) }));
310
-
311
- for (const entry of numbers) {
312
- const afterNo = entry.no;
313
- if (afterNo % interval !== 0) continue;
314
- if (seenMsgAfter.has(afterNo)) continue;
315
-
316
- let id = pickNextId(pool, usedMessages);
317
- if (!id) {
318
- id = recycleOldest(
319
- fifoMsg,
320
- usedMessages,
321
- (o) => `.ezoic-ad-post[data-ezoic-after="${o.after}"][data-ezoic-id="${o.id}"]`,
322
- lastPost
323
- );
324
- if (!id) break;
325
- }
213
+ const $posts = $('[component="post"][data-pid]');
214
+ if (!$posts.length) return;
215
+
216
+ const newIds = [];
217
+ $posts.each(function (idx) {
218
+ const postNo = idx + 1; // 1-based within loaded set
219
+ if (postNo % interval !== 0) return;
220
+
221
+ // Do not insert after the last post in DOM (to keep infinite scroll sentinel stable)
222
+ if (idx === $posts.length - 1) return;
326
223
 
327
- const wrap = document.createElement('div');
328
- wrap.className = 'ezoic-ad ezoic-ad-post';
329
- wrap.setAttribute('data-ezoic-after', String(afterNo));
330
- wrap.setAttribute('data-ezoic-id', String(id));
331
- wrap.innerHTML = `<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-${id}"></div></div>`;
224
+ const $post = $(this);
225
+ const existing = $post.next('.ezoic-ad-topic');
226
+ if (existing.length) return;
332
227
 
333
- entry.el.insertAdjacentElement('afterend', wrap);
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
+ });
334
239
 
335
- seenMsgAfter.add(afterNo);
336
- usedMessages.add(id);
337
- fifoMsg.push({ after: afterNo, id });
338
- newIds.push(id);
240
+ if (newIds.length) callEzoic(newIds);
339
241
  }
340
242
 
341
- if (newIds.length) callEzoic(newIds);
342
- }
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;
251
+
252
+ const newIds = [];
253
+ $items.each(function (idx) {
254
+ const pos = idx + 1;
255
+ if (pos % interval !== 0) return;
256
+
257
+ if (idx === $items.length - 1) return;
343
258
 
344
- // ---------- Main refresh ----------
345
- async function refreshAds() {
346
- if (inFlight) {
347
- rerunRequested = true;
348
- return;
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;
267
+ }
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);
349
276
  }
350
- inFlight = true;
351
- rerunRequested = false;
352
277
 
353
- try {
354
- const cfg = await fetchConfig();
355
- if (cfg.excluded) return;
278
+ function refresh() {
279
+ if (!settings) return;
280
+ if (userExcluded()) return;
356
281
 
357
282
  const key = getPageKey();
358
- if (key !== pageKey) {
283
+ if (pageKey !== key) {
359
284
  pageKey = key;
360
- resetState();
361
- cleanupOnNav();
285
+ cleanupForNewPage();
362
286
  }
363
287
 
364
- const onTopic = isTopicPage();
365
- const onCategory = isCategoryTopicListPage() && !onTopic;
288
+ setupAutoHeight();
289
+
290
+ if (isTopicPage()) {
291
+ injectInTopic();
292
+ } else if (isCategoryTopicList()) {
293
+ injectInCategory();
294
+ }
295
+ }
366
296
 
367
- if (onCategory) injectBetweenAds(cfg);
368
- if (onTopic) injectMessageAds(cfg);
369
- } finally {
370
- inFlight = false;
371
- if (rerunRequested) setTimeout(refreshAds, 50);
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);
305
+ });
372
306
  }
373
- }
374
307
 
375
- function debounceRefresh() {
376
- if (debounceTimer) clearTimeout(debounceTimer);
377
- debounceTimer = setTimeout(refreshAds, 250);
378
- }
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
+ });
379
317
 
380
- // ---------- Observers / Events ----------
381
- (function setupTriggers() {
382
- // Ajaxify navigation: only cleanup when URL changes
318
+ // Clean only on real navigation start (not infinite loads)
383
319
  $(window).on('action:ajaxify.start', function (ev, data) {
384
320
  try {
385
- const targetUrl = (data && (data.url || data.href)) ? String(data.url || data.href) : '';
386
- if (targetUrl) {
387
- const a = document.createElement('a');
388
- a.href = targetUrl;
389
- const targetPath = a.pathname || targetUrl;
390
- if (targetPath === window.location.pathname) return;
391
- }
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;
392
326
  } catch (e) {}
393
327
  pageKey = null;
394
- resetState();
395
- cleanupOnNav();
328
+ cleanupForNewPage();
396
329
  });
397
330
 
398
- $(window).on('action:ajaxify.end action:posts.loaded action:topics.loaded action:topic.loaded action:category.loaded', debounceRefresh);
399
-
400
- // MutationObserver (new posts/topics appended)
401
- try {
402
- const obs = new MutationObserver(function (mutations) {
403
- for (const m of mutations) {
404
- if (!m.addedNodes) continue;
405
- for (const n of m.addedNodes) {
406
- if (!n || n.nodeType !== 1) continue;
407
- if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
408
- debounceRefresh();
409
- return;
410
- }
411
- if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
412
- debounceRefresh();
413
- return;
414
- }
415
- }
416
- }
417
- });
418
- obs.observe(document.body, { childList: true, subtree: true });
419
- window.__ezoicInfiniteObserver = obs;
420
- } catch (e) {}
421
-
422
- // Poller fallback (count changes)
423
- let lastPosts = 0;
424
- let lastTopics = 0;
425
- setInterval(function () {
426
- const p = document.querySelectorAll('[component="post"][data-pid]').length;
427
- const t = document.querySelectorAll('li[component="category/topic"]').length;
428
- if (p !== lastPosts || t !== lastTopics) {
429
- lastPosts = p; lastTopics = t;
430
- debounceRefresh();
431
- }
432
- }, 1500);
433
-
434
- // First run(s) - important for hard load
435
- document.addEventListener('DOMContentLoaded', function () {
436
- debounceRefresh();
437
- setTimeout(debounceRefresh, 1500);
438
- setTimeout(debounceRefresh, 5000);
439
- setTimeout(debounceRefresh, 10000);
440
- });
441
- // In case this script loads after DOMContentLoaded
442
- setTimeout(debounceRefresh, 800);
443
331
  })();
package/public/style.css CHANGED
@@ -1,4 +1,4 @@
1
1
  .ezoic-ad{min-height:0 !important;height:auto !important;padding:0 !important;margin:0.5rem 0;}
2
2
  .ezoic-ad:not(.ezoic-filled){display:none !important;}
3
- .ezoic-ad-message-inner{padding:0;margin:0;}
4
- .ezoic-ad-message-inner > div{margin:0;padding:0;}
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>