X Spaces + r

Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, and host/cohost events, and speaker queuing.

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

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