X Spaces + r

Addon for X Spaces with custom emojis, better transcript, and speaker queuing.

当前为 2025-03-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name X Spaces + r
  3. // @namespace Violentmonkey Scripts
  4. // @version 1.76
  5. // @description Addon for X Spaces with custom emojis, better transcript, and speaker queuing.
  6. // @author x.com/blankspeaker and x.com/PrestonHenshawX
  7. // @match https://twitter.com/*
  8. // @match https://x.com/*
  9. // @run-at document-start
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const OrigWebSocket = window.WebSocket;
  17. const OrigXMLHttpRequest = window.XMLHttpRequest;
  18. let myUserId = null;
  19. let captionsData = [];
  20. let emojiReactions = [];
  21. let currentSpaceId = null;
  22. let lastSpaceId = null;
  23. let handRaiseDurations = [];
  24. const activeHandRaises = new Map();
  25. let dynamicUrl = ''; // Store the dynamic URL globally
  26.  
  27. let selectedCustomEmoji = null;
  28.  
  29. const customEmojis = [
  30. '😂', '😲', '😢', '✌️', '💯',
  31. '👏', '✊', '👍', '👎', '👋',
  32. '😍', '😃', '😠', '🤔', '😷',
  33. '🔥', '🎯', '✨', '🥇', '✋',
  34. '🙌', '🙏', '🎶', '🎙', '🙉',
  35. '🪐', '🎨', '🎮', '🏛️', '💸',
  36. '🌲', '🐞', '❤️', '🧡', '💛',
  37. '💚', '💙', '💜', '🖤', '🤎',
  38. '💄', '🏠', '💡', '💢', '💻',
  39. '🖥️', '📺', '🎚️', '🎛️', '📡',
  40. '🔋', '🗒️', '📰', '📌', '💠',
  41. ];
  42.  
  43. const originalEmojis = ['😂', '😲', '😢', '💜', '💯', '👏', '✊', '👍', '👎', '👋'];
  44. const emojiMap = new Map();
  45. customEmojis.forEach((emoji, index) => {
  46. const originalEmoji = originalEmojis[index % originalEmojis.length];
  47. emojiMap.set(emoji, originalEmoji);
  48. });
  49.  
  50. async function fetchReplayUrl(dynUrl) {
  51. if (!dynUrl || !dynUrl.includes('/dynamic_playlist.m3u8?type=live')) {
  52. return 'Invalid Dynamic URL';
  53. }
  54. const masterUrl = dynUrl.replace('/dynamic_playlist.m3u8?type=live', '/master_playlist.m3u8');
  55. try {
  56. const response = await fetch(masterUrl);
  57. const text = await response.text();
  58. const playlistMatch = text.match(/playlist_\d+\.m3u8/);
  59. if (playlistMatch) {
  60. return dynUrl.replace('dynamic_playlist.m3u8', playlistMatch[0]).replace('type=live', 'type=replay');
  61. }
  62. return 'No playlist found';
  63. } catch (error) {
  64. console.error('Direct fetch failed:', error);
  65. const converterUrl = `data:text/html;charset=utf-8,${encodeURIComponent(`
  66. <!DOCTYPE html>
  67. <html>
  68. <body>
  69. <textarea id="input" rows="4" cols="50">${dynUrl}</textarea><br>
  70. <button onclick="convert()">Generate Replay URL</button><br>
  71. <textarea id="result" rows="4" cols="50" readonly></textarea><br>
  72. <button onclick="navigator.clipboard.writeText(document.getElementById('result').value)">Copy</button>
  73. <script>
  74. async function convert() {
  75. const corsProxy = "https://cors.viddastrage.workers.dev/corsproxy/?apiurl=";
  76. const dynUrl = document.getElementById('input').value;
  77. const masterUrl = dynUrl.replace('/dynamic_playlist.m3u8?type=live', '/master_playlist.m3u8');
  78. try {
  79. const response = await fetch(corsProxy + masterUrl);
  80. const text = await response.text();
  81. const playlistMatch = text.match(/playlist_\\d+\\.m3u8/);
  82. if (playlistMatch) {
  83. const replayUrl = dynUrl.replace('dynamic_playlist.m3u8', playlistMatch[0]).replace('type=live', 'type=replay');
  84. document.getElementById('result').value = replayUrl;
  85. } else {
  86. document.getElementById('result').value = 'No playlist found';
  87. }
  88. } catch (e) {
  89. document.getElementById('result').value = 'Error: ' + e.message;
  90. }
  91. }
  92. </script>
  93. </body>
  94. </html>
  95. `)}`;
  96. return converterUrl;
  97. }
  98. }
  99.  
  100. function debounce(func, wait) {
  101. let timeout;
  102. return function (...args) {
  103. clearTimeout(timeout);
  104. timeout = setTimeout(() => func(...args), wait);
  105. };
  106. }
  107.  
  108. function getSpaceIdFromUrl() {
  109. const urlMatch = window.location.pathname.match(/\/i\/spaces\/([^/]+)/);
  110. return urlMatch ? urlMatch[1] : null;
  111. }
  112.  
  113. window.WebSocket = function (url, protocols) {
  114. const ws = new OrigWebSocket(url, protocols);
  115. const originalSend = ws.send;
  116.  
  117. ws.send = function (data) {
  118. if (typeof data === 'string') {
  119. try {
  120. const parsed = JSON.parse(data);
  121. if (parsed.payload && typeof parsed.payload === 'string') {
  122. try {
  123. const payloadParsed = JSON.parse(parsed.payload);
  124. if (payloadParsed.body && selectedCustomEmoji) {
  125. const bodyParsed = JSON.parse(payloadParsed.body);
  126. if (bodyParsed.type === 2) {
  127. bodyParsed.body = selectedCustomEmoji;
  128. payloadParsed.body = JSON.stringify(bodyParsed);
  129. parsed.payload = JSON.stringify(payloadParsed);
  130. data = JSON.stringify(parsed);
  131. if (parsed.sender && parsed.sender.user_id) {
  132. myUserId = parsed.sender.user_id;
  133. }
  134. }
  135. }
  136. } catch (e) {}
  137. }
  138. } catch (e) {}
  139. }
  140. return originalSend.call(this, data);
  141. };
  142.  
  143. let originalOnMessage = null;
  144. ws.onmessage = function (event) {
  145. if (originalOnMessage) originalOnMessage.call(this, event);
  146. try {
  147. const message = JSON.parse(event.data);
  148. if (message.kind !== 1 || !message.payload) return;
  149.  
  150. const payload = JSON.parse(message.payload);
  151. const body = payload.body ? JSON.parse(payload.body) : null;
  152.  
  153. const payloadString = JSON.stringify(payload);
  154. if (payloadString.includes('dynamic_playlist.m3u8?type=live')) {
  155. const urlMatch = payloadString.match(/https:\/\/prod-fastly-[^/]+?\.video\.pscp\.tv\/[^"]+?dynamic_playlist\.m3u8\?type=live/);
  156. if (urlMatch) dynamicUrl = urlMatch[0];
  157. }
  158.  
  159. if (payload.room_id) {
  160. currentSpaceId = payload.room_id;
  161. }
  162.  
  163. const urlSpaceId = getSpaceIdFromUrl();
  164. if (urlSpaceId && payload.room_id !== urlSpaceId) return;
  165.  
  166. const participantIndex = body?.guestParticipantIndex || payload.sender?.participant_index || 'unknown';
  167. const displayName = payload.sender?.display_name || body?.displayName || 'Unknown';
  168. const handle = payload.sender?.username || body?.username || 'Unknown';
  169. const timestamp = message.timestamp / 1e6 || Date.now();
  170.  
  171. if ((body?.emoji === '✋' || (body?.body && body.body.includes('✋'))) && body?.type !== 2) {
  172. handQueue.set(participantIndex, { displayName, timestamp });
  173. activeHandRaises.set(participantIndex, timestamp);
  174. } else if (body?.type === 40 && body?.emoji === '') {
  175. if (handQueue.has(participantIndex) && activeHandRaises.has(participantIndex)) {
  176. const startTime = activeHandRaises.get(participantIndex);
  177. const duration = (timestamp - startTime) / 1000;
  178. const sortedQueue = Array.from(handQueue.entries())
  179. .sort(([, a], [, b]) => a.timestamp - b.timestamp);
  180. if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) {
  181. handRaiseDurations.push(duration);
  182. if (handRaiseDurations.length > 50) {
  183. handRaiseDurations.shift();
  184. }
  185. }
  186. handQueue.delete(participantIndex);
  187. activeHandRaises.delete(participantIndex);
  188. }
  189. } else if (body?.type === 45 && body.body && handQueue.has(participantIndex)) {
  190. const startTime = activeHandRaises.get(participantIndex);
  191. if (startTime) {
  192. const duration = (timestamp - startTime) / 1000;
  193. const sortedQueue = Array.from(handQueue.entries())
  194. .sort(([, a], [, b]) => a.timestamp - b.timestamp);
  195. if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) {
  196. handRaiseDurations.push(duration);
  197. if (handRaiseDurations.length > 50) {
  198. handRaiseDurations.shift();
  199. }
  200. }
  201. handQueue.delete(participantIndex);
  202. activeHandRaises.delete(participantIndex);
  203. }
  204. }
  205.  
  206. if (body?.type === 45 && body.body) {
  207. const caption = {
  208. displayName,
  209. handle: `@${handle}`,
  210. text: body.body,
  211. timestamp,
  212. uniqueId: `${timestamp}-${displayName}-${handle}-${body.body}`
  213. };
  214. const isDuplicate = captionsData.some(c => c.uniqueId === caption.uniqueId);
  215. const lastCaption = captionsData[captionsData.length - 1];
  216. const isDifferentText = !lastCaption || lastCaption.text !== caption.text;
  217. if (!isDuplicate && isDifferentText) {
  218. captionsData.push(caption);
  219. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  220. updateTranscriptPopup();
  221. }
  222. }
  223. }
  224.  
  225. if (body?.type === 2 && body.body) {
  226. const emojiReaction = {
  227. displayName,
  228. handle: `@${handle}`,
  229. emoji: body.body,
  230. timestamp,
  231. uniqueId: `${timestamp}-${displayName}-${body.body}-${Date.now()}`
  232. };
  233. const isDuplicate = emojiReactions.some(e =>
  234. e.uniqueId === emojiReaction.uniqueId ||
  235. (e.displayName === emojiReaction.displayName &&
  236. e.emoji === emojiReaction.emoji &&
  237. Math.abs(e.timestamp - emojiReaction.timestamp) < 50)
  238. );
  239. if (!isDuplicate) {
  240. emojiReactions.push(emojiReaction);
  241. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  242. debouncedUpdateTranscriptPopup();
  243. }
  244. }
  245. }
  246.  
  247. if (transcriptPopup && transcriptPopup.style.display === 'block') debouncedUpdateTranscriptPopup();
  248. } catch (e) {}
  249. };
  250.  
  251. Object.defineProperty(ws, 'onmessage', {
  252. set: function (callback) {
  253. originalOnMessage = callback;
  254. },
  255. get: function () {
  256. return ws.onmessage;
  257. }
  258. });
  259.  
  260. return ws;
  261. };
  262.  
  263. window.XMLHttpRequest = function () {
  264. const xhr = new OrigXMLHttpRequest();
  265. const originalOpen = xhr.open;
  266.  
  267. xhr.open = function (method, url, async, user, password) {
  268. if (typeof url === 'string' && url.includes('dynamic_playlist.m3u8?type=live')) {
  269. dynamicUrl = url;
  270. }
  271. return originalOpen.apply(this, arguments);
  272. };
  273.  
  274. return xhr;
  275. };
  276.  
  277. let transcriptPopup = null;
  278. let transcriptButton = null;
  279. let queueRefreshInterval = null;
  280. const handQueue = new Map();
  281. let lastSpaceState = false;
  282. let lastSpeaker = { username: '', handle: '' };
  283.  
  284. const STORAGE_KEYS = {
  285. LAST_SPACE_ID: 'xSpacesCustomReactions_lastSpaceId',
  286. HAND_DURATIONS: 'xSpacesCustomReactions_handRaiseDurations',
  287. SHOW_EMOJIS: 'xSpacesCustomReactions_showEmojis'
  288. };
  289.  
  290. const debouncedUpdateTranscriptPopup = debounce(updateTranscriptPopup, 2000);
  291.  
  292. function saveSettings() {
  293. localStorage.setItem(STORAGE_KEYS.LAST_SPACE_ID, currentSpaceId || '');
  294. localStorage.setItem(STORAGE_KEYS.HAND_DURATIONS, JSON.stringify(handRaiseDurations));
  295. }
  296.  
  297. function loadSettings() {
  298. lastSpaceId = localStorage.getItem(STORAGE_KEYS.LAST_SPACE_ID) || null;
  299. const savedDurations = localStorage.getItem(STORAGE_KEYS.HAND_DURATIONS);
  300. if (savedDurations) {
  301. handRaiseDurations = JSON.parse(savedDurations);
  302. }
  303. }
  304.  
  305. function hideOriginalEmojiButtons() {
  306. const originalButtons = document.querySelectorAll('.css-175oi2r.r-1awozwy.r-18u37iz.r-9aw3ui.r-1777fci.r-tuq35u > div > button');
  307. originalButtons.forEach(button => {
  308. button.style.display = 'none';
  309. });
  310. }
  311.  
  312. function createEmojiPickerGrid() {
  313. const emojiPicker = document.querySelector('.css-175oi2r.r-1awozwy.r-18u37iz.r-9aw3ui.r-1777fci.r-tuq35u');
  314. if (!emojiPicker) return;
  315.  
  316. if (emojiPicker.querySelector('.emoji-grid-container')) return;
  317.  
  318. hideOriginalEmojiButtons();
  319.  
  320. const gridContainer = document.createElement('div');
  321. gridContainer.className = 'emoji-grid-container';
  322. gridContainer.style.display = 'grid';
  323. gridContainer.style.gridTemplateColumns = 'repeat(5, 1fr)';
  324. gridContainer.style.gap = '10px';
  325. gridContainer.style.padding = '10px';
  326.  
  327. const fragment = document.createDocumentFragment();
  328.  
  329. customEmojis.forEach(emoji => {
  330. const emojiButton = document.createElement('button');
  331. emojiButton.setAttribute('aria-label', `React with ${emoji}`);
  332. emojiButton.setAttribute('role', 'button');
  333. 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';
  334. emojiButton.type = 'button';
  335. emojiButton.style.margin = '5px';
  336.  
  337. const emojiDiv = document.createElement('div');
  338. emojiDiv.dir = 'ltr';
  339. emojiDiv.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-1blvdjr r-vrz42v r-16dba41';
  340. emojiDiv.style.color = 'rgb(231, 233, 234)';
  341.  
  342. const emojiImg = document.createElement('img');
  343. emojiImg.alt = emoji;
  344. emojiImg.draggable = 'false';
  345. emojiImg.src = `https://abs-0.twimg.com/emoji/v2/svg/${emoji.codePointAt(0).toString(16)}.svg`;
  346. emojiImg.title = emoji;
  347. emojiImg.className = 'r-4qtqp9 r-dflpy8 r-k4bwe5 r-1kpi4qh r-pp5qcn r-h9hxbl';
  348.  
  349. emojiDiv.appendChild(emojiImg);
  350. emojiButton.appendChild(emojiDiv);
  351.  
  352. emojiButton.addEventListener('click', (e) => {
  353. e.preventDefault();
  354. e.stopPropagation();
  355.  
  356. selectedCustomEmoji = emoji;
  357.  
  358. const originalEmoji = emojiMap.get(emoji);
  359. if (originalEmoji) {
  360. const originalButton = Array.from(document.querySelectorAll('button[aria-label^="React with"]'))
  361. .find(button => button.querySelector('img')?.alt === originalEmoji);
  362. if (originalButton) {
  363. originalButton.click();
  364. }
  365. }
  366. });
  367.  
  368. fragment.appendChild(emojiButton);
  369. });
  370.  
  371. const linksDiv = document.createElement('div');
  372. linksDiv.style.gridColumn = '1 / -1';
  373. linksDiv.style.textAlign = 'center';
  374. linksDiv.style.fontSize = '12px';
  375. linksDiv.style.color = 'rgba(231, 233, 234, 0.8)';
  376. linksDiv.style.marginTop = '10px';
  377. linksDiv.style.display = 'flex';
  378. linksDiv.style.justifyContent = 'center';
  379. linksDiv.style.gap = '15px';
  380.  
  381. const aboutLink = document.createElement('a');
  382. aboutLink.href = 'https://greasyfork.org/en/scripts/530560-x-spaces';
  383. aboutLink.textContent = 'About';
  384. aboutLink.style.color = 'inherit';
  385. aboutLink.style.textDecoration = 'none';
  386. aboutLink.target = '_blank';
  387. linksDiv.appendChild(aboutLink);
  388.  
  389. const dynamicLink = document.createElement('a');
  390. dynamicLink.href = '#';
  391. dynamicLink.textContent = dynamicUrl ? 'Live' : 'Live (N/A)';
  392. dynamicLink.style.color = 'inherit';
  393. dynamicLink.style.textDecoration = 'none';
  394. dynamicLink.style.cursor = 'pointer';
  395. dynamicLink.addEventListener('click', (e) => {
  396. e.preventDefault();
  397. if (dynamicUrl) {
  398. navigator.clipboard.writeText(dynamicUrl).then(() => {
  399. dynamicLink.textContent = 'Live (Copied!)';
  400. setTimeout(() => {
  401. dynamicLink.textContent = 'Live';
  402. }, 2000);
  403. }).catch(err => {
  404. console.error('Failed to copy:', err);
  405. dynamicLink.textContent = 'Live (Copy Failed)';
  406. setTimeout(() => {
  407. dynamicLink.textContent = 'Live';
  408. }, 2000);
  409. });
  410. }
  411. });
  412. linksDiv.appendChild(dynamicLink);
  413.  
  414. const replayLink = document.createElement('a');
  415. replayLink.href = '#';
  416. replayLink.textContent = 'Replay';
  417. replayLink.style.color = 'inherit';
  418. replayLink.style.textDecoration = 'none';
  419. replayLink.style.cursor = 'pointer';
  420. replayLink.addEventListener('click', async (e) => {
  421. e.preventDefault();
  422. if (!dynamicUrl) {
  423. replayLink.textContent = 'Replay (No Live URL)';
  424. setTimeout(() => {
  425. replayLink.textContent = 'Replay';
  426. }, 2000);
  427. return;
  428. }
  429. replayLink.textContent = 'Generating...';
  430. const newReplayUrl = await fetchReplayUrl(dynamicUrl);
  431. if (newReplayUrl.startsWith('http')) {
  432. navigator.clipboard.writeText(newReplayUrl).then(() => {
  433. replayLink.textContent = 'Replay (Copied!)';
  434. setTimeout(() => {
  435. replayLink.textContent = 'Replay';
  436. }, 2000);
  437. }).catch(err => {
  438. console.error('Failed to copy:', err);
  439. replayLink.textContent = 'Replay (Copy Failed)';
  440. setTimeout(() => {
  441. replayLink.textContent = 'Replay';
  442. }, 2000);
  443. });
  444. } else if (newReplayUrl.startsWith('data:text/html')) {
  445. replayLink.textContent = 'Replay (Open Converter)';
  446. replayLink.href = newReplayUrl;
  447. replayLink.target = '_blank';
  448. setTimeout(() => {
  449. replayLink.textContent = 'Replay';
  450. replayLink.href = '#';
  451. replayLink.target = '';
  452. }, 5000);
  453. } else {
  454. replayLink.textContent = `Replay (${newReplayUrl})`;
  455. setTimeout(() => {
  456. replayLink.textContent = 'Replay';
  457. }, 2000);
  458. }
  459. });
  460. linksDiv.appendChild(replayLink);
  461.  
  462. const updateDynamicLink = () => {
  463. dynamicLink.textContent = dynamicUrl ? 'Live' : 'Live (N/A)';
  464. };
  465. setInterval(updateDynamicLink, 1000);
  466.  
  467. fragment.appendChild(linksDiv);
  468. gridContainer.appendChild(fragment);
  469. emojiPicker.appendChild(gridContainer);
  470. }
  471.  
  472. function detectEndedUI() {
  473. const endedContainer = document.querySelector(
  474. 'div[data-testid="sheetDialog"] div.css-175oi2r.r-18u37iz.r-13qz1uu.r-1wtj0ep'
  475. );
  476. if (endedContainer) {
  477. const hasEndedText = Array.from(endedContainer.querySelectorAll('span')).some(
  478. span => span.textContent.toLowerCase().includes('ended')
  479. );
  480. const hasCloseButton = endedContainer.querySelector('button[aria-label="Close"]');
  481. const hasShareButton = endedContainer.querySelector('button[aria-label="Share"]');
  482. if (hasEndedText && hasCloseButton && hasShareButton) {
  483. return endedContainer;
  484. }
  485. }
  486. return null;
  487. }
  488.  
  489. function addDownloadOptionToShareDropdown(dropdown) {
  490. if (dropdown.querySelector('#download-transcript-share') &&
  491. dropdown.querySelector('#copy-replay-url-share') &&
  492. dropdown.querySelector('#copy-live-url-share')) return;
  493.  
  494. const menuItems = dropdown.querySelectorAll('div[role="menuitem"]');
  495. const itemCount = Array.from(menuItems).filter(item =>
  496. item.id !== 'download-transcript-share' &&
  497. item.id !== 'copy-replay-url-share' &&
  498. item.id !== 'copy-live-url-share').length;
  499.  
  500. if (itemCount !== 4) return;
  501.  
  502. // Add "Download Transcript" option
  503. const downloadItem = document.createElement('div');
  504. downloadItem.id = 'download-transcript-share';
  505. downloadItem.setAttribute('role', 'menuitem');
  506. downloadItem.setAttribute('tabindex', '0');
  507. downloadItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l';
  508. downloadItem.style.transition = 'background-color 0.2s ease';
  509.  
  510. const downloadIconContainer = document.createElement('div');
  511. downloadIconContainer.className = 'css-175oi2r r-1777fci r-faml9v';
  512.  
  513. const downloadIcon = document.createElement('svg');
  514. downloadIcon.viewBox = '0 0 24 24';
  515. downloadIcon.setAttribute('aria-hidden', 'true');
  516. downloadIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx';
  517. downloadIcon.innerHTML = '<g><path d="M19 3H5c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm-2 16H7v-6h10v6zm2-8H5V5h14v6z"/></g>';
  518. downloadIconContainer.appendChild(downloadIcon);
  519.  
  520. const downloadTextContainer = document.createElement('div');
  521. downloadTextContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2';
  522.  
  523. const downloadText = document.createElement('div');
  524. downloadText.dir = 'ltr';
  525. downloadText.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q';
  526. downloadText.style.color = 'rgb(231, 233, 234)';
  527. downloadText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Download Transcript</span>';
  528. downloadTextContainer.appendChild(downloadText);
  529.  
  530. downloadItem.appendChild(downloadIconContainer);
  531. downloadItem.appendChild(downloadTextContainer);
  532.  
  533. const downloadStyle = document.createElement('style');
  534. downloadStyle.textContent = `
  535. #download-transcript-share:hover {
  536. background-color: rgba(231, 233, 234, 0.1);
  537. }
  538. `;
  539. downloadItem.appendChild(downloadStyle);
  540.  
  541. downloadItem.addEventListener('click', async (e) => {
  542. e.preventDefault();
  543. const replayUrl = dynamicUrl ? await fetchReplayUrl(dynamicUrl) : 'No Replay URL Available';
  544. const liveUrl = dynamicUrl || 'No Live URL Available';
  545. const transcriptContent = `Live URL: ${liveUrl}\nReplay URL: ${replayUrl}\n----------------------------------------\n${formatTranscriptForDownload()}`;
  546. const blob = new Blob([transcriptContent], { type: 'text/plain' });
  547. const url = URL.createObjectURL(blob);
  548. const a = document.createElement('a');
  549. a.href = url;
  550. a.download = `transcript_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
  551. document.body.appendChild(a);
  552. a.click();
  553. document.body.removeChild(a);
  554. URL.revokeObjectURL(url);
  555. dropdown.style.display = 'none';
  556. });
  557.  
  558. // Add "Copy Replay URL" option
  559. const replayItem = document.createElement('div');
  560. replayItem.id = 'copy-replay-url-share';
  561. replayItem.setAttribute('role', 'menuitem');
  562. replayItem.setAttribute('tabindex', '0');
  563. replayItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l';
  564. replayItem.style.transition = 'background-color 0.2s ease';
  565.  
  566. const replayIconContainer = document.createElement('div');
  567. replayIconContainer.className = 'css-175oi2r r-1777fci r-faml9v';
  568.  
  569. const replayIcon = document.createElement('svg');
  570. replayIcon.viewBox = '0 0 24 24';
  571. replayIcon.setAttribute('aria-hidden', 'true');
  572. replayIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx';
  573. replayIcon.innerHTML = '<g><path d="M12 3.75c-4.55 0-8.25 3.69-8.25 8.25 0 1.92.66 3.68 1.75 5.08L4.3 19.2l2.16-1.19c1.4 1.09 3.16 1.74 5.04 1.74 4.56 0 8.25-3.69 8.25-8.25S16.56 3.75 12 3.75zm1 11.24h-2v-2h2v2zm0-3.5h-2v-4h2v4z"/></g>';
  574. replayIconContainer.appendChild(replayIcon);
  575.  
  576. const replayTextContainer = document.createElement('div');
  577. replayTextContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2';
  578.  
  579. const replayText = document.createElement('div');
  580. replayText.dir = 'ltr';
  581. replayText.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q';
  582. replayText.style.color = 'rgb(231, 233, 234)';
  583. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  584. replayTextContainer.appendChild(replayText);
  585.  
  586. replayItem.appendChild(replayIconContainer);
  587. replayItem.appendChild(replayTextContainer);
  588.  
  589. const replayStyle = document.createElement('style');
  590. replayStyle.textContent = `
  591. #copy-replay-url-share:hover {
  592. background-color: rgba(231, 233, 234, 0.1);
  593. }
  594. `;
  595. replayItem.appendChild(replayStyle);
  596.  
  597. replayItem.addEventListener('click', async (e) => {
  598. e.preventDefault();
  599. if (!dynamicUrl) {
  600. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">No Live URL</span>';
  601. setTimeout(() => {
  602. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  603. }, 2000);
  604. dropdown.style.display = 'none';
  605. return;
  606. }
  607. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Generating...</span>';
  608. const newReplayUrl = await fetchReplayUrl(dynamicUrl);
  609. if (newReplayUrl.startsWith('http')) {
  610. navigator.clipboard.writeText(newReplayUrl).then(() => {
  611. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copied!</span>';
  612. setTimeout(() => {
  613. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  614. }, 2000);
  615. }).catch(err => {
  616. console.error('Failed to copy:', err);
  617. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Failed</span>';
  618. setTimeout(() => {
  619. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  620. }, 2000);
  621. });
  622. } else if (newReplayUrl.startsWith('data:text/html')) {
  623. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Open Converter</span>';
  624. window.open(newReplayUrl, '_blank');
  625. setTimeout(() => {
  626. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  627. }, 5000);
  628. } else {
  629. replayText.innerHTML = `<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">${newReplayUrl}</span>`;
  630. setTimeout(() => {
  631. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  632. }, 2000);
  633. }
  634. dropdown.style.display = 'none';
  635. });
  636.  
  637. // Add "Copy Live URL" option
  638. const liveItem = document.createElement('div');
  639. liveItem.id = 'copy-live-url-share';
  640. liveItem.setAttribute('role', 'menuitem');
  641. liveItem.setAttribute('tabindex', '0');
  642. liveItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l';
  643. liveItem.style.transition = 'background-color 0.2s ease';
  644.  
  645. const liveIconContainer = document.createElement('div');
  646. liveIconContainer.className = 'css-175oi2r r-1777fci r-faml9v';
  647.  
  648. const liveIcon = document.createElement('svg');
  649. liveIcon.viewBox = '0 0 24 24';
  650. liveIcon.setAttribute('aria-hidden', 'true');
  651. liveIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx';
  652. liveIcon.innerHTML = '<g><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></g>';
  653. liveIconContainer.appendChild(liveIcon);
  654.  
  655. const liveTextContainer = document.createElement('div');
  656. liveTextContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2';
  657.  
  658. const liveText = document.createElement('div');
  659. liveText.dir = 'ltr';
  660. liveText.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q';
  661. liveText.style.color = 'rgb(231, 233, 234)';
  662. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Live URL</span>';
  663. liveTextContainer.appendChild(liveText);
  664.  
  665. liveItem.appendChild(liveIconContainer);
  666. liveItem.appendChild(liveTextContainer);
  667.  
  668. const liveStyle = document.createElement('style');
  669. liveStyle.textContent = `
  670. #copy-live-url-share:hover {
  671. background-color: rgba(231, 233, 234, 0.1);
  672. }
  673. `;
  674. liveItem.appendChild(liveStyle);
  675.  
  676. liveItem.addEventListener('click', (e) => {
  677. e.preventDefault();
  678. if (!dynamicUrl) {
  679. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">No Live URL</span>';
  680. setTimeout(() => {
  681. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Live URL</span>';
  682. }, 2000);
  683. } else {
  684. navigator.clipboard.writeText(dynamicUrl).then(() => {
  685. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copied!</span>';
  686. setTimeout(() => {
  687. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Live URL</span>';
  688. }, 2000);
  689. }).catch(err => {
  690. console.error('Failed to copy:', err);
  691. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Failed</span>';
  692. setTimeout(() => {
  693. liveText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Live URL</span>';
  694. }, 2000);
  695. });
  696. }
  697. dropdown.style.display = 'none';
  698. });
  699.  
  700. const shareViaItem = dropdown.querySelector('div[data-testid="share-by-tweet"]');
  701. if (shareViaItem) {
  702. dropdown.insertBefore(downloadItem, shareViaItem.nextSibling);
  703. dropdown.insertBefore(replayItem, downloadItem.nextSibling);
  704. dropdown.insertBefore(liveItem, replayItem.nextSibling);
  705. } else {
  706. dropdown.appendChild(downloadItem);
  707. dropdown.appendChild(replayItem);
  708. dropdown.appendChild(liveItem);
  709. }
  710. }
  711.  
  712. function updateVisibilityAndPosition() {
  713. 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"]');
  714. 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"]');
  715. const isInSpace = reactionToggle !== null || peopleButton !== null;
  716. 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'));
  717.  
  718. if (isInSpace && !lastSpaceState) {
  719. const urlSpaceId = getSpaceIdFromUrl();
  720. if (urlSpaceId) {
  721. currentSpaceId = urlSpaceId;
  722. if (currentSpaceId !== lastSpaceId) {
  723. handQueue.clear();
  724. activeHandRaises.clear();
  725. captionsData = [];
  726. emojiReactions = [];
  727. lastSpeaker = { username: '', handle: '' };
  728. lastRenderedCaptionCount = 0;
  729. if (transcriptPopup) {
  730. const captionWrapper = transcriptPopup.querySelector('#transcript-output');
  731. if (captionWrapper) captionWrapper.innerHTML = '';
  732. }
  733. } else {
  734. handQueue.clear();
  735. activeHandRaises.clear();
  736. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  737. updateTranscriptPopup();
  738. }
  739. }
  740. lastSpaceId = currentSpaceId;
  741. saveSettings();
  742. }
  743. } else if (!isInSpace && lastSpaceState && !endedScreen) {
  744. currentSpaceId = null;
  745. saveSettings();
  746. activeHandRaises.clear();
  747. }
  748.  
  749. if (isInSpace) {
  750. if (peopleButton) {
  751. const peopleBtn = peopleButton.closest('button');
  752. if (peopleBtn) {
  753. const rect = peopleBtn.getBoundingClientRect();
  754. transcriptButton.style.position = 'fixed';
  755. transcriptButton.style.left = `${rect.left - 46}px`;
  756. transcriptButton.style.top = `${rect.top}px`;
  757. transcriptButton.style.display = 'block';
  758. }
  759. }
  760. if (reactionToggle) {
  761. createEmojiPickerGrid();
  762. }
  763. } else {
  764. transcriptButton.style.display = 'none';
  765. transcriptPopup.style.display = 'none';
  766. if (queueRefreshInterval) {
  767. clearInterval(queueRefreshInterval);
  768. queueRefreshInterval = null;
  769. }
  770. }
  771.  
  772. const endedContainer = detectEndedUI();
  773. if (endedContainer && lastSpaceState) {
  774. currentSpaceId = null;
  775. saveSettings();
  776. activeHandRaises.clear();
  777. transcriptButton.style.display = 'none';
  778. transcriptPopup.style.display = 'none';
  779. if (queueRefreshInterval) {
  780. clearInterval(queueRefreshInterval);
  781. queueRefreshInterval = null;
  782. }
  783. }
  784.  
  785. lastSpaceState = isInSpace;
  786. }
  787.  
  788. function formatTranscriptForDownload() {
  789. let transcriptText = '';
  790. let previousSpeaker = { username: '', handle: '' };
  791. const combinedData = [
  792. ...captionsData.map(item => ({ ...item, type: 'caption' })),
  793. ...emojiReactions.map(item => ({ ...item, type: 'emoji' }))
  794. ].sort((a, b) => a.timestamp - b.timestamp);
  795.  
  796. combinedData.forEach((item, i) => {
  797. let { displayName, handle } = item;
  798. if (displayName === 'Unknown' && previousSpeaker.username) {
  799. displayName = previousSpeaker.username;
  800. handle = previousSpeaker.handle;
  801. }
  802. if (i > 0 && previousSpeaker.username !== displayName && item.type === 'caption') {
  803. transcriptText += '\n----------------------------------------\n';
  804. }
  805. if (item.type === 'caption') {
  806. transcriptText += `${displayName} ${handle}\n${item.text}\n\n`;
  807. } else if (item.type === 'emoji') {
  808. transcriptText += `${displayName} reacted with ${item.emoji}\n`;
  809. }
  810. previousSpeaker = { username: displayName, handle };
  811. });
  812. return transcriptText;
  813. }
  814.  
  815. let lastRenderedCaptionCount = 0;
  816. let isUserScrolledUp = false;
  817. let currentFontSize = 14;
  818. let searchTerm = '';
  819.  
  820. function filterTranscript(captions, emojis, term) {
  821. if (!term) return { captions, emojis };
  822. const filteredCaptions = captions.filter(caption =>
  823. caption.text.toLowerCase().includes(term.toLowerCase()) ||
  824. caption.displayName.toLowerCase().includes(term.toLowerCase()) ||
  825. caption.handle.toLowerCase().includes(term.toLowerCase())
  826. );
  827. const filteredEmojis = emojis.filter(emoji =>
  828. emoji.emoji.toLowerCase().includes(term.toLowerCase()) ||
  829. emoji.displayName.toLowerCase().includes(term.toLowerCase()) ||
  830. emoji.handle.toLowerCase().includes(term.toLowerCase())
  831. );
  832. return { captions: filteredCaptions, emojis: filteredEmojis };
  833. }
  834.  
  835. function updateTranscriptPopup() {
  836. if (!transcriptPopup) return;
  837.  
  838. let queueContainer = transcriptPopup.querySelector('#queue-container');
  839. let searchContainer = transcriptPopup.querySelector('#search-container');
  840. let scrollArea = transcriptPopup.querySelector('#transcript-scrollable');
  841. let saveButton = transcriptPopup.querySelector('.save-button');
  842. let textSizeContainer = transcriptPopup.querySelector('.text-size-container');
  843. let handQueuePopup = transcriptPopup.querySelector('#hand-queue-popup');
  844. let emojiToggleButton = transcriptPopup.querySelector('#emoji-toggle-button');
  845. let currentScrollTop = scrollArea ? scrollArea.scrollTop : 0;
  846. let wasAtBottom = scrollArea ? (scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight < 50) : true;
  847.  
  848. let showEmojis = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) === 'true' ? true : false;
  849.  
  850. if (!queueContainer || !searchContainer || !scrollArea || !saveButton || !textSizeContainer || !emojiToggleButton) {
  851. transcriptPopup.innerHTML = '';
  852.  
  853. queueContainer = document.createElement('div');
  854. queueContainer.id = 'queue-container';
  855. queueContainer.style.marginBottom = '10px';
  856. transcriptPopup.appendChild(queueContainer);
  857.  
  858. searchContainer = document.createElement('div');
  859. searchContainer.id = 'search-container';
  860. searchContainer.style.display = 'none';
  861. searchContainer.style.marginBottom = '5px';
  862.  
  863. const searchInput = document.createElement('input');
  864. searchInput.type = 'text';
  865. searchInput.placeholder = 'Search transcript...';
  866. searchInput.style.width = '87%';
  867. searchInput.style.padding = '5px';
  868. searchInput.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
  869. searchInput.style.border = 'none';
  870. searchInput.style.borderRadius = '5px';
  871. searchInput.style.color = 'white';
  872. searchInput.style.fontSize = '14px';
  873. searchInput.addEventListener('input', (e) => {
  874. searchTerm = e.target.value.trim();
  875. updateTranscriptPopup();
  876. });
  877.  
  878. searchContainer.appendChild(searchInput);
  879. transcriptPopup.appendChild(searchContainer);
  880.  
  881. scrollArea = document.createElement('div');
  882. scrollArea.id = 'transcript-scrollable';
  883. scrollArea.style.flex = '1';
  884. scrollArea.style.overflowY = 'auto';
  885. scrollArea.style.maxHeight = '300px';
  886.  
  887. const captionWrapper = document.createElement('div');
  888. captionWrapper.id = 'transcript-output';
  889. captionWrapper.style.color = '#e7e9ea';
  890. captionWrapper.style.fontFamily = 'Arial, sans-serif';
  891. captionWrapper.style.whiteSpace = 'pre-wrap';
  892. captionWrapper.style.fontSize = `${currentFontSize}px`;
  893. scrollArea.appendChild(captionWrapper);
  894.  
  895. const controlsContainer = document.createElement('div');
  896. controlsContainer.style.display = 'flex';
  897. controlsContainer.style.alignItems = 'center';
  898. controlsContainer.style.justifyContent = 'space-between';
  899. controlsContainer.style.padding = '5px 0';
  900. controlsContainer.style.borderTop = '1px solid rgba(255, 255, 255, 0.3)';
  901.  
  902. saveButton = document.createElement('div');
  903. saveButton.className = 'save-button';
  904. saveButton.textContent = '💾 Save Transcript';
  905. saveButton.style.color = '#1DA1F2';
  906. saveButton.style.fontSize = '14px';
  907. saveButton.style.cursor = 'pointer';
  908. saveButton.addEventListener('click', async () => {
  909. const replayUrl = dynamicUrl ? await fetchReplayUrl(dynamicUrl) : 'No Replay URL Available';
  910. const liveUrl = dynamicUrl || 'No Live URL Available';
  911. const transcriptContent = `Live URL: ${liveUrl}\nReplay URL: ${replayUrl}\n----------------------------------------\n${formatTranscriptForDownload()}`;
  912. const blob = new Blob([transcriptContent], { type: 'text/plain' });
  913. const url = URL.createObjectURL(blob);
  914. const a = document.createElement('a');
  915. a.href = url;
  916. a.download = `transcript_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
  917. document.body.appendChild(a);
  918. a.click();
  919. document.body.removeChild(a);
  920. URL.revokeObjectURL(url);
  921. });
  922. saveButton.addEventListener('mouseover', () => {
  923. saveButton.style.color = '#FF9800';
  924. });
  925. saveButton.addEventListener('mouseout', () => {
  926. saveButton.style.color = '#1DA1F2';
  927. });
  928.  
  929. textSizeContainer = document.createElement('div');
  930. textSizeContainer.className = 'text-size-container';
  931. textSizeContainer.style.display = 'flex';
  932. textSizeContainer.style.alignItems = 'center';
  933.  
  934. emojiToggleButton = document.createElement('span');
  935. emojiToggleButton.id = 'emoji-toggle-button';
  936. emojiToggleButton.style.position = 'relative';
  937. emojiToggleButton.style.fontSize = '14px';
  938. emojiToggleButton.style.cursor = 'pointer';
  939. emojiToggleButton.style.marginRight = '5px';
  940. emojiToggleButton.style.width = '14px';
  941. emojiToggleButton.style.height = '14px';
  942. emojiToggleButton.style.display = 'inline-flex';
  943. emojiToggleButton.style.alignItems = 'center';
  944. emojiToggleButton.style.justifyContent = 'center';
  945. emojiToggleButton.title = 'Toggle Emoji Notifications';
  946. emojiToggleButton.innerHTML = '🙂';
  947.  
  948. const notAllowedOverlay = document.createElement('span');
  949. notAllowedOverlay.style.position = 'absolute';
  950. notAllowedOverlay.style.width = '14px';
  951. notAllowedOverlay.style.height = '14px';
  952. notAllowedOverlay.style.border = '2px solid red';
  953. notAllowedOverlay.style.borderRadius = '50%';
  954. notAllowedOverlay.style.transform = 'rotate(45deg)';
  955. notAllowedOverlay.style.background = 'transparent';
  956. notAllowedOverlay.style.display = showEmojis ? 'none' : 'block';
  957.  
  958. const slash = document.createElement('span');
  959. slash.style.position = 'absolute';
  960. slash.style.width = '2px';
  961. slash.style.height = '18px';
  962. slash.style.background = 'red';
  963. slash.style.transform = 'rotate(-45deg)';
  964. slash.style.top = '-2px';
  965. slash.style.left = '6px';
  966. notAllowedOverlay.appendChild(slash);
  967.  
  968. emojiToggleButton.appendChild(notAllowedOverlay);
  969.  
  970. emojiToggleButton.addEventListener('click', () => {
  971. showEmojis = !showEmojis;
  972. notAllowedOverlay.style.display = showEmojis ? 'none' : 'block';
  973. localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, showEmojis);
  974. updateTranscriptPopup();
  975. });
  976.  
  977. const handEmoji = document.createElement('span');
  978. handEmoji.textContent = '✋';
  979. handEmoji.style.marginRight = '5px';
  980. handEmoji.style.fontSize = '14px';
  981. handEmoji.style.cursor = 'pointer';
  982. handEmoji.title = 'View Speaking Queue';
  983. handEmoji.addEventListener('click', () => {
  984. if (!handQueuePopup) {
  985. handQueuePopup = document.createElement('div');
  986. handQueuePopup.id = 'hand-queue-popup';
  987. handQueuePopup.style.position = 'absolute';
  988. handQueuePopup.style.bottom = '45px';
  989. handQueuePopup.style.right = '0';
  990. handQueuePopup.style.backgroundColor = 'rgba(21, 32, 43, 0.8)';
  991. handQueuePopup.style.borderRadius = '10px';
  992. handQueuePopup.style.padding = '10px';
  993. handQueuePopup.style.zIndex = '10003';
  994. handQueuePopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';
  995. handQueuePopup.style.width = '200px';
  996. handQueuePopup.style.maxHeight = '200px';
  997. handQueuePopup.style.overflowY = 'auto';
  998. handQueuePopup.style.color = 'white';
  999. handQueuePopup.style.fontSize = '14px';
  1000.  
  1001. const closeHandButton = document.createElement('button');
  1002. closeHandButton.textContent = 'X';
  1003. closeHandButton.style.position = 'sticky';
  1004. closeHandButton.style.top = '5px';
  1005. closeHandButton.style.right = '5px';
  1006. closeHandButton.style.float = 'right';
  1007. closeHandButton.style.background = 'none';
  1008. closeHandButton.style.border = 'none';
  1009. closeHandButton.style.color = 'white';
  1010. closeHandButton.style.fontSize = '14px';
  1011. closeHandButton.style.cursor = 'pointer';
  1012. closeHandButton.style.padding = '0';
  1013. closeHandButton.style.width = '20px';
  1014. closeHandButton.style.height = '20px';
  1015. closeHandButton.style.lineHeight = '20px';
  1016. closeHandButton.style.textAlign = 'center';
  1017. closeHandButton.addEventListener('mouseover', () => {
  1018. closeHandButton.style.color = 'red';
  1019. });
  1020. closeHandButton.addEventListener('mouseout', () => {
  1021. closeHandButton.style.color = 'white';
  1022. });
  1023. closeHandButton.addEventListener('click', (e) => {
  1024. e.stopPropagation();
  1025. handQueuePopup.style.display = 'none';
  1026. });
  1027.  
  1028. const queueContent = document.createElement('div');
  1029. queueContent.id = 'hand-queue-content';
  1030. queueContent.style.paddingTop = '10px';
  1031.  
  1032. handQueuePopup.appendChild(closeHandButton);
  1033. handQueuePopup.appendChild(queueContent);
  1034. transcriptPopup.appendChild(handQueuePopup);
  1035. }
  1036.  
  1037. handQueuePopup.style.display = handQueuePopup.style.display === 'block' ? 'none' : 'block';
  1038. if (handQueuePopup.style.display === 'block') {
  1039. updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content'));
  1040. if (queueRefreshInterval) clearInterval(queueRefreshInterval);
  1041. queueRefreshInterval = setInterval(() => updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content')), 1000);
  1042. } else if (queueRefreshInterval) {
  1043. clearInterval(queueRefreshInterval);
  1044. queueRefreshInterval = null;
  1045. }
  1046. });
  1047.  
  1048. const magnifierEmoji = document.createElement('span');
  1049. magnifierEmoji.textContent = '🔍';
  1050. magnifierEmoji.style.marginRight = '5px';
  1051. magnifierEmoji.style.fontSize = '14px';
  1052. magnifierEmoji.style.cursor = 'pointer';
  1053. magnifierEmoji.title = 'Search transcript';
  1054. magnifierEmoji.addEventListener('click', () => {
  1055. searchContainer.style.display = searchContainer.style.display === 'none' ? 'block' : 'none';
  1056. if (searchContainer.style.display === 'block') {
  1057. searchInput.focus();
  1058. } else {
  1059. searchTerm = '';
  1060. searchInput.value = '';
  1061. updateTranscriptPopup();
  1062. }
  1063. });
  1064.  
  1065. const textSizeSlider = document.createElement('input');
  1066. textSizeSlider.type = 'range';
  1067. textSizeSlider.min = '12';
  1068. textSizeSlider.max = '18';
  1069. textSizeSlider.value = currentFontSize;
  1070. textSizeSlider.style.width = '50px';
  1071. textSizeSlider.style.cursor = 'pointer';
  1072. textSizeSlider.title = 'Adjust transcript text size';
  1073. textSizeSlider.addEventListener('input', () => {
  1074. currentFontSize = parseInt(textSizeSlider.value, 10);
  1075. const captionWrapper = transcriptPopup.querySelector('#transcript-output');
  1076. if (captionWrapper) {
  1077. captionWrapper.style.fontSize = `${currentFontSize}px`;
  1078. const allSpans = captionWrapper.querySelectorAll('span');
  1079. allSpans.forEach(span => {
  1080. span.style.fontSize = `${currentFontSize}px`;
  1081. });
  1082. }
  1083. localStorage.setItem('xSpacesCustomReactions_textSize', currentFontSize);
  1084. });
  1085.  
  1086. const savedTextSize = localStorage.getItem('xSpacesCustomReactions_textSize');
  1087. if (savedTextSize) {
  1088. currentFontSize = parseInt(savedTextSize, 10);
  1089. textSizeSlider.value = currentFontSize;
  1090. captionWrapper.style.fontSize = `${currentFontSize}px`;
  1091. }
  1092.  
  1093. textSizeContainer.appendChild(emojiToggleButton);
  1094. textSizeContainer.appendChild(handEmoji);
  1095. textSizeContainer.appendChild(magnifierEmoji);
  1096. textSizeContainer.appendChild(textSizeSlider);
  1097.  
  1098. controlsContainer.appendChild(saveButton);
  1099. controlsContainer.appendChild(textSizeContainer);
  1100.  
  1101. transcriptPopup.appendChild(queueContainer);
  1102. transcriptPopup.appendChild(searchContainer);
  1103. transcriptPopup.appendChild(scrollArea);
  1104. transcriptPopup.appendChild(controlsContainer);
  1105. lastRenderedCaptionCount = 0;
  1106. }
  1107.  
  1108. const { captions: filteredCaptions, emojis: filteredEmojis } = filterTranscript(captionsData, emojiReactions, searchTerm);
  1109. const totalItems = filteredCaptions.length + (showEmojis ? filteredEmojis.length : 0);
  1110.  
  1111. const captionWrapper = scrollArea.querySelector('#transcript-output');
  1112. if (captionWrapper) {
  1113. captionWrapper.innerHTML = '';
  1114. let previousSpeaker = lastSpeaker;
  1115.  
  1116. const combinedData = [
  1117. ...filteredCaptions.map(item => ({ ...item, type: 'caption' })),
  1118. ...(showEmojis ? filteredEmojis.map(item => ({ ...item, type: 'emoji' })) : [])
  1119. ].sort((a, b) => a.timestamp - b.timestamp);
  1120.  
  1121. let emojiGroups = [];
  1122. let currentGroup = null;
  1123.  
  1124. combinedData.forEach((item) => {
  1125. if (item.type === 'caption') {
  1126. if (currentGroup) {
  1127. emojiGroups.push(currentGroup);
  1128. currentGroup = null;
  1129. }
  1130. emojiGroups.push(item);
  1131. } else if (item.type === 'emoji' && showEmojis) {
  1132. if (!currentGroup) {
  1133. currentGroup = { displayName: item.displayName, emoji: item.emoji, count: 1, items: [item] };
  1134. } else if (currentGroup.displayName === item.displayName &&
  1135. currentGroup.emoji === item.emoji &&
  1136. Math.abs(item.timestamp - currentGroup.items[currentGroup.items.length - 1].timestamp) < 50) {
  1137. currentGroup.count++;
  1138. currentGroup.items.push(item);
  1139. } else {
  1140. emojiGroups.push(currentGroup);
  1141. currentGroup = { displayName: item.displayName, emoji: item.emoji, count: 1, items: [item] };
  1142. }
  1143. }
  1144. });
  1145. if (currentGroup) emojiGroups.push(currentGroup);
  1146.  
  1147. emojiGroups.forEach((group, i) => {
  1148. if (group.type === 'caption') {
  1149. let { displayName, handle, text } = group;
  1150. if (displayName === 'Unknown' && previousSpeaker.username) {
  1151. displayName = previousSpeaker.username;
  1152. handle = previousSpeaker.handle;
  1153. }
  1154. if (i > 0 && previousSpeaker.username !== displayName) {
  1155. captionWrapper.insertAdjacentHTML('beforeend', '<div style="border-top: 1px solid rgba(255, 255, 255, 0.3); margin: 5px 0;"></div>');
  1156. }
  1157. captionWrapper.insertAdjacentHTML('beforeend',
  1158. `<span style="font-size: ${currentFontSize}px; color: #1DA1F2">${displayName}</span> ` +
  1159. `<span style="font-size: ${currentFontSize}px; color: #808080">${handle}</span><br>` +
  1160. `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">${text}</span><br><br>`
  1161. );
  1162. previousSpeaker = { username: displayName, handle };
  1163. } else if (showEmojis) {
  1164. let { displayName, emoji, count } = group;
  1165. if (displayName === 'Unknown' && previousSpeaker.username) {
  1166. displayName = previousSpeaker.username;
  1167. }
  1168. const countText = count > 1 ? ` <span style="font-size: ${currentFontSize}px; color: #FFD700">x${count}</span>` : '';
  1169. captionWrapper.insertAdjacentHTML('beforeend',
  1170. `<span style="font-size: ${currentFontSize}px; color: #FFD700">${displayName}</span> ` +
  1171. `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">reacted with ${emoji}${countText}</span><br>`
  1172. );
  1173. previousSpeaker = { username: displayName, handle: group.items[0].handle };
  1174. }
  1175. });
  1176.  
  1177. lastSpeaker = previousSpeaker;
  1178. lastRenderedCaptionCount = totalItems;
  1179. }
  1180.  
  1181. if (wasAtBottom && !searchTerm) {
  1182. scrollArea.scrollTop = scrollArea.scrollHeight;
  1183. } else {
  1184. scrollArea.scrollTop = currentScrollTop;
  1185. }
  1186.  
  1187. scrollArea.onscroll = () => {
  1188. isUserScrolledUp = scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight > 50;
  1189. };
  1190.  
  1191. if (handQueuePopup && handQueuePopup.style.display === 'block') {
  1192. updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content'));
  1193. }
  1194. }
  1195.  
  1196. function updateHandQueueContent(queueContent) {
  1197. if (!queueContent) return;
  1198. queueContent.innerHTML = '<strong>Speaking Queue</strong><br>';
  1199. if (handQueue.size === 0) {
  1200. queueContent.innerHTML += 'No hands raised.<br>';
  1201. } else {
  1202. const now = Date.now();
  1203. const sortedQueue = Array.from(handQueue.entries())
  1204. .sort(([, a], [, b]) => a.timestamp - b.timestamp);
  1205.  
  1206. const queueList = document.createElement('div');
  1207. queueList.style.display = 'flex';
  1208. queueList.style.flexDirection = 'column';
  1209. queueList.style.gap = '8px';
  1210.  
  1211. const numberEmojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟'];
  1212.  
  1213. sortedQueue.forEach(([, { displayName, timestamp }], index) => {
  1214. const timeUp = Math.floor((now - timestamp) / 1000);
  1215. let timeStr;
  1216. if (timeUp >= 3600) {
  1217. const hours = Math.floor(timeUp / 3600);
  1218. const minutes = Math.floor((timeUp % 3600) / 60);
  1219. const seconds = timeUp % 60;
  1220. timeStr = `${hours}h ${minutes}m ${seconds}s`;
  1221. } else {
  1222. const minutes = Math.floor(timeUp / 60);
  1223. const seconds = timeUp % 60;
  1224. timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
  1225. }
  1226.  
  1227. const entry = document.createElement('div');
  1228. entry.style.display = 'flex';
  1229. entry.style.alignItems = 'center';
  1230. entry.style.justifyContent = 'space-between';
  1231.  
  1232. const text = document.createElement('span');
  1233. const positionEmoji = index < 10 ? numberEmojis[index] : '';
  1234. text.textContent = `${positionEmoji} ${displayName}: ${timeStr}`;
  1235.  
  1236. entry.appendChild(text);
  1237. queueList.appendChild(entry);
  1238. });
  1239.  
  1240. queueContent.appendChild(queueList);
  1241. }
  1242.  
  1243. if (handRaiseDurations.length > 0) {
  1244. const averageContainer = document.createElement('div');
  1245. averageContainer.style.color = 'red';
  1246. averageContainer.style.fontSize = '12px';
  1247. averageContainer.style.marginTop = '10px';
  1248. averageContainer.style.textAlign = 'right';
  1249.  
  1250. const averageSeconds = handRaiseDurations.reduce((a, b) => a + b, 0) / handRaiseDurations.length;
  1251. let avgStr;
  1252. if (averageSeconds >= 3600) {
  1253. const hours = Math.floor(averageSeconds / 3600);
  1254. const minutes = Math.floor((averageSeconds % 3600) / 60);
  1255. const seconds = Math.floor(averageSeconds % 60);
  1256. avgStr = `${hours}h ${minutes}m ${seconds}s`;
  1257. } else {
  1258. const minutes = Math.floor(averageSeconds / 60);
  1259. const seconds = Math.floor(averageSeconds % 60);
  1260. avgStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
  1261. }
  1262. averageContainer.textContent = `Average Wait: ${avgStr}`;
  1263.  
  1264. queueContent.appendChild(averageContainer);
  1265. }
  1266. }
  1267.  
  1268. function init() {
  1269. transcriptButton = document.createElement('button');
  1270. transcriptButton.textContent = '📜';
  1271. transcriptButton.style.zIndex = '10001';
  1272. transcriptButton.style.fontSize = '18px';
  1273. transcriptButton.style.padding = '0';
  1274. transcriptButton.style.backgroundColor = 'transparent';
  1275. transcriptButton.style.border = '0.3px solid #40648085';
  1276. transcriptButton.style.borderRadius = '50%';
  1277. transcriptButton.style.width = '36px';
  1278. transcriptButton.style.height = '36px';
  1279. transcriptButton.style.cursor = 'pointer';
  1280. transcriptButton.style.display = 'none';
  1281. transcriptButton.style.lineHeight = '32px';
  1282. transcriptButton.style.textAlign = 'center';
  1283. transcriptButton.style.position = 'fixed';
  1284. transcriptButton.style.color = 'white';
  1285. transcriptButton.style.filter = 'grayscale(100%) brightness(200%)';
  1286. transcriptButton.title = 'Transcript';
  1287.  
  1288. transcriptButton.addEventListener('mouseover', () => {
  1289. transcriptButton.style.backgroundColor = '#595b5b40';
  1290. });
  1291. transcriptButton.addEventListener('mouseout', () => {
  1292. transcriptButton.style.backgroundColor = 'transparent';
  1293. });
  1294.  
  1295. transcriptButton.addEventListener('click', () => {
  1296. const isVisible = transcriptPopup.style.display === 'block';
  1297. transcriptPopup.style.display = isVisible ? 'none' : 'block';
  1298. if (!isVisible) updateTranscriptPopup();
  1299. });
  1300.  
  1301. transcriptPopup = document.createElement('div');
  1302. transcriptPopup.style.position = 'fixed';
  1303. transcriptPopup.style.bottom = '150px';
  1304. transcriptPopup.style.right = '20px';
  1305. transcriptPopup.style.backgroundColor = 'rgba(21, 32, 43, 0.9)';
  1306. transcriptPopup.style.borderRadius = '10px';
  1307. transcriptPopup.style.padding = '10px';
  1308. transcriptPopup.style.zIndex = '10002';
  1309. transcriptPopup.style.maxHeight = '400px';
  1310. transcriptPopup.style.display = 'none';
  1311. transcriptPopup.style.width = '270px';
  1312. transcriptPopup.style.color = 'white';
  1313. transcriptPopup.style.fontSize = '14px';
  1314. transcriptPopup.style.lineHeight = '1.5';
  1315. transcriptPopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';
  1316. transcriptPopup.style.flexDirection = 'column';
  1317.  
  1318. document.body.appendChild(transcriptButton);
  1319. document.body.appendChild(transcriptPopup);
  1320.  
  1321. loadSettings();
  1322.  
  1323. const observer = new MutationObserver((mutationsList) => {
  1324. for (const mutation of mutationsList) {
  1325. if (mutation.type === 'childList') {
  1326. updateVisibilityAndPosition();
  1327.  
  1328. const dropdown = document.querySelector('div[data-testid="Dropdown"]');
  1329. if (dropdown && dropdown.closest('[role="menu"]') && (captionsData.length > 0 || emojiReactions.length > 0)) {
  1330. addDownloadOptionToShareDropdown(dropdown);
  1331. }
  1332.  
  1333. const audioElements = document.querySelectorAll('audio');
  1334. audioElements.forEach(audio => {
  1335. if (audio.src && audio.src.includes('dynamic_playlist.m3u8?type=live')) {
  1336. dynamicUrl = audio.src;
  1337. }
  1338. });
  1339. }
  1340. }
  1341. });
  1342.  
  1343. observer.observe(document.body, { childList: true, subtree: true });
  1344. updateVisibilityAndPosition();
  1345. setInterval(updateVisibilityAndPosition, 2000);
  1346. }
  1347.  
  1348. if (document.readyState === 'loading') {
  1349. document.addEventListener('DOMContentLoaded', init);
  1350. } else {
  1351. init();
  1352. }
  1353. })();