feat: add chat scroll debug and use alternative ways of forcing scroll in DOM!

This commit is contained in:
Vitaly Turovsky 2025-05-08 21:23:04 +03:00
commit 674b6ab00d
5 changed files with 112 additions and 19 deletions

View file

@ -605,6 +605,10 @@ export const guiOptionsScheme: {
debugResponseTimeIndicator: {
text: 'Debug Input Lag',
},
},
{
debugChatScroll: {
},
}
],
'export-import': [

View file

@ -72,6 +72,7 @@ const defaultOptions = {
preventBackgroundTimeoutKick: false,
preventSleep: false,
debugContro: false,
debugChatScroll: false,
chatVanillaRestrictions: true,
debugResponseTimeIndicator: false,
// antiAliasing: false,

View file

@ -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 && <div ref={chatMessages} className={`chat ${opened ? 'opened' : ''}`} id="chat-messages" style={{ opacity }}>
{debugChatScroll && (
<div
style={{
position: 'absolute',
top: 5,
left: 5,
display: 'flex',
gap: 4,
zIndex: 100,
}}
>
<div
title="Right now is at bottom (updated every 50ms)"
style={{
width: 12,
height: 12,
backgroundColor: rightNowAtBottom ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Currently at bottom"
style={{
width: 12,
height: 12,
backgroundColor: currentlyAtBottom ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Was at bottom"
style={{
width: 12,
height: 12,
backgroundColor: wasAtBottom() ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
<div
title="Chat opened"
style={{
width: 12,
height: 12,
backgroundColor: opened ? '#00ff00' : '#ff0000',
border: '1px solid #fff',
}}
/>
</div>
)}
{messages.map((m) => (
<MessageLine key={reactKeyForMessage(m)} message={m} />
))}

View file

@ -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 <Chat
chatVanillaRestrictions={chatVanillaRestrictions}
debugChatScroll={debugChatScroll}
allowSelection={chatSelect}
usingTouch={!!usingTouch}
opacity={(isChatActive ? chatOpacityOpened : chatOpacity) / 100}

View file

@ -1,4 +1,4 @@
import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'
import { RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { pixelartIcons } from '../PixelartIcon'
export const useScrollBehavior = (
@ -12,6 +12,8 @@ export const useScrollBehavior = (
}
) => {
const openedWasAtBottom = useRef(true) // before new messages
const [currentlyAtBottom, setCurrentlyAtBottom] = useState(true)
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(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
}
}