From 674b6ab00daadcb1caefcffe93b30e17d0f83e81 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Thu, 8 May 2025 21:23:04 +0300 Subject: [PATCH] feat: add chat scroll debug and use alternative ways of forcing scroll in DOM! --- src/optionsGuiScheme.tsx | 4 ++ src/optionsStorage.ts | 1 + src/react/Chat.tsx | 64 +++++++++++++++++++++++++++- src/react/ChatProvider.tsx | 3 +- src/react/hooks/useScrollBehavior.ts | 59 ++++++++++++++++++------- 5 files changed, 112 insertions(+), 19 deletions(-) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index b9c44b29..d4be46f6 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -605,6 +605,10 @@ export const guiOptionsScheme: { debugResponseTimeIndicator: { text: 'Debug Input Lag', }, + }, + { + debugChatScroll: { + }, } ], 'export-import': [ diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index bc58d21c..ab164454 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -72,6 +72,7 @@ const defaultOptions = { preventBackgroundTimeoutKick: false, preventSleep: false, debugContro: false, + debugChatScroll: false, chatVanillaRestrictions: true, debugResponseTimeIndicator: false, // antiAliasing: false, diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 130f25a0..c2e59b07 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -41,6 +41,7 @@ type Props = { inputDisabled?: string placeholder?: string chatVanillaRestrictions?: boolean + debugChatScroll?: boolean } export const chatInputValueGlobal = proxy({ @@ -69,7 +70,8 @@ export default ({ allowSelection, inputDisabled, placeholder, - chatVanillaRestrictions + chatVanillaRestrictions, + debugChatScroll }: Props) => { const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]')) const [isInputFocused, setIsInputFocused] = useState(false) @@ -86,7 +88,16 @@ export default ({ const chatHistoryPos = useRef(sendHistoryRef.current.length) const inputCurrentlyEnteredValue = useRef('') - const { scrollToBottom } = useScrollBehavior(chatMessages, { messages, opened }) + const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened }) + const [rightNowAtBottom, setRightNowAtBottom] = useState(false) + + useEffect(() => { + if (!debugChatScroll) return + const interval = setInterval(() => { + setRightNowAtBottom(isAtBottom()) + }, 50) + return () => clearInterval(interval) + }, [debugChatScroll]) const setSendHistory = (newHistory: string[]) => { sendHistoryRef.current = newHistory @@ -252,6 +263,55 @@ export default ({ }} > {opacity &&
+ {debugChatScroll && ( +
+
+
+
+
+
+ )} {messages.map((m) => ( ))} diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 4acc9e79..5c499029 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -17,7 +17,7 @@ export default () => { const isChatActive = useIsModalActive('chat') const lastMessageId = useRef(0) const usingTouch = useSnapshot(miscUiState).currentTouch - const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions } = useSnapshot(options) + const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll } = useSnapshot(options) const isUsingMicrosoftAuth = useMemo(() => !!lastConnectOptions.value?.authenticatedAccount, []) const { forwardChat } = useSnapshot(viewerVersionState) const { viewerConnection } = useSnapshot(gameAdditionalState) @@ -48,6 +48,7 @@ export default () => { return { const openedWasAtBottom = useRef(true) // before new messages + const [currentlyAtBottom, setCurrentlyAtBottom] = useState(true) + const scrollTimeoutRef = useRef(null) const isAtBottom = () => { if (!elementRef.current) return true @@ -20,17 +22,30 @@ export const useScrollBehavior = ( return distanceFromBottom < 1 } - const scrollToBottom = () => { - if (elementRef.current) { - elementRef.current.scrollTop = elementRef.current.scrollHeight - setTimeout(() => { - if (!elementRef.current) return - elementRef.current.scrollTo({ - top: elementRef.current.scrollHeight, - behavior: 'instant' - }) - }, 0) + const scrollToBottom = (behavior: ScrollBehavior = 'instant') => { + if (!elementRef.current) return + + // Clear any existing scroll timeout + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) } + + const el = elementRef.current + + // Immediate scroll + el.scrollTop = el.scrollHeight + + // Double-check after a short delay to ensure we're really at the bottom + scrollTimeoutRef.current = setTimeout(() => { + if (!elementRef.current) return + const el = elementRef.current + el.scrollTo({ + top: el.scrollHeight, + behavior + }) + setCurrentlyAtBottom(true) + openedWasAtBottom.current = true + }, 5) } // Handle scroll position tracking @@ -39,18 +54,28 @@ export const useScrollBehavior = ( if (!element) return const handleScroll = () => { - openedWasAtBottom.current = isAtBottom() + const atBottom = isAtBottom() + openedWasAtBottom.current = atBottom + setCurrentlyAtBottom(atBottom) } element.addEventListener('scroll', handleScroll) - return () => element.removeEventListener('scroll', handleScroll) + return () => { + element.removeEventListener('scroll', handleScroll) + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) + } + } }, []) // Handle opened state changes useLayoutEffect(() => { if (opened) { - openedWasAtBottom.current = true - } else { + // Wait a frame before scrolling to ensure DOM has updated + requestAnimationFrame(() => { + scrollToBottom() + }) + } else if (elementRef.current) { scrollToBottom() } }, [opened]) @@ -64,6 +89,8 @@ export const useScrollBehavior = ( return { scrollToBottom, - isAtBottom + isAtBottom, + wasAtBottom: () => openedWasAtBottom.current, + currentlyAtBottom } }