nodebb-plugin-ezoic-infinite 1.5.6 → 1.5.7
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 +1 -1
- package/package.json +1 -1
- package/public/client.js +419 -328
- package/public/style.css +1 -3
package/library.js
CHANGED
|
@@ -107,7 +107,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
107
107
|
|
|
108
108
|
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
109
109
|
const settings = await getSettings();
|
|
110
|
-
const excluded = await isUserExcluded(
|
|
110
|
+
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
111
111
|
|
|
112
112
|
res.json({
|
|
113
113
|
excluded,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,386 +1,477 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/**
|
|
3
|
-
* Ezoic Infinite Ads for NodeBB 4.x
|
|
4
|
-
* - Event-driven only (ajaxify hooks + MutationObserver + IntersectionObserver)
|
|
5
|
-
* - Hard gate on server config (respects excluded groups BEFORE any injection)
|
|
6
|
-
* - Placeholder Registry: keeps all placeholder elements alive in a hidden pool so ez-standalone never complains
|
|
7
|
-
* - Safe showAds wrapper (array, varargs, single) + tokenized execution across ajaxify navigations
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
(function () {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
|
+
|
|
6
|
+
const WRAP_CLASS = 'ezoic-ad';
|
|
7
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
8
|
+
|
|
9
|
+
const SELECTORS = {
|
|
10
|
+
topicItem: 'li[component="category/topic"]',
|
|
11
|
+
categoryItem: 'li[component="categories/category"]',
|
|
12
|
+
postItem: '[component="post"][data-pid]',
|
|
13
|
+
postContent: '[component="post/content"]',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ----------------------------
|
|
17
|
+
// State
|
|
18
|
+
// ----------------------------
|
|
19
|
+
const state = {
|
|
20
|
+
pageKey: null,
|
|
21
|
+
pageToken: 0,
|
|
22
|
+
cfgPromise: null,
|
|
23
|
+
cfg: null,
|
|
24
|
+
// throttle per id
|
|
25
|
+
lastShowAt: new Map(),
|
|
26
|
+
// observed placeholders -> ids
|
|
27
|
+
io: null,
|
|
28
|
+
mo: null,
|
|
29
|
+
scheduled: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ----------------------------
|
|
33
|
+
// Small utils
|
|
34
|
+
// ----------------------------
|
|
35
|
+
function getPageKey() {
|
|
36
|
+
try {
|
|
37
|
+
const ax = window.ajaxify;
|
|
38
|
+
if (ax && ax.data) {
|
|
39
|
+
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
40
|
+
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
return window.location.pathname || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeBool(v) {
|
|
47
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on' || v === 'yes';
|
|
23
48
|
}
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let cfg = null;
|
|
34
|
-
|
|
35
|
-
// -------------------------
|
|
36
|
-
// Placeholder Registry (keeps IDs alive)
|
|
37
|
-
// -------------------------
|
|
38
|
-
const Pool = {
|
|
39
|
-
el: null,
|
|
40
|
-
ensurePool() {
|
|
41
|
-
if (this.el && document.body.contains(this.el)) return this.el;
|
|
42
|
-
const d = document.createElement('div');
|
|
43
|
-
d.id = 'ezoic-placeholder-pool';
|
|
44
|
-
d.style.display = 'none';
|
|
45
|
-
d.setAttribute('aria-hidden', 'true');
|
|
46
|
-
document.body.appendChild(d);
|
|
47
|
-
this.el = d;
|
|
48
|
-
return d;
|
|
49
|
-
},
|
|
50
|
-
ensurePlaceholder(id) {
|
|
51
|
-
if (!id) return null;
|
|
52
|
-
let ph = document.getElementById(id);
|
|
53
|
-
if (ph) return ph;
|
|
54
|
-
const pool = this.ensurePool();
|
|
55
|
-
ph = document.createElement('div');
|
|
56
|
-
ph.id = id;
|
|
57
|
-
pool.appendChild(ph);
|
|
58
|
-
return ph;
|
|
59
|
-
},
|
|
60
|
-
returnToPool(id) {
|
|
61
|
-
const ph = document.getElementById(id);
|
|
62
|
-
if (!ph) return;
|
|
63
|
-
const pool = this.ensurePool();
|
|
64
|
-
if (ph.parentElement !== pool) {
|
|
65
|
-
pool.appendChild(ph);
|
|
50
|
+
function uniqInts(lines) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const v of lines) {
|
|
54
|
+
const n = parseInt(v, 10);
|
|
55
|
+
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
56
|
+
seen.add(n);
|
|
57
|
+
out.push(n);
|
|
66
58
|
}
|
|
67
59
|
}
|
|
68
|
-
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
function normalizeIds(argsLike) {
|
|
74
|
-
const args = Array.from(argsLike);
|
|
75
|
-
if (!args.length) return [];
|
|
76
|
-
// showAds([id1,id2]) or showAds([[...]]) edge
|
|
77
|
-
if (args.length === 1 && Array.isArray(args[0])) return args[0].flat().map(String);
|
|
78
|
-
return args.flat().map(String);
|
|
63
|
+
function parsePool(raw) {
|
|
64
|
+
if (!raw) return [];
|
|
65
|
+
return uniqInts(String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean));
|
|
79
66
|
}
|
|
80
67
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const el = document.getElementById(id);
|
|
93
|
-
if (el && document.body.contains(el)) {
|
|
94
|
-
try {
|
|
95
|
-
// Call one-by-one to avoid batch logging on missing nodes
|
|
96
|
-
original.call(window.ezstandalone, id);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// swallow: Ezoic can throw if called during transitions
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
68
|
+
function getKind() {
|
|
69
|
+
const p = window.location.pathname || '';
|
|
70
|
+
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
|
+
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
|
+
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
73
|
+
// fallback by DOM
|
|
74
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
75
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
76
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
77
|
+
return 'other';
|
|
78
|
+
}
|
|
103
79
|
|
|
104
|
-
|
|
105
|
-
return
|
|
80
|
+
function getTopicItems() {
|
|
81
|
+
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
106
82
|
}
|
|
107
83
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
window.ezstandalone.showAds = wrapShowAds(window.ezstandalone.showAds);
|
|
112
|
-
}
|
|
84
|
+
function getCategoryItems() {
|
|
85
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
86
|
+
}
|
|
113
87
|
|
|
114
|
-
|
|
88
|
+
function getPostContainers() {
|
|
89
|
+
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
90
|
+
return nodes.filter((el) => {
|
|
91
|
+
if (!el || !el.isConnected) return false;
|
|
92
|
+
if (!el.querySelector(SELECTORS.postContent)) return false;
|
|
93
|
+
const parentPost = el.parentElement && el.parentElement.closest(SELECTORS.postItem);
|
|
94
|
+
if (parentPost && parentPost !== el) return false;
|
|
95
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPlaceholderPresent(id) {
|
|
101
|
+
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
102
|
+
return !!(el && el.isConnected);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function schedule(fn) {
|
|
106
|
+
if (state.scheduled) return;
|
|
107
|
+
state.scheduled = true;
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
state.scheduled = false;
|
|
110
|
+
try { fn(); } catch (e) {}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ----------------------------
|
|
115
|
+
// Ezoic showAds patch (handles arrays, varargs, and filters absent placeholders)
|
|
116
|
+
// ----------------------------
|
|
117
|
+
function patchShowAds() {
|
|
115
118
|
try {
|
|
116
|
-
|
|
119
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
117
120
|
const ez = window.ezstandalone;
|
|
121
|
+
if (ez.__nodebbEzoicPatched) return;
|
|
122
|
+
|
|
123
|
+
// If showAds isn't ready yet, patch when it appears via cmd (no polling)
|
|
124
|
+
const apply = () => {
|
|
125
|
+
try {
|
|
126
|
+
if (!window.ezstandalone || typeof window.ezstandalone.showAds !== 'function') return;
|
|
127
|
+
const ez2 = window.ezstandalone;
|
|
128
|
+
if (ez2.__nodebbEzoicPatched) return;
|
|
129
|
+
|
|
130
|
+
const orig = ez2.showAds;
|
|
131
|
+
ez2.showAds = function () {
|
|
132
|
+
// Normalize ids from:
|
|
133
|
+
// - showAds([1,2])
|
|
134
|
+
// - showAds(1,2,3)
|
|
135
|
+
// - showAds(1)
|
|
136
|
+
const ids = [];
|
|
137
|
+
if (arguments.length === 1 && Array.isArray(arguments[0])) {
|
|
138
|
+
for (const v of arguments[0]) ids.push(v);
|
|
139
|
+
} else {
|
|
140
|
+
for (let i = 0; i < arguments.length; i++) ids.push(arguments[i]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
for (const v of ids) {
|
|
145
|
+
const id = parseInt(v, 10);
|
|
146
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
147
|
+
if (!isPlaceholderPresent(id)) continue;
|
|
148
|
+
seen.add(id);
|
|
149
|
+
try { orig.call(ez2, id); } catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ez2.__nodebbEzoicPatched = true;
|
|
154
|
+
} catch (e) {}
|
|
155
|
+
};
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
157
|
+
apply();
|
|
158
|
+
if (!window.ezstandalone.__nodebbEzoicPatchQueued) {
|
|
159
|
+
window.ezstandalone.__nodebbEzoicPatchQueued = true;
|
|
160
|
+
ez.cmd = ez.cmd || [];
|
|
161
|
+
ez.cmd.push(apply);
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {}
|
|
164
|
+
}
|
|
122
165
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
166
|
+
function safeCmd(token, fn) {
|
|
167
|
+
try {
|
|
168
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
169
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
170
|
+
window.ezstandalone.cmd.push(function () {
|
|
171
|
+
// Drop stale work after ajaxify navigation
|
|
172
|
+
if (token !== state.pageToken) return;
|
|
173
|
+
if (getPageKey() !== state.pageKey) return;
|
|
174
|
+
try { fn(); } catch (e) {}
|
|
129
175
|
});
|
|
130
|
-
|
|
131
|
-
// Re-assign current value through setter
|
|
132
|
-
ez.showAds = _showAds;
|
|
133
|
-
} catch (e) {
|
|
134
|
-
// If defineProperty fails, best-effort wrap is still in place above.
|
|
135
|
-
}
|
|
176
|
+
} catch (e) {}
|
|
136
177
|
}
|
|
137
178
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
179
|
+
function showAd(id) {
|
|
180
|
+
if (!id) return;
|
|
181
|
+
// throttle to avoid repeated calls during rerenders
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
const last = state.lastShowAt.get(id) || 0;
|
|
184
|
+
if (now - last < 1500) return;
|
|
185
|
+
state.lastShowAt.set(id, now);
|
|
186
|
+
|
|
187
|
+
const token = state.pageToken;
|
|
188
|
+
safeCmd(token, () => {
|
|
189
|
+
if (!isPlaceholderPresent(id)) return;
|
|
190
|
+
patchShowAds();
|
|
191
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
192
|
+
window.ezstandalone.showAds(id);
|
|
193
|
+
}
|
|
146
194
|
});
|
|
147
195
|
}
|
|
148
196
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.catch(() => {
|
|
165
|
-
cfg = null;
|
|
166
|
-
return null;
|
|
167
|
-
});
|
|
197
|
+
// ----------------------------
|
|
198
|
+
// DOM insertion (HTML-valid: if target is <li>, wrapper is <li>)
|
|
199
|
+
// ----------------------------
|
|
200
|
+
function buildWrap(id, kindClass, afterPos, liLike) {
|
|
201
|
+
const wrap = document.createElement(liLike ? 'li' : 'div');
|
|
202
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
203
|
+
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
204
|
+
wrap.setAttribute('role', 'presentation');
|
|
205
|
+
wrap.style.width = '100%';
|
|
206
|
+
|
|
207
|
+
// Keep list styling if we're inside list-group
|
|
208
|
+
if (liLike && !wrap.classList.contains('list-group-item')) {
|
|
209
|
+
const targetIsListGroup = wrap.parentElement && wrap.parentElement.classList && wrap.parentElement.classList.contains('list-group');
|
|
210
|
+
// can't detect parent yet; we'll keep it minimal
|
|
211
|
+
}
|
|
168
212
|
|
|
169
|
-
|
|
213
|
+
const ph = document.createElement('div');
|
|
214
|
+
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
215
|
+
wrap.appendChild(ph);
|
|
216
|
+
return wrap;
|
|
170
217
|
}
|
|
171
218
|
|
|
172
|
-
function
|
|
173
|
-
return
|
|
174
|
-
.split(',')
|
|
175
|
-
.map(x => x.trim())
|
|
176
|
-
.filter(Boolean);
|
|
219
|
+
function findWrap(kindClass, afterPos) {
|
|
220
|
+
return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
|
|
177
221
|
}
|
|
178
222
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const Ad = {
|
|
183
|
-
// throttle showAds per placeholder id
|
|
184
|
-
lastShowAt: new Map(),
|
|
185
|
-
minShowIntervalMs: 1500,
|
|
223
|
+
function insertAfter(target, id, kindClass, afterPos) {
|
|
224
|
+
if (!target || !target.insertAdjacentElement) return null;
|
|
225
|
+
if (findWrap(kindClass, afterPos)) return null;
|
|
186
226
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
mo: null,
|
|
227
|
+
// avoid duplicates if already exists
|
|
228
|
+
if (isPlaceholderPresent(id)) return null;
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
initObservers() {
|
|
195
|
-
if (!this.io) {
|
|
196
|
-
this.io = new IntersectionObserver((entries) => {
|
|
197
|
-
for (const e of entries) {
|
|
198
|
-
if (!e.isIntersecting) continue;
|
|
199
|
-
const id = e.target && e.target.getAttribute('data-ezoic-id');
|
|
200
|
-
if (id) this.requestShow(id);
|
|
201
|
-
}
|
|
202
|
-
}, { root: null, rootMargin: '1200px 0px', threshold: 0.01 });
|
|
203
|
-
}
|
|
230
|
+
const liLike = String(target.tagName).toUpperCase() === 'LI';
|
|
231
|
+
const wrap = buildWrap(id, kindClass, afterPos, liLike);
|
|
204
232
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
async scanAndInject() {
|
|
229
|
-
const c = await prefetchConfig();
|
|
230
|
-
if (!c) return;
|
|
231
|
-
|
|
232
|
-
if (c.excluded) return; // HARD stop: never inject for excluded users
|
|
233
|
-
|
|
234
|
-
installShowAdsHook();
|
|
235
|
-
this.initObservers();
|
|
236
|
-
|
|
237
|
-
if (c.enableBetweenAds) this.injectBetweenTopics(c);
|
|
238
|
-
// Extend: categories ads if you use them
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
// NodeBB topic list injection (between li rows)
|
|
242
|
-
injectBetweenTopics(c) {
|
|
243
|
-
// NodeBB 4.x (Harmony) uses: <ul component="category" class="topics-list ...">
|
|
244
|
-
// Older themes may use .topic-list
|
|
245
|
-
const container =
|
|
246
|
-
document.querySelector('ul[component="category"].topics-list') ||
|
|
247
|
-
document.querySelector('ul[component="category"].topic-list') ||
|
|
248
|
-
document.querySelector('ul.topics-list[component="category"]') ||
|
|
249
|
-
document.querySelector('ul.topics-list') ||
|
|
250
|
-
document.querySelector('.category ul.topics-list') ||
|
|
251
|
-
document.querySelector('.category .topic-list') ||
|
|
252
|
-
document.querySelector('.topics .topic-list') ||
|
|
253
|
-
document.querySelector('.topic-list');
|
|
254
|
-
|
|
255
|
-
if (!container) return;
|
|
256
|
-
|
|
257
|
-
let rows = Array.from(container.querySelectorAll(':scope > li')).filter(el => el && el.nodeType === 1);
|
|
258
|
-
if (!rows.length) {
|
|
259
|
-
// Fallback for older markups
|
|
260
|
-
rows = Array.from(container.querySelectorAll('li, .topic-row, .topic-item')).filter(el => el && el.nodeType === 1);
|
|
233
|
+
// If list-group-item exists on target, mirror it to keep theme markup consistent
|
|
234
|
+
if (liLike && target.classList && target.classList.contains('list-group-item')) {
|
|
235
|
+
wrap.classList.add('list-group-item');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
239
|
+
return wrap;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ----------------------------
|
|
243
|
+
// Observers: preload + rerun on NodeBB DOM changes
|
|
244
|
+
// ----------------------------
|
|
245
|
+
function ensureIO() {
|
|
246
|
+
if (state.io) return;
|
|
247
|
+
if (!('IntersectionObserver' in window)) return;
|
|
248
|
+
|
|
249
|
+
state.io = new IntersectionObserver((entries) => {
|
|
250
|
+
for (const e of entries) {
|
|
251
|
+
if (!e.isIntersecting) continue;
|
|
252
|
+
const ph = e.target;
|
|
253
|
+
const id = parseInt(String(ph.id).replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
254
|
+
if (Number.isFinite(id)) showAd(id);
|
|
255
|
+
try { state.io.unobserve(ph); } catch (err) {}
|
|
261
256
|
}
|
|
262
|
-
|
|
257
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0 });
|
|
258
|
+
}
|
|
263
259
|
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
function observePlaceholder(id) {
|
|
261
|
+
ensureIO();
|
|
262
|
+
if (!state.io) return;
|
|
263
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
264
|
+
if (!ph) return;
|
|
265
|
+
try { state.io.observe(ph); } catch (e) {}
|
|
266
|
+
}
|
|
266
267
|
|
|
267
|
-
|
|
268
|
+
function ensureMO() {
|
|
269
|
+
if (state.mo) return;
|
|
270
|
+
state.mo = new MutationObserver(() => schedule(runCore));
|
|
271
|
+
try { state.mo.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
|
|
272
|
+
}
|
|
268
273
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
// ----------------------------
|
|
275
|
+
// Config / exclusion
|
|
276
|
+
// ----------------------------
|
|
277
|
+
async function fetchConfig() {
|
|
278
|
+
if (state.cfg) return state.cfg;
|
|
279
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
280
|
+
|
|
281
|
+
state.cfgPromise = (async () => {
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
284
|
+
if (!res.ok) return null;
|
|
285
|
+
const cfg = await res.json();
|
|
286
|
+
state.cfg = cfg;
|
|
287
|
+
return cfg;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
return null;
|
|
290
|
+
} finally {
|
|
291
|
+
state.cfgPromise = null;
|
|
272
292
|
}
|
|
293
|
+
})();
|
|
273
294
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
for (let i = interval; i < rows.length; i += interval) {
|
|
277
|
-
idIndex = (idIndex + 1) % ids.length;
|
|
278
|
-
this.insertAdAfterRow(rows[i - 1], ids[idIndex], `between-topic-${i}`);
|
|
279
|
-
}
|
|
280
|
-
},
|
|
295
|
+
return state.cfgPromise;
|
|
296
|
+
}
|
|
281
297
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
function getUserGroupNamesFromAjaxify() {
|
|
299
|
+
try {
|
|
300
|
+
const ax = window.ajaxify;
|
|
301
|
+
const u = ax && ax.data && (ax.data.user || ax.data.profile || null);
|
|
302
|
+
if (!u) return [];
|
|
303
|
+
// NodeBB varies by route/theme; handle multiple shapes
|
|
304
|
+
const groupsA = u.groups || u.group_names || u.groupNames;
|
|
305
|
+
if (Array.isArray(groupsA)) return groupsA.map(g => (g && g.name) ? g.name : String(g)).filter(Boolean);
|
|
306
|
+
if (typeof groupsA === 'string') return groupsA.split(',').map(s => s.trim()).filter(Boolean);
|
|
307
|
+
} catch (e) {}
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
285
310
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
function parseExcludedGroupsFromCfg(cfg) {
|
|
312
|
+
const v = cfg && (cfg.excludedGroups || cfg.excludedGroupNames || cfg.excluded_groups);
|
|
313
|
+
if (!v) return [];
|
|
314
|
+
if (Array.isArray(v)) return v.map(x => (x && x.name) ? x.name : String(x)).filter(Boolean);
|
|
315
|
+
return String(v).split(',').map(s => s.trim()).filter(Boolean);
|
|
316
|
+
}
|
|
291
317
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
wrapper.setAttribute('role', 'presentation');
|
|
296
|
-
wrapper.setAttribute('data-ezoic-id', placeholderId);
|
|
318
|
+
function isExcludedClientSide(cfg) {
|
|
319
|
+
// Prefer server decision if present
|
|
320
|
+
if (cfg && cfg.excluded === true) return true;
|
|
297
321
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
322
|
+
// Extra safety: if cfg contains excluded group names, cross-check client-side.
|
|
323
|
+
const excludedGroups = parseExcludedGroupsFromCfg(cfg);
|
|
324
|
+
if (!excludedGroups.length) return false;
|
|
325
|
+
const userGroups = getUserGroupNamesFromAjaxify();
|
|
326
|
+
if (!userGroups.length) return false;
|
|
302
327
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
328
|
+
const set = new Set(userGroups);
|
|
329
|
+
return excludedGroups.some(g => set.has(g));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ----------------------------
|
|
333
|
+
// Core injection
|
|
334
|
+
// ----------------------------
|
|
335
|
+
function computeTargets(count, interval, showFirst) {
|
|
336
|
+
const out = [];
|
|
337
|
+
if (count <= 0) return out;
|
|
338
|
+
if (showFirst) out.push(1);
|
|
339
|
+
for (let i = interval; i <= count; i += interval) out.push(i);
|
|
340
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
341
|
+
}
|
|
307
342
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
function injectBetween(kindClass, items, interval, showFirst, pool) {
|
|
344
|
+
if (!items.length || !pool.length) return [];
|
|
345
|
+
|
|
346
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
347
|
+
const insertedIds = [];
|
|
348
|
+
for (const afterPos of targets) {
|
|
349
|
+
const el = items[afterPos - 1];
|
|
350
|
+
if (!el || !el.isConnected) continue;
|
|
351
|
+
if (!pool.length) break;
|
|
352
|
+
if (findWrap(kindClass, afterPos)) continue;
|
|
353
|
+
|
|
354
|
+
const id = pool.shift();
|
|
355
|
+
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
356
|
+
if (!wrap) {
|
|
357
|
+
// push back if couldn't insert
|
|
358
|
+
pool.unshift(id);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
311
361
|
|
|
312
|
-
|
|
313
|
-
if (this.io) this.io.observe(wrapper);
|
|
362
|
+
insertedIds.push(id);
|
|
314
363
|
|
|
315
|
-
//
|
|
316
|
-
const rect =
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
// Above-the-fold: immediate attempt
|
|
365
|
+
const rect = wrap.getBoundingClientRect ? wrap.getBoundingClientRect() : null;
|
|
366
|
+
const vh = window.innerHeight || 800;
|
|
367
|
+
if (rect && rect.top < (vh * 1.5)) {
|
|
368
|
+
showAd(id);
|
|
369
|
+
} else {
|
|
370
|
+
observePlaceholder(id);
|
|
319
371
|
}
|
|
320
|
-
}
|
|
372
|
+
}
|
|
373
|
+
return insertedIds;
|
|
374
|
+
}
|
|
321
375
|
|
|
322
|
-
|
|
323
|
-
|
|
376
|
+
function removeAllAds() {
|
|
377
|
+
try { document.querySelectorAll(`.${WRAP_CLASS}`).forEach(n => n.remove()); } catch (e) {}
|
|
378
|
+
}
|
|
324
379
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if ((t - last) < this.minShowIntervalMs) return;
|
|
328
|
-
this.lastShowAt.set(placeholderId, t);
|
|
380
|
+
async function runCore() {
|
|
381
|
+
state.pageKey = getPageKey();
|
|
329
382
|
|
|
330
|
-
|
|
331
|
-
Pool.ensurePlaceholder(placeholderId);
|
|
383
|
+
patchShowAds();
|
|
332
384
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (!window.ezstandalone || !window.ezstandalone.showAds) return;
|
|
336
|
-
// showAds is wrapped; calling with one id is safest
|
|
337
|
-
window.ezstandalone.showAds(placeholderId);
|
|
338
|
-
};
|
|
385
|
+
const cfg = await fetchConfig();
|
|
386
|
+
if (!cfg) return;
|
|
339
387
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
388
|
+
// If excluded: ensure we remove any previously injected wrappers
|
|
389
|
+
if (isExcludedClientSide(cfg)) {
|
|
390
|
+
removeAllAds();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const kind = getKind();
|
|
395
|
+
|
|
396
|
+
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
397
|
+
const pool = parsePool(cfg.messagePlaceholderIds);
|
|
398
|
+
injectBetween(
|
|
399
|
+
'ezoic-ad-message',
|
|
400
|
+
getPostContainers(),
|
|
401
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
402
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
403
|
+
pool
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
409
|
+
const pool = parsePool(cfg.placeholderIds);
|
|
410
|
+
injectBetween(
|
|
411
|
+
'ezoic-ad-between',
|
|
412
|
+
getTopicItems(),
|
|
413
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
414
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
415
|
+
pool
|
|
416
|
+
);
|
|
417
|
+
return;
|
|
357
418
|
}
|
|
358
|
-
};
|
|
359
419
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
420
|
+
if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
421
|
+
const pool = parsePool(cfg.categoryPlaceholderIds);
|
|
422
|
+
injectBetween(
|
|
423
|
+
'ezoic-ad-categories',
|
|
424
|
+
getCategoryItems(),
|
|
425
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
426
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
427
|
+
pool
|
|
428
|
+
);
|
|
429
|
+
}
|
|
367
430
|
}
|
|
368
431
|
|
|
369
|
-
function
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
432
|
+
function cleanupForNav() {
|
|
433
|
+
// New token => any pending safeCmd work becomes stale
|
|
434
|
+
state.pageToken += 1;
|
|
435
|
+
state.cfg = null;
|
|
436
|
+
state.cfgPromise = null;
|
|
437
|
+
state.lastShowAt.clear();
|
|
438
|
+
|
|
439
|
+
// Disconnect observers for old DOM
|
|
440
|
+
try { if (state.io) state.io.disconnect(); } catch (e) {}
|
|
441
|
+
state.io = null;
|
|
442
|
+
try { if (state.mo) state.mo.disconnect(); } catch (e) {}
|
|
443
|
+
state.mo = null;
|
|
444
|
+
|
|
445
|
+
removeAllAds();
|
|
373
446
|
}
|
|
374
447
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
448
|
+
// ----------------------------
|
|
449
|
+
// Bind to NodeBB 4.x ajaxify events
|
|
450
|
+
// ----------------------------
|
|
451
|
+
function bind() {
|
|
452
|
+
if (!$) return;
|
|
378
453
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
454
|
+
$(window).off('.ezoicInfinite');
|
|
455
|
+
|
|
456
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => cleanupForNav());
|
|
457
|
+
|
|
458
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
459
|
+
state.pageKey = getPageKey();
|
|
460
|
+
ensureMO();
|
|
461
|
+
schedule(runCore);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Infinite scroll hooks
|
|
465
|
+
$(window).on('action:posts.loaded.ezoicInfinite', () => schedule(runCore));
|
|
466
|
+
$(window).on('action:topics.loaded.ezoicInfinite', () => schedule(runCore));
|
|
467
|
+
$(window).on('action:category.loaded.ezoicInfinite', () => schedule(runCore));
|
|
468
|
+
$(window).on('action:topic.loaded.ezoicInfinite', () => schedule(runCore));
|
|
469
|
+
}
|
|
385
470
|
|
|
386
|
-
|
|
471
|
+
// Boot
|
|
472
|
+
cleanupForNav();
|
|
473
|
+
bind();
|
|
474
|
+
ensureMO();
|
|
475
|
+
state.pageKey = getPageKey();
|
|
476
|
+
schedule(runCore);
|
|
477
|
+
})();
|