X Spaces +

Addon for X Spaces with custom emojis, enhanced transcript, speaker queuing, recording toggle, and robust auto-download of transcript and logs when space ends

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