// ==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
// ==/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 += '<div style="color: #FFD700; font-size: 12px; margin-bottom: 10px;">Showing the last 200 lines. Save transcript to see the full conversation.</div>';
}
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) {
html += '<div style="border-top: 1px solid rgba(255, 255, 255, 0.3); margin: 5px 0;"></div>';
}
html += `<span style="font-size: ${currentFontSize}px; color: #1DA1F2">${displayName}</span> ` +
`<span style="font-size: ${currentFontSize}px; color: #808080">${handle}</span><br>` +
`<span style="font-size: ${currentFontSize}px; color: ${displayName === 'System' ? '#FF4500' : '#FFFFFF'}">${text}</span><br><br>`;
previousSpeaker = { username: displayName, handle };
} else if (showEmojis) {
let { displayName, emoji, count } = group;
if (displayName === 'Unknown' && previousSpeaker.username) {
displayName = previousSpeaker.username;
}
const countText = count > 1 ? ` <span style="font-size: ${currentFontSize}px; color: #FFD700">x${count}</span>` : '';
html += `<span style="font-size: ${currentFontSize}px; color: #FFD700">${displayName}</span> ` +
`<span style="font-size: ${currentFontSize}px; color: #FFFFFF">reacted with ${emoji}${countText}</span><br>`;
previousSpeaker = { username: displayName, handle: group.items[0].handle };
}
});
// Update the DOM once
const captionWrapper = scrollArea.querySelector('#transcript-output');
if (captionWrapper) {
captionWrapper.innerHTML = html;
lastSpeaker = previousSpeaker;
// Maintain scroll position
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'));
}
}
// [Unchanged functions: updateHandQueueContent, init, etc. omitted for brevity]
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();