// ==UserScript== // @name X Spaces + r // @namespace Violentmonkey Scripts // @version 1.91 // @description Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, and speaker queuing. // @author x.com/blankspeaker and x.com/PrestonHenshawX // @match https://twitter.com/* // @match https://x.com/* // @run-at document-start // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; // [Previous unchanged code omitted for brevity: WebSocket, XMLHttpRequest, variables, fetchReplayUrl, debounce, getSpaceIdFromUrl, etc.] // [Keeping all other functions unchanged until formatTranscriptForDownload] async function formatTranscriptForDownload() { let transcriptText = '--- Space URLs ---\n'; // Append live URL if (dynamicUrl) { transcriptText += `Live URL: ${dynamicUrl}\n`; } else { transcriptText += 'Live URL: Not available\n'; } // Append replay URL (async fetch) try { const replayUrl = await fetchReplayUrl(dynamicUrl); transcriptText += `Replay URL: ${replayUrl}\n`; } catch (e) { transcriptText += 'Replay URL: Failed to generate\n'; } transcriptText += '-----------------\n\n'; let previousSpeaker = { username: '', handle: '' }; const combinedData = [ ...captionsData.map(item => ({ ...item, type: 'caption' })), ...emojiReactions.map(item => ({ ...item, type: 'emoji' })) ].sort((a, b) => a.timestamp - b.timestamp); combinedData.forEach((item, i) => { let { displayName, handle } = item; if (displayName === 'Unknown' && previousSpeaker.username) { displayName = previousSpeaker.username; handle = previousSpeaker.handle; } if (i > 0 && previousSpeaker.username !== displayName && item.type === 'caption') { transcriptText += '\n----------------------------------------\n'; } if (item.type === 'caption') { transcriptText += `${displayName} ${handle}\n${item.text}\n\n`; } else if (item.type === 'emoji') { transcriptText += `${displayName} reacted with ${item.emoji}\n`; } previousSpeaker = { username: displayName, handle }; }); return transcriptText; } // [Unchanged functions: filterTranscript] function updateTranscriptPopup() { if (!transcriptPopup || transcriptPopup.style.display !== 'block') return; let queueContainer = transcriptPopup.querySelector('#queue-container'); let searchContainer = transcriptPopup.querySelector('#search-container'); let scrollArea = transcriptPopup.querySelector('#transcript-scrollable'); let saveButton = transcriptPopup.querySelector('.save-button'); let textSizeContainer = transcriptPopup.querySelector('.text-size-container'); let systemToggleButton = transcriptPopup.querySelector('#system-toggle-button'); let emojiToggleButton = transcriptPopup.querySelector('#emoji-toggle-button'); let currentScrollTop = scrollArea ? scrollArea.scrollTop : 0; let wasAtBottom = scrollArea ? (scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight < 50) : true; let showEmojis = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false'; let showSystemMessages = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false'; if (!queueContainer || !searchContainer || !scrollArea || !saveButton || !textSizeContainer || !systemToggleButton || !emojiToggleButton) { transcriptPopup.innerHTML = ''; queueContainer = document.createElement('div'); queueContainer.id = 'queue-container'; queueContainer.style.marginBottom = '10px'; transcriptPopup.appendChild(queueContainer); searchContainer = document.createElement('div'); searchContainer.id = 'search-container'; searchContainer.style.display = 'none'; searchContainer.style.marginBottom = '5px'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search transcript...'; searchInput.style.width = '87%'; searchInput.style.padding = '5px'; searchInput.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; searchInput.style.border = 'none'; searchInput.style.borderRadius = '5px'; searchInput.style.color = 'white'; searchInput.style.fontSize = '14px'; searchInput.addEventListener('input', (e) => { searchTerm = e.target.value.trim(); updateTranscriptPopup(); }); searchContainer.appendChild(searchInput); transcriptPopup.appendChild(searchContainer); scrollArea = document.createElement('div'); scrollArea.id = 'transcript-scrollable'; scrollArea.style.flex = '1'; scrollArea.style.overflowY = 'auto'; scrollArea.style.maxHeight = '300px'; const captionWrapper = document.createElement('div'); captionWrapper.id = 'transcript-output'; captionWrapper.style.color = '#e7e9ea'; captionWrapper.style.fontFamily = 'Arial, sans-serif'; captionWrapper.style.whiteSpace = 'pre-wrap'; captionWrapper.style.fontSize = `${currentFontSize}px`; scrollArea.appendChild(captionWrapper); const controlsContainer = document.createElement('div'); controlsContainer.style.display = 'flex'; controlsContainer.style.alignItems = 'center'; controlsContainer.style.justifyContent = 'space-between'; controlsContainer.style.padding = '5px 0'; controlsContainer.style.borderTop = '1px solid rgba(255, 255, 255, 0.3)'; saveButton = document.createElement('div'); saveButton.className = 'save-button'; saveButton.textContent = '💾 Save Transcript'; saveButton.style.color = '#1DA1F2'; saveButton.style.fontSize = '14px'; saveButton.style.cursor = 'pointer'; saveButton.addEventListener('click', async () => { // Updated to async saveButton.textContent = '💾 Saving...'; // Feedback during async operation const transcriptContent = await formatTranscriptForDownload(); const blob = new Blob([transcriptContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `transcript_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); saveButton.textContent = '💾 Save Transcript'; // Reset button text }); saveButton.addEventListener('mouseover', () => saveButton.style.color = '#FF9800'); saveButton.addEventListener('mouseout', () => saveButton.style.color = '#1DA1F2'); textSizeContainer = document.createElement('div'); textSizeContainer.className = 'text-size-container'; textSizeContainer.style.display = 'flex'; textSizeContainer.style.alignItems = 'center'; systemToggleButton = document.createElement('span'); systemToggleButton.id = 'system-toggle-button'; systemToggleButton.style.position = 'relative'; systemToggleButton.style.fontSize = '14px'; systemToggleButton.style.cursor = 'pointer'; systemToggleButton.style.marginRight = '5px'; systemToggleButton.style.width = '14px'; systemToggleButton.style.height = '14px'; systemToggleButton.style.display = 'inline-flex'; systemToggleButton.style.alignItems = 'center'; systemToggleButton.style.justifyContent = 'center'; systemToggleButton.title = 'Toggle System Messages'; systemToggleButton.innerHTML = '📢'; const systemNotAllowedOverlay = document.createElement('span'); systemNotAllowedOverlay.style.position = 'absolute'; systemNotAllowedOverlay.style.width = '14px'; systemNotAllowedOverlay.style.height = '14px'; systemNotAllowedOverlay.style.border = '2px solid red'; systemNotAllowedOverlay.style.borderRadius = '50%'; systemNotAllowedOverlay.style.transform = 'rotate(45deg)'; systemNotAllowedOverlay.style.background = 'transparent'; systemNotAllowedOverlay.style.display = showSystemMessages ? 'none' : 'block'; const systemSlash = document.createElement('span'); systemSlash.style.position = 'absolute'; systemSlash.style.width = '2px'; systemSlash.style.height = '18px'; systemSlash.style.background = 'red'; systemSlash.style.transform = 'rotate(-45deg)'; systemSlash.style.top = '-2px'; systemSlash.style.left = '6px'; systemNotAllowedOverlay.appendChild(systemSlash); systemToggleButton.appendChild(systemNotAllowedOverlay); systemToggleButton.addEventListener('click', () => { showSystemMessages = !showSystemMessages; systemNotAllowedOverlay.style.display = showSystemMessages ? 'none' : 'block'; localStorage.setItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES, showSystemMessages); updateTranscriptPopup(); }); emojiToggleButton = document.createElement('span'); emojiToggleButton.id = 'emoji-toggle-button'; emojiToggleButton.style.position = 'relative'; emojiToggleButton.style.fontSize = '14px'; emojiToggleButton.style.cursor = 'pointer'; emojiToggleButton.style.marginRight = '5px'; emojiToggleButton.style.width = '14px'; emojiToggleButton.style.height = '14px'; emojiToggleButton.style.display = 'inline-flex'; emojiToggleButton.style.alignItems = 'center'; emojiToggleButton.style.justifyContent = 'center'; emojiToggleButton.title = 'Toggle Emoji Capturing'; emojiToggleButton.innerHTML = '🙂'; const emojiNotAllowedOverlay = document.createElement('span'); emojiNotAllowedOverlay.style.position = 'absolute'; emojiNotAllowedOverlay.style.width = '14px'; emojiNotAllowedOverlay.style.height = '14px'; emojiNotAllowedOverlay.style.border = '2px solid red'; emojiNotAllowedOverlay.style.borderRadius = '50%'; emojiNotAllowedOverlay.style.transform = 'rotate(45deg)'; emojiNotAllowedOverlay.style.background = 'transparent'; emojiNotAllowedOverlay.style.display = showEmojis ? 'none' : 'block'; const emojiSlash = document.createElement('span'); emojiSlash.style.position = 'absolute'; emojiSlash.style.width = '2px'; emojiSlash.style.height = '18px'; emojiSlash.style.background = 'red'; emojiSlash.style.transform = 'rotate(-45deg)'; emojiSlash.style.top = '-2px'; emojiSlash.style.left = '6px'; emojiNotAllowedOverlay.appendChild(emojiSlash); emojiToggleButton.appendChild(emojiNotAllowedOverlay); emojiToggleButton.addEventListener('click', () => { showEmojis = !showEmojis; emojiNotAllowedOverlay.style.display = showEmojis ? 'none' : 'block'; localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, showEmojis); updateTranscriptPopup(); }); const magnifierEmoji = document.createElement('span'); magnifierEmoji.textContent = '🔍'; magnifierEmoji.style.marginRight = '5px'; magnifierEmoji.style.fontSize = '14px'; magnifierEmoji.style.cursor = 'pointer'; magnifierEmoji.title = 'Search transcript'; magnifierEmoji.addEventListener('click', () => { searchContainer.style.display = searchContainer.style.display === 'none' ? 'block' : 'none'; if (searchContainer.style.display === 'block') searchInput.focus(); else { searchTerm = ''; searchInput.value = ''; updateTranscriptPopup(); } }); const textSizeSlider = document.createElement('input'); textSizeSlider.type = 'range'; textSizeSlider.min = '12'; textSizeSlider.max = '18'; textSizeSlider.value = currentFontSize; textSizeSlider.style.width = '50px'; textSizeSlider.style.cursor = 'pointer'; textSizeSlider.title = 'Adjust transcript text size'; textSizeSlider.addEventListener('input', () => { currentFontSize = parseInt(textSizeSlider.value, 10); const captionWrapper = transcriptPopup.querySelector('#transcript-output'); if (captionWrapper) captionWrapper.style.fontSize = `${currentFontSize}px`; localStorage.setItem('xSpacesCustomReactions_textSize', currentFontSize); }); const savedTextSize = localStorage.getItem('xSpacesCustomReactions_textSize'); if (savedTextSize) { currentFontSize = parseInt(savedTextSize, 10); textSizeSlider.value = currentFontSize; } textSizeContainer.appendChild(systemToggleButton); textSizeContainer.appendChild(emojiToggleButton); textSizeContainer.appendChild(magnifierEmoji); textSizeContainer.appendChild(textSizeSlider); controlsContainer.appendChild(saveButton); controlsContainer.appendChild(textSizeContainer); transcriptPopup.appendChild(queueContainer); transcriptPopup.appendChild(searchContainer); transcriptPopup.appendChild(scrollArea); transcriptPopup.appendChild(controlsContainer); } const { captions: filteredCaptions, emojis: filteredEmojis } = filterTranscript(captionsData, emojiReactions, searchTerm); const combinedData = [ ...filteredCaptions.map(item => ({ ...item, type: 'caption' })), ...(showEmojis ? filteredEmojis.map(item => ({ ...item, type: 'emoji' })) : []) ].sort((a, b) => a.timestamp - b.timestamp); // Find the previous speaker before the last 200 entries let previousSpeaker = lastSpeaker || { username: '', handle: '' }; if (combinedData.length > 200) { for (let i = combinedData.length - 201; i >= 0; i--) { if (combinedData[i].type === 'caption') { previousSpeaker = { username: combinedData[i].displayName, handle: combinedData[i].handle }; break; } } } // Limit to the last 200 entries const recentData = combinedData.slice(-200); // Group consecutive emojis within the 200 entries let emojiGroups = []; let currentGroup = null; recentData.forEach(item => { if (item.type === 'caption') { if (currentGroup) { emojiGroups.push(currentGroup); currentGroup = null; } emojiGroups.push(item); } else if (item.type === 'emoji' && showEmojis) { if (currentGroup && currentGroup.displayName === item.displayName && currentGroup.emoji === item.emoji && Math.abs(item.timestamp - currentGroup.items[currentGroup.items.length - 1].timestamp) < 50) { currentGroup.count++; currentGroup.items.push(item); } else { if (currentGroup) emojiGroups.push(currentGroup); currentGroup = { displayName: item.displayName, emoji: item.emoji, count: 1, items: [item] }; } } }); if (currentGroup) emojiGroups.push(currentGroup); // Build the HTML string let html = ''; if (combinedData.length > 200) { html += '