nodebb-plugin-ezoic-infinite 1.2.2 → 1.2.5

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 +52 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.2.2",
3
+ "version": "1.2.5",
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
@@ -35,10 +35,6 @@
35
35
  seenTopicAnchors: new WeakSet(),
36
36
  seenPostAnchors: new WeakSet(),
37
37
 
38
- // FIFO for recycling
39
- fifoTopics: [],
40
- fifoPosts: [],
41
-
42
38
  // showAds anti-double
43
39
  lastShowById: new Map(),
44
40
  pendingById: new Set(),
@@ -97,11 +93,57 @@
97
93
  return 'category';
98
94
  }
99
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
+
100
122
  function hasAdImmediatelyAfter(el) {
101
123
  const n = el && el.nextElementSibling;
102
124
  return !!(n && n.classList && n.classList.contains(WRAP_CLASS));
103
125
  }
104
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
+
105
147
  function buildWrap(id, kind, afterPos) {
106
148
  const wrap = document.createElement('div');
107
149
  wrap.className = `${WRAP_CLASS} ${kind}`;
@@ -123,12 +165,6 @@
123
165
  function safeRect(el) {
124
166
  try { return el.getBoundingClientRect(); } catch (e) { return null; }
125
167
  }
126
-
127
- function destroyPlaceholder(id) {
128
- try {
129
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
130
- window.ezstandalone.destroyPlaceholders([id]);
131
- }
132
168
  } catch (e) {}
133
169
  }
134
170
 
@@ -216,41 +252,14 @@
216
252
  if (attempts < 40) setTimeout(waitForPh, 50);
217
253
  })();
218
254
  }
219
-
220
- function recycleOne(pool, usedSet, fifo, wrapperSelector) {
221
- const margin = 1600;
222
- const topLimit = -margin;
223
-
224
- for (let i = 0; i < fifo.length; i++) {
225
- const old = fifo[i];
226
- const w = document.querySelector(wrapperSelector(old));
227
- if (!w) { fifo.splice(i, 1); i--; continue; }
228
-
229
- const r = safeRect(w);
230
- if (r && r.bottom < topLimit) {
231
- fifo.splice(i, 1);
232
- w.remove();
233
- usedSet.delete(old.id);
234
- destroyPlaceholder(old.id);
235
- pool.push(old.id);
236
- return true;
237
- }
238
255
  }
239
256
  return false;
240
257
  }
241
258
 
242
259
  function nextId(kind) {
243
- const isTopics = kind === 'between';
244
- const pool = isTopics ? state.poolTopics : state.poolPosts;
245
- const used = isTopics ? state.usedTopics : state.usedPosts;
246
- const fifo = isTopics ? state.fifoTopics : state.fifoPosts;
247
- const sel = isTopics
248
- ? (old) => `.${WRAP_CLASS}.ezoic-ad-between[data-ezoic-after="${old.after}"]`
249
- : (old) => `.${WRAP_CLASS}.ezoic-ad-message[data-ezoic-after="${old.after}"]`;
250
-
260
+ const pool = (kind === 'between') ? state.poolTopics : state.poolPosts;
251
261
  if (pool.length) return pool.shift();
252
- if (recycleOne(pool, used, fifo, sel) && pool.length) return pool.shift();
253
- return null;
262
+ return null; // stop injecting when pool is empty
254
263
  }
255
264
 
256
265
  async function fetchConfig() {
@@ -285,7 +294,7 @@
285
294
  const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
286
295
  const first = normalizeBool(cfg.showFirstTopicAd);
287
296
 
288
- const items = Array.from(document.querySelectorAll(SELECTORS.topicItem));
297
+ const items = getTopicItems();
289
298
  if (!items.length) return 0;
290
299
 
291
300
  let inserted = 0;
@@ -310,7 +319,6 @@
310
319
  const wrap = insertAfter(li, id, 'ezoic-ad-between', pos);
311
320
  if (!wrap) continue;
312
321
 
313
- state.fifoTopics.push({ id, after: pos });
314
322
  inserted += 1;
315
323
 
316
324
  callShowAdsWhenReady(id);
@@ -326,7 +334,7 @@
326
334
  const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
327
335
  const first = normalizeBool(cfg.showFirstMessageAd);
328
336
 
329
- const posts = Array.from(document.querySelectorAll(SELECTORS.postItem));
337
+ const posts = getPostContainers();
330
338
  if (!posts.length) return 0;
331
339
 
332
340
  let inserted = 0;
@@ -350,7 +358,6 @@
350
358
  const wrap = insertAfter(post, id, 'ezoic-ad-message', no);
351
359
  if (!wrap) continue;
352
360
 
353
- state.fifoPosts.push({ id, after: no });
354
361
  inserted += 1;
355
362
 
356
363
  callShowAdsWhenReady(id);
@@ -373,9 +380,6 @@
373
380
  state.seenTopicAnchors = new WeakSet();
374
381
  state.seenPostAnchors = new WeakSet();
375
382
 
376
- state.fifoTopics = [];
377
- state.fifoPosts = [];
378
-
379
383
  state.lastShowById = new Map();
380
384
  state.pendingById = new Set();
381
385
 
@@ -408,6 +412,8 @@
408
412
  if (kind === 'topic') inserted = injectPosts(cfg);
409
413
  else inserted = injectTopics(cfg);
410
414
 
415
+ enforceNoAdjacentAds();
416
+
411
417
  // If we inserted max per run, schedule another pass to gradually fill (avoids “burst”)
412
418
  if (inserted >= MAX_INSERTS_PER_RUN) {
413
419
  setTimeout(() => scheduleRun('continue-fill'), 120);