Reddit Delete Chat Messages

Add UI to auto-mark and delete Reddit chat messages (with username awareness)

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Reddit Delete Chat Messages
// @namespace    http://tampermonkey.net/
// @version      3.0.2
// @description  Add UI to auto-mark and delete Reddit chat messages (with username awareness)
// @author       mrrobot
// @match        https://chat.reddit.com/*
// @match        https://www.reddit.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ============================== Config ==============================
  const CLICK_DELAY_MS = 800;              // pacing between actions
  const MENU_WAIT_MS = 350;
  const DIALOG_OPEN_WAIT_MS = 250;         // pause after clicking Delete before dialog search/confirm
  const HOVER_REVEAL_RETRIES = 10;
  const HOVER_REVEAL_INTERVAL_MS = 150;
  const PRE_MENU_EXTRA_WAIT_MS = 400;
  const BATCH_SIZE_DEFAULT = 10;

  // Auto-confirm dialog behavior
  const AUTO_CONFIRM_ENABLED = true;
  const AUTO_CONFIRM_COOLDOWN_MS = 900;    // min gap between auto-confirms
  const OBSERVER_TICK_MS = 80;             // throttle observer reaction
  const DIALOG_DISAPPEAR_TIMEOUT_MS = 8000;
  const NODE_REMOVAL_TIMEOUT_MS = 8000;

  // Continuous sweep tuning
  const SWEEP_BATCH_SIZE = 10;             // max deletes per sweep cycle
  const SWEEP_IDLE_PAUSE_MS = 500;         // pause between cycles
  const SCROLL_STEP_PX = 1200;             // upward scroll step to load older messages
  const SCROLL_SETTLE_MS = 650;            // wait after scrolling for virtualization to render
  const TOP_STALL_MAX = 4;                 // consecutive "at top & nothing new" cycles before stopping

  // LocalStorage key for saved username (avoid collisions across versions)
  const LS_KEY_USERNAME = 'rc_username_v30_public';

  // ============================ UI Panel =============================
  const panel = document.createElement('div');
  Object.assign(panel.style, {
    position:'fixed', right:'16px', bottom:'16px', zIndex:2147483647,
    background:'rgba(17,17,17,.96)', color:'#fff', padding:'12px', borderRadius:'12px',
    fontFamily:'ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial',
    fontSize:'12px', boxShadow:'0 8px 24px rgba(0,0,0,.35)', maxWidth:'600px'
  });
  const savedName = localStorage.getItem(LS_KEY_USERNAME) || '';
  panel.innerHTML = `
    <div style="font-weight:700;margin-bottom:6px">Reddit Chat — Username Aware v3.0.2</div>

    <div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;margin-bottom:8px">
      <input id="rc-username" type="text" placeholder="Your Reddit username (no u/)"
             value="${savedName.replace(/"/g, '&quot;')}"
             style="padding:6px 8px;border-radius:8px;border:1px solid #444;background:#111;color:#fff;outline:none">
      <button id="rc-save" style="padding:6px 8px;border:none;border-radius:8px;cursor:pointer">Save Username</button>
    </div>

    <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
      <button id="rc-pick">Pick Container</button>
      <button id="rc-parent">Select Parent</button>
      <button id="rc-auto-you">Auto-Mark "You said"</button>
      <button id="rc-auto-user">Auto-Mark by Username</button>
      <button id="rc-mark">Toggle Mark Mode</button>
      <button id="rc-clear">Clear Marks</button>
      <button id="rc-del-one" style="background:#ef4444;color:#fff">Delete One Marked</button>
      <button id="rc-del-batch" style="background:#ef4444;color:#fff">Delete ${BATCH_SIZE_DEFAULT} Marked</button>
      <button id="rc-menulog">Toggle Menu Log</button>
      <button id="rc-dump">Debug Dump</button>
    </div>

    <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
      <button id="rc-sweep-start" style="background:#10b981;color:#001">Start Continuous Sweep</button>
      <button id="rc-sweep-stop"  style="background:#f59e0b;color:#001">Stop Sweep</button>
      <span id="rc-sweep-status" style="align-self:center;opacity:.9"></span>
    </div>

    <div style="opacity:.9;margin-bottom:6px">
      Tip: Save your username → <b>Pick Container</b> → Start Continuous Sweep (or manual: Auto-Mark + Delete).
      Auto-confirm is ${AUTO_CONFIRM_ENABLED ? '<b>ON</b>' : '<b>OFF</b>'}.
    </div>
    <div id="rc-log" style="max-height:360px;overflow:auto;white-space:pre-wrap;line-height:1.25"></div>
  `;
  for (const b of panel.querySelectorAll('button')) {
    Object.assign(b.style, { padding:'6px 8px', border:'none', borderRadius:'8px', cursor:'pointer' });
  }
  document.documentElement.appendChild(panel);

  const logEl = panel.querySelector('#rc-log');
  logEl.style.maxHeight = '50px';
  logEl.style.overflowY = 'auto';
  const statusEl = panel.querySelector('#rc-sweep-status');
  const inpUser = panel.querySelector('#rc-username');
  const btnSave = panel.querySelector('#rc-save');
  const btnPick = panel.querySelector('#rc-pick');
  const btnParent = panel.querySelector('#rc-parent');
  const btnAutoYou = panel.querySelector('#rc-auto-you');
  const btnAutoUser = panel.querySelector('#rc-auto-user');
  const btnMark = panel.querySelector('#rc-mark');
  const btnClear = panel.querySelector('#rc-clear');
  const btnDelOne = panel.querySelector('#rc-del-one');
  const btnDelBatch = panel.querySelector('#rc-del-batch');
  const btnMenu = panel.querySelector('#rc-menulog');
  const btnDump = panel.querySelector('#rc-dump');
  const btnSweepStart = panel.querySelector('#rc-sweep-start');
  const btnSweepStop = panel.querySelector('#rc-sweep-stop');

  const log = (m)=>{ const t=new Date().toLocaleTimeString(); logEl.textContent = `[${t}] ${m}\n` + (logEl.textContent||''); };

  // ============================ Utilities ============================
  function* deepNodes(root){
    yield root;
    const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
    while (tw.nextNode()){
      const el = tw.currentNode;
      yield el;
      if (el.shadowRoot){ yield el.shadowRoot; yield* deepNodes(el.shadowRoot); }
      if (el.tagName === 'IFRAME'){
        try{ if (el.contentDocument){ yield el.contentDocument; yield* deepNodes(el.contentDocument); } }catch(_){}
      }
    }
  }
  function deepQueryAll(sel, scope=document){
    const out = [];
    for (const n of deepNodes(scope)) if (n.querySelectorAll) { try { out.push(...n.querySelectorAll(sel)); } catch(_) {} }
    return Array.from(new Set(out));
  }
  function localQuery(scope, sel){
    const out = [];
    (function walk(n){
      if (!n) return;
      if (n.querySelectorAll) { try { out.push(...n.querySelectorAll(sel)); } catch(_) {} }
      if (n.shadowRoot) walk(n.shadowRoot);
      for (const c of n.children || []) walk(c);
    })(scope);
    return out[0] || null;
  }
  function outline(el, css='2px solid #f59e0b'){ if (el){ el.style.outline=css; el.style.outlineOffset='0'; } }
  const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
  function isVisible(el){ if(!el) return false; const cs=getComputedStyle(el); const r=el.getBoundingClientRect(); return cs.display!=='none' && cs.visibility!=='hidden' && cs.opacity!=='0' && r.width>0 && r.height>0; }
  function pressEsc(){ document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,which:27,bubbles:true})); }

  // ====================== Container & Node Access =====================
  let selectedContainer=null;
  function countEvents(scope){ return deepQueryAll('rs-timeline-event', scope).length; }
  function getEvents(scope){ return deepQueryAll('rs-timeline-event', scope).filter(isVisible); }
  function bubbleToTimelineContainer(node){
    let cur=node, best=null, bestCount=0;
    for(let i=0;i<12 && cur;i++){
      const cnt = countEvents(cur);
      if (cnt>bestCount){ best=cur; bestCount=cnt; }
      cur = cur.parentNode instanceof ShadowRoot ? cur.parentNode.host : cur.parentElement;
    }
    return best||node;
  }
  function pickElementOnce(){
    return new Promise(res=>{
      panel.style.pointerEvents='none';
      const onKey=e=>{ if(e.key==='Escape') cleanup(null); };
      const onClick=e=>{ e.preventDefault(); e.stopPropagation(); const t=e.composedPath?e.composedPath()[0]:e.target; cleanup(t); };
      function cleanup(v){
        document.removeEventListener('click', onClick, true);
        document.removeEventListener('keydown', onKey, true);
        panel.style.pointerEvents=''; res(v);
      }
      document.addEventListener('click', onClick, true);
      document.addEventListener('keydown', onKey, true);
      log('Pick mode: click the message list (Esc cancels)…');
    });
  }

  // ======================== Scrolling Helpers ========================
  function getScrollable(){
    // Prefer inner virtual scroll container if present
    const vs = selectedContainer && localQuery(selectedContainer, 'rs-virtual-scroll-dynamic, rs-virtual-scroll, [class*="virtual-scroll"], [data-testid*="virtual"]');
    if (vs) return vs;
    // Otherwise, nearest ancestor that scrolls
    let cur = selectedContainer;
    for (let i=0; i<10 && cur; i++){
      const el = cur instanceof ShadowRoot ? cur.host : cur;
      const cs = getComputedStyle(el);
      if ((cs.overflowY==='auto' || cs.overflowY==='scroll') && el.scrollHeight > el.clientHeight) return el;
      cur = el.parentNode instanceof ShadowRoot ? el.parentNode.host : el.parentElement;
    }
    // Fallback to main/document
    const main = document.querySelector('main');
    if (main && main.scrollHeight > main.clientHeight) return main;
    return document.scrollingElement || document.documentElement || document.body;
  }
  function atTop(scrollEl){ return !scrollEl ? true : (scrollEl.scrollTop <= 2); }
  async function nudgeUp(scrollEl){
    if (!scrollEl) return;
    if (scrollEl.scrollTop > 0){
      scrollEl.scrollTop = Math.max(0, scrollEl.scrollTop - SCROLL_STEP_PX);
    } else {
      scrollEl.scrollTop = 1; await sleep(30); scrollEl.scrollTop = 0;
    }
    await sleep(SCROLL_SETTLE_MS);
  }

  // ==================== Marked ID Set (virtualization) ====================
  const markedIdSet = new Set();
  function getEventId(evt){
    const id = evt.getAttribute && evt.getAttribute('data-id');
    if (id) return id;
    const child = localQuery(evt, '[data-id]');
    return child ? child.getAttribute('data-id') : null;
  }
  function ensureMarksFromIdSet(){
    if (!selectedContainer || !markedIdSet.size) return;
    for (const evt of getEvents(selectedContainer)){
      const id = getEventId(evt);
      if (id && markedIdSet.has(id)){
        if (evt.dataset.rcMarked!=='true'){ setMarked(evt,true); }
      }
    }
  }

  // ====================== Marking & Ownership ======================
  let markMode=false;
  function setMarked(evt,on){
    if (on){ evt.dataset.rcMarked='true'; evt.style.boxShadow='0 0 0 2px #22c55e inset'; evt.style.outline='2px solid #22c55e'; }
    else { delete evt.dataset.rcMarked; evt.style.boxShadow=''; evt.style.outline=''; }
  }
  function toggleMark(evt){
    const on = !(evt.dataset.rcMarked==='true');
    setMarked(evt, on);
    const id = getEventId(evt);
    if (id){
      if (on) markedIdSet.add(id); else markedIdSet.delete(id);
    }
  }
  function attachMarkHandlers(container){
    for (const evt of getEvents(container)){
      if (evt._rcBound) continue;
      evt._rcBound = true;
      evt.addEventListener('click',(e)=>{
        if (!markMode) return;
        e.preventDefault(); e.stopPropagation();
        toggleMark(evt);
      }, true);
    }
  }
  function isAriaYouSaid(evt){
    const m = localQuery(evt, '.room-message[aria-label]');
    const aria = m ? (m.getAttribute('aria-label')||'') : '';
    return /^you said\b/i.test(aria.trim());
  }
  function isByUsername(evt, username){
    if (!username) return false;
    const uname = String(username).trim().toLowerCase();
    const m = localQuery(evt, '.room-message[aria-label]');
    const aria = m ? (m.getAttribute('aria-label')||'') : '';
    if (new RegExp(`^${uname}\\s+said\\b`, 'i').test(aria.trim())) return true;
    const n = localQuery(evt, '.user-name, [class*="user-name"], [data-testid="message-author"]');
    if (n){
      const txt = (n.innerText || n.textContent || '').trim().toLowerCase();
      if (txt === uname) return true;
    }
    return false;
  }
  function autoMarkYouSaid(){
    if (!selectedContainer){ log('Pick a container first.'); return 0; }
    let count=0;
    for (const evt of getEvents(selectedContainer)){
      if (isAriaYouSaid(evt)){
        setMarked(evt,true);
        const id = getEventId(evt); if (id) markedIdSet.add(id);
        count++;
      }
    }
    log(`Auto-Mark "You said": marked ${count}.`);
    return count;
  }
  function autoMarkByUsername(username){
    if (!selectedContainer){ log('Pick a container first.'); return 0; }
    if (!username){ log('No username set. Enter it above and click Save Username.'); return 0; }
    let count=0;
    for (const evt of getEvents(selectedContainer)){
      if (isByUsername(evt, username)){
        setMarked(evt,true);
        const id = getEventId(evt); if (id) markedIdSet.add(id);
        count++;
      }
    }
    log(`Auto-Mark by Username (“${username}”): marked ${count}.`);
    return count;
  }

  // =================== Action Bar / Menu Access ===================
  async function prepareForActions(evt){
    evt.scrollIntoView({block:'center'});
    evt.focus?.();
    evt.dispatchEvent(new MouseEvent('mouseenter',{bubbles:true}));
    await sleep(120);
    const inner = localQuery(evt,'[data-testid], rs-message, rs-bubble, div, span') || evt;
    inner.dispatchEvent(new MouseEvent('mouseenter',{bubbles:true}));
    await sleep(PRE_MENU_EXTRA_WAIT_MS);
  }
  async function openActions(evt){
    for (let i=0;i<HOVER_REVEAL_RETRIES;i++){
      await prepareForActions(evt);

      // direct trash
      let trash = localQuery(evt, [
        '[aria-label="Delete"]','[title="Delete"]','[data-testid="delete-message"]',
        'button:has(svg[aria-label="delete"])','rs-icon-button[icon="trash"]','rs-icon-button[icon="delete"]','rs-button[aria-label="Delete"]'
      ].join(','));
      if (trash && isVisible(trash)) return { directDelete: trash };

      // 3-dots
      let more = localQuery(evt, [
        '[aria-label="More options"]','[aria-label="More"]','[title="More"]',
        'button[aria-haspopup="menu"]','[role="button"][aria-haspopup="menu"]',
        'button:has(svg[aria-label="more"])','rs-icon-button[icon="more"]'
      ].join(','));
      if (more && isVisible(more)){
        more.click();
        await sleep(MENU_WAIT_MS);
        return { openedMenu: true };
      }

      // context menu fallback
      evt.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,button:2}));
      await sleep(MENU_WAIT_MS);
      const anyMenu = deepQueryAll('[role="menuitem"], rs-menu-item, rs-dropdown-item').length;
      if (anyMenu) return { openedMenu: true };

      await sleep(HOVER_REVEAL_INTERVAL_MS);
    }
    return {};
  }

  // ========================= Menu Helpers =========================
  let MENU_LOG = true;  // single declaration (no duplicates)
  function listMenuItems(){
    if(!MENU_LOG) return [];
    const items = deepQueryAll('[role="menuitem"], rs-menu-item, rs-dropdown-item');
    const lines = items.slice(0,40).map((el,i)=>`${i+1}. ${(el.getAttribute('aria-label')||'').trim()} "${(el.innerText||el.textContent||'').trim()}"`).join('\n');
    if(lines) log('Menu items:\n'+lines);
    return items;
  }
  function findDeleteMenuItem(){
    const items = deepQueryAll('[role="menuitem"], rs-menu-item, rs-dropdown-item');
    for(const el of items){
      const aria=(el.getAttribute('aria-label')||'').toLowerCase();
      const txt=(el.innerText||el.textContent||'').toLowerCase();
      if(aria.includes('delete') || txt.includes('delete')) return el;
      if(aria.includes('remove') || txt.includes('remove message') || txt.includes('remove')) return el;
    }
    return null;
  }

  // =================== Dialog Auto-Confirm (safe) ===================
  let confirmLock = false;
  let lastConfirmTs = 0;
  let activeDialog = null;
  let activeDialogVisible = false;
  let observerPending = false;

  function findVisibleDeleteDialog(){
    const rsDlg = deepQueryAll('rs-delete-message-modal rpl-dialog').find(isVisible);
    if (rsDlg) return rsDlg;
    const candidates = deepQueryAll('rpl-dialog, [role="dialog"], div').filter(isVisible);
    return candidates.find(el => {
      const txt = (el.innerText || '').toLowerCase();
      return txt.includes('delete this message?') || (txt.includes('delete') && txt.includes("you can't undo"));
    }) || null;
  }
  function clickYesDelete(dlg){
    const btns = deepQueryAll('button, [role="button"]', dlg);
    const yes = btns.find(b => /yes,\s*delete/i.test((b.innerText || b.textContent || '').trim()));
    if (yes){ yes.click(); return 'yes'; }
    const fallback = btns.find(b => /delete|confirm|yes/i.test((b.innerText||b.textContent||'').trim()));
    if (fallback){ fallback.click(); return 'fallback'; }
    return null;
  }
  async function waitDialogGone(timeoutMs=DIALOG_DISAPPEAR_TIMEOUT_MS){
    const start=performance.now();
    while(performance.now()-start<timeoutMs){
      const d = findVisibleDeleteDialog();
      if (!d){
        activeDialog = null;
        activeDialogVisible = false;
        return true;
      }
      await sleep(80);
    }
    return false;
  }
  async function tryAutoConfirm(reason='observer'){
    if (!AUTO_CONFIRM_ENABLED) return;
    if (confirmLock) return;
    const now = performance.now();
    if (now - lastConfirmTs < AUTO_CONFIRM_COOLDOWN_MS) return;

    const dlg = findVisibleDeleteDialog();
    if (!dlg){ activeDialog = null; activeDialogVisible = false; return; }
    if (dlg === activeDialog && activeDialogVisible) return;

    activeDialog = dlg;
    activeDialogVisible = true;

    confirmLock = true;
    try {
      const which = clickYesDelete(dlg);
      if (which){
        lastConfirmTs = performance.now();
        log(`Auto-confirm (${reason}): clicked ${which === 'yes' ? '"Yes, Delete"' : 'fallback confirm'}.`);
        await waitDialogGone();
      } else {
        log('Auto-confirm: confirm button not found.');
      }
    } catch (e){
      log('Auto-confirm error: ' + (e && e.message ? e.message : e));
    } finally {
      confirmLock = false;
    }
  }
  const modalObserver = new MutationObserver(() => {
    if (observerPending) return;
    observerPending = true;
    setTimeout(() => {
      observerPending = false;
      tryAutoConfirm('observer');
    }, OBSERVER_TICK_MS);
  });
  modalObserver.observe(document.documentElement, {subtree:true, childList:true});

  // ========================== Delete Core ==========================
  async function waitNodeRemoved(node, timeoutMs=NODE_REMOVAL_TIMEOUT_MS){
    const start = performance.now();
    while (performance.now() - start < timeoutMs){
      if (!node.isConnected) return true;
      await sleep(80);
    }
    return false;
  }
  async function deleteEvent(evt){
    const id = getEventId(evt);
    const open = await openActions(evt);

    if(open.directDelete){
      log('Clicking direct Delete…');
      open.directDelete.click();
      await sleep(DIALOG_OPEN_WAIT_MS);
      await tryAutoConfirm('direct');
      await waitDialogGone();
      await waitNodeRemoved(evt);
      if (id) markedIdSet.delete(id);
      pressEsc();
      await sleep(CLICK_DELAY_MS);
      return true;
    }

    listMenuItems();
    const del = findDeleteMenuItem();
    if(!del){ log('No Delete/Remove in menu (might not be your message, or thread is persistent).'); return false; }
    del.click();
    await sleep(DIALOG_OPEN_WAIT_MS);
    await tryAutoConfirm('menu');
    await waitDialogGone();
    await waitNodeRemoved(evt);
    if (id) markedIdSet.delete(id);
    pressEsc();
    await sleep(CLICK_DELAY_MS);
    return true;
  }

  // ============================ Buttons ============================
  btnSave.addEventListener('click', ()=>{
    const name = (inpUser.value || '').trim();
    localStorage.setItem(LS_KEY_USERNAME, name);
    log(name ? `Saved username: ${name}` : 'Username cleared.');
  });

  btnPick.addEventListener('click', async ()=>{
    const picked = await pickElementOnce();
    if(!picked){ log('Pick cancelled.'); return; }
    selectedContainer = bubbleToTimelineContainer(picked) || picked;
    outline(selectedContainer);
    attachMarkHandlers(selectedContainer);
    ensureMarksFromIdSet();
    log(`Picked container. ~${countEvents(selectedContainer)} timeline events.`);
  });

  btnParent.addEventListener('click', ()=>{
    if(!selectedContainer){ log('Pick a container first.'); return; }
    const parent = selectedContainer.parentNode instanceof ShadowRoot ? selectedContainer.parentNode.host : selectedContainer.parentElement;
    if(!parent){ log('No parent to select.'); return; }
    selectedContainer = bubbleToTimelineContainer(parent) || parent;
    outline(selectedContainer);
    attachMarkHandlers(selectedContainer);
    ensureMarksFromIdSet();
    log(`Selected parent. ~${countEvents(selectedContainer)} timeline events.`);
  });

  btnAutoYou.addEventListener('click', ()=> { autoMarkYouSaid(); ensureMarksFromIdSet(); });
  btnAutoUser.addEventListener('click', ()=>{
    const name = (inpUser.value || '').trim();
    autoMarkByUsername(name);
    ensureMarksFromIdSet();
  });

  btnMark.addEventListener('click', ()=>{
    if(!selectedContainer){ log('Pick a container first.'); return; }
    markMode = !markMode;
    btnMark.textContent = markMode ? 'Mark Mode (ON)' : 'Toggle Mark Mode';
    if(markMode){ attachMarkHandlers(selectedContainer); log('Mark Mode ON: click bubbles to toggle.'); }
    else { log('Mark Mode OFF.'); }
  });

  btnClear.addEventListener('click', ()=>{
    if(!selectedContainer){ log('Pick a container first.'); return; }
    for(const evt of getEvents(selectedContainer)) setMarked(evt,false);
    markedIdSet.clear();
    log('Cleared marks.');
  });

  btnDelOne.addEventListener('click', async ()=>{
    if(!selectedContainer){ log('Pick a container first.'); return; }
    ensureMarksFromIdSet();
    const targets = getEvents(selectedContainer).filter(e=>e.dataset.rcMarked==='true');
    if(!targets.length){ log('No marked messages. Use Auto-Mark or Mark Mode.'); return; }
    const t = targets[0];
    const ok = await deleteEvent(t);
    if(ok) { setMarked(t,false); log('Deleted one marked.'); } else { log('Delete failed for that bubble.'); }
  });

  btnDelBatch.addEventListener('click', async ()=>{
    if(!selectedContainer){ log('Pick a container first.'); return; }
    let success=0, fail=0;
    for (let i=0; i<BATCH_SIZE_DEFAULT; i++){
      ensureMarksFromIdSet();
      const targets = getEvents(selectedContainer).filter(e=>e.dataset.rcMarked==='true');
      if (!targets.length) break;
      const t = targets[0];
      try{
        const r = await deleteEvent(t);
        if (r){ setMarked(t,false); success++; } else { fail++; }
      }catch(e){
        log('Delete error: ' + (e && e.message ? e.message : e));
        fail++;
      }
    }
    log(`Batch done. Success: ${success}, Fail: ${fail}.`);
  });

  btnMenu.addEventListener('click', ()=>{
    MENU_LOG = !MENU_LOG;
    log('Menu Log ' + (MENU_LOG ? 'ENABLED' : 'DISABLED'));
  });

  btnDump.addEventListener('click', ()=>{
    ensureMarksFromIdSet();
    const events = selectedContainer ? getEvents(selectedContainer).slice(0,8) : [];
    const info = {
      url: location.href,
      hasSelectedContainer: !!selectedContainer,
      approxEventCount: selectedContainer ? countEvents(selectedContainer) : 0,
      sampleIds: events.map(e => getEventId(e)),
      sampleAria: events.map(e => {
        const m = localQuery(e, '.room-message[aria-label]');
        return m ? m.getAttribute('aria-label') : null;
      }),
      sampleAuthors: events.map(e => {
        const n = localQuery(e, '.user-name, [class*="user-name"], [data-testid="message-author"]');
        return n ? (n.innerText || n.textContent || '').trim() : null;
      })
    };
    log('DEBUG:\n' + JSON.stringify(info,null,2));
  });

  // ======================= Continuous Sweep =======================
  let sweepRunning = false;
  function setStatus(txt){ statusEl.textContent = txt || ''; }

  async function markVisiblePass(){
    const uname = (inpUser.value || '').trim();
    let n=0;
    if (uname){ n += autoMarkByUsername(uname); }
    n += autoMarkYouSaid(); // also try aria variant
    ensureMarksFromIdSet();
    return n;
  }
  async function deleteSomeMarked(maxCount=SWEEP_BATCH_SIZE){
    let success=0, fail=0;
    for (let i=0; i<maxCount; i++){
      ensureMarksFromIdSet();
      const targets = getEvents(selectedContainer).filter(e=>e.dataset.rcMarked==='true');
      if (!targets.length) break;
      const t = targets[0];
      try{
        const r = await deleteEvent(t);
        if (r){ setMarked(t,false); success++; } else { fail++; }
      }catch(e){
        log('Delete error: ' + (e && e.message ? e.message : e));
        fail++;
      }
    }
    return {success, fail};
  }
  async function sweepLoop(){
    if (!selectedContainer){ log('Pick a container first.'); setStatus('Pick container first'); return; }
    if (sweepRunning) return;
    sweepRunning = true;
    setStatus('Sweeping…');
    log('Continuous Sweep started.');

    let topStalls = 0;
    let totalDeleted = 0;

    while (sweepRunning){
      attachMarkHandlers(selectedContainer);
      ensureMarksFromIdSet();

      const newlyMarked = await markVisiblePass();
      if (newlyMarked > 0){
        const {success, fail} = await deleteSomeMarked(SWEEP_BATCH_SIZE);
        totalDeleted += success;
        log(`Sweep cycle: deleted ${success}, failed ${fail}, total so far ${totalDeleted}.`);
        topStalls = 0;
        await sleep(SWEEP_IDLE_PAUSE_MS);
        continue;
      }

      // No marked visible — scroll up to load more
      const scrollEl = getScrollable();
      if (!scrollEl){ log('No scrollable element found; stopping sweep.'); break; }
      const wasAtTop = atTop(scrollEl);
      await nudgeUp(scrollEl);
      attachMarkHandlers(selectedContainer);
      ensureMarksFromIdSet();

      const afterScrollMarked = await markVisiblePass();
      if (afterScrollMarked > 0){ topStalls = 0; continue; }

      if (wasAtTop && atTop(scrollEl)){
        topStalls++;
        log(`No more messages above (top stall ${topStalls}/${TOP_STALL_MAX}).`);
        if (topStalls >= TOP_STALL_MAX){
          log('Reached top with no more deletable messages. Stopping sweep.');
          break;
        }
      }
      await sleep(SWEEP_IDLE_PAUSE_MS);
    }

    sweepRunning = false;
    setStatus('');
    log('Continuous Sweep stopped.');
  }

  btnSweepStart.addEventListener('click', ()=>{ if (!sweepRunning) sweepLoop(); });
  btnSweepStop.addEventListener('click', ()=>{ sweepRunning = false; setStatus('Stopping…'); });

  // =================== SPA Navigation & Reapply ===================
  let lastUrl = location.href;
  new MutationObserver(()=>{
    if(location.href!==lastUrl){
      lastUrl=location.href; selectedContainer=null; log('URL changed — pick container again.');
      sweepRunning = false; setStatus('');
    }
  }).observe(document,{subtree:true, childList:true});

  const reapplyObserver = new MutationObserver(()=>{
    if (!selectedContainer) return;
    ensureMarksFromIdSet();
  });
  reapplyObserver.observe(document.body, {subtree:true, childList:true});

  log('Loaded v3.0.2. Continuous Sweep available — Start/Stop to auto-scroll up and remove your messages.');
})();