nodebb-plugin-ezoic-infinite 0.8.9 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -22,8 +22,8 @@
22
22
  "scripts": [
23
23
  "public/client.js"
24
24
  ],
25
- "templates": "public/templates",
26
25
  "css": [
27
26
  "public/style.css"
28
- ]
27
+ ],
28
+ "templates": "public/templates"
29
29
  }
package/public/client.js CHANGED
@@ -1,174 +1,174 @@
1
1
  'use strict';
2
-
3
2
  /* globals ajaxify */
3
+
4
4
  window.ezoicInfiniteLoaded = true;
5
5
 
6
- let cachedConfig;
6
+ let cachedConfig = null;
7
7
  let lastFetch = 0;
8
- let debounceTimer;
9
-
10
8
  let inFlight = false;
11
9
  let rerunRequested = false;
10
+ let debounceTimer = null;
12
11
 
13
- // Per-page state (keyed by tid/cid)
14
12
  let pageKey = null;
15
13
 
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}]
14
+ // Separate state per "mode" so category and topic don't leak ids
15
+ let usedBetween = new Set();
16
+ let usedMessages = new Set();
20
17
 
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}]
18
+ let seenBetweenAfter = new Set(); // category: after absolute topic position
19
+ let fifoBetween = []; // [{afterPos, id}]
20
+ let seenMsgAfter = new Set(); // topic: after absolute post number
21
+ let fifoMsg = []; // [{afterPostNo, id}]
24
22
 
25
- function resetState() {
26
- seenAfterPostNo = new Set();
27
- seenAfterTopicPos = new Set();
28
- usedIds = new Set();
29
- fifo = [];
30
- fifoCat = [];
31
- }
23
+ // Destroy spam guard
24
+ window.__ezoicLastDestroy = window.__ezoicLastDestroy || {};
25
+ window.__ezoicRecycling = false;
32
26
 
33
- function getPageKey() {
34
- try {
35
- if (ajaxify && ajaxify.data) {
36
- if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
37
- if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
38
- }
39
- } catch (e) {}
40
- return window.location.pathname;
41
- }
27
+ // ---------- Config ----------
28
+ async function fetchConfig() {
29
+ const now = Date.now();
30
+ if (cachedConfig && (now - lastFetch) < 5000) return cachedConfig;
31
+ lastFetch = now;
42
32
 
43
- function parsePool(raw) {
44
- if (!raw) return [];
45
- return Array.from(new Set(
46
- String(raw).split(/[\n,;\s]+/)
47
- .map(x => parseInt(x, 10))
48
- .filter(n => Number.isFinite(n) && n > 0)
49
- ));
33
+ try {
34
+ const res = await fetch('/api/admin/settings/ezoic-infinite', { credentials: 'same-origin' });
35
+ const data = await res.json();
36
+ cachedConfig = {
37
+ excluded: !!data.excluded,
38
+ enableBetweenAds: data.enableBetweenAds !== false,
39
+ placeholderIds: String(data.placeholderIds || '').trim(),
40
+ intervalPosts: parseInt(data.intervalPosts, 10) || 6,
41
+
42
+ enableMessageAds: data.enableMessageAds !== false,
43
+ messagePlaceholderIds: String(data.messagePlaceholderIds || '').trim(),
44
+ messageIntervalPosts: parseInt(data.messageIntervalPosts, 10) || 3,
45
+ };
46
+ return cachedConfig;
47
+ } catch (e) {
48
+ return cachedConfig || {
49
+ excluded: false,
50
+ enableBetweenAds: true,
51
+ placeholderIds: '',
52
+ intervalPosts: 6,
53
+ enableMessageAds: true,
54
+ messagePlaceholderIds: '',
55
+ messageIntervalPosts: 3,
56
+ };
57
+ }
50
58
  }
51
59
 
52
- async function fetchConfig() {
53
- if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
54
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
55
- cachedConfig = await res.json();
56
- lastFetch = Date.now();
57
- return cachedConfig;
60
+ function parsePool(text) {
61
+ return String(text || '')
62
+ .split(/\r?\n|,|;/g)
63
+ .map(s => s.trim())
64
+ .filter(Boolean)
65
+ .map(s => parseInt(s, 10))
66
+ .filter(n => Number.isFinite(n) && n > 0);
58
67
  }
59
68
 
69
+ // ---------- Page detection ----------
60
70
  function isTopicPage() {
61
- return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
71
+ try {
72
+ if (ajaxify && ajaxify.data && ajaxify.data.tid) return true;
73
+ } catch (e) {}
74
+ return /^\/topic\//.test(window.location.pathname);
62
75
  }
63
76
 
64
77
  function isCategoryTopicListPage() {
65
- return $('li[component="category/topic"]').length > 0;
66
- }
67
-
68
- function getTopicPosts() {
69
- const $primary = $('[component="post"][data-pid]');
70
- if ($primary.length) return $primary;
71
-
72
- // fallback: top-level with post/content
73
- return $('[data-pid]').filter(function () {
74
- const $el = $(this);
75
- const hasContent = $el.find('[component="post/content"]').length > 0;
76
- const nested = $el.parents('[data-pid]').length > 0;
77
- return hasContent && !nested;
78
- });
79
- }
80
-
81
- function getCategoryTopicItems() {
82
- return $('li[component="category/topic"]');
78
+ return document.querySelectorAll('li[component="category/topic"]').length > 0;
83
79
  }
84
80
 
85
- // If target's parent is UL/OL, wrapper MUST be LI (otherwise browser may move it to top)
86
- function wrapperTagFor($target) {
87
- if (!$target || !$target.length) return 'div';
88
- const parentTag = ($target.parent().prop('tagName') || '').toUpperCase();
89
- if (parentTag === 'UL' || parentTag === 'OL') return 'li';
90
- const selfTag = ($target.prop('tagName') || '').toUpperCase();
91
- if (selfTag === 'LI') return 'li';
92
- return 'div';
81
+ function getPageKey() {
82
+ try {
83
+ if (ajaxify && ajaxify.data) {
84
+ if (ajaxify.data.tid) return `topic:${ajaxify.data.tid}`;
85
+ if (ajaxify.data.cid) return `category:${ajaxify.data.cid}`;
86
+ }
87
+ } catch (e) {}
88
+ return `path:${window.location.pathname}`;
93
89
  }
94
90
 
95
- function makeWrapperLike($target, classes, innerHtml, attrs) {
96
- const tag = wrapperTagFor($target);
97
- const attrStr = attrs ? ' ' + attrs : '';
98
- if (tag === 'li') {
99
- return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
100
- }
101
- return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
91
+ function resetState() {
92
+ usedBetween = new Set();
93
+ usedMessages = new Set();
94
+ seenBetweenAfter = new Set();
95
+ seenMsgAfter = new Set();
96
+ fifoBetween = [];
97
+ fifoMsg = [];
98
+ window.__ezoicLastShowKey = null;
99
+ window.__ezoicLastShowAt = 0;
102
100
  }
103
101
 
104
102
  function cleanupOnNav() {
105
- $('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad-between').remove();
106
- }
107
-
108
- function pickNextId(pool) {
109
- for (const id of pool) {
110
- if (!usedIds.has(id)) return id;
111
- }
112
- return null;
103
+ document.querySelectorAll('.ezoic-ad-post, .ezoic-ad-topic, .ezoic-ad').forEach(el => el.remove());
113
104
  }
114
105
 
115
- function removeOldestTopicAd() {
116
- fifo.sort((a, b) => a.afterPostNo - b.afterPostNo);
117
- const old = fifo.shift();
118
- if (!old) return false;
119
-
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();
106
+ // ---------- Ezoic helpers ----------
107
+ function destroyEzoicId(id) {
108
+ const now = Date.now();
109
+ if (window.__ezoicLastDestroy[id] && (now - window.__ezoicLastDestroy[id]) < 2000) return;
110
+ window.__ezoicLastDestroy[id] = now;
123
111
 
124
- usedIds.delete(old.id);
125
- // DO NOT delete seenAfterPostNo to prevent re-insertion in the top area
126
- return true;
112
+ try {
113
+ window.ezstandalone = window.ezstandalone || {};
114
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
115
+ window.ezstandalone.destroyPlaceholders(id);
116
+ return;
117
+ }
118
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
119
+ window.ezstandalone.cmd.push(function () {
120
+ try {
121
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
122
+ window.ezstandalone.destroyPlaceholders(id);
123
+ }
124
+ } catch (e) {}
125
+ });
126
+ } catch (e) {}
127
127
  }
128
128
 
129
- function removeOldestCategoryAd() {
130
- fifoCat.sort((a, b) => a.afterPos - b.afterPos);
131
- const old = fifoCat.shift();
132
- if (!old) return false;
129
+ // Auto-height: hide until filled
130
+ function setupAdAutoHeight() {
131
+ if (window.__ezoicAutoHeightAttached) return;
132
+ window.__ezoicAutoHeightAttached = true;
133
133
 
134
- const sel = '.ezoic-ad-topic[data-ezoic-after="' + old.afterPos + '"][data-ezoic-id="' + old.id + '"]';
135
- const $el = $(sel);
136
- if ($el.length) $el.remove();
134
+ const attach = function () {
135
+ document.querySelectorAll('.ezoic-ad').forEach(function (wrap) {
136
+ if (!wrap.classList.contains('ezoic-filled')) {
137
+ wrap.style.display = 'none';
138
+ }
139
+ const ph = wrap.querySelector('[id^="ezoic-pub-ad-placeholder-"]');
140
+ if (ph && ph.children && ph.children.length) {
141
+ wrap.classList.add('ezoic-filled');
142
+ wrap.style.display = '';
143
+ }
144
+ });
145
+ };
137
146
 
138
- usedIds.delete(old.id);
139
- return true;
147
+ attach();
148
+ setTimeout(attach, 500);
149
+ setTimeout(attach, 1500);
150
+ setTimeout(attach, 3000);
151
+ setInterval(attach, 1000);
140
152
  }
141
153
 
142
154
  function callEzoic(ids) {
143
- // ids optional; if omitted, we will scan DOM for unrendered placeholders
144
155
  window.ezstandalone = window.ezstandalone || {};
145
156
  window.ezstandalone.cmd = window.ezstandalone.cmd || [];
146
157
 
147
- const collect = function () {
148
- const list = [];
149
- document.querySelectorAll('.ezoic-ad [id^="ezoic-pub-ad-placeholder-"]').forEach(function (ph) {
150
- const idStr = ph.id.replace('ezoic-pub-ad-placeholder-', '');
151
- const id = parseInt(idStr, 10);
152
- if (!Number.isFinite(id) || id <= 0) return;
158
+ const uniq = Array.from(new Set(ids || []));
159
+ if (!uniq.length) return;
153
160
 
154
- const wrap = ph.closest('.ezoic-ad');
155
- if (!wrap) return;
156
- if (wrap.getAttribute('data-ezoic-rendered') === '1') return;
157
-
158
- list.push(id);
159
- wrap.setAttribute('data-ezoic-rendered', '1');
160
- });
161
- // de-dupe
162
- return Array.from(new Set(list));
163
- };
164
-
165
- const toShow = (ids && ids.length) ? Array.from(new Set(ids)) : collect();
166
- if (!toShow.length) return;
161
+ // De-dupe rapid duplicates
162
+ const key = uniq.join(',');
163
+ const now = Date.now();
164
+ if (window.__ezoicLastShowKey === key && (now - (window.__ezoicLastShowAt || 0)) < 1200) return;
165
+ window.__ezoicLastShowKey = key;
166
+ window.__ezoicLastShowAt = now;
167
167
 
168
168
  const run = function () {
169
169
  try {
170
170
  if (typeof window.ezstandalone.showAds === 'function') {
171
- window.ezstandalone.showAds.apply(window.ezstandalone, toShow);
171
+ window.ezstandalone.showAds.apply(window.ezstandalone, uniq);
172
172
  return true;
173
173
  }
174
174
  } catch (e) {}
@@ -179,194 +179,235 @@ function callEzoic(ids) {
179
179
  window.ezstandalone.cmd.push(function () { run(); });
180
180
 
181
181
  // Retry only if showAds isn't available yet
182
- let tries = 0;
183
- const maxTries = 6;
184
- const retry = function () {
185
- tries++;
186
- const ok = run();
187
- if (ok) return;
188
- try {
189
- if (typeof window.ezstandalone.showAds === 'function') return;
190
- } catch (e) {}
191
- if (tries < maxTries) setTimeout(retry, 800);
192
- };
193
-
194
182
  try {
195
183
  if (typeof window.ezstandalone.showAds !== 'function') {
184
+ let tries = 0;
185
+ const retry = function () {
186
+ tries++;
187
+ if (run()) return;
188
+ if (tries < 6) setTimeout(retry, 800);
189
+ };
196
190
  setTimeout(retry, 800);
197
191
  }
198
- } catch (e) {
199
- setTimeout(retry, 800);
200
- }
192
+ } catch (e) {}
201
193
  }
202
194
 
203
- function getPostNumber($post) {
204
- const di = parseInt($post.attr('data-index'), 10);
205
- if (Number.isFinite(di) && di > 0) return di;
206
-
207
- const txt = ($post.find('a.post-index').first().text() || '').trim();
208
- const m = txt.match(/#\s*(\d+)/);
209
- if (m) return parseInt(m[1], 10);
210
-
211
- return NaN;
195
+ // ---------- Pool logic ----------
196
+ function pickNextId(pool, usedSet) {
197
+ for (const id of pool) {
198
+ if (!usedSet.has(id)) return id;
199
+ }
200
+ return null;
212
201
  }
213
202
 
214
- function getTopicPos($item) {
215
- const pos = parseInt($item.attr('data-index'), 10);
216
- if (Number.isFinite(pos) && pos >= 0) return pos + 1;
217
- const schemaPos = parseInt($item.find('meta[itemprop="position"]').attr('content'), 10);
218
- if (Number.isFinite(schemaPos) && schemaPos > 0) return schemaPos;
219
- return NaN;
220
- }
203
+ function recycleOldest(fifo, usedSet, selector, avoidAfterNode) {
204
+ if (window.__ezoicRecycling) return null;
205
+ window.__ezoicRecycling = true;
206
+ try {
207
+ fifo.sort((a, b) => (a.after - b.after));
208
+ while (fifo.length) {
209
+ const old = fifo.shift();
210
+ const el = document.querySelector(selector(old));
211
+ if (!el) continue;
212
+
213
+ // Don't recycle if it is right after the last real item (protect sentinel)
214
+ try {
215
+ if (avoidAfterNode && el.previousElementSibling === avoidAfterNode) {
216
+ fifo.push(old);
217
+ return null;
218
+ }
219
+ } catch (e) {}
221
220
 
222
- function injectTopicMessageAds($posts, pool, interval) {
223
- const newIds = [];
221
+ el.remove();
222
+ usedSet.delete(old.id);
223
+ destroyEzoicId(old.id);
224
+ return old.id;
225
+ }
226
+ return null;
227
+ } finally {
228
+ window.__ezoicRecycling = false;
229
+ }
230
+ }
224
231
 
225
- $posts.each(function () {
226
- const $p = $(this);
227
- // Never insert after the last real post: it can break NodeBB infinite scroll
228
- if ($p.is($posts.last())) return;
229
- const postNo = getPostNumber($p);
230
- if (!Number.isFinite(postNo) || postNo <= 0) return;
232
+ // ---------- Injection ----------
233
+ function injectBetweenAds(config) {
234
+ if (!config.enableBetweenAds) return;
235
+ const pool = parsePool(config.placeholderIds);
236
+ if (!pool.length) return;
231
237
 
232
- if (postNo % interval !== 0) return;
233
- if (seenAfterPostNo.has(postNo)) return;
238
+ const interval = Math.max(1, config.intervalPosts);
239
+ const items = Array.from(document.querySelectorAll('li[component="category/topic"]'));
240
+ if (!items.length) return;
234
241
 
235
- let id = pickNextId(pool);
236
- if (!id) { return; }
242
+ const newIds = [];
243
+ const lastItem = items[items.length - 1];
244
+
245
+ for (let i = 0; i < items.length; i++) {
246
+ const pos = i + 1; // absolute position in loaded list
247
+ if (pos % interval !== 0) continue;
248
+ if (seenBetweenAfter.has(pos)) continue;
249
+
250
+ let id = pickNextId(pool, usedBetween);
251
+ if (!id) {
252
+ // recycle oldest
253
+ id = recycleOldest(
254
+ fifoBetween,
255
+ usedBetween,
256
+ (o) => `.ezoic-ad-topic[data-ezoic-after="${o.after}"][data-ezoic-id="${o.id}"]`,
257
+ lastItem
258
+ );
259
+ if (!id) break;
260
+ }
237
261
 
238
- const inner = '<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
239
- const html = makeWrapperLike(
240
- $p,
241
- 'ezoic-ad-post ezoic-ad',
242
- inner,
243
- 'data-ezoic-after="' + postNo + '" data-ezoic-id="' + id + '"'
244
- );
262
+ const anchor = items[i];
263
+ const wrap = document.createElement('li');
264
+ wrap.className = 'ezoic-ad ezoic-ad-topic';
265
+ wrap.setAttribute('data-ezoic-after', String(pos));
266
+ wrap.setAttribute('data-ezoic-id', String(id));
267
+ wrap.innerHTML = `<div id="ezoic-pub-ad-placeholder-${id}"></div>`;
245
268
 
246
- $p.after(html);
269
+ anchor.insertAdjacentElement('afterend', wrap);
247
270
 
248
- seenAfterPostNo.add(postNo);
249
- usedIds.add(id);
250
- fifo.push({ afterPostNo: postNo, id: id });
271
+ seenBetweenAfter.add(pos);
272
+ usedBetween.add(id);
273
+ fifoBetween.push({ after: pos, id });
251
274
  newIds.push(id);
252
- });
275
+ }
253
276
 
254
- return newIds;
277
+ if (newIds.length) callEzoic(newIds);
255
278
  }
256
279
 
257
- function injectCategoryBetweenAds($items, pool, interval) {
258
- const newIds = [];
280
+ function getPostNumberFromPost(postEl) {
281
+ // Prefer post-index text (#72)
282
+ try {
283
+ const idx = postEl.querySelector('a.post-index');
284
+ if (idx) {
285
+ const n = parseInt(String(idx.textContent || '').replace('#', '').trim(), 10);
286
+ if (Number.isFinite(n)) return n;
287
+ }
288
+ } catch (e) {}
289
+
290
+ // Fallback: pid-based ordering not reliable, but DOM order is ok for interval within loaded window.
291
+ return null;
292
+ }
259
293
 
260
- $items.each(function () {
261
- const $it = $(this);
262
- // Never insert after the last real topic item (keeps NodeBB infinite scroll working)
263
- if ($it.is($items.last())) return;
264
- const pos = getTopicPos($it);
265
- if (!Number.isFinite(pos) || pos <= 0) return;
294
+ function injectMessageAds(config) {
295
+ if (!config.enableMessageAds) return;
296
+ const pool = parsePool(config.messagePlaceholderIds);
297
+ if (!pool.length) return;
266
298
 
267
- if (pos % interval !== 0) return;
268
- if (seenAfterTopicPos.has(pos)) return;
299
+ const interval = Math.max(1, config.messageIntervalPosts);
300
+ const posts = Array.from(document.querySelectorAll('[component="post"][data-pid]'));
301
+ if (!posts.length) return;
269
302
 
270
- let id = pickNextId(pool);
271
- if (!id) { return; }
303
+ const newIds = [];
304
+ const lastPost = posts[posts.length - 1];
305
+
306
+ // Determine absolute post numbers if available; else use DOM position
307
+ let numbers = posts.map((p, i) => ({ el: p, no: getPostNumberFromPost(p) || (i + 1) }));
308
+ // Ensure strictly increasing by DOM
309
+ numbers = numbers.map((x, i) => ({ el: x.el, no: x.no || (i + 1) }));
310
+
311
+ for (const entry of numbers) {
312
+ const afterNo = entry.no;
313
+ if (afterNo % interval !== 0) continue;
314
+ if (seenMsgAfter.has(afterNo)) continue;
315
+
316
+ let id = pickNextId(pool, usedMessages);
317
+ if (!id) {
318
+ id = recycleOldest(
319
+ fifoMsg,
320
+ usedMessages,
321
+ (o) => `.ezoic-ad-post[data-ezoic-after="${o.after}"][data-ezoic-id="${o.id}"]`,
322
+ lastPost
323
+ );
324
+ if (!id) break;
325
+ }
272
326
 
273
- const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
274
- const html = makeWrapperLike(
275
- $it,
276
- 'ezoic-ad-topic ezoic-ad',
277
- placeholder,
278
- 'data-ezoic-after="' + pos + '" data-ezoic-id="' + id + '"'
279
- );
327
+ const wrap = document.createElement('div');
328
+ wrap.className = 'ezoic-ad ezoic-ad-post';
329
+ wrap.setAttribute('data-ezoic-after', String(afterNo));
330
+ wrap.setAttribute('data-ezoic-id', String(id));
331
+ wrap.innerHTML = `<div class="ezoic-ad-message-inner"><div id="ezoic-pub-ad-placeholder-${id}"></div></div>`;
280
332
 
281
- $it.after(html);
333
+ entry.el.insertAdjacentElement('afterend', wrap);
282
334
 
283
- seenAfterTopicPos.add(pos);
284
- usedIds.add(id);
285
- fifoCat.push({ afterPos: pos, id: id });
335
+ seenMsgAfter.add(afterNo);
336
+ usedMessages.add(id);
337
+ fifoMsg.push({ after: afterNo, id });
286
338
  newIds.push(id);
287
- });
339
+ }
288
340
 
289
- return newIds;
341
+ if (newIds.length) callEzoic(newIds);
290
342
  }
291
343
 
344
+ // ---------- Main refresh ----------
292
345
  async function refreshAds() {
293
- const key = getPageKey();
294
- if (pageKey !== key) {
295
- pageKey = key;
296
- resetState();
297
- cleanupOnNav();
346
+ if (inFlight) {
347
+ rerunRequested = true;
348
+ return;
298
349
  }
299
-
300
- if (inFlight) { rerunRequested = true; return; }
301
350
  inFlight = true;
351
+ rerunRequested = false;
302
352
 
303
353
  try {
304
354
  const cfg = await fetchConfig();
305
- if (!cfg || cfg.excluded) return;
355
+ if (cfg.excluded) return;
306
356
 
307
- const betweenPool = parsePool(cfg.placeholderIds);
308
- const betweenInterval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
309
-
310
- const messagePool = parsePool(cfg.messagePlaceholderIds);
311
- const messageInterval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
357
+ const key = getPageKey();
358
+ if (key !== pageKey) {
359
+ pageKey = key;
360
+ resetState();
361
+ cleanupOnNav();
362
+ }
312
363
 
313
364
  const onTopic = isTopicPage();
314
- const onCategory = !onTopic && isCategoryTopicListPage();
315
-
316
- const newIds = [];
317
-
318
- if (onCategory) {
319
- const $items = getCategoryTopicItems();
320
- if (cfg.enableBetweenAds && betweenPool.length && $items.length) {
321
- newIds.push(...injectCategoryBetweenAds($items, betweenPool, betweenInterval));
322
- }
323
- callEzoic(newIds);
324
- callEzoic();
325
- return;
326
- }
365
+ const onCategory = isCategoryTopicListPage() && !onTopic;
327
366
 
328
- if (onTopic) {
329
- const $posts = getTopicPosts();
330
- if (cfg.enableMessageAds && messagePool.length && $posts.length) {
331
- newIds.push(...injectTopicMessageAds($posts, messagePool, messageInterval));
332
- }
333
- callEzoic(newIds);
334
- callEzoic();
335
- }
367
+ if (onCategory) injectBetweenAds(cfg);
368
+ if (onTopic) injectMessageAds(cfg);
336
369
  } finally {
337
370
  inFlight = false;
338
- if (rerunRequested) {
339
- rerunRequested = false;
340
- setTimeout(refreshAds, 160);
341
- }
371
+ if (rerunRequested) setTimeout(refreshAds, 50);
342
372
  }
343
373
  }
344
374
 
345
375
  function debounceRefresh() {
346
- clearTimeout(debounceTimer);
347
- debounceTimer = setTimeout(refreshAds, 220);
376
+ if (debounceTimer) clearTimeout(debounceTimer);
377
+ debounceTimer = setTimeout(refreshAds, 250);
348
378
  }
349
379
 
350
- $(document).ready(debounceRefresh);
351
- $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded action:topics.loaded action:category.loaded', debounceRefresh);
352
- setTimeout(debounceRefresh, 2200);
380
+ // ---------- Observers / Events ----------
381
+ (function setupTriggers() {
382
+ // Ajaxify navigation: only cleanup when URL changes
383
+ $(window).on('action:ajaxify.start', function (ev, data) {
384
+ try {
385
+ const targetUrl = (data && (data.url || data.href)) ? String(data.url || data.href) : '';
386
+ if (targetUrl) {
387
+ const a = document.createElement('a');
388
+ a.href = targetUrl;
389
+ const targetPath = a.pathname || targetUrl;
390
+ if (targetPath === window.location.pathname) return;
391
+ }
392
+ } catch (e) {}
393
+ pageKey = null;
394
+ resetState();
395
+ cleanupOnNav();
396
+ });
397
+
398
+ $(window).on('action:ajaxify.end action:posts.loaded action:topics.loaded action:topic.loaded action:category.loaded', debounceRefresh);
353
399
 
354
- // Fallback: some themes/devices don't emit the expected events for infinite scroll.
355
- // Observe DOM additions and trigger a refresh when new posts/topics are appended/prepended.
356
- (function setupEzoicObserver() {
357
- if (window.__ezoicInfiniteObserver) return;
400
+ // MutationObserver (new posts/topics appended)
358
401
  try {
359
402
  const obs = new MutationObserver(function (mutations) {
360
403
  for (const m of mutations) {
361
- if (!m.addedNodes || !m.addedNodes.length) continue;
404
+ if (!m.addedNodes) continue;
362
405
  for (const n of m.addedNodes) {
363
406
  if (!n || n.nodeType !== 1) continue;
364
- // direct match
365
407
  if (n.matches && (n.matches('[component="post"][data-pid]') || n.matches('li[component="category/topic"]'))) {
366
408
  debounceRefresh();
367
409
  return;
368
410
  }
369
- // descendant match
370
411
  if (n.querySelector && (n.querySelector('[component="post"][data-pid]') || n.querySelector('li[component="category/topic"]'))) {
371
412
  debounceRefresh();
372
413
  return;
@@ -377,4 +418,26 @@ setTimeout(debounceRefresh, 2200);
377
418
  obs.observe(document.body, { childList: true, subtree: true });
378
419
  window.__ezoicInfiniteObserver = obs;
379
420
  } catch (e) {}
421
+
422
+ // Poller fallback (count changes)
423
+ let lastPosts = 0;
424
+ let lastTopics = 0;
425
+ setInterval(function () {
426
+ const p = document.querySelectorAll('[component="post"][data-pid]').length;
427
+ const t = document.querySelectorAll('li[component="category/topic"]').length;
428
+ if (p !== lastPosts || t !== lastTopics) {
429
+ lastPosts = p; lastTopics = t;
430
+ debounceRefresh();
431
+ }
432
+ }, 1500);
433
+
434
+ // First run(s) - important for hard load
435
+ document.addEventListener('DOMContentLoaded', function () {
436
+ debounceRefresh();
437
+ setTimeout(debounceRefresh, 1500);
438
+ setTimeout(debounceRefresh, 5000);
439
+ setTimeout(debounceRefresh, 10000);
440
+ });
441
+ // In case this script loads after DOMContentLoaded
442
+ setTimeout(debounceRefresh, 800);
380
443
  })();
package/public/style.css CHANGED
@@ -1,2 +1,4 @@
1
- .ezoic-ad-post{margin:0.75rem 0;}
2
- .ezoic-ad-message-inner{padding:0.75rem 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-message-inner{padding:0;margin:0;}
4
+ .ezoic-ad-message-inner > div{margin:0;padding:0;}