nodebb-plugin-ezoic-infinite 1.4.98 → 1.4.99
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 +2 -1
- package/package.json +1 -1
- package/public/client.js +303 -430
- package/public/style.css +7 -21
package/library.js
CHANGED
|
@@ -68,7 +68,7 @@ async function getSettings() {
|
|
|
68
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
69
69
|
if (!uid || !excludedGroups.length) return false;
|
|
70
70
|
const userGroups = await groups.getUserGroups([uid]);
|
|
71
|
-
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : g));
|
|
71
|
+
return (userGroups[0] || []).some(g => excludedGroups.includes((g && g.name) ? g.name : String(g)));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
plugin.onSettingsSet = function (data) {
|
|
@@ -111,6 +111,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
111
111
|
|
|
112
112
|
res.json({
|
|
113
113
|
excluded,
|
|
114
|
+
excludedGroups: settings.excludedGroups,
|
|
114
115
|
enableBetweenAds: settings.enableBetweenAds,
|
|
115
116
|
showFirstTopicAd: settings.showFirstTopicAd,
|
|
116
117
|
placeholderIds: settings.placeholderIds,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,58 +1,50 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// NodeBB client context
|
|
5
4
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
6
5
|
|
|
7
6
|
const WRAP_CLASS = 'ezoic-ad';
|
|
8
7
|
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
9
8
|
|
|
10
|
-
// Insert at most N ads per run to keep the UI smooth on infinite scroll
|
|
11
|
-
const MAX_INSERTS_PER_RUN = 3;
|
|
12
|
-
|
|
13
|
-
// Preload before viewport (tune if you want even earlier)
|
|
14
|
-
const PRELOAD_ROOT_MARGIN = '1200px 0px';
|
|
15
|
-
|
|
16
9
|
const SELECTORS = {
|
|
17
10
|
topicItem: 'li[component="category/topic"]',
|
|
18
|
-
postItem: '[component="post"][data-pid]',
|
|
19
11
|
categoryItem: 'li[component="categories/category"]',
|
|
12
|
+
postItem: '[component="post"][data-pid]',
|
|
13
|
+
postContent: '[component="post/content"]',
|
|
20
14
|
};
|
|
21
15
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
// ----------------------------
|
|
17
|
+
// State
|
|
18
|
+
// ----------------------------
|
|
25
19
|
const state = {
|
|
26
20
|
pageKey: null,
|
|
21
|
+
pageToken: 0,
|
|
22
|
+
cfgPromise: null,
|
|
27
23
|
cfg: null,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
poolCategories: [],
|
|
32
|
-
|
|
33
|
-
usedTopics: new Set(),
|
|
34
|
-
usedPosts: new Set(),
|
|
35
|
-
usedCategories: new Set(),
|
|
36
|
-
|
|
37
|
-
// throttle per placeholder id
|
|
38
|
-
lastShowById: new Map(),
|
|
39
|
-
|
|
40
|
-
// observers / schedulers
|
|
41
|
-
domObs: null,
|
|
24
|
+
// throttle per id
|
|
25
|
+
lastShowAt: new Map(),
|
|
26
|
+
// observed placeholders -> ids
|
|
42
27
|
io: null,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// hero
|
|
46
|
-
heroDoneForPage: false,
|
|
28
|
+
mo: null,
|
|
29
|
+
scheduled: false,
|
|
47
30
|
};
|
|
48
31
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
45
|
|
|
54
46
|
function normalizeBool(v) {
|
|
55
|
-
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
47
|
+
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on' || v === 'yes';
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
function uniqInts(lines) {
|
|
@@ -70,22 +62,7 @@
|
|
|
70
62
|
|
|
71
63
|
function parsePool(raw) {
|
|
72
64
|
if (!raw) return [];
|
|
73
|
-
|
|
74
|
-
.split(/\r?\n/)
|
|
75
|
-
.map(s => s.trim())
|
|
76
|
-
.filter(Boolean);
|
|
77
|
-
return uniqInts(lines);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getPageKey() {
|
|
81
|
-
try {
|
|
82
|
-
const ax = window.ajaxify;
|
|
83
|
-
if (ax && ax.data) {
|
|
84
|
-
if (ax.data.tid) return `topic:${ax.data.tid}`;
|
|
85
|
-
if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
|
|
86
|
-
}
|
|
87
|
-
} catch (e) {}
|
|
88
|
-
return window.location.pathname;
|
|
65
|
+
return uniqInts(String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean));
|
|
89
66
|
}
|
|
90
67
|
|
|
91
68
|
function getKind() {
|
|
@@ -93,7 +70,6 @@
|
|
|
93
70
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
94
71
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
95
72
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
96
|
-
|
|
97
73
|
// fallback by DOM
|
|
98
74
|
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
99
75
|
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
@@ -113,135 +89,130 @@
|
|
|
113
89
|
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
114
90
|
return nodes.filter((el) => {
|
|
115
91
|
if (!el || !el.isConnected) return false;
|
|
116
|
-
if (!el.querySelector(
|
|
117
|
-
const parentPost = el.parentElement && el.parentElement.closest(
|
|
92
|
+
if (!el.querySelector(SELECTORS.postContent)) return false;
|
|
93
|
+
const parentPost = el.parentElement && el.parentElement.closest(SELECTORS.postItem);
|
|
118
94
|
if (parentPost && parentPost !== el) return false;
|
|
119
95
|
if (el.getAttribute('component') === 'post/parent') return false;
|
|
120
96
|
return true;
|
|
121
97
|
});
|
|
122
98
|
}
|
|
123
99
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
function warmUpNetwork() {
|
|
128
|
-
try {
|
|
129
|
-
const head = document.head || document.getElementsByTagName('head')[0];
|
|
130
|
-
if (!head) return;
|
|
131
|
-
const links = [
|
|
132
|
-
['preconnect', 'https://g.ezoic.net', true],
|
|
133
|
-
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
134
|
-
['preconnect', 'https://go.ezoic.net', true],
|
|
135
|
-
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
136
|
-
];
|
|
137
|
-
for (const [rel, href, cors] of links) {
|
|
138
|
-
const key = `${rel}|${href}`;
|
|
139
|
-
if (_warmLinksDone.has(key)) continue;
|
|
140
|
-
_warmLinksDone.add(key);
|
|
141
|
-
const link = document.createElement('link');
|
|
142
|
-
link.rel = rel;
|
|
143
|
-
link.href = href;
|
|
144
|
-
if (cors) link.crossOrigin = 'anonymous';
|
|
145
|
-
head.appendChild(link);
|
|
146
|
-
}
|
|
147
|
-
} catch (e) {}
|
|
100
|
+
function isPlaceholderPresent(id) {
|
|
101
|
+
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
102
|
+
return !!(el && el.isConnected);
|
|
148
103
|
}
|
|
149
104
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (typeof ez.showAds !== 'function') return;
|
|
158
|
-
|
|
159
|
-
window.__nodebbEzoicPatched = true;
|
|
160
|
-
const orig = ez.showAds;
|
|
161
|
-
|
|
162
|
-
ez.showAds = function (...args) {
|
|
163
|
-
if (EZOIC_BLOCKED) return;
|
|
164
|
-
|
|
165
|
-
let ids = [];
|
|
166
|
-
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
167
|
-
else ids = args;
|
|
168
|
-
|
|
169
|
-
const seen = new Set();
|
|
170
|
-
for (const v of ids) {
|
|
171
|
-
const id = parseInt(v, 10);
|
|
172
|
-
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
173
|
-
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
174
|
-
if (!ph || !ph.isConnected) continue;
|
|
175
|
-
seen.add(id);
|
|
176
|
-
try { orig.call(ez, id); } catch (e) {}
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
} catch (e) {}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
applyPatch();
|
|
183
|
-
if (!window.__nodebbEzoicPatched) {
|
|
184
|
-
try {
|
|
185
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
186
|
-
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
187
|
-
window.ezstandalone.cmd.push(applyPatch);
|
|
188
|
-
} catch (e) {}
|
|
189
|
-
}
|
|
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
|
+
});
|
|
190
112
|
}
|
|
191
113
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
114
|
+
// ----------------------------
|
|
115
|
+
// Ezoic showAds patch (handles arrays, varargs, and filters absent placeholders)
|
|
116
|
+
// ----------------------------
|
|
117
|
+
function patchShowAds() {
|
|
196
118
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
119
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
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
|
+
};
|
|
156
|
+
|
|
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) {}
|
|
204
164
|
}
|
|
205
165
|
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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) {}
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {}
|
|
211
177
|
}
|
|
212
178
|
|
|
213
|
-
|
|
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);
|
|
214
186
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
}
|
|
194
|
+
});
|
|
222
195
|
}
|
|
223
196
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// preserve common NodeBB list styling
|
|
230
|
-
if (target && target.classList && target.classList.contains('list-group-item')) wrap.classList.add('list-group-item');
|
|
231
|
-
}
|
|
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');
|
|
232
202
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
233
|
-
if (wrap.tagName === 'LI') {
|
|
234
|
-
wrap.setAttribute('role', 'presentation');
|
|
235
|
-
wrap.setAttribute('aria-hidden', 'true');
|
|
236
|
-
}
|
|
237
203
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
204
|
+
wrap.setAttribute('role', 'presentation');
|
|
238
205
|
wrap.style.width = '100%';
|
|
239
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
|
+
}
|
|
212
|
+
|
|
240
213
|
const ph = document.createElement('div');
|
|
241
214
|
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
242
|
-
ph.setAttribute('data-ezoic-id', String(id));
|
|
243
215
|
wrap.appendChild(ph);
|
|
244
|
-
|
|
245
216
|
return wrap;
|
|
246
217
|
}
|
|
247
218
|
|
|
@@ -252,353 +223,255 @@
|
|
|
252
223
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
253
224
|
if (!target || !target.insertAdjacentElement) return null;
|
|
254
225
|
if (findWrap(kindClass, afterPos)) return null;
|
|
255
|
-
if (insertingIds.has(id)) return null;
|
|
256
226
|
|
|
257
|
-
|
|
258
|
-
if (
|
|
227
|
+
// avoid duplicates if already exists
|
|
228
|
+
if (isPlaceholderPresent(id)) return null;
|
|
259
229
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
insertingIds.delete(id);
|
|
230
|
+
const liLike = String(target.tagName).toUpperCase() === 'LI';
|
|
231
|
+
const wrap = buildWrap(id, kindClass, afterPos, liLike);
|
|
232
|
+
|
|
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');
|
|
267
236
|
}
|
|
268
|
-
}
|
|
269
237
|
|
|
270
|
-
|
|
271
|
-
return
|
|
238
|
+
target.insertAdjacentElement('afterend', wrap);
|
|
239
|
+
return wrap;
|
|
272
240
|
}
|
|
273
241
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
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) {}
|
|
256
|
+
}
|
|
257
|
+
}, { root: null, rootMargin: '1200px 0px', threshold: 0 });
|
|
258
|
+
}
|
|
280
259
|
|
|
260
|
+
function observePlaceholder(id) {
|
|
261
|
+
ensureIO();
|
|
262
|
+
if (!state.io) return;
|
|
281
263
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
282
|
-
if (!ph
|
|
264
|
+
if (!ph) return;
|
|
265
|
+
try { state.io.observe(ph); } catch (e) {}
|
|
266
|
+
}
|
|
283
267
|
|
|
284
|
-
|
|
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
|
+
}
|
|
285
273
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
274
|
+
// ----------------------------
|
|
275
|
+
// Config / exclusion
|
|
276
|
+
// ----------------------------
|
|
277
|
+
async function fetchConfig() {
|
|
278
|
+
if (state.cfg) return state.cfg;
|
|
279
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
289
280
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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;
|
|
295
292
|
}
|
|
293
|
+
})();
|
|
296
294
|
|
|
297
|
-
|
|
298
|
-
ez.cmd = ez.cmd || [];
|
|
299
|
-
if (!ph.__ezoicQueued) {
|
|
300
|
-
ph.__ezoicQueued = true;
|
|
301
|
-
ez.cmd.push(() => {
|
|
302
|
-
try {
|
|
303
|
-
if (EZOIC_BLOCKED) return;
|
|
304
|
-
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
305
|
-
if (!el || !el.isConnected) return;
|
|
306
|
-
window.ezstandalone.showAds(id);
|
|
307
|
-
sessionDefinedIds.add(id);
|
|
308
|
-
} catch (e) {}
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
} catch (e) {}
|
|
295
|
+
return state.cfgPromise;
|
|
312
296
|
}
|
|
313
297
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
function ensurePreloadObserver() {
|
|
317
|
-
if (state.io) return state.io;
|
|
298
|
+
function getUserGroupNamesFromAjaxify() {
|
|
318
299
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
}, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
|
|
330
|
-
} catch (e) {
|
|
331
|
-
state.io = null;
|
|
332
|
-
}
|
|
333
|
-
return state.io;
|
|
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 [];
|
|
334
309
|
}
|
|
335
310
|
|
|
336
|
-
function
|
|
337
|
-
const
|
|
338
|
-
if (!
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// If already above fold, fire immediately
|
|
343
|
-
try {
|
|
344
|
-
const r = ph.getBoundingClientRect();
|
|
345
|
-
if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
|
|
346
|
-
} catch (e) {}
|
|
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);
|
|
347
316
|
}
|
|
348
317
|
|
|
349
|
-
|
|
318
|
+
function isExcludedClientSide(cfg) {
|
|
319
|
+
// Prefer server decision if present
|
|
320
|
+
if (cfg && cfg.excluded === true) return true;
|
|
350
321
|
|
|
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;
|
|
327
|
+
|
|
328
|
+
const set = new Set(userGroups);
|
|
329
|
+
return excludedGroups.some(g => set.has(g));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ----------------------------
|
|
333
|
+
// Core injection
|
|
334
|
+
// ----------------------------
|
|
351
335
|
function computeTargets(count, interval, showFirst) {
|
|
352
336
|
const out = [];
|
|
353
337
|
if (count <= 0) return out;
|
|
354
338
|
if (showFirst) out.push(1);
|
|
355
|
-
for (let i =
|
|
356
|
-
if (i % interval === 0) out.push(i);
|
|
357
|
-
}
|
|
339
|
+
for (let i = interval; i <= count; i += interval) out.push(i);
|
|
358
340
|
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
359
341
|
}
|
|
360
342
|
|
|
361
|
-
function injectBetween(kindClass, items, interval, showFirst, pool
|
|
362
|
-
if (!items.length) return
|
|
343
|
+
function injectBetween(kindClass, items, interval, showFirst, pool) {
|
|
344
|
+
if (!items.length || !pool.length) return [];
|
|
363
345
|
|
|
364
346
|
const targets = computeTargets(items.length, interval, showFirst);
|
|
365
|
-
|
|
366
|
-
|
|
347
|
+
const insertedIds = [];
|
|
367
348
|
for (const afterPos of targets) {
|
|
368
|
-
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
369
|
-
|
|
370
349
|
const el = items[afterPos - 1];
|
|
371
350
|
if (!el || !el.isConnected) continue;
|
|
372
|
-
if (
|
|
351
|
+
if (!pool.length) break;
|
|
373
352
|
if (findWrap(kindClass, afterPos)) continue;
|
|
374
353
|
|
|
375
|
-
const id =
|
|
376
|
-
if (!id) break;
|
|
377
|
-
|
|
378
|
-
usedSet.add(id);
|
|
354
|
+
const id = pool.shift();
|
|
379
355
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
380
356
|
if (!wrap) {
|
|
381
|
-
|
|
357
|
+
// push back if couldn't insert
|
|
382
358
|
pool.unshift(id);
|
|
383
359
|
continue;
|
|
384
360
|
}
|
|
385
361
|
|
|
386
|
-
|
|
387
|
-
|
|
362
|
+
insertedIds.push(id);
|
|
363
|
+
|
|
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);
|
|
371
|
+
}
|
|
388
372
|
}
|
|
373
|
+
return insertedIds;
|
|
374
|
+
}
|
|
389
375
|
|
|
390
|
-
|
|
376
|
+
function removeAllAds() {
|
|
377
|
+
try { document.querySelectorAll(`.${WRAP_CLASS}`).forEach(n => n.remove()); } catch (e) {}
|
|
391
378
|
}
|
|
392
379
|
|
|
393
|
-
async function
|
|
394
|
-
|
|
395
|
-
const cfg = await fetchConfigOnce();
|
|
396
|
-
if (!cfg || cfg.excluded) return;
|
|
380
|
+
async function runCore() {
|
|
381
|
+
state.pageKey = getPageKey();
|
|
397
382
|
|
|
398
|
-
|
|
383
|
+
patchShowAds();
|
|
399
384
|
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
let pool = null;
|
|
403
|
-
let usedSet = null;
|
|
404
|
-
let kindClass = '';
|
|
385
|
+
const cfg = await fetchConfig();
|
|
386
|
+
if (!cfg) return;
|
|
405
387
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
usedSet = state.usedPosts;
|
|
410
|
-
kindClass = 'ezoic-ad-message';
|
|
411
|
-
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
412
|
-
items = getTopicItems();
|
|
413
|
-
pool = state.poolTopics;
|
|
414
|
-
usedSet = state.usedTopics;
|
|
415
|
-
kindClass = 'ezoic-ad-between';
|
|
416
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
417
|
-
items = getCategoryItems();
|
|
418
|
-
pool = state.poolCategories;
|
|
419
|
-
usedSet = state.usedCategories;
|
|
420
|
-
kindClass = 'ezoic-ad-categories';
|
|
421
|
-
} else {
|
|
388
|
+
// If excluded: ensure we remove any previously injected wrappers
|
|
389
|
+
if (isExcludedClientSide(cfg)) {
|
|
390
|
+
removeAllAds();
|
|
422
391
|
return;
|
|
423
392
|
}
|
|
424
393
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Insert after the very first item (above-the-fold)
|
|
428
|
-
const afterPos = 1;
|
|
429
|
-
const el = items[afterPos - 1];
|
|
430
|
-
if (!el || !el.isConnected) return;
|
|
431
|
-
if (isAdjacentAd(el)) return;
|
|
432
|
-
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
433
|
-
|
|
434
|
-
const id = pickId(pool);
|
|
435
|
-
if (!id) return;
|
|
394
|
+
const kind = getKind();
|
|
436
395
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
);
|
|
442
405
|
return;
|
|
443
406
|
}
|
|
444
407
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (!cfg || cfg.excluded) return;
|
|
456
|
-
initPools(cfg);
|
|
457
|
-
|
|
458
|
-
const kind = getKind();
|
|
459
|
-
|
|
460
|
-
if (kind === 'topic') {
|
|
461
|
-
if (normalizeBool(cfg.enableMessageAds)) {
|
|
462
|
-
injectBetween(
|
|
463
|
-
'ezoic-ad-message',
|
|
464
|
-
getPostContainers(),
|
|
465
|
-
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
466
|
-
normalizeBool(cfg.showFirstMessageAd),
|
|
467
|
-
state.poolPosts,
|
|
468
|
-
state.usedPosts
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
} else if (kind === 'categoryTopics') {
|
|
472
|
-
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
473
|
-
injectBetween(
|
|
474
|
-
'ezoic-ad-between',
|
|
475
|
-
getTopicItems(),
|
|
476
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
477
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
478
|
-
state.poolTopics,
|
|
479
|
-
state.usedTopics
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
} else if (kind === 'categories') {
|
|
483
|
-
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
484
|
-
injectBetween(
|
|
485
|
-
'ezoic-ad-categories',
|
|
486
|
-
getCategoryItems(),
|
|
487
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
488
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
489
|
-
state.poolCategories,
|
|
490
|
-
state.usedCategories
|
|
491
|
-
);
|
|
492
|
-
}
|
|
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;
|
|
493
418
|
}
|
|
494
|
-
}
|
|
495
419
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
}
|
|
505
430
|
}
|
|
506
431
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
EZOIC_BLOCKED = true;
|
|
511
|
-
|
|
512
|
-
// remove all wrappers
|
|
513
|
-
try {
|
|
514
|
-
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
515
|
-
try { el.remove(); } catch (e) {}
|
|
516
|
-
});
|
|
517
|
-
} catch (e) {}
|
|
518
|
-
|
|
519
|
-
// reset state
|
|
432
|
+
function cleanupForNav() {
|
|
433
|
+
// New token => any pending safeCmd work becomes stale
|
|
434
|
+
state.pageToken += 1;
|
|
520
435
|
state.cfg = null;
|
|
521
|
-
state.
|
|
522
|
-
state.
|
|
523
|
-
state.poolCategories = [];
|
|
524
|
-
state.usedTopics.clear();
|
|
525
|
-
state.usedPosts.clear();
|
|
526
|
-
state.usedCategories.clear();
|
|
527
|
-
state.lastShowById.clear();
|
|
528
|
-
state.heroDoneForPage = false;
|
|
529
|
-
|
|
530
|
-
sessionDefinedIds.clear();
|
|
531
|
-
|
|
532
|
-
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
533
|
-
}
|
|
436
|
+
state.cfgPromise = null;
|
|
437
|
+
state.lastShowAt.clear();
|
|
534
438
|
|
|
535
|
-
|
|
536
|
-
if (state.
|
|
537
|
-
state.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
} catch (e) {}
|
|
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();
|
|
543
446
|
}
|
|
544
447
|
|
|
545
|
-
|
|
448
|
+
// ----------------------------
|
|
449
|
+
// Bind to NodeBB 4.x ajaxify events
|
|
450
|
+
// ----------------------------
|
|
451
|
+
function bind() {
|
|
546
452
|
if (!$) return;
|
|
547
453
|
|
|
548
454
|
$(window).off('.ezoicInfinite');
|
|
549
455
|
|
|
550
|
-
$(window).on('action:ajaxify.start.ezoicInfinite', () =>
|
|
551
|
-
cleanup();
|
|
552
|
-
});
|
|
456
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => cleanupForNav());
|
|
553
457
|
|
|
554
458
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
555
459
|
state.pageKey = getPageKey();
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
warmUpNetwork();
|
|
559
|
-
patchShowAds();
|
|
560
|
-
ensurePreloadObserver();
|
|
561
|
-
ensureDomObserver();
|
|
562
|
-
|
|
563
|
-
// Ultra-fast above-the-fold first
|
|
564
|
-
insertHeroAdEarly().catch(() => {});
|
|
565
|
-
|
|
566
|
-
// Then normal insertion
|
|
567
|
-
scheduleRun();
|
|
460
|
+
ensureMO();
|
|
461
|
+
schedule(runCore);
|
|
568
462
|
});
|
|
569
463
|
|
|
570
|
-
// Infinite scroll
|
|
571
|
-
$(window).on('action:posts.loaded.ezoicInfinite
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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));
|
|
575
469
|
}
|
|
576
470
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
ticking = true;
|
|
582
|
-
window.requestAnimationFrame(() => {
|
|
583
|
-
ticking = false;
|
|
584
|
-
if (!EZOIC_BLOCKED) scheduleRun();
|
|
585
|
-
});
|
|
586
|
-
}, { passive: true });
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ---------- boot ----------
|
|
590
|
-
|
|
471
|
+
// Boot
|
|
472
|
+
cleanupForNav();
|
|
473
|
+
bind();
|
|
474
|
+
ensureMO();
|
|
591
475
|
state.pageKey = getPageKey();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
ensurePreloadObserver();
|
|
595
|
-
ensureDomObserver();
|
|
596
|
-
|
|
597
|
-
bindNodeBB();
|
|
598
|
-
bindScroll();
|
|
599
|
-
|
|
600
|
-
// First paint: try hero + run
|
|
601
|
-
EZOIC_BLOCKED = false;
|
|
602
|
-
insertHeroAdEarly().catch(() => {});
|
|
603
|
-
scheduleRun();
|
|
604
|
-
})();
|
|
476
|
+
schedule(runCore);
|
|
477
|
+
})();
|
package/public/style.css
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
.ezoic-ad
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
11
|
-
margin: 0 !important;
|
|
12
|
-
padding: 0 !important;
|
|
13
|
-
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
-
.ezoic-ad span.ezoic-ad,
|
|
18
|
-
.ezoic-ad .ezoic-ad {
|
|
19
|
-
margin: 0 !important;
|
|
20
|
-
padding: 0 !important;
|
|
21
|
-
}
|
|
1
|
+
.ezoic-ad,
|
|
2
|
+
.ezoic-ad *,
|
|
3
|
+
span.ezoic-ad,
|
|
4
|
+
span[class*="ezoic"] {
|
|
5
|
+
min-height: 0 !important;
|
|
6
|
+
min-width: 0 !important;
|
|
7
|
+
}
|