X Spaces + r goodfsdfaadsfa

Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, and speaker queuing.

目前为 2025-03-25 提交的版本,查看 最新版本

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