Bluesky Threading Improvements

Adds colors and expand/collapse functionality to Bluesky threads

  1. // ==UserScript==
  2. // @name Bluesky Threading Improvements
  3. // @namespace zetaphor.com
  4. // @description Adds colors and expand/collapse functionality to Bluesky threads
  5. // @version 0.4
  6. // @license MIT
  7. // @match https://bsky.app/*
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Add our styles
  15. GM_addStyle(`
  16. /* Thread depth colors */
  17. div[style*="border-left-width: 2px"] {
  18. border-left-width: 2px !important;
  19. border-left-style: solid !important;
  20. }
  21.  
  22. /* Color definitions */
  23. div[style*="border-left-width: 2px"]:nth-child(1) { border-color: #2962ff !important; } /* Blue */
  24. div[style*="border-left-width: 2px"]:nth-child(2) { border-color: #8e24aa !important; } /* Purple */
  25. div[style*="border-left-width: 2px"]:nth-child(3) { border-color: #2e7d32 !important; } /* Green */
  26. div[style*="border-left-width: 2px"]:nth-child(4) { border-color: #ef6c00 !important; } /* Orange */
  27. div[style*="border-left-width: 2px"]:nth-child(5) { border-color: #c62828 !important; } /* Red */
  28. div[style*="border-left-width: 2px"]:nth-child(6) { border-color: #00796b !important; } /* Teal */
  29. div[style*="border-left-width: 2px"]:nth-child(7) { border-color: #c2185b !important; } /* Pink */
  30. div[style*="border-left-width: 2px"]:nth-child(8) { border-color: #ffa000 !important; } /* Amber */
  31. div[style*="border-left-width: 2px"]:nth-child(9) { border-color: #1565c0 !important; } /* Dark Blue */
  32. div[style*="border-left-width: 2px"]:nth-child(10) { border-color: #6a1b9a !important; } /* Deep Purple */
  33. div[style*="border-left-width: 2px"]:nth-child(11) { border-color: #558b2f !important; } /* Light Green */
  34. div[style*="border-left-width: 2px"]:nth-child(12) { border-color: #d84315 !important; } /* Deep Orange */
  35. div[style*="border-left-width: 2px"]:nth-child(13) { border-color: #303f9f !important; } /* Indigo */
  36. div[style*="border-left-width: 2px"]:nth-child(14) { border-color: #b71c1c !important; } /* Dark Red */
  37. div[style*="border-left-width: 2px"]:nth-child(15) { border-color: #006064 !important; } /* Cyan */
  38.  
  39. /* Collapse button styles */
  40. .thread-collapse-btn {
  41. cursor: pointer;
  42. width: 20px;
  43. height: 20px;
  44. position: absolute;
  45. left: -16px;
  46. top: 18px;
  47. background-color: #1e2937;
  48. color: #aebbc9;
  49. border: 1px solid #4a6179;
  50. border-radius: 25%;
  51. z-index: 100;
  52. padding: 0;
  53. transition: background-color 0.2s ease;
  54. }
  55.  
  56. .thread-collapse-btn:hover {
  57. background-color: #2e4054;
  58. }
  59.  
  60. /* Indicator styles */
  61. .thread-collapse-indicator {
  62. position: absolute;
  63. top: 50%;
  64. left: 50%;
  65. transform: translate(-50%, -50%);
  66. font-family: monospace;
  67. font-size: 16px;
  68. line-height: 1;
  69. user-select: none;
  70. }
  71.  
  72. /* Collapsed thread styles */
  73. .thread-collapsed {
  74. display: none !important;
  75. }
  76.  
  77. /* Post container relative positioning for collapse button */
  78. .post-with-collapse {
  79. position: relative;
  80. }
  81.  
  82. /* Animation for button spin */
  83. @keyframes spin {
  84. 0% { transform: rotate(0deg); }
  85. 100% { transform: rotate(360deg); }
  86. }
  87.  
  88. .thread-collapse-btn.spinning {
  89. animation: spin 0.2s ease-in-out;
  90. }
  91. `);
  92.  
  93. // Utility function to check if we're on a post page
  94. function isPostPage() {
  95. return window.location.pathname.match(/^\/profile\/[^\/]+\/post\/.+/);
  96. }
  97.  
  98. function getIndentCount(postContainer) {
  99. const parent = postContainer.parentElement;
  100. if (!parent) return 0;
  101.  
  102. const indents = Array.from(parent.parentElement.children).filter(child =>
  103. child.getAttribute('style')?.includes('border-left-width: 2px')
  104. );
  105.  
  106. return indents.length;
  107. }
  108.  
  109. function hasChildThreads(postContainer) {
  110. const currentIndents = getIndentCount(postContainer);
  111. const threadContainer = postContainer.closest('[data-thread-container]') ||
  112. postContainer.parentElement?.parentElement?.parentElement?.parentElement;
  113.  
  114. if (!threadContainer) return false;
  115.  
  116. const nextThreadContainer = threadContainer.nextElementSibling;
  117. if (!nextThreadContainer) return false;
  118.  
  119. const nextPost = nextThreadContainer.querySelector('div[role="link"][tabindex="0"]');
  120. if (!nextPost) return false;
  121.  
  122. const nextIndents = getIndentCount(nextPost);
  123. return nextIndents > currentIndents;
  124. }
  125.  
  126. function toggleThread(threadStart, isCollapsed) {
  127. const currentIndents = getIndentCount(threadStart);
  128. const threadContainer = threadStart.closest('[data-thread-container]') ||
  129. threadStart.parentElement?.parentElement?.parentElement?.parentElement;
  130.  
  131. let nextContainer = threadContainer?.nextElementSibling;
  132. while (nextContainer) {
  133. const nextPost = nextContainer.querySelector('div[role="link"][tabindex="0"]');
  134. if (nextPost) {
  135. const nextIndents = getIndentCount(nextPost);
  136.  
  137. if (nextIndents <= currentIndents) break;
  138.  
  139. if (isCollapsed) {
  140. nextContainer.classList.add('thread-collapsed');
  141. } else {
  142. nextContainer.classList.remove('thread-collapsed');
  143. }
  144. }
  145. nextContainer = nextContainer.nextElementSibling;
  146. }
  147. }
  148.  
  149. function addCollapseButton(postContainer) {
  150. if (!postContainer || postContainer.querySelector('.thread-collapse-btn')) {
  151. return;
  152. }
  153.  
  154. const button = document.createElement('button');
  155. button.className = 'thread-collapse-btn';
  156. button.setAttribute('aria-label', 'Collapse thread');
  157.  
  158. const indicator = document.createElement('div');
  159. indicator.className = 'thread-collapse-indicator';
  160. indicator.textContent = '-';
  161. button.appendChild(indicator);
  162.  
  163. postContainer.classList.add('post-with-collapse');
  164. postContainer.appendChild(button);
  165.  
  166. button.addEventListener('click', (e) => {
  167. e.stopPropagation();
  168. const isCollapsed = button.classList.toggle('collapsed');
  169.  
  170. button.classList.add('spinning');
  171.  
  172. setTimeout(() => {
  173. indicator.textContent = isCollapsed ? '+' : '-';
  174. button.classList.remove('spinning');
  175. }, 200);
  176.  
  177. toggleThread(postContainer, isCollapsed);
  178. });
  179. }
  180.  
  181. function initializeThreadCollapse() {
  182. if (!isPostPage()) return false;
  183.  
  184. const posts = document.querySelectorAll('div[role="link"][tabindex="0"]');
  185. let hasAddedButtons = false;
  186.  
  187. posts.forEach(post => {
  188. if (hasChildThreads(post)) {
  189. addCollapseButton(post);
  190. hasAddedButtons = true;
  191. }
  192. });
  193.  
  194. return hasAddedButtons;
  195. }
  196.  
  197. // Enhanced initialization with retry mechanism
  198. function initializeWithRetry() {
  199. const maxAttempts = 10;
  200. let attempts = 0;
  201. let initialized = false;
  202.  
  203. function attempt() {
  204. if (attempts >= maxAttempts || initialized) return;
  205.  
  206. attempts++;
  207.  
  208. // Check if the main post container is present
  209. const mainPost = document.querySelector('div[role="link"][tabindex="0"]');
  210. if (!mainPost) {
  211. setTimeout(attempt, 500);
  212. return;
  213. }
  214.  
  215. // Try to initialize
  216. initialized = initializeThreadCollapse();
  217.  
  218. if (!initialized) {
  219. setTimeout(attempt, 500);
  220. }
  221. }
  222.  
  223. // Start the first attempt
  224. attempt();
  225. }
  226.  
  227. // Initialize on page load
  228. initializeWithRetry();
  229.  
  230. // Set up observer for dynamic content changes
  231. const observer = new MutationObserver((mutations) => {
  232. for (const mutation of mutations) {
  233. for (const node of mutation.addedNodes) {
  234. if (node.nodeType === 1) {
  235. const posts = node.querySelectorAll('div[role="link"][tabindex="0"]');
  236. if (posts.length > 0) {
  237. initializeWithRetry();
  238. break;
  239. }
  240. }
  241. }
  242. }
  243. });
  244.  
  245. // Start observing after a short delay to ensure the page is ready
  246. setTimeout(() => {
  247. observer.observe(document.body, {
  248. childList: true,
  249. subtree: true
  250. });
  251. }, 1000);
  252. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址