nodebb-plugin-ezoic-infinite 0.6.7 → 0.6.9

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 +108 -74
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -10,19 +10,27 @@ let debounceTimer;
10
10
  let inFlight = false;
11
11
  let rerunRequested = false;
12
12
 
13
- // Per-page incremental state
13
+ // Per-page state (keyed by tid/cid)
14
14
  let pageKey = null;
15
- let seenSlots = new Set(); // slots already processed (never cleared on eviction)
16
- let usedIds = new Set(); // IDs currently present in DOM
17
- let adsFIFO = []; // [{slot, id}] oldest first
18
15
 
19
- function resetPageState() {
20
- seenSlots = new Set();
16
+ // Topic page state: anchor ads to absolute post number (not DOM index)
17
+ let seenAfterPostNo = new Set(); // post numbers we've already inserted an ad after
18
+ let usedIds = new Set(); // ids currently in DOM
19
+ let fifo = []; // [{afterPostNo, id}]
20
+
21
+ // Category page state: anchor to absolute topic position if available
22
+ let seenAfterTopicPos = new Set(); // topic positions we've inserted after
23
+ let fifoCat = []; // [{afterPos, id}]
24
+
25
+ function resetState() {
26
+ seenAfterPostNo = new Set();
27
+ seenAfterTopicPos = new Set();
21
28
  usedIds = new Set();
22
- adsFIFO = [];
29
+ fifo = [];
30
+ fifoCat = [];
23
31
  }
24
32
 
25
- function currentPageKey() {
33
+ function getPageKey() {
26
34
  try {
27
35
  if (ajaxify && ajaxify.data) {
28
36
  if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
@@ -58,11 +66,10 @@ function isCategoryTopicListPage() {
58
66
  }
59
67
 
60
68
  function getTopicPosts() {
61
- // Only real posts
62
69
  const $primary = $('[component="post"][data-pid]');
63
70
  if ($primary.length) return $primary;
64
71
 
65
- // Fallback: top-level with post/content
72
+ // fallback: top-level with post/content
66
73
  return $('[data-pid]').filter(function () {
67
74
  const $el = $(this);
68
75
  const hasContent = $el.find('[component="post/content"]').length > 0;
@@ -94,7 +101,7 @@ function makeWrapperLike($target, classes, innerHtml, attrs) {
94
101
  return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
95
102
  }
96
103
 
97
- function cleanupWrappersOnNav() {
104
+ function cleanupOnNav() {
98
105
  $('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad-between').remove();
99
106
  }
100
107
 
@@ -105,11 +112,26 @@ function pickNextId(pool) {
105
112
  return null;
106
113
  }
107
114
 
108
- function evictOldestOne() {
109
- const old = adsFIFO.shift();
115
+ function removeOldestTopicAd() {
116
+ fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
117
+ const old = fifo.shift();
110
118
  if (!old) return false;
111
119
 
112
- const sel = '[data-ezoic-slot="' + old.slot + '"][data-ezoic-id="' + old.id + '"]';
120
+ const sel = '.ezoic-ad-post[data-ezoic-after="' + old.afterPostNo + '"][data-ezoic-id="' + old.id + '"]';
121
+ const $el = $(sel);
122
+ if ($el.length) $el.remove();
123
+
124
+ usedIds.delete(old.id);
125
+ // DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
126
+ return true;
127
+ }
128
+
129
+ function removeOldestCategoryAd() {
130
+ fifoCat.sort((a, b) => a.afterPos - b.afterPos);
131
+ const old = fifoCat.shift();
132
+ if (!old) return false;
133
+
134
+ const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
113
135
  const $el = $(sel);
114
136
  if ($el.length) $el.remove();
115
137
 
@@ -135,7 +157,6 @@ function callEzoic(ids) {
135
157
 
136
158
  window.ezstandalone.cmd.push(function () { run(); });
137
159
 
138
- // Retry (Ezoic can load late)
139
160
  let tries = 0;
140
161
  const maxTries = 6;
141
162
  const timer = setInterval(function () {
@@ -144,93 +165,109 @@ function callEzoic(ids) {
144
165
  }, 800);
145
166
  }
146
167
 
147
- function injectBetweenIncremental($items, pool, interval, wrapperClass) {
148
- const total = $items.length;
149
- const maxSlot = Math.floor(total / interval);
150
- if (maxSlot <= 0) return [];
168
+ function getPostNumber($post) {
169
+ const di = parseInt($post.attr('data-index'), 10);
170
+ if (Number.isFinite(di) && di > 0) return di;
171
+
172
+ const txt = ($post.find('a.post-index').first().text() || '').trim();
173
+ const m = txt.match(/#\s*(\d+)/);
174
+ if (m) return parseInt(m[1], 10);
175
+
176
+ return NaN;
177
+ }
178
+
179
+ function getTopicPos($item) {
180
+ const pos = parseInt($item.attr('data-index'), 10);
181
+ if (Number.isFinite(pos) && pos >= 0) return pos + 1;
182
+ const schemaPos = parseInt($item.find('meta[itemprop="position"]').attr('content'), 10);
183
+ if (Number.isFinite(schemaPos) && schemaPos > 0) return schemaPos;
184
+ return NaN;
185
+ }
151
186
 
187
+ function injectTopicMessageAds($posts, pool, interval) {
152
188
  const newIds = [];
153
189
 
154
- for (let slot = 1; slot <= maxSlot; slot++) {
155
- if (seenSlots.has(slot)) continue;
190
+ $posts.each(function () {
191
+ const $p = $(this);
192
+ // Never insert after the last real post: it can break NodeBB infinite scroll
193
+ if ($p.is($posts.last())) return;
194
+ const postNo = getPostNumber($p);
195
+ if (!Number.isFinite(postNo) || postNo <= 0) return;
156
196
 
157
- const index = slot * interval - 1;
158
- const $target = $items.eq(index);
159
- if (!$target.length) continue;
197
+ if (postNo % interval !== 0) return;
198
+ if (seenAfterPostNo.has(postNo)) return;
160
199
 
161
200
  let id = pickNextId(pool);
162
201
  if (!id) {
163
- if (!evictOldestOne()) break;
202
+ if (!removeOldestTopicAd()) return;
164
203
  id = pickNextId(pool);
165
- if (!id) break;
204
+ if (!id) return;
166
205
  }
167
206
 
168
- const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
207
+ const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
169
208
  const html = makeWrapperLike(
170
- $target,
171
- wrapperClass + ' ezoic-ad',
172
- placeholder,
173
- 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"'
209
+ $p,
210
+ 'ezoic-ad-post ezoic-ad',
211
+ inner,
212
+ 'data-ezoic-after="' + postNo + '" data-ezoic-id="' + id + '"'
174
213
  );
175
214
 
176
- $target.after(html);
215
+ $p.after(html);
177
216
 
178
- seenSlots.add(slot);
217
+ seenAfterPostNo.add(postNo);
179
218
  usedIds.add(id);
180
- adsFIFO.push({ slot: slot, id: id });
219
+ fifo.push({ afterPostNo: postNo, id: id });
181
220
  newIds.push(id);
182
- }
221
+ });
183
222
 
184
223
  return newIds;
185
224
  }
186
225
 
187
- function injectMessageIncremental($posts, pool, interval) {
188
- const total = $posts.length;
189
- const maxSlot = Math.floor(total / interval);
190
- if (maxSlot <= 0) return [];
191
-
226
+ function injectCategoryBetweenAds($items, pool, interval) {
192
227
  const newIds = [];
193
228
 
194
- for (let slot = 1; slot <= maxSlot; slot++) {
195
- if (seenSlots.has(slot)) continue;
229
+ $items.each(function () {
230
+ const $it = $(this);
231
+ // Never insert after the last real topic item (keeps NodeBB infinite scroll working)
232
+ if ($it.is($items.last())) return;
233
+ const pos = getTopicPos($it);
234
+ if (!Number.isFinite(pos) || pos <= 0) return;
196
235
 
197
- const index = slot * interval - 1;
198
- const $target = $posts.eq(index);
199
- if (!$target.length) continue;
236
+ if (pos % interval !== 0) return;
237
+ if (seenAfterTopicPos.has(pos)) return;
200
238
 
201
239
  let id = pickNextId(pool);
202
240
  if (!id) {
203
- if (!evictOldestOne()) break;
241
+ if (!removeOldestCategoryAd()) return;
204
242
  id = pickNextId(pool);
205
- if (!id) break;
243
+ if (!id) return;
206
244
  }
207
245
 
208
- // Do NOT use class "post" nor component="post" for ads: can break NodeBB infinite scroll.
209
- const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
246
+ const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
210
247
  const html = makeWrapperLike(
211
- $target,
212
- 'ezoic-ad-post ezoic-ad',
213
- inner,
214
- 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"'
248
+ $it,
249
+ 'ezoic-ad-topic ezoic-ad',
250
+ placeholder,
251
+ 'data-ezoic-after="' + pos + '" data-ezoic-id="' + id + '"'
215
252
  );
216
253
 
217
- $target.after(html);
254
+ $it.after(html);
218
255
 
219
- seenSlots.add(slot);
256
+ seenAfterTopicPos.add(pos);
220
257
  usedIds.add(id);
221
- adsFIFO.push({ slot: slot, id: id });
258
+ fifoCat.push({ afterPos: pos, id: id });
222
259
  newIds.push(id);
223
- }
260
+ });
224
261
 
225
262
  return newIds;
226
263
  }
227
264
 
228
265
  async function refreshAds() {
229
- const key = currentPageKey();
266
+ const key = getPageKey();
230
267
  if (pageKey !== key) {
231
268
  pageKey = key;
232
- resetPageState();
233
- cleanupWrappersOnNav();
269
+ resetState();
270
+ cleanupOnNav();
234
271
  }
235
272
 
236
273
  if (inFlight) { rerunRequested = true; return; }
@@ -249,24 +286,21 @@ async function refreshAds() {
249
286
  const onTopic = isTopicPage();
250
287
  const onCategory = !onTopic && isCategoryTopicListPage();
251
288
 
252
- const $posts = onTopic ? getTopicPosts() : $();
253
- const $topicItems = onCategory ? getCategoryTopicItems() : $();
254
-
255
- if (!$posts.length && !$topicItems.length) return;
256
-
257
289
  const newIds = [];
258
290
 
259
- if ($topicItems.length) {
260
- if (cfg.enableBetweenAds && betweenPool.length) {
261
- newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
291
+ if (onCategory) {
292
+ const $items = getCategoryTopicItems();
293
+ if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
294
+ newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
262
295
  }
263
296
  callEzoic(newIds);
264
297
  return;
265
298
  }
266
299
 
267
- if ($posts.length) {
268
- if (cfg.enableMessageAds && messagePool.length) {
269
- newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
300
+ if (onTopic) {
301
+ const $posts = getTopicPosts();
302
+ if (cfg.enableMessageAds && messagePool.length && $posts.length) {
303
+ newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
270
304
  }
271
305
  callEzoic(newIds);
272
306
  }
@@ -274,16 +308,16 @@ async function refreshAds() {
274
308
  inFlight = false;
275
309
  if (rerunRequested) {
276
310
  rerunRequested = false;
277
- setTimeout(refreshAds, 120);
311
+ setTimeout(refreshAds, 160);
278
312
  }
279
313
  }
280
314
  }
281
315
 
282
316
  function debounceRefresh() {
283
317
  clearTimeout(debounceTimer);
284
- debounceTimer = setTimeout(refreshAds, 200);
318
+ debounceTimer = setTimeout(refreshAds, 220);
285
319
  }
286
320
 
287
321
  $(document).ready(debounceRefresh);
288
322
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
289
- setTimeout(debounceRefresh, 1800);
323
+ setTimeout(debounceRefresh, 2200);