nodebb-plugin-ezoic-infinite 1.6.40 → 1.6.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.40",
3
+ "version": "1.6.42",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -498,48 +498,6 @@ function globalGapFixInit() {
498
498
  return el;
499
499
  }
500
500
 
501
- // Best-effort refresh for Ezoic standalone when we defer placement.
502
- // Some stacks won't fill ads if a placeholder is rendered while collapsed/0px.
503
- let _refreshTimer = null;
504
-
505
- function collectEzoicIds(root) {
506
- const ids = new Set();
507
- if (!root || !root.querySelectorAll) return [];
508
- // Common Ezoic placeholder ids
509
- root.querySelectorAll('[id^="ezoic-pub-ad-placeholder-"]').forEach(el => ids.add(el.id));
510
- // Some integrations keep ids on wrappers
511
- root.querySelectorAll('[data-ezoic-id]').forEach(el => {
512
- if (el.id) ids.add(el.id);
513
- });
514
- return Array.from(ids);
515
- }
516
-
517
- function refreshAds(ids) {
518
- clearTimeout(_refreshTimer);
519
- _refreshTimer = setTimeout(() => {
520
- try {
521
- if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
522
- // Prefer a full rescan; fall back to ids if supported.
523
- try {
524
- window.ezstandalone.showAds();
525
- } catch (e) {
526
- if (ids && ids.length) window.ezstandalone.showAds(ids);
527
- }
528
- if (window.__EZ_DEBUG) console.debug('[ezoic-infinite] refreshAds (ezstandalone)', { ids });
529
- return;
530
- }
531
- if (window.ez && typeof window.ez.showAds === 'function') {
532
- try {
533
- window.ez.showAds();
534
- } catch (e) {
535
- if (ids && ids.length) window.ez.showAds(ids);
536
- }
537
- if (window.__EZ_DEBUG) console.debug('[ezoic-infinite] refreshAds (ez)', { ids });
538
- }
539
- } catch (e) {}
540
- }, 75);
541
- }
542
-
543
501
  function primePlaceholderPool(allIds) {
544
502
  try {
545
503
  if (!Array.isArray(allIds) || !allIds.length) return;
@@ -1553,21 +1511,16 @@ function buildOrdinalMap(items) {
1553
1511
 
1554
1512
 
1555
1513
 
1556
-
1557
-
1558
-
1559
- // ===== V17.8: No pile-up by design — keep "future" between slots pending/collapsed until their anchor topic exists =====
1514
+ // ===== V17 minimal pile-fix (no insert hooks) =====
1560
1515
  (function () {
1516
+ // Goal: keep ad injection intact. Only repair when we detect "pile-up" of between wraps.
1561
1517
  var TOPIC_LI_SEL = 'li[component="category/topic"]';
1562
1518
  var BETWEEN_WRAP_SEL = 'div.nodebb-ezoic-wrap.ezoic-ad-between';
1563
1519
  var HOST_CLASS = 'nodebb-ezoic-host';
1564
- var PENDING_CLASS = 'nodebb-ezoic-pending';
1565
1520
 
1566
- var ul = null;
1567
- var mo = null;
1568
1521
  var scheduled = false;
1569
1522
  var lastRun = 0;
1570
- var COOLDOWN = 80;
1523
+ var COOLDOWN = 180;
1571
1524
 
1572
1525
  function getTopicList() {
1573
1526
  try {
@@ -1577,41 +1530,29 @@ function buildOrdinalMap(items) {
1577
1530
  } catch (e) { return null; }
1578
1531
  }
1579
1532
 
1580
- function ensureUL() { ul = ul || getTopicList(); return ul; }
1581
-
1582
- function isHost(el) {
1583
- return !!(el && el.nodeType === 1 && el.tagName === 'LI' && el.classList && el.classList.contains(HOST_CLASS));
1533
+ function isHost(node) {
1534
+ return !!(node && node.nodeType === 1 && node.tagName === 'LI' && node.classList && node.classList.contains(HOST_CLASS));
1584
1535
  }
1585
1536
 
1586
- function isBetweenWrap(el) {
1587
- try { return !!(el && el.nodeType === 1 && el.matches && el.matches(BETWEEN_WRAP_SEL)); } catch(e){ return false; }
1588
- }
1589
-
1590
- function ensureHostForWrap(wrap, ulEl) {
1591
- // If Ezoic inserts ul > div..., wrap it into a LI to keep a valid list structure (less NodeBB churn).
1537
+ function ensureHostForWrap(wrap, ul) {
1592
1538
  try {
1593
- if (!wrap || !isBetweenWrap(wrap)) return null;
1539
+ if (!wrap || wrap.nodeType !== 1) return null;
1540
+ if (!(wrap.matches && wrap.matches(BETWEEN_WRAP_SEL))) return null;
1541
+
1594
1542
  var host = wrap.closest ? wrap.closest('li.' + HOST_CLASS) : null;
1595
1543
  if (host) return host;
1596
1544
 
1597
- ulEl = ulEl || (wrap.closest ? wrap.closest('ul,ol') : null);
1598
- if (!ulEl || !(ulEl.tagName === 'UL' || ulEl.tagName === 'OL')) return null;
1545
+ if (!ul) ul = wrap.closest ? wrap.closest('ul,ol') : null;
1546
+ if (!ul || !(ul.tagName === 'UL' || ul.tagName === 'OL')) return null;
1599
1547
 
1600
- if (wrap.parentElement === ulEl) {
1548
+ // Only wrap if direct child of list (invalid / fragile)
1549
+ if (wrap.parentElement === ul) {
1601
1550
  host = document.createElement('li');
1602
1551
  host.className = HOST_CLASS;
1603
1552
  host.setAttribute('role', 'listitem');
1604
1553
  host.style.listStyle = 'none';
1605
1554
  host.style.width = '100%';
1606
-
1607
- try {
1608
- var after = wrap.getAttribute('data-ezoic-after');
1609
- if (after) host.setAttribute('data-ezoic-after', after);
1610
- var pin = wrap.getAttribute('data-ezoic-pin');
1611
- if (pin) host.setAttribute('data-ezoic-pin', pin);
1612
- } catch (e) {}
1613
-
1614
- ulEl.insertBefore(host, wrap);
1555
+ ul.insertBefore(host, wrap);
1615
1556
  host.appendChild(wrap);
1616
1557
  try { wrap.style.width = '100%'; } catch (e) {}
1617
1558
  return host;
@@ -1620,259 +1561,217 @@ function buildOrdinalMap(items) {
1620
1561
  return null;
1621
1562
  }
1622
1563
 
1623
- function getAfter(el) {
1564
+ function previousTopicLi(node) {
1624
1565
  try {
1625
- var v = null;
1626
- if (el && el.getAttribute) v = el.getAttribute('data-ezoic-after');
1627
- if (!v && el && el.querySelector) {
1628
- var w = el.querySelector(BETWEEN_WRAP_SEL);
1629
- if (w) v = w.getAttribute('data-ezoic-after');
1566
+ var prev = node.previousElementSibling;
1567
+ while (prev) {
1568
+ if (prev.matches && prev.matches(TOPIC_LI_SEL)) return prev;
1569
+ // skip other hosts/wraps
1570
+ prev = prev.previousElementSibling;
1630
1571
  }
1631
- var n = parseInt(v, 10);
1632
- return isNaN(n) ? null : n;
1633
- } catch (e) { return null; }
1634
- }
1635
-
1636
- function getTopics(ulEl) {
1637
- try { return ulEl ? ulEl.querySelectorAll(TOPIC_LI_SEL) : []; } catch(e){ return []; }
1638
- }
1639
-
1640
- function lastTopic(ulEl, topics) {
1641
- try {
1642
- topics = topics || getTopics(ulEl);
1643
- return topics && topics.length ? topics[topics.length - 1] : null;
1644
- } catch (e) { return null; }
1645
- }
1646
-
1647
- function moveAfter(node, anchor) {
1648
- try {
1649
- if (!node || !anchor || !anchor.insertAdjacentElement) return;
1650
- if (node.previousElementSibling === anchor) return;
1651
- anchor.insertAdjacentElement('afterend', node);
1652
1572
  } catch (e) {}
1573
+ return null;
1653
1574
  }
1654
1575
 
1655
- function placeOrPend(host, ulEl, topics) {
1576
+ function detectPileUp(ul) {
1577
+ // Pile-up signature: 2+ between wraps/hosts adjacent with no topics between, often near top.
1656
1578
  try {
1657
- if (!host) return;
1658
- ulEl = ulEl || ensureUL();
1659
- if (!ulEl) return;
1660
-
1661
- topics = topics || getTopics(ulEl);
1662
-
1663
- var after = getAfter(host);
1664
- if (!after) {
1665
- var lt = lastTopic(ulEl, topics);
1666
- if (lt) moveAfter(host, lt);
1667
- host.classList && host.classList.remove(PENDING_CLASS);
1668
- return;
1669
- }
1670
-
1671
- if (after > topics.length) {
1672
- // Defer placement until the anchor exists.
1673
- // Keeping a placeholder in a collapsed/0px container can prevent ad fill on some stacks.
1674
- host.classList && host.classList.add(PENDING_CLASS);
1675
- try {
1676
- getPoolEl().appendChild(host);
1677
- } catch (e) {}
1678
- return;
1679
- }
1680
-
1681
- var anchor = topics[after - 1];
1682
- if (anchor) moveAfter(host, anchor);
1683
- if (host.classList && host.classList.contains(PENDING_CLASS)) {
1684
- host.classList.remove(PENDING_CLASS);
1685
- if (host.dataset && host.dataset.ezoicId) refreshAds([host.dataset.ezoicId]);
1579
+ var kids = ul.children;
1580
+ var run = 0;
1581
+ var maxRun = 0;
1582
+ for (var i = 0; i < kids.length; i++) {
1583
+ var el = kids[i];
1584
+ var isBetween = false;
1585
+ if (isHost(el)) {
1586
+ isBetween = !!(el.querySelector && el.querySelector(BETWEEN_WRAP_SEL));
1587
+ } else if (el.matches && el.matches(BETWEEN_WRAP_SEL)) {
1588
+ isBetween = true;
1589
+ }
1590
+ if (isBetween) {
1591
+ run++;
1592
+ if (run > maxRun) maxRun = run;
1593
+ } else if (el.matches && el.matches(TOPIC_LI_SEL)) {
1594
+ run = 0;
1595
+ } else {
1596
+ // other nodes reset lightly
1597
+ run = 0;
1598
+ }
1686
1599
  }
1600
+ return maxRun >= 2;
1687
1601
  } catch (e) {}
1602
+ return false;
1688
1603
  }
1689
1604
 
1690
- function reconcile() {
1691
- var ulEl = ensureUL();
1692
- if (!ulEl) return;
1605
+ function redistribute(ul) {
1606
+ try {
1607
+ if (!ul) return;
1693
1608
 
1694
- var topics = getTopics(ulEl);
1609
+ // Step 1: wrap any direct child between DIVs into LI hosts (makes list stable)
1610
+ ul.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function(w){ ensureHostForWrap(w, ul); });
1695
1611
 
1696
- try {
1697
- ulEl.querySelectorAll(':scope > ' + BETWEEN_WRAP_SEL).forEach(function (w) {
1698
- var h = ensureHostForWrap(w, ulEl);
1699
- if (h) placeOrPend(h, ulEl, topics);
1700
- });
1612
+ // Step 2: only act if we see pile-up (avoid touching infinite scroll during normal flow)
1613
+ if (!detectPileUp(ul)) return;
1701
1614
 
1702
- ulEl.querySelectorAll('li.' + HOST_CLASS).forEach(function (h) {
1703
- placeOrPend(h, ulEl, topics);
1704
- });
1615
+ // Move each host to immediately after the closest previous topic LI at its current position.
1616
+ var hosts = ul.querySelectorAll(':scope > li.' + HOST_CLASS);
1617
+ hosts.forEach(function(host){
1618
+ try {
1619
+ var wrap = host.querySelector && host.querySelector(BETWEEN_WRAP_SEL);
1620
+ if (!wrap) return;
1705
1621
 
1706
- ulEl.querySelectorAll(BETWEEN_WRAP_SEL).forEach(function (w) {
1707
- var h = ensureHostForWrap(w, ulEl) || (w.closest ? w.closest('li.' + HOST_CLASS) : null);
1708
- if (h) placeOrPend(h, ulEl, topics);
1622
+ var anchor = previousTopicLi(host);
1623
+ if (!anchor) return; // if none, don't move (prevents yanking to top/bottom)
1624
+
1625
+ if (host.previousElementSibling !== anchor) {
1626
+ anchor.insertAdjacentElement('afterend', host);
1627
+ }
1628
+ } catch (e) {}
1709
1629
  });
1710
1630
  } catch (e) {}
1711
1631
  }
1712
1632
 
1713
- function scheduleReconcile() {
1633
+ function schedule(reason) {
1714
1634
  var now = Date.now();
1715
- if (scheduled) return;
1716
1635
  if (now - lastRun < COOLDOWN) return;
1636
+ if (scheduled) return;
1717
1637
  scheduled = true;
1718
- lastRun = now;
1719
1638
  requestAnimationFrame(function () {
1720
1639
  scheduled = false;
1721
- reconcile();
1640
+ lastRun = Date.now();
1641
+ try {
1642
+ var ul = getTopicList();
1643
+ if (!ul) return;
1644
+ redistribute(ul);
1645
+ } catch (e) {}
1722
1646
  });
1723
1647
  }
1724
1648
 
1725
- // -------- Viewport "poke" for empty Ezoic placeholders --------
1726
- // Some Ezoic placeholders can be placed into the DOM already in-view, but
1727
- // the ad stack only reacts on the next scroll tick. This keeps blocks from
1728
- // staying blank until the user nudges the page.
1729
- var io = null;
1730
- var observedPlaceholders = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
1649
+ // --- Empty ad "poke" helper ---
1650
+ // Some GPT safeframe creatives may remain visually blank until a small scroll/reflow happens.
1651
+ // We avoid calling showAds()/refresh() (which can hurt fill) and instead trigger a cheap reflow.
1652
+ var ezEmptySeen = Object.create(null);
1653
+ var ezEmptyPokeTimer = Object.create(null);
1731
1654
 
1732
- function isEzoicPlaceholder(el) {
1733
- return !!(el && el.id && el.id.indexOf('ezoic-pub-ad-placeholder-') === 0);
1734
- }
1735
-
1736
- function iframeCount(el) {
1655
+ function inViewportLoose(el) {
1737
1656
  try {
1738
- return (el && el.querySelectorAll) ? el.querySelectorAll('iframe').length : 0;
1657
+ if (!el || !el.getBoundingClientRect) return false;
1658
+ var r = el.getBoundingClientRect();
1659
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
1660
+ // within 1.5 viewports from top/bottom
1661
+ return r.bottom > -vh * 0.5 && r.top < vh * 1.5;
1739
1662
  } catch (e) {
1740
- return 0;
1663
+ return false;
1741
1664
  }
1742
1665
  }
1743
1666
 
1744
- function pokePlaceholder(ph, reason) {
1745
- if (!ph || !ph.id) return;
1746
- // throttle per placeholder
1747
- var now = Date.now();
1748
- var last = 0;
1749
- try { last = parseInt(ph.getAttribute('data-ez-poke-at') || '0', 10) || 0; } catch (e) {}
1750
- if (now - last < 1200) return;
1751
- try { ph.setAttribute('data-ez-poke-at', String(now)); } catch (e) {}
1752
-
1753
- // Only poke if still empty
1754
- if (iframeCount(ph) > 0) return;
1755
-
1756
- if (DEBUG) {
1757
- try { console.log('[EZ POKE]', reason || 'io', ph.id); } catch (e) {}
1758
- }
1759
-
1760
- // Ask Ezoic to fill that specific placeholder
1761
- refreshAds([ph.id]);
1762
-
1763
- // And also emit a lightweight scroll/resize tick to trigger any lazy logic
1764
- try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1765
- try { window.dispatchEvent(new Event('resize')); } catch (e) {}
1766
-
1767
- // One re-try shortly after, in case the first request was dropped.
1768
- setTimeout(function () {
1769
- try {
1770
- if (iframeCount(ph) === 0) {
1771
- refreshAds([ph.id]);
1772
- try { window.dispatchEvent(new Event('scroll')); } catch (e2) {}
1773
- }
1774
- } catch (e3) {}
1775
- }, 450);
1667
+ function pokeReflowForDivId(divId) {
1668
+ try {
1669
+ var el = document.getElementById(divId);
1670
+ if (!el) return;
1671
+ if (!inViewportLoose(el)) return;
1672
+
1673
+ // Force a tiny reflow/paint on the closest wrapper
1674
+ var wrap = el.closest ? (el.closest('.ez-fixed-wrap') || el.parentElement) : el.parentElement;
1675
+ if (!wrap) wrap = el;
1676
+
1677
+ // Toggle visibility for one frame
1678
+ var prevVis = wrap.style.visibility;
1679
+ wrap.style.visibility = 'hidden';
1680
+ requestAnimationFrame(function(){
1681
+ wrap.style.visibility = prevVis || '';
1682
+ // Dispatch scroll/resize events without moving the page
1683
+ try { window.dispatchEvent(new Event('scroll')); } catch (e) {}
1684
+ try { window.dispatchEvent(new Event('resize')); } catch (e) {}
1685
+ });
1686
+ } catch (e) {}
1776
1687
  }
1777
1688
 
1778
- function ensureIO() {
1779
- if (io) return;
1780
- if (!('IntersectionObserver' in window)) return;
1781
- io = new IntersectionObserver(function (entries) {
1782
- for (var i = 0; i < entries.length; i++) {
1783
- var e = entries[i];
1784
- if (!e || !e.isIntersecting) continue;
1785
- var ph = e.target;
1786
- if (!isEzoicPlaceholder(ph)) continue;
1787
- if (iframeCount(ph) === 0) {
1788
- pokePlaceholder(ph, 'in-view');
1789
- }
1790
- }
1791
- }, { root: null, rootMargin: '200px 0px', threshold: 0.01 });
1689
+ function schedulePoke(divId) {
1690
+ try {
1691
+ if (!divId) return;
1692
+ var now = Date.now();
1693
+ // Cooldown per slot to avoid loops
1694
+ if (ezEmptySeen[divId] && now - ezEmptySeen[divId] < 15000) return;
1695
+ ezEmptySeen[divId] = now;
1696
+
1697
+ if (ezEmptyPokeTimer[divId]) return;
1698
+ ezEmptyPokeTimer[divId] = setTimeout(function(){
1699
+ ezEmptyPokeTimer[divId] = null;
1700
+ pokeReflowForDivId(divId);
1701
+ }, 350);
1702
+ } catch (e) {}
1792
1703
  }
1793
1704
 
1794
- function observePlaceholder(ph) {
1795
- if (!ph) return;
1796
- ensureIO();
1797
- if (!io) return;
1798
- if (observedPlaceholders) {
1799
- if (observedPlaceholders.has(ph)) return;
1800
- observedPlaceholders.add(ph);
1801
- } else {
1802
- // fallback: mark directly
1803
- if (ph.getAttribute && ph.getAttribute('data-ez-observed') === '1') return;
1804
- try { ph.setAttribute('data-ez-observed', '1'); } catch (e) {}
1805
- }
1806
- try { io.observe(ph); } catch (e) {}
1705
+ function initGptEmptyPoke() {
1706
+ try {
1707
+ if (!window.googletag || !googletag.cmd || !googletag.pubads) return;
1708
+ googletag.cmd.push(function(){
1709
+ try {
1710
+ var pub = googletag.pubads();
1711
+ if (!pub || !pub.addEventListener) return;
1712
+ pub.addEventListener('slotRenderEnded', function(e){
1713
+ try {
1714
+ if (!e) return;
1715
+ // divId is the GPT container id for the slot (e.g. div-gpt-ad-...)
1716
+ var divId = e.slot && e.slot.getSlotElementId ? e.slot.getSlotElementId() : (e.slotElementId || '');
1717
+ if (e.isEmpty) {
1718
+ if (divId) console.log('[EZ EMPTY]', divId);
1719
+ schedulePoke(divId);
1720
+ }
1721
+ } catch (err) {}
1722
+ });
1723
+ } catch (err2) {}
1724
+ });
1725
+ } catch (e) {}
1807
1726
  }
1808
1727
 
1809
- function scanPlaceholders(root) {
1810
- if (!root || !root.querySelectorAll) return;
1811
- var list = [];
1812
- try { list = root.querySelectorAll('div[id^="ezoic-pub-ad-placeholder-"]'); } catch (e) { list = []; }
1813
- for (var i = 0; i < list.length; i++) observePlaceholder(list[i]);
1814
- }
1728
+ function init() {
1729
+ schedule('init');
1815
1730
 
1816
- function initObserver() {
1817
- var ulEl = ensureUL();
1818
- if (!ulEl || mo) return;
1731
+ // Observe only the topic list once available
1732
+ try {
1733
+ if (typeof MutationObserver !== 'undefined') {
1734
+ var observeList = function(ul){
1735
+ if (!ul) return;
1736
+ var mo = new MutationObserver(function(muts){
1737
+ // schedule on any change; redistribute() itself is guarded by pile-up detection
1738
+ schedule('mo');
1739
+ });
1740
+ mo.observe(ul, { childList: true, subtree: true });
1741
+ };
1819
1742
 
1820
- mo = new MutationObserver(function (mutations) {
1821
- scheduleReconcile();
1822
- // Also observe any newly injected placeholders so in-view blanks get poked.
1823
- try {
1824
- for (var i = 0; i < (mutations || []).length; i++) {
1825
- var m = mutations[i];
1826
- if (!m || !m.addedNodes) continue;
1827
- for (var j = 0; j < m.addedNodes.length; j++) {
1828
- var n = m.addedNodes[j];
1829
- if (!n) continue;
1830
- if (isEzoicPlaceholder(n)) observePlaceholder(n);
1831
- else scanPlaceholders(n);
1832
- }
1743
+ var ul = getTopicList();
1744
+ if (ul) observeList(ul);
1745
+ else {
1746
+ var mo2 = new MutationObserver(function(){
1747
+ var u2 = getTopicList();
1748
+ if (u2) {
1749
+ try { observeList(u2); } catch(e){}
1750
+ try { mo2.disconnect(); } catch(e){}
1751
+ }
1752
+ });
1753
+ mo2.observe(document.documentElement || document.body, { childList: true, subtree: true });
1833
1754
  }
1834
- } catch (e) {}
1835
- });
1836
-
1837
- mo.observe(ulEl, { childList: true, subtree: true });
1838
- }
1839
-
1840
- function init() {
1841
- initObserver();
1842
- scheduleReconcile();
1843
- // initial scan for placeholders already in the DOM
1844
- scanPlaceholders(document);
1755
+ }
1756
+ } catch (e) {}
1845
1757
 
1758
+ // NodeBB events: run after infinite scroll batches
1846
1759
  if (window.jQuery) {
1847
1760
  try {
1848
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function () {
1849
- ul = null;
1850
- try { if (mo) mo.disconnect(); } catch (e) {}
1851
- mo = null;
1852
- initObserver();
1853
- scheduleReconcile();
1854
- setTimeout(scheduleReconcile, 120);
1855
- setTimeout(scheduleReconcile, 500);
1856
- // re-scan after ajaxify/infinite load
1857
- setTimeout(function () { scanPlaceholders(document); }, 50);
1761
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', function(){
1762
+ setTimeout(function(){ schedule('event'); }, 50);
1763
+ setTimeout(function(){ schedule('event2'); }, 400);
1858
1764
  });
1859
1765
  } catch (e) {}
1860
1766
  }
1861
1767
 
1862
- var tries = 0;
1863
- var t = setInterval(function () {
1864
- tries++;
1865
- if (ensureUL()) {
1866
- clearInterval(t);
1867
- initObserver();
1868
- scheduleReconcile();
1869
- }
1870
- if (tries > 30) clearInterval(t);
1871
- }, 200);
1768
+ // Listen for GPT empty renders and apply a cheap reflow "poke".
1769
+ // This is intentionally non-invasive (no showAds/refresh) to preserve fill.
1770
+ initGptEmptyPoke();
1872
1771
  }
1873
1772
 
1874
1773
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1875
1774
  else init();
1876
1775
  })();
1877
- // ===== /V17.8 =====
1776
+ // ===== /V17 =====
1878
1777
 
package/public/style.css CHANGED
@@ -86,15 +86,3 @@ li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
86
86
  li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
87
87
  /* ===== /V17 ===== */
88
88
 
89
-
90
-
91
- /* ===== V17.8 pending slots (prevents pile-up top/bottom) ===== */
92
- li.nodebb-ezoic-host { list-style:none; width:100%; display:block; }
93
- li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width:100%; display:block; }
94
-
95
- /* slots whose anchor topic isn't loaded yet */
96
- li.nodebb-ezoic-host.nodebb-ezoic-pending{
97
- display: none !important;
98
- }
99
- /* ===== /V17.8 ===== */
100
-