// ==UserScript== // @name X Viral Monitor Minimal Badge // @namespace https://github.com/Icy-Cat/x-viral-monitor // @version 0.1.23 // @description Minimal X velocity badges from GraphQL tweet metrics. // @author IcyCat, Chlience // @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/* // @run-at document-start // @sandbox JavaScript // @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 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; } function getScriptHostWindow() { try { if (typeof unsafeWindow === 'object' && unsafeWindow) return unsafeWindow; } catch (_) {} return window; } function getPageWindow() { const host = getScriptHostWindow(); try { return host.wrappedJSObject || host; } catch (_) { return host; } } function exportToPage(fn) { const host = getScriptHostWindow(); try { if (typeof exportFunction === 'function') return exportFunction(fn, host); } catch (_) {} return fn; } 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 = getScriptHostWindow(); const debugState = { language: currentLanguage, locale: currentLocale, capturedGraphql: 0, extractedTweets: 0, leaderboardItems: 0, settings, hookInstalled: false, receivedMessages: 0, ignoredMessages: 0, lastMessageUrl: '', lastIgnoredReason: '', lastCapturedAt: 0, lastHookError: '', getTweets: () => Array.from(tweetDataStore.values()), }; window.__xvmTampermonkey = debugState; try { debugWindow.__xvmTampermonkey = debugState; } catch (_) {} function captureGraphqlText(url, text, source) { debugState.receivedMessages += 1; debugState.lastMessageUrl = url || ''; if (!GRAPHQL_RE.test(url || '')) { debugState.ignoredMessages += 1; debugState.lastIgnoredReason = 'non-graphql-url'; return; } if (typeof text !== 'string' || !text) { debugState.ignoredMessages += 1; debugState.lastIgnoredReason = 'empty-response-text'; return; } let payload; try { payload = JSON.parse(text); } catch (_) { debugState.ignoredMessages += 1; debugState.lastIgnoredReason = 'invalid-json'; return; } debugState.capturedGraphql += 1; const found = scanForTweets(payload); if (found) { debugState.extractedTweets += found; console.debug('[XVM-TM] GraphQL captured', source, 'tweets:', found); scheduleRender(); } debugState.lastCapturedAt = Date.now(); } 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: parsed.leaderboardEnabled !== false, 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: true, 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; }`; (document.head || document.documentElement).appendChild(style); } function installPageHook() { const pageWindow = getPageWindow(); if (!pageWindow) return; try { if (pageWindow.__xvmTampermonkeyPageHook) return; pageWindow.__xvmTampermonkeyPageHook = true; } catch (_) {} function extractUrl(input) { try { if (typeof input === 'string') return input; if (input && typeof input === 'object') { if (typeof input.url === 'string') return input.url; if (typeof input.href === 'string') return input.href; } } catch (_) {} return ''; } const originalFetch = pageWindow.fetch; if (typeof originalFetch === 'function' && !originalFetch.__xvmTmWrapped) { const wrappedFetch = exportToPage(function (...args) { const url = extractUrl(args[0]); const responsePromise = originalFetch.apply(this, args); if (url && GRAPHQL_RE.test(url)) { Promise.resolve(responsePromise).then((response) => { try { response.clone().text() .then((text) => captureGraphqlText(url, text, 'fetch')) .catch(() => {}); } catch (_) {} }).catch(() => {}); } return responsePromise; }); try { wrappedFetch.__xvmTmWrapped = true; } catch (_) {} pageWindow.fetch = wrappedFetch; } const xhrProto = pageWindow.XMLHttpRequest?.prototype; if (xhrProto && !xhrProto.__xvmTmPatched) { try { xhrProto.__xvmTmPatched = true; } catch (_) {} const xhrOpen = xhrProto.open; xhrProto.open = exportToPage(function (method, url, ...rest) { const urlStr = extractUrl(url); if (urlStr && GRAPHQL_RE.test(urlStr)) { try { this.addEventListener('load', exportToPage(function () { try { captureGraphqlText(urlStr, this.responseText, 'xhr'); } catch (_) {} })); } catch (_) {} } return xhrOpen.call(this, method, url, ...rest); }); } debugState.hookInstalled = true; console.debug('[XVM-TM] page GraphQL hook installed'); } function injectPageHook() { try { installPageHook(); } catch (err) { debugState.lastHookError = err?.message || String(err); console.debug('[XVM-TM] page GraphQL hook install failed', 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]); } } 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 hours = Math.max((Date.now() - created) / 3600000, 0.1); 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 }; } 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) => ``, 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 = `