nodebb-plugin-ezoic-infinite 1.5.48 → 1.5.50
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 +21 -5
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/public/admin.js +2 -3
- package/public/client.js +792 -193
- package/public/style.css +38 -3
package/library.js
CHANGED
|
@@ -27,13 +27,21 @@ async function getAllGroups() {
|
|
|
27
27
|
}
|
|
28
28
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
29
|
const data = await groups.getGroupsData(filtered);
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
31
|
+
const valid = data.filter(g => g && g.name);
|
|
32
|
+
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
33
|
+
return valid;
|
|
33
34
|
}
|
|
35
|
+
let _settingsCache = null;
|
|
36
|
+
let _settingsCacheAt = 0;
|
|
37
|
+
const SETTINGS_TTL = 30000; // 30s
|
|
38
|
+
|
|
34
39
|
async function getSettings() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
35
42
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
36
|
-
|
|
43
|
+
_settingsCacheAt = Date.now();
|
|
44
|
+
_settingsCache = {
|
|
37
45
|
// Between-post ads (simple blocks) in category topic list
|
|
38
46
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
39
47
|
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
@@ -54,6 +62,7 @@ async function getSettings() {
|
|
|
54
62
|
|
|
55
63
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
56
64
|
};
|
|
65
|
+
return _settingsCache;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
async function isUserExcluded(uid, excludedGroups) {
|
|
@@ -62,6 +71,13 @@ async function isUserExcluded(uid, excludedGroups) {
|
|
|
62
71
|
return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
plugin.onSettingsSet = function (data) {
|
|
75
|
+
// Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
|
|
76
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
77
|
+
_settingsCache = null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
plugin.addAdminNavigation = async (header) => {
|
|
66
82
|
header.plugins = header.plugins || [];
|
|
67
83
|
header.plugins.push({
|
|
@@ -89,7 +105,7 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
89
105
|
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
90
106
|
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
91
107
|
|
|
92
|
-
router.get('/api/plugins/ezoic-infinite/config',
|
|
108
|
+
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
93
109
|
const settings = await getSettings();
|
|
94
110
|
const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
|
|
95
111
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
e.preventDefault();
|
|
14
14
|
|
|
15
15
|
Settings.save('ezoic-infinite', $form, function () {
|
|
16
|
-
// Toast vert (NodeBB core)
|
|
17
16
|
if (alerts && typeof alerts.success === 'function') {
|
|
18
|
-
alerts.success('
|
|
17
|
+
alerts.success('[[admin/settings:saved]]');
|
|
19
18
|
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
20
|
-
window.app.alertSuccess('
|
|
19
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
21
20
|
}
|
|
22
21
|
});
|
|
23
22
|
});
|
package/public/client.js
CHANGED
|
@@ -1,43 +1,177 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// NodeBB client context
|
|
4
5
|
const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
6
|
|
|
7
|
+
const WRAP_CLASS = 'ezoic-ad';
|
|
8
|
+
const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
9
|
+
|
|
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 (earlier load for smoother scroll)
|
|
14
|
+
const PRELOAD_MARGIN_DESKTOP = '2600px 0px 2600px 0px';
|
|
15
|
+
const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
|
|
16
|
+
|
|
17
|
+
// When the user scrolls very fast, temporarily preload more aggressively.
|
|
18
|
+
// This helps ensure ads are already in-flight before the user reaches them.
|
|
19
|
+
const PRELOAD_MARGIN_DESKTOP_BOOST = '4200px 0px 4200px 0px';
|
|
20
|
+
const PRELOAD_MARGIN_MOBILE_BOOST = '2500px 0px 2500px 0px';
|
|
21
|
+
const BOOST_DURATION_MS = 2500;
|
|
22
|
+
const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
|
|
23
|
+
|
|
24
|
+
const MAX_INFLIGHT_DESKTOP = 4;
|
|
25
|
+
const MAX_INFLIGHT_MOBILE = 3;
|
|
26
|
+
|
|
27
|
+
function isBoosted() {
|
|
28
|
+
try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isMobile() {
|
|
32
|
+
try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPreloadRootMargin() {
|
|
36
|
+
if (isMobile()) return isBoosted() ? PRELOAD_MARGIN_MOBILE_BOOST : PRELOAD_MARGIN_MOBILE;
|
|
37
|
+
return isBoosted() ? PRELOAD_MARGIN_DESKTOP_BOOST : PRELOAD_MARGIN_DESKTOP;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getMaxInflight() {
|
|
41
|
+
const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
|
|
42
|
+
return base + (isBoosted() ? 1 : 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
const SELECTORS = {
|
|
7
46
|
topicItem: 'li[component="category/topic"]',
|
|
8
47
|
postItem: '[component="post"][data-pid]',
|
|
9
48
|
categoryItem: 'li[component="categories/category"]',
|
|
10
49
|
};
|
|
11
50
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
51
|
+
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
52
|
+
let blockedUntil = 0;
|
|
53
|
+
function isBlocked() {
|
|
54
|
+
return Date.now() < blockedUntil;
|
|
55
|
+
}
|
|
15
56
|
|
|
16
57
|
const state = {
|
|
17
58
|
pageKey: null,
|
|
18
59
|
cfg: null,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
|
|
61
|
+
// Full lists (never consumed) + cursors for round-robin reuse
|
|
62
|
+
allTopics: [],
|
|
63
|
+
allPosts: [],
|
|
64
|
+
allCategories: [],
|
|
65
|
+
curTopics: 0,
|
|
66
|
+
curPosts: 0,
|
|
67
|
+
curCategories: 0,
|
|
68
|
+
|
|
69
|
+
// throttle per placeholder id
|
|
26
70
|
lastShowById: new Map(),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
internalDomChange: 0,
|
|
72
|
+
lastRecycleAt: { topic: 0, categoryTopics: 0, categories: 0 },
|
|
73
|
+
|
|
74
|
+
// track placeholders that have been shown at least once in this pageview
|
|
75
|
+
usedOnce: new Set(),
|
|
76
|
+
|
|
77
|
+
// observers / schedulers
|
|
78
|
+
domObs: null,
|
|
79
|
+
io: null,
|
|
80
|
+
runQueued: false,
|
|
81
|
+
|
|
82
|
+
// preloading budget
|
|
83
|
+
inflight: 0,
|
|
84
|
+
pending: [],
|
|
85
|
+
pendingSet: new Set(),
|
|
86
|
+
|
|
87
|
+
// fast scroll boosting
|
|
88
|
+
scrollBoostUntil: 0,
|
|
89
|
+
lastScrollY: 0,
|
|
90
|
+
lastScrollTs: 0,
|
|
91
|
+
ioMargin: null,
|
|
92
|
+
|
|
93
|
+
// hero)
|
|
94
|
+
heroDoneForPage: false,
|
|
31
95
|
};
|
|
32
96
|
|
|
33
|
-
const
|
|
97
|
+
const insertingIds = new Set();
|
|
98
|
+
|
|
99
|
+
// ---------- lightweight "fade-in" for ad iframes ----------
|
|
100
|
+
// This reduces the perception of "flashing" when a slot appears empty then fills.
|
|
101
|
+
// We avoid scroll listeners and only react to DOM insertions.
|
|
102
|
+
const _faded = new WeakSet();
|
|
103
|
+
function _fadeInIframe(iframe) {
|
|
104
|
+
try {
|
|
105
|
+
if (!iframe || _faded.has(iframe)) return;
|
|
106
|
+
_faded.add(iframe);
|
|
107
|
+
iframe.style.opacity = '0';
|
|
108
|
+
iframe.style.transition = 'opacity 140ms ease';
|
|
109
|
+
// Next frame: show
|
|
110
|
+
requestAnimationFrame(() => {
|
|
111
|
+
try { iframe.style.opacity = '1'; } catch (e) {}
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const _adFillObserver = new MutationObserver((muts) => {
|
|
117
|
+
try {
|
|
118
|
+
for (const m of muts) {
|
|
119
|
+
if (m.addedNodes && m.addedNodes.length) {
|
|
120
|
+
for (const n of m.addedNodes) {
|
|
121
|
+
if (!n || n.nodeType !== 1) continue;
|
|
122
|
+
if (n.tagName === 'IFRAME') {
|
|
123
|
+
const w = n.closest && n.closest(`.${WRAP_CLASS}`);
|
|
124
|
+
if (w) _fadeInIframe(n);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// If a subtree is added, look for iframes inside ad wrappers only.
|
|
128
|
+
const ifs = n.querySelectorAll ? n.querySelectorAll(`.${WRAP_CLASS} iframe`) : null;
|
|
129
|
+
if (ifs && ifs.length) ifs.forEach(_fadeInIframe);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
_adFillObserver.observe(document.documentElement, { subtree: true, childList: true });
|
|
138
|
+
} catch (e) {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
function markEmptyWrapper(id) {
|
|
143
|
+
try {
|
|
144
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
145
|
+
if (!ph || !ph.isConnected) return;
|
|
146
|
+
const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
|
|
147
|
+
if (!wrap) return;
|
|
148
|
+
// If still empty after a delay, collapse it.
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
try {
|
|
151
|
+
const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
152
|
+
if (!ph2 || !ph2.isConnected) return;
|
|
153
|
+
const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
|
|
154
|
+
if (!w2) return;
|
|
155
|
+
// consider empty if only whitespace and no iframes/ins/img
|
|
156
|
+
const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
|
|
157
|
+
if (!hasAd) w2.classList.add('is-empty');
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}, 3500);
|
|
160
|
+
} catch (e) {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Production build: debug disabled
|
|
164
|
+
function dbg() {}
|
|
165
|
+
|
|
166
|
+
// ---------- small utils ----------
|
|
34
167
|
|
|
35
168
|
function normalizeBool(v) {
|
|
36
169
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
37
170
|
}
|
|
38
171
|
|
|
39
172
|
function uniqInts(lines) {
|
|
40
|
-
const out = []
|
|
173
|
+
const out = [];
|
|
174
|
+
const seen = new Set();
|
|
41
175
|
for (const v of lines) {
|
|
42
176
|
const n = parseInt(v, 10);
|
|
43
177
|
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
@@ -50,7 +184,10 @@
|
|
|
50
184
|
|
|
51
185
|
function parsePool(raw) {
|
|
52
186
|
if (!raw) return [];
|
|
53
|
-
const lines = String(raw)
|
|
187
|
+
const lines = String(raw)
|
|
188
|
+
.split(/\r?\n/)
|
|
189
|
+
.map(s => s.trim())
|
|
190
|
+
.filter(Boolean);
|
|
54
191
|
return uniqInts(lines);
|
|
55
192
|
}
|
|
56
193
|
|
|
@@ -70,6 +207,11 @@
|
|
|
70
207
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
208
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
209
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
210
|
+
|
|
211
|
+
// fallback by DOM
|
|
212
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
213
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
214
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
73
215
|
return 'other';
|
|
74
216
|
}
|
|
75
217
|
|
|
@@ -77,76 +219,234 @@
|
|
|
77
219
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
78
220
|
}
|
|
79
221
|
|
|
222
|
+
function getCategoryItems() {
|
|
223
|
+
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
224
|
+
}
|
|
225
|
+
|
|
80
226
|
function getPostContainers() {
|
|
81
|
-
|
|
227
|
+
const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
228
|
+
return nodes.filter((el) => {
|
|
229
|
+
if (!el || !el.isConnected) return false;
|
|
230
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
231
|
+
const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
232
|
+
if (parentPost && parentPost !== el) return false;
|
|
233
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
82
236
|
}
|
|
83
237
|
|
|
84
|
-
|
|
85
|
-
|
|
238
|
+
// ---------- warm-up & patching ----------
|
|
239
|
+
|
|
240
|
+
const _warmLinksDone = new Set();
|
|
241
|
+
function warmUpNetwork() {
|
|
242
|
+
try {
|
|
243
|
+
const head = document.head || document.getElementsByTagName('head')[0];
|
|
244
|
+
if (!head) return;
|
|
245
|
+
const links = [
|
|
246
|
+
['preconnect', 'https://g.ezoic.net', true],
|
|
247
|
+
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
248
|
+
['preconnect', 'https://go.ezoic.net', true],
|
|
249
|
+
['dns-prefetch', 'https://go.ezoic.net', false],
|
|
250
|
+
|
|
251
|
+
// Google ad stack (helps Safeframe/GPT warm up)
|
|
252
|
+
['preconnect', 'https://pagead2.googlesyndication.com', true],
|
|
253
|
+
['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
|
|
254
|
+
['preconnect', 'https://securepubads.g.doubleclick.net', true],
|
|
255
|
+
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
256
|
+
['preconnect', 'https://tpc.googlesyndication.com', true],
|
|
257
|
+
['dns-prefetch', 'https://tpc.googlesyndication.com', false],
|
|
258
|
+
['preconnect', 'https://googleads.g.doubleclick.net', true],
|
|
259
|
+
['dns-prefetch', 'https://googleads.g.doubleclick.net', false],
|
|
260
|
+
['preconnect', 'https://static.doubleclick.net', true],
|
|
261
|
+
['dns-prefetch', 'https://static.doubleclick.net', false],
|
|
262
|
+
];
|
|
263
|
+
for (const [rel, href, cors] of links) {
|
|
264
|
+
const key = `${rel}|${href}`;
|
|
265
|
+
if (_warmLinksDone.has(key)) continue;
|
|
266
|
+
_warmLinksDone.add(key);
|
|
267
|
+
const link = document.createElement('link');
|
|
268
|
+
link.rel = rel;
|
|
269
|
+
link.href = href;
|
|
270
|
+
if (cors) link.crossOrigin = 'anonymous';
|
|
271
|
+
head.appendChild(link);
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {}
|
|
86
274
|
}
|
|
87
275
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
276
|
+
// Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
|
|
277
|
+
function patchShowAds() {
|
|
278
|
+
const applyPatch = () => {
|
|
279
|
+
try {
|
|
280
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
281
|
+
const ez = window.ezstandalone;
|
|
282
|
+
if (window.__nodebbEzoicPatched) return;
|
|
283
|
+
if (typeof ez.showAds !== 'function') return;
|
|
284
|
+
|
|
285
|
+
window.__nodebbEzoicPatched = true;
|
|
286
|
+
const orig = ez.showAds;
|
|
287
|
+
|
|
288
|
+
ez.showAds = function (...args) {
|
|
289
|
+
if (isBlocked()) return;
|
|
290
|
+
|
|
291
|
+
let ids = [];
|
|
292
|
+
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
293
|
+
else ids = args;
|
|
294
|
+
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const v of ids) {
|
|
297
|
+
const id = parseInt(v, 10);
|
|
298
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
299
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
300
|
+
if (!ph || !ph.isConnected) continue;
|
|
301
|
+
seen.add(id);
|
|
302
|
+
try { orig.call(ez, id); } catch (e) {}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
} catch (e) {}
|
|
306
|
+
};
|
|
91
307
|
|
|
92
|
-
|
|
308
|
+
applyPatch();
|
|
309
|
+
if (!window.__nodebbEzoicPatched) {
|
|
93
310
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
})();
|
|
311
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
312
|
+
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
313
|
+
window.ezstandalone.cmd.push(applyPatch);
|
|
314
|
+
} catch (e) {}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
103
317
|
|
|
104
|
-
|
|
318
|
+
const RECYCLE_COOLDOWN_MS = 1500;
|
|
319
|
+
|
|
320
|
+
function kindKeyFromClass(kindClass) {
|
|
321
|
+
if (kindClass === 'ezoic-ad-message') return 'topic';
|
|
322
|
+
if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
|
|
323
|
+
if (kindClass === 'ezoic-ad-categories') return 'categories';
|
|
324
|
+
return 'topic';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function withInternalDomChange(fn) {
|
|
328
|
+
state.internalDomChange++;
|
|
329
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function canRecycle(kind) {
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
const last = state.lastRecycleAt[kind] || 0;
|
|
335
|
+
if (now - last < RECYCLE_COOLDOWN_MS) return false;
|
|
336
|
+
state.lastRecycleAt[kind] = now;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
// ---------- config & pools ----------
|
|
340
|
+
|
|
341
|
+
async function fetchConfigOnce() {
|
|
342
|
+
if (state.cfg) return state.cfg;
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
345
|
+
if (!res.ok) return null;
|
|
346
|
+
state.cfg = await res.json();
|
|
347
|
+
return state.cfg;
|
|
348
|
+
} catch (e) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
105
351
|
}
|
|
106
352
|
|
|
107
353
|
function initPools(cfg) {
|
|
108
|
-
if (!
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
354
|
+
if (!cfg) return;
|
|
355
|
+
if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
|
|
356
|
+
if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
357
|
+
if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
111
358
|
}
|
|
112
359
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!
|
|
360
|
+
// ---------- insertion primitives ----------
|
|
361
|
+
|
|
362
|
+
function isAdjacentAd(target) {
|
|
363
|
+
if (!target) return false;
|
|
364
|
+
const next = target.nextElementSibling;
|
|
365
|
+
if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
|
|
366
|
+
const prev = target.previousElementSibling;
|
|
367
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
117
370
|
|
|
371
|
+
|
|
372
|
+
function getWrapIdFromWrap(wrap) {
|
|
118
373
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
374
|
+
const v = wrap.getAttribute('data-ezoic-wrapid');
|
|
375
|
+
if (v) return String(v);
|
|
376
|
+
const ph = wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
377
|
+
if (ph && ph.id) return ph.id.replace(PLACEHOLDER_PREFIX, '');
|
|
378
|
+
} catch (e) {}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
127
381
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
382
|
+
function safeDestroyById(id) {
|
|
383
|
+
try {
|
|
384
|
+
const ez = window.ezstandalone;
|
|
385
|
+
if (ez && typeof ez.destroyPlaceholders === 'function') {
|
|
386
|
+
ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
|
|
132
387
|
}
|
|
133
|
-
|
|
134
|
-
// Recyclage: libérer après 100ms
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
filtered.forEach(id => sessionDefinedIds.delete(id));
|
|
137
|
-
}, 100);
|
|
138
388
|
} catch (e) {}
|
|
139
389
|
}
|
|
140
390
|
|
|
141
|
-
function
|
|
391
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
392
|
+
if (!items || !items.length) return 0;
|
|
393
|
+
const itemSet = new Set(items);
|
|
394
|
+
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
395
|
+
let removed = 0;
|
|
396
|
+
|
|
397
|
+
wraps.forEach((wrap) => {
|
|
398
|
+
// NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
|
|
399
|
+
let ok = false;
|
|
400
|
+
let prev = wrap.previousElementSibling;
|
|
401
|
+
for (let i = 0; i < 3 && prev; i++) {
|
|
402
|
+
if (itemSet.has(prev)) { ok = true; break; }
|
|
403
|
+
prev = prev.previousElementSibling;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!ok) {
|
|
407
|
+
const id = getWrapIdFromWrap(wrap);
|
|
408
|
+
withInternalDomChange(() => {
|
|
409
|
+
try {
|
|
410
|
+
if (id) safeDestroyById(id);
|
|
411
|
+
wrap.remove();
|
|
412
|
+
} catch (e) {}
|
|
413
|
+
});
|
|
414
|
+
removed++;
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
419
|
+
return removed;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function refreshEmptyState(id) {
|
|
423
|
+
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
424
|
+
window.setTimeout(() => {
|
|
425
|
+
try {
|
|
426
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
427
|
+
if (!ph || !ph.isConnected) return;
|
|
428
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
429
|
+
if (!wrap) return;
|
|
430
|
+
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
431
|
+
if (hasContent) wrap.classList.remove('is-empty');
|
|
432
|
+
else wrap.classList.add('is-empty');
|
|
433
|
+
} catch (e) {}
|
|
434
|
+
}, 3500);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
142
438
|
const wrap = document.createElement('div');
|
|
143
439
|
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
144
440
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
441
|
+
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
442
|
+
wrap.setAttribute('data-ezoic-ts', String(Date.now()));
|
|
145
443
|
wrap.style.width = '100%';
|
|
146
444
|
|
|
147
445
|
const ph = document.createElement('div');
|
|
148
446
|
ph.id = `${PLACEHOLDER_PREFIX}${id}`;
|
|
447
|
+
ph.setAttribute('data-ezoic-id', String(id));
|
|
149
448
|
wrap.appendChild(ph);
|
|
449
|
+
|
|
150
450
|
return wrap;
|
|
151
451
|
}
|
|
152
452
|
|
|
@@ -157,215 +457,514 @@
|
|
|
157
457
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
158
458
|
if (!target || !target.insertAdjacentElement) return null;
|
|
159
459
|
if (findWrap(kindClass, afterPos)) return null;
|
|
460
|
+
if (insertingIds.has(id)) return null;
|
|
461
|
+
|
|
462
|
+
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
463
|
+
if (existingPh && existingPh.isConnected) return null;
|
|
160
464
|
|
|
465
|
+
insertingIds.add(id);
|
|
161
466
|
try {
|
|
162
467
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
163
468
|
target.insertAdjacentElement('afterend', wrap);
|
|
164
469
|
return wrap;
|
|
470
|
+
} finally {
|
|
471
|
+
insertingIds.delete(id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function pickIdFromAll(allIds, cursorKey) {
|
|
476
|
+
const n = allIds.length;
|
|
477
|
+
if (!n) return null;
|
|
478
|
+
|
|
479
|
+
// Try at most n ids to find one that's not already in the DOM
|
|
480
|
+
for (let tries = 0; tries < n; tries++) {
|
|
481
|
+
const idx = state[cursorKey] % n;
|
|
482
|
+
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
483
|
+
|
|
484
|
+
const id = allIds[idx];
|
|
485
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
486
|
+
if (ph && ph.isConnected) continue;
|
|
487
|
+
|
|
488
|
+
return id;
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
function removeOneOldWrap(kindClass) {
|
|
495
|
+
try {
|
|
496
|
+
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
497
|
+
if (!wraps.length) return false;
|
|
498
|
+
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
|
|
501
|
+
// Only recycle wraps that are clearly out of view AND old enough.
|
|
502
|
+
// This avoids the "unstable" feeling where ads disappear when the user scrolls back a bit.
|
|
503
|
+
const MIN_AGE_MS = 20000; // 20s
|
|
504
|
+
const OFFSCREEN_PX = -5000; // far above viewport
|
|
505
|
+
|
|
506
|
+
let victim = null;
|
|
507
|
+
for (const w of wraps) {
|
|
508
|
+
const r = w.getBoundingClientRect();
|
|
509
|
+
const ts = parseInt(w.getAttribute('data-ezoic-ts') || '0', 10);
|
|
510
|
+
const ageOk = !ts || (now - ts) >= MIN_AGE_MS;
|
|
511
|
+
if (ageOk && r.bottom < OFFSCREEN_PX) { victim = w; break; }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// If nothing is eligible, do not recycle. We'll simply skip inserting new ads this run.
|
|
515
|
+
if (!victim) return false;
|
|
516
|
+
|
|
517
|
+
// Unobserve placeholder if still observed
|
|
518
|
+
try {
|
|
519
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
520
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
521
|
+
} catch (e) {}
|
|
522
|
+
|
|
523
|
+
victim.remove();
|
|
524
|
+
return true;
|
|
165
525
|
} catch (e) {
|
|
166
|
-
return
|
|
526
|
+
return false;
|
|
167
527
|
}
|
|
168
528
|
}
|
|
169
529
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
530
|
+
function enqueueShow(id) {
|
|
531
|
+
if (!id || isBlocked()) return;
|
|
532
|
+
|
|
533
|
+
// Basic per-id throttle (prevents rapid re-requests when DOM churns)
|
|
534
|
+
const now = Date.now();
|
|
535
|
+
const last = state.lastShowById.get(id) || 0;
|
|
536
|
+
if (now - last < 900) return;
|
|
537
|
+
|
|
538
|
+
const max = getMaxInflight();
|
|
539
|
+
if (state.inflight >= max) {
|
|
540
|
+
if (!state.pendingSet.has(id)) {
|
|
541
|
+
state.pending.push(id);
|
|
542
|
+
state.pendingSet.add(id);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
173
545
|
}
|
|
546
|
+
startShow(id);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function drainQueue() {
|
|
550
|
+
if (isBlocked()) return;
|
|
551
|
+
const max = getMaxInflight();
|
|
552
|
+
while (state.inflight < max && state.pending.length) {
|
|
553
|
+
const id = state.pending.shift();
|
|
554
|
+
state.pendingSet.delete(id);
|
|
555
|
+
startShow(id);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function startShow(id) {
|
|
560
|
+
if (!id || isBlocked()) return;
|
|
561
|
+
|
|
562
|
+
state.inflight++;
|
|
563
|
+
let released = false;
|
|
564
|
+
const release = () => {
|
|
565
|
+
if (released) return;
|
|
566
|
+
released = true;
|
|
567
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
568
|
+
drainQueue();
|
|
569
|
+
};
|
|
174
570
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
571
|
+
const hardTimer = setTimeout(release, 6500);
|
|
572
|
+
|
|
573
|
+
requestAnimationFrame(() => {
|
|
178
574
|
try {
|
|
575
|
+
if (isBlocked()) return;
|
|
576
|
+
|
|
577
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
578
|
+
if (!ph || !ph.isConnected) return;
|
|
579
|
+
|
|
580
|
+
const now2 = Date.now();
|
|
581
|
+
const last2 = state.lastShowById.get(id) || 0;
|
|
582
|
+
if (now2 - last2 < 900) return;
|
|
583
|
+
state.lastShowById.set(id, now2);
|
|
584
|
+
|
|
179
585
|
window.ezstandalone = window.ezstandalone || {};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
586
|
+
const ez = window.ezstandalone;
|
|
587
|
+
|
|
588
|
+
const doShow = () => {
|
|
589
|
+
try {
|
|
590
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
591
|
+
try { ez.destroyPlaceholders(id); } catch (e) {}
|
|
592
|
+
}
|
|
593
|
+
} catch (e) {}
|
|
594
|
+
|
|
595
|
+
try { ez.showAds(id); } catch (e) {}
|
|
596
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
597
|
+
try { markEmptyWrapper(id); } catch (e) {}
|
|
598
|
+
|
|
599
|
+
setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
if (Array.isArray(ez.cmd)) {
|
|
603
|
+
try { ez.cmd.push(doShow); } catch (e) { doShow(); }
|
|
604
|
+
} else {
|
|
605
|
+
doShow();
|
|
606
|
+
}
|
|
607
|
+
} finally {
|
|
608
|
+
// If we returned early, hardTimer will release.
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
// ---------- preload / above-the-fold ----------
|
|
615
|
+
|
|
616
|
+
function ensurePreloadObserver() {
|
|
617
|
+
const desiredMargin = getPreloadRootMargin();
|
|
618
|
+
if (state.io && state.ioMargin === desiredMargin) return state.io;
|
|
619
|
+
|
|
620
|
+
// Rebuild IO if margin changed (e.g., scroll boost toggled)
|
|
621
|
+
if (state.io) {
|
|
622
|
+
try { state.io.disconnect(); } catch (e) {}
|
|
623
|
+
state.io = null;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
state.io = new IntersectionObserver((entries) => {
|
|
627
|
+
for (const ent of entries) {
|
|
628
|
+
if (!ent.isIntersecting) continue;
|
|
629
|
+
const el = ent.target;
|
|
630
|
+
try { state.io && state.io.unobserve(el); } catch (e) {}
|
|
631
|
+
|
|
632
|
+
const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
|
|
633
|
+
const id = parseInt(idAttr, 10);
|
|
634
|
+
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
186
635
|
}
|
|
187
|
-
});
|
|
636
|
+
}, { root: null, rootMargin: desiredMargin, threshold: 0 });
|
|
637
|
+
state.ioMargin = desiredMargin;
|
|
638
|
+
} catch (e) {
|
|
639
|
+
state.io = null;
|
|
640
|
+
state.ioMargin = null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// If we rebuilt the observer, re-observe existing placeholders so we don't miss them.
|
|
644
|
+
try {
|
|
645
|
+
if (state.io) {
|
|
646
|
+
const nodes = document.querySelectorAll(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
647
|
+
nodes.forEach((n) => { try { state.io.observe(n); } catch (e) {} });
|
|
648
|
+
}
|
|
188
649
|
} catch (e) {}
|
|
650
|
+
return state.io;
|
|
189
651
|
}
|
|
190
652
|
|
|
191
|
-
function
|
|
192
|
-
|
|
653
|
+
function observePlaceholder(id) {
|
|
654
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
655
|
+
if (!ph || !ph.isConnected) return;
|
|
656
|
+
const io = ensurePreloadObserver();
|
|
657
|
+
try { io && io.observe(ph); } catch (e) {}
|
|
193
658
|
|
|
194
|
-
|
|
195
|
-
|
|
659
|
+
// If already above fold, fire immediately
|
|
660
|
+
try {
|
|
661
|
+
const r = ph.getBoundingClientRect();
|
|
662
|
+
const screens = isBoosted() ? 5.0 : 3.0;
|
|
663
|
+
const minBottom = isBoosted() ? -1500 : -800;
|
|
664
|
+
if (r.top < window.innerHeight * screens && r.bottom > minBottom) enqueueShow(id);
|
|
665
|
+
} catch (e) {}
|
|
666
|
+
}
|
|
196
667
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
668
|
+
// ---------- insertion logic ----------
|
|
669
|
+
|
|
670
|
+
function computeTargets(count, interval, showFirst) {
|
|
671
|
+
const out = [];
|
|
672
|
+
if (count <= 0) return out;
|
|
673
|
+
if (showFirst) out.push(1);
|
|
674
|
+
for (let i = 1; i <= count; i++) {
|
|
675
|
+
if (i % interval === 0) out.push(i);
|
|
202
676
|
}
|
|
677
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
681
|
+
if (!items.length) return 0;
|
|
682
|
+
|
|
683
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
684
|
+
let inserted = 0;
|
|
685
|
+
const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
|
|
203
686
|
|
|
204
687
|
for (const afterPos of targets) {
|
|
205
|
-
if (inserted >=
|
|
688
|
+
if (inserted >= maxInserts) break;
|
|
206
689
|
|
|
207
690
|
const el = items[afterPos - 1];
|
|
208
691
|
if (!el || !el.isConnected) continue;
|
|
692
|
+
if (isAdjacentAd(el)) continue;
|
|
209
693
|
if (findWrap(kindClass, afterPos)) continue;
|
|
210
694
|
|
|
211
|
-
|
|
212
|
-
if (!id)
|
|
213
|
-
|
|
695
|
+
let id = pickIdFromAll(allIds, cursorKey);
|
|
696
|
+
if (!id) {
|
|
697
|
+
// No free ids: recycle an old ad wrapper so we can reuse its placeholder id.
|
|
698
|
+
// Guard against tight observer loops.
|
|
699
|
+
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
700
|
+
dbg('recycle-skip-cooldown', kindClass);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
let recycled = false;
|
|
704
|
+
withInternalDomChange(() => {
|
|
705
|
+
recycled = removeOneOldWrap(kindClass);
|
|
706
|
+
});
|
|
707
|
+
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
708
|
+
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
214
711
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
215
|
-
if (!wrap)
|
|
712
|
+
if (!wrap) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
216
715
|
|
|
217
|
-
|
|
218
|
-
callShowAds(id);
|
|
716
|
+
observePlaceholder(id);
|
|
219
717
|
inserted += 1;
|
|
220
718
|
}
|
|
221
719
|
|
|
222
720
|
return inserted;
|
|
223
721
|
}
|
|
224
722
|
|
|
225
|
-
async function
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
723
|
+
async function insertHeroAdEarly() {
|
|
724
|
+
if (state.heroDoneForPage) return;
|
|
725
|
+
const cfg = await fetchConfigOnce();
|
|
726
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
727
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
230
728
|
|
|
231
729
|
initPools(cfg);
|
|
232
730
|
|
|
233
731
|
const kind = getKind();
|
|
234
|
-
let
|
|
732
|
+
let items = [];
|
|
733
|
+
let allIds = [];
|
|
734
|
+
let cursorKey = '';
|
|
735
|
+
let kindClass = '';
|
|
736
|
+
let showFirst = false;
|
|
235
737
|
|
|
236
738
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
state.poolPosts,
|
|
243
|
-
state.usedPosts
|
|
244
|
-
);
|
|
739
|
+
items = getPostContainers();
|
|
740
|
+
allIds = state.allPosts;
|
|
741
|
+
cursorKey = 'curPosts';
|
|
742
|
+
kindClass = 'ezoic-ad-message';
|
|
743
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
245
744
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
state.poolTopics,
|
|
252
|
-
state.usedTopics
|
|
253
|
-
);
|
|
745
|
+
items = getTopicItems();
|
|
746
|
+
allIds = state.allTopics;
|
|
747
|
+
cursorKey = 'curTopics';
|
|
748
|
+
kindClass = 'ezoic-ad-between';
|
|
749
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
254
750
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
);
|
|
751
|
+
items = getCategoryItems();
|
|
752
|
+
allIds = state.allCategories;
|
|
753
|
+
cursorKey = 'curCategories';
|
|
754
|
+
kindClass = 'ezoic-ad-categories';
|
|
755
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
756
|
+
} else {
|
|
757
|
+
return;
|
|
263
758
|
}
|
|
759
|
+
|
|
760
|
+
if (!items.length) return;
|
|
761
|
+
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
762
|
+
|
|
763
|
+
// Insert after the very first item (above-the-fold)
|
|
764
|
+
const afterPos = 1;
|
|
765
|
+
const el = items[afterPos - 1];
|
|
766
|
+
if (!el || !el.isConnected) return;
|
|
767
|
+
if (isAdjacentAd(el)) return;
|
|
768
|
+
if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
|
|
769
|
+
|
|
770
|
+
const id = pickIdFromAll(allIds, cursorKey);
|
|
771
|
+
if (!id) return;
|
|
772
|
+
|
|
773
|
+
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
774
|
+
if (!wrap) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
state.heroDoneForPage = true;
|
|
779
|
+
observePlaceholder(id);
|
|
264
780
|
}
|
|
265
781
|
|
|
266
|
-
function
|
|
267
|
-
if (
|
|
268
|
-
|
|
782
|
+
async function runCore() {
|
|
783
|
+
if (isBlocked()) { dbg('blocked'); return; }
|
|
784
|
+
|
|
785
|
+
patchShowAds();
|
|
786
|
+
|
|
787
|
+
const cfg = await fetchConfigOnce();
|
|
788
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
789
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
790
|
+
initPools(cfg);
|
|
791
|
+
|
|
792
|
+
const kind = getKind();
|
|
269
793
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
794
|
+
if (kind === 'topic') {
|
|
795
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
796
|
+
const __items = getPostContainers();
|
|
797
|
+
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
798
|
+
injectBetween(
|
|
799
|
+
'ezoic-ad-message',
|
|
800
|
+
__items,
|
|
801
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
802
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
803
|
+
state.allPosts,
|
|
804
|
+
'curPosts'
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
} else if (kind === 'categoryTopics') {
|
|
808
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
809
|
+
const __items = getTopicItems();
|
|
810
|
+
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
811
|
+
injectBetween(
|
|
812
|
+
'ezoic-ad-between',
|
|
813
|
+
__items,
|
|
814
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
815
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
816
|
+
state.allTopics,
|
|
817
|
+
'curTopics'
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
} else if (kind === 'categories') {
|
|
821
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
822
|
+
const __items = getCategoryItems();
|
|
823
|
+
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
824
|
+
injectBetween(
|
|
825
|
+
'ezoic-ad-categories',
|
|
826
|
+
__items,
|
|
827
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
828
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
829
|
+
state.allCategories,
|
|
830
|
+
'curCategories'
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function scheduleRun() {
|
|
837
|
+
if (state.runQueued) return;
|
|
838
|
+
state.runQueued = true;
|
|
839
|
+
window.requestAnimationFrame(() => {
|
|
840
|
+
state.runQueued = false;
|
|
273
841
|
const pk = getPageKey();
|
|
274
842
|
if (state.pageKey && pk !== state.pageKey) return;
|
|
275
843
|
runCore().catch(() => {});
|
|
276
|
-
}
|
|
844
|
+
});
|
|
277
845
|
}
|
|
278
846
|
|
|
847
|
+
// ---------- observers / lifecycle ----------
|
|
848
|
+
|
|
279
849
|
function cleanup() {
|
|
280
|
-
|
|
281
|
-
if (allIds.length) destroyPlaceholderIds(allIds);
|
|
850
|
+
blockedUntil = Date.now() + 1200;
|
|
282
851
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
852
|
+
// remove all wrappers
|
|
853
|
+
try {
|
|
854
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
855
|
+
try { el.remove(); } catch (e) {}
|
|
856
|
+
});
|
|
857
|
+
} catch (e) {}
|
|
286
858
|
|
|
287
|
-
|
|
859
|
+
// reset state
|
|
288
860
|
state.cfg = null;
|
|
289
|
-
state.
|
|
290
|
-
state.
|
|
291
|
-
state.
|
|
292
|
-
state.
|
|
293
|
-
state.
|
|
294
|
-
state.
|
|
295
|
-
state.usedCategories.clear();
|
|
861
|
+
state.allTopics = [];
|
|
862
|
+
state.allPosts = [];
|
|
863
|
+
state.allCategories = [];
|
|
864
|
+
state.curTopics = 0;
|
|
865
|
+
state.curPosts = 0;
|
|
866
|
+
state.curCategories = 0;
|
|
296
867
|
state.lastShowById.clear();
|
|
297
|
-
|
|
868
|
+
state.inflight = 0;
|
|
869
|
+
state.pending = [];
|
|
870
|
+
try { state.pendingSet && state.pendingSet.clear(); } catch (e) {}
|
|
871
|
+
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
872
|
+
state.heroDoneForPage = false;
|
|
298
873
|
|
|
299
|
-
|
|
300
|
-
state.obs.disconnect();
|
|
301
|
-
state.obs = null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
state.scheduled = false;
|
|
305
|
-
clearTimeout(state.timer);
|
|
306
|
-
state.timer = null;
|
|
874
|
+
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
307
875
|
}
|
|
308
876
|
|
|
309
|
-
function
|
|
310
|
-
if (state.
|
|
311
|
-
state.
|
|
877
|
+
function ensureDomObserver() {
|
|
878
|
+
if (state.domObs) return;
|
|
879
|
+
state.domObs = new MutationObserver(() => {
|
|
880
|
+
if (state.internalDomChange > 0) return;
|
|
881
|
+
if (!isBlocked()) scheduleRun();
|
|
882
|
+
});
|
|
312
883
|
try {
|
|
313
|
-
state.
|
|
884
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
314
885
|
} catch (e) {}
|
|
315
886
|
}
|
|
316
887
|
|
|
317
|
-
function
|
|
318
|
-
const kind = getKind();
|
|
319
|
-
let selector = SELECTORS.postItem;
|
|
320
|
-
if (kind === 'categoryTopics') selector = SELECTORS.topicItem;
|
|
321
|
-
else if (kind === 'categories') selector = SELECTORS.categoryItem;
|
|
322
|
-
|
|
323
|
-
const check = () => {
|
|
324
|
-
if (document.querySelector(selector)) {
|
|
325
|
-
scheduleRun();
|
|
326
|
-
} else {
|
|
327
|
-
setTimeout(check, 200);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
check();
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function bind() {
|
|
888
|
+
function bindNodeBB() {
|
|
334
889
|
if (!$) return;
|
|
335
890
|
|
|
336
891
|
$(window).off('.ezoicInfinite');
|
|
337
|
-
|
|
892
|
+
|
|
893
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', () => {
|
|
894
|
+
cleanup();
|
|
895
|
+
});
|
|
896
|
+
|
|
338
897
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
339
898
|
state.pageKey = getPageKey();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
899
|
+
blockedUntil = 0;
|
|
900
|
+
|
|
901
|
+
warmUpNetwork();
|
|
902
|
+
patchShowAds();
|
|
903
|
+
ensurePreloadObserver();
|
|
904
|
+
ensureDomObserver();
|
|
905
|
+
|
|
906
|
+
// Ultra-fast above-the-fold first
|
|
907
|
+
insertHeroAdEarly().catch(() => {});
|
|
344
908
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
waitForContentThenRun();
|
|
909
|
+
// Then normal insertion
|
|
910
|
+
scheduleRun();
|
|
348
911
|
});
|
|
349
912
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
913
|
+
// Infinite scroll / partial updates
|
|
914
|
+
$(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
|
|
915
|
+
if (isBlocked()) return;
|
|
916
|
+
scheduleRun();
|
|
353
917
|
});
|
|
354
918
|
}
|
|
355
919
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
920
|
+
function bindScroll() {
|
|
921
|
+
let ticking = false;
|
|
922
|
+
window.addEventListener('scroll', () => {
|
|
923
|
+
// Detect very fast scrolling and temporarily boost preload/parallelism.
|
|
924
|
+
try {
|
|
925
|
+
const now = Date.now();
|
|
926
|
+
const y = window.scrollY || window.pageYOffset || 0;
|
|
927
|
+
if (state.lastScrollTs) {
|
|
928
|
+
const dt = now - state.lastScrollTs;
|
|
929
|
+
const dy = Math.abs(y - (state.lastScrollY || 0));
|
|
930
|
+
if (dt > 0) {
|
|
931
|
+
const speed = dy / dt; // px/ms
|
|
932
|
+
if (speed >= BOOST_SPEED_PX_PER_MS) {
|
|
933
|
+
const wasBoosted = isBoosted();
|
|
934
|
+
state.scrollBoostUntil = Math.max(state.scrollBoostUntil || 0, now + BOOST_DURATION_MS);
|
|
935
|
+
if (!wasBoosted) {
|
|
936
|
+
// margin changed -> rebuild IO so existing placeholders get earlier preload
|
|
937
|
+
ensurePreloadObserver();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
state.lastScrollY = y;
|
|
943
|
+
state.lastScrollTs = now;
|
|
944
|
+
} catch (e) {}
|
|
945
|
+
|
|
946
|
+
if (ticking) return;
|
|
947
|
+
ticking = true;
|
|
948
|
+
window.requestAnimationFrame(() => {
|
|
949
|
+
ticking = false;
|
|
950
|
+
if (!isBlocked()) scheduleRun();
|
|
951
|
+
});
|
|
952
|
+
}, { passive: true });
|
|
362
953
|
}
|
|
363
954
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
955
|
+
// ---------- boot ----------
|
|
956
|
+
|
|
957
|
+
state.pageKey = getPageKey();
|
|
958
|
+
warmUpNetwork();
|
|
959
|
+
patchShowAds();
|
|
960
|
+
ensurePreloadObserver();
|
|
961
|
+
ensureDomObserver();
|
|
962
|
+
|
|
963
|
+
bindNodeBB();
|
|
964
|
+
bindScroll();
|
|
965
|
+
|
|
966
|
+
// First paint: try hero + run
|
|
967
|
+
blockedUntil = 0;
|
|
968
|
+
insertHeroAdEarly().catch(() => {});
|
|
969
|
+
scheduleRun();
|
|
371
970
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
1
2
|
.ezoic-ad {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin: 0 !important;
|
|
6
|
+
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
11
|
+
margin: 0 !important;
|
|
2
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 {
|
|
3
19
|
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
+
.ezoic-ad.is-empty {
|
|
26
|
+
display: block !important;
|
|
27
|
+
margin: 0 !important;
|
|
28
|
+
padding: 0 !important;
|
|
29
|
+
height: 0 !important;
|
|
4
30
|
min-height: 0 !important;
|
|
5
|
-
|
|
31
|
+
overflow: hidden !important;
|
|
6
32
|
}
|
|
7
33
|
|
|
8
|
-
.ezoic-ad
|
|
34
|
+
.ezoic-ad {
|
|
9
35
|
min-height: 0 !important;
|
|
10
|
-
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
39
|
+
min-height: 0 !important;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Avoid baseline gap under iframes (can look like extra space) */
|
|
43
|
+
.ezoic-ad iframe {
|
|
44
|
+
display: block !important;
|
|
45
|
+
vertical-align: top !important;
|
|
11
46
|
}
|