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-09 提交的版本,查看 最新版本

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