nodebb-plugin-ezoic-infinite 0.9.1 → 0.9.3

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,36 +2,96 @@
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 (params) {
9
- const { router, middleware } = params;
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
+ }
10
15
 
11
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
12
- router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
13
- };
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
+ }
14
22
 
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
- });
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),
43
+
44
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
45
+ };
46
+ }
47
+
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));
25
52
  }
26
53
 
27
- Plugin.addAdminNavigation = async function (header) {
54
+ plugin.addAdminNavigation = async (header) => {
28
55
  header.plugins = header.plugins || [];
29
56
  header.plugins.push({
30
57
  route: '/plugins/ezoic-infinite',
31
- icon: 'fa-bullhorn',
32
- name: 'Ezoic Infinite',
58
+ icon: 'fa-ad',
59
+ name: 'Ezoic Infinite Ads'
33
60
  });
34
61
  return header;
35
62
  };
36
63
 
37
- module.exports = Plugin;
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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.9.1",
4
- "description": "Ezoic ads injection for NodeBB infinite scroll (topics list + topic posts) using a pool of placeholder IDs.",
3
+ "version": "0.9.3",
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,7 +11,7 @@
11
11
  "ads",
12
12
  "infinite-scroll"
13
13
  ],
14
- "nbbpm": {
15
- "compatibility": "^4.0.0"
14
+ "engines": {
15
+ "node": ">=18"
16
16
  }
17
17
  }
package/plugin.json CHANGED
@@ -22,8 +22,5 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "css": [
26
- "public/style.css"
27
- ],
28
25
  "templates": "public/templates"
29
26
  }
package/public/admin.js CHANGED
@@ -1,51 +1,29 @@
1
- /* global $, app, socket */
1
+ /* globals ajaxify */
2
2
  'use strict';
3
3
 
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()));
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
+ });
24
23
  });
25
24
  });
26
25
  }
27
26
 
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
- });
27
+ $(document).ready(init);
28
+ $(window).on('action:ajaxify.end', init);
29
+ })();
package/public/client.js CHANGED
@@ -1,331 +1,252 @@
1
- /* global $, ajaxify, app */
2
1
  'use strict';
3
2
 
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
- });
33
- }
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;
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
+ // Incremental state (prevents ads "jumping to the top")
14
+ let pageKey = null;
15
+ let injectedSlots = new Set(); // slotNumber per page
16
+ let usedIds = new Set(); // ids currently injected per page
17
+
18
+ function resetPageState() {
19
+ injectedSlots = new Set();
20
+ usedIds = new Set();
21
+ }
22
+
23
+ function currentPageKey() {
24
+ // Stable key per ajaxified page
25
+ try {
26
+ if (ajaxify && ajaxify.data) {
27
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
28
+ if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
29
+ if (ajaxify.data.template) return 'tpl:' + ajaxify.data.template + ':' + (ajaxify.data.url || window.location.pathname);
45
30
  }
46
- }
47
-
48
- function getPageKey() {
31
+ } catch (e) {}
32
+ return window.location.pathname;
33
+ }
34
+
35
+ function parsePool(raw) {
36
+ if (!raw) return [];
37
+ return Array.from(new Set(
38
+ String(raw).split(/[\n,;\s]+/)
39
+ .map(x => parseInt(x, 10))
40
+ .filter(n => Number.isFinite(n) && n > 0)
41
+ ));
42
+ }
43
+
44
+ async function fetchConfig() {
45
+ if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
46
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
47
+ cachedConfig = await res.json();
48
+ lastFetch = Date.now();
49
+ return cachedConfig;
50
+ }
51
+
52
+ function isTopicPage() {
53
+ return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
54
+ }
55
+
56
+ function isCategoryTopicListPage() {
57
+ return $('li[component="category/topic"]').length > 0;
58
+ }
59
+
60
+ function getTopicPosts() {
61
+ const $primary = $('[component="post"][data-pid]');
62
+ if ($primary.length) return $primary.not('.ezoic-ad-post');
63
+
64
+ return $('[data-pid]').filter(function () {
65
+ const $el = $(this);
66
+ const hasContent = $el.find('[component="post/content"]').length > 0;
67
+ const nested = $el.parents('[data-pid]').length > 0;
68
+ return hasContent && !nested;
69
+ }).not('.ezoic-ad-post');
70
+ }
71
+
72
+ function getCategoryTopicItems() {
73
+ return $('li[component="category/topic"]').not('.ezoic-ad-topic');
74
+ }
75
+
76
+ function tagName($el) {
77
+ return ($el && $el.length ? (($el.prop('tagName') || '').toUpperCase()) : '');
78
+ }
79
+
80
+ function makeWrapperLike($target, classes, innerHtml, attrs) {
81
+ const t = tagName($target);
82
+ const attrStr = attrs ? ' ' + attrs : '';
83
+ if (t === 'LI') {
84
+ return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
85
+ }
86
+ return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
87
+ }
88
+
89
+ function pickNextId(pool) {
90
+ for (const id of pool) {
91
+ if (!usedIds.has(id)) return id;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function callEzoic(ids) {
97
+ if (!ids || !ids.length) return;
98
+
99
+ window.ezstandalone = window.ezstandalone || {};
100
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
101
+
102
+ const run = function () {
49
103
  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;
104
+ if (typeof window.ezstandalone.showAds === 'function') {
105
+ window.ezstandalone.showAds.apply(window.ezstandalone, ids);
106
+ return true;
53
107
  }
54
108
  } catch (e) {}
55
- return window.location.pathname;
56
- }
109
+ return false;
110
+ };
111
+
112
+ window.ezstandalone.cmd.push(function () { run(); });
113
+
114
+ // retry a few times (Ezoic can load late)
115
+ let tries = 0;
116
+ const maxTries = 6;
117
+ const timer = setInterval(function () {
118
+ tries++;
119
+ if (run() || tries >= maxTries) clearInterval(timer);
120
+ }, 800);
121
+ }
122
+
123
+ function injectBetweenIncremental($items, pool, interval, wrapperClass) {
124
+ const total = $items.length;
125
+ const maxSlot = Math.floor(total / interval);
126
+ if (maxSlot <= 0) return [];
127
+
128
+ const newIds = [];
129
+
130
+ for (let slot = 1; slot <= maxSlot; slot++) {
131
+ if (injectedSlots.has(slot)) continue;
132
+
133
+ const index = slot * interval - 1;
134
+ const $target = $items.eq(index);
135
+ if (!$target.length) continue;
136
+
137
+ const id = pickNextId(pool);
138
+ if (!id) {
139
+ // pool exhausted: stop injecting further to avoid reusing ids and "jumping"
140
+ break;
141
+ }
57
142
 
58
- function isTopicPage() {
59
- try { if (ajaxify && ajaxify.data && ajaxify.data.tid) return true; } catch (e) {}
60
- return /^\/topic\//.test(window.location.pathname);
61
- }
143
+ const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
144
+ const html = makeWrapperLike($target, wrapperClass, placeholder, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
62
145
 
63
- function isCategoryTopicList() {
64
- return $('li[component="category/topic"]').length > 0 && !isTopicPage();
65
- }
146
+ $target.after(html);
66
147
 
67
- function cleanupForNewPage() {
68
- $('.ezoic-ad').remove();
69
- usedTopic = new Set();
70
- usedCat = new Set();
71
- fifoTopic = [];
72
- fifoCat = [];
148
+ injectedSlots.add(slot);
149
+ usedIds.add(id);
150
+ newIds.push(id);
73
151
  }
74
152
 
75
- function pickNextId(pool, usedSet) {
76
- for (const id of pool) {
77
- if (!usedSet.has(id)) return id;
78
- }
79
- return null;
80
- }
153
+ return newIds;
154
+ }
81
155
 
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
- }
156
+ function injectMessageIncremental($posts, pool, interval) {
157
+ const total = $posts.length;
158
+ const maxSlot = Math.floor(total / interval);
159
+ if (maxSlot <= 0) return [];
95
160
 
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
- }
161
+ const newIds = [];
121
162
 
122
- // Auto height: show wrapper only when placeholder gets children
123
- function setupAutoHeight() {
124
- if (window.__ezoicAutoHeight) return;
125
- window.__ezoicAutoHeight = true;
163
+ for (let slot = 1; slot <= maxSlot; slot++) {
164
+ if (injectedSlots.has(slot)) continue;
126
165
 
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
- };
166
+ const index = slot * interval - 1;
167
+ const $target = $posts.eq(index);
168
+ if (!$target.length) continue;
134
169
 
135
- const scan = function () {
136
- document.querySelectorAll('.ezoic-ad').forEach(mark);
137
- };
170
+ const id = pickNextId(pool);
171
+ if (!id) break;
138
172
 
139
- scan();
140
- setInterval(scan, 1000);
173
+ const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
174
+ const html = makeWrapperLike($target, 'post ezoic-ad-post', inner, 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"');
141
175
 
142
- try {
143
- const mo = new MutationObserver(scan);
144
- mo.observe(document.body, { childList: true, subtree: true });
145
- } catch (e) {}
146
- }
176
+ $target.after(html);
147
177
 
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;
178
+ injectedSlots.add(slot);
179
+ usedIds.add(id);
180
+ newIds.push(id);
156
181
  }
157
182
 
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) {}
174
-
175
- $el.remove();
176
- usedTopic.delete(old.id);
177
- destroyPlaceholder(old.id);
178
- return old.id;
179
- }
180
- return null;
181
- }
183
+ return newIds;
184
+ }
182
185
 
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) {}
198
-
199
- $el.remove();
200
- usedCat.delete(old.id);
201
- destroyPlaceholder(old.id);
202
- return old.id;
203
- }
204
- return null;
186
+ async function refreshAds() {
187
+ // reset state when navigating (ajaxify)
188
+ const key = currentPageKey();
189
+ if (pageKey !== key) {
190
+ pageKey = key;
191
+ resetPageState();
192
+ // also cleanup any injected wrappers that may have been left by browser bfcache
193
+ $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
205
194
  }
206
195
 
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;
196
+ if (inFlight) { rerunRequested = true; return; }
197
+ inFlight = true;
212
198
 
213
- const $posts = $('[component="post"][data-pid]');
214
- if (!$posts.length) return;
199
+ try {
200
+ const cfg = await fetchConfig();
201
+ if (!cfg || cfg.excluded) return;
215
202
 
216
- const newIds = [];
217
- $posts.each(function (idx) {
218
- const postNo = idx + 1; // 1-based within loaded set
219
- if (postNo % interval !== 0) return;
203
+ const betweenPool = parsePool(cfg.placeholderIds);
204
+ const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
220
205
 
221
- // Do not insert after the last post in DOM (to keep infinite scroll sentinel stable)
222
- if (idx === $posts.length - 1) return;
206
+ const messagePool = parsePool(cfg.messagePlaceholderIds);
207
+ const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
223
208
 
224
- const $post = $(this);
225
- const existing = $post.next('.ezoic-ad-topic');
226
- if (existing.length) return;
209
+ const onTopic = isTopicPage();
210
+ const onCategory = !onTopic && isCategoryTopicListPage();
227
211
 
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 });
212
+ const $posts = onTopic ? getTopicPosts() : $();
213
+ const $topicItems = onCategory ? getCategoryTopicItems() : $();
235
214
 
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;
215
+ if (!$posts.length && !$topicItems.length) return;
251
216
 
252
217
  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;
258
-
259
- const $li = $(this);
260
- const existing = $li.next('.ezoic-ad-between');
261
- if (existing.length) return;
262
218
 
263
- let id = pickNextId(pool, usedCat);
264
- if (!id) {
265
- id = recycleCat($items);
266
- if (!id) return;
219
+ // Your rule:
220
+ // - Category topic list: BETWEEN only
221
+ // - Topic page: MESSAGE only
222
+ if ($topicItems.length) {
223
+ if (cfg.enableBetweenAds && betweenPool.length) {
224
+ newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
267
225
  }
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();
226
+ callEzoic(newIds);
227
+ return;
286
228
  }
287
229
 
288
- setupAutoHeight();
289
-
290
- if (isTopicPage()) {
291
- injectInTopic();
292
- } else if (isCategoryTopicList()) {
293
- injectInCategory();
230
+ if ($posts.length) {
231
+ if (cfg.enableMessageAds && messagePool.length) {
232
+ newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
233
+ }
234
+ callEzoic(newIds);
235
+ }
236
+ } finally {
237
+ inFlight = false;
238
+ if (rerunRequested) {
239
+ rerunRequested = false;
240
+ setTimeout(refreshAds, 120);
294
241
  }
295
242
  }
243
+ }
296
244
 
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
- });
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
- });
245
+ function debounceRefresh() {
246
+ clearTimeout(debounceTimer);
247
+ debounceTimer = setTimeout(refreshAds, 180);
248
+ }
330
249
 
331
- })();
250
+ $(document).ready(debounceRefresh);
251
+ $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
252
+ setTimeout(debounceRefresh, 1800);
@@ -1,59 +1,59 @@
1
1
  <div class="acp-page-container">
2
- <h1 class="mb-3">Ezoic Infinite</h1>
2
+ <h2>Ezoic - Publicités Infinite Scroll Ads</h2>
3
3
 
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>
4
+ <form class="ezoic-infinite-settings" role="form">
5
+ <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
7
6
 
8
- <form role="form" class="ezoic-infinite-settings">
9
- <div class="mb-3">
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>
17
- </div>
18
-
19
- <hr/>
20
-
21
- <h3>Between topics in category (topic list)</h3>
22
-
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>
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>
26
10
  </div>
27
11
 
28
12
  <div class="mb-3">
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">
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>
31
16
  </div>
32
17
 
33
18
  <div class="mb-3">
34
- <label class="form-label">Placeholder ID pool (one per line)</label>
35
- <textarea class="form-control" name="placeholderIds" rows="6"></textarea>
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">
36
21
  </div>
37
22
 
38
23
  <hr/>
39
24
 
40
- <h3>Inside topics (between posts)</h3>
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>
41
27
 
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>
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>
45
31
  </div>
46
32
 
47
33
  <div class="mb-3">
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">
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>
50
37
  </div>
51
38
 
52
39
  <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>
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">
42
+ </div>
43
+
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>
55
55
  </div>
56
56
 
57
- <button type="button" class="btn btn-primary ezoic-infinite-save">Save</button>
57
+ <button id="save" class="btn btn-primary">Enregistrer</button>
58
58
  </form>
59
59
  </div>
package/public/style.css DELETED
@@ -1,4 +0,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;}