X Spaces +

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

当前为 2025-04-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name X Spaces +
  3. // @namespace Violentmonkey Scripts
  4. // @version 2.03
  5. // @description Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, speaker queuing, and recording toggle.
  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. const handQueue = new Map();
  28. let dynamicUrl = '';
  29. let previousOccupancy = null;
  30. let totalParticipants = 0;
  31. let capturedCookie = null;
  32.  
  33. let selectedCustomEmoji = null;
  34.  
  35. const customEmojis = [
  36. '😂', '😲', '😢', '✌️', '💯',
  37. '👏', '✊', '👍', '👎', '👋',
  38. '😍', '😃', '😠', '🤔', '😷',
  39. '🔥', '🎯', '✨', '🥇', '✋',
  40. '🙌', '🙏', '🎶', '🎙', '🙉',
  41. '🪐', '🎨', '🎮', '🏛️', '💸',
  42. '🌲', '🐞', '❤️', '🧡', '💛',
  43. '💚', '💙', '💜', '🖤', '🤎',
  44. '💄', '🏠', '💡', '💢', '💻',
  45. '🖥️', '📺', '🎚️', '🎛️', '📡',
  46. '🔋', '🗒️', '📰', '📌', '💠',
  47. '🍽️', '🎟️', '🧳', '⌚', '👟',
  48. '💼', '🤐'
  49. ];
  50.  
  51. const originalEmojis = ['😂', '😲', '😢', '💜', '💯', '👏', '✊', '👍', '👎', '👋'];
  52. const emojiMap = new Map();
  53. customEmojis.forEach((emoji, index) => {
  54. const originalEmoji = originalEmojis[index % originalEmojis.length];
  55. emojiMap.set(emoji, originalEmoji);
  56. });
  57.  
  58. async function fetchReplayUrl(dynUrl) {
  59. if (!dynUrl || !dynUrl.includes('/dynamic_playlist.m3u8?type=live')) {
  60. return 'Invalid Dynamic URL';
  61. }
  62. const masterUrl = dynUrl.replace('/dynamic_playlist.m3u8?type=live', '/master_playlist.m3u8');
  63. try {
  64. const response = await fetch(masterUrl);
  65. const text = await response.text();
  66. const playlistMatch = text.match(/playlist_\d+\.m3u8/);
  67. if (playlistMatch) {
  68. return dynUrl.replace('dynamic_playlist.m3u8', playlistMatch[0]).replace('type=live', 'type=replay');
  69. }
  70. return 'No playlist found';
  71. } catch (error) {
  72. const converterUrl = `data:text/html;charset=utf-8,${encodeURIComponent(`
  73. <!DOCTYPE html>
  74. <html>
  75. <body>
  76. <textarea id="input" rows="4" cols="50">${dynUrl}</textarea><br>
  77. <button onclick="convert()">Generate Replay URL</button><br>
  78. <textarea id="result" rows="4" cols="50" readonly></textarea><br>
  79. <button onclick="navigator.clipboard.writeText(document.getElementById('result').value)">Copy</button>
  80. <script>
  81. async function convert() {
  82. const corsProxy = "https://cors.viddastrage.workers.dev/corsproxy/?apiurl=";
  83. const dynUrl = document.getElementById('input').value;
  84. const masterUrl = dynUrl.replace('/dynamic_playlist.m3u8?type=live', '/master_playlist.m3u8');
  85. try {
  86. const response = await fetch(corsProxy + masterUrl);
  87. const text = await response.text();
  88. const playlistMatch = text.match(/playlist_\\d+\\.m3u8/);
  89. if (playlistMatch) {
  90. const replayUrl = dynUrl.replace('dynamic_playlist.m3u8', playlistMatch[0]).replace('type=live', 'type=replay');
  91. document.getElementById('result').value = replayUrl;
  92. } else {
  93. document.getElementById('result').value = 'No playlist found';
  94. }
  95. } catch (e) {
  96. document.getElementById('result').value = 'Error: ' + e.message;
  97. }
  98. }
  99. </script>
  100. </body>
  101. </html>
  102. `)}`;
  103. return converterUrl;
  104. }
  105. }
  106.  
  107. function debounce(func, wait) {
  108. let timeout;
  109. return function (...args) {
  110. clearTimeout(timeout);
  111. timeout = setTimeout(() => func(...args), wait);
  112. };
  113. }
  114.  
  115. function getSpaceIdFromUrl() {
  116. const urlMatch = window.location.pathname.match(/\/i\/spaces\/([^/]+)/);
  117. return urlMatch ? urlMatch[1] : null;
  118. }
  119.  
  120. window.WebSocket = function (url, protocols) {
  121. const ws = new OrigWebSocket(url, protocols);
  122. const originalSend = ws.send;
  123.  
  124. ws.send = function (data) {
  125. if (typeof data === 'string') {
  126. try {
  127. const parsed = JSON.parse(data);
  128. if (parsed.payload && typeof parsed.payload === 'string') {
  129. const payloadParsed = JSON.parse(parsed.payload);
  130. if (payloadParsed.body) {
  131. const bodyParsed = JSON.parse(payloadParsed.body);
  132. if (parsed.sender && parsed.sender.user_id) {
  133. myUserId = myUserId || parsed.sender.user_id;
  134. myParticipantIndex = myParticipantIndex || payloadParsed.participant_index;
  135. myUsername = myUsername || payloadParsed.sender?.username || bodyParsed.username || 'You';
  136. }
  137. if (bodyParsed.type === 2 && selectedCustomEmoji) {
  138. bodyParsed.body = selectedCustomEmoji;
  139. payloadParsed.body = JSON.stringify(bodyParsed);
  140. parsed.payload = JSON.stringify(payloadParsed);
  141. data = JSON.stringify(parsed);
  142. const captureEmojis = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false';
  143. if (captureEmojis) {
  144. const emojiReaction = {
  145. displayName: myUsername || 'You',
  146. handle: `@${myUsername || 'You'}`,
  147. emoji: selectedCustomEmoji,
  148. timestamp: Date.now(),
  149. uniqueId: `${Date.now()}-${myUsername || 'You'}-${selectedCustomEmoji}-${Date.now()}`
  150. };
  151. emojiReactions.push(emojiReaction);
  152. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  153. debouncedUpdateTranscriptPopup();
  154. }
  155. }
  156. }
  157. }
  158. }
  159. } catch (e) {
  160. }
  161. }
  162. return originalSend.call(this, data);
  163. };
  164.  
  165. let originalOnMessage = null;
  166. ws.onmessage = function (event) {
  167. if (originalOnMessage) originalOnMessage.call(this, event);
  168. try {
  169. const message = JSON.parse(event.data);
  170.  
  171. if (message.kind === 1 && message.payload) {
  172. const payload = JSON.parse(message.payload);
  173. const body = payload.body ? JSON.parse(payload.body) : null;
  174.  
  175. if (body) {
  176. const participantIndex = body.guestParticipantIndex || payload.sender?.participant_index || 'unknown';
  177. let displayName = payload.sender?.display_name || body.displayName || 'Unknown';
  178. let handle = payload.sender?.username || body.username || 'Unknown';
  179. const timestamp = message.timestamp / 1e6 || Date.now();
  180.  
  181. const logSystemMessages = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false';
  182.  
  183. if (body.type === 40 && body.guestBroadcastingEvent) {
  184. let eventText = '';
  185. switch (body.guestBroadcastingEvent) {
  186. case 4:
  187. eventText = `${displayName} (${handle}) dropped the mic`;
  188. break;
  189. case 5:
  190. eventText = `${displayName} (${handle}) invited you to grab a mic`;
  191. break;
  192. case 9:
  193. eventText = `${displayName} (${handle}) grabbed a mic`;
  194. break;
  195. case 10:
  196. eventText = `${displayName} (${handle}) had their mic removed by host`;
  197. break;
  198. case 16:
  199. eventText = `${displayName} (${handle}) muted`;
  200. break;
  201. case 17:
  202. eventText = `${displayName} (${handle}) unmuted`;
  203. break;
  204. case 18:
  205. eventText = `${displayName} (${handle}) muted all participants`;
  206. break;
  207. case 19:
  208. eventText = `${displayName} (${handle}) unmuted all participants`;
  209. break;
  210. case 20:
  211. eventText = `${displayName} (${handle}) invited a new cohost`;
  212. break;
  213. case 21:
  214. eventText = `${displayName} (${handle}) removed a cohost`;
  215. break;
  216. case 22:
  217. eventText = `${displayName} (${handle}) became a cohost`;
  218. break;
  219. case 23:
  220. eventText = `${displayName} (${handle}) raised their hand`;
  221. handQueue.set(participantIndex, { displayName, timestamp });
  222. activeHandRaises.set(participantIndex, timestamp);
  223. updateQueueButtonVisibility();
  224. break;
  225. case 24:
  226. eventText = `${displayName} (${handle}) lowered their hand`;
  227. const startTime = activeHandRaises.get(participantIndex);
  228. if (startTime) {
  229. const duration = (timestamp - startTime) / 1000;
  230. const sortedQueue = Array.from(handQueue.entries())
  231. .sort(([, a], [, b]) => a.timestamp - b.timestamp);
  232. if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) {
  233. handRaiseDurations.push(duration);
  234. if (handRaiseDurations.length > 50) handRaiseDurations.shift();
  235. }
  236. handQueue.delete(participantIndex);
  237. activeHandRaises.delete(participantIndex);
  238. updateQueueButtonVisibility();
  239. }
  240. break;
  241. default:
  242. eventText = `${displayName} (${handle}) triggered event ${body.guestBroadcastingEvent}`;
  243. }
  244. if (eventText && logSystemMessages && body.guestBroadcastingEvent !== 23 && body.guestBroadcastingEvent !== 24) {
  245. const systemEvent = {
  246. displayName: 'System',
  247. handle: '',
  248. text: eventText,
  249. timestamp,
  250. uniqueId: `${timestamp}-event-${body.guestBroadcastingEvent}-${handle}`
  251. };
  252. captionsData.push(systemEvent);
  253. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  254. updateTranscriptPopup();
  255. }
  256. }
  257. }
  258.  
  259. if (body.type === 45 && body.body) {
  260. const caption = {
  261. displayName,
  262. handle: `@${handle}`,
  263. text: body.body,
  264. timestamp,
  265. uniqueId: `${timestamp}-${displayName}-${handle}-${body.body}`
  266. };
  267. const isDuplicate = captionsData.some(c => c.uniqueId === caption.uniqueId);
  268. const lastCaption = captionsData[captionsData.length - 1];
  269. const isDifferentText = !lastCaption || lastCaption.text !== caption.text;
  270. if (!isDuplicate && isDifferentText) {
  271. if (activeHandRaises.has(participantIndex) && logSystemMessages) {
  272. const startTime = activeHandRaises.get(participantIndex);
  273. const duration = (timestamp - startTime) / 1000;
  274. const sortedQueue = Array.from(handQueue.entries())
  275. .sort(([, a], [, b]) => a.timestamp - b.timestamp);
  276. if (sortedQueue.length > 0 && sortedQueue[0][0] === participantIndex && duration >= 60) {
  277. handRaiseDurations.push(duration);
  278. if (handRaiseDurations.length > 50) handRaiseDurations.shift();
  279. }
  280. const handLowerEvent = {
  281. displayName: 'System',
  282. handle: '',
  283. text: `${displayName} (${handle}) lowered their hand (started speaking)`,
  284. timestamp,
  285. uniqueId: `${timestamp}-handlower-speaking-${participantIndex}`
  286. };
  287. captionsData.push(handLowerEvent);
  288. handQueue.delete(participantIndex);
  289. activeHandRaises.delete(participantIndex);
  290. updateQueueButtonVisibility();
  291. }
  292. captionsData.push(caption);
  293. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  294. updateTranscriptPopup();
  295. }
  296. }
  297. }
  298.  
  299. if (body.type === 2 && body.body) {
  300. const captureEmojis = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false';
  301. if (captureEmojis) {
  302. const emojiReaction = {
  303. displayName,
  304. handle: `@${handle}`,
  305. emoji: body.body,
  306. timestamp,
  307. uniqueId: `${timestamp}-${displayName}-${body.body}-${Date.now()}`
  308. };
  309. const isDuplicate = emojiReactions.some(e =>
  310. e.uniqueId === emojiReaction.uniqueId ||
  311. (e.displayName === emojiReaction.displayName &&
  312. e.emoji === emojiReaction.emoji &&
  313. Math.abs(e.timestamp - emojiReaction.timestamp) < 50)
  314. );
  315. if (!isDuplicate) {
  316. emojiReactions.push(emojiReaction);
  317. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  318. debouncedUpdateTranscriptPopup();
  319. }
  320. }
  321. }
  322. }
  323. }
  324. }
  325.  
  326. if (message.kind === 2 && message.payload) {
  327. const payload = JSON.parse(message.payload);
  328. const body = payload.body ? JSON.parse(payload.body) : null;
  329.  
  330. if (body && body.occupancy !== undefined && body.total_participants !== undefined) {
  331. const currentOccupancy = body.occupancy;
  332. totalParticipants = body.total_participants;
  333. const timestamp = Date.now();
  334. const logSystemMessages = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false';
  335.  
  336. if (previousOccupancy !== null && logSystemMessages) {
  337. let eventText;
  338. if (currentOccupancy > previousOccupancy) {
  339. eventText = `A new user joined - Current ${currentOccupancy} - Total ${totalParticipants}`;
  340. } else if (currentOccupancy < previousOccupancy) {
  341. eventText = `A user left - Current ${currentOccupancy} - Total ${totalParticipants}`;
  342. }
  343. if (eventText) {
  344. const occupancyEvent = {
  345. displayName: 'System',
  346. handle: '',
  347. text: eventText,
  348. timestamp,
  349. uniqueId: `${timestamp}-occupancy-${currentOccupancy}`
  350. };
  351. captionsData.push(occupancyEvent);
  352. if (transcriptPopup && transcriptPopup.style.display === 'block') {
  353. updateTranscriptPopup();
  354. }
  355. }
  356. }
  357. previousOccupancy = currentOccupancy;
  358. }
  359. }
  360.  
  361. const payloadString = JSON.stringify(payload);
  362. if (payloadString.includes('dynamic_playlist.m3u8?type=live')) {
  363. const urlMatch = payloadString.match(/https:\/\/prod-fastly-[^/]+?\.video\.pscp\.tv\/[^"]+?dynamic_playlist\.m3u8\?type=live/);
  364. if (urlMatch) dynamicUrl = urlMatch[0];
  365. }
  366.  
  367. if (payload.room_id) {
  368. currentSpaceId = payload.room_id;
  369. }
  370.  
  371. const urlSpaceId = getSpaceIdFromUrl();
  372. if (urlSpaceId && payload.room_id !== urlSpaceId) return;
  373. } catch (e) {
  374. }
  375. };
  376.  
  377. Object.defineProperty(ws, 'onmessage', {
  378. set: function (callback) {
  379. originalOnMessage = callback;
  380. },
  381. get: function () {
  382. return ws.onmessage;
  383. }
  384. });
  385.  
  386. return ws;
  387. };
  388.  
  389. window.XMLHttpRequest = function () {
  390. const xhr = new OrigXMLHttpRequest();
  391. const originalOpen = xhr.open;
  392. const originalSend = xhr.send;
  393.  
  394. xhr.open = function (method, url, async, user, password) {
  395. if (typeof url === 'string' && url.includes('dynamic_playlist.m3u8?type=live')) {
  396. dynamicUrl = url;
  397. }
  398. xhr._method = method;
  399. xhr._url = url;
  400. return originalOpen.apply(this, arguments);
  401. };
  402.  
  403. xhr.send = function (data) {
  404. if (xhr._method === 'POST') {
  405. try {
  406. let payload = null;
  407. if (data && typeof data === 'string') {
  408. if (data.startsWith('debug=') || data.startsWith('sub_topics') || data.startsWith('category=')) {
  409. payload = data;
  410. } else {
  411. payload = JSON.parse(data);
  412. }
  413. }
  414.  
  415. if (payload) {
  416. const cookieEndpoints = [
  417. 'https://proxsee.pscp.tv/api/v2/createBroadcast',
  418. 'https://proxsee.pscp.tv/api/v2/accessChat',
  419. 'https://proxsee.pscp.tv/api/v2/startWatching',
  420. 'https://proxsee.pscp.tv/api/v2/turnServers',
  421. 'https://proxsee.pscp.tv/api/v2/pingWatching',
  422. 'https://guest.pscp.tv/api/v1/audiospace/join'
  423. ];
  424.  
  425. if (cookieEndpoints.some(endpoint => xhr._url.startsWith(endpoint)) && payload.cookie) {
  426. capturedCookie = payload.cookie;
  427. }
  428.  
  429. if (payload.broadcast_id &&
  430. (xhr._url.includes('https://proxsee.pscp.tv/api/v2/') ||
  431. xhr._url.includes('https://guest.pscp.tv/api/v1/audiospace/'))) {
  432. currentSpaceId = payload.broadcast_id;
  433. }
  434. }
  435. } catch (e) {
  436. }
  437. }
  438. return originalSend.apply(this, arguments);
  439. };
  440.  
  441. return xhr;
  442. };
  443.  
  444. const OriginalFetch = window.fetch;
  445. window.fetch = function (resource, init = {}) {
  446. const url = typeof resource === 'string' ? resource : resource.url;
  447. const method = init.method || 'GET';
  448.  
  449. if (method === 'POST' && init.body) {
  450. try {
  451. let payload = null;
  452. if (typeof init.body === 'string') {
  453. if (init.body.startsWith('debug=') || init.body.startsWith('sub_topics') || init.body.startsWith('category=')) {
  454. payload = init.body;
  455. } else {
  456. payload = JSON.parse(init.body);
  457. }
  458. }
  459.  
  460. if (payload) {
  461. const cookieEndpoints = [
  462. 'https://proxsee.pscp.tv/api/v2/createBroadcast',
  463. 'https://proxsee.pscp.tv/api/v2/accessChat',
  464. 'https://proxsee.pscp.tv/api/v2/startWatching',
  465. 'https://proxsee.pscp.tv/api/v2/turnServers',
  466. 'https://proxsee.pscp.tv/api/v2/pingWatching',
  467. 'https://guest.pscp.tv/api/v1/audiospace/join'
  468. ];
  469.  
  470. if (cookieEndpoints.some(endpoint => url.startsWith(endpoint)) && payload.cookie) {
  471. capturedCookie = payload.cookie;
  472. }
  473.  
  474. if (payload.broadcast_id &&
  475. (url.includes('https://proxsee.pscp.tv/api/v2/') ||
  476. url.includes('https://guest.pscp.tv/api/v1/audiospace/'))) {
  477. currentSpaceId = payload.broadcast_id;
  478. }
  479. }
  480. } catch (e) {
  481. }
  482. }
  483. return OriginalFetch.apply(this, arguments);
  484. };
  485.  
  486. let transcriptPopup = null;
  487. let transcriptButton = null;
  488. let queueButton = null;
  489. let handQueuePopup = null;
  490. let queueRefreshInterval = null;
  491. let lastSpaceState = false;
  492. let lastSpeaker = { username: '', handle: '' };
  493.  
  494. const STORAGE_KEYS = {
  495. LAST_SPACE_ID: 'xSpacesCustomReactions_lastSpaceId',
  496. HAND_DURATIONS: 'xSpacesCustomReactions_handRaiseDurations',
  497. SHOW_EMOJIS: 'xSpacesCustomReactions_showEmojis',
  498. SHOW_SYSTEM_MESSAGES: 'xSpacesCustomReactions_showSystemMessages',
  499. REPLAY_ENABLED: 'xSpacesCustomReactions_replayEnabled'
  500. };
  501.  
  502. const debouncedUpdateTranscriptPopup = debounce(updateTranscriptPopup, 2000);
  503.  
  504. function saveSettings() {
  505. localStorage.setItem(STORAGE_KEYS.LAST_SPACE_ID, currentSpaceId || '');
  506. localStorage.setItem(STORAGE_KEYS.HAND_DURATIONS, JSON.stringify(handRaiseDurations));
  507. }
  508.  
  509. function loadSettings() {
  510. lastSpaceId = localStorage.getItem(STORAGE_KEYS.LAST_SPACE_ID) || null;
  511. const savedDurations = localStorage.getItem(STORAGE_KEYS.HAND_DURATIONS);
  512. if (savedDurations) handRaiseDurations = JSON.parse(savedDurations);
  513. if (localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) === null) {
  514. localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, 'false');
  515. }
  516. if (localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) === null) {
  517. localStorage.setItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES, 'false');
  518. }
  519. }
  520.  
  521. function hideOriginalEmojiButtons() {
  522. const originalButtons = document.querySelectorAll('div[class*="r-1awozwy"][class*="r-18u37iz"][class*="r-9aw3ui"] > div > button');
  523. originalButtons.forEach(button => button.style.display = 'none');
  524. }
  525.  
  526. function createEmojiPickerGrid() {
  527. const emojiPicker = document.querySelector('div[class*="r-1awozwy"][class*="r-18u37iz"][class*="r-9aw3ui"]');
  528. if (!emojiPicker || emojiPicker.querySelector('.emoji-grid-container')) return;
  529.  
  530. hideOriginalEmojiButtons();
  531.  
  532. const gridContainer = document.createElement('div');
  533. gridContainer.className = 'emoji-grid-container';
  534. gridContainer.style.display = 'grid';
  535. gridContainer.style.gridTemplateColumns = 'repeat(5, 1fr)';
  536. gridContainer.style.gap = '10px';
  537. gridContainer.style.padding = '10px';
  538.  
  539. const fragment = document.createDocumentFragment();
  540.  
  541. customEmojis.forEach(emoji => {
  542. const emojiButton = document.createElement('button');
  543. emojiButton.setAttribute('aria-label', `React with ${emoji}`);
  544. emojiButton.setAttribute('role', 'button');
  545. 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';
  546. emojiButton.type = 'button';
  547. emojiButton.style.margin = '5px';
  548.  
  549. const emojiDiv = document.createElement('div');
  550. emojiDiv.dir = 'ltr';
  551. emojiDiv.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-1blvdjr r-vrz42v r-16dba41';
  552. emojiDiv.style.color = 'rgb(231, 233, 234)';
  553.  
  554. const emojiImg = document.createElement('img');
  555. emojiImg.alt = emoji;
  556. emojiImg.draggable = 'false';
  557. emojiImg.src = `https://abs-0.twimg.com/emoji/v2/svg/${emoji.codePointAt(0).toString(16)}.svg`;
  558. emojiImg.title = emoji;
  559. emojiImg.className = 'r-4qtqp9 r-dflpy8 r-k4bwe5 r-1kpi4qh r-pp5qcn r-h9hxbl';
  560.  
  561. emojiDiv.appendChild(emojiImg);
  562. emojiButton.appendChild(emojiDiv);
  563.  
  564. emojiButton.addEventListener('click', (e) => {
  565. e.preventDefault();
  566. e.stopPropagation();
  567.  
  568. selectedCustomEmoji = emoji;
  569.  
  570. const originalEmoji = emojiMap.get(emoji);
  571. if (originalEmoji) {
  572. const originalButton = Array.from(document.querySelectorAll('button[aria-label^="React with"]'))
  573. .find(button => button.querySelector('img')?.alt === originalEmoji);
  574. if (originalButton) originalButton.click();
  575. }
  576. });
  577.  
  578. fragment.appendChild(emojiButton);
  579. });
  580.  
  581. const linksDiv = document.createElement('div');
  582. linksDiv.style.gridColumn = '1 / -1';
  583. linksDiv.style.textAlign = 'center';
  584. linksDiv.style.fontSize = '12px';
  585. linksDiv.style.color = 'rgba(231, 233, 234, 0.8)';
  586. linksDiv.style.marginTop = '10px';
  587. linksDiv.style.display = 'flex';
  588. linksDiv.style.justifyContent = 'center';
  589. linksDiv.style.gap = '15px';
  590.  
  591. const aboutLink = document.createElement('a');
  592. aboutLink.href = 'https://greasyfork.org/en/scripts/530560-x-spaces';
  593. aboutLink.textContent = 'About';
  594. aboutLink.style.color = 'inherit';
  595. aboutLink.style.textDecoration = 'none';
  596. aboutLink.target = '_blank';
  597. linksDiv.appendChild(aboutLink);
  598.  
  599. const dynamicLink = document.createElement('a');
  600. dynamicLink.href = '#';
  601. dynamicLink.textContent = dynamicUrl ? 'Dynamic (Click to Copy)' : 'Dynamic (N/A)';
  602. dynamicLink.style.color = 'inherit';
  603. dynamicLink.style.textDecoration = 'none';
  604. dynamicLink.style.cursor = 'pointer';
  605. dynamicLink.addEventListener('click', (e) => {
  606. e.preventDefault();
  607. if (dynamicUrl) {
  608. navigator.clipboard.writeText(dynamicUrl).then(() => {
  609. dynamicLink.textContent = 'Dynamic (Copied!)';
  610. setTimeout(() => dynamicLink.textContent = 'Dynamic (Click to Copy)', 2000);
  611. }).catch(() => {
  612. dynamicLink.textContent = 'Dynamic (Copy Failed)';
  613. setTimeout(() => dynamicLink.textContent = 'Dynamic (Click to Copy)', 2000);
  614. });
  615. }
  616. });
  617. linksDiv.appendChild(dynamicLink);
  618.  
  619. const replayLink = document.createElement('a');
  620. replayLink.href = '#';
  621. replayLink.textContent = 'Replay (Click to Copy)';
  622. replayLink.style.color = 'inherit';
  623. replayLink.style.textDecoration = 'none';
  624. replayLink.style.cursor = 'pointer';
  625. replayLink.addEventListener('click', async (e) => {
  626. e.preventDefault();
  627. if (!dynamicUrl) {
  628. replayLink.textContent = 'Replay (No Dynamic URL)';
  629. setTimeout(() => replayLink.textContent = 'Replay (Click to Copy)', 2000);
  630. return;
  631. }
  632. replayLink.textContent = 'Generating...';
  633. const newReplayUrl = await fetchReplayUrl(dynamicUrl);
  634. if (newReplayUrl.startsWith('http')) {
  635. navigator.clipboard.writeText(newReplayUrl).then(() => {
  636. replayLink.textContent = 'Replay (Copied!)';
  637. setTimeout(() => replayLink.textContent = 'Replay (Click to Copy)', 2000);
  638. }).catch(() => {
  639. replayLink.textContent = 'Replay (Copy Failed)';
  640. setTimeout(() => replayLink.textContent = 'Replay (Click to Copy)', 2000);
  641. });
  642. } else if (newReplayUrl.startsWith('data:text/html')) {
  643. replayLink.textContent = 'Replay (Open Converter)';
  644. replayLink.href = newReplayUrl;
  645. replayLink.target = '_blank';
  646. setTimeout(() => {
  647. replayLink.textContent = 'Replay (Click to Copy)';
  648. replayLink.href = '#';
  649. replayLink.target = '';
  650. }, 5000);
  651. } else {
  652. replayLink.textContent = `Replay (${newReplayUrl})`;
  653. setTimeout(() => replayLink.textContent = 'Replay (Click to Copy)', 2000);
  654. }
  655. });
  656. linksDiv.appendChild(replayLink);
  657.  
  658. const updateDynamicLink = () => {
  659. dynamicLink.textContent = dynamicUrl ? 'Dynamic (Click to Copy)' : 'Dynamic (N/A)';
  660. };
  661. setInterval(updateDynamicLink, 1000);
  662.  
  663. fragment.appendChild(linksDiv);
  664. gridContainer.appendChild(fragment);
  665. emojiPicker.appendChild(gridContainer);
  666. }
  667.  
  668. function detectEndedUI() {
  669. const endedContainer = document.querySelector('div[data-testid="sheetDialog"] div[class*="r-18u37iz"][class*="r-13qz1uu"]');
  670. if (endedContainer) {
  671. const hasEndedText = Array.from(endedContainer.querySelectorAll('span')).some(span => span.textContent.toLowerCase().includes('ended'));
  672. const hasCloseButton = endedContainer.querySelector('button[aria-label="Close"]');
  673. const hasShareButton = endedContainer.querySelector('button[aria-label="Share"]');
  674. if (hasEndedText && hasCloseButton && hasShareButton) return endedContainer;
  675. }
  676. return null;
  677. }
  678.  
  679. function addDownloadOptionToShareDropdown(dropdown) {
  680. if (dropdown.querySelector('#download-transcript-share') && dropdown.querySelector('#copy-replay-url-share')) return;
  681.  
  682. const menuItems = dropdown.querySelectorAll('div[role="menuitem"]');
  683. const itemCount = Array.from(menuItems).filter(item => item.id !== 'download-transcript-share' && item.id !== 'copy-replay-url-share').length;
  684. if (itemCount !== 4) return;
  685.  
  686. const downloadItem = document.createElement('div');
  687. downloadItem.id = 'download-transcript-share';
  688. downloadItem.setAttribute('role', 'menuitem');
  689. downloadItem.setAttribute('tabindex', '0');
  690. downloadItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l';
  691. downloadItem.style.transition = 'background-color 0.2s ease';
  692.  
  693. const downloadIconContainer = document.createElement('div');
  694. downloadIconContainer.className = 'css-175oi2r r-1777fci r-faml9v';
  695.  
  696. const downloadIcon = document.createElement('svg');
  697. downloadIcon.viewBox = '0 0 24 24';
  698. downloadIcon.setAttribute('aria-hidden', 'true');
  699. downloadIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx';
  700. 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>';
  701. downloadIconContainer.appendChild(downloadIcon);
  702.  
  703. const downloadTextContainer = document.createElement('div');
  704. downloadTextContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2';
  705.  
  706. const downloadText = document.createElement('div');
  707. downloadText.dir = 'ltr';
  708. downloadText.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q';
  709. downloadText.style.color = 'rgb(231, 233, 234)';
  710. downloadText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Download Transcript</span>';
  711. downloadTextContainer.appendChild(downloadText);
  712.  
  713. downloadItem.appendChild(downloadIconContainer);
  714. downloadItem.appendChild(downloadTextContainer);
  715.  
  716. const downloadStyle = document.createElement('style');
  717. downloadStyle.textContent = '#download-transcript-share:hover { background-color: rgba(231, 233, 234, 0.1); }';
  718. downloadItem.appendChild(downloadStyle);
  719.  
  720. downloadItem.addEventListener('click', async (e) => {
  721. e.preventDefault();
  722. const transcripts = await formatTranscriptForDownload();
  723.  
  724. const transcriptionBlob = new Blob([transcripts.transcription.content], { type: 'text/plain' });
  725. const transcriptionUrl = URL.createObjectURL(transcriptionBlob);
  726. const transcriptionLink = document.createElement('a');
  727. transcriptionLink.href = transcriptionUrl;
  728. transcriptionLink.download = transcripts.transcription.filename;
  729. document.body.appendChild(transcriptionLink);
  730. transcriptionLink.click();
  731. document.body.removeChild(transcriptionLink);
  732. URL.revokeObjectURL(transcriptionUrl);
  733.  
  734. const systemBlob = new Blob([transcripts.system.content], { type: 'text/plain' });
  735. const systemUrl = URL.createObjectURL(systemBlob);
  736. const systemLink = document.createElement('a');
  737. systemLink.href = systemUrl;
  738. systemLink.download = transcripts.system.filename;
  739. document.body.appendChild(systemLink);
  740. systemLink.click();
  741. document.body.removeChild(systemLink);
  742. URL.revokeObjectURL(systemUrl);
  743.  
  744. dropdown.style.display = 'none';
  745. });
  746.  
  747. const replayItem = document.createElement('div');
  748. replayItem.id = 'copy-replay-url-share';
  749. replayItem.setAttribute('role', 'menuitem');
  750. replayItem.setAttribute('tabindex', '0');
  751. replayItem.className = 'css-175oi2r r-1loqt21 r-18u37iz r-1mmae3n r-3pj75a r-13qz1uu r-o7ynqc r-6416eg r-1ny4l3l';
  752. replayItem.style.transition = 'background-color 0.2s ease';
  753.  
  754. const replayIconContainer = document.createElement('div');
  755. replayIconContainer.className = 'css-175oi2r r-1777fci r-faml9v';
  756.  
  757. const replayIcon = document.createElement('svg');
  758. replayIcon.viewBox = '0 0 24 24';
  759. replayIcon.setAttribute('aria-hidden', 'true');
  760. replayIcon.className = 'r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1nao33i r-1q142lx';
  761. 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>';
  762. replayIconContainer.appendChild(replayIcon);
  763.  
  764. const replayTextContainer = document.createElement('div');
  765. replayTextContainer.className = 'css-175oi2r r-16y2uox r-1wbh5a2';
  766.  
  767. const replayText = document.createElement('div');
  768. replayText.dir = 'ltr';
  769. replayText.className = 'css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-b88u0q';
  770. replayText.style.color = 'rgb(231, 233, 234)';
  771. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>';
  772. replayTextContainer.appendChild(replayText);
  773.  
  774. replayItem.appendChild(replayIconContainer);
  775. replayItem.appendChild(replayTextContainer);
  776.  
  777. const replayStyle = document.createElement('style');
  778. replayStyle.textContent = '#copy-replay-url-share:hover { background-color: rgba(231, 233, 234, 0.1); }';
  779. replayItem.appendChild(replayStyle);
  780.  
  781. replayItem.addEventListener('click', async (e) => {
  782. e.preventDefault();
  783. if (!dynamicUrl) {
  784. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">No Dynamic URL</span>';
  785. setTimeout(() => replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>', 2000);
  786. dropdown.style.display = 'none';
  787. return;
  788. }
  789. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Generating...</span>';
  790. const newReplayUrl = await fetchReplayUrl(dynamicUrl);
  791. if (newReplayUrl.startsWith('http')) {
  792. navigator.clipboard.writeText(newReplayUrl).then(() => {
  793. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copied!</span>';
  794. setTimeout(() => replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>', 2000);
  795. }).catch(() => {
  796. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Failed</span>';
  797. setTimeout(() => replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>', 2000);
  798. });
  799. } else if (newReplayUrl.startsWith('data:text/html')) {
  800. replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Open Converter</span>';
  801. window.open(newReplayUrl, '_blank');
  802. setTimeout(() => replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>', 5000);
  803. } else {
  804. replayText.innerHTML = `<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">${newReplayUrl}</span>`;
  805. setTimeout(() => replayText.innerHTML = '<span class="css-1jxf684 r-bcqeeo r-1ttztb7 r-qvutc0 r-poiln3">Copy Replay URL</span>', 2000);
  806. }
  807. dropdown.style.display = 'none';
  808. });
  809.  
  810. const shareViaItem = dropdown.querySelector('div[data-testid="share-by-tweet"]');
  811. if (shareViaItem) {
  812. dropdown.insertBefore(downloadItem, shareViaItem.nextSibling);
  813. dropdown.insertBefore(replayItem, downloadItem.nextSibling);
  814. } else {
  815. dropdown.appendChild(downloadItem);
  816. dropdown.appendChild(replayItem);
  817. }
  818. }
  819.  
  820. function updateVisibilityAndPosition() {
  821. const reactionToggle = document.querySelector('button svg path[d*="M17 12v3h-2.998v2h3v3h2v-3h3v-2h-3.001v-3H17"]');
  822. const peopleButton = document.querySelector('button svg path[d*="M6.662 18H.846l.075-1.069"]');
  823. const isInSpace = reactionToggle !== null || peopleButton !== null;
  824. const endedScreen = Array.from(document.querySelectorAll('span[class*="r-bcqeeo"][class*="r-1ttztb7"]')).find(span => span.textContent.includes('Ended'));
  825.  
  826. if (isInSpace && !lastSpaceState) {
  827. const urlSpaceId = getSpaceIdFromUrl();
  828. if (urlSpaceId) {
  829. currentSpaceId = urlSpaceId;
  830. if (currentSpaceId !== lastSpaceId) {
  831. handQueue.clear();
  832. activeHandRaises.clear();
  833. captionsData = [];
  834. emojiReactions = [];
  835. lastSpeaker = { username: '', handle: '' };
  836. previousOccupancy = null;
  837. totalParticipants = 0;
  838. if (transcriptPopup) {
  839. const captionWrapper = transcriptPopup.querySelector('#transcript-output');
  840. if (captionWrapper) captionWrapper.innerHTML = '';
  841. }
  842. } else {
  843. handQueue.clear();
  844. activeHandRaises.clear();
  845. if (transcriptPopup && transcriptPopup.style.display === 'block') updateTranscriptPopup();
  846. }
  847. lastSpaceId = currentSpaceId;
  848. saveSettings();
  849. }
  850. } else if (!isInSpace && lastSpaceState && !endedScreen) {
  851. currentSpaceId = null;
  852. saveSettings();
  853. activeHandRaises.clear();
  854. }
  855.  
  856. if (isInSpace && peopleButton) {
  857. const peopleBtn = peopleButton.closest('button');
  858. if (peopleBtn) {
  859. const rect = peopleBtn.getBoundingClientRect();
  860. const baseTop = rect.top - 10;
  861. transcriptButton.style.position = 'fixed';
  862. transcriptButton.style.left = `${rect.left - 46}px`;
  863. transcriptButton.style.top = `${queueButton && queueButton.style.display !== 'none' ? baseTop + 40 : rect.top}px`;
  864. transcriptButton.style.display = 'block';
  865.  
  866. if (queueButton) {
  867. queueButton.style.position = 'fixed';
  868. queueButton.style.left = `${rect.left - 46}px`;
  869. queueButton.style.top = `${baseTop}px`;
  870. queueButton.style.display = handQueue.size > 0 ? 'block' : 'none';
  871. }
  872.  
  873. if (handQueuePopup) {
  874. handQueuePopup.style.right = transcriptPopup.style.right;
  875. handQueuePopup.style.bottom = transcriptPopup.style.bottom;
  876. }
  877. }
  878. if (reactionToggle) createEmojiPickerGrid();
  879. } else {
  880. transcriptButton.style.display = 'none';
  881. if (queueButton) queueButton.style.display = 'none';
  882. if (handQueuePopup) handQueuePopup.style.display = 'none';
  883. transcriptPopup.style.display = 'none';
  884. if (queueRefreshInterval) {
  885. clearInterval(queueRefreshInterval);
  886. queueRefreshInterval = null;
  887. }
  888. }
  889.  
  890. const endedContainer = detectEndedUI();
  891. if (endedContainer && lastSpaceState) {
  892. currentSpaceId = null;
  893. saveSettings();
  894. activeHandRaises.clear();
  895. transcriptButton.style.display = 'none';
  896. if (queueButton) queueButton.style.display = 'none';
  897. if (handQueuePopup) handQueuePopup.style.display = 'none';
  898. transcriptPopup.style.display = 'none';
  899. if (queueRefreshInterval) {
  900. clearInterval(queueRefreshInterval);
  901. queueRefreshInterval = null;
  902. }
  903. }
  904.  
  905. lastSpaceState = isInSpace;
  906. }
  907.  
  908. function updateQueueButtonVisibility() {
  909. if (queueButton) {
  910. queueButton.style.display = handQueue.size > 0 ? 'block' : 'none';
  911. updateVisibilityAndPosition();
  912. }
  913. }
  914.  
  915. async function formatTranscriptForDownload() {
  916. const spaceId = getSpaceIdFromUrl();
  917. const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  918. const baseHeader = '--- Space URLs ---\n' +
  919. (spaceId ? `Space URL: https://x.com/i/spaces/${spaceId}\n` : 'Space URL: Not available\n') +
  920. (dynamicUrl ? `Live URL: ${dynamicUrl}\n` : 'Live URL: Not available\n');
  921.  
  922. let replayUrl = 'Replay URL: Not available\n';
  923. try {
  924. const generatedReplayUrl = await fetchReplayUrl(dynamicUrl);
  925. replayUrl = `Replay URL: ${generatedReplayUrl}\n`;
  926. } catch (e) {
  927. replayUrl = 'Replay URL: Failed to generate\n';
  928. }
  929.  
  930. const header = `${baseHeader}${replayUrl}-----------------\n\n`;
  931.  
  932. const combinedData = [
  933. ...captionsData.map(item => ({ ...item, type: 'caption' })),
  934. ...emojiReactions.map(item => ({ ...item, type: 'emoji' }))
  935. ].sort((a, b) => a.timestamp - b.timestamp);
  936.  
  937. let transcriptionText = header;
  938. let previousSpeakerTrans = { username: '', handle: '' };
  939. const transcriptions = combinedData.filter(item => item.type === 'caption' && item.displayName !== 'System');
  940.  
  941. transcriptions.forEach((item, i) => {
  942. let { displayName, handle } = item;
  943. if (displayName === 'Unknown' && previousSpeakerTrans.username) {
  944. displayName = previousSpeakerTrans.username;
  945. handle = previousSpeakerTrans.handle;
  946. }
  947. if (i > 0 && previousSpeakerTrans.username !== displayName) {
  948. const date = new Date(item.timestamp);
  949. const timestampStr = date.toISOString().replace('T', ' ').substring(0, 19);
  950. transcriptionText += `\n[${timestampStr}]\n`;
  951. }
  952. transcriptionText += `${displayName} ${handle}\n${item.text}\n\n`;
  953. previousSpeakerTrans = { username: displayName, handle };
  954. });
  955.  
  956. let systemText = header;
  957. let previousSpeakerSys = { username: '', handle: '' };
  958. const systemAndReactions = combinedData.filter(item => item.type === 'emoji' || (item.type === 'caption' && item.displayName === 'System'));
  959.  
  960. systemAndReactions.forEach((item, i) => {
  961. let { displayName, handle } = item;
  962. if (displayName === 'Unknown' && previousSpeakerSys.username) {
  963. displayName = previousSpeakerSys.username;
  964. handle = previousSpeakerSys.handle;
  965. }
  966. if (i > 0 && previousSpeakerSys.username !== displayName && item.type === 'caption') {
  967. const date = new Date(item.timestamp);
  968. const timestampStr = date.toISOString().replace('T', ' ').substring(0, 19);
  969. systemText += `\n[${timestampStr}]\n`;
  970. }
  971. if (item.type === 'caption') {
  972. systemText += `${displayName} ${handle}\n${item.text}\n\n`;
  973. } else if (item.type === 'emoji') {
  974. systemText += `${displayName} reacted with ${item.emoji}\n`;
  975. }
  976. previousSpeakerSys = { username: displayName, handle };
  977. });
  978.  
  979. return {
  980. transcription: { content: transcriptionText, filename: `transcriptions_${timestamp}.txt` },
  981. system: { content: systemText, filename: `system_reactions_${timestamp}.txt` }
  982. };
  983. }
  984.  
  985. let isUserScrolledUp = false;
  986. let currentFontSize = 14;
  987. let searchTerm = '';
  988.  
  989. function filterTranscript(captions, emojis, term) {
  990. if (!term) return { captions, emojis };
  991. const filteredCaptions = captions.filter(caption =>
  992. caption.text.toLowerCase().includes(term.toLowerCase()) ||
  993. caption.displayName.toLowerCase().includes(term.toLowerCase()) ||
  994. caption.handle.toLowerCase().includes(term.toLowerCase())
  995. );
  996. const filteredEmojis = emojis.filter(emoji =>
  997. emoji.emoji.toLowerCase().includes(term.toLowerCase()) ||
  998. emoji.displayName.toLowerCase().includes(term.toLowerCase()) ||
  999. emoji.handle.toLowerCase().includes(term.toLowerCase())
  1000. );
  1001. return { captions: filteredCaptions, emojis: filteredEmojis };
  1002. }
  1003.  
  1004. async function toggleRecording(isEnabled) {
  1005. if (!capturedCookie || !currentSpaceId) return false;
  1006.  
  1007. const payload = {
  1008. topics: [],
  1009. is_space_available_for_clipping: false,
  1010. cookie: capturedCookie,
  1011. is_space_available_for_replay: isEnabled,
  1012. locale: "en",
  1013. replay_start_time: 0,
  1014. no_incognito: false,
  1015. replay_edited_title: "",
  1016. replay_thumbnail_time_code: 0,
  1017. broadcast_id: currentSpaceId
  1018. };
  1019.  
  1020. try {
  1021. const response = await fetch('https://proxsee.pscp.tv/api/v2/replayBroadcastEdit?build=com.atebits.Tweetie210.86', {
  1022. method: 'POST',
  1023. headers: {
  1024. 'Content-Type': 'application/json'
  1025. },
  1026. body: JSON.stringify(payload)
  1027. });
  1028. return response.ok;
  1029. } catch (error) {
  1030. return false;
  1031. }
  1032. }
  1033.  
  1034. function groupEmojiReactions(reactions) {
  1035. const grouped = {};
  1036. reactions.forEach(({ displayName, handle, emoji }) => {
  1037. const key = `${displayName}|${handle}`;
  1038. if (!grouped[key]) {
  1039. grouped[key] = { displayName, handle, emojis: {} };
  1040. }
  1041. grouped[key].emojis[emoji] = (grouped[key].emojis[emoji] || 0) + 1;
  1042. });
  1043.  
  1044. return Object.values(grouped).map(({ displayName, handle, emojis }) => {
  1045. const emojiString = Object.entries(emojis)
  1046. .map(([emoji, count]) => count > 1 ? `${emoji}x${count}` : emoji)
  1047. .join('');
  1048. return { displayName, handle, emojiString };
  1049. });
  1050. }
  1051.  
  1052. function updateTranscriptPopup() {
  1053. if (!transcriptPopup || transcriptPopup.style.display !== 'block') return;
  1054.  
  1055. let queueContainer = transcriptPopup.querySelector('#queue-container');
  1056. let searchContainer = transcriptPopup.querySelector('#search-container');
  1057. let scrollArea = transcriptPopup.querySelector('#transcript-scrollable');
  1058. let systemArea = transcriptPopup.querySelector('#system-messages');
  1059. let saveButton = transcriptPopup.querySelector('.save-button');
  1060. let textSizeContainer = transcriptPopup.querySelector('.text-size-container');
  1061. let systemToggleButton = transcriptPopup.querySelector('#system-toggle-button');
  1062. let emojiToggleButton = transcriptPopup.querySelector('#emoji-toggle-button');
  1063. let replayToggleButton = transcriptPopup.querySelector('#replay-toggle-button');
  1064. let currentScrollTop = scrollArea ? scrollArea.scrollTop : 0;
  1065. let wasAtBottom = scrollArea ? (scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight < 50) : true;
  1066.  
  1067. let showEmojisInUI = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false';
  1068. let showSystemMessagesInUI = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false';
  1069. let isReplayEnabled = localStorage.getItem(STORAGE_KEYS.REPLAY_ENABLED) !== 'false';
  1070.  
  1071. if (!queueContainer || !searchContainer || !scrollArea || !systemArea || !saveButton || !textSizeContainer || !systemToggleButton || !emojiToggleButton || !replayToggleButton) {
  1072. transcriptPopup.innerHTML = '';
  1073.  
  1074. queueContainer = document.createElement('div');
  1075. queueContainer.id = 'queue-container';
  1076. queueContainer.style.marginBottom = '10px';
  1077. transcriptPopup.appendChild(queueContainer);
  1078.  
  1079. searchContainer = document.createElement('div');
  1080. searchContainer.id = 'search-container';
  1081. searchContainer.style.display = 'none';
  1082. searchContainer.style.marginBottom = '5px';
  1083.  
  1084. const searchInput = document.createElement('input');
  1085. searchInput.type = 'text';
  1086. searchInput.placeholder = 'Search transcript...';
  1087. searchInput.style.width = '87%';
  1088. searchInput.style.padding = '5px';
  1089. searchInput.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
  1090. searchInput.style.border = 'none';
  1091. searchInput.style.borderRadius = '5px';
  1092. searchInput.style.color = 'white';
  1093. searchInput.style.fontSize = '14px';
  1094. searchInput.addEventListener('input', (e) => {
  1095. searchTerm = e.target.value.trim();
  1096. updateTranscriptPopup();
  1097. });
  1098.  
  1099. searchContainer.appendChild(searchInput);
  1100. transcriptPopup.appendChild(searchContainer);
  1101.  
  1102. scrollArea = document.createElement('div');
  1103. scrollArea.id = 'transcript-scrollable';
  1104. scrollArea.style.flex = '1';
  1105. scrollArea.style.overflowY = 'auto';
  1106. scrollArea.style.maxHeight = '250px';
  1107. scrollArea.style.marginBottom = '5px';
  1108.  
  1109. const captionWrapper = document.createElement('div');
  1110. captionWrapper.id = 'transcript-output';
  1111. captionWrapper.style.color = '#e7e9ea';
  1112. captionWrapper.style.fontFamily = 'Arial, sans-serif';
  1113. captionWrapper.style.whiteSpace = 'pre-wrap';
  1114. captionWrapper.style.fontSize = `${currentFontSize}px`;
  1115. scrollArea.appendChild(captionWrapper);
  1116. transcriptPopup.appendChild(scrollArea);
  1117.  
  1118. systemArea = document.createElement('div');
  1119. systemArea.id = 'system-messages';
  1120. systemArea.style.height = '4em';
  1121. systemArea.style.overflowY = 'auto';
  1122. systemArea.style.borderTop = '1px solid rgba(255, 255, 255, 0.3)';
  1123. systemArea.style.paddingTop = '5px';
  1124. systemArea.style.marginBottom = '5px';
  1125. systemArea.style.display = showSystemMessagesInUI ? 'block' : 'none';
  1126.  
  1127. const systemWrapper = document.createElement('div');
  1128. systemWrapper.id = 'system-output';
  1129. systemWrapper.style.color = '#e7e9ea';
  1130. systemWrapper.style.fontFamily = 'Arial, sans-serif';
  1131. systemWrapper.style.whiteSpace = 'pre-wrap';
  1132. systemWrapper.style.fontSize = `${currentFontSize}px`;
  1133. systemArea.appendChild(systemWrapper);
  1134. transcriptPopup.appendChild(systemArea);
  1135.  
  1136. const controlsContainer = document.createElement('div');
  1137. controlsContainer.style.display = 'flex';
  1138. controlsContainer.style.alignItems = 'center';
  1139. controlsContainer.style.justifyContent = 'space-between';
  1140. controlsContainer.style.padding = '5px 0';
  1141. controlsContainer.style.borderTop = '1px solid rgba(255, 255, 255, 0.3)';
  1142.  
  1143. saveButton = document.createElement('div');
  1144. saveButton.className = 'save-button';
  1145. saveButton.textContent = '💾 Save Transcript';
  1146. saveButton.style.color = '#1DA1F2';
  1147. saveButton.style.fontSize = '14px';
  1148. saveButton.style.cursor = 'pointer';
  1149. saveButton.addEventListener('click', async () => {
  1150. saveButton.textContent = '💾 Saving...';
  1151. const transcripts = await formatTranscriptForDownload();
  1152.  
  1153. const transcriptionBlob = new Blob([transcripts.transcription.content], { type: 'text/plain' });
  1154. const transcriptionUrl = URL.createObjectURL(transcriptionBlob);
  1155. const transcriptionLink = document.createElement('a');
  1156. transcriptionLink.href = transcriptionUrl;
  1157. transcriptionLink.download = transcripts.transcription.filename;
  1158. document.body.appendChild(transcriptionLink);
  1159. transcriptionLink.click();
  1160. document.body.removeChild(transcriptionLink);
  1161. URL.revokeObjectURL(transcriptionUrl);
  1162.  
  1163. const showEmojisInUI = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false';
  1164. const showSystemMessagesInUI = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false';
  1165.  
  1166. if (showEmojisInUI || showSystemMessagesInUI) {
  1167. await new Promise(resolve => setTimeout(resolve, 5000));
  1168. const systemBlob = new Blob([transcripts.system.content], { type: 'text/plain' });
  1169. const systemUrl = URL.createObjectURL(systemBlob);
  1170. const systemLink = document.createElement('a');
  1171. systemLink.href = systemUrl;
  1172. systemLink.download = transcripts.system.filename;
  1173. document.body.appendChild(systemLink);
  1174. systemLink.click();
  1175. document.body.removeChild(systemLink);
  1176. URL.revokeObjectURL(systemUrl);
  1177. }
  1178.  
  1179. saveButton.textContent = '💾 Save Transcript';
  1180. });
  1181. saveButton.addEventListener('mouseover', () => saveButton.style.color = '#FF9800');
  1182. saveButton.addEventListener('mouseout', () => saveButton.style.color = '#1DA1F2');
  1183.  
  1184. textSizeContainer = document.createElement('div');
  1185. textSizeContainer.className = 'text-size-container';
  1186. textSizeContainer.style.display = 'flex';
  1187. textSizeContainer.style.alignItems = 'center';
  1188.  
  1189. systemToggleButton = document.createElement('span');
  1190. systemToggleButton.id = 'system-toggle-button';
  1191. systemToggleButton.style.position = 'relative';
  1192. systemToggleButton.style.fontSize = '14px';
  1193. systemToggleButton.style.cursor = 'pointer';
  1194. systemToggleButton.style.marginRight = '5px';
  1195. systemToggleButton.style.width = '14px';
  1196. systemToggleButton.style.height = '14px';
  1197. systemToggleButton.style.display = 'inline-flex';
  1198. systemToggleButton.style.alignItems = 'center';
  1199. systemToggleButton.style.justifyContent = 'center';
  1200. systemToggleButton.title = 'Toggle System Messages in UI';
  1201. systemToggleButton.innerHTML = '📢';
  1202.  
  1203. const systemNotAllowedOverlay = document.createElement('span');
  1204. systemNotAllowedOverlay.style.position = 'absolute';
  1205. systemNotAllowedOverlay.style.width = '14px';
  1206. systemNotAllowedOverlay.style.height = '14px';
  1207. systemNotAllowedOverlay.style.border = '2px solid red';
  1208. systemNotAllowedOverlay.style.borderRadius = '50%';
  1209. systemNotAllowedOverlay.style.transform = 'rotate(45deg)';
  1210. systemNotAllowedOverlay.style.background = 'transparent';
  1211. systemNotAllowedOverlay.style.display = showSystemMessagesInUI ? 'none' : 'block';
  1212.  
  1213. const systemSlash = document.createElement('span');
  1214. systemSlash.style.position = 'absolute';
  1215. systemSlash.style.width = '2px';
  1216. systemSlash.style.height = '18px';
  1217. systemSlash.style.background = 'red';
  1218. systemSlash.style.transform = 'rotate(-45deg)';
  1219. systemSlash.style.top = '-2px';
  1220. systemSlash.style.left = '6px';
  1221. systemNotAllowedOverlay.appendChild(systemSlash);
  1222.  
  1223. systemToggleButton.appendChild(systemNotAllowedOverlay);
  1224.  
  1225. systemToggleButton.addEventListener('click', () => {
  1226. showSystemMessagesInUI = !showSystemMessagesInUI;
  1227. systemNotAllowedOverlay.style.display = showSystemMessagesInUI ? 'none' : 'block';
  1228. localStorage.setItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES, showSystemMessagesInUI);
  1229. systemArea.style.display = showSystemMessagesInUI ? 'block' : 'none';
  1230. updateTranscriptPopup();
  1231. });
  1232.  
  1233. emojiToggleButton = document.createElement('span');
  1234. emojiToggleButton.id = 'emoji-toggle-button';
  1235. emojiToggleButton.style.position = 'relative';
  1236. emojiToggleButton.style.fontSize = '14px';
  1237. emojiToggleButton.style.cursor = 'pointer';
  1238. emojiToggleButton.style.marginRight = '5px';
  1239. emojiToggleButton.style.width = '14px';
  1240. emojiToggleButton.style.height = '14px';
  1241. emojiToggleButton.style.display = 'inline-flex';
  1242. emojiToggleButton.style.alignItems = 'center';
  1243. emojiToggleButton.style.justifyContent = 'center';
  1244. emojiToggleButton.title = 'Toggle Emoji Reactions in UI';
  1245. emojiToggleButton.innerHTML = '🙂';
  1246.  
  1247. const emojiNotAllowedOverlay = document.createElement('span');
  1248. emojiNotAllowedOverlay.style.position = 'absolute';
  1249. emojiNotAllowedOverlay.style.width = '14px';
  1250. emojiNotAllowedOverlay.style.height = '14px';
  1251. emojiNotAllowedOverlay.style.border = '2px solid red';
  1252. emojiNotAllowedOverlay.style.borderRadius = '50%';
  1253. emojiNotAllowedOverlay.style.transform = 'rotate(45deg)';
  1254. emojiNotAllowedOverlay.style.background = 'transparent';
  1255. emojiNotAllowedOverlay.style.display = showEmojisInUI ? 'none' : 'block';
  1256.  
  1257. const emojiSlash = document.createElement('span');
  1258. emojiSlash.style.position = 'absolute';
  1259. emojiSlash.style.width = '2px';
  1260. emojiSlash.style.height = '18px';
  1261. emojiSlash.style.background = 'red';
  1262. emojiSlash.style.transform = 'rotate(-45deg)';
  1263. emojiSlash.style.top = '-2px';
  1264. emojiSlash.style.left = '6px';
  1265. emojiNotAllowedOverlay.appendChild(emojiSlash);
  1266.  
  1267. emojiToggleButton.appendChild(emojiNotAllowedOverlay);
  1268.  
  1269. emojiToggleButton.addEventListener('click', () => {
  1270. showEmojisInUI = !showEmojisInUI;
  1271. emojiNotAllowedOverlay.style.display = showEmojisInUI ? 'none' : 'block';
  1272. localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, showEmojisInUI);
  1273. updateTranscriptPopup();
  1274. });
  1275.  
  1276. replayToggleButton = document.createElement('span');
  1277. replayToggleButton.id = 'replay-toggle-button';
  1278. replayToggleButton.style.position = 'relative';
  1279. replayToggleButton.style.fontSize = '14px';
  1280. replayToggleButton.style.cursor = 'pointer';
  1281. replayToggleButton.style.marginRight = '5px';
  1282. replayToggleButton.style.width = '14px';
  1283. replayToggleButton.style.height = '14px';
  1284. replayToggleButton.style.display = 'inline-flex';
  1285. replayToggleButton.style.alignItems = 'center';
  1286. replayToggleButton.style.justifyContent = 'center';
  1287. replayToggleButton.title = 'Toggle Recording Availability';
  1288. replayToggleButton.innerHTML = '📼';
  1289.  
  1290. const replayNotAllowedOverlay = document.createElement('span');
  1291. replayNotAllowedOverlay.style.position = 'absolute';
  1292. replayNotAllowedOverlay.style.width = '14px';
  1293. replayNotAllowedOverlay.style.height = '14px';
  1294. replayNotAllowedOverlay.style.border = '2px solid red';
  1295. replayNotAllowedOverlay.style.borderRadius = '50%';
  1296. replayNotAllowedOverlay.style.transform = 'rotate(45deg)';
  1297. replayNotAllowedOverlay.style.background = 'transparent';
  1298. replayNotAllowedOverlay.style.display = isReplayEnabled ? 'none' : 'block';
  1299.  
  1300. const replaySlash = document.createElement('span');
  1301. replaySlash.style.position = 'absolute';
  1302. replaySlash.style.width = '2px';
  1303. replaySlash.style.height = '18px';
  1304. replaySlash.style.background = 'red';
  1305. replaySlash.style.transform = 'rotate(-45deg)';
  1306. replaySlash.style.top = '-2px';
  1307. replaySlash.style.left = '6px';
  1308. replayNotAllowedOverlay.appendChild(replaySlash);
  1309.  
  1310. replayToggleButton.appendChild(replayNotAllowedOverlay);
  1311.  
  1312. replayToggleButton.addEventListener('click', async () => {
  1313. isReplayEnabled = !isReplayEnabled;
  1314. replayNotAllowedOverlay.style.display = isReplayEnabled ? 'none' : 'block';
  1315. localStorage.setItem(STORAGE_KEYS.REPLAY_ENABLED, isReplayEnabled);
  1316.  
  1317. const success = await toggleRecording(isReplayEnabled);
  1318. if (!success) {
  1319. isReplayEnabled = !isReplayEnabled;
  1320. replayNotAllowedOverlay.style.display = isReplayEnabled ? 'none' : 'block';
  1321. localStorage.setItem(STORAGE_KEYS.REPLAY_ENABLED, isReplayEnabled);
  1322. }
  1323.  
  1324. updateTranscriptPopup();
  1325. });
  1326.  
  1327. const magnifierEmoji = document.createElement('span');
  1328. magnifierEmoji.textContent = '🔍';
  1329. magnifierEmoji.style.marginRight = '5px';
  1330. magnifierEmoji.style.fontSize = '14px';
  1331. magnifierEmoji.style.cursor = 'pointer';
  1332. magnifierEmoji.title = 'Search transcript';
  1333. magnifierEmoji.addEventListener('click', () => {
  1334. searchContainer.style.display = searchContainer.style.display === 'none' ? 'block' : 'none';
  1335. if (searchContainer.style.display === 'block') searchInput.focus();
  1336. else {
  1337. searchTerm = '';
  1338. searchInput.value = '';
  1339. updateTranscriptPopup();
  1340. }
  1341. });
  1342.  
  1343. const textSizeSlider = document.createElement('input');
  1344. textSizeSlider.type = 'range';
  1345. textSizeSlider.min = '12';
  1346. textSizeSlider.max = '18';
  1347. textSizeSlider.value = currentFontSize;
  1348. textSizeSlider.style.width = '50px';
  1349. textSizeSlider.style.cursor = 'pointer';
  1350. textSizeSlider.title = 'Adjust transcript text size';
  1351. textSizeSlider.addEventListener('input', () => {
  1352. currentFontSize = parseInt(textSizeSlider.value, 10);
  1353. const captionWrapper = transcriptPopup.querySelector('#transcript-output');
  1354. const systemWrapper = transcriptPopup.querySelector('#system-output');
  1355. if (captionWrapper) captionWrapper.style.fontSize = `${currentFontSize}px`;
  1356. if (systemWrapper) systemWrapper.style.fontSize = `${currentFontSize}px`;
  1357. localStorage.setItem('xSpacesCustomReactions_textSize', currentFontSize);
  1358. });
  1359.  
  1360. const savedTextSize = localStorage.getItem('xSpacesCustomReactions_textSize');
  1361. if (savedTextSize) {
  1362. currentFontSize = parseInt(savedTextSize, 10);
  1363. textSizeSlider.value = currentFontSize;
  1364. }
  1365.  
  1366. textSizeContainer.appendChild(systemToggleButton);
  1367. textSizeContainer.appendChild(emojiToggleButton);
  1368. textSizeContainer.appendChild(replayToggleButton);
  1369. textSizeContainer.appendChild(magnifierEmoji);
  1370. textSizeContainer.appendChild(textSizeSlider);
  1371.  
  1372. controlsContainer.appendChild(saveButton);
  1373. controlsContainer.appendChild(textSizeContainer);
  1374.  
  1375. transcriptPopup.appendChild(controlsContainer);
  1376. }
  1377.  
  1378. if (systemArea) systemArea.style.display = showSystemMessagesInUI ? 'block' : 'none';
  1379.  
  1380. const { captions: filteredCaptions, emojis: filteredEmojis } = filterTranscript(captionsData, emojiReactions, searchTerm);
  1381. const uiCaptions = filteredCaptions.filter(c => c.displayName !== 'System');
  1382. const uiSystemMessages = showSystemMessagesInUI ? filteredCaptions.filter(c => c.displayName === 'System') : [];
  1383. const uiEmojis = showEmojisInUI ? filteredEmojis : [];
  1384.  
  1385. const transcriptionData = [
  1386. ...uiCaptions.map(item => ({ ...item, type: 'caption' })),
  1387. ...uiEmojis.map(item => ({ ...item, type: 'emoji' }))
  1388. ].sort((a, b) => a.timestamp - b.timestamp);
  1389.  
  1390. const systemData = uiSystemMessages.map(item => ({ ...item, type: 'caption' }))
  1391. .sort((a, b) => a.timestamp - b.timestamp);
  1392.  
  1393. const hasTranscriptions = uiCaptions.length > 0;
  1394.  
  1395. if (!hasTranscriptions && !searchTerm) {
  1396. const captionWrapper = scrollArea.querySelector('#transcript-output');
  1397. if (captionWrapper) {
  1398. captionWrapper.innerHTML = `<div style="color: #FFD700; font-size: ${currentFontSize}px; margin-bottom: 10px;">Transcription not started. To start, turn closed captions on and off momentarily from the ... menu.</div>`;
  1399. }
  1400. const systemWrapper = systemArea.querySelector('#system-output');
  1401. if (systemWrapper) systemWrapper.innerHTML = '';
  1402. return;
  1403. }
  1404.  
  1405. let transcriptionHtml = '';
  1406. if (transcriptionData.length > 200) {
  1407. transcriptionHtml += '<div style="color: #FFD700; font-size: 12px; margin-bottom: 10px;">Showing the last 200 lines. Save transcript to see the full conversation.</div>';
  1408. }
  1409.  
  1410. let lastSpeakerDisplayed = '';
  1411. const recentTranscriptionData = transcriptionData.slice(-200);
  1412. let currentReactions = [];
  1413. let lastCaptionTimestamp = 0;
  1414.  
  1415. recentTranscriptionData.forEach((item, index) => {
  1416. if (item.type === 'caption') {
  1417. let { displayName, handle, text, timestamp } = item;
  1418. if (displayName === 'Unknown' && lastSpeaker.username) {
  1419. displayName = lastSpeaker.username;
  1420. handle = lastSpeaker.handle;
  1421. }
  1422.  
  1423. if (currentReactions.length > 0 && showEmojisInUI) {
  1424. const groupedReactions = groupEmojiReactions(currentReactions);
  1425. groupedReactions.forEach(({ displayName: rDisplayName, handle: rHandle, emojiString }) => {
  1426. transcriptionHtml += `<span style="font-size: ${currentFontSize}px; color: #FFD700">${rDisplayName}</span> ` +
  1427. `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">reacted with ${emojiString}</span><br>`;
  1428. });
  1429. currentReactions = [];
  1430. }
  1431.  
  1432. if (lastSpeakerDisplayed && lastSpeakerDisplayed !== displayName) {
  1433. transcriptionHtml += '<br>';
  1434. }
  1435. transcriptionHtml += `<span style="font-size: ${currentFontSize}px; color: #1DA1F2">${displayName} ${handle}</span> ` +
  1436. `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">${text}</span><br>`;
  1437. lastSpeaker = { username: displayName, handle };
  1438. lastSpeakerDisplayed = displayName;
  1439. lastCaptionTimestamp = timestamp;
  1440. } else if (item.type === 'emoji' && showEmojisInUI) {
  1441. let { displayName, handle, emoji } = item;
  1442. if (displayName === 'Unknown' && lastSpeaker.username) {
  1443. displayName = lastSpeaker.username;
  1444. handle = lastSpeaker.handle;
  1445. }
  1446. currentReactions.push({ displayName, handle, emoji });
  1447. lastSpeaker = { username: displayName, handle };
  1448. }
  1449.  
  1450. if (index === recentTranscriptionData.length - 1 && currentReactions.length > 0 && showEmojisInUI) {
  1451. const groupedReactions = groupEmojiReactions(currentReactions);
  1452. groupedReactions.forEach(({ displayName: rDisplayName, handle: rHandle, emojiString }) => {
  1453. transcriptionHtml += `<span style="font-size: ${currentFontSize}px; color: #FFD700">${rDisplayName}</span> ` +
  1454. `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">reacted with ${emojiString}</span><br>`;
  1455. });
  1456. }
  1457. });
  1458.  
  1459. let systemHtml = '';
  1460. systemData.slice(-10).forEach((item) => {
  1461. let { text } = item;
  1462. systemHtml += `<span style="font-size: ${currentFontSize}px; color: #FF4500">${text}</span><br>`;
  1463. });
  1464.  
  1465. const captionWrapper = scrollArea.querySelector('#transcript-output');
  1466. if (captionWrapper) {
  1467. captionWrapper.innerHTML = transcriptionHtml;
  1468. if (wasAtBottom && !searchTerm) scrollArea.scrollTop = scrollArea.scrollHeight;
  1469. else scrollArea.scrollTop = currentScrollTop;
  1470. scrollArea.onscroll = () => {
  1471. isUserScrolledUp = scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight > 50;
  1472. };
  1473. }
  1474.  
  1475. const systemWrapper = systemArea.querySelector('#system-output');
  1476. if (systemWrapper) {
  1477. systemWrapper.innerHTML = systemHtml;
  1478. systemArea.scrollTop = systemArea.scrollHeight;
  1479. }
  1480.  
  1481. if (handQueuePopup && handQueuePopup.style.display === 'block') {
  1482. updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content'));
  1483. }
  1484. }
  1485.  
  1486. function updateHandQueueContent(queueContent) {
  1487. if (!queueContent) return;
  1488. queueContent.innerHTML = '<strong>Speaking Queue</strong><br>';
  1489. if (handQueue.size === 0) {
  1490. queueContent.innerHTML += 'No hands raised.<br>';
  1491. } else {
  1492. const now = Date.now();
  1493. const sortedQueue = Array.from(handQueue.entries()).sort(([, a], [, b]) => a.timestamp - b.timestamp);
  1494.  
  1495. const queueList = document.createElement('div');
  1496. queueList.style.display = 'flex';
  1497. queueList.style.flexDirection = 'column';
  1498. queueList.style.gap = '8px';
  1499.  
  1500. const numberEmojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟'];
  1501.  
  1502. sortedQueue.forEach(([participantIndex, { displayName, timestamp }], index) => {
  1503. const entry = document.createElement('div');
  1504. entry.style.display = 'flex';
  1505. entry.style.justifyContent = 'space-between';
  1506. entry.style.alignItems = 'center';
  1507.  
  1508. const text = document.createElement('span');
  1509. const timeUp = Math.floor((now - timestamp) / 1000);
  1510. let timeStr;
  1511. if (timeUp >= 3600) {
  1512. const hours = Math.floor(timeUp / 3600);
  1513. const minutes = Math.floor((timeUp % 3600) / 60);
  1514. const seconds = timeUp % 60;
  1515. timeStr = `${hours}h ${minutes}m ${seconds}s`;
  1516. } else {
  1517. const minutes = Math.floor(timeUp / 60);
  1518. const seconds = timeUp % 60;
  1519. timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
  1520. }
  1521.  
  1522. const positionEmoji = index < 10 ? numberEmojis[index] : '';
  1523. text.textContent = `${positionEmoji} ${displayName}: ${timeStr}`;
  1524.  
  1525. const removeButton = document.createElement('button');
  1526. removeButton.textContent = '❌';
  1527. removeButton.style.background = 'none';
  1528. removeButton.style.border = 'none';
  1529. removeButton.style.color = 'white';
  1530. removeButton.style.fontSize = '14px';
  1531. removeButton.style.cursor = 'pointer';
  1532. removeButton.style.padding = '0';
  1533. removeButton.style.width = '20px';
  1534. removeButton.style.height = '20px';
  1535. removeButton.style.lineHeight = '20px';
  1536. removeButton.style.textAlign = 'center';
  1537. removeButton.title = `Remove ${displayName} from queue`;
  1538. removeButton.addEventListener('mouseover', () => removeButton.style.color = 'red');
  1539. removeButton.addEventListener('mouseout', () => removeButton.style.color = 'white');
  1540. removeButton.addEventListener('click', (e) => {
  1541. e.stopPropagation();
  1542. handQueue.delete(participantIndex);
  1543. activeHandRaises.delete(participantIndex);
  1544. updateQueueButtonVisibility();
  1545. updateHandQueueContent(queueContent);
  1546. });
  1547.  
  1548. entry.appendChild(text);
  1549. entry.appendChild(removeButton);
  1550. queueList.appendChild(entry);
  1551. });
  1552.  
  1553. queueContent.appendChild(queueList);
  1554. }
  1555.  
  1556. if (handRaiseDurations.length > 0) {
  1557. const averageContainer = document.createElement('div');
  1558. averageContainer.style.color = 'red';
  1559. averageContainer.style.fontSize = '12px';
  1560. averageContainer.style.marginTop = '10px';
  1561. averageContainer.style.textAlign = 'right';
  1562.  
  1563. const averageSeconds = handRaiseDurations.reduce((a, b) => a + b, 0) / handRaiseDurations.length;
  1564. let avgStr;
  1565. if (averageSeconds >= 3600) {
  1566. const hours = Math.floor(averageSeconds / 3600);
  1567. const minutes = Math.floor((averageSeconds % 3600) / 60);
  1568. const seconds = Math.floor(averageSeconds % 60);
  1569. avgStr = `${hours}h ${minutes}m ${seconds}s`;
  1570. } else {
  1571. const minutes = Math.floor(averageSeconds / 60);
  1572. const seconds = Math.floor(averageSeconds % 60);
  1573. avgStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
  1574. }
  1575. averageContainer.textContent = `Average Wait: ${avgStr}`;
  1576.  
  1577. queueContent.appendChild(averageContainer);
  1578. }
  1579. }
  1580.  
  1581. function init() {
  1582. transcriptButton = document.createElement('button');
  1583. transcriptButton.textContent = '📜';
  1584. transcriptButton.style.zIndex = '10001';
  1585. transcriptButton.style.fontSize = '18px';
  1586. transcriptButton.style.padding = '0';
  1587. transcriptButton.style.backgroundColor = 'transparent';
  1588. transcriptButton.style.border = '0.3px solid #40648085';
  1589. transcriptButton.style.borderRadius = '50%';
  1590. transcriptButton.style.width = '36px';
  1591. transcriptButton.style.height = '36px';
  1592. transcriptButton.style.cursor = 'pointer';
  1593. transcriptButton.style.display = 'none';
  1594. transcriptButton.style.lineHeight = '32px';
  1595. transcriptButton.style.textAlign = 'center';
  1596. transcriptButton.style.position = 'fixed';
  1597. transcriptButton.style.color = 'white';
  1598. transcriptButton.style.filter = 'grayscale(100%) brightness(200%)';
  1599. transcriptButton.title = 'Transcript';
  1600.  
  1601. transcriptButton.addEventListener('mouseover', () => transcriptButton.style.backgroundColor = '#595b5b40');
  1602. transcriptButton.addEventListener('mouseout', () => transcriptButton.style.backgroundColor = 'transparent');
  1603. transcriptButton.addEventListener('click', () => {
  1604. const isVisible = transcriptPopup.style.display === 'block';
  1605. transcriptPopup.style.display = isVisible ? 'none' : 'block';
  1606. if (!isVisible) updateTranscriptPopup();
  1607. });
  1608.  
  1609. queueButton = document.createElement('button');
  1610. queueButton.textContent = '✋';
  1611. queueButton.style.zIndex = '10001';
  1612. queueButton.style.fontSize = '18px';
  1613. queueButton.style.padding = '0';
  1614. queueButton.style.backgroundColor = 'transparent';
  1615. queueButton.style.border = '0.3px solid #40648085';
  1616. queueButton.style.borderRadius = '50%';
  1617. queueButton.style.width = '36px';
  1618. queueButton.style.height = '36px';
  1619. queueButton.style.cursor = 'pointer';
  1620. queueButton.style.display = 'none';
  1621. queueButton.style.lineHeight = '32px';
  1622. queueButton.style.textAlign = 'center';
  1623. queueButton.style.position = 'fixed';
  1624. queueButton.style.color = 'white';
  1625. queueButton.style.filter = 'grayscale(100%) brightness(200%)';
  1626. queueButton.title = 'View Speaking Queue';
  1627.  
  1628. queueButton.addEventListener('mouseover', () => queueButton.style.backgroundColor = '#595b5b40');
  1629. queueButton.addEventListener('mouseout', () => queueButton.style.backgroundColor = 'transparent');
  1630. queueButton.addEventListener('click', () => {
  1631. if (!handQueuePopup) {
  1632. handQueuePopup = document.createElement('div');
  1633. handQueuePopup.id = 'hand-queue-popup';
  1634. handQueuePopup.style.position = 'fixed';
  1635. handQueuePopup.style.backgroundColor = 'rgba(21, 32, 43, 0.8)';
  1636. handQueuePopup.style.borderRadius = '10px';
  1637. handQueuePopup.style.padding = '10px';
  1638. handQueuePopup.style.zIndex = '10003';
  1639. handQueuePopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';
  1640. handQueuePopup.style.width = '200px';
  1641. handQueuePopup.style.maxHeight = '200px';
  1642. handQueuePopup.style.overflowY = 'auto';
  1643. handQueuePopup.style.color = 'white';
  1644. handQueuePopup.style.fontSize = '14px';
  1645. handQueuePopup.style.display = 'none';
  1646.  
  1647. const closeHandButton = document.createElement('button');
  1648. closeHandButton.textContent = 'X';
  1649. closeHandButton.style.position = 'sticky';
  1650. closeHandButton.style.top = '5px';
  1651. closeHandButton.style.right = '5px';
  1652. closeHandButton.style.float = 'right';
  1653. closeHandButton.style.background = 'none';
  1654. closeHandButton.style.border = 'none';
  1655. closeHandButton.style.color = 'white';
  1656. closeHandButton.style.fontSize = '14px';
  1657. closeHandButton.style.cursor = 'pointer';
  1658. closeHandButton.style.padding = '0';
  1659. closeHandButton.style.width = '20px';
  1660. closeHandButton.style.height = '20px';
  1661. closeHandButton.style.lineHeight = '20px';
  1662. closeHandButton.style.textAlign = 'center';
  1663. closeHandButton.addEventListener('mouseover', () => closeHandButton.style.color = 'red');
  1664. closeHandButton.addEventListener('mouseout', () => closeHandButton.style.color = 'white');
  1665. closeHandButton.addEventListener('click', (e) => {
  1666. e.stopPropagation();
  1667. handQueuePopup.style.display = 'none';
  1668. });
  1669.  
  1670. const queueContent = document.createElement('div');
  1671. queueContent.id = 'hand-queue-content';
  1672. queueContent.style.paddingTop = '10px';
  1673.  
  1674. handQueuePopup.appendChild(closeHandButton);
  1675. handQueuePopup.appendChild(queueContent);
  1676. document.body.appendChild(handQueuePopup);
  1677. }
  1678.  
  1679. handQueuePopup.style.display = handQueuePopup.style.display === 'block' ? 'none' : 'block';
  1680. if (handQueuePopup.style.display === 'block') {
  1681. updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content'));
  1682. if (queueRefreshInterval) clearInterval(queueRefreshInterval);
  1683. queueRefreshInterval = setInterval(() => updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content')), 1000);
  1684. } else if (queueRefreshInterval) {
  1685. clearInterval(queueRefreshInterval);
  1686. queueRefreshInterval = null;
  1687. }
  1688. updateVisibilityAndPosition();
  1689. });
  1690.  
  1691. transcriptPopup = document.createElement('div');
  1692. transcriptPopup.style.position = 'fixed';
  1693. transcriptPopup.style.bottom = '150px';
  1694. transcriptPopup.style.right = '20px';
  1695. transcriptPopup.style.backgroundColor = 'rgba(21, 32, 43, 0.9)';
  1696. transcriptPopup.style.borderRadius = '10px';
  1697. transcriptPopup.style.padding = '10px';
  1698. transcriptPopup.style.zIndex = '10002';
  1699. transcriptPopup.style.maxHeight = '400px';
  1700. transcriptPopup.style.display = 'none';
  1701. transcriptPopup.style.width = '306px';
  1702. transcriptPopup.style.color = 'white';
  1703. transcriptPopup.style.fontSize = '14px';
  1704. transcriptPopup.style.lineHeight = '1.5';
  1705. transcriptPopup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';
  1706. transcriptPopup.style.display = 'flex';
  1707. transcriptPopup.style.flexDirection = 'column';
  1708.  
  1709. document.body.appendChild(queueButton);
  1710. document.body.appendChild(transcriptButton);
  1711. document.body.appendChild(transcriptPopup);
  1712.  
  1713. loadSettings();
  1714.  
  1715. const observer = new MutationObserver((mutationsList) => {
  1716. for (const mutation of mutationsList) {
  1717. if (mutation.type === 'childList') {
  1718. updateVisibilityAndPosition();
  1719. const dropdown = document.querySelector('div[data-testid="Dropdown"]');
  1720. if (dropdown && dropdown.closest('[role="menu"]') && (captionsData.length > 0 || emojiReactions.length > 0)) {
  1721. addDownloadOptionToShareDropdown(dropdown);
  1722. }
  1723. const audioElements = document.querySelectorAll('audio');
  1724. audioElements.forEach(audio => {
  1725. if (audio.src && audio.src.includes('dynamic_playlist.m3u8?type=live')) dynamicUrl = audio.src;
  1726. });
  1727. }
  1728. }
  1729. });
  1730.  
  1731. observer.observe(document.body, { childList: true, subtree: true });
  1732. updateVisibilityAndPosition();
  1733. setInterval(updateVisibilityAndPosition, 2000);
  1734. }
  1735.  
  1736. if (document.readyState === 'loading') {
  1737. document.addEventListener('DOMContentLoaded', () => {
  1738. init();
  1739. });
  1740. } else {
  1741. init();
  1742. }
  1743. })();