电报网页版 - 健壮滚动修复 (v1.4 - 图片/加载处理)

更健壮地修复滚动问题,尝试处理延迟加载的图片和初始加载场景。

  1. // ==UserScript==
  2. // @name Telegram Web - Robust Scroll Fix (v1.4 - Image/Load Handling)
  3. // @name:zh-CN 电报网页版 - 健壮滚动修复 (v1.4 - 图片/加载处理)
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.4
  6. // @description Attempts to fix scroll issues more robustly, handling late-loading images and initial load scenarios.
  7. // @description:zh-CN 更健壮地修复滚动问题,尝试处理延迟加载的图片和初始加载场景。
  8. // @author AI Assistant & You
  9. // @match *://web.telegram.org/k/*
  10. // @grant GM_addStyle
  11. // @icon https://web.telegram.org/favicon.ico
  12. // @run-at document-idle
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // --- 配置 ---
  19. const SCROLL_THRESHOLD = 300; // 判断“接近底部”的阈值 (像素)
  20. const POST_CORRECTION_CHECK_DELAY = 150; // rAF修正后再次检查的延迟(ms), 处理图片等慢加载
  21. const INITIAL_SCROLL_DELAY = 1800; // 初始加载后检查滚动的延迟(ms), 增加时间
  22. const CHECK_INTERVAL = 1000; // 查找容器间隔
  23. const SCROLL_CONTAINER_SELECTOR = '#column-center .scrollable.scrollable-y'; // K 版滚动容器
  24. // --- --- ---
  25.  
  26. let targetNode = null; // 滚动容器
  27. let observer = null;
  28. let correctionFrameId = null; // 用于 requestAnimationFrame
  29. let postCorrectionTimeoutId = null; // 后置检查计时器
  30. let wasNearBottom = false; // 由滚动事件更新
  31. let isScrolling = false; // 标记用户是否正在滚动
  32. let scrollDebounceTimeout = null;
  33. let styleAdded = false; // 标记样式是否已添加
  34.  
  35. console.log("[ScrollFixer v1.4] Script initializing...");
  36.  
  37. // 注入 CSS 样式
  38. function applyStyles() {
  39. if (!styleAdded) {
  40. try {
  41. GM_addStyle(`
  42. ${SCROLL_CONTAINER_SELECTOR} {
  43. overflow-anchor: auto !important;
  44. }
  45. .scrollable-y.script-scrolling {
  46. scroll-behavior: auto !important;
  47. }
  48. `);
  49. styleAdded = true;
  50. console.log("[ScrollFixer v1.4] Applied CSS via GM_addStyle.");
  51. } catch (e) {
  52. console.error("[ScrollFixer v1.4] GM_addStyle failed.", e);
  53. const styleElement = document.createElement('style');
  54. styleElement.textContent = `
  55. ${SCROLL_CONTAINER_SELECTOR} { overflow-anchor: auto !important; }
  56. .scrollable-y.script-scrolling { scroll-behavior: auto !important; }
  57. `;
  58. document.head.appendChild(styleElement);
  59. console.warn("[ScrollFixer v1.4] Using fallback style injection.");
  60. }
  61. }
  62. }
  63.  
  64. function isNearBottom(element) {
  65. if (!element) return false;
  66. const isScrollable = element.scrollHeight > element.clientHeight;
  67. return !isScrollable || (element.scrollHeight - element.scrollTop - element.clientHeight < SCROLL_THRESHOLD);
  68. }
  69.  
  70. // 检查是否 *明确地* 远离底部
  71. function isFarFromBottom(element) {
  72. if (!element) return false; // 如果没有元素,不认为远离 (避免错误滚动)
  73. const isScrollable = element.scrollHeight > element.clientHeight;
  74. // 只有可滚动且距离底部大于阈值才算远离
  75. return isScrollable && (element.scrollHeight - element.scrollTop - element.clientHeight >= SCROLL_THRESHOLD);
  76. }
  77.  
  78. function scrollToBottom(element, reason = "correction", behavior = 'auto') {
  79. if (!element || !document.contains(element)) return;
  80. const currentScroll = element.scrollTop;
  81. const maxScroll = element.scrollHeight - element.clientHeight;
  82. // 增加容错
  83. if (maxScroll - currentScroll < 1) {
  84. // console.log(`[ScrollFixer v1.4] Already at bottom (${reason}), scroll skipped.`);
  85. // 即使跳过滚动,也要确保状态正确
  86. if (!wasNearBottom) { // 如果之前认为不在底部,现在强制设为在底部
  87. wasNearBottom = true;
  88. // console.log("[ScrollFixer v1.4] State corrected to wasNearBottom=true after skip.");
  89. }
  90. return;
  91. }
  92. console.log(`[ScrollFixer v1.4] Forcing scroll to bottom (Reason: ${reason}, Behavior: ${behavior}).`);
  93.  
  94. element.classList.add('script-scrolling');
  95. element.scrollTo({ top: element.scrollHeight, behavior: behavior });
  96. // 滚动后立即更新状态
  97. wasNearBottom = true;
  98.  
  99. requestAnimationFrame(() => {
  100. requestAnimationFrame(() => {
  101. if (element) element.classList.remove('script-scrolling');
  102. });
  103. });
  104. }
  105.  
  106. // 处理滚动事件
  107. function handleScroll() {
  108. if (!targetNode) return;
  109. if (targetNode.classList.contains('script-scrolling')) {
  110. return; // 忽略脚本触发的滚动
  111. }
  112. isScrolling = true;
  113. clearTimeout(scrollDebounceTimeout);
  114. wasNearBottom = isNearBottom(targetNode);
  115. // console.log(`[ScrollFixer v1.4] Scroll event. Near bottom: ${wasNearBottom}`);
  116. scrollDebounceTimeout = setTimeout(() => {
  117. isScrolling = false;
  118. // console.log("[ScrollFixer v1.4] Scroll ended.");
  119. }, 150);
  120. }
  121.  
  122.  
  123. // 处理 DOM 变化的函数 - 即时 rAF + 后置检查
  124. function handleMutations(mutationsList, observer) {
  125. if (isScrolling) {
  126. return;
  127. }
  128.  
  129. if (wasNearBottom && targetNode) {
  130. // console.log("[ScrollFixer v1.4] Mutation detected while near bottom. Scheduling immediate rAF correction.");
  131.  
  132. // 取消上一个可能未执行的帧和后置检查
  133. if (correctionFrameId) cancelAnimationFrame(correctionFrameId);
  134. clearTimeout(postCorrectionTimeoutId); // 清除上一个后置检查
  135.  
  136. // 立即请求下一帧执行修正
  137. correctionFrameId = requestAnimationFrame(() => {
  138. correctionFrameId = null; // 清除 ID
  139. if (targetNode && document.contains(targetNode)) {
  140. // console.log("[ScrollFixer v1.4] Performing immediate rAF scroll correction.");
  141. scrollToBottom(targetNode, "mutation_rAF", 'auto');
  142.  
  143. // --- 新增:rAF 修正后,安排一个后置检查 ---
  144. postCorrectionTimeoutId = setTimeout(() => {
  145. postCorrectionTimeoutId = null; // 清除 ID
  146. if (targetNode && document.contains(targetNode) && !isScrolling) {
  147. // console.log("[ScrollFixer v1.4] Performing post-correction check.");
  148. if (!isNearBottom(targetNode)) { // 如果此时因为图片加载等原因又不在底部了
  149. console.warn("[ScrollFixer v1.4] Post-correction check failed. Scrolling again.");
  150. scrollToBottom(targetNode, "post_check", 'auto'); // 再次滚动
  151. } else {
  152. // console.log("[ScrollFixer v1.4] Post-correction check passed.");
  153. // 确保状态正确
  154. wasNearBottom = true;
  155. }
  156. }
  157. }, POST_CORRECTION_CHECK_DELAY);
  158. // --- --- ---
  159.  
  160. } else {
  161. // console.warn("[ScrollFixer v1.4] Target node lost before immediate rAF execution.");
  162. }
  163. });
  164. }
  165. }
  166.  
  167. // 查找并观察消息列表容器
  168. function findAndObserve() {
  169. const targetSelector = SCROLL_CONTAINER_SELECTOR;
  170. let potentialNode = document.querySelector(targetSelector);
  171.  
  172. if (observer && targetNode === potentialNode && document.contains(targetNode)) {
  173. return;
  174. }
  175.  
  176. // Cleanup
  177. if (observer) { /* ... 清理逻辑 ... */ }
  178. targetNode = potentialNode;
  179.  
  180. if (targetNode) {
  181. console.log("[ScrollFixer v1.4] Chat scroll container found:", targetNode);
  182. applyStyles();
  183.  
  184. // 初始滚动检查 - 更保守
  185. setTimeout(() => {
  186. if (targetNode && document.contains(targetNode)) {
  187. console.log("[ScrollFixer v1.4] Performing initial check...");
  188. // 只有当明确检测到远离底部时,才执行初始滚动
  189. if (isFarFromBottom(targetNode)) {
  190. console.log("[ScrollFixer v1.4] Far from bottom on initial check. Scrolling down.");
  191. scrollToBottom(targetNode, "initial_load", 'auto');
  192. wasNearBottom = true;
  193. } else {
  194. console.log("[ScrollFixer v1.4] Already near bottom or content not fully loaded? Initial scroll skipped/deferred.");
  195. // 确保状态与实际情况一致
  196. wasNearBottom = isNearBottom(targetNode);
  197. }
  198. console.log(`[ScrollFixer v1.4] Initial state - near bottom: ${wasNearBottom}`);
  199.  
  200. // Setup listeners/observer
  201. if (!observer) {
  202. targetNode.addEventListener('scroll', handleScroll, { passive: true });
  203. console.log("[ScrollFixer v1.4] Scroll listener added.");
  204. observer = new MutationObserver(handleMutations);
  205. const config = { childList: true, subtree: true };
  206. observer.observe(targetNode, config);
  207. console.log("[ScrollFixer v1.4] MutationObserver started.");
  208. }
  209. } else {
  210. console.log("[ScrollFixer v1.4] Target node disappeared before initial setup.");
  211. }
  212. }, INITIAL_SCROLL_DELAY);
  213.  
  214. } else {
  215. console.log(`[ScrollFixer v1.4] Chat container not found, retrying...`);
  216. setTimeout(findAndObserve, CHECK_INTERVAL);
  217. }
  218. }
  219.  
  220. // --- SPA Navigation Handling & Initial Start ---
  221. // (与 v1.3 类似,注意清理 postCorrectionTimeoutId)
  222. let currentUrl = location.href;
  223. const urlObserver = new MutationObserver(() => {
  224. if (location.href !== currentUrl) {
  225. console.log(`[ScrollFixer v1.4] URL changed. Re-initializing for ${location.href}`);
  226. currentUrl = location.href;
  227. // Cleanup state and timers
  228. if (observer) { observer.disconnect(); observer = null; }
  229. if (targetNode && typeof targetNode.removeEventListener === 'function') {
  230. targetNode.removeEventListener('scroll', handleScroll);
  231. }
  232. if (correctionFrameId) { cancelAnimationFrame(correctionFrameId); correctionFrameId = null; }
  233. clearTimeout(postCorrectionTimeoutId); // 清理后置检查计时器
  234. targetNode = null;
  235. wasNearBottom = false;
  236. isScrolling = false;
  237. clearTimeout(scrollDebounceTimeout);
  238. // styleAdded = false; // GM_addStyle persists
  239. setTimeout(findAndObserve, 500);
  240. }
  241. });
  242. urlObserver.observe(document.body, { childList: true, subtree: false });
  243.  
  244. setTimeout(findAndObserve, 750);
  245.  
  246. })();