Skip to content

Latest commit

 

History

History
2104 lines (1654 loc) · 78.2 KB

File metadata and controls

2104 lines (1654 loc) · 78.2 KB

Technical Documentation (v3.3.0 Filtering, Recovery & UI Shell Notes)

Overview

FilterTube v3.3.0 builds on the earlier performance and whitelist-mode work with watch-page SPA recovery hardening, Mix/watch fallback-menu fixes, stronger collaboration roster recovery, cross-browser subscribed-channels import hardening, and smaller UX controls around backups and menu injection. This technical documentation covers the filtering logic, identity recovery behavior, mode switching, and user experience enhancements.

Collaboration roster checkpoint (2026-04-28)

The collaboration pipeline now treats YouTube's explicit Collaborators sheet as authoritative:

shortBylineText.runs[0]
  .navigationEndpoint.showSheetCommand
  .panelLoadingStrategy.inlineContent.sheetViewModel
  .header.panelHeaderViewModel.title.content == "Collaborators"

Why this matters:

  • prefetch/global search can see several candidates for the same videoId
  • avatar stacks and direct list models are useful warm-up fallbacks, but they can contain weak composite labels
  • a longer fallback list is not necessarily richer than the actual Collaborators sheet

Runtime rules:

  • injector.js tags header-backed rosters as collaborators-sheet and scores them above fallback candidates
  • content_bridge.js and injector.js sanitize collaborator lists before caching, menu rendering, and expected-count stamping
  • placeholder rows such as and 2 more are removed
  • weak name-only composite rows are removed when fully covered by two other collaborator labels, for example Daddy Yankee Bizarrap beside Daddy Yankee and Bizarrap
  • Mix/Radio containers remain excluded from collaborator promotion through renderer, overlay, RD playlist, and Mix -/–/— title guards

Nanah technical checkpoint

FilterTube now also includes a profile-aware device sync surface in Accounts & Sync.

Core modules:

tab-view.js
  -> Accounts & Sync UI
  -> live session state
  -> trusted-link UI

nanah_sync_adapter.js
  -> converts Nanah payloads into FilterTube apply/import requests

io_manager.js
  -> canonical merge/replace/import logic for active/main/kids/full scopes

Nanah client
  -> pairing
  -> SAS verification
  -> signaling
  -> WebRTC data channel

Transport model

sequenceDiagram
    participant A as "Device A"
    participant Relay as "Signaling relay"
    participant B as "Device B"

    A->>Relay: create / host pairing session
    B->>Relay: join pairing session
    A->>B: verify same safety phrase
    B->>A: verify same safety phrase
    A->>B: settings payload over direct data channel
Loading

Technical rules

  • the relay is only for connection setup
  • trusted links live in local extension storage
  • PINs stay local and are never transmitted through Nanah
  • explicit remote target profile can be chosen during a live session
  • managed links can pin one fixed receiver-side local profile for later sessions
  • refresh ends the live session but not the saved trust state

Child approval rule

FIRST MANAGED PARENT -> CHILD CONNECTION
    may require one local parent approval on child device

LATER MATCHING UPDATES
    depends on saved managed-link policy:
      autoApply
      reconnectMode
      lockedChildMode
      strict child protection preset

Typography System (v3.2.6)

Modern Clean Sans-Serif Design

FilterTube v3.2.6 introduces a typography update, replacing the previous serif-based system with a modern, highly readable sans-serif design:

Font Family Changes:

/* Before (v3.2.4 and earlier) */
--ft-font-family-base: "Inter", sans-serif;
--ft-font-family-serif: "Crimson Pro", "Playfair Display", "Georgia", serif;
--ft-font-family-display: "Inter", sans-serif;

/* After (v3.2.6) */
--ft-font-family-base: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
--ft-font-family-serif: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;  /* Unified */
--ft-font-family-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--ft-font-family-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Source Code Pro", monospace;

Rationale:

  • Consistency: All headings and body text now use Inter for a cohesive, modern look
  • Readability: Sans-serif fonts are more readable on screens, especially at smaller sizes
  • Performance: Reduced font loading by eliminating separate serif font families
  • Technical Display: Added dedicated monospace font for code/channel IDs

Font Scale Refinement

The font scale has been tightened for better hierarchy and modern aesthetics:

┌─────────────────────────────────────────────────────────┐
│         FilterTube Typography Scale (v3.2.6)            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Token              Size (rem)  Pixels   Use Case       │
│  ─────────────────  ──────────  ──────   ────────────   │
│  caption            0.75        12px     Hints, labels  │
│  body               0.875       14px     Body text      │
│  base               0.9375      15px     Base size      │
│  subtitle           1.0625      17px     Subheadings    │
│  h4                 1.125       18px     Minor heads    │
│  h3                 1.25        20px     Section heads  │
│  h2                 1.5         24px     Page heads     │
│  h1                 1.875       30px     Main titles    │
│                                                         │
│  Previous Scale (v3.2.4):                               │
│  caption: 13px, body: 15px, base: 16px, h3: 22px,       │
│  h2: 28px, h1: 36px                                     │
│                                                         │
│  Change: Tighter, more refined scale with smaller       │
│  increments for better visual hierarchy                 │
└─────────────────────────────────────────────────────────┘

Line Height & Letter Spacing

Optimized for improved readability and modern aesthetics:

/* Line Heights */
--ft-line-height-tight: 1.3;      /* Headings (was 1.35) */
--ft-line-height-base: 1.6;       /* Body text (was 1.65) */
--ft-line-height-relaxed: 1.75;   /* Unchanged */

/* Letter Spacing */
--ft-letter-spacing-tight: -0.01em;    /* Headings (was -0.02em) */
--ft-letter-spacing-normal: -0.003em;  /* Body (was 0em) */
--ft-letter-spacing-wide: 0.01em;      /* Emphasis (was 0.025em) */

Impact:

  • Tighter line heights reduce vertical space, creating a more compact, modern feel
  • Subtle negative letter spacing improves readability at larger sizes
  • Consistent spacing creates visual harmony across all text elements

Typography Hierarchy Visualization

┌────────────────────────────────────────────────────────────┐
│                    FilterTube                              │  ← H1 (30px, -0.01em)
│                                                            │
│  Dashboard Overview                                        │  ← H2 (24px, -0.01em)
│                                                            │
│  Quick Actions                                             │  ← H3 (20px, -0.01em)
│  ─────────────                                             │
│                                                            │
│  Block Channel                                             │  ← H4 (18px, -0.01em)
│  Manage your blocked channels and keywords                 │  ← Subtitle (17px)
│                                                            │
│  FilterTube helps you curate your YouTube experience       │  ← Base (15px)
│  by blocking unwanted content. All filtering happens       │
│  locally in your browser for maximum privacy.              │
│                                                            │
│  Videos Hidden Today: 42                                   │  ← Body (14px)
│  Time Saved: 2h 15m                                        │
│                                                            │
│  Hint: Click the 3-dot menu to block channels              │  ← Caption (12px)
│                                                            │
│  UC1234567890abcdefghijk                                   │  ← Mono (code)
└────────────────────────────────────────────────────────────┘

Whitelist Mode Filtering Logic (v3.2.5)

Dual Mode Filtering Engine

The filtering engine now supports both blocklist and whitelist modes:

// Core filtering decision function
function shouldHideContent(title, channel, settings) {
    const listMode = settings.listMode === 'whitelist' ? 'whitelist' : 'blocklist';
    
    if (listMode === 'whitelist') {
        // Whitelist Mode: Hide by default, allow only matches
        return !matchesWhitelist(title, channel, settings);
    } else {
        // Blocklist Mode: Show by default, hide matches
        return matchesBlocklist(title, channel, settings);
    }
}

function matchesWhitelist(title, channel, settings) {
    // Check channel whitelist
    if (settings.whitelistChannels && 
        settings.whitelistChannels.some(ch => matchesChannel(channel, ch))) {
        return true;
    }
    
    // Check keyword whitelist
    if (settings.whitelistKeywords && 
        settings.whitelistKeywords.some(regex => regex.test(title))) {
        return true;
    }
    
    return false;
}

function matchesBlocklist(title, channel, settings) {
    // Check channel blocklist
    if (settings.filterChannels && 
        settings.filterChannels.some(ch => matchesChannel(channel, ch))) {
        return true;
    }
    
    // Check keyword blocklist
    if (settings.filterKeywords && 
        settings.filterKeywords.some(regex => regex.test(title))) {
        return true;
    }
    
    return false;
}

Subscribed Channels Import Pipeline (v3.3.0 state)

This feature does not reuse the normal manual addChannel() path. It has a dedicated acquisition pipeline because it needs a live signed-in YouTube page context plus bulk merge semantics.

Detailed reference: docs/SUBSCRIBED_CHANNELS_IMPORT.md

Runtime contract

tab-view.js
  -> resolveSubscriptionsImportTab()
  -> waitForYoutubeTabReady()
  -> StateManager.importSubscribedChannelsToWhitelist()

state_manager.js
  -> send message to selected YouTube tab
  -> validate profile/lock state before and after fetch
  -> send merged list to background

content/bridge_settings.js
  -> inject MAIN-world scripts when needed
  -> wait for FilterTube_InjectorBridgeReady
  -> forward progress + response

injector.js
  -> collect /feed/channels seed
  -> keep recent browse-response history
  -> prefer page-driven expansion when available
  -> fetch FEchannels browse pages
  -> normalize entries

background.js
  -> mergeImportedWhitelistChannels()
  -> persist whitelistChannels + legacy mirrors
  -> refresh YouTube tabs

Sequence

sequenceDiagram
    participant UI as "tab-view.js"
    participant Tab as "YouTube /feed/channels tab"
    participant Bridge as "bridge_settings.js"
    participant Main as "injector.js"
    participant BG as "background.js"

    UI->>Tab: ping + wait for import bridge
    UI->>Tab: FilterTube_ImportSubscribedChannels
    Tab->>Bridge: runtime listener
    Bridge->>Main: FilterTube_RequestSubscriptionImport
    Main-->>Bridge: FilterTube_SubscriptionsImportProgress
    Main-->>Bridge: FilterTube_SubscriptionsImportResponse
    Bridge-->>UI: tab response
    UI->>BG: FilterTube_BatchImportWhitelistChannels
    BG-->>UI: counts + current mode
Loading

Data source behavior

The current importer is not purely API-first yet. It does:

  1. /feed/channels page seed lookup
  2. recent real-page browse-response harvesting
  3. FEchannels browse requests
  4. dedupe/merge of normalized entries

That is why the UI's pages read counter is an importer metric, not a strict continuation-page count.

Cross-browser note

The importer now leans more heavily on real page-driven /feed/channels growth because browser/session behavior is not identical:

  • Edge often materializes more rows through active page expansion.
  • Chrome may expose the same continuation chain later or less eagerly.
  • Synthetic continuation replay is still useful, but it is no longer treated as the only source of truth.

Request profile behavior

injector.js builds request profiles from page ytcfg context instead of hardcoding a minimal body:

  • web_fechannels
  • mweb_fechannels

The request can retry with the alternate profile when:

  • the first profile fails
  • the first profile times out
  • the first profile looks logged out and produced no rows

Why /feed/channels is exempt from normal hiding

The subscriptions-management page itself is treated as a control surface rather than a normal feed:

  • blocked channels still remain visible there
  • whitelist building still works after import
  • users can inspect and audit their subscribed-channel roster even if those channels are hidden elsewhere

Menu Injection Controls (v3.3.0)

FilterTube now has two separate direct-action controls:

  • showQuickBlockButton
  • showBlockMenuItem

The first controls the hover quick-block affordance on cards. The second controls whether FilterTube injects its own entry inside YouTube's native 3-dot menu.

This separation matters because some users want the fast hover action but do not want an extra menu item competing with YouTube's native actions.

Stored result shape

Imported rows are normalized into whitelist channel objects with:

  • id
  • handle
  • handleDisplay
  • canonicalHandle
  • customUrl
  • name
  • logo
  • source: 'subscriptions_import'

Merge semantics

background.js performs the canonical merge via mergeImportedWhitelistChannels(...).

Results are counted as:

  • imported
  • updated
  • duplicates
  • skipped

The merge writes to:

  • ftProfilesV4.profiles[activeId].main.whitelistChannels
  • ftProfilesV3.main.whitelistChannels
  • ftProfilesV3.main.whitelistedChannels

and can also update channelMap from imported handle/custom URL identity.

Mode-switch follow-up

After a successful import:

  • Import Only leaves the current blocklist untouched
  • Import + Turn On Whitelist triggers FilterTube_SetListMode

Current implementation note:

  • the existing whitelist activation path merges current blocklist channels and keywords into whitelist and clears the blocklist
  • the subscriptions import modal documents this explicitly so the behavior is not surprising

Whitelist Mode Logic Improvements (v3.3.0 state)

The original whitelist-mode architecture landed earlier, but the current 3.3.0 behavior includes several additional correctness fixes:

  • /feed/channels is exempt from normal whitelist hiding so the subscriptions-management surface stays usable
  • creator/channel pages can use page identity to avoid false hiding while still blocking unresolved feed cards elsewhere
  • YTM renderer coverage is broader, so more mobile cards now participate in whitelist identity extraction and DOM fallback
  • unresolved cards no longer fail open by default on normal feeds just because identity has not arrived yet

Watch Page Protection Logic

Whitelist mode preserves critical watch page functionality to maintain video playback:

// In ensureContentControlStyles() - DOM fallback
const listMode = (settings && settings.listMode === 'whitelist') ? 'whitelist' : 'blocklist';
const hideInfoMaster = (listMode !== 'whitelist') && !!settings.hideVideoInfo;

// Only hide watch page elements if NOT in whitelist mode
if ((listMode !== 'whitelist') && (hideInfoMaster || settings.hideVideoButtonsBar)) {
    rules.push(`
        #actions.ytd-watch-metadata,
        #info > #menu-container {
            display: none !important;
        }
    `);
}

Impact: Ensures video actions, channel row, and description remain visible in whitelist mode for proper playback.

Search Page Indeterminate State Handling

Prevents page blanking when search results render before channel identity is available:

// In shouldHideContent() - DOM fallback
try {
    const path = document.location?.pathname || '';
    const hasNameSignal = Boolean(normalizeChannelNameForComparison(channelMeta?.name || '') || 
                              normalizeChannelNameForComparison(channel || ''));
    
    // Don't hide indeterminate search results to avoid recursive loading
    if (path === '/results' && !hasChannelIdentity && !hasNameSignal && collaboratorMetas.length === 0) {
        return false;
    }
} catch (e) {
    // Fallback to safe behavior
}

Impact: Prevents blank search pages and recursive continuation loads during identity resolution.

Homepage Duplicate Removal Logic

Automatic cleanup of duplicate content in whitelist mode to improve feed quality:

// In applyDOMFallback() - DOM fallback
try {
    const path = document.location?.pathname || '';
    if (path === '/' && listMode === 'whitelist') {
        const items = document.querySelectorAll('ytd-rich-grid-renderer ytd-rich-item-renderer');
        const seen = new Map();
        
        for (const item of items) {
            const videoId = item.getAttribute('data-filtertube-video-id') || extractVideoIdFromCard(item) || '';
            if (!videoId) continue;
            
            const existing = seen.get(videoId);
            if (!existing) {
                seen.set(videoId, item);
                continue;
            }
            
            // Hide duplicates with same video ID
            toggleVisibility(item, true, 'Duplicate item', true);
        }
    }
} catch (e) {
    // Graceful fallback
}

Impact: Cleans up mixed content feeds by removing duplicate videos in whitelist mode.

Whitelist-Pending Processing Optimization

Efficient re-evaluation of content awaiting channel identity to reduce processing overhead:

// In applyDOMFallback() - DOM fallback
const { onlyWhitelistPending = false } = options;

// Process only pending items in whitelist mode for efficiency
const videoElements = (onlyWhitelistPending && listMode === 'whitelist')
    ? document.querySelectorAll(`${VIDEO_CARD_SELECTORS}[data-filtertube-whitelist-pending="true"]`)
    : document.querySelectorAll(VIDEO_CARD_SELECTORS);

Impact: Reduces DOM processing overhead by targeting only relevant elements during whitelist updates.

Search Thumbnail Channel Extraction

Enhanced channel extraction for search pages to prevent whitelist false-negatives:

// In applyDOMFallback() - DOM fallback
let searchThumbAnchor = null;
if (elementTag === 'ytd-video-renderer') {
    try {
        searchThumbAnchor = element.querySelector(
            '#thumbnail a[data-filtertube-channel-handle], ' +
            '#thumbnail a[data-filtertube-channel-id], ' +
            'a#thumbnail[data-filtertube-channel-handle], ' +
            'a#thumbnail[data-filtertube-channel-id], ' +
            '#thumbnail[data-filtertube-channel-handle], ' +
            '#thumbnail[data-filtertube-channel-id]'
        );
    } catch (e) {
        // Fallback handling
    }
}

const relatedElements = [channelAnchor, channelElement, channelSubtitleElement, searchThumbAnchor];

Impact: Prevents first-batch whitelist false-negatives by including thumbnail anchor data in channel extraction.

Watch Page SPA Swap Cleanup

Prevents stuck hidden elements during YouTube's single-page application navigation:

// In applyDOMFallback() - DOM fallback
const watchMeta = document.querySelector('ytd-watch-metadata');
if (watchMeta) {
    watchMeta.querySelectorAll('[data-filtertube-whitelist-pending="true"], [data-filtertube-hidden], .filtertube-hidden, .filtertube-hidden-shelf').forEach(el => {
        try {
            toggleVisibility(el, false, '', true); // Clear stuck flags
        } catch (e) {
            // Individual element cleanup failure
        }
    });
}

Impact: Ensures watch page elements don't remain stuck hidden after navigation between videos.

Follow-up note: this cleanup now matters for collaborator state too. During watch-to-watch SPA swaps, FilterTube re-validates stamped collaborator metadata/menu assumptions against stronger watch roots so a stale A and 2 more placeholder does not survive after the selected playlist row or watch metadata changes.

Whitelist-Pending Re-evaluation Timing

Optimized timing for re-evaluating pending content to reduce "recursive hiding" window:

// In initializeDOMFallback() - content bridge
try {
    if (typeof applyDOMFallback === 'function') {
        // Immediate re-evaluation
        setTimeout(() => {
            try {
                applyDOMFallback(null, { preserveScroll: true, onlyWhitelistPending: true });
            } catch (e) {
                // Fallback handling
            }
        }, 0);
        
        // Delayed re-evaluation for late-arriving content
        setTimeout(() => {
            try {
                applyDOMFallback(null, { preserveScroll: true, onlyWhitelistPending: true });
            } catch (e) {
                // Fallback handling
            }
        }, 90);
    }
} catch (e) {
    // Initialization failure handling
}

Impact: Reduces the window where content might be recursively hidden during search page loading.

Channel Identity Check Optimization

Removed redundant identity checking for improved performance:

// Before: Redundant check
if (hasChannelIdentity && channelMetaMatchesIndex(channelMeta, index, channelMap)) {
    return false;
}

// After: Streamlined check
if (channelMetaMatchesIndex(channelMeta, index, channelMap)) {
    return false;
}

Impact: Minor performance optimization by eliminating redundant boolean checks.

Mode-Aware UI Rendering

The UI adapts display text and controls based on the active mode:

// Render engine updates for mode awareness
function renderKeywords(state, profile = 'main') {
    const mode = profile === 'kids' ? state.kids?.mode : state.mode;
    const listType = mode === 'whitelist' ? 'whitelist' : 'blocked';
    const emptyMessage = mode === 'whitelist' 
        ? 'No keywords allowed' 
        : 'No keywords blocked';
    
    // Render with appropriate labels
}

Background Mode Switching

The background script handles mode switching with list migration:

// Mode switching with staging merge
async function handleSetListMode(request) {
    const { profileType, mode, copyBlocklist } = request;
    
    // Load current profiles
    const profilesV4 = await storageGet(FT_PROFILES_V4_KEY);
    
    // Apply mode switch with optional merge
    if (mode === 'whitelist' && copyBlocklist) {
        mergeBlocklistIntoWhitelist(profilesV4, profileType);
    }
    
    // Update profile mode
    profilesV4.profiles[activeId][profileType].mode = mode;
    
    // Save and trigger refresh
    await storageSet({ [FT_PROFILES_V4_KEY]: profilesV4 });
    triggerContentRefresh();
}

Data Flow Architecture (v3.2.3 - Experimental)

Dual Mode Filtering Data Flow

graph TD
    A[YouTube Content] --> B[Main World Interception]
    B --> C[Extract Channel Data]
    C --> D[Background Processing]
    
    D --> E{Profile Mode?}
    E -->|Blocklist| F[Compile Blocklist Settings]
    E -->|Whitelist| G[Compile Whitelist Settings]
    
    F --> H[Send to Content Scripts]
    G --> H
    H --> I[DOM Stamping with Mode]
    I --> J[Apply Filtering Logic]
    
    J --> K{Content Matches Rules?}
    K -->|Blocklist: Match| L[Hide Content]
    K -->|Blocklist: No Match| M[Show Content]
    K -->|Whitelist: Match| M
    K -->|Whitelist: No Match| L
    
    L --> N[Update UI State]
    M --> N
    N --> O[User Sees Filtered Results]
    
    style E fill:#2196f3
    style F fill:#f44336
    style G fill:#4caf50
    style K fill:#ff9800
    style L fill:#f44336
    style M fill:#4caf50
Loading

Mode Switching Data Flow

graph TD
    A[User Toggles Mode] --> B[UI Component Event]
    B --> C[Send to Background]
    C --> D[Validate Request]
    D --> E{Target Mode?}
    
    E -->|Whitelist| F[Merge Blocklist → Whitelist]
    E -->|Blocklist| G[Set Mode Directly]
    
    F --> H[Update Profiles V4]
    G --> H
    H --> I[Clear Compiled Cache]
    I --> J[Broadcast to All Tabs]
    J --> K[Refresh Content Scripts]
    K --> L[Update UI Controls]
    L --> M[Apply New Filtering]
    
    style F fill:#4caf50
    style G fill:#2196f3
    style H fill:#ff9800
    style M fill:#4caf50
Loading

UI/UX Improvements (v3.2.2)

Optimistic UI Updates with Automatic Restoration

The most significant user experience improvement in v3.2.2 is the implementation of optimistic UI updates for channel blocking:

// Optimistic hide state tracking
const optimisticHideState = [];
let didOptimisticHide = false;

const recordOptimisticHide = (element, meta) => {
    optimisticHideState.push({
        element,
        prevDisplay: element.style.display,
        prevHiddenAttr: element.getAttribute('data-filtertube-hidden'),
        prevHadHiddenClass: element.classList.contains('filtertube-hidden'),
        prevBlocked: { /* capture all data attributes */ }
    });
    markElementAsBlocked(element, meta, 'pending');
    element.style.display = 'none';
    element.classList.add('filtertube-hidden');
};

const restoreOptimisticHide = () => {
    // Restore all saved state if blocking fails
    for (const item of optimisticHideState) {
        // Restore display, classes, and attributes
        element.style.display = item.prevDisplay || '';
        // ... restore other properties
    }
    optimisticHideState.length = 0;
};

Benefits:

  • Instant Feedback: Content hides immediately when user clicks "Block Channel"
  • No Uncertainty: Users see immediate results without wondering if the action worked
  • Automatic Recovery: If blocking fails, content automatically reappears
  • Error Handling: Failed operations show proper error states while maintaining UI consistency

Enhanced Mobile Menu Support

v3.2.2 adds comprehensive support for YouTube mobile menu structures:

// Mobile menu detection and renderer selection
const isMobileMenu = Boolean(menuList.closest?.('ytm-menu-popup-renderer')) || 
                    Boolean(menuContainer?.matches?.('ytm-menu-popup-renderer'));

const rendererTag = isMobileMenu ? 'ytm-menu-service-item-renderer' : 'ytd-menu-service-item-renderer';
const rendererScope = isMobileMenu ? 'ytm-menu-popup-renderer' : 'ytd-menu-popup-renderer';

// Create appropriate menu item for the platform
const filterTubeItem = document.createElement(rendererTag);
filterTubeItem.className = `style-scope ${rendererScope} filtertube-block-channel-item`;

Mobile Enhancements:

  • Proper Renderer Tags: Uses ytm-* tags for mobile instead of ytd-*
  • Scope Handling: Correct CSS scoping for mobile vs desktop
  • Bottom Sheet Support: Handles mobile bottom-sheet containers
  • Touch-Friendly: Maintains proper touch interactions on mobile devices

Debug Gated Logging System

All console output is now gated behind a debug flag to reduce production noise:

const debugLog = (...args) => {
    try {
        if (window.__filtertubeDebug) {
            console.log(...args);
        }
    } catch (e) {
        // Silent fail if debug flag not available
    }
};

// Usage throughout codebase
if (window.__filtertubeDebug) {
    console.log('FilterTube: Detailed operation info');
}

Debug Benefits:

  • Clean Production: No console spam for regular users
  • Full Debug Info: Developers can enable with window.__filtertubeDebug = true
  • Performance: Reduced console overhead in production
  • Selective Logging: Only important errors show without debug flag

Scroll Preservation During Filtering

Enhanced DOM fallback processing respects user scrolling:

const scrollState = window.__filtertubeScrollState || (window.__filtertubeScrollState = {
    lastScrollTs: 0,
    listenerAttached: false
});

// Track user scrolling
window.addEventListener('scroll', () => {
    scrollState.lastScrollTs = Date.now();
}, { passive: true, capture: true });

// Preserve scroll position during filtering
const isUserScrolling = now - (scrollState.lastScrollTs || 0) < 150;
const allowPreserveScroll = preserveScroll && !forceReprocess && !isUserScrolling;

Scroll Benefits:

  • No Jarring Jumps: Scroll position stays stable during filtering
  • User Respect: Doesn't fight user scrolling operations
  • Smooth Experience: Filtering happens invisibly in the background
  • Intelligent Behavior: Only preserves scroll when appropriate

Performance Optimizations (v3.2.1+)

Async DOM Processing with Main Thread Yielding

The core performance breakthrough in v3.2.1+ is the conversion of applyDOMFallback() to async processing with main thread yielding:

async function applyDOMFallback(settings, options = {}) {
    // Run state management prevents overlapping executions
    const runState = window.__filtertubeDomFallbackRunState || 
        (window.__filtertubeDomFallbackRunState = {
            running: false,
            pending: false,
            latestSettings: null,
            latestOptions: null
        });

    if (runState.running) {
        runState.pending = true;
        return;
    }
    runState.running = true;

    // Yield to main thread every 30-60 elements
    const yieldToMain = () => new Promise(resolve => setTimeout(resolve, 0));

    try {
        // Process videoElements with yielding
        for (let elementIndex = 0; elementIndex < videoElements.length; elementIndex++) {
            const element = videoElements[elementIndex];
            // Process element...
            
            if (elementIndex > 0 && elementIndex % 60 === 0) {
                await yieldToMain();
            }
        }
    } finally {
        runState.running = false;
        if (runState.pending) {
            runState.pending = false;
            setTimeout(() => applyDOMFallback(runState.latestSettings, runState.latestOptions), 0);
        }
    }
}

Key Benefits:

  • Prevents browser freezing during large DOM operations
  • Maintains UI responsiveness during heavy filtering
  • Queues overlapping calls instead of running simultaneously

Compiled Regex & Channel Filter Caching

v3.2.1+ introduces persistent caching for expensive regex operations:

// WeakMap-based caching for keyword regexes
const compiledKeywordRegexCache = new WeakMap();

function getCompiledKeywordRegexes(rawList) {
    if (!Array.isArray(rawList) || rawList.length === 0) return [];

    const cached = compiledKeywordRegexCache.get(rawList);
    if (cached) return cached;
    
    const compiled = [];
    for (const keywordData of rawList) {
        try {
            if (keywordData instanceof RegExp) {
                compiled.push(keywordData);
                continue;
            }
            if (keywordData && keywordData.pattern) {
                compiled.push(new RegExp(keywordData.pattern, keywordData.flags || 'i'));
                continue;
            }
            if (typeof keywordData === 'string' && keywordData.trim()) {
                const escaped = keywordData.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                compiled.push(new RegExp(escaped, 'i'));
            }
        } catch (e) {
            // ignore invalid
        }
    }

    compiledKeywordRegexCache.set(rawList, compiled);
    return compiled;
}
// Channel filter index caching
const compiledChannelFilterIndexCache = new WeakMap();

function getCompiledChannelFilterIndex(settings) {
    const list = settings.filterChannels;
    const channelMap = settings.channelMap || {};
    const existing = compiledChannelFilterIndexCache.get(settings);
    if (existing && existing.sourceList === list && existing.sourceChannelMap === channelMap) {
        return existing;
    }
    
    // Build optimized index for fast lookups
    const index = {
        sourceList: list,
        sourceChannelMap: channelMap,
        ids: new Set(),
        handles: new Set(),
        customUrls: new Set(),
        names: new Set(),
        unresolvedHandleKeys: []
    };
    
    compiledChannelFilterIndexCache.set(settings, index);
    return index;
}

Performance Impact:

  • Eliminates repeated regex compilation (expensive operation)
  • Fast O(1) lookups for channel filtering
  • Reduces CPU usage by 60-80% during filtering operations

Batched Storage Updates

Channel map updates are now batched to reduce storage I/O:

// Background script batching system
let channelMapFlushTimer = null;
const pendingChannelMapUpdates = new Map();

function enqueueChannelMapUpdate(key, value) {
    pendingChannelMapUpdates.set(key, value);
    scheduleChannelMapFlush();
}

function scheduleChannelMapFlush() {
    if (channelMapFlushTimer) return;
    channelMapFlushTimer = setTimeout(() => {
        channelMapFlushTimer = null;
        flushChannelMapUpdates(); // Batch write to storage
    }, 250); // 250ms batch window
}

Benefits:

  • Reduces storage operations by 70-90%
  • Prevents storage contention during rapid updates
  • Maintains data consistency with batched writes

Debounced Settings Refresh

Settings updates are throttled to prevent excessive DOM reprocessing:

// Content script debouncing
let pendingStorageRefreshTimer = 0;
let lastStorageRefreshTs = 0;
const MIN_STORAGE_REFRESH_INTERVAL_MS = 250;

function scheduleSettingsRefreshFromStorage() {
    const now = Date.now();
    const elapsed = now - lastStorageRefreshTs;
    if (elapsed >= MIN_STORAGE_REFRESH_INTERVAL_MS) {
        lastStorageRefreshTs = now;
        requestSettingsFromBackground();
        return;
    }
    
    if (pendingStorageRefreshTimer) return;
    const delay = Math.max(0, MIN_STORAGE_REFRESH_INTERVAL_MS - elapsed);
    pendingStorageRefreshTimer = setTimeout(() => {
        pendingStorageRefreshTimer = 0;
        lastStorageRefreshTs = Date.now();
        requestSettingsFromBackground();
    }, delay);
}

Browser-Specific Optimizations

Chromium-based Browsers:

  • Async yielding is highly effective
  • Storage batching provides maximum efficiency
  • Overall performance improvement: 90%+ lag reduction

Firefox-based Browsers:

  • Good improvements but less dramatic
  • Some yielding effectiveness but needs tuning
  • Storage operations may need different batching strategy
  • Ongoing optimization work required

Snapshot Architecture

// In seed.js - comprehensive network interception
function stashNetworkSnapshot(data, dataName) {
    try {
        if (!window.filterTube) return;
        if (!data || typeof data !== 'object') return;
        
        const ts = Date.now();
        
        if (dataName.includes('/youtubei/v1/next')) {
            window.filterTube.lastYtNextResponse = data;
            window.filterTube.lastYtNextResponseName = dataName;
            window.filterTube.lastYtNextResponseTs = ts;
        }
        if (dataName.includes('/youtubei/v1/browse')) {
            window.filterTube.lastYtBrowseResponse = data;
            window.filterTube.lastYtBrowseResponseName = dataName;
            window.filterTube.lastYtBrowseResponseTs = ts;
        }
        if (dataName.includes('/youtubei/v1/player')) {
            window.filterTube.lastYtPlayerResponse = data;
            window.filterTube.lastYtPlayerResponseName = dataName;
            window.filterTube.lastYtPlayerResponseTs = ts;
        }
    } catch (e) {
        // Silently fail to avoid breaking YouTube
    }
}

Snapshot storage:

  • lastYtNextResponse - Latest next feed data with timestamp
  • lastYtBrowseResponse - Latest browse data with timestamp
  • lastYtPlayerResponse - Latest player data with timestamp

Post-Block Enrichment System (v3.2.1)

Intelligent Enrichment Pipeline

// In background.js - rate-limited enrichment
function schedulePostBlockEnrichment(channel, profile = 'main', metadata = {}) {
    const source = metadata?.source || '';
    if (source === 'postBlockEnrichment') return;

    const id = channel?.id || '';
    if (!id || !id.toUpperCase().startsWith('UC')) return;

    // Rate limiting: 6-hour cooldown per channel
    const key = `${profile === 'kids' ? 'kids' : 'main'}:${id.toLowerCase()}`;
    const now = Date.now();
    const lastAttempt = postBlockEnrichmentAttempted.get(key) || 0;
    if (now - lastAttempt < 6 * 60 * 60 * 1000) return;

    // Check if enrichment is needed
    const needsEnrichment = (
        (!channel.handle && !channel.customUrl) || 
        !channel.logo || 
        !channel.name
    );
    if (!needsEnrichment) return;

    // Schedule with random delay (3.5-4s) to avoid patterns
    const delayMs = 3500 + Math.floor(Math.random() * 750);
    
    setTimeout(async () => {
        await handleAddFilteredChannel(
            id,
            false,
            null,
            null,
            { source: 'postBlockEnrichment' },
            profile,
            ''
        );
    }, delayMs);
}

Enrichment features:

  • Smart detection - only enriches channels missing key metadata
  • Rate limited - 6-hour cooldown prevents excessive requests
  • Background processing - doesn't block UI operations
  • Random delays - avoids detectable request patterns
  • Profile-aware - separate tracking for Main and Kids profiles

Enhanced CORS and Error Handling (v3.2.1)

Robust Fetch Strategies

// In background.js - improved fetch with fallbacks
async function fetchChannelInfo(channelIdOrHandle) {
    try {
        const response = await fetch(channelUrl, {
            credentials: 'include',
            headers: { 'Accept': 'text/html' }
        });
        
        // Handle 404s for @handle/about by falling back to @handle
        if (!response.ok && isHandle) {
            const fallbackUrl = `https://www.youtube.com/@${encodedHandle}`;
            return await fetch(fallbackUrl, {
                credentials: 'include',
                headers: { 'Accept': 'text/html' }
            });
        }
        
        return response;
    } catch (error) {
        // CORS errors trigger alternative fetch methods
        if (error.name === 'TypeError' && error.message.includes('CORS')) {
            return await fetchAlternativeMethod(url);
        }
        throw error;
    }
}

OG Meta Tag Extraction (Ultimate Fallback)

// Extract channel info from HTML meta tags when JSON parsing fails
const extractMeta = (key) => {
    const patterns = [
        new RegExp(`<meta[^>]+property=["']${key}["'][^>]+content=["']([^"']+)["'][^>]*>`, 'i'),
        new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+property=["']${key}["'][^>]*>`, 'i')
    ];
    
    for (const re of patterns) {
        const match = html.match(re);
        if (match && match[1]) return decodeHtmlEntities(match[1]);
    }
    return null;
};

// Extract channel name, image, and URL from OG tags
const ogTitle = extractMeta('og:title');
const ogImage = extractMeta('og:image'); 
const ogUrl = extractMeta('og:url');

Watch Identity Resolution as Fallback (v3.2.1)

Video Payload Channel Resolution

// When channel page scraping fails, use video payload data
if (!channelInfo.success && effectiveVideoId) {
    try {
        const isKids = profile === 'kids';
        const identity = isKids
            ? (await performKidsWatchIdentityFetch(effectiveVideoId) || 
               await performWatchIdentityFetch(effectiveVideoId))
            : await performWatchIdentityFetch(effectiveVideoId);

        if (identity && (identity.id || identity.handle || identity.name)) {
            channelInfo = {
                success: true,
                id: identity.id || mappedId || '',
                handle: identity.handle || '',
                name: identity.name || '',
                logo: identity.logo || '',
                customUrl: identity.customUrl || ''
            };
        }
    } catch (e) {
        // Silently fail and use minimal fallback
    }
}

Channel Name Sanitization (v3.2.1)

Smart Name Validation

// In background.js - prevent persisting bad channel names
const isProbablyNotChannelName = (value) => {
    if (!value || typeof value !== 'string') return true;
    const trimmed = value.trim();
    if (!trimmed) return true;
    if (trimmed.startsWith('@')) return true;
    if (trimmed.toLowerCase() === 'channel') return true;
    if (trimmed.includes('•')) return true;
    if (/\bviews?\b/i.test(trimmed)) return true;
    if (/\bago\b/i.test(trimmed)) return true;
    if (/\bwatching\b/i.test(trimmed)) return true;
    return false;
};

const sanitizePersistedChannelName = (value) => {
    if (!value || typeof value !== 'string') return '';
    const trimmed = value.trim();
    if (!trimmed) return '';
    if (isProbablyNotChannelName(trimmed)) return '';
    return trimmed;
};

Current Behavior Notes (v3.2.1)

Proactive System Impact

  • Enrichment is now rare thanks to proactive XHR interception
  • Kids zero-network mode works entirely from intercepted JSON
  • Handle → UC ID resolution uses persisted channelMap first
  • Network fetches are avoided on Kids (allowDirectFetch: false)
  • Main world searches use ytInitialData snapshots when needed

Performance Characteristics

  • Zero-delay blocking - 3-dot menus show correct names instantly
  • Reduced API calls - most identity comes from intercepted JSON
  • Better cache hit rates - shared data across all surfaces
  • Reliable Kids operation - works even when Kids blocks external requests

1. Data Interception: ytInitialData Hook

Motivation: YouTube loads content by injecting JSON data into the page. Traditional filtering waits for the DOM, causing a "flash of content". FilterTube intercepts this data before it renders.

How it works (Simplified): YouTube tries to hand a list of videos to the webpage. FilterTube steps in the middle, takes the list, crosses out the videos you don't want, and then hands the cleaned list to the webpage. The webpage never knows those videos existed.

Technical Flow:

sequenceDiagram
    participant YT as YouTube Server
    participant Hook as FilterTube Hook
    participant Engine as Filter Engine
    participant Page as Web Page

    YT->>Hook: Sends Video Data (JSON)
    Hook->>Engine: "Check this list!"
    Engine->>Engine: Removes Blocked Videos
    Engine->>Hook: Returns Clean List
    Hook->>Page: Delivers Safe Content
    Page->>Page: Renders (Zero Flash)
Loading

2. Data Interception: Fetch Hook

Motivation: YouTube loads more content as you scroll (infinite scroll) using fetch requests. FilterTube must intercept these dynamic requests to ensure new content is also filtered.

How it works (Simplified): When you scroll down, YouTube asks its server for "more videos". FilterTube listens for this request. When the server replies with new videos, FilterTube quickly checks them, removes the bad ones, and then gives the rest to YouTube to show you.

Technical Flow:


+-----------+      +-------------+      +-------------+
|  YouTube  | ---> | window.fetch| ---> |  Original   |
|  (Scroll) |      |   (Proxy)   |      |   Fetch     |
+-----------+      +-------------+      +-------------+
                                               |
                                               v
+-----------+      +-------------+      +-------------+
|  Receive  | <--- | New Response| <--- |   Clone &   |
| Filtered  |      | (Filtered)  |      |   Parse     |
+-----------+      +-------------+      +-------------+
                                               |
                                               v
                                        | FilterTube  |
                                        |   Engine    |
                                        +-------------+

3. Data Interception: XHR Hook

Motivation: YouTube also uses XMLHttpRequest for critical API endpoints (search, browse, guide, next, player). FilterTube intercepts these to ensure comprehensive coverage.

How it works: FilterTube overrides the XMLHttpRequest.prototype.open and send methods to monitor specific YouTube API endpoints. When a matching request completes, it parses the JSON response and runs it through the filter engine before YouTube can process it.

Technical Implementation:

// In js/seed.js - setupXhrInterception()
const xhrEndpoints = [
    '/youtubei/v1/search',      // Search results
    '/youtubei/v1/guide',      // Sidebar recommendations  
    '/youtubei/v1/browse',      // Home feed, channel pages
    '/youtubei/v1/next',       // Infinite scroll pagination
    '/youtubei/v1/player'       // Video player data
];

// Override XMLHttpRequest prototype
const proto = window.XMLHttpRequest.prototype;
const originalOpen = proto.open;
const originalSend = proto.send;

proto.open = function(method, url) {
    this.__filtertube_url = url;  // Store URL for later use
    return originalOpen.apply(this, arguments);
};

proto.send = function() {
    const urlStr = String(this.__filtertube_url || '');
    
    if (xhrEndpoints.some(endpoint => urlStr.includes(endpoint))) {
        this.addEventListener('load', () => {
            try {
                const text = this.responseText;
                const jsonData = JSON.parse(text);
                processWithEngine(jsonData, `xhr:${getPathname(urlStr)}`);
            } catch (e) {
                // Silently handle parsing errors
            }
        }, { once: true });
    }
    
    return originalSend.apply(this, arguments);
};

Key XHR Endpoints Monitored:

  • /youtubei/v1/search - Search results and autocomplete
  • /youtubei/v1/browse - Home feed, channel pages, recommendations
  • /youtubei/v1/next - Infinite scroll pagination (load more)
  • /youtubei/v1/guide - Sidebar guide recommendations
  • /youtubei/v1/player - Video player metadata and related videos

3. Filtering Engine: Recursive Blocking Decision

Motivation: YouTube's data structure is complex and nested. Videos can appear inside "shelves", "grids", or "lists". The engine must find every video, extract its details (title, channel), and check if it matches your filters.

How it works (Simplified): The engine acts like a meticulous inspector. It opens every box (data object) YouTube sends. If it finds a video inside, it reads the label (title/channel). If the label is on your "Block List", it throws the video in the trash. If it's a box of boxes (a playlist or shelf), it opens those too and checks everything inside.

Technical Flow:


+-------------+
| processData |
+-------------+
       |
       v
+-------------+       +-------------+
|  Traverse   | ----> | Check Type  |
|  JSON Tree  |       | (Renderer?) |
+-------------+       +-------------+
       ^                     | Yes
       |                     v
       |              +-------------+
    (Recurse)         |   Extract   |
       |              |   Metadata  |
       |              +-------------+
       |                     |
       |                     v
+-------------+       +-------------+
| Keep Item   | <---- | Match Rules?|
+-------------+  No   +-------------+
                             | Yes
                             v
                      +-------------+
                      | Block Item  |
                      | (Set Null)  |
                      +-------------+

4. 3-Dot Menu System & Channel Resolution

4.1 Menu Injection Flow

When user clicks 3-dot menu on any video card, FilterTube injects a "Block Channel" option:

sequenceDiagram
    participant UI as UI
    participant Content as Content
    participant Background as Background
    participant Cache as Cache
    participant API as External API

    UI->>Content: User clicks 3-dot menu
    Content->>Content: extractChannelFromCard()
    Note over Content: Often returns {id: "UC..."} (DOM), otherwise {needsFetch: true}
    Content->>Background: Request channel enrichment
    Background->>Cache: Check videoChannelMap
    alt Cache has mapping
        Cache-->>Background: Return cached channelId
    else No mapping
        Background->>API: Fetch channel details
        API-->>Background: Return channel info
    end
    Background->>Content: Update menu label
    Content->>UI: Display updated channel name
Loading

Filter All is selection state inside the custom fallback popover, not the action itself. The actual block is committed only when the user activates the real Block • Channel row, after which background can persist both:

  • the channel entry
  • the linked channel-derived keyword state

4.2 Channel Identity Resolution

FilterTube uses a multi-layer approach to resolve channel identities:

Layer 1: DOM Extraction (Immediate)

// Extract what's available from DOM
function extractChannelFromCard(card) {
    // Try multiple selectors based on card type
    const channelLink = card.querySelector('a[href*="/channel/"], a[href*="/@"]');
    const nameElement = card.querySelector('ytd-channel-name a, #channel-info a');
    
    return {
        id: extractChannelId(channelLink?.href),
        handle: extractRawHandle(channelLink?.href),
        name: nameElement?.textContent?.trim(),
        videoId: extractVideoId(card),
        needsFetch: !id || !name  // Requires network if missing key info
    };
}

Layer 2.1: Main World player payload harvesting (no extra fetch)

In many cases, channel ownership is learned before the menu is opened because the Main World interception layer processes:

  • window.ytInitialPlayerResponse
  • /youtubei/v1/player (via fetch/XHR interception)

This allows filter_logic.js to harvest videoId -> UC... mappings and persist them into videoChannelMap, making Shorts and Watch behave more like regular Home/Search cards (near-instant identity).

Layer 2: Main World Lookup (Fast)

// Request data from main world (injector.js)
function requestChannelInfo(videoId) {
    window.postMessage({
        type: 'FilterTube_RequestChannelInfo',
        videoId: videoId,
        expectedHandle: extractedHandle,
        expectedName: extractedName
    });
    
    // Listen for response
    window.addEventListener('message', (event) => {
        if (event.data.type === 'FilterTube_ChannelInfoResponse') {
            return event.data.channelInfo;
        }
    });
}

Watch-Page Mix / Playlist Recovery Order

When a watch-page playlist / Mix row does not expose stable owner identity directly, the current fallback order is:

  1. UC ID
  2. customUrl
  3. @handle
  4. watch:VIDEO_ID

watch:VIDEO_ID is recovery-only. It is not treated as canonical channel identity; it is a bridge that lets background recover the real owner from watch-page payloads.

Layer 3: Network Fetch (Fallback)

// Fetch from YouTube pages when needed
async function fetchChannelFromWatchUrl(videoId) {
    const response = await fetch(`https://www.youtube.com/watch?v=${videoId}`);
    const html = await response.text();
    
    // Parse channel info from HTML
    const channelMatch = html.match(/"channelId":"(UC[\w-]{22})"/);
    const nameMatch = html.match(/"author":"([^"]+)"/);
    
    return {
        id: channelMatch?.[1],
        name: nameMatch?.[1],
        source: 'watch-fetch'
    };
}

Cross-surface behavior: the overall identity resolution strategy is intended to behave the same on YouTube Main and YouTube Kids:

  • Prefer DOM-extracted UC... when present.
  • Prefer videoChannelMap when already learned.
  • Prefer Main World interception/harvesting (ytInitialPlayerResponse, /youtubei/v1/player).
  • Use HTML fetch parsing only as a fallback.

The main difference is that Kids has stricter CORS limits, so the fallback network fetch layer may fail more often on youtubekids.com, making the interception + caching layers even more important.

4.3 Label Upgrade System

The 3-dot menu intelligently upgrades placeholder labels:

// Initial render - may show placeholder
renderFilterTubeMenuEntries({
    channelInfo: { name: '@handle', id: null },  // Placeholder
    placeholder: true
});

// After enrichment - upgrade to real name
updateInjectedMenuChannelName(dropdown, {
    name: 'Actual Channel Name',
    id: 'UCxxxxxxxx...'
});

Placeholder Detection Rules:

  • UC IDs: /^UC[\w-]{22}$/
  • Mix titles: /^mix\s+-/i or contains separator
  • Metadata strings: Contains views, ago, watching
  • Handles only: Starts with @ and no actual name
  • Collapsed collaboration labels: values like A and 2 more are display hints, not final channel identity

If later recovery returns a stronger canonical name for the same UC ID, the stored label can now be repaired instead of permanently keeping a title-like placeholder.

4.4 Profile-Aware Resolution

YouTube Main:

  • Uses standard fetch pipeline
  • Caches in videoChannelMap and channelMap
  • Full enrichment features available

YouTube Kids:

  • Limited CORS for network requests
  • Falls back to main-world extraction
  • Uses ftProfilesV3.kids storage namespace

Collaboration filtering relies on coordinated logic across all three execution worlds. The end-to-end detection pipeline is:


YouTube DOM (Isolated World)
  ├─ js/content/* (loaded before content_bridge.js)
  │   • dom_extractors.js / dom_helpers.js (videoId, duration, card lookup)
  │   • dom_fallback.js (MutationObserver fallback hide/restore)
  │   • block_channel.js (3-dot dropdown observer + card resolver)
  └─ content_bridge.js
      • Settings sync + main-world injection
      • Parse collaboration signals (#attributed-channel-name / yt-text-view-model / avatar stack)
      • Inject menu items + handle clicks (Block Channel / Filter All / Block All Collaborators)
      • If data missing → post FilterTube_RequestCollaboratorInfo →

Main World
  └─ injector.js / filter_logic.js
      • searchYtInitialDataForVideoChannel(videoId)
      • Extract listItems[].listItemViewModel entries (name, handle, UC ID)
      • Respond with allCollaborators[] payload →

Isolated World (resume)
  • Generate collaborationGroupId (UUID)
  • Send chrome.runtime.sendMessage(handleAddFilteredChannel, {
        input, filterAll, collaborationWith, collaborationGroupId,
        allCollaborators
    }) →

Background Service Worker
  • sanitizeChannelEntry() persists full metadata
  • Broadcasts StateManager events to every UI context

UI Contexts (tab-view.js, popup.js)
  • render_engine.buildCollaborationMeta() groups rows by collaborationGroupId
  • A 🤝 badge + tooltip summarizes present/missing collaborators

Data Structures Propagated

  • collaborationGroupId: deterministic link between channels blocked in the same action.
  • collaborationWith[]: per-channel "other members" list used for warnings and tooltips.
  • allCollaborators[]: canonical roster (name/handle/id) stored on each channel row so the UI can reason about partial groups even after reordering or searching.

10. Collaboration UI & Storage Semantics

  • Storage (background.js + settings_shared.js): sanitizeChannelEntry preserves collaborationGroupId, collaborationWith, and allCollaborators, meaning compiled settings always contain the metadata necessary for UI rehydration.
  • Render Engine: buildCollaborationMeta compares the stored roster with currently filtered entries, computes presentCount/totalCount, and emits:
    • collaboration-entry + yellow rail (full groups)
    • collaboration-partial + dashed rail (missing members)
    • title attribute tooltips with “Originally blocked with / Still blocked / Missing now” copy.
  • Search & Sort Integrity: Because every collaborator remains an independent row, FCFS ordering, keyword search, and sort toggles behave exactly as non-collab entries, avoiding clipping issues seen with floating group containers.
  • Home Feed Menu Parity: block_channel.js watcher treats button-view-model wrappers as click anchors, so collaboration-aware menu injection (multi-channel + Block All) works on lockup-based home cards, grid shelves, and Shorts shelves alike.
  • Watch SPA Rehydration: recovered watch-page collaborator rosters reconnect to stored collaborationGroupId / allCollaborators after navigation, so the tab UI and subsequent menus can reflect the full group rather than the first visible name only.

11. Handle Normalization & Regex Improvements

  • Extraction + decoding: handle parsing is percent-decoding + unicode-aware, so unicode handles and encoded links normalize consistently across DOM scraping and URL parsing.
  • Canonicalization: normalization strips URLs/querystrings and enforces lowercase so duplicates and mixed input sources converge to the same key.
  • Storage sync: once an association is learned (handle ↔ UC ID, and custom URL ↔ UC ID), it is persisted in channelMap so future matching avoids network calls.
  • Regex Compilation: compileKeywords escapes user input but keeps literal dots/underscores intact, ensuring collaboration-derived keywords like @foo.bar remain matchable.

12. Shorts Canonical Resolution (Detailed)

Shorts cards rarely embed canonical IDs, so FilterTube performs a resolution pipeline that prefers cache + main-world data before network:

sequenceDiagram
    participant DOM as block_channel.js + content_bridge.js (Shorts UI)
    participant FETCH as Remote Fetches
    participant BG as background.js

    DOM->>DOM: Detect ytd-reel-item / ytd-shorts-lockup
    DOM->>DOM: Prefer DOM-extracted /channel/UC... when present
    DOM->>DOM: Prefer videoChannelMap (learned passively from player payloads)
    DOM->>DOM: try channelMap / ytInitialData replay (no network)
    DOM->>FETCH: fetch shorts/watch HTML only as a fallback
    DOM->>BG: handleAddFilteredChannel({ id, handle, isShorts:true })
    BG->>BG: Persist channel, broadcast state update
    DOM->>DOM: hide container (parent rich-item) immediately → zero blank slots
Loading
  • Grace period: while identity enrichment runs, DOM fallback can hide immediately so the user does not see blocked content.
  • Convergence: once the canonical UC ID is known, subsequent interceptors (data + DOM) recognize the entry on every surface without repeated fetches.
  • Collaborator Harvesting: When Shorts expose the avatar stack, extractCollaboratorsFromAvatarStackElement seeds collaborator names/handles and the main-world hop fills UC IDs. The same multi-select UI appears regardless of layout.

Current behavior note: as of v3.2.1, Shorts identity is increasingly learned without explicit Shorts-page fetching because:

  • seed.js intercepts ytInitialPlayerResponse and /youtubei/v1/player, and filter_logic.js harvests videoId -> UC... into videoChannelMap.
  • Proactive XHR interception provides most channel identity before rendering.
  • Many cards now expose /channel/UC... anchors directly, allowing isolated-world extraction to return id immediately.

4. DOM Fallback System (Safety Net)

Motivation: Sometimes data interception misses something (e.g., complex updates). The DOM Fallback is a safety net that watches the screen itself and hides anything that slipped through.

How it works (Simplified): This is the backup security guard patrolling the building. If a banned video somehow snuck past the front door check, this guard spots it on the wall (the screen) and immediately throws a "Do Not Display" sheet over it so you can't see it.

Technical Flow:


+-------------+
|  Mutation   |
|  Observer   |
+-------------+
       |
       v
+-------------+       +-------------+
|  New Node   | ----> | Is Video?   |
|  Detected   |       | (Selector)  |
+-------------+       +-------------+
                             | Yes
                             v
                      +-------------+
                      |   Extract   |
                      |   Data      |
                      +-------------+
                             |
                             v
+-------------+       +-------------+
|  Do Nothing | <---- | Match Rules?|
+-------------+  No   +-------------+
                             | Yes
                             v
                      +-------------+
                      |  Apply CSS  |
                      |  (Hide)     |
                      +-------------+

5. Stats Calculation (Time Saved)

Motivation: Users want to know how much time they've saved by not watching unwanted content.

How it works:

  1. Detection: When a video is blocked, FilterTube looks for its duration (e.g., "10:05").
  2. Calculation: It parses "10:05" into 605 seconds.
  3. Accumulation: It adds this to a running total stored locally.
  4. Display: The UI converts the total seconds back into "Minutes/Hours Saved".

Technical Flow:

graph TD
    A[Video Blocked] --> B{Has Duration?}
    B -- Yes --> C[Parse Duration]
    B -- No --> D[Use Default: 3m for videos, 30s for shorts]
    C --> E[Add to Total Stats]
    D --> E
    E --> F[Save to Storage]
    F --> G[Update UI Badge]
Loading

Note (v3.0.1): FilterTube now tracks actual video durations extracted from YouTube's metadata whenever available, providing accurate time saved calculations instead of generic estimates.

6. Centralized State Management

Motivation: With multiple UIs (Popup, Tab View) and background processes, keeping settings in sync is critical. StateManager acts as the single source of truth.

How it works:

  • Single Source: All components read/write settings via StateManager.
  • Broadcasting: When settings change in one place, StateManager notifies all other parts of the extension.
  • Consistency: Ensures that if you add a filter in the Tab View, the Popup updates instantly.

This now includes shell follow-up behavior too:

  • popup and tab shells both refresh from the same confirmed background state
  • route changes reset the page-scroll position in tab view
  • profile/dropdown fixes and row-shell behavior are driven by shared state updates rather than local one-off assumptions

7. Channel Matching Algorithm

Motivation: Channels can be identified by Name ("My Channel"), Handle ("@mychannel"), or ID ("UC..."). Users might use any of these. The algorithm must normalize and match correctly.

How it works (Simplified): If you ban "@coolguy", the system needs to know that "Cool Guy Vlogs" is the same person. It looks at the video's "ID card" which lists their Name, Handle, and ID number. It checks if any of those match what you banned.

Technical Flow:


+-------------+
| Channel In  |
| (Name/ID/@) |
+-------------+
       |
       v
+-------------+       +-------------+
|  Normalize  | ----> |  Compare    |
|  (Lowercase)|       |  (Rules)    |
+-------------+       +-------------+
                             |
                             v
                      +-------------+
                      | Match Type? |
                      +-------------+
                       /     |     \
                  (@Handle) (ID)  (Name)
                     /       |       \
             +-------+   +-------+   +-------+
             | Exact |   | Exact |   |Partial|
             +-------+   +-------+   +-------+


8. Shorts Blocking Architecture (Hybrid Model)

13. Release Notes System (v3.1.6)

The release notes experience is now shared between the banner that appears on YouTube and the new “What’s New” tab in the dashboard. Both consume data/release_notes.json.

13.1 Data Flow Overview

graph TD
    RN["data/release_notes.json"] --> BG["background.js"]
    BG -->|"buildReleaseNotesPayload"| Storage["chrome.storage.local releaseNotesPayload"]
    Storage --> Banner["content/release_notes_prompt.js<br/>YouTube CTA"]
    RN --> Dashboard["tab-view.js<br/>loadReleaseNotesIntoDashboard"]
Loading

13.2 Banner CTA flow

sequenceDiagram
    participant CS as release_notes_prompt.js
    participant BG as background.js
    participant TAB as Browser Tabs

    CS->>BG: FilterTube_OpenWhatsNew (target URL)
    BG->>TAB: query tab-view.html*
    alt tab exists
        BG->>TAB: tabs.update(id, url=?view=whatsnew, active=true)
    else new tab
        BG->>TAB: tabs.create(url=?view=whatsnew)
    end
Loading

13.3 What’s New tab behavior

  • tab-view.js reads both the hash and ?view= query param so deep links select the correct nav item and scroll it into view.
  • data/release_notes.json entries support bannerSummary, highlights[], and detailsUrl. Dashboard cards render the highlights list; the banner uses bannerSummary.
  • Import/export doc updates reference this shared file so future releases update one source.

14. Import/Export Implementation Details (v3.1.6)

14.1 Module responsibilities

Module Responsibility
js/io_manager.js Normalizes keywords/channels, adapters, merge logic, and v3 schema builder.
state_manager.js + FilterTubeSettings Entry points that read/write storage so compilation remains centralized.
html/tab-view.html + tab-view.js Provide UI controls (file picker, merge/replace).

14.2 ASCII dataflow (Export)

Tab View Export Button
    |
    v
StateManager.loadSettings()
    |
    v
io_manager.js::buildV3Export()
    |
    v
Blob + FileSaver

14.3 ASCII dataflow (Import Merge)

User selects JSON
    |
    v
io_manager.js::importV3()
    |
    v
normalizeIncomingV3()
    |
    v
mergeChannelLists() / mergeKeywordLists()
    |
    v
FilterTubeSettings.saveSettings()

14.4 Notes on custom channels

  • Inputs such as https://www.youtube.com/c/Filmy_Gyaan normalize to c/filmy_gyaan.
  • Merge priority: UCID > @handle > customUrl > name/originalInput.
  • When both a handle and custom URL are present, sanitizeChannelEntry retains both to improve lookups across surfaces.

14.5 Channel enrichment queue & throttling

Imported backups often contain bare IDs/handles without canonical metadata (logo, custom URL, collaboration hints). To avoid hammering YouTube for every entry simultaneously, the UI defers enrichment through a queue managed inside state_manager.js:

sequenceDiagram
    participant Import as importV3 / merge logic
    participant SM as StateManager
    participant Queue as channelEnrichmentQueue
    participant BG as background.js
    participant YT as youtube.com

    Import->>SM: saveSettings({ channels })
    SM->>Queue: enqueue channels lacking canonical metadata
    Note over Queue: Deduplicate via channelEnrichmentAttempted Set
    Queue->>BG: handleAddFilteredChannel (one entry)
    BG->>YT: best-effort fetch (handle/custom URL lookup)
    BG-->>SM: sanitized channel metadata
    SM->>Queue: wait ~6s, pop next item (prevents floods/DDOS)
Loading

Key behaviors:

  • Deduping: channelEnrichmentAttempted ensures each unique handle/ID is only fetched once per session.
  • Throttle: processChannelEnrichmentQueue now sleeps a randomized 5–7 seconds between requests, preventing burst traffic after large imports.
  • Auto-drain: Once the queue is empty, enrichment stops until the next import or manual add; nothing persists that would keep pinging YouTube.
  • Fallback safe: If a fetch fails (network/offline), the entry stays without enriched data but the queue still advances—avoiding infinite retries.
  • Instrumentation: Every dequeued entry logs channel enrichment start/complete with queue depth, duration, and the next randomized delay so QA can confirm pacing without extra tooling.

14.6 Kids Mode profile + UI

FilterTube maintains a second profile that mirrors the main blocklists but targets YouTube Kids domains:

  • Separate storage: state_manager.js exposes getKidsState, addKidsKeyword, addKidsChannel, etc., which persist to FilterTubeIO.saveProfilesV3() under the kids key so main filters never leak into kids browsing.
  • Dashboard tabs: tab-view.js renders initializeKidsTabs() with keyword/channel managers, search & sort controls, and the same calendar presets used in the main Filters view—users can curate Kids lists without touching the main ones.
  • Passive capture on youtubekids.com: content/block_channel.js detects the native “Block this video” toast. When a parent blocks content directly inside YouTube Kids, FilterTube synthesizes a minimal channel entry and sends it to FilterTube_KidsBlockChannel, keeping the Kids profile in sync even if the extension UI is never opened.
  • Shared enrichment safety: the regular enrichment queue runs for Kids entries too, but because Kids browsing happens on a distinct domain, the throttle + logging described above prevent the passive capture flow from overwhelming background requests.

Motivation: Shorts are unique because they often lack channel information in the initial DOM. To ensure robust blocking, the system uses cache-first + main-world recovery to resolve a canonical UC ID whenever possible.

How it works:

  1. User Action: User clicks "Block Channel" in 3 dots menu.
  2. Cache-first resolution: resolve using channelMap (handle/customUrl ↔ UC ID) when possible.
  3. Main-world recovery: if a videoId is available, query main-world ytInitialData for the uploader identity.
  4. Network fallback: fetch handle/customUrl pages only when necessary.
  5. Finalize: persist the channel keyed by UC ID; the DOM fallback provides immediate visual feedback while enrichment completes.

The exact latency depends on which fallback path is needed (cache/main-world/network). The goal is that blocks remain correct and converge quickly to a canonical UC ID.

Technical Flow:

[User Click "Block"]
       |
       v
+-----------------------+
|  1. Identify Type     |
+-----------------------+
       |
       +--------(If Short)-------+
       |                         |
       v                         v
+--------------------+    +----------------------+
| 2. Fetch Short URL |    | (Standard Video)     |
| -> Get Channel ID  |    | Have Channel ID      |
+--------------------+    +----------------------+
       |                         |
       +-----------+-------------+
                   |
                   v
+-----------------------------------+
| 3. Fetch Canonical ID (Robustness)| <--- "The 1-sec Safety Check"
| -> Resolve to unique UC ID        |      (Ensures Zero Leakage)
+-----------------------------------+
                   |
                   v
+-----------------------------------+
| 4. Update Block List & Hide Card  |
+-----------------------------------+
| 5. Update videoChannelMap (Global)|
+-----------------------------------+

UI Component Styling (v3.2.6 + v3.2.8 follow-up)

Serene Shell Follow-Up

The current extension UI is no longer just a set of styled legacy controls. The popup and tab view now sit inside a shared shell layer with:

  • a compact popup tray rather than a mini full page
  • a persistent app-like desktop shell in tab view
  • ambient local hero video behind popup and tab surfaces
  • stronger semantic pill colors for Exact, Comments, and Filter All
  • separate compact popup-row behavior vs shared full tab-view row shells
  • internal scroll containers and mobile drawer/sidebar handling tuned independently

Relevant runtime/build pieces:

  • /Users/devanshvarshney/FilterTube/src/extension-shell/popup.jsx
  • /Users/devanshvarshney/FilterTube/src/extension-shell/tab-view-decor.jsx
  • /Users/devanshvarshney/FilterTube/js/ui-shell/popup-shell.js
  • /Users/devanshvarshney/FilterTube/js/ui-shell/tab-view-decor.js
  • /Users/devanshvarshney/FilterTube/css/serene-shell.css
  • /Users/devanshvarshney/FilterTube/css/design_tokens.css

Enhanced Dropdown & Select Components

FilterTube v3.2.6 introduces refined dropdown and select styling with left accent borders and improved interactivity:

Visual Design:

┌─────────────────────────────────────────┐
│  Select Component Anatomy               │
├─────────────────────────────────────────┤
│                                         │
│  ┃  Sort by: Newest First        ▼      │
│  ┃  ─────────────────────────────       │
│  ┃  3px brand accent border             │
│  ┃                                      │
│  └─ Floating shadow + hover glow        │
│                                         │
│  Dropdown Menu:                         │
│  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓      │
│  ┃ ● Newest First                ┃      │
│  ┃ ○ Oldest First                ┃      │
│  ┃ ○ A-Z                         ┃      │
│  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛      │
│                                         │
└─────────────────────────────────────────┘

Extension Status Label:

┌─────────────────────────────────┐
│  🎯 FilterTube                  │
│     ENABLED                     │  ← 7px uppercase status
└─────────────────────────────────┘
     Green border + background

┌─────────────────────────────────┐
│  🎯 FilterTube                  │
│     DISABLED                    │  ← 7px uppercase status
└─────────────────────────────────┘
     Red border + background

Follow-up shell/UI behavior now also includes:

  • popup profile dropdown sizing tuned to popup scale
  • detached dropdown anchoring fixes in tab view
  • short-height sidebar internal scrolling
  • route/page scroll reset between tab-view sections
  • corrected list-row shells so long channel/keyword lists do not overlap or clip metadata
  • custom fallback 3-dot feedback states: pressed, focus-visible, and open
  • popup now registers the shared window.FilterTubeIsUiLocked hook so StateManager.updateSetting() cannot be bypassed from the header brand toggle on locked profiles
  • popup header enabled/disabled state is visually disabled and hard-stopped while the active profile is PIN-locked
  • tab-view profile switching mirrors popup on denied/cancelled PIN prompts by refreshing profile/badge/lock UI instead of leaving stale selection state behind
  • tab-view exposes resetTabViewScroll the same way it exposes closeProfileDropdownTab, so lock-gate redirects and profile-driven view changes do not crash on scope boundaries

Advance Video Filters Section (v3.2.6)

Collapsible Popup UI

The popup now includes a collapsible Advance video filters section:

Visual Layout:

┌─────────────────────────────────────────┐
│  Content Controls                       │
│  ─────────────────────────────────────  │
│                                         │
│  ▼ Advance Video Filters                │  ← Collapsible header
│  ┌───────────────────────────────────┐  │
│  │ Duration: [5] to [20] minutes     │  │  ← Inline inputs
│  │ Upload Date: [2024-01-01] to now  │  │
│  │ Uppercase: Single word (min 2)    │  │
│  │                                   │  │
│  │ [Manage in Settings →]            │  │  ← Link button
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

Current note:

  • the newer popup direction is a compact quick-control tray
  • advanced controls still exist, but the shell is optimized around fast actions first
  • Filter All in the custom fallback 3-dot popover is selection-only; only the actual block row commits the action

Custom Radio Buttons:

.custom-radio {
    width: 18px;
    height: 18px;
    border: 2px solid var(--ft-color-sem-neutral-border);
    border-radius: 50%;
    position: relative;
}

.custom-radio::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--ft-color-brand-secondary);
    transition: transform 0.2s ease;
}

.video-filter-radio-card.selected .custom-radio::after {
    transform: translate(-50%, -50%) scale(1);
}

Visual Layout:

┌─────────────────────────────────────────────────────────┐
│ ┃ Duration Filter                                       │
│ ┃ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│ ┃ │ ◉ Longer    │ │ ○ Shorter   │ │ ○ Between   │       │
│ ┃ │ than [10]   │ │ than [5]    │ │ [5] - [20]  │       │
│ ┃ │ minutes     │ │ minutes     │ │ minutes     │       │
│ ┃ └─────────────┘ └─────────────┘ └─────────────┘       │
│ ┃                                                       │
│ ┃ ← 3px brand accent border                             │
└─────────────────────────────────────────────────────────┘