Telegram Web - Robust Scroll Fix (v1.4 - Image/Load Handling)

Attempts to fix scroll issues more robustly, handling late-loading images and initial load scenarios.

// ==UserScript==
// @name         Telegram Web - Robust Scroll Fix (v1.4 - Image/Load Handling)
// @name:zh-CN   电报网页版 - 健壮滚动修复 (v1.4 - 图片/加载处理)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Attempts to fix scroll issues more robustly, handling late-loading images and initial load scenarios.
// @description:zh-CN 更健壮地修复滚动问题,尝试处理延迟加载的图片和初始加载场景。
// @author       AI Assistant & You
// @match        *://web.telegram.org/k/*
// @grant        GM_addStyle
// @icon         https://web.telegram.org/favicon.ico
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const SCROLL_THRESHOLD = 300;         // 判断“接近底部”的阈值 (像素)
    const POST_CORRECTION_CHECK_DELAY = 150; // rAF修正后再次检查的延迟(ms), 处理图片等慢加载
    const INITIAL_SCROLL_DELAY = 1800;    // 初始加载后检查滚动的延迟(ms), 增加时间
    const CHECK_INTERVAL = 1000;        // 查找容器间隔
    const SCROLL_CONTAINER_SELECTOR = '#column-center .scrollable.scrollable-y'; // K 版滚动容器
    // --- --- ---

    let targetNode = null; // 滚动容器
    let observer = null;
    let correctionFrameId = null; // 用于 requestAnimationFrame
    let postCorrectionTimeoutId = null; // 后置检查计时器
    let wasNearBottom = false; // 由滚动事件更新
    let isScrolling = false; // 标记用户是否正在滚动
    let scrollDebounceTimeout = null;
    let styleAdded = false; // 标记样式是否已添加

    console.log("[ScrollFixer v1.4] Script initializing...");

    // 注入 CSS 样式
    function applyStyles() {
        if (!styleAdded) {
            try {
                GM_addStyle(`
                    ${SCROLL_CONTAINER_SELECTOR} {
                        overflow-anchor: auto !important;
                    }
                    .scrollable-y.script-scrolling {
                         scroll-behavior: auto !important;
                    }
                `);
                styleAdded = true;
                console.log("[ScrollFixer v1.4] Applied CSS via GM_addStyle.");
            } catch (e) {
                console.error("[ScrollFixer v1.4] GM_addStyle failed.", e);
                const styleElement = document.createElement('style');
                 styleElement.textContent = `
                    ${SCROLL_CONTAINER_SELECTOR} { overflow-anchor: auto !important; }
                    .scrollable-y.script-scrolling { scroll-behavior: auto !important; }
                 `;
                 document.head.appendChild(styleElement);
                 console.warn("[ScrollFixer v1.4] Using fallback style injection.");
            }
        }
    }

    function isNearBottom(element) {
        if (!element) return false;
        const isScrollable = element.scrollHeight > element.clientHeight;
        return !isScrollable || (element.scrollHeight - element.scrollTop - element.clientHeight < SCROLL_THRESHOLD);
    }

     // 检查是否 *明确地* 远离底部
     function isFarFromBottom(element) {
        if (!element) return false; // 如果没有元素,不认为远离 (避免错误滚动)
        const isScrollable = element.scrollHeight > element.clientHeight;
        // 只有可滚动且距离底部大于阈值才算远离
        return isScrollable && (element.scrollHeight - element.scrollTop - element.clientHeight >= SCROLL_THRESHOLD);
    }

    function scrollToBottom(element, reason = "correction", behavior = 'auto') {
        if (!element || !document.contains(element)) return;
        const currentScroll = element.scrollTop;
        const maxScroll = element.scrollHeight - element.clientHeight;
        // 增加容错
        if (maxScroll - currentScroll < 1) {
             // console.log(`[ScrollFixer v1.4] Already at bottom (${reason}), scroll skipped.`);
            // 即使跳过滚动,也要确保状态正确
            if (!wasNearBottom) { // 如果之前认为不在底部,现在强制设为在底部
                wasNearBottom = true;
                // console.log("[ScrollFixer v1.4] State corrected to wasNearBottom=true after skip.");
            }
            return;
        }
        console.log(`[ScrollFixer v1.4] Forcing scroll to bottom (Reason: ${reason}, Behavior: ${behavior}).`);

        element.classList.add('script-scrolling');
        element.scrollTo({ top: element.scrollHeight, behavior: behavior });
        // 滚动后立即更新状态
        wasNearBottom = true;

        requestAnimationFrame(() => {
             requestAnimationFrame(() => {
                 if (element) element.classList.remove('script-scrolling');
             });
        });
    }

    // 处理滚动事件
    function handleScroll() {
        if (!targetNode) return;
        if (targetNode.classList.contains('script-scrolling')) {
             return; // 忽略脚本触发的滚动
        }
        isScrolling = true;
        clearTimeout(scrollDebounceTimeout);
        wasNearBottom = isNearBottom(targetNode);
        // console.log(`[ScrollFixer v1.4] Scroll event. Near bottom: ${wasNearBottom}`);
        scrollDebounceTimeout = setTimeout(() => {
            isScrolling = false;
            // console.log("[ScrollFixer v1.4] Scroll ended.");
        }, 150);
    }


    // 处理 DOM 变化的函数 - 即时 rAF + 后置检查
    function handleMutations(mutationsList, observer) {
        if (isScrolling) {
            return;
        }

        if (wasNearBottom && targetNode) {
            // console.log("[ScrollFixer v1.4] Mutation detected while near bottom. Scheduling immediate rAF correction.");

            // 取消上一个可能未执行的帧和后置检查
            if (correctionFrameId) cancelAnimationFrame(correctionFrameId);
            clearTimeout(postCorrectionTimeoutId); // 清除上一个后置检查

            // 立即请求下一帧执行修正
            correctionFrameId = requestAnimationFrame(() => {
                correctionFrameId = null; // 清除 ID
                if (targetNode && document.contains(targetNode)) {
                    // console.log("[ScrollFixer v1.4] Performing immediate rAF scroll correction.");
                    scrollToBottom(targetNode, "mutation_rAF", 'auto');

                    // --- 新增:rAF 修正后,安排一个后置检查 ---
                    postCorrectionTimeoutId = setTimeout(() => {
                        postCorrectionTimeoutId = null; // 清除 ID
                        if (targetNode && document.contains(targetNode) && !isScrolling) {
                             // console.log("[ScrollFixer v1.4] Performing post-correction check.");
                             if (!isNearBottom(targetNode)) { // 如果此时因为图片加载等原因又不在底部了
                                 console.warn("[ScrollFixer v1.4] Post-correction check failed. Scrolling again.");
                                 scrollToBottom(targetNode, "post_check", 'auto'); // 再次滚动
                             } else {
                                 // console.log("[ScrollFixer v1.4] Post-correction check passed.");
                                 // 确保状态正确
                                 wasNearBottom = true;
                             }
                        }
                    }, POST_CORRECTION_CHECK_DELAY);
                    // --- --- ---

                } else {
                    // console.warn("[ScrollFixer v1.4] Target node lost before immediate rAF execution.");
                }
            });
        }
    }

    // 查找并观察消息列表容器
    function findAndObserve() {
        const targetSelector = SCROLL_CONTAINER_SELECTOR;
        let potentialNode = document.querySelector(targetSelector);

        if (observer && targetNode === potentialNode && document.contains(targetNode)) {
            return;
        }

        // Cleanup
        if (observer) { /* ... 清理逻辑 ... */ }
        targetNode = potentialNode;

        if (targetNode) {
            console.log("[ScrollFixer v1.4] Chat scroll container found:", targetNode);
            applyStyles();

            // 初始滚动检查 - 更保守
            setTimeout(() => {
                if (targetNode && document.contains(targetNode)) {
                    console.log("[ScrollFixer v1.4] Performing initial check...");
                    // 只有当明确检测到远离底部时,才执行初始滚动
                    if (isFarFromBottom(targetNode)) {
                        console.log("[ScrollFixer v1.4] Far from bottom on initial check. Scrolling down.");
                        scrollToBottom(targetNode, "initial_load", 'auto');
                        wasNearBottom = true;
                    } else {
                        console.log("[ScrollFixer v1.4] Already near bottom or content not fully loaded? Initial scroll skipped/deferred.");
                        // 确保状态与实际情况一致
                        wasNearBottom = isNearBottom(targetNode);
                    }
                    console.log(`[ScrollFixer v1.4] Initial state - near bottom: ${wasNearBottom}`);

                    // Setup listeners/observer
                    if (!observer) {
                        targetNode.addEventListener('scroll', handleScroll, { passive: true });
                        console.log("[ScrollFixer v1.4] Scroll listener added.");
                        observer = new MutationObserver(handleMutations);
                        const config = { childList: true, subtree: true };
                        observer.observe(targetNode, config);
                        console.log("[ScrollFixer v1.4] MutationObserver started.");
                    }
                } else {
                    console.log("[ScrollFixer v1.4] Target node disappeared before initial setup.");
                }
            }, INITIAL_SCROLL_DELAY);

        } else {
             console.log(`[ScrollFixer v1.4] Chat container not found, retrying...`);
             setTimeout(findAndObserve, CHECK_INTERVAL);
        }
    }

    // --- SPA Navigation Handling & Initial Start ---
    // (与 v1.3 类似,注意清理 postCorrectionTimeoutId)
     let currentUrl = location.href;
     const urlObserver = new MutationObserver(() => {
         if (location.href !== currentUrl) {
             console.log(`[ScrollFixer v1.4] URL changed. Re-initializing for ${location.href}`);
             currentUrl = location.href;
             // Cleanup state and timers
             if (observer) { observer.disconnect(); observer = null; }
             if (targetNode && typeof targetNode.removeEventListener === 'function') {
                  targetNode.removeEventListener('scroll', handleScroll);
             }
             if (correctionFrameId) { cancelAnimationFrame(correctionFrameId); correctionFrameId = null; }
             clearTimeout(postCorrectionTimeoutId); // 清理后置检查计时器
             targetNode = null;
             wasNearBottom = false;
             isScrolling = false;
             clearTimeout(scrollDebounceTimeout);
             // styleAdded = false; // GM_addStyle persists
             setTimeout(findAndObserve, 500);
         }
     });
     urlObserver.observe(document.body, { childList: true, subtree: false });

     setTimeout(findAndObserve, 750);

})();