Sort posts by staff

 avatar
unknown
plain_text
2 days ago
12 kB
7
No Index
// ==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);
})();

Editor is loading...
Leave a Comment