nodebb-plugin-ezoic-infinite 1.2.3 → 1.2.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +84 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.2.3",
3
+ "version": "1.2.6",
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
@@ -93,11 +93,57 @@
93
93
  return 'category';
94
94
  }
95
95
 
96
+
97
+ function getTopicItems() {
98
+ return Array.from(document.querySelectorAll(SELECTORS.topicItem));
99
+ }
100
+
101
+ function getPostContainers() {
102
+ // Harmony can include multiple [component="post"] blocks (e.g. parent previews, nested structures).
103
+ // We only want top-level post containers that actually contain post content.
104
+ const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
105
+ return nodes.filter((el) => {
106
+ if (!el || !el.isConnected) return false;
107
+
108
+ // Must contain post content
109
+ if (!el.querySelector('[component="post/content"]')) return false;
110
+
111
+ // Must not be nested within another post container
112
+ const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
113
+ if (parentPost && parentPost !== el) return false;
114
+
115
+ // Avoid "parent/quote" blocks that use component="post/parent"
116
+ if (el.getAttribute('component') === 'post/parent') return false;
117
+
118
+ return true;
119
+ });
120
+ }
121
+
96
122
  function hasAdImmediatelyAfter(el) {
97
123
  const n = el && el.nextElementSibling;
98
124
  return !!(n && n.classList && n.classList.contains(WRAP_CLASS));
99
125
  }
100
126
 
127
+ function enforceNoAdjacentAds() {
128
+ // NodeBB can virtualize (remove) topics/posts from the DOM while keeping our ad wrappers,
129
+ // which can temporarily make two ad wrappers adjacent. Hide the later one to avoid back-to-back ads.
130
+ const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
131
+ for (let i = 0; i < ads.length; i++) {
132
+ const ad = ads[i];
133
+ const prev = ad.previousElementSibling;
134
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
135
+ ad.style.display = 'none';
136
+ } else {
137
+ ad.style.display = '';
138
+ }
139
+ }
140
+ }
141
+
142
+
143
+ const n = el && el.nextElementSibling;
144
+ return !!(n && n.classList && n.classList.contains(WRAP_CLASS));
145
+ }
146
+
101
147
  function buildWrap(id, kind, afterPos) {
102
148
  const wrap = document.createElement('div');
103
149
  wrap.className = `${WRAP_CLASS} ${kind}`;
@@ -248,7 +294,7 @@
248
294
  const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
249
295
  const first = normalizeBool(cfg.showFirstTopicAd);
250
296
 
251
- const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
297
+ const items = getTopicItems();
252
298
  if (!items.length) return 0;
253
299
 
254
300
  let inserted = 0;
@@ -288,7 +334,7 @@
288
334
  const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
289
335
  const first = normalizeBool(cfg.showFirstMessageAd);
290
336
 
291
- const posts = Array.from(document.querySelectorAll(SELECTORS.postItem));
337
+ const posts = getPostContainers();
292
338
  if (!posts.length) return 0;
293
339
 
294
340
  let inserted = 0;
@@ -321,7 +367,40 @@
321
367
  return inserted;
322
368
  }
323
369
 
370
+
371
+ function destroyUsedPlaceholders() {
372
+ const ids = [];
373
+ try {
374
+ state.usedTopics.forEach((id) => ids.push(id));
375
+ state.usedPosts.forEach((id) => ids.push(id));
376
+ } catch (e) {}
377
+
378
+ if (!ids.length) return;
379
+
380
+ const call = () => {
381
+ try {
382
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
383
+ window.ezstandalone.destroyPlaceholders(ids);
384
+ }
385
+ } catch (e) {}
386
+ };
387
+
388
+ try {
389
+ window.ezstandalone = window.ezstandalone || {};
390
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
391
+
392
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
393
+ call();
394
+ } else {
395
+ // queue for when ezstandalone becomes ready
396
+ window.ezstandalone.cmd.push(call);
397
+ }
398
+ } catch (e) {}
399
+ }
400
+
324
401
  function cleanup() {
402
+ // Destroy slots for IDs we used on the previous view before we reuse the same IDs on the next page
403
+ destroyUsedPlaceholders();
325
404
  state.pageKey = getPageKey();
326
405
  state.cfg = null;
327
406
  state.cfgPromise = null;
@@ -354,6 +433,7 @@
354
433
 
355
434
  async function runCore() {
356
435
  patchShowAds();
436
+ patchShowAds();
357
437
 
358
438
  const cfg = await fetchConfig();
359
439
  if (!cfg || cfg.excluded) return;
@@ -366,6 +446,8 @@
366
446
  if (kind === 'topic') inserted = injectPosts(cfg);
367
447
  else inserted = injectTopics(cfg);
368
448
 
449
+ enforceNoAdjacentAds();
450
+
369
451
  // If we inserted max per run, schedule another pass to gradually fill (avoids “burst”)
370
452
  if (inserted >= MAX_INSERTS_PER_RUN) {
371
453
  setTimeout(() => scheduleRun('continue-fill'), 120);