Documentation Index
Fetch the complete documentation index at: https://mintlify.com/chefnaphtha/xBlockOrigin/llms.txt
Use this file to discover all available pages before exploring further.
xBlockOrigin uses content scripts to scan X.com pages, detect users from blacklisted countries, and hide their posts in real-time.
Entry point
The content script initializes when the page loads:
// packages/extension/src/Content/index.ts:7
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
function init() {
console.log('[xBlockOrigin] Content script loaded')
startOrchestrator()
setupUnmuteMessageHandler()
setupWhitelistListener()
setTimeout(() => {
watchThemeChanges((theme) => {
chrome.storage.local.set({ xTheme: theme })
})
}, 1000)
}
The initialization process:
- Starts the orchestrator - Main coordination logic for page scanning
- Sets up unmute handler - Listens for unmute messages from popup
- Sets up whitelist listener - Watches for newly whitelisted users to unhide their posts
- Detects theme - Monitors X.com theme changes (light/dim/lights-out)
The theme detection runs after a 1-second delay to ensure X.com’s UI has fully loaded.
Orchestrator architecture
The orchestrator (orchestrator.ts:286) coordinates all scanning activity and manages navigation.
Page detection
The orchestrator detects which X.com page is currently active:
// packages/extension/src/Content/orchestrator.ts:230
function getCurrentPage(): string {
const path = window.location.pathname
if (path === '/home' || path === '/') {
return 'timeline'
}
if (path.startsWith('/search')) {
return 'search'
}
if (path.startsWith('/notifications')) {
return 'notifications'
}
// format: /{username}/status/{id}
if (path.includes('/status/')) {
return 'status'
}
const systemPages = ['explore', 'messages', 'settings', 'compose', 'i']
const firstSegment = path.split('/')[1]
if (firstSegment && !systemPages.includes(firstSegment)) {
return 'profile'
}
return 'unknown'
}
Supported page types:
- timeline - Home feed (
/home or /)
- search - Search results (
/search)
- notifications - Notifications tab (
/notifications)
- status - Individual post with replies (
/{username}/status/{id})
- profile - User profile pages (any path not matching system pages)
Scanner lifecycle
The orchestrator starts the appropriate scanner for each page type:
// packages/extension/src/Content/orchestrator.ts:294
const startScanners = (page: string) => {
switch (page) {
case 'timeline':
cleanupFns.push(scanTimeline(handleUser))
break
case 'search':
cleanupFns.push(scanSearch(handleUser))
break
case 'notifications':
cleanupFns.push(scanReplies(handleUser))
break
case 'status':
cleanupFns.push(scanStatus(handleUser))
break
case 'profile':
cleanupFns.push(scanProfile(handleUser))
break
}
}
Each scanner:
- Returns a cleanup function to disconnect observers
- Receives a
handleUser callback to process discovered usernames
- Scans both existing content and watches for new content via
MutationObserver
Scanner cleanup functions are automatically called when navigating to a new page to prevent memory leaks.
Navigation handling
X.com is a single-page application (SPA), so the orchestrator detects navigation without full page reloads:
// packages/extension/src/Content/orchestrator.ts:261
function waitForNavigation(currentUrl: string): Promise<string> {
// race between popstate event and polling
const popstatePromise = new Promise<string>((resolve) => {
const handler = () => {
if (window.location.href !== currentUrl) {
resolve(window.location.href)
}
}
window.addEventListener('popstate', handler, { once: true })
})
const pollingPromise = new Promise<string>((resolve) => {
const checkUrl = () => {
if (window.location.href !== currentUrl) {
resolve(window.location.href)
} else {
setTimeout(checkUrl, 100)
}
}
checkUrl()
})
return Promise.race([popstatePromise, pollingPromise])
}
Navigation detection uses two strategies:
- popstate event - Fired when user clicks browser back/forward buttons
- URL polling - Checks every 100ms for URL changes (catches in-app navigation)
The first strategy to detect a change wins the race.
Main orchestrator loop
The orchestrator runs continuously until stopped:
// packages/extension/src/Content/orchestrator.ts:314
const handleNavigation = async () => {
let currentUrl = window.location.href
startScanners(getCurrentPage())
while (running) {
const newUrl = await waitForNavigation(currentUrl)
if (!running) break
currentUrl = newUrl
// clear pending API requests on navigation
apiQueue.clear()
// cleanup old scanners
cleanupFns.forEach((fn) => fn())
cleanupFns.length = 0
// start new scanners
startScanners(getCurrentPage())
}
}
On each navigation:
- Wait for URL to change
- Clear pending API requests (rate limiter queue)
- Cleanup old page scanners
- Start new scanners for the new page
Clearing the API queue prevents processing users from pages the user has already left.
User processing flow
When a scanner discovers a username, the orchestrator processes it:
// packages/extension/src/Content/orchestrator.ts:49
async function processUser(username: string, tweetElement?: Element) {
// skip if already processing this user
if (inFlightUsers.has(username)) {
console.log(`[xBlockOrigin] Skipping @${username} - already in queue`)
return
}
inFlightUsers.add(username)
// ... user processing logic ...
// processing complete, remove from in-flight tracker
inFlightUsers.delete(username)
}
The inFlightUsers Set prevents race conditions when multiple posts from the same user appear simultaneously.
Processing steps
- Get user ID and following status - Fetches from X.com API (with cache)
- Check whitelist - Skip processing if user is whitelisted
- Check following status - Skip if user is followed (unless
muteFollowing is enabled)
- Get country - Fetches user’s country from API (with cache)
- Check blacklist - Compare country against blacklisted countries
- Hide post - If blacklisted, hide the post immediately
- Mute via API - Call X.com’s mute endpoint for newly discovered users
- Save to database - Store muted user info locally
The user processing logic is implemented in orchestrator.ts:49-228.
Post hiding
When a post needs to be hidden, the content script creates an overlay:
// packages/extension/src/Content/postHider.ts:12
export function hidePost(
tweetElement: Element,
userId: string,
username: string,
country: string
): void {
// skip if already hidden
if (tweetElement.querySelector('[data-testid="xbo-hidden-post"]')) {
return
}
// ensure tweet element has relative positioning for overlay
if (tweetElement instanceof HTMLElement) {
const currentPosition = window.getComputedStyle(tweetElement).position
if (currentPosition === 'static') {
tweetElement.style.position = 'relative'
}
}
// create notice overlay
const notice = createHiddenPostNotice(userId, username, country)
noticeToTweet.set(notice, tweetElement)
// track by userId
if (!hiddenByUserId.has(userId)) {
hiddenByUserId.set(userId, new Set())
}
hiddenByUserId.get(userId)?.add(notice)
// append overlay to tweet element
tweetElement.appendChild(notice)
}
The hidden post notice overlay (hiddenPostNotice.ts:55):
- Uses
position: absolute to cover the entire post
- Applies backdrop blur for visual effect
- Adapts colors based on X.com theme (light/dim/lights-out)
- Provides “Unhide” and “Unmute and whitelist” buttons
Hidden post notice example
// packages/extension/src/Content/hiddenPostNotice.ts:55
export function createHiddenPostNotice(
userId: string,
username: string,
country: string
): Element {
const theme = detectXTheme()
const styles = getThemeStyles(theme)
const bgColor = getActualBackgroundColor()
const notice = document.createElement('div')
notice.setAttribute('data-xbo-hidden-user', userId)
notice.setAttribute('data-testid', 'xbo-hidden-post')
notice.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: ${bgColor};
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
gap: 12px;
z-index: 10;
`
// ... button creation ...
}
Unhiding posts
Users can unhide individual posts:
// packages/extension/src/Content/postHider.ts:45
export function unhidePost(
noticeElement: Element,
userId: string,
username: string
): void {
// get the tweet element that contains this notice
const tweetElement = noticeElement.parentElement
// remove the overlay element
if (tweetElement) {
noticeElement.remove()
// create and insert the unhidden notice after the tweet
const unhiddenNotice = createUnhiddenNotice(userId, username)
// check if there's already an unhidden notice
const existingNotice = tweetElement.nextElementSibling
if (
existingNotice &&
existingNotice.getAttribute('data-xbo-unhidden-notice') === userId
) {
// replace existing notice
existingNotice.replaceWith(unhiddenNotice)
} else {
// insert after tweet element
tweetElement.after(unhiddenNotice)
}
}
// cleanup tracking
const userSet = hiddenByUserId.get(userId)
if (userSet) {
userSet.delete(noticeElement)
if (userSet.size === 0) {
hiddenByUserId.delete(userId)
}
}
noticeToTweet.delete(noticeElement)
}
Unhiding a post:
- Removes the overlay to reveal the original post
- Displays an “unhidden notice” below the post
- Keeps the user muted (unless they click “Unmute and whitelist”)
Whitelist integration
The content script listens for whitelist changes to automatically unhide posts:
// packages/extension/src/Content/index.ts:26
function setupWhitelistListener() {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'sync' && changes.whitelist) {
const oldWhitelist = (changes.whitelist.oldValue || []) as WhitelistedUser[]
const newWhitelist = (changes.whitelist.newValue || []) as WhitelistedUser[]
// find newly whitelisted users
const oldIds = new Set(oldWhitelist.map((u) => u.userId))
const newlyWhitelisted = newWhitelist.filter(
(u) => !oldIds.has(u.userId)
)
// unhide posts from newly whitelisted users
for (const user of newlyWhitelisted) {
console.log(
`[xBlockOrigin] Unhiding posts from newly whitelisted user @${user.username}`
)
unhideAllPostsByUserId(user.userId)
}
}
})
}
When a user is added to the whitelist (via popup or hidden post notice):
- The whitelist change event fires
- Content script detects the newly whitelisted user
- All hidden posts from that user are unhidden automatically
This ensures a smooth user experience - whitelisting instantly reveals all posts from that user across the page.