// ==UserScript== // @name X Viral Monitor Mobile Badge // @namespace https://github.com/Icy-Cat/x-viral-monitor // @version 0.1.15 // @description Mobile Safari/Userscripts badge build: DOM fallback badges, no floating leaderboard by default. // @author IcyCat // @homepageURL https://github.com/Icy-Cat/x-viral-monitor // @supportURL https://github.com/Icy-Cat/x-viral-monitor/issues // @match https://x.com/* // @match https://pro.x.com/* // @match https://mobile.x.com/* // @run-at document-start // @grant unsafeWindow // @grant GM_registerMenuCommand // ==/UserScript== (() => { 'use strict'; const GRAPHQL_RE = /\/i\/api\/graphql\//; const tweetDataStore = new Map(); const velocityThresholds = { trending: 1000, viral: 10000 }; const DEFAULT_COLUMNS = [ { id: 'rank', visible: true }, { id: 'icon', visible: true }, { id: 'handle', visible: false }, { id: 'preview', visible: true }, { id: 'views', visible: true }, { id: 'velocity', visible: true }, ]; const ENABLE_DEBUG_LEADERBOARD = false; const ENABLE_MOBILE_DEBUG = /(?:[?&])xvm-debug=1(?:&|$)/.test(location.search); const I18N = { en: { colRank: 'Rank', colIcon: 'Icon', colHandle: 'Handle', colPreview: 'Preview', colViews: 'Views', colVelocity: 'Velocity', contentViews: 'Views', contentLikes: 'Likes', contentRetweets: 'Retweets', contentReplies: 'Replies', contentBookmarks: 'Bookmarks', contentVelocity: 'Velocity', contentViralScore: 'Viral Score', contentPosted: 'Posted', contentFallbackTweetLabel: 'Tweet', contentLeaderboardTitle: 'Velocity Monitor', contentLeaderboardDragToMove: 'Drag to move', contentLeaderboardSettings: 'Settings', contentLeaderboardBackToPrevious: 'Back to previous position', contentLeaderboardTotalViews: 'Total views', settingBadgeStyle: 'Badge style', settingBadgePillSolid: 'Pill solid', settingBadgeInlineClassic: 'Inline classic', settingTrending: 'Trending /h', settingViral: 'Viral /h', settingRows: 'Rows', settingColumns: 'Columns', settingLeaderboardEnabled: 'Floating leaderboard', settingSave: 'Save', }, zh: { colRank: '排名', colIcon: '等级图标', colHandle: '用户名', colPreview: '推文预览', colViews: '浏览量', colVelocity: '流速', contentViews: '浏览量', contentLikes: '点赞', contentRetweets: '转发', contentReplies: '回复', contentBookmarks: '收藏', contentVelocity: '流速', contentViralScore: '爆帖指数', contentPosted: '发布时间', contentFallbackTweetLabel: '推文', contentLeaderboardTitle: '本页热点', contentLeaderboardDragToMove: '拖动以移动', contentLeaderboardSettings: '设置', contentLeaderboardBackToPrevious: '返回之前的滚动位置', contentLeaderboardTotalViews: '总浏览量', settingBadgeStyle: '徽章样式', settingBadgePillSolid: '胶囊实底', settingBadgeInlineClassic: '经典行内', settingTrending: '蹿升阈值 /h', settingViral: '爆款阈值 /h', settingRows: '显示行数', settingColumns: '显示字段', settingLeaderboardEnabled: '悬浮榜', settingSave: '保存', }, ja: { colRank: '順位', colIcon: 'ティアアイコン', colHandle: 'ユーザー名', colPreview: '投稿プレビュー', colViews: '表示回数', colVelocity: '流速', contentViews: '表示回数', contentLikes: 'いいね', contentRetweets: 'リポスト', contentReplies: '返信', contentBookmarks: 'ブックマーク', contentVelocity: '流速', contentViralScore: 'バズ指数', contentPosted: '投稿日時', contentFallbackTweetLabel: '投稿', contentLeaderboardTitle: 'このページの注目投稿', contentLeaderboardDragToMove: 'ドラッグして移動', contentLeaderboardSettings: '設定', contentLeaderboardBackToPrevious: '前のスクロール位置に戻る', contentLeaderboardTotalViews: '総表示回数', settingBadgeStyle: 'バッジスタイル', settingBadgePillSolid: 'ソリッドピル', settingBadgeInlineClassic: 'クラシックインライン', settingTrending: '上昇中のしきい値 /h', settingViral: 'バズのしきい値 /h', settingRows: '表示件数', settingColumns: '表示項目', settingLeaderboardEnabled: 'フローティングリーダーボード', settingSave: '保存', }, }; function detectLanguage() { const candidates = Array.isArray(navigator.languages) && navigator.languages.length ? navigator.languages : [navigator.language || 'en']; for (const lang of candidates) { const normalized = String(lang || '').toLowerCase(); if (normalized.startsWith('zh')) return 'zh'; if (normalized.startsWith('ja')) return 'ja'; if (normalized.startsWith('en')) return 'en'; } return 'en'; } const currentLanguage = detectLanguage(); const currentLocale = currentLanguage === 'zh' ? 'zh-CN' : currentLanguage === 'ja' ? 'ja-JP' : 'en-US'; function t(key) { return I18N[currentLanguage]?.[key] || I18N.en[key] || key; } const COLUMN_LABELS = { rank: 'colRank', icon: 'colIcon', handle: 'colHandle', preview: 'colPreview', views: 'colViews', velocity: 'colVelocity', }; const KNOWN_COLUMN_IDS = DEFAULT_COLUMNS.map((column) => column.id); const settings = loadSettings(); velocityThresholds.trending = settings.trending; velocityThresholds.viral = Math.max(settings.viral, settings.trending + 1); const debugWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const debugState = { debugBuild: ENABLE_MOBILE_DEBUG, language: currentLanguage, locale: currentLocale, capturedGraphql: 0, extractedTweets: 0, leaderboardItems: 0, badgeMountAttempts: 0, badgeMounts: 0, domFallbackTweets: 0, articleScanCount: 0, visibleArticles: 0, badges: 0, articles: 0, graphqlResourceUrls: [], graphqlDebugBuffer: [], refetchAttempts: 0, refetchSuccesses: 0, refetchFailures: 0, pageHookMode: 'none', lastBadgeReason: '', lastLog: '', settings, hookInstalled: false, receivedMessages: 0, ignoredMessages: 0, lastMessageUrl: '', lastIgnoredReason: '', lastCapturedAt: 0, getTweets: () => Array.from(tweetDataStore.values()), }; window.__xvmTampermonkey = debugState; try { debugWindow.__xvmTampermonkey = debugState; } catch (_) {} function debugLog(...args) { if (!ENABLE_MOBILE_DEBUG) return; const message = args.map((arg) => { if (typeof arg === 'string') return arg; try { return JSON.stringify(arg); } catch (_) { return String(arg); } }).join(' '); debugState.lastLog = message.slice(0, 220); console.log('[XVM-TM-MOBILE]', ...args); updateDebugOverlay(); } function loadEruda() { if (document.getElementById('xvm-eruda-loader') || window.eruda) return; const script = document.createElement('script'); script.id = 'xvm-eruda-loader'; script.src = 'https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js'; script.onload = () => { try { window.eruda?.init?.(); debugLog('Eruda loaded'); } catch (err) { debugLog('Eruda init failed', err?.message || err); } }; script.onerror = () => debugLog('Eruda load failed'); (document.head || document.documentElement).appendChild(script); } function graphqlOpNameFromUrl(url) { try { const path = new URL(url, location.origin).pathname; return decodeURIComponent(path.split('/').filter(Boolean).pop() || ''); } catch (_) { return ''; } } function shouldCaptureGraphqlUrl(url) { const op = graphqlOpNameFromUrl(url); return /^(HomeTimeline|HomeLatestTimeline|TweetDetail|ListLatestTweetsTimeline|UserTweets|UserTweetsAndReplies|UserMedia|SearchTimeline|Bookmarks)$/i.test(op); } function parseGraphqlUrl(url) { try { const u = new URL(url, location.origin); const params = {}; for (const key of ['variables', 'features', 'fieldToggles']) { const value = u.searchParams.get(key); if (!value) continue; try { params[key] = JSON.parse(value); } catch (_) { params[key] = value; } } return { opName: graphqlOpNameFromUrl(url), params }; } catch (_) { return { opName: '', params: {} }; } } function truncateDebugPayload(value, max = 18000) { if (value == null) return value; const text = typeof value === 'string' ? value : JSON.stringify(value); if (text.length <= max) { try { return JSON.parse(text); } catch (_) { return text; } } return `${text.slice(0, max)}\n/* XVM_DEBUG_TRUNCATED ${text.length - max} chars */`; } function getCookieValue(name) { const escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : ''; } function recordGraphqlDebug(entry) { if (!entry?.url || !GRAPHQL_RE.test(entry.url)) return null; const parsed = parseGraphqlUrl(entry.url); const record = { at: new Date().toISOString(), source: entry.source || 'unknown', method: entry.method || 'GET', url: entry.url, opName: entry.opName || parsed.opName, requestBody: truncateDebugPayload(entry.requestBody || ''), variables: parsed.params.variables, features: parsed.params.features, fieldToggles: parsed.params.fieldToggles, status: entry.status || 0, responseBody: truncateDebugPayload(entry.responseBody || null), error: entry.error || '', }; debugState.graphqlDebugBuffer.push(record); while (debugState.graphqlDebugBuffer.length > 10) debugState.graphqlDebugBuffer.shift(); updateDebugOverlay(); return record; } async function refetchGraphqlUrl(url, source = 'resource-refetch') { if (!url || !GRAPHQL_RE.test(url) || !shouldCaptureGraphqlUrl(url)) return; const existing = debugState.graphqlDebugBuffer.find((item) => item.url === url && item.source === source); if (existing) return; debugState.refetchAttempts += 1; try { const csrf = getCookieValue('ct0'); const headers = { accept: 'application/json', 'x-twitter-active-user': 'yes', 'x-twitter-auth-type': 'OAuth2Session', }; if (csrf) headers['x-csrf-token'] = csrf; const response = await fetch(url, { credentials: 'include', cache: 'no-store', headers, }); const text = await response.text(); debugState.refetchSuccesses += response.ok ? 1 : 0; if (!response.ok) debugState.refetchFailures += 1; recordGraphqlDebug({ source, method: 'GET', url, status: response.status, responseBody: text, }); debugLog('GraphQL resource refetched', { opName: graphqlOpNameFromUrl(url), status: response.status }); } catch (err) { debugState.refetchFailures += 1; recordGraphqlDebug({ source, method: 'GET', url, error: String(err?.message || err), }); debugLog('GraphQL resource refetch failed', { opName: graphqlOpNameFromUrl(url), error: err?.message || String(err) }); } } function buildDebugBundle() { return { copiedAt: new Date().toISOString(), location: location.href, metrics: collectDebugMetrics(), graphql: debugState.graphqlDebugBuffer, }; } function rememberGraphqlResourceUrl(url, source) { if (!url || !GRAPHQL_RE.test(url)) return; const urls = debugState.graphqlResourceUrls; if (!urls.includes(url)) urls.push(url); while (urls.length > 20) urls.shift(); debugState.lastLog = `resource:${source}:${url}`.slice(0, 220); updateDebugOverlay(); refetchGraphqlUrl(url, `performance-${source}`); } function installResourceObserver() { try { const existing = performance.getEntriesByType?.('resource') || []; for (const entry of existing) rememberGraphqlResourceUrl(entry.name || '', 'initial'); } catch (_) {} if (window.PerformanceObserver) { try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) rememberGraphqlResourceUrl(entry.name || '', 'observer'); }); observer.observe({ type: 'resource', buffered: true }); debugLog('PerformanceObserver resource fallback installed'); return; } catch (err) { debugLog('PerformanceObserver install failed', err?.message || err); } } setInterval(() => { try { const entries = performance.getEntriesByType?.('resource') || []; for (const entry of entries) rememberGraphqlResourceUrl(entry.name || '', 'poll'); } catch (_) {} }, 2500); debugLog('PerformanceObserver unavailable; resource polling installed'); } function collectDebugMetrics() { const articles = document.querySelectorAll('article').length; const tweetArticles = document.querySelectorAll('article[data-testid="tweet"]').length; const badges = document.querySelectorAll('.xvm-badge').length; debugState.articles = articles; debugState.visibleArticles = tweetArticles; debugState.badges = badges; return { hookInstalled: !!debugState.hookInstalled, capturedGraphql: debugState.capturedGraphql || 0, extractedTweets: debugState.extractedTweets || 0, leaderboardItems: debugState.leaderboardItems || 0, badges, articles, tweetArticles, domFallbackTweets: debugState.domFallbackTweets || 0, graphqlResourceUrls: debugState.graphqlResourceUrls || [], graphqlDebugItems: debugState.graphqlDebugBuffer || [], refetchAttempts: debugState.refetchAttempts || 0, refetchSuccesses: debugState.refetchSuccesses || 0, refetchFailures: debugState.refetchFailures || 0, pageHookMode: debugState.pageHookMode || 'none', badgeMountAttempts: debugState.badgeMountAttempts || 0, badgeMounts: debugState.badgeMounts || 0, lastBadgeReason: debugState.lastBadgeReason || '', lastIgnoredReason: debugState.lastIgnoredReason || '', lastLog: debugState.lastLog || '', }; } function updateDebugOverlay() { const panel = document.getElementById('xvm-debug-panel'); const badge = document.getElementById('xvm-debug-launcher'); if (badge) { badge.textContent = `X${debugState.capturedGraphql || debugState.domFallbackTweets ? '*' : ''}`; badge.title = `XVM Debug: GQL ${debugState.capturedGraphql || 0}, DOM ${debugState.domFallbackTweets || 0}, refetch ${debugState.refetchSuccesses || 0}/${debugState.refetchAttempts || 0}`; } if (!panel) return; const m = collectDebugMetrics(); panel.querySelector('[data-xvm-debug-body]').innerHTML = `
hookInstalled: ${m.hookInstalled ? 'YES' : 'NO'}
capturedGraphql: ${m.capturedGraphql}
extractedTweets: ${m.extractedTweets}
badges/articles: ${m.badges} / ${m.articles}
tweetArticles: ${m.tweetArticles}
pageHookMode: ${escapeHtml(m.pageHookMode)}
graphqlResourceUrls: ${m.graphqlResourceUrls.length}
graphqlBuffer: ${m.graphqlDebugItems.length}
refetch: ${m.refetchSuccesses}/${m.refetchAttempts} ok, ${m.refetchFailures} fail
leaderboardItems: ${m.leaderboardItems}
domFallbackTweets: ${m.domFallbackTweets}
badgeAttempts/mounts: ${m.badgeMountAttempts} / ${m.badgeMounts}
lastBadgeReason: ${escapeHtml(m.lastBadgeReason)}
lastIgnored: ${escapeHtml(m.lastIgnoredReason)}
lastLog: ${escapeHtml(m.lastLog)}
`; } function installDebugOverlay() { if (!document.body || document.getElementById('xvm-debug-panel')) return; const launcher = document.createElement('button'); launcher.id = 'xvm-debug-launcher'; launcher.type = 'button'; launcher.textContent = 'X'; launcher.setAttribute('aria-label', 'Open XVM debug panel'); document.body.appendChild(launcher); const backdrop = document.createElement('div'); backdrop.id = 'xvm-debug-backdrop'; document.body.appendChild(backdrop); const panel = document.createElement('div'); panel.id = 'xvm-debug-panel'; panel.hidden = true; panel.innerHTML = `
XVM Debug
`; document.body.appendChild(panel); const openPanel = () => { panel.hidden = false; backdrop.hidden = false; updateDebugOverlay(); }; const closePanel = () => { panel.hidden = true; backdrop.hidden = true; }; backdrop.hidden = true; launcher.addEventListener('click', openPanel); backdrop.addEventListener('click', closePanel); panel.querySelector('[data-xvm-debug-eruda]').addEventListener('click', loadEruda); panel.querySelector('[data-xvm-debug-copy]').addEventListener('click', async () => { const payload = JSON.stringify(buildDebugBundle(), null, 2); try { await navigator.clipboard?.writeText?.(payload); debugLog('Full debug bundle copied'); } catch (_) { debugLog('Copy failed; bundle', payload); } }); panel.querySelector('[data-xvm-debug-close]').addEventListener('click', closePanel); updateDebugOverlay(); setInterval(updateDebugOverlay, 1500); debugLog('debug overlay installed'); } function loadSettings() { try { const parsed = JSON.parse(localStorage.getItem('xvmTampermonkeySettings') || '{}'); const trending = Number.parseInt(parsed.trending, 10); const viral = Number.parseInt(parsed.viral, 10); const leaderboardCount = Number.parseInt(parsed.leaderboardCount, 10); const leaderboardWidth = Number.parseInt(parsed.leaderboardWidth, 10); const leaderboardHeight = Number.parseInt(parsed.leaderboardHeight, 10); const pos = parsed.leaderboardPos; return { trending: Number.isFinite(trending) && trending > 0 ? trending : 1000, viral: Number.isFinite(viral) && viral > 0 ? viral : 10000, leaderboardEnabled: ENABLE_DEBUG_LEADERBOARD && parsed.leaderboardEnabled === true, leaderboardCount: Number.isFinite(leaderboardCount) ? Math.max(1, Math.min(50, leaderboardCount)) : 10, leaderboardWidth: Number.isFinite(leaderboardWidth) ? Math.max(240, Math.min(640, leaderboardWidth)) : 280, leaderboardHeight: Number.isFinite(leaderboardHeight) ? Math.max(120, Math.min(800, leaderboardHeight)) : 300, leaderboardPos: pos && Number.isFinite(pos.left) && Number.isFinite(pos.top) ? { left: pos.left, top: pos.top } : null, leaderboardColumns: normalizeColumns(parsed.leaderboardColumns), badgeStyle: parsed.badgeStyle === 'inline-classic' ? 'inline-classic' : 'pill-solid', }; } catch (_) { return { trending: 1000, viral: 10000, leaderboardEnabled: false, leaderboardCount: 10, leaderboardWidth: 280, leaderboardHeight: 300, leaderboardPos: null, leaderboardColumns: normalizeColumns(null), badgeStyle: 'pill-solid' }; } } function normalizeColumns(raw) { if (!Array.isArray(raw)) return DEFAULT_COLUMNS.map((column) => ({ ...column })); const seen = new Set(); const out = []; for (const column of raw) { if (!column || typeof column.id !== 'string' || !KNOWN_COLUMN_IDS.includes(column.id)) continue; if (seen.has(column.id)) continue; seen.add(column.id); out.push({ id: column.id, visible: !!column.visible }); } for (const column of DEFAULT_COLUMNS) { if (!seen.has(column.id)) out.push({ ...column }); } return out; } function saveSettings() { localStorage.setItem('xvmTampermonkeySettings', JSON.stringify(settings)); } function applySettings() { velocityThresholds.trending = settings.trending; velocityThresholds.viral = Math.max(settings.viral, settings.trending + 1); document.documentElement.dataset.xvmBadgeStyle = settings.badgeStyle; document.querySelectorAll('article[data-xvm-tm-scored]').forEach((article) => article.removeAttribute('data-xvm-tm-scored')); document.querySelectorAll('.xvm-badge').forEach((badge) => badge.remove()); leaderboardHtml = ''; if (!settings.leaderboardEnabled) hideLeaderboard(); scheduleRender(); } function injectCss() { document.documentElement.dataset.xvmBadgeStyle = settings.badgeStyle; if (document.getElementById('xvm-tm-style')) return; const style = document.createElement('style'); style.id = 'xvm-tm-style'; style.textContent = ` .xvm-badge { display: inline-flex; align-items: center; gap: 4px; margin-left: 6px; padding: 2px 7px; border-radius: 999px; font-size: 12px; font-weight: 700; line-height: 16px; color: #fff; vertical-align: middle; cursor: default; user-select: none; } .xvm-badge::before { content: attr(data-prefix); } .xvm-badge::after { content: attr(data-velocity) "/h"; } .xvm-badge--green { color: #15803d; background: rgba(22, 163, 74, 0.25); } .xvm-badge--orange { color: #c2410c; background: rgba(234, 88, 12, 0.25); } .xvm-badge--red { color: #b91c1c; background: rgba(220, 38, 38, 0.25); } html[data-xvm-badge-style="inline-classic"] .xvm-badge { gap: 0; margin-left: 0; margin-right: 4px; padding: 0; border-radius: 0; font-size: 13px; font-weight: 400; line-height: 20px; color: rgb(83, 100, 113); background: transparent; white-space: nowrap; transition: color 0.2s; } html[data-xvm-badge-style="inline-classic"] .xvm-badge::before { content: attr(data-prefix) " " attr(data-velocity) "/h"; } html[data-xvm-badge-style="inline-classic"] .xvm-badge::after { content: ""; } html[data-xvm-badge-style="inline-classic"] .xvm-badge:hover { color: rgb(29, 155, 240); } html[data-xvm-badge-style="inline-classic"] .xvm-badge--green { color: #4caf50; background: transparent; } html[data-xvm-badge-style="inline-classic"] .xvm-badge--orange { color: #ff9800; background: transparent; } html[data-xvm-badge-style="inline-classic"] .xvm-badge--red { color: #f44336; background: transparent; } .xvm-lb { display: none; position: fixed; right: 16px; top: 72px; width: 280px; max-width: calc(100vw - 32px); background: #fffcf6; color: #24180f; border: 1px solid rgba(86, 60, 34, 0.18); border-radius: 14px; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; box-shadow: 0 10px 28px rgba(36, 24, 15, 0.22), 0 2px 6px rgba(36, 24, 15, 0.08); z-index: 2147483646; overflow: hidden; } .xvm-lb.xvm-lb-dragging { box-shadow: 0 16px 36px rgba(36, 24, 15, 0.32), 0 2px 6px rgba(36, 24, 15, 0.12); opacity: 0.96; } .xvm-lb.xvm-lb-resizing { box-shadow: 0 16px 36px rgba(36, 24, 15, 0.32), 0 2px 6px rgba(36, 24, 15, 0.12); } .xvm-lb-head { display: flex; align-items: center; gap: 6px; padding: 7px 10px 6px; border-bottom: 1px solid rgba(86, 60, 34, 0.14); background: linear-gradient(180deg, rgba(191, 90, 42, 0.06), rgba(191, 90, 42, 0)); cursor: grab; user-select: none; } .xvm-lb-head:active, .xvm-lb.xvm-lb-dragging .xvm-lb-head { cursor: grabbing; } .xvm-lb-grip { font-size: 10px; color: #9b877a; letter-spacing: -1px; } .xvm-lb-title { flex: 1; font-size: 11px; font-weight: 700; color: #6e5b4d; } .xvm-lb-settings-btn { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: 6px; background: transparent; color: #9b877a; cursor: pointer; font-size: 14px; line-height: 1; transition: background 0.12s, color 0.12s; } .xvm-lb-settings-btn:hover { background: rgba(191, 90, 42, 0.14); color: #8f3d17; } .xvm-lb-count { font-size: 10px; font-weight: 700; color: #9b877a; font-variant-numeric: tabular-nums; } .xvm-lb-back { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: 6px; background: transparent; color: #bf5a2a; cursor: pointer; transition: background 0.12s, color 0.12s; } .xvm-lb-back:hover { background: rgba(191, 90, 42, 0.14); color: #8f3d17; } .xvm-lb-back[hidden] { display: none; } .xvm-lb-resize { position: absolute; top: 0; right: 0; width: 12px; height: 100%; cursor: ew-resize; } .xvm-lb-resize::before { content: ""; position: absolute; top: 50%; right: 3px; width: 3px; height: 28px; border-radius: 999px; background: rgba(110, 91, 77, 0.22); transform: translateY(-50%); transition: background 0.12s; } .xvm-lb:hover .xvm-lb-resize::before, .xvm-lb.xvm-lb-resizing .xvm-lb-resize::before { background: rgba(191, 90, 42, 0.35); } .xvm-lb-resize-v { position: absolute; bottom: 0; left: 0; width: 100%; height: 12px; cursor: ns-resize; } .xvm-lb-resize-v::before { content: ""; position: absolute; left: 50%; bottom: 3px; width: 28px; height: 3px; border-radius: 999px; background: rgba(110, 91, 77, 0.22); transform: translateX(-50%); transition: background 0.12s; } .xvm-lb:hover .xvm-lb-resize-v::before, .xvm-lb.xvm-lb-resizing .xvm-lb-resize-v::before { background: rgba(191, 90, 42, 0.35); } .xvm-settings { display: none; padding: 9px 10px 10px; border-bottom: 1px solid rgba(86, 60, 34, 0.14); background: rgba(255, 248, 241, 0.92); } .xvm-settings.xvm-settings-open { display: grid; gap: 8px; } .xvm-setting-row { display: grid; grid-template-columns: 92px 1fr; align-items: center; gap: 8px; font-size: 11px; color: #6e5b4d; } .xvm-setting-row input { width: 100%; min-width: 0; box-sizing: border-box; border: 1px solid rgba(86, 60, 34, 0.22); border-radius: 6px; padding: 4px 6px; background: #fff; color: #24180f; font: 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .xvm-setting-select { position: relative; width: 100%; min-width: 0; } .xvm-setting-select-trigger { width: 100%; min-width: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 6px; border: 1px solid rgba(86, 60, 34, 0.22); border-radius: 6px; padding: 4px 7px; background: #fff; color: #24180f; font: 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: left; cursor: pointer; } .xvm-setting-select-trigger:focus-visible { outline: 2px solid rgba(191, 90, 42, 0.42); outline-offset: 1px; } .xvm-setting-select-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .xvm-setting-select-chev { color: #8a6a55; transition: transform 0.12s; } .xvm-setting-select[data-open="1"] .xvm-setting-select-chev { transform: rotate(180deg); } .xvm-setting-select-menu { position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; right: 0; display: none; padding: 4px; border: 1px solid rgba(86, 60, 34, 0.22); border-radius: 8px; background: #fffaf3; box-shadow: 0 10px 22px rgba(36, 24, 15, 0.16); } .xvm-setting-select[data-open="1"] .xvm-setting-select-menu { display: block; } .xvm-setting-select-choice { width: 100%; border: 0; border-radius: 6px; padding: 5px 7px; background: transparent; color: #24180f; font: 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: left; cursor: pointer; } .xvm-setting-select-choice:hover, .xvm-setting-select-choice:focus-visible { outline: none; background: #f5ede1; } .xvm-setting-select-choice[aria-selected="true"] { background: #bf5a2a; color: #fffaf3; font-weight: 600; } .xvm-setting-row input[type="checkbox"] { width: auto; min-width: 0; justify-self: start; } .xvm-setting-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px 8px; } .xvm-setting-col { display: inline-flex; align-items: center; gap: 5px; min-width: 0; font-size: 11px; color: #6e5b4d; } .xvm-setting-col input { width: auto; min-width: 0; margin: 0; } .xvm-settings-actions { display: flex; justify-content: flex-end; } .xvm-settings-save { border: none; border-radius: 6px; padding: 5px 9px; background: #bf5a2a; color: #fff; font-size: 11px; font-weight: 700; cursor: pointer; } .xvm-lb-list { list-style: none; margin: 0; padding: 2px 0; max-height: 300px; overflow-y: auto; } .xvm-lb-list::-webkit-scrollbar { width: 5px; } .xvm-lb-list::-webkit-scrollbar-thumb { background: rgba(86, 60, 34, 0.2); border-radius: 2px; } .xvm-lb-item { display: flex; align-items: center; gap: 6px; height: 28px; padding: 5px 12px; font-size: 12px; cursor: pointer; user-select: none; transition: background 0.12s; } .xvm-lb-item:hover { background: rgba(191, 90, 42, 0.08); } .xvm-lb-item-selected { background: rgba(191, 90, 42, 0.14); box-shadow: inset 0 0 0 1.5px #bf5a2a; border-radius: 6px; } .xvm-lb-item-selected:hover { background: rgba(191, 90, 42, 0.18); } .xvm-lb-rank { width: 14px; text-align: center; color: #9b877a; font-variant-numeric: tabular-nums; font-size: 11px; font-weight: 600; } .xvm-lb-icon { flex-shrink: 0; } .xvm-lb-handle { flex: 0 0 auto; max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #24180f; font-weight: 500; } .xvm-lb-preview { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #3a2b1f; font-size: 11.5px; } .xvm-lb-views { font-variant-numeric: tabular-nums; font-size: 10px; color: #6e5b4d; flex-shrink: 0; } .xvm-lb-vel { font-variant-numeric: tabular-nums; font-size: 11px; font-weight: 700; flex-shrink: 0; } .xvm-lb-green .xvm-lb-vel { color: #3b8a3f; } .xvm-lb-orange .xvm-lb-vel { color: #bf5a2a; } .xvm-lb-red .xvm-lb-vel { color: #c23c1c; } article[data-testid="tweet"].xvm-article-linked { outline: 2px solid #bf5a2a; outline-offset: -1px; border-radius: 12px; transition: outline-color 0.18s; } .xvm-lb-link-path { stroke: #bf5a2a; stroke-width: 2; stroke-linecap: round; filter: drop-shadow(0 2px 4px rgba(191, 90, 42, 0.35)); } .xvm-lb-link-dot { fill: #fff8f1; stroke: #bf5a2a; stroke-width: 2; filter: drop-shadow(0 1px 3px rgba(191, 90, 42, 0.4)); } .xvm-lb-link-start { animation: xvm-lb-pulse 1.8s ease-in-out infinite; } @keyframes xvm-lb-pulse { 0%, 100% { r: 5; } 50% { r: 7; } } .xvm-tooltip { position: fixed; z-index: 2147483647; display: none; max-width: 260px; padding: 10px 12px; border-radius: 8px; background: rgba(15, 20, 25, 0.96); color: #fff; font: 12px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; white-space: pre-line; box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28); pointer-events: auto; } #xvm-debug-panel { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2147483647; width: min(360px, calc(100vw - 20px)); max-height: 72vh; overflow: auto; border: 1px solid rgba(34, 197, 94, 0.45); border-radius: 10px; background: rgba(15, 23, 42, 0.92); color: #e2e8f0; font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow: 0 14px 34px rgba(0, 0, 0, 0.38); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); } #xvm-debug-panel[hidden], #xvm-debug-backdrop[hidden] { display: none !important; } #xvm-debug-backdrop { position: fixed; inset: 0; z-index: 2147483646; background: rgba(15, 23, 42, 0.28); -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px); } #xvm-debug-launcher { position: fixed; right: 10px; bottom: 10px; z-index: 2147483647; width: 22px; height: 22px; border: 1px solid rgba(34, 197, 94, 0.7); border-radius: 999px; padding: 0; background: rgba(15, 23, 42, 0.86); color: #86efac; font: 700 11px/20px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.22); } .xvm-debug-head { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-bottom: 1px solid rgba(148, 163, 184, 0.25); color: #86efac; font-weight: 700; } .xvm-debug-head span { flex: 1; } .xvm-debug-head button { border: 1px solid rgba(134, 239, 172, 0.35); border-radius: 6px; padding: 3px 6px; background: rgba(22, 163, 74, 0.18); color: #dcfce7; font: inherit; } .xvm-debug-body { display: grid; gap: 2px; padding: 7px 8px 9px; word-break: break-word; }`; (document.head || document.documentElement).appendChild(style); } function installPageHook(targetWindow) { const pageWindow = targetWindow || window; if (pageWindow.__xvmTampermonkeyPageHook) { debugLog('page GraphQL hook already installed'); return; } pageWindow.__xvmTampermonkeyPageHook = true; const GRAPHQL_RE = /\/i\/api\/graphql\//; function extractUrl(input) { if (pageWindow.Request && input instanceof pageWindow.Request) return input.url; if (pageWindow.URL && input instanceof pageWindow.URL) return input.href; if (input && typeof input.url === 'string') return input.url; if (input && typeof input.href === 'string') return input.href; return typeof input === 'string' ? input : ''; } function opNameFromUrl(url) { try { const path = new pageWindow.URL(url, pageWindow.location.origin).pathname; return decodeURIComponent(path.split('/').filter(Boolean).pop() || ''); } catch (_) { return ''; } } function postGraphql(url, payload, source) { if (!payload || typeof payload !== 'object') return; debugLog('GraphQL response captured by hook', { source, opName: opNameFromUrl(url), url }); recordGraphqlDebug({ source, method: 'GET', url, responseBody: payload, status: 200 }); pageWindow.postMessage({ type: 'XVM_TM_GRAPHQL_RESPONSE', url, opName: opNameFromUrl(url), source, payload, capturedAt: Date.now(), }, '*'); } const originalFetch = pageWindow.fetch; pageWindow.fetch = async function (...args) { const url = extractUrl(args[0]); const init = args[1] || {}; const requestBody = typeof init.body === 'string' ? init.body : ''; if (url && GRAPHQL_RE.test(url)) recordGraphqlDebug({ source: 'userscript-fetch-request', method: init.method || 'GET', url, requestBody }); const response = await originalFetch.apply(this, args); if (url && GRAPHQL_RE.test(url)) { response.clone().json().then((payload) => postGraphql(url, payload, 'fetch')).catch(() => {}); } return response; }; debugLog('fetch hook installed'); const xhrOpen = pageWindow.XMLHttpRequest.prototype.open; pageWindow.XMLHttpRequest.prototype.open = function (method, url, ...rest) { const urlStr = pageWindow.URL && url instanceof pageWindow.URL ? url.href : (typeof url === 'string' ? url : ''); this.__xvmTmUrl = urlStr; if (urlStr && GRAPHQL_RE.test(urlStr)) recordGraphqlDebug({ source: 'userscript-xhr-request', method, url: urlStr }); if (urlStr && GRAPHQL_RE.test(urlStr)) { this.addEventListener('load', function () { try { postGraphql(urlStr, JSON.parse(this.responseText), 'xhr'); } catch (_) {} }); } return xhrOpen.call(this, method, url, ...rest); }; debugLog('XHR hook installed'); try { pageWindow.__xvmTampermonkey.hookInstalled = true; } catch (_) {} debugState.hookInstalled = true; debugLog('page GraphQL hook installed', { href: pageWindow.location?.href || '' }); console.debug('[XVM-TM] page GraphQL hook installed'); } function injectPageHook() { try { installPageHook(typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); } catch (err) { debugLog('page GraphQL hook install failed', err?.message || err); console.debug('[XVM-TM] page GraphQL hook install failed', err); } injectPageWorldScriptHook(); } function injectPageWorldScriptHook() { const root = document.documentElement || document.head; if (!root || document.getElementById('xvm-page-world-hook')) { debugState.pageHookMode = debugState.pageHookMode === 'none' ? 'already-present' : debugState.pageHookMode; return; } const script = document.createElement('script'); script.id = 'xvm-page-world-hook'; script.textContent = `(() => { if (window.__xvmTmPageWorldHook) { window.postMessage({ type: 'XVM_TM_PAGE_HOOK_STATUS', mode: 'page-script-already-installed', at: Date.now() }, '*'); return; } window.__xvmTmPageWorldHook = true; const GRAPHQL_RE = /\\/i\\/api\\/graphql\\//; function extractUrl(input) { try { if (window.Request && input instanceof window.Request) return input.url; if (window.URL && input instanceof window.URL) return input.href; if (input && typeof input.url === 'string') return input.url; if (input && typeof input.href === 'string') return input.href; return typeof input === 'string' ? input : ''; } catch (_) { return typeof input === 'string' ? input : ''; } } function opNameFromUrl(url) { try { const path = new URL(url, location.origin).pathname; return decodeURIComponent(path.split('/').filter(Boolean).pop() || ''); } catch (_) { return ''; } } function postGraphql(url, payload, source) { if (!payload || typeof payload !== 'object') return; window.postMessage({ type: 'XVM_TM_GRAPHQL_RESPONSE', url, opName: opNameFromUrl(url), source, payload, capturedAt: Date.now() }, '*'); } function postRequest(url, method, body, source) { if (!url || !GRAPHQL_RE.test(url)) return; window.postMessage({ type: 'XVM_TM_GRAPHQL_REQUEST', url, opName: opNameFromUrl(url), source, method: method || 'GET', body: typeof body === 'string' ? body : '', capturedAt: Date.now() }, '*'); } try { const originalFetch = window.fetch; if (typeof originalFetch === 'function') { window.fetch = async function (...args) { const url = extractUrl(args[0]); const init = args[1] || {}; postRequest(url, init.method || 'GET', init.body || '', 'page-fetch'); const response = await originalFetch.apply(this, args); if (url && GRAPHQL_RE.test(url)) { response.clone().json().then((payload) => postGraphql(url, payload, 'page-fetch')).catch(() => {}); } return response; }; } } catch (err) { window.postMessage({ type: 'XVM_TM_PAGE_HOOK_STATUS', mode: 'page-script-fetch-failed', error: String(err?.message || err), at: Date.now() }, '*'); } try { const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype; const originalOpen = proto && proto.open; if (originalOpen) { proto.open = function (method, url, ...rest) { const urlStr = extractUrl(url); if (urlStr && GRAPHQL_RE.test(urlStr)) { postRequest(urlStr, method || 'GET', '', 'page-xhr'); this.addEventListener('load', function () { try { postGraphql(urlStr, JSON.parse(this.responseText), 'page-xhr'); } catch (_) {} }); } return originalOpen.call(this, method, url, ...rest); }; } } catch (err) { window.postMessage({ type: 'XVM_TM_PAGE_HOOK_STATUS', mode: 'page-script-xhr-failed', error: String(err?.message || err), at: Date.now() }, '*'); } window.postMessage({ type: 'XVM_TM_PAGE_HOOK_STATUS', mode: 'page-script-active', at: Date.now() }, '*'); })();`; try { root.appendChild(script); script.remove(); debugState.pageHookMode = 'script-tag-injected'; debugLog('page-world script hook injected'); } catch (err) { debugState.pageHookMode = 'script-tag-failed'; debugLog('page-world script hook injection failed', err?.message || err); } } function scanForTweets(obj) { if (!obj || typeof obj !== 'object') return 0; let found = 0; if (obj.tweet_results?.result) { const data = extractTweetData(obj.tweet_results.result); if (data) { tweetDataStore.set(data.id, data); found += 1; } } if (Array.isArray(obj)) { for (const item of obj) found += scanForTweets(item); } else { for (const key of Object.keys(obj)) { if (key === 'tweet_results') continue; found += scanForTweets(obj[key]); } } if (found) debugLog('tweets extracted from GraphQL payload', { found, stored: tweetDataStore.size }); return found; } function extractTweetData(result) { const tweet = result?.tweet || result; const legacy = tweet?.legacy; if (!legacy) return null; const rtResult = legacy.retweeted_status_result?.result; if (rtResult) return extractTweetData(rtResult); if (legacy.promotedMetadata || tweet.promotedMetadata) return null; const viewCount = Number.parseInt(tweet.views?.count, 10); if (!viewCount || (tweet.views?.state && tweet.views.state !== 'EnabledWithCount')) return null; const id = legacy.id_str || tweet.rest_id || result?.rest_id; if (!id) return null; const user = tweet.core?.user_results?.result || {}; const userLegacy = user.legacy || {}; const noteText = tweet.note_tweet?.note_tweet_results?.result?.text; return { id, views: viewCount, likes: legacy.favorite_count || 0, retweets: legacy.retweet_count || 0, replies: legacy.reply_count || 0, bookmarks: legacy.bookmark_count || 0, createdAt: legacy.created_at, text: noteText || legacy.full_text || '', authorName: userLegacy.name || '', authorScreenName: userLegacy.screen_name || '', }; } function formatVelocity(v) { if (v >= 1000000) return (v / 1000000).toFixed(1) + 'M'; if (v >= 1000) return (v / 1000).toFixed(1) + 'k'; return Math.round(v).toString(); } function computeScore(data) { const created = new Date(data.createdAt).getTime(); const rawHours = Number.isFinite(created) ? (Date.now() - created) / 3600000 : 0; const minHours = data.estimatedCreatedAt ? 1 : 5 / 60; const hours = Math.max(Number.isFinite(data.estimatedAgeHours) ? data.estimatedAgeHours : rawHours, minHours); const velocity = data.views / hours; const engagements = data.likes + data.retweets + data.replies; const engagementRate = data.views > 0 ? engagements / data.views : 0; const rtRatio = data.likes > 0 ? data.retweets / data.likes : 0; const bmRatio = data.likes > 0 ? data.bookmarks / data.likes : 0; const score = Math.round( Math.min(velocity / 50000, 1) * 40 + Math.min(engagementRate / 0.1, 1) * 25 + Math.min(rtRatio / 0.5, 1) * 20 + Math.min(bmRatio / 0.3, 1) * 15 ); return { velocity, score: Math.min(score, 100) }; } let tooltipEl = null; function getTooltip() { if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.className = 'xvm-tooltip'; tooltipEl.addEventListener('mouseleave', () => { tooltipEl.style.display = 'none'; }); document.body.appendChild(tooltipEl); } return tooltipEl; } function getTweetIdFromArticle(article) { const links = article.querySelectorAll('a[href*="/status/"]'); for (const link of links) { const match = (link.getAttribute('href') || '').match(/\/status\/(\d+)$/); if (match && tweetDataStore.has(match[1])) return match[1]; } const firstLink = article.querySelector('a[href*="/status/"]'); const match = (firstLink?.getAttribute('href') || '').match(/\/status\/(\d+)/); return match ? match[1] : null; } function getAuthorInfo(article) { const nameBlock = article.querySelector('[data-testid="User-Name"]'); let displayName = ''; let handle = ''; if (nameBlock) { const spans = nameBlock.querySelectorAll('span'); for (const span of spans) { const text = (span.textContent || '').trim(); if (!handle && text.startsWith('@')) handle = text; else if (!displayName && text && !text.startsWith('@') && text !== '·') displayName = text; if (displayName && handle) break; } } return { displayName, handle }; } function parseCompactNumber(raw) { const text = String(raw || '').trim(); if (!text) return 0; const match = text.match(/([\d,.]+)\s*([KkMm]|万|千)?/); if (!match) return 0; const n = Number.parseFloat(match[1].replace(/,/g, '')); if (!Number.isFinite(n)) return 0; const suffix = match[2] || ''; if (suffix === '万') return Math.round(n * 10000); if (suffix === '千' || suffix.toLowerCase() === 'k') return Math.round(n * 1000); if (suffix.toLowerCase() === 'm') return Math.round(n * 1000000); return Math.round(n); } function textCandidatesForMetric(article, selectors) { const out = []; for (const selector of selectors) { article.querySelectorAll(selector).forEach((node) => { out.push(node.getAttribute?.('aria-label') || ''); out.push(node.getAttribute?.('title') || ''); out.push(node.textContent || ''); }); } return out.map((s) => String(s || '').trim()).filter(Boolean); } function firstMetric(article, selectors, labelPattern) { const candidates = textCandidatesForMetric(article, selectors); for (const text of candidates) { if (labelPattern && !labelPattern.test(text)) continue; const n = parseCompactNumber(text); if (n > 0) return n; } if (labelPattern) { for (const node of article.querySelectorAll('[aria-label], [title]')) { const text = `${node.getAttribute('aria-label') || ''} ${node.getAttribute('title') || ''}`; if (!labelPattern.test(text)) continue; const n = parseCompactNumber(text); if (n > 0) return n; } } return 0; } function getTweetTextFromArticle(article) { const tweetText = article.querySelector('[data-testid="tweetText"]'); if (tweetText?.textContent) return tweetText.textContent.trim(); const blocks = [...article.querySelectorAll('div[lang]')] .map((node) => (node.textContent || '').trim()) .filter(Boolean); return blocks[0] || ''; } function getCreatedAtFromArticle(article) { const value = article.querySelector('time[datetime]')?.getAttribute('datetime') || ''; const parsed = value ? new Date(value) : null; if (parsed && !Number.isNaN(parsed.getTime())) { return { createdAt: parsed.toUTCString(), estimatedAgeHours: null, estimated: false, }; } const fallbackAgeHours = 1; return { createdAt: new Date(Date.now() - fallbackAgeHours * 3600000).toUTCString(), estimatedAgeHours: fallbackAgeHours, estimated: true, }; } function extractVisibleTweetData(article, id) { const replies = firstMetric(article, ['[data-testid="reply"]'], /reply|repl|回复|回覆|返信/i); const retweets = firstMetric(article, ['[data-testid="retweet"]', '[data-testid="unretweet"]'], /repost|retweet|转发|轉發|リポスト/i); const likes = firstMetric(article, ['[data-testid="like"]', '[data-testid="unlike"]'], /like|喜欢|喜歡|いいね/i); const bookmarks = firstMetric(article, ['[data-testid="bookmark"]', '[data-testid="removeBookmark"]'], /bookmark|收藏|ブックマーク/i); let views = firstMetric(article, ['a[href$="/analytics"]', 'a[href*="/analytics"]', '[aria-label*="view" i]', '[aria-label*="查看"]', '[aria-label*="表示"]'], /view|views|查看|浏览|瀏覽|表示/i); if (!views) { const numbers = [...article.querySelectorAll('a[href*="/analytics"], [aria-label], [title], [role="group"]')] .map((node) => `${node.getAttribute?.('aria-label') || ''} ${node.getAttribute?.('title') || ''} ${node.textContent || ''}`) .flatMap((text) => String(text).split(/\s+/)) .map(parseCompactNumber) .filter((n) => n > 0); views = numbers.length ? Math.max(...numbers) : 0; } const engagementFloor = likes + retweets + replies + bookmarks; if (!views && engagementFloor) views = Math.max(engagementFloor * 20, engagementFloor + 1); if (!views) return null; const { displayName, handle } = getAuthorInfo(article); const domTime = getCreatedAtFromArticle(article); return { id, views, likes, retweets, replies, bookmarks, createdAt: domTime.createdAt, estimatedAgeHours: domTime.estimatedAgeHours, estimatedCreatedAt: domTime.estimated, text: getTweetTextFromArticle(article), authorName: displayName || '', authorScreenName: handle.replace(/^@/, ''), source: 'dom-visible-fallback', }; } let leaderboardEl = null; let selectedLeaderboardId = ''; let leaderboardHtml = ''; let leaderboardDragInstalled = false; let leaderboardResizeInstalled = false; let leaderboardResizeHeightInstalled = false; let savedScrollY = null; let linkState = null; let linkUpdateRaf = 0; let linkFollowRaf = 0; let leaderboardListClickInstalled = false; let lbScrollTick = false; const SVG_NS = 'http://www.w3.org/2000/svg'; function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } function formatViews(n) { if (!Number.isFinite(n) || n <= 0) return '0'; if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return String(n); } function formatPostedDate(date, fallback) { if (Number.isNaN(date.getTime())) return fallback || ''; const pad = (n) => String(n).padStart(2, '0'); return `${date.getFullYear()}:${pad(date.getMonth() + 1)}:${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function tierForVelocity(velocity) { if (velocity >= velocityThresholds.viral) return 'red'; if (velocity >= velocityThresholds.trending) return 'orange'; return 'green'; } function iconForTier(tier) { if (tier === 'red') return '🔥'; if (tier === 'orange') return '🚀'; return '🌱'; } const leaderboardColumnRenderers = { rank: (_item, index) => `${index + 1}`, icon: (item) => `${iconForTier(item.tier)}`, handle: (item) => { const handle = (item.handle || '').trim() || item.authorName || t('contentFallbackTweetLabel'); return `${escapeHtml(handle)}`; }, preview: (item) => { const text = (item.text || '').replace(/\s+/g, ' ').trim(); return `${escapeHtml(text)}`; }, views: (item) => `👁 ${formatViews(item.views)}`, velocity: (item) => `${formatVelocity(item.velocity)}/h`, }; function ensureLeaderboard() { if (leaderboardEl) return leaderboardEl; leaderboardEl = document.createElement('div'); leaderboardEl.className = 'xvm-lb'; leaderboardEl.innerHTML = `
⋮⋮ 🔥 ${escapeHtml(t('contentLeaderboardTitle'))} 0
${escapeHtml(t('settingColumns'))}
`; document.body.appendChild(leaderboardEl); applyLeaderboardWidth(); applyLeaderboardHeight(); applyLeaderboardPosition(); installLeaderboardDrag(); installLeaderboardResize(); installLeaderboardResizeHeight(); installLeaderboardBackButton(); installSettingsPanel(); return leaderboardEl; } function syncSettingsForm() { if (!leaderboardEl) return; const enabled = leaderboardEl.querySelector('.xvm-setting-enabled'); const badgeStyle = leaderboardEl.querySelector('.xvm-setting-badge-style'); const trending = leaderboardEl.querySelector('.xvm-setting-trending'); const viral = leaderboardEl.querySelector('.xvm-setting-viral'); const count = leaderboardEl.querySelector('.xvm-setting-count'); if (enabled) enabled.checked = settings.leaderboardEnabled !== false; if (badgeStyle) badgeStyle.value = settings.badgeStyle; setSettingsBadgeStyleValue(settings.badgeStyle); if (trending) trending.value = String(settings.trending); if (viral) viral.value = String(settings.viral); if (count) count.value = String(settings.leaderboardCount); renderColumnSettings(); } function renderColumnSettings() { const root = leaderboardEl?.querySelector('.xvm-setting-columns'); if (!root) return; root.innerHTML = settings.leaderboardColumns.map((column) => ` `).join(''); } function installSettingsPanel() { const btn = leaderboardEl?.querySelector('.xvm-lb-settings-btn'); const panel = leaderboardEl?.querySelector('.xvm-settings'); const save = leaderboardEl?.querySelector('.xvm-settings-save'); if (!btn || !panel || !save) return; installSettingsSelect(); syncSettingsForm(); btn.addEventListener('mousedown', (event) => event.stopPropagation()); btn.addEventListener('click', (event) => { event.stopPropagation(); panel.classList.toggle('xvm-settings-open'); syncSettingsForm(); }); panel.addEventListener('mousedown', (event) => event.stopPropagation()); panel.addEventListener('click', (event) => event.stopPropagation()); save.addEventListener('click', (event) => { event.stopPropagation(); const nextTrending = Number.parseInt(leaderboardEl.querySelector('.xvm-setting-trending')?.value, 10); const nextViral = Number.parseInt(leaderboardEl.querySelector('.xvm-setting-viral')?.value, 10); const nextCount = Number.parseInt(leaderboardEl.querySelector('.xvm-setting-count')?.value, 10); const nextStyle = leaderboardEl.querySelector('.xvm-setting-badge-style')?.value; const nextEnabled = leaderboardEl.querySelector('.xvm-setting-enabled')?.checked; const nextColumns = settings.leaderboardColumns.map((column) => { const checkbox = leaderboardEl.querySelector(`.xvm-setting-columns input[data-column-id="${column.id}"]`); return { id: column.id, visible: checkbox ? checkbox.checked : column.visible }; }); settings.trending = Number.isFinite(nextTrending) && nextTrending > 0 ? nextTrending : 1000; settings.viral = Number.isFinite(nextViral) && nextViral > settings.trending ? nextViral : Math.max(10000, settings.trending + 1); settings.leaderboardCount = Number.isFinite(nextCount) ? Math.max(1, Math.min(50, nextCount)) : 10; settings.leaderboardColumns = normalizeColumns(nextColumns); settings.badgeStyle = nextStyle === 'inline-classic' ? 'inline-classic' : 'pill-solid'; settings.leaderboardEnabled = nextEnabled !== false; saveSettings(); applySettings(); syncSettingsForm(); panel.classList.remove('xvm-settings-open'); }); } function setSettingsBadgeStyleValue(value) { const root = leaderboardEl?.querySelector('.xvm-setting-select'); const input = root?.querySelector('.xvm-setting-badge-style'); const label = root?.querySelector('.xvm-setting-select-label'); const choices = root?.querySelectorAll('.xvm-setting-select-choice') || []; const next = value === 'inline-classic' ? 'inline-classic' : 'pill-solid'; if (input) input.value = next; let text = ''; choices.forEach((choice) => { const selected = choice.dataset.value === next; choice.setAttribute('aria-selected', selected ? 'true' : 'false'); if (selected) text = choice.textContent || ''; }); if (label) label.textContent = text; } function closeSettingsSelect() { const root = leaderboardEl?.querySelector('.xvm-setting-select'); const trigger = root?.querySelector('.xvm-setting-select-trigger'); if (!root || !trigger) return; root.dataset.open = '0'; trigger.setAttribute('aria-expanded', 'false'); } function openSettingsSelect() { const root = leaderboardEl?.querySelector('.xvm-setting-select'); const trigger = root?.querySelector('.xvm-setting-select-trigger'); if (!root || !trigger) return; root.dataset.open = '1'; trigger.setAttribute('aria-expanded', 'true'); } function installSettingsSelect() { const root = leaderboardEl?.querySelector('.xvm-setting-select'); const trigger = root?.querySelector('.xvm-setting-select-trigger'); const choices = root?.querySelectorAll('.xvm-setting-select-choice') || []; if (!root || !trigger || root.dataset.wired === '1') return; root.dataset.wired = '1'; trigger.addEventListener('click', () => { if (root.dataset.open === '1') closeSettingsSelect(); else openSettingsSelect(); }); trigger.addEventListener('keydown', (event) => { if (!['Enter', ' ', 'ArrowDown'].includes(event.key)) return; event.preventDefault(); openSettingsSelect(); root.querySelector('.xvm-setting-select-choice[aria-selected="true"]')?.focus(); }); choices.forEach((choice) => { choice.addEventListener('click', () => { setSettingsBadgeStyleValue(choice.dataset.value); closeSettingsSelect(); trigger.focus(); }); choice.addEventListener('keydown', (event) => { const list = [...choices]; const index = list.indexOf(choice); if (event.key === 'Escape') { event.preventDefault(); closeSettingsSelect(); trigger.focus(); } else if (event.key === 'ArrowDown') { event.preventDefault(); list[Math.min(index + 1, list.length - 1)]?.focus(); } else if (event.key === 'ArrowUp') { event.preventDefault(); list[Math.max(index - 1, 0)]?.focus(); } else if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); choice.click(); } }); }); document.addEventListener('click', (event) => { if (!root.contains(event.target)) closeSettingsSelect(); }); } function setBackButtonVisible(visible) { const btn = leaderboardEl?.querySelector('.xvm-lb-back'); if (!btn) return; if (visible) btn.removeAttribute('hidden'); else btn.setAttribute('hidden', ''); } function installLeaderboardBackButton() { const btn = leaderboardEl?.querySelector('.xvm-lb-back'); if (!btn) return; btn.addEventListener('mousedown', (event) => event.stopPropagation()); btn.addEventListener('click', (event) => { event.stopPropagation(); if (savedScrollY === null) return; const target = savedScrollY; clearLink(); window.scrollTo({ top: target, behavior: 'smooth' }); savedScrollY = null; setBackButtonVisible(false); }); } function clampLeaderboardWidth(width) { const safeWidth = Number.isFinite(width) ? width : 280; const maxByViewport = Math.max(240, Math.min(640, window.innerWidth - 16)); const left = settings.leaderboardPos?.left; const maxByPosition = Number.isFinite(left) ? Math.max(240, Math.min(maxByViewport, window.innerWidth - left - 8)) : maxByViewport; return Math.max(240, Math.min(safeWidth, maxByPosition)); } function applyLeaderboardWidth() { if (!leaderboardEl) return; settings.leaderboardWidth = clampLeaderboardWidth(settings.leaderboardWidth); leaderboardEl.style.width = `${settings.leaderboardWidth}px`; } function clampLeaderboardHeight(height) { const safeHeight = Number.isFinite(height) ? height : 300; const maxByViewport = Math.max(120, Math.min(800, window.innerHeight - 80)); const top = settings.leaderboardPos?.top; const maxByPosition = Number.isFinite(top) ? Math.max(120, Math.min(maxByViewport, window.innerHeight - top - 16)) : maxByViewport; return Math.max(120, Math.min(safeHeight, maxByPosition)); } function applyLeaderboardHeight() { if (!leaderboardEl) return; settings.leaderboardHeight = clampLeaderboardHeight(settings.leaderboardHeight); const list = leaderboardEl.querySelector('.xvm-lb-list'); if (list) list.style.maxHeight = `${settings.leaderboardHeight}px`; } function applyLeaderboardPosition() { if (!leaderboardEl) return; applyLeaderboardWidth(); applyLeaderboardHeight(); const pos = settings.leaderboardPos; if (pos && Number.isFinite(pos.left) && Number.isFinite(pos.top)) { const clamped = clampLeaderboardToViewport(pos.left, pos.top); settings.leaderboardPos = clamped; leaderboardEl.style.left = `${clamped.left}px`; leaderboardEl.style.top = `${clamped.top}px`; leaderboardEl.style.right = 'auto'; } } function clampLeaderboardToViewport(left, top) { const rect = leaderboardEl.getBoundingClientRect(); return { left: Math.max(8, Math.min(left, window.innerWidth - rect.width - 8)), top: Math.max(8, Math.min(top, window.innerHeight - rect.height - 8)), }; } function installLeaderboardDrag() { if (!leaderboardEl || leaderboardDragInstalled) return; leaderboardDragInstalled = true; const head = leaderboardEl.querySelector('.xvm-lb-head'); if (!head) return; let dragState = null; let dragRaf = 0; let pendingX = 0; let pendingY = 0; const flush = () => { dragRaf = 0; if (!dragState) return; const pos = clampLeaderboardToViewport(pendingX - dragState.offsetX, pendingY - dragState.offsetY); leaderboardEl.style.left = `${pos.left}px`; leaderboardEl.style.top = `${pos.top}px`; leaderboardEl.style.right = 'auto'; settings.leaderboardPos = pos; updateLinkGeometry(); }; head.addEventListener('mousedown', (event) => { if (event.button !== 0) return; const rect = leaderboardEl.getBoundingClientRect(); dragState = { offsetX: event.clientX - rect.left, offsetY: event.clientY - rect.top, }; leaderboardEl.classList.add('xvm-lb-dragging'); event.preventDefault(); }); window.addEventListener('mousemove', (event) => { if (!dragState) return; pendingX = event.clientX; pendingY = event.clientY; if (!dragRaf) dragRaf = requestAnimationFrame(flush); }, { passive: true }); window.addEventListener('mouseup', () => { if (!dragState) return; dragState = null; leaderboardEl.classList.remove('xvm-lb-dragging'); if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; } saveSettings(); updateLinkGeometry(); }); } function installLeaderboardResize() { if (!leaderboardEl || leaderboardResizeInstalled) return; leaderboardResizeInstalled = true; const handle = leaderboardEl.querySelector('.xvm-lb-resize'); if (!handle) return; let resizeState = null; let resizeRaf = 0; let pendingX = 0; const flush = () => { resizeRaf = 0; if (!resizeState) return; settings.leaderboardWidth = clampLeaderboardWidth(resizeState.startWidth + (pendingX - resizeState.startX)); applyLeaderboardWidth(); applyLeaderboardPosition(); updateLinkGeometry(); }; handle.addEventListener('mousedown', (event) => { if (event.button !== 0) return; resizeState = { startWidth: leaderboardEl.getBoundingClientRect().width, startX: event.clientX, }; leaderboardEl.classList.add('xvm-lb-resizing'); event.stopPropagation(); event.preventDefault(); }); window.addEventListener('mousemove', (event) => { if (!resizeState) return; pendingX = event.clientX; if (!resizeRaf) resizeRaf = requestAnimationFrame(flush); }, { passive: true }); window.addEventListener('mouseup', () => { if (!resizeState) return; resizeState = null; leaderboardEl.classList.remove('xvm-lb-resizing'); if (resizeRaf) { cancelAnimationFrame(resizeRaf); resizeRaf = 0; } saveSettings(); updateLinkGeometry(); }); } function installLeaderboardResizeHeight() { if (!leaderboardEl || leaderboardResizeHeightInstalled) return; leaderboardResizeHeightInstalled = true; const handle = leaderboardEl.querySelector('.xvm-lb-resize-v'); if (!handle) return; let resizeState = null; let resizeRaf = 0; let pendingY = 0; const flush = () => { resizeRaf = 0; if (!resizeState) return; settings.leaderboardHeight = clampLeaderboardHeight(resizeState.startHeight + (pendingY - resizeState.startY)); applyLeaderboardHeight(); updateLinkGeometry(); }; handle.addEventListener('mousedown', (event) => { if (event.button !== 0) return; const list = leaderboardEl.querySelector('.xvm-lb-list'); resizeState = { startHeight: list ? list.getBoundingClientRect().height : settings.leaderboardHeight, startY: event.clientY, }; leaderboardEl.classList.add('xvm-lb-resizing'); event.stopPropagation(); event.preventDefault(); }); window.addEventListener('mousemove', (event) => { if (!resizeState) return; pendingY = event.clientY; if (!resizeRaf) resizeRaf = requestAnimationFrame(flush); }, { passive: true }); window.addEventListener('mouseup', () => { if (!resizeState) return; resizeState = null; leaderboardEl.classList.remove('xvm-lb-resizing'); if (resizeRaf) { cancelAnimationFrame(resizeRaf); resizeRaf = 0; } saveSettings(); updateLinkGeometry(); }); } function getArticleByTweetId(id) { const articles = document.querySelectorAll('article[data-testid="tweet"]'); for (const article of articles) { if (getTweetIdFromArticle(article) === id) return article; } return null; } function getLeaderboardItemById(id) { const article = getArticleByTweetId(id); const data = tweetDataStore.get(id); if (!article || !data) return null; const { velocity } = computeScore(data); const { displayName, handle: domHandle } = getAuthorInfo(article); return { ...data, velocity, tier: tierForVelocity(velocity), handle: displayName || domHandle || (data.authorScreenName ? `@${data.authorScreenName}` : '') || data.authorName || '', article, }; } function collectLeaderboardItems() { const seen = new Set(); const items = []; const articles = document.querySelectorAll('article[data-testid="tweet"]'); for (const article of articles) { const id = getTweetIdFromArticle(article); if (!id || seen.has(id)) continue; const data = tweetDataStore.get(id); if (!data) continue; seen.add(id); const { velocity } = computeScore(data); const { displayName, handle: domHandle } = getAuthorInfo(article); items.push({ ...data, velocity, tier: tierForVelocity(velocity), handle: displayName || domHandle || (data.authorScreenName ? `@${data.authorScreenName}` : '') || data.authorName || '', article, }); } return items .sort((a, b) => b.velocity - a.velocity) .slice(0, settings.leaderboardCount); } function ensureLinkSvg() { const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('class', 'xvm-lb-link'); svg.style.position = 'fixed'; svg.style.left = '0'; svg.style.top = '0'; svg.style.width = '100vw'; svg.style.height = '100vh'; svg.style.pointerEvents = 'none'; svg.style.zIndex = '2147483645'; svg.innerHTML = ` `; document.body.appendChild(svg); return svg; } function updateLinkGeometry() { if (!linkState) return; const { tweetId, itemEl, svg } = linkState; let article = linkState.article; if (!article || !article.isConnected) { article = getArticleByTweetId(tweetId); linkState.article = article; } if (!itemEl.isConnected || !article) { if (!article) svg.style.display = 'none'; return; } svg.style.display = ''; if (!article.classList.contains('xvm-article-linked')) article.classList.add('xvm-article-linked'); const itemRect = itemEl.getBoundingClientRect(); const articleRect = article.getBoundingClientRect(); const itemCx = itemRect.left + itemRect.width / 2; const articleCx = articleRect.left + articleRect.width / 2; const startOnRight = articleCx >= itemCx; const startX = startOnRight ? itemRect.right : itemRect.left; const startY = itemRect.top + itemRect.height / 2; const articleVisibleTop = Math.max(articleRect.top, 8); const articleVisibleBottom = Math.min(articleRect.bottom, window.innerHeight - 8); const endY = Math.max(articleVisibleTop, Math.min(startY, articleVisibleBottom)); const endX = startOnRight ? articleRect.left : articleRect.right; const dx = Math.abs(endX - startX); const handle = Math.max(60, dx * 0.4); const c1x = startX + (startOnRight ? handle : -handle); const c2x = endX - (startOnRight ? handle : -handle); svg.querySelector('.xvm-lb-link-path')?.setAttribute('d', `M ${startX},${startY} C ${c1x},${startY} ${c2x},${endY} ${endX},${endY}`); const start = svg.querySelector('.xvm-lb-link-start'); start?.setAttribute('cx', startX); start?.setAttribute('cy', startY); const end = svg.querySelector('.xvm-lb-link-end'); end?.setAttribute('cx', endX); end?.setAttribute('cy', endY); } function scheduleLinkUpdate() { if (!linkState || linkUpdateRaf) return; linkUpdateRaf = requestAnimationFrame(() => { linkUpdateRaf = 0; updateLinkGeometry(); }); } function startLinkFollowLoop() { if (linkFollowRaf) return; const tick = () => { if (!linkState) return; updateLinkGeometry(); linkFollowRaf = requestAnimationFrame(tick); }; linkFollowRaf = requestAnimationFrame(tick); } function setLink(tweetId, itemEl, article) { clearLink(); const svg = ensureLinkSvg(); selectedLeaderboardId = tweetId; itemEl.classList.add('xvm-lb-item-selected'); article.classList.add('xvm-article-linked'); linkState = { tweetId, itemEl, article, svg }; updateLinkGeometry(); startLinkFollowLoop(); } function clearLink() { if (!linkState) return; linkState.itemEl?.classList.remove('xvm-lb-item-selected'); linkState.article?.classList.remove('xvm-article-linked'); const stale = getArticleByTweetId(linkState.tweetId); stale?.classList.remove('xvm-article-linked'); linkState.svg?.remove(); selectedLeaderboardId = ''; linkState = null; if (linkUpdateRaf) { cancelAnimationFrame(linkUpdateRaf); linkUpdateRaf = 0; } if (linkFollowRaf) { cancelAnimationFrame(linkFollowRaf); linkFollowRaf = 0; } } function installLeaderboardListClick(list) { if (leaderboardListClickInstalled) return; leaderboardListClickInstalled = true; list.addEventListener('click', (event) => { const row = event.target.closest('.xvm-lb-item'); if (!row) return; event.stopPropagation(); const id = row.dataset.id; const item = getLeaderboardItemById(id); if (!item?.article?.isConnected) return; if (linkState && linkState.tweetId === item.id) { clearLink(); return; } if (savedScrollY === null) { savedScrollY = window.scrollY; setBackButtonVisible(true); } item.article.scrollIntoView({ behavior: 'smooth', block: 'center' }); setLink(item.id, row, item.article); }); } function hideLeaderboard() { if (leaderboardEl) leaderboardEl.style.display = 'none'; clearLink(); } function renderLeaderboard() { if (!document.body) return; if (!ENABLE_DEBUG_LEADERBOARD || !settings.leaderboardEnabled) { hideLeaderboard(); return; } const items = collectLeaderboardItems(); debugState.leaderboardItems = items.length; const el = ensureLeaderboard(); const list = el.querySelector('.xvm-lb-list'); const count = el.querySelector('.xvm-lb-count'); installLeaderboardListClick(list); if (!items.length) { el.style.display = 'none'; list.innerHTML = ''; count.textContent = '0'; leaderboardHtml = ''; clearLink(); return; } el.style.display = 'block'; count.textContent = String(items.length); const visibleColumns = settings.leaderboardColumns.filter((column) => column.visible && leaderboardColumnRenderers[column.id]); const nextHtml = items.map((item, index) => { const handle = (item.handle || '').trim() || item.authorName || t('contentFallbackTweetLabel'); const text = (item.text || '').replace(/\s+/g, ' ').trim(); const selected = item.id === selectedLeaderboardId ? ' xvm-lb-item-selected' : ''; const cells = visibleColumns.map((column) => leaderboardColumnRenderers[column.id](item, index)).join(''); return `
  • ${cells}
  • `; }).join(''); if (nextHtml === leaderboardHtml) return; leaderboardHtml = nextHtml; list.innerHTML = nextHtml; if (linkState) { const row = list.querySelector(`.xvm-lb-item[data-id="${linkState.tweetId}"]`); if (row) { linkState.itemEl = row; row.classList.add('xvm-lb-item-selected'); updateLinkGeometry(); } else { clearLink(); } } } function renderBadges() { const articles = document.querySelectorAll('article[data-testid="tweet"]'); debugState.articles = articles.length; debugState.articleScanCount += articles.length; for (const article of articles) { const tweetId = getTweetIdFromArticle(article); if (!tweetId) { debugState.lastBadgeReason = 'skip:no-tweet-id'; continue; } if (article.hasAttribute('data-xvm-tm-scored')) { debugState.lastBadgeReason = 'skip:already-scored'; continue; } let data = tweetDataStore.get(tweetId); if (!data) { data = extractVisibleTweetData(article, tweetId); if (data) { tweetDataStore.set(tweetId, data); debugState.domFallbackTweets += 1; debugLog('DOM visible metrics fallback extracted', { tweetId, views: data.views, likes: data.likes, retweets: data.retweets, replies: data.replies, estimatedAgeHours: data.estimatedAgeHours || '', estimatedCreatedAt: !!data.estimatedCreatedAt, }); } else { debugState.lastBadgeReason = 'skip:no-captured-data'; continue; } } debugState.badgeMountAttempts += 1; const caretBtn = article.querySelector('[data-testid="caret"]'); if (!caretBtn) { debugState.lastBadgeReason = 'skip:no-caret-button'; continue; } let headerRow = caretBtn; while (headerRow && headerRow !== article) { if (headerRow.getBoundingClientRect().width > 200) break; headerRow = headerRow.parentElement; } if (!headerRow || headerRow === article) { debugState.lastBadgeReason = 'skip:no-header-row'; continue; } const { velocity, score } = computeScore(data); const tier = velocity >= velocityThresholds.viral ? 'viral' : velocity >= velocityThresholds.trending ? 'trending' : 'normal'; const badge = document.createElement('span'); badge.className = `xvm-badge xvm-badge--${tier === 'viral' ? 'red' : tier === 'trending' ? 'orange' : 'green'}`; badge.dataset.prefix = tier === 'viral' ? '🔥' : tier === 'trending' ? '🚀' : '🌱'; badge.dataset.velocity = formatVelocity(velocity); const posted = new Date(data.createdAt); const tooltipText = [ `${t('contentViews')}: ${data.views.toLocaleString(currentLocale)}`, `${t('contentLikes')}: ${data.likes.toLocaleString(currentLocale)}`, `${t('contentRetweets')}: ${data.retweets.toLocaleString(currentLocale)}`, `${t('contentReplies')}: ${data.replies.toLocaleString(currentLocale)}`, `${t('contentBookmarks')}: ${data.bookmarks.toLocaleString(currentLocale)}`, `${t('contentVelocity')}: ${formatVelocity(velocity)}/h`, `${t('contentViralScore')}: ${score}/100`, `${t('contentPosted')}: ${formatPostedDate(posted, data.createdAt)}`, ].join('\n'); badge.addEventListener('mouseenter', () => { const tip = getTooltip(); tip.textContent = tooltipText; const rect = badge.getBoundingClientRect(); tip.style.display = 'block'; tip.style.top = `${rect.bottom + 6}px`; const tipWidth = tip.offsetWidth; tip.style.left = `${Math.max(8, rect.right - tipWidth)}px`; }); badge.addEventListener('mouseleave', (e) => { const tip = getTooltip(); if (!tip.contains(e.relatedTarget)) tip.style.display = 'none'; }); article.setAttribute('data-xvm-tm-scored', '1'); headerRow.insertBefore(badge, headerRow.lastElementChild); debugState.badgeMounts += 1; debugState.badges = document.querySelectorAll('.xvm-badge').length; debugState.lastBadgeReason = 'mounted'; debugLog('badge mounted', { tweetId, tier, velocity: Math.round(velocity) }); } } function scheduleRender() { if (scheduleRender.raf) return; scheduleRender.raf = requestAnimationFrame(() => { scheduleRender.raf = 0; renderBadges(); if (ENABLE_DEBUG_LEADERBOARD && settings.leaderboardEnabled) renderLeaderboard(); else hideLeaderboard(); }); } window.addEventListener('scroll', scheduleLinkUpdate, { capture: true, passive: true }); window.addEventListener('scroll', () => { if (lbScrollTick) return; lbScrollTick = true; setTimeout(() => { lbScrollTick = false; if (ENABLE_DEBUG_LEADERBOARD && settings.leaderboardEnabled) renderLeaderboard(); }, 250); }, { passive: true }); window.addEventListener('resize', () => { applyLeaderboardWidth(); applyLeaderboardHeight(); applyLeaderboardPosition(); scheduleLinkUpdate(); }, { passive: true }); document.addEventListener('click', (event) => { if (!linkState) return; const insideItem = linkState.itemEl && linkState.itemEl.contains(event.target); const insideArticle = linkState.article && linkState.article.contains(event.target); const insidePanel = leaderboardEl && leaderboardEl.contains(event.target); if (!insideItem && !insideArticle && !insidePanel) clearLink(); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && linkState) clearLink(); }); const seenGraphqlMessages = new Set(); function handleGraphqlMessage(event) { if (event.data?.type === 'XVM_TM_PAGE_HOOK_STATUS') { debugState.pageHookMode = event.data.mode || debugState.pageHookMode || 'page-status'; if (event.data.error) debugState.lastIgnoredReason = event.data.error; debugLog('page-world hook status', { mode: debugState.pageHookMode, error: event.data.error || '' }); return; } if (event.data?.type === 'XVM_TM_GRAPHQL_REQUEST') { recordGraphqlDebug({ source: event.data.source || 'page-request', method: event.data.method || 'GET', url: event.data.url || '', requestBody: event.data.body || '', }); debugLog('GraphQL request captured', { opName: event.data.opName || graphqlOpNameFromUrl(event.data.url || '') }); return; } if (event.data?.type !== 'XVM_TM_GRAPHQL_RESPONSE') return; const messageKey = [ event.data.source || '', event.data.url || '', event.data.capturedAt || '', ].join('|'); if (seenGraphqlMessages.has(messageKey)) return; seenGraphqlMessages.add(messageKey); if (seenGraphqlMessages.size > 1000) seenGraphqlMessages.clear(); debugState.receivedMessages += 1; debugState.lastMessageUrl = event.data.url || ''; if (!GRAPHQL_RE.test(event.data.url || '')) { debugState.ignoredMessages += 1; debugState.lastIgnoredReason = 'non-graphql-url'; debugLog('GraphQL message ignored', { reason: debugState.lastIgnoredReason, url: event.data.url || '' }); return; } debugState.capturedGraphql += 1; const found = scanForTweets(event.data.payload); debugLog('GraphQL message accepted', { opName: event.data.opName || '', found, totalCaptured: debugState.capturedGraphql, }); if (found) { debugState.extractedTweets += found; console.debug('[XVM-TM] GraphQL captured', event.data.opName, 'tweets:', found); scheduleRender(); } debugState.lastCapturedAt = Date.now(); } if (ENABLE_MOBILE_DEBUG) { window.addEventListener('message', handleGraphqlMessage); if (debugWindow !== window && debugWindow.addEventListener) { debugWindow.addEventListener('message', handleGraphqlMessage); } } function bootDomObservers() { injectCss(); if (ENABLE_MOBILE_DEBUG) { installDebugOverlay(); installResourceObserver(); } const observer = new MutationObserver(scheduleRender); observer.observe(document.documentElement, { childList: true, subtree: true }); setInterval(scheduleRender, 2000); debugLog('DOM observers installed', { readyState: document.readyState, href: location.href }); scheduleRender(); } function openSettingsPanel() { if (!ENABLE_DEBUG_LEADERBOARD) return; settings.leaderboardEnabled = true; saveSettings(); const el = ensureLeaderboard(); el.style.display = 'block'; const panel = el.querySelector('.xvm-settings'); if (panel) panel.classList.add('xvm-settings-open'); syncSettingsForm(); scheduleRender(); } if (ENABLE_DEBUG_LEADERBOARD && typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand(t('contentLeaderboardSettings') || 'Settings', openSettingsPanel); } if (ENABLE_MOBILE_DEBUG) injectPageHook(); debugLog('mobile userscript boot', { href: location.href, readyState: document.readyState, debug: ENABLE_MOBILE_DEBUG }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootDomObservers, { once: true }); } else { bootDomObservers(); } })();