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
}
}