// ==UserScript== // @name X Spaces + // @namespace Violentmonkey Scripts // @version 1.75-reverted // @description Addon for X Spaces with custom emojis, better transcript, 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'; const OrigWebSocket = window.WebSocket; let myUserId = null; let captionsData = []; let emojiReactions = []; let currentSpaceId = null; let lastSpaceId = null; let handRaiseDurations = []; const activeHandRaises = new Map(); let selectedCustomEmoji = null; const customEmojis = [ '๐Ÿ˜‚', '๐Ÿ˜ฒ', '๐Ÿ˜ข', 'โœŒ๏ธ', '๐Ÿ’ฏ', '๐Ÿ‘', 'โœŠ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ‘‹', '๐Ÿ˜', '๐Ÿ˜ƒ', '๐Ÿ˜ ', '๐Ÿค”', '๐Ÿ˜ท', '๐Ÿ”ฅ', '๐ŸŽฏ', 'โœจ', '๐Ÿฅ‡', 'โœ‹', '๐Ÿ™Œ', '๐Ÿ™', '๐ŸŽถ', '๐ŸŽ™', '๐Ÿ™‰', '๐Ÿช', '๐ŸŽจ', '๐ŸŽฎ', '๐Ÿ›๏ธ', '๐Ÿ’ธ', '๐ŸŒฒ', '๐Ÿž', 'โค๏ธ', '๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿ’œ', '๐Ÿ–ค', '๐ŸคŽ', '๐Ÿ’„', '๐Ÿ ', '๐Ÿ’ก', '๐Ÿ’ข', '๐Ÿ’ป', '๐Ÿ–ฅ๏ธ', '๐Ÿ“บ', '๐ŸŽš๏ธ', '๐ŸŽ›๏ธ', '๐Ÿ“ก', '๐Ÿ”‹', '๐Ÿ—’๏ธ', '๐Ÿ“ฐ', '๐Ÿ“Œ', '๐Ÿ’ ', ]; const originalEmojis = ['๐Ÿ˜‚', '๐Ÿ˜ฒ', '๐Ÿ˜ข', '๐Ÿ’œ', '๐Ÿ’ฏ', '๐Ÿ‘', 'โœŠ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ‘‹']; const emojiMap = new Map(); customEmojis.forEach((emoji, index) => { const originalEmoji = originalEmojis[index % originalEmojis.length]; emojiMap.set(emoji, originalEmoji); }); function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function getSpaceIdFromUrl() { const urlMatch = window.location.pathname.match(/\/i\/spaces\/([^/]+)/); return urlMatch ? urlMatch[1] : null; } window.WebSocket = function (url, protocols) { const ws = new OrigWebSocket(url, protocols); const originalSend = ws.send; ws.send = function (data) { if (typeof data === 'string') { try { const parsed = JSON.parse(data); if (parsed.payload && typeof parsed.payload === 'string') { try { const payloadParsed = JSON.parse(parsed.payload); if (payloadParsed.body && selectedCustomEmoji) { const bodyParsed = JSON.parse(payloadParsed.body); if (bodyParsed.type === 2) { bodyParsed.body = selectedCustomEmoji; payloadParsed.body = JSON.stringify(bodyParsed); parsed.payload = JSON.stringify(payloadParsed); data = JSON.stringify(parsed); if (parsed.sender && parsed.sender.user_id) { myUserId = parsed.sender.user_id; } } } } catch (e) {} } } catch (e) {} } return originalSend.call(this, data); }; let originalOnMessage = null; ws.onmessage = function (event) { if (originalOnMessage) originalOnMessage.call(this, event); try { const message = JSON.parse(event.data); if (message.kind !== 1 || !message.payload) return; const payload = JSON.parse(message.payload); const body = payload.body ? JSON.parse(payload.body) : null; if (payload.room_id) { currentSpaceId = payload.room_id; } const urlSpaceId = getSpaceIdFromUrl(); if (urlSpaceId && payload.room_id !== urlSpaceId) return; const participantIndex = body?.guestParticipantIndex || payload.sender?.participant_index || 'unknown'; const displayName = payload.sender?.display_name || body?.displayName || 'Unknown'; const handle = payload.sender?.username || body?.username || 'Unknown'; const timestamp = message.timestamp / 1e6 || Date.now(); if ((body?.emoji === 'โœ‹' || (body?.body && body.body.includes('โœ‹'))) && body?.type !== 2) { handQueue.set(participantIndex, { displayName, timestamp }); activeHandRaises.set(participantIndex, timestamp); } else if (body?.type === 40 && body?.emoji === '') { if (handQueue.has(participantIndex) && activeHandRaises.has(participantIndex)) { const startTime = activeHandRaises.get(participantIndex); const duration = (timestamp - startTime) / 1000; const sortedQueue = Array.from(handQueue.entries()) .sort(([, a], [, b]) => a.timestamp - b.timestamp); if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) { handRaiseDurations.push(duration); if (handRaiseDurations.length > 50) { handRaiseDurations.shift(); } } handQueue.delete(participantIndex); activeHandRaises.delete(participantIndex); } } else if (body?.type === 45 && body.body && handQueue.has(participantIndex)) { const startTime = activeHandRaises.get(participantIndex); if (startTime) { const duration = (timestamp - startTime) / 1000; const sortedQueue = Array.from(handQueue.entries()) .sort(([, a], [, b]) => a.timestamp - b.timestamp); if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) { handRaiseDurations.push(duration); if (handRaiseDurations.length > 50) { handRaiseDurations.shift(); } } handQueue.delete(participantIndex); activeHandRaises.delete(participantIndex); } } if (body?.type === 45 && body.body) { const caption = { displayName, handle: `@${handle}`, text: body.body, timestamp, uniqueId: `${timestamp}-${displayName}-${handle}-${body.body}` }; const isDuplicate = captionsData.some(c => c.uniqueId === caption.uniqueId); const lastCaption = captionsData[captionsData.length - 1]; const isDifferentText = !lastCaption || lastCaption.text !== caption.text; if (!isDuplicate && isDifferentText) { captionsData.push(caption); if (transcriptPopup && transcriptPopup.style.display === 'block') { updateTranscriptPopup(); } } } if (body?.type === 2 && body.body) { const emojiReaction = { displayName, handle: `@${handle}`, emoji: body.body, timestamp, uniqueId: `${timestamp}-${displayName}-${body.body}-${Date.now()}` }; const isDuplicate = emojiReactions.some(e => e.uniqueId === emojiReaction.uniqueId || (e.displayName === emojiReaction.displayName && e.emoji === emojiReaction.emoji && Math.abs(e.timestamp - emojiReaction.timestamp) < 50) ); if (!isDuplicate) { emojiReactions.push(emojiReaction); if (transcriptPopup && transcriptPopup.style.display === 'block') { debouncedUpdateTranscriptPopup(); } } } if (transcriptPopup && transcriptPopup.style.display === 'block') debouncedUpdateTranscriptPopup(); } catch (e) {} }; Object.defineProperty(ws, 'onmessage', { set: function (callback) { originalOnMessage = callback; }, get: function () { return ws.onmessage; } }); return ws; }; let transcriptPopup = null; let transcriptButton = null; let queueRefreshInterval = null; const handQueue = new Map(); let lastSpaceState = false; let lastSpeaker = { username: '', handle: '' }; const STORAGE_KEYS = { LAST_SPACE_ID: 'xSpacesCustomReactions_lastSpaceId', HAND_DURATIONS: 'xSpacesCustomReactions_handRaiseDurations', SHOW_EMOJIS: 'xSpacesCustomReactions_showEmojis' }; const debouncedUpdateTranscriptPopup = debounce(updateTranscriptPopup, 2000); function saveSettings() { localStorage.setItem(STORAGE_KEYS.LAST_SPACE_ID, currentSpaceId || ''); localStorage.setItem(STORAGE_KEYS.HAND_DURATIONS, JSON.stringify(handRaiseDurations)); } function loadSettings() { lastSpaceId = localStorage.getItem(STORAGE_KEYS.LAST_SPACE_ID) || null; const savedDurations = localStorage.getItem(STORAGE_KEYS.HAND_DURATIONS); if (savedDurations) { handRaiseDurations = JSON.parse(savedDurations); } } function hideOriginalEmojiButtons() { const originalButtons = document.querySelectorAll('.css-175oi2r.r-1awozwy.r-18u37iz.r-9aw3ui.r-1777fci.r-tuq35u > div > button'); originalButtons.forEach(button => { button.style.display = 'none'; }); } function createEmojiPickerGrid() { const emojiPicker = document.querySelector('.css-175oi2r.r-1awozwy.r-18u37iz.r-9aw3ui.r-1777fci.r-tuq35u'); if (!emojiPicker) return; if (emojiPicker.querySelector('.emoji-grid-container')) return; hideOriginalEmojiButtons(); const gridContainer = document.createElement('div'); gridContainer.className = 'emoji-grid-container'; gridContainer.style.display = 'grid'; gridContainer.style.gridTemplateColumns = 'repeat(5, 1fr)'; gridContainer.style.gap = '10px'; gridContainer.style.padding = '10px'; const fragment = document.createDocumentFragment(); customEmojis.forEach(emoji => { const emojiButton = document.createElement('button'); emojiButton.setAttribute('aria-label', `React with ${emoji}`); emojiButton.setAttribute('role', 'button'); emojiButton.className = 'css-175oi2r r-1awozwy r-z2wwpe r-6koalj r-18u37iz r-1w6e6rj r-a2tzq0 r-tuq35u r-1loqt21 r-o7ynqc r-6416eg r-1ny4l3l'; emojiButton.type = 'button'; emojiButton.style.margin = '5px'; const emojiDiv = document.createElement('div'); emojiDiv.dir = 'ltr'; emojiDiv.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-1blvdjr r-vrz42v r-16dba41'; emojiDiv.style.color = 'rgb(231, 233, 234)'; const emojiImg = document.createElement('img'); emojiImg.alt = emoji; emojiImg.draggable = 'false'; emojiImg.src = `https://abs-0.twimg.com/emoji/v2/svg/${emoji.codePointAt(0).toString(16)}.svg`; emojiImg.title = emoji; emojiImg.className = 'r-4qtqp9 r-dflpy8 r-k4bwe5 r-1kpi4qh r-pp5qcn r-h9hxbl'; emojiDiv.appendChild(emojiImg); emojiButton.appendChild(emojiDiv); emojiButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); selectedCustomEmoji = emoji; const originalEmoji = emojiMap.get(emoji); if (originalEmoji) { const originalButton = Array.from(document.querySelectorAll('button[aria-label^="React with"]')) .find(button => button.querySelector('img')?.alt === originalEmoji); if (originalButton) { originalButton.click(); } } }); fragment.appendChild(emojiButton); }); gridContainer.appendChild(fragment); emojiPicker.appendChild(gridContainer); } function detectEndedUI() { const endedContainer = document.querySelector( 'div[data-testid="sheetDialog"] div.css-175oi2r.r-18u37iz.r-13qz1uu.r-1wtj0ep' ); if (endedContainer) { const hasEndedText = Array.from(endedContainer.querySelectorAll('span')).some( span => span.textContent.toLowerCase().includes('ended') ); const hasCloseButton = endedContainer.querySelector('button[aria-label="Close"]'); const hasShareButton = endedContainer.querySelector('button[aria-label="Share"]'); if (hasEndedText && hasCloseButton && hasShareButton) { return endedContainer; } } return null; } function addDownloadOptionToShareDropdown(dropdown) { if (dropdown.querySelector('#download-transcript-share')) return; const menuItems = dropdown.querySelectorAll('div[role="menuitem"]'); const itemCount = Array.from(menuItems).filter(item => item.id !== 'download-transcript-share').length; if (itemCount !== 4) return; const downloadItem = document.createElement('div'); downloadItem.id = 'download-transcript-share'; downloadItem.setAttribute('role', 'menuitem'); downloadItem.setAttribute('tabindex', '0'); downloadItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l'; downloadItem.style.transition = 'background-color 0.2s ease'; const iconContainer = document.createElement('div'); iconContainer.className = 'css-175oi2r r-1777fci r-faml9v'; const downloadIcon = document.createElement('svg'); downloadIcon.viewBox = '0 0 24 24'; downloadIcon.setAttribute('aria-hidden', 'true'); downloadIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx'; downloadIcon.innerHTML = ''; iconContainer.appendChild(downloadIcon); const textContainer = document.createElement('div'); textContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2'; const text = document.createElement('div'); text.dir = 'ltr'; text.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q'; text.style.color = 'rgb(231, 233, 234)'; text.innerHTML = 'Download Transcript'; textContainer.appendChild(text); downloadItem.appendChild(iconContainer); downloadItem.appendChild(textContainer); const style = document.createElement('style'); style.textContent = ` #download-transcript-share:hover { background-color: rgba(231, 233, 234, 0.1); } `; downloadItem.appendChild(style); downloadItem.addEventListener('click', (e) => { e.preventDefault(); const transcriptContent = 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); dropdown.style.display = 'none'; }); const shareViaItem = dropdown.querySelector('div[data-testid="share-by-tweet"]'); if (shareViaItem) { dropdown.insertBefore(downloadItem, shareViaItem.nextSibling); } else { dropdown.appendChild(downloadItem); } } function updateVisibilityAndPosition() { const reactionToggle = document.querySelector('button svg path[d="M17 12v3h-2.998v2h3v3h2v-3h3v-2h-3.001v-3H17zm-5 6.839c-3.871-2.34-6.053-4.639-7.127-6.609-1.112-2.04-1.031-3.7-.479-4.82.561-1.13 1.667-1.84 2.91-1.91 1.222-.06 2.68.51 3.892 2.16l.806 1.09.805-1.09c1.211-1.65 2.668-2.22 3.89-2.16 1.242.07 2.347.78 2.908 1.91.334.677.49 1.554.321 2.59h2.011c.153-1.283-.039-2.469-.539-3.48-.887-1.79-2.647-2.91-4.601-3.01-1.65-.09-3.367.56-4.796 2.01-1.43-1.45-3.147-2.1-4.798-2.01-1.954.1-3.714 1.22-4.601 3.01-.896 1.81-.846 4.17.514 6.67 1.353 2.48 4.003 5.12 8.382 7.67l.502.299v-2.32z"]'); const peopleButton = document.querySelector('button svg path[d="M6.662 18H.846l.075-1.069C1.33 11.083 4.335 9 7.011 9c1.416 0 2.66.547 3.656 1.53-1.942 1.373-3.513 3.758-4.004 7.47zM7 8c1.657 0 3-1.346 3-3S8.657 2 7 2 4 3.346 4 5s1.343 3 3 3zm10.616 1.27C18.452 8.63 19 7.632 19 6.5 19 4.57 17.433 3 15.5 3S12 4.57 12 6.5c0 1.132.548 2.13 1.384 2.77.589.451 1.317.73 2.116.73s1.527-.279 2.116-.73zM8.501 19.972l-.029 1.027h14.057l-.029-1.027c-.184-6.618-3.736-8.977-7-8.977s-6.816 2.358-7 8.977z"]'); const isInSpace = reactionToggle !== null || peopleButton !== null; const endedScreen = Array.from(document.querySelectorAll('.css-146c3p1.r-bcqeeo.r-1ttztb7.r-qvutc0.r-37j5jr.r-1b43r93.r-b88u0q.r-xnfwke.r-tsynxw span.css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3')).find(span => span.textContent.includes('Ended')); if (isInSpace && !lastSpaceState) { const urlSpaceId = getSpaceIdFromUrl(); if (urlSpaceId) { currentSpaceId = urlSpaceId; if (currentSpaceId !== lastSpaceId) { handQueue.clear(); activeHandRaises.clear(); captionsData = []; emojiReactions = []; lastSpeaker = { username: '', handle: '' }; lastRenderedCaptionCount = 0; if (transcriptPopup) { const captionWrapper = transcriptPopup.querySelector('#transcript-output'); if (captionWrapper) captionWrapper.innerHTML = ''; } } else { handQueue.clear(); activeHandRaises.clear(); if (transcriptPopup && transcriptPopup.style.display === 'block') { updateTranscriptPopup(); } } lastSpaceId = currentSpaceId; saveSettings(); } } else if (!isInSpace && lastSpaceState && !endedScreen) { currentSpaceId = null; saveSettings(); activeHandRaises.clear(); } if (isInSpace) { if (peopleButton) { const peopleBtn = peopleButton.closest('button'); if (peopleBtn) { const rect = peopleBtn.getBoundingClientRect(); transcriptButton.style.position = 'fixed'; transcriptButton.style.left = `${rect.left - 46}px`; // Position to the left of the People button (36px width + 10px spacing) transcriptButton.style.top = `${rect.top}px`; transcriptButton.style.display = 'block'; } } if (reactionToggle) { createEmojiPickerGrid(); } } else { transcriptButton.style.display = 'none'; transcriptPopup.style.display = 'none'; if (queueRefreshInterval) { clearInterval(queueRefreshInterval); queueRefreshInterval = null; } } const endedContainer = detectEndedUI(); if (endedContainer && lastSpaceState) { currentSpaceId = null; saveSettings(); activeHandRaises.clear(); transcriptButton.style.display = 'none'; transcriptPopup.style.display = 'none'; if (queueRefreshInterval) { clearInterval(queueRefreshInterval); queueRefreshInterval = null; } } lastSpaceState = isInSpace; } function formatTranscriptForDownload() { let transcriptText = ''; 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; } let lastRenderedCaptionCount = 0; let isUserScrolledUp = false; let currentFontSize = 14; let searchTerm = ''; function filterTranscript(captions, emojis, term) { if (!term) return { captions, emojis }; const filteredCaptions = captions.filter(caption => caption.text.toLowerCase().includes(term.toLowerCase()) || caption.displayName.toLowerCase().includes(term.toLowerCase()) || caption.handle.toLowerCase().includes(term.toLowerCase()) ); const filteredEmojis = emojis.filter(emoji => emoji.emoji.toLowerCase().includes(term.toLowerCase()) || emoji.displayName.toLowerCase().includes(term.toLowerCase()) || emoji.handle.toLowerCase().includes(term.toLowerCase()) ); return { captions: filteredCaptions, emojis: filteredEmojis }; } function updateTranscriptPopup() { if (!transcriptPopup) 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 handQueuePopup = transcriptPopup.querySelector('#hand-queue-popup'); 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) === 'true' ? true : false; if (!queueContainer || !searchContainer || !scrollArea || !saveButton || !textSizeContainer || !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', () => { const transcriptContent = 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.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'; 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 Notifications'; emojiToggleButton.innerHTML = '๐Ÿ™‚'; const notAllowedOverlay = document.createElement('span'); notAllowedOverlay.style.position = 'absolute'; notAllowedOverlay.style.width = '14px'; notAllowedOverlay.style.height = '14px'; notAllowedOverlay.style.border = '2px solid red'; notAllowedOverlay.style.borderRadius = '50%'; notAllowedOverlay.style.transform = 'rotate(45deg)'; notAllowedOverlay.style.background = 'transparent'; notAllowedOverlay.style.display = showEmojis ? 'none' : 'block'; const slash = document.createElement('span'); slash.style.position = 'absolute'; slash.style.width = '2px'; slash.style.height = '18px'; slash.style.background = 'red'; slash.style.transform = 'rotate(-45deg)'; slash.style.top = '-2px'; slash.style.left = '6px'; notAllowedOverlay.appendChild(slash); emojiToggleButton.appendChild(notAllowedOverlay); emojiToggleButton.addEventListener('click', () => { showEmojis = !showEmojis; notAllowedOverlay.style.display = showEmojis ? 'none' : 'block'; localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, showEmojis); updateTranscriptPopup(); }); const handEmoji = document.createElement('span'); handEmoji.textContent = 'โœ‹'; handEmoji.style.marginRight = '5px'; handEmoji.style.fontSize = '14px'; handEmoji.style.cursor = 'pointer'; handEmoji.title = 'View Speaking Queue'; handEmoji.addEventListener('click', () => { if (!handQueuePopup) { handQueuePopup = document.createElement('div'); handQueuePopup.id = 'hand-queue-popup'; handQueuePopup.style.position = 'absolute'; handQueuePopup.style.bottom = '45px'; handQueuePopup.style.right = '0'; handQueuePopup.style.backgroundColor = 'rgba(21, 32, 43, 0.8)'; handQueuePopup.style.borderRadius = '10px'; handQueuePopup.style.padding = '10px'; handQueuePopup.style.zIndex = '10003'; handQueuePopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)'; handQueuePopup.style.width = '200px'; handQueuePopup.style.maxHeight = '200px'; handQueuePopup.style.overflowY = 'auto'; handQueuePopup.style.color = 'white'; handQueuePopup.style.fontSize = '14px'; const closeHandButton = document.createElement('button'); closeHandButton.textContent = 'X'; closeHandButton.style.position = 'sticky'; closeHandButton.style.top = '5px'; closeHandButton.style.right = '5px'; closeHandButton.style.float = 'right'; closeHandButton.style.background = 'none'; closeHandButton.style.border = 'none'; closeHandButton.style.color = 'white'; closeHandButton.style.fontSize = '14px'; closeHandButton.style.cursor = 'pointer'; closeHandButton.style.padding = '0'; closeHandButton.style.width = '20px'; closeHandButton.style.height = '20px'; closeHandButton.style.lineHeight = '20px'; closeHandButton.style.textAlign = 'center'; closeHandButton.addEventListener('mouseover', () => { closeHandButton.style.color = 'red'; }); closeHandButton.addEventListener('mouseout', () => { closeHandButton.style.color = 'white'; }); closeHandButton.addEventListener('click', (e) => { e.stopPropagation(); handQueuePopup.style.display = 'none'; }); const queueContent = document.createElement('div'); queueContent.id = 'hand-queue-content'; queueContent.style.paddingTop = '10px'; handQueuePopup.appendChild(closeHandButton); handQueuePopup.appendChild(queueContent); transcriptPopup.appendChild(handQueuePopup); } handQueuePopup.style.display = handQueuePopup.style.display === 'block' ? 'none' : 'block'; if (handQueuePopup.style.display === 'block') { updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content')); if (queueRefreshInterval) clearInterval(queueRefreshInterval); queueRefreshInterval = setInterval(() => updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content')), 1000); } else if (queueRefreshInterval) { clearInterval(queueRefreshInterval); queueRefreshInterval = null; } }); 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`; const allSpans = captionWrapper.querySelectorAll('span'); allSpans.forEach(span => { span.style.fontSize = `${currentFontSize}px`; }); } localStorage.setItem('xSpacesCustomReactions_textSize', currentFontSize); }); const savedTextSize = localStorage.getItem('xSpacesCustomReactions_textSize'); if (savedTextSize) { currentFontSize = parseInt(savedTextSize, 10); textSizeSlider.value = currentFontSize; captionWrapper.style.fontSize = `${currentFontSize}px`; } textSizeContainer.appendChild(emojiToggleButton); textSizeContainer.appendChild(handEmoji); textSizeContainer.appendChild(magnifierEmoji); textSizeContainer.appendChild(textSizeSlider); controlsContainer.appendChild(saveButton); controlsContainer.appendChild(textSizeContainer); transcriptPopup.appendChild(queueContainer); transcriptPopup.appendChild(searchContainer); transcriptPopup.appendChild(scrollArea); transcriptPopup.appendChild(controlsContainer); lastRenderedCaptionCount = 0; } const { captions: filteredCaptions, emojis: filteredEmojis } = filterTranscript(captionsData, emojiReactions, searchTerm); const totalItems = filteredCaptions.length + (showEmojis ? filteredEmojis.length : 0); const captionWrapper = scrollArea.querySelector('#transcript-output'); if (captionWrapper) { captionWrapper.innerHTML = ''; let previousSpeaker = lastSpeaker; const combinedData = [ ...filteredCaptions.map(item => ({ ...item, type: 'caption' })), ...(showEmojis ? filteredEmojis.map(item => ({ ...item, type: 'emoji' })) : []) ].sort((a, b) => a.timestamp - b.timestamp); let emojiGroups = []; let currentGroup = null; combinedData.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, emoji: item.emoji, count: 1, items: [item] }; } else if (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 { emojiGroups.push(currentGroup); currentGroup = { displayName: item.displayName, emoji: item.emoji, count: 1, items: [item] }; } } }); if (currentGroup) emojiGroups.push(currentGroup); emojiGroups.forEach((group, i) => { if (group.type === 'caption') { let { displayName, handle, text } = group; if (displayName === 'Unknown' && previousSpeaker.username) { displayName = previousSpeaker.username; handle = previousSpeaker.handle; } if (i > 0 && previousSpeaker.username !== displayName) { captionWrapper.insertAdjacentHTML('beforeend', '
'); } captionWrapper.insertAdjacentHTML('beforeend', `${displayName} ` + `${handle}
` + `${text}

` ); previousSpeaker = { username: displayName, handle }; } else if (showEmojis) { let { displayName, emoji, count } = group; if (displayName === 'Unknown' && previousSpeaker.username) { displayName = previousSpeaker.username; } const countText = count > 1 ? ` x${count}` : ''; captionWrapper.insertAdjacentHTML('beforeend', `${displayName} ` + `reacted with ${emoji}${countText}
` ); previousSpeaker = { username: displayName, handle: group.items[0].handle }; } }); lastSpeaker = previousSpeaker; lastRenderedCaptionCount = totalItems; } if (wasAtBottom && !searchTerm) { scrollArea.scrollTop = scrollArea.scrollHeight; } else { scrollArea.scrollTop = currentScrollTop; } scrollArea.onscroll = () => { isUserScrolledUp = scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight > 50; }; if (handQueuePopup && handQueuePopup.style.display === 'block') { updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content')); } } function updateHandQueueContent(queueContent) { if (!queueContent) return; queueContent.innerHTML = 'Speaking Queue
'; if (handQueue.size === 0) { queueContent.innerHTML += 'No hands raised.
'; } else { const now = Date.now(); const sortedQueue = Array.from(handQueue.entries()) .sort(([, a], [, b]) => a.timestamp - b.timestamp); const queueList = document.createElement('div'); queueList.style.display = 'flex'; queueList.style.flexDirection = 'column'; queueList.style.gap = '8px'; const numberEmojis = ['1๏ธโƒฃ', '2๏ธโƒฃ', '3๏ธโƒฃ', '4๏ธโƒฃ', '5๏ธโƒฃ', '6๏ธโƒฃ', '7๏ธโƒฃ', '8๏ธโƒฃ', '9๏ธโƒฃ', '๐Ÿ”Ÿ']; sortedQueue.forEach(([, { displayName, timestamp }], index) => { const timeUp = Math.floor((now - timestamp) / 1000); let timeStr; if (timeUp >= 3600) { const hours = Math.floor(timeUp / 3600); const minutes = Math.floor((timeUp % 3600) / 60); const seconds = timeUp % 60; timeStr = `${hours}h ${minutes}m ${seconds}s`; } else { const minutes = Math.floor(timeUp / 60); const seconds = timeUp % 60; timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; } const entry = document.createElement('div'); entry.style.display = 'flex'; entry.style.alignItems = 'center'; entry.style.justifyContent = 'space-between'; const text = document.createElement('span'); const positionEmoji = index < 10 ? numberEmojis[index] : ''; text.textContent = `${positionEmoji} ${displayName}: ${timeStr}`; entry.appendChild(text); queueList.appendChild(entry); }); queueContent.appendChild(queueList); } if (handRaiseDurations.length > 0) { const averageContainer = document.createElement('div'); averageContainer.style.color = 'red'; averageContainer.style.fontSize = '12px'; averageContainer.style.marginTop = '10px'; averageContainer.style.textAlign = 'right'; const averageSeconds = handRaiseDurations.reduce((a, b) => a + b, 0) / handRaiseDurations.length; let avgStr; if (averageSeconds >= 3600) { const hours = Math.floor(averageSeconds / 3600); const minutes = Math.floor((averageSeconds % 3600) / 60); const seconds = Math.floor(averageSeconds % 60); avgStr = `${hours}h ${minutes}m ${seconds}s`; } else { const minutes = Math.floor(averageSeconds / 60); const seconds = Math.floor(averageSeconds % 60); avgStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; } averageContainer.textContent = `Average Wait: ${avgStr}`; queueContent.appendChild(averageContainer); } } function init() { transcriptButton = document.createElement('button'); transcriptButton.textContent = '๐Ÿ“œ'; transcriptButton.style.zIndex = '10001'; transcriptButton.style.fontSize = '18px'; transcriptButton.style.padding = '0'; transcriptButton.style.backgroundColor = 'transparent'; transcriptButton.style.border = '0.3px solid #40648085'; transcriptButton.style.borderRadius = '50%'; transcriptButton.style.width = '36px'; transcriptButton.style.height = '36px'; transcriptButton.style.cursor = 'pointer'; transcriptButton.style.display = 'none'; transcriptButton.style.lineHeight = '32px'; transcriptButton.style.textAlign = 'center'; transcriptButton.style.position = 'fixed'; transcriptButton.style.color = 'white'; transcriptButton.style.filter = 'grayscale(100%) brightness(200%)'; transcriptButton.title = 'Transcript'; transcriptButton.addEventListener('mouseover', () => { transcriptButton.style.backgroundColor = '#595b5b40'; }); transcriptButton.addEventListener('mouseout', () => { transcriptButton.style.backgroundColor = 'transparent'; }); transcriptButton.addEventListener('click', () => { const isVisible = transcriptPopup.style.display === 'block'; transcriptPopup.style.display = isVisible ? 'none' : 'block'; if (!isVisible) updateTranscriptPopup(); }); transcriptPopup = document.createElement('div'); transcriptPopup.style.position = 'fixed'; transcriptPopup.style.bottom = '150px'; transcriptPopup.style.right = '20px'; transcriptPopup.style.backgroundColor = 'rgba(21, 32, 43, 0.9)'; transcriptPopup.style.borderRadius = '10px'; transcriptPopup.style.padding = '10px'; transcriptPopup.style.zIndex = '10002'; transcriptPopup.style.maxHeight = '400px'; transcriptPopup.style.display = 'none'; transcriptPopup.style.width = '270px'; transcriptPopup.style.color = 'white'; transcriptPopup.style.fontSize = '14px'; transcriptPopup.style.lineHeight = '1.5'; transcriptPopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)'; transcriptPopup.style.flexDirection = 'column'; document.body.appendChild(transcriptButton); document.body.appendChild(transcriptPopup); loadSettings(); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { updateVisibilityAndPosition(); const dropdown = document.querySelector('div[data-testid="Dropdown"]'); if (dropdown && dropdown.closest('[role="menu"]') && (captionsData.length > 0 || emojiReactions.length > 0)) { addDownloadOptionToShareDropdown(dropdown); } } } }); observer.observe(document.body, { childList: true, subtree: true }); updateVisibilityAndPosition(); setInterval(updateVisibilityAndPosition, 2000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();