On3 Staff Posts Filter

Toggle between all posts and staff-only posts on On3 threads

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         On3 Staff Posts Filter
// @namespace    https://www.on3.com
// @version      1.4.0
// @description  Toggle between all posts and staff-only posts on On3 threads
// @match        https://www.on3.com/boards/threads/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  console.log('[On3 StaffFilter] Script loaded');

  // Only run on thread pages like /boards/threads/slug.1234567/
  if (!/\/boards\/threads\/[^.]+\.\d+/.test(location.pathname)) {
    console.log('[On3 StaffFilter] Not a thread page');
    return;
  }

  console.log('[On3 StaffFilter] Thread page detected, initializing...');

  // Inject CSS
  const css = `
#xfStaffOnlyOff:checked ~ .block.block--messages .message {
  display: block !important;
}

#xfStaffOnlyOn:checked ~ .block.block--messages .message:not(:has(.userBanner--staff)) {
  display: none !important;
}

#xfStaffOnlyOff:checked ~ .block.block--messages .buttonGroup #btnAllPosts  { font-weight: 600; text-decoration: underline; }
#xfStaffOnlyOff:checked ~ .block.block--messages .buttonGroup #btnStaffOnly { opacity: .6; }

#xfStaffOnlyOn:checked  ~ .block.block--messages .buttonGroup #btnStaffOnly { font-weight: 600; text-decoration: underline; }
#xfStaffOnlyOn:checked  ~ .block.block--messages .buttonGroup #btnAllPosts  { opacity: .6; }
`;
  const style = document.createElement('style');
  style.textContent = css;
  document.head.appendChild(style);

  function injectControls() {
    const messagesBlock = document.querySelector('.block.block--messages');
    if (!messagesBlock) {
      console.log('[On3 StaffFilter] Waiting for .block.block--messages...');
      return false;
    }

    // Don't double-inject
    if (document.getElementById('xfStaffOnlyOn') || document.getElementById('xfStaffOnlyOff')) {
      console.log('[On3 StaffFilter] Controls already exist');
      return true;
    }

    console.log('[On3 StaffFilter] Injecting controls...');

    const blockOuter = messagesBlock.querySelector('.block-outer');
    if (!blockOuter) return false;

    let outerOpp = blockOuter.querySelector('.block-outer-opposite');
    if (!outerOpp) {
      outerOpp = document.createElement('div');
      outerOpp.className = 'block-outer-opposite';
      blockOuter.appendChild(outerOpp);
    }

    let buttonGroup = outerOpp.querySelector('.buttonGroup');
    if (!buttonGroup) {
      buttonGroup = document.createElement('div');
      buttonGroup.className = 'buttonGroup';
      outerOpp.appendChild(buttonGroup);
    }

    const staffLabel = document.createElement('label');
    staffLabel.id = 'btnStaffOnly';
    staffLabel.className = 'button xf-staffOnlyBtn';
    staffLabel.htmlFor = 'xfStaffOnlyOn';
    staffLabel.textContent = 'Staff posts';

    const allLabel = document.createElement('label');
    allLabel.id = 'btnAllPosts';
    allLabel.className = 'button';
    allLabel.htmlFor = 'xfStaffOnlyOff';
    allLabel.textContent = 'All posts';

    buttonGroup.appendChild(staffLabel);
    buttonGroup.appendChild(allLabel);

    // Create radios as direct siblings of messagesBlock (required for CSS ~ selector)
    const offRadio = document.createElement('input');
    offRadio.type = 'radio';
    offRadio.name = 'xfStaffOnlyMode';
    offRadio.id = 'xfStaffOnlyOff';
    offRadio.className = 'u-hidden';
    offRadio.checked = true;
    offRadio.style.display = 'none';

    const onRadio = document.createElement('input');
    onRadio.type = 'radio';
    onRadio.name = 'xfStaffOnlyMode';
    onRadio.id = 'xfStaffOnlyOn';
    onRadio.className = 'u-hidden';
    onRadio.style.display = 'none';

    // Insert radios as direct siblings before messagesBlock
    messagesBlock.parentNode.insertBefore(offRadio, messagesBlock);
    messagesBlock.parentNode.insertBefore(onRadio, messagesBlock);

    console.log('[On3 StaffFilter] ✓ Controls injected successfully!');
    return true;
  }

  function setupBehavior() {
    function parseTidFromCurrent() {
      const m = location.pathname.match(/\/boards\/threads\/[^.]+\.(\d+)(?:\/|$)/i);
      return m ? m[1] : null;
    }

    function parseTid(href) {
      try {
        const u = new URL(href, location.href);
        const m = u.pathname.match(/\/boards\/threads\/[^.]+\.(\d+)(?:\/|$)/i);
        return m ? m[1] : null;
      } catch { return null; }
    }

    const tid = parseTidFromCurrent();
    if (!tid) return;

    const KEY = 'xfStaffOnly:' + tid;         // per-thread stored preference
    const HINT = '__xfWithinThread:' + tid;   // marks pagination within this thread

    const on  = () => document.getElementById('xfStaffOnlyOn');
    const off = () => document.getElementById('xfStaffOnlyOff');

    function supportsHas() {
      try { return CSS.supports('selector(:has(*))'); } catch { return false; }
    }
    function isOn() {
      const r = on();
      return !!(r && r.checked);
    }

    function applyFallback() {
      if (supportsHas()) return;
      const hide = isOn();
      document.querySelectorAll('.block--messages .message').forEach(m => {
        const staff = !!m.querySelector('.userBanner--staff');
        m.style.display = (hide && !staff) ? 'none' : '';
      });
    }

    function cameFromSameThread() {
      const ref = document.referrer;
      return ref && parseTid(ref) === String(tid);
    }

    function resetToAll() {
      try { sessionStorage.removeItem(KEY); } catch {}
      const onEl = on();
      const offEl = off();
      if (offEl) offEl.checked = true;
      if (onEl) onEl.checked = false;
      // Clear inline fallback styles
      document.querySelectorAll('.block--messages .message[style]').forEach(m => {
        m.style.display = '';
      });
    }

    function applyFromStorage() {
      let prefOn = false;
      try { prefOn = sessionStorage.getItem(KEY) === '1'; } catch {}
      const onEl = on();
      const offEl = off();
      if (onEl && offEl) {
        onEl.checked = prefOn;
        offEl.checked = !prefOn;
      }
      applyFallback();
    }

    function save() {
      try { sessionStorage.setItem(KEY, isOn() ? '1' : '0'); } catch {}
      applyFallback();
    }

    function entry() {
      let within = false;
      try { within = sessionStorage.getItem(HINT) === '1'; } catch {}
      // If we didn't come from this same thread (i.e., not pagination), default to All posts
      if (!cameFromSameThread() && !within) {
        resetToAll();
      }
      try { sessionStorage.removeItem(HINT); } catch {}
      applyFromStorage(); // apply stored pref if any (or Off if we just reset)
    }

    // Set up event handlers
    function attachEvents() {
      const onRadio = document.getElementById('xfStaffOnlyOn');
      const offRadio = document.getElementById('xfStaffOnlyOff');
      const staffBtn = document.getElementById('btnStaffOnly');
      const allBtn = document.getElementById('btnAllPosts');

      if (!onRadio || !offRadio) {
        console.warn('[On3 StaffFilter] Radios not found');
        return;
      }

      // Change listeners on radios
      onRadio.addEventListener('change', () => {
        console.log('[On3 StaffFilter] Staff only selected');
        save();
      });

      offRadio.addEventListener('change', () => {
        console.log('[On3 StaffFilter] All posts selected');
        save();
      });

      // Click handlers on labels (as backup, though htmlFor should handle it)
      if (staffBtn) {
        staffBtn.addEventListener('click', () => {
          console.log('[On3 StaffFilter] Staff button clicked');
          setTimeout(save, 0);
        });
      }

      if (allBtn) {
        allBtn.addEventListener('click', () => {
          console.log('[On3 StaffFilter] All button clicked');
          setTimeout(save, 0);
        });
      }
    }

    // Mark intra-thread pagination vs leaving the thread
    document.addEventListener('click', function (e) {
      const a = e.target && e.target.closest && e.target.closest('a[href]');
      if (!a) return;
      // ignore new-tab/modified or in-page links
      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
      if (a.target && a.target !== '' && a.target !== '_self') return;
      const href = a.getAttribute('href');
      if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;

      const destTid = parseTid(href);
      if (destTid && String(destTid) === String(tid)) {
        // pagination inside this thread
        try { sessionStorage.setItem(HINT, '1'); } catch {}
      } else {
        // leaving the thread -> clear so returning defaults to All posts
        resetToAll();
        try { sessionStorage.removeItem(HINT); } catch {}
      }
    }, true);

    // Set up event handlers
    attachEvents();

    // Initial entry - check if we came from same thread or should reset
    entry();

    // Also handle XenForo's page-load event (AJAX navigation)
    document.addEventListener('xf:page-load', entry);

    // Handle bfcache/page restoration
    window.addEventListener('pageshow', function (ev) {
      let within = false;
      try { within = sessionStorage.getItem(HINT) === '1'; } catch {}
      if (ev.persisted && !within) resetToAll();
      try { sessionStorage.removeItem(HINT); } catch {}
      applyFromStorage();
    });

    console.log('[On3 StaffFilter] Behavior setup complete');
  }

  // Try injection with retries
  function tryInject() {
    if (injectControls()) {
      setupBehavior();
      return true;
    }
    return false;
  }

  // Try immediately
  if (tryInject()) {
    console.log('[On3 StaffFilter] ✓ Injected immediately');
    return;
  }

  // Listen for XenForo's page-load event (fires on AJAX navigation)
  document.addEventListener('xf:page-load', () => {
    console.log('[On3 StaffFilter] xf:page-load event fired');
    setTimeout(tryInject, 200);
  });

  // Retry on DOM ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      console.log('[On3 StaffFilter] DOMContentLoaded fired');
      setTimeout(tryInject, 200);
    });
  } else {
    setTimeout(tryInject, 300);
  }

  // Also try on window load
  window.addEventListener('load', () => {
    console.log('[On3 StaffFilter] Window load fired');
    if (!document.getElementById('xfStaffOnlyOn')) {
      setTimeout(tryInject, 200);
    }
  });

  // Fallback: MutationObserver (watches for DOM changes)
  const observer = new MutationObserver(() => {
    if (!document.getElementById('xfStaffOnlyOn')) {
      if (tryInject()) {
        console.log('[On3 StaffFilter] ✓ Injected via MutationObserver');
        observer.disconnect();
      }
    }
  });

  if (document.body) {
    observer.observe(document.body, { childList: true, subtree: true });
    console.log('[On3 StaffFilter] MutationObserver started');
  } else {
    const bodyObserver = new MutationObserver(() => {
      if (document.body) {
        observer.observe(document.body, { childList: true, subtree: true });
        console.log('[On3 StaffFilter] MutationObserver started (after body created)');
        bodyObserver.disconnect();
      }
    });
    bodyObserver.observe(document.documentElement, { childList: true });
  }

  // Cleanup observer after 15s
  setTimeout(() => {
    observer.disconnect();
    if (!document.getElementById('xfStaffOnlyOn')) {
      console.warn('[On3 StaffFilter] ⚠ Failed to inject after 15s timeout');
    }
  }, 15000);
})();