nodebb-plugin-ezoic-infinite 1.3.0 → 1.4.0

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
@@ -34,13 +34,21 @@ async function getAllGroups() {
34
34
  async function getSettings() {
35
35
  const s = await meta.settings.get(SETTINGS_KEY);
36
36
  return {
37
- // Between-post ads (simple blocks)
37
+ // Between-post ads (simple blocks) in category topic list
38
38
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
39
40
  placeholderIds: (s.placeholderIds || '').trim(),
40
41
  intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
41
42
 
43
+ // Home/categories list ads (between categories on / or /categories)
44
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
45
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
46
+ categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
47
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
48
+
42
49
  // "Ad message" between replies (looks like a post)
43
50
  enableMessageAds: parseBool(s.enableMessageAds, false),
51
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
44
52
  messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
45
53
  messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
46
54
 
@@ -88,9 +96,15 @@ plugin.init = async ({ router, middleware }) => {
88
96
  res.json({
89
97
  excluded,
90
98
  enableBetweenAds: settings.enableBetweenAds,
99
+ showFirstTopicAd: settings.showFirstTopicAd,
91
100
  placeholderIds: settings.placeholderIds,
92
101
  intervalPosts: settings.intervalPosts,
102
+ enableCategoryAds: settings.enableCategoryAds,
103
+ showFirstCategoryAd: settings.showFirstCategoryAd,
104
+ categoryPlaceholderIds: settings.categoryPlaceholderIds,
105
+ intervalCategories: settings.intervalCategories,
93
106
  enableMessageAds: settings.enableMessageAds,
107
+ showFirstMessageAd: settings.showFirstMessageAd,
94
108
  messagePlaceholderIds: settings.messagePlaceholderIds,
95
109
  messageIntervalPosts: settings.messageIntervalPosts,
96
110
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  (function () {
3
2
  'use strict';
4
3
 
@@ -7,6 +6,7 @@
7
6
  const SELECTORS = {
8
7
  topicItem: 'li[component="category/topic"]',
9
8
  postItem: '[component="post"][data-pid]',
9
+ categoryItem: 'li[component="categories/category"]',
10
10
  };
11
11
 
12
12
  const WRAP_CLASS = 'ezoic-ad';
@@ -21,9 +21,11 @@
21
21
 
22
22
  poolTopics: [],
23
23
  poolPosts: [],
24
+ poolCategories: [],
24
25
 
25
26
  usedTopics: new Set(),
26
27
  usedPosts: new Set(),
28
+ usedCategories: new Set(),
27
29
 
28
30
  lastShowById: new Map(),
29
31
  pendingById: new Set(),
@@ -72,15 +74,23 @@
72
74
  function getKind() {
73
75
  const p = window.location.pathname || '';
74
76
  if (/^\/topic\//.test(p)) return 'topic';
75
- if (/^\/category\//.test(p)) return 'category';
76
- if (document.querySelector('[component="topic"]') || document.querySelector('[component="post"][data-pid]')) return 'topic';
77
- return 'category';
77
+ if (/^\/category\//.test(p)) return 'categoryTopics';
78
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
79
+ // fallback by DOM
80
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
81
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
82
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
83
+ return 'other';
78
84
  }
79
85
 
80
86
  function getTopicItems() {
81
87
  return Array.from(document.querySelectorAll(SELECTORS.topicItem));
82
88
  }
83
89
 
90
+ function getCategoryItems() {
91
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
92
+ }
93
+
84
94
  function getPostContainers() {
85
95
  const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
86
96
  return nodes.filter((el) => {
@@ -93,9 +103,9 @@
93
103
  });
94
104
  }
95
105
 
96
- function buildWrap(id, kind, afterPos) {
106
+ function buildWrap(id, kindClass, afterPos) {
97
107
  const wrap = document.createElement('div');
98
- wrap.className = `${WRAP_CLASS} ${kind}`;
108
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
99
109
  wrap.setAttribute('data-ezoic-after', String(afterPos));
100
110
  wrap.style.width = '100%';
101
111
 
@@ -122,6 +132,7 @@
122
132
  try {
123
133
  state.usedTopics.forEach((id) => ids.push(id));
124
134
  state.usedPosts.forEach((id) => ids.push(id));
135
+ state.usedCategories.forEach((id) => ids.push(id));
125
136
  } catch (e) {}
126
137
 
127
138
  if (!ids.length) return;
@@ -229,7 +240,7 @@
229
240
  }
230
241
 
231
242
  function nextId(kind) {
232
- const pool = (kind === 'between') ? state.poolTopics : state.poolPosts;
243
+ const pool = (kind === 'between') ? state.poolTopics : (kind === 'message') ? state.poolPosts : state.poolCategories;
233
244
  if (pool.length) return pool.shift();
234
245
  return null;
235
246
  }
@@ -257,6 +268,7 @@
257
268
  function initPools(cfg) {
258
269
  if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
259
270
  if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
271
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
260
272
  }
261
273
 
262
274
  function computeTargets(count, interval, showFirst) {
@@ -266,75 +278,31 @@
266
278
  for (let i = 1; i <= count; i++) {
267
279
  if (i % interval === 0) out.push(i);
268
280
  }
269
- // uniq + sort
270
281
  return Array.from(new Set(out)).sort((a, b) => a - b);
271
282
  }
272
283
 
273
- function injectTopics(cfg) {
274
- if (!normalizeBool(cfg.enableBetweenAds)) return 0;
275
-
276
- const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
277
- const showFirst = normalizeBool(cfg.showFirstTopicAd);
278
-
279
- const items = getTopicItems();
284
+ function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
280
285
  if (!items.length) return 0;
281
-
282
286
  const targets = computeTargets(items.length, interval, showFirst);
283
287
 
284
288
  let inserted = 0;
285
289
  for (const afterPos of targets) {
286
290
  if (inserted >= MAX_INSERTS_PER_RUN) break;
287
291
 
288
- const li = items[afterPos - 1];
289
- if (!li || !li.isConnected) continue;
292
+ const el = items[afterPos - 1];
293
+ if (!el || !el.isConnected) continue;
290
294
 
291
- // Avoid back-to-back ads at load: if afterPos-1 already has an ad wrapper, skip
292
- const prevWrap = findWrap('ezoic-ad-between', afterPos - 1);
295
+ // Prevent back-to-back at load
296
+ const prevWrap = findWrap(kindClass, afterPos - 1);
293
297
  if (prevWrap) continue;
294
298
 
295
- if (findWrap('ezoic-ad-between', afterPos)) continue;
299
+ if (findWrap(kindClass, afterPos)) continue;
296
300
 
297
- const id = nextId('between');
301
+ const id = nextId(kindPool);
298
302
  if (!id) break;
299
303
 
300
- state.usedTopics.add(id);
301
- const wrap = insertAfter(li, id, 'ezoic-ad-between', afterPos);
302
- if (!wrap) continue;
303
-
304
- callShowAdsWhenReady(id);
305
- inserted += 1;
306
- }
307
- return inserted;
308
- }
309
-
310
- function injectPosts(cfg) {
311
- if (!normalizeBool(cfg.enableMessageAds)) return 0;
312
-
313
- const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
314
- const showFirst = normalizeBool(cfg.showFirstMessageAd);
315
-
316
- const posts = getPostContainers();
317
- if (!posts.length) return 0;
318
-
319
- const targets = computeTargets(posts.length, interval, showFirst);
320
-
321
- let inserted = 0;
322
- for (const afterPos of targets) {
323
- if (inserted >= MAX_INSERTS_PER_RUN) break;
324
-
325
- const post = posts[afterPos - 1];
326
- if (!post || !post.isConnected) continue;
327
-
328
- const prevWrap = findWrap('ezoic-ad-message', afterPos - 1);
329
- if (prevWrap) continue;
330
-
331
- if (findWrap('ezoic-ad-message', afterPos)) continue;
332
-
333
- const id = nextId('message');
334
- if (!id) break;
335
-
336
- state.usedPosts.add(id);
337
- const wrap = insertAfter(post, id, 'ezoic-ad-message', afterPos);
304
+ usedSet.add(id);
305
+ const wrap = insertAfter(el, id, kindClass, afterPos);
338
306
  if (!wrap) continue;
339
307
 
340
308
  callShowAdsWhenReady(id);
@@ -344,7 +312,6 @@
344
312
  }
345
313
 
346
314
  function enforceNoAdjacentAds() {
347
- // If DOM changes produce adjacent wrappers (rare but possible), hide the later one.
348
315
  const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
349
316
  for (let i = 0; i < ads.length; i++) {
350
317
  const ad = ads[i];
@@ -363,8 +330,10 @@
363
330
 
364
331
  state.poolTopics = [];
365
332
  state.poolPosts = [];
333
+ state.poolCategories = [];
366
334
  state.usedTopics.clear();
367
335
  state.usedPosts.clear();
336
+ state.usedCategories.clear();
368
337
 
369
338
  state.lastShowById = new Map();
370
339
  state.pendingById = new Set();
@@ -381,7 +350,7 @@
381
350
 
382
351
  function ensureObserver() {
383
352
  if (state.obs) return;
384
- state.obs = new MutationObserver(() => scheduleRun('dom-mutation'));
353
+ state.obs = new MutationObserver(() => scheduleRun('mutation'));
385
354
  try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
386
355
  setTimeout(() => { if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; } }, 15000);
387
356
  }
@@ -397,22 +366,47 @@
397
366
  const kind = getKind();
398
367
  let inserted = 0;
399
368
 
400
- if (kind === 'topic') inserted = injectPosts(cfg);
401
- else inserted = injectTopics(cfg);
369
+ if (kind === 'topic') {
370
+ if (normalizeBool(cfg.enableMessageAds)) {
371
+ inserted = injectBetween('ezoic-ad-message', getPostContainers(),
372
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
373
+ normalizeBool(cfg.showFirstMessageAd),
374
+ 'message',
375
+ state.usedPosts);
376
+ }
377
+ } else if (kind === 'categoryTopics') {
378
+ if (normalizeBool(cfg.enableBetweenAds)) {
379
+ inserted = injectBetween('ezoic-ad-between', getTopicItems(),
380
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
381
+ normalizeBool(cfg.showFirstTopicAd),
382
+ 'between',
383
+ state.usedTopics);
384
+ }
385
+ } else if (kind === 'categories') {
386
+ if (normalizeBool(cfg.enableCategoryAds)) {
387
+ inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
388
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
389
+ normalizeBool(cfg.showFirstCategoryAd),
390
+ 'categories',
391
+ state.usedCategories);
392
+ }
393
+ }
402
394
 
403
395
  enforceNoAdjacentAds();
404
396
 
405
- // If nothing inserted and we have no items yet (first click), retry a few times
406
- const itemCount = (kind === 'topic') ? getPostContainers().length : getTopicItems().length;
407
- if (itemCount === 0 && state.attempts < 25) {
397
+ // If nothing inserted and list isn't in DOM yet (first click), retry a bit
398
+ let count = 0;
399
+ if (kind === 'topic') count = getPostContainers().length;
400
+ else if (kind === 'categoryTopics') count = getTopicItems().length;
401
+ else if (kind === 'categories') count = getCategoryItems().length;
402
+
403
+ if (count === 0 && state.attempts < 25) {
408
404
  state.attempts += 1;
409
405
  setTimeout(() => scheduleRun('await-items'), 120);
410
406
  return;
411
407
  }
412
408
 
413
- if (inserted >= MAX_INSERTS_PER_RUN) {
414
- setTimeout(() => scheduleRun('continue-fill'), 140);
415
- }
409
+ if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
416
410
  }
417
411
 
418
412
  function scheduleRun() {
@@ -468,11 +462,10 @@
468
462
  });
469
463
  }
470
464
 
471
- // Boot
472
465
  cleanup();
473
466
  bind();
474
467
  ensureObserver();
475
468
  state.pageKey = getPageKey();
476
469
  scheduleRun();
477
470
  setTimeout(scheduleRun, 250);
478
- })();
471
+ })();
@@ -26,6 +26,32 @@
26
26
 
27
27
  <hr/>
28
28
 
29
+
30
+ <hr/>
31
+
32
+ <h4 class="mt-3">Pubs entre les catégories (page d’accueil)</h4>
33
+ <p class="form-text">Insère des pubs entre les catégories sur la page d’accueil (liste des catégories).</p>
34
+
35
+ <div class="form-check mb-3">
36
+ <input class="form-check-input" type="checkbox" id="enableCategoryAds" name="enableCategoryAds">
37
+ <label class="form-check-label" for="enableCategoryAds">Activer les pubs entre les catégories</label>
38
+ <div class="form-check mt-2">
39
+ <input class="form-check-input" type="checkbox" name="showFirstCategoryAd" />
40
+ <label class="form-check-label">Afficher une pub après la 1ère catégorie</label>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="mb-3">
45
+ <label class="form-label" for="categoryPlaceholderIds">Pool d’IDs Ezoic (catégories)</label>
46
+ <textarea id="categoryPlaceholderIds" name="categoryPlaceholderIds" class="form-control" rows="4">{categoryPlaceholderIds}</textarea>
47
+ <p class="form-text">IDs numériques, un par ligne. Utilise un pool dédié (différent des pools topics/messages).</p>
48
+ </div>
49
+
50
+ <div class="mb-3">
51
+ <label class="form-label" for="intervalCategories">Afficher une pub toutes les N catégories</label>
52
+ <input type="number" id="intervalCategories" name="intervalCategories" class="form-control" value="{intervalCategories}" min="1">
53
+ </div>
54
+
29
55
  <h4 class="mt-3">Pubs “message” entre les réponses</h4>
30
56
  <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
31
57