Skip to content

Commit

Permalink
fix: fix all known issues with chat auto scroll!
Browse files Browse the repository at this point in the history
  • Loading branch information
zardoy committed Feb 18, 2025
1 parent 5e0ece8 commit 9a8451f
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 105 deletions.
10 changes: 7 additions & 3 deletions src/mineflayer/java-tester/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const writeCmd = (cmd: string) => {
bot.chat(cmd)
}

let msg = 0
const LIMIT_MSG = 100
export const javaServerTester = {
itemCustomLore () {
const cmd = customStickNbt({
Expand Down Expand Up @@ -51,14 +53,16 @@ export const javaServerTester = {
})
writeCmd(cmd)
},

spamChat () {
for (let i = 0; i < 100; i++) {
for (let i = msg; i < msg + LIMIT_MSG; i++) {
bot.chat('Hello, world, ' + i)
}
msg += LIMIT_MSG
},
spamChatComplexMessage () {
for (let i = 0; i < 100; i++) {
bot.chat('/tell @a ')
for (let i = msg; i < msg + LIMIT_MSG; i++) {
bot.chat('/tell @a ' + i)
}
}
}
3 changes: 2 additions & 1 deletion src/react/Chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ div.chat-wrapper {
transform: none;
top: 100%;
padding-left: calc(env(safe-area-inset-left) / 2);
margin-top: 20px;
margin-top: 14px;
margin-left: 20px;
/* input height */
}

Expand Down
110 changes: 31 additions & 79 deletions src/react/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import './Chat.css'
import { isIos, reactKeyForMessage } from './utils'
import Button from './Button'
import { pixelartIcons } from './PixelartIcon'
import { useScrollBehavior } from './hooks/useScrollBehavior'

export type Message = {
parts: MessageFormatPart[],
Expand Down Expand Up @@ -77,22 +78,10 @@ export default ({

const chatInput = useRef<HTMLInputElement>(null!)
const chatMessages = useRef<HTMLDivElement>(null)
const openedChatWasAtBottom = useRef(false)
const wasAtBottomBeforeOpen = useRef(false)
const chatHistoryPos = useRef(sendHistoryRef.current.length)
const inputCurrentlyEnteredValue = useRef('')

const isAtBottom = () => {
if (!chatMessages.current) return true
const { scrollTop, scrollHeight, clientHeight } = chatMessages.current
return Math.abs(scrollHeight - clientHeight - scrollTop) < 1
}

const scrollToBottom = () => {
if (chatMessages.current) {
chatMessages.current.scrollTop = chatMessages.current.scrollHeight
}
}
const { scrollToBottom } = useScrollBehavior(chatMessages, { messages, opened })

const setSendHistory = (newHistory: string[]) => {
sendHistoryRef.current = newHistory
Expand All @@ -117,6 +106,11 @@ export default ({
}, 0)
}

const auxInputFocus = (fireKey: string) => {
chatInput.current.focus()
chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true }))
}

useEffect(() => {
// todo focus input on any keypress except tab
}, [])
Expand All @@ -133,11 +127,6 @@ export default ({
if (!usingTouch) {
chatInput.current.focus()
}
// Check if was at bottom before opening
wasAtBottomBeforeOpen.current = isAtBottom()
if (wasAtBottomBeforeOpen.current) {
scrollToBottom()
}
const unsubscribe = subscribe(chatInputValueGlobal, () => {
if (!chatInputValueGlobal.value) return
updateInputValue(chatInputValueGlobal.value)
Expand All @@ -146,9 +135,6 @@ export default ({
})
return unsubscribe
}
if (!opened && chatMessages.current) {
scrollToBottom()
}
}, [opened])

useMemo(() => {
Expand All @@ -158,50 +144,23 @@ export default ({
}
}, [opened])

useEffect(() => {
if ((!opened || (opened && openedChatWasAtBottom.current)) && chatMessages.current) {
openedChatWasAtBottom.current = false
// stay at bottom on messages changes only if we were at bottom
if (isAtBottom()) {
scrollToBottom()
}
}
}, [messages])

useMemo(() => {
if ((opened && chatMessages.current)) {
openedChatWasAtBottom.current = isAtBottom()
const onMainInputChange = () => {
const completeValue = getCompleteValue()
setCompletePadText(completeValue === '/' ? '' : completeValue)
if (completeRequestValue.current === completeValue) {
updateFilteredCompleteItems(completionItemsSource)
return
}
}, [messages])

// Add scroll handler to track scroll position
useEffect(() => {
const messagesEl = chatMessages.current
if (!messagesEl) return

const handleScroll = () => {
openedChatWasAtBottom.current = isAtBottom()
if (completeValue.startsWith('/')) {
void fetchCompletions(true)
} else {
resetCompletionItems()
}

messagesEl.addEventListener('scroll', handleScroll)
return () => messagesEl.removeEventListener('scroll', handleScroll)
}, [])

const auxInputFocus = (fireKey: string) => {
chatInput.current.focus()
chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true }))
}

const getDefaultCompleteValue = () => {
const raw = chatInput.current.value
return raw.slice(0, chatInput.current.selectionEnd ?? raw.length)
}
const getCompleteValue = (value = getDefaultCompleteValue()) => {
const valueParts = value.split(' ')
const lastLength = valueParts.at(-1)!.length
const completeValue = lastLength ? value.slice(0, -lastLength) : value
if (valueParts.length === 1 && value.startsWith('/')) return '/'
return completeValue
completeRequestValue.current = completeValue
// if (completeValue === '/') {
// void fetchCompletions(true)
// }
}

const fetchCompletions = async (implicit: boolean, inputValue = chatInput.current.value) => {
Expand All @@ -224,23 +183,16 @@ export default ({
setCompletionItems(newCompleteItems)
}

const onMainInputChange = () => {
const completeValue = getCompleteValue()
setCompletePadText(completeValue === '/' ? '' : completeValue)
if (completeRequestValue.current === completeValue) {
updateFilteredCompleteItems(completionItemsSource)
return
}

if (completeValue.startsWith('/')) {
void fetchCompletions(true)
} else {
resetCompletionItems()
}
completeRequestValue.current = completeValue
// if (completeValue === '/') {
// void fetchCompletions(true)
// }
const getDefaultCompleteValue = () => {
const raw = chatInput.current.value
return raw.slice(0, chatInput.current.selectionEnd ?? raw.length)
}
const getCompleteValue = (value = getDefaultCompleteValue()) => {
const valueParts = value.split(' ')
const lastLength = valueParts.at(-1)!.length
const completeValue = lastLength ? value.slice(0, -lastLength) : value
if (valueParts.length === 1 && value.startsWith('/')) return '/'
return completeValue
}

return (
Expand Down
32 changes: 10 additions & 22 deletions src/react/hooks/useScrollBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RefObject, useEffect, useRef } from 'react'
import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'

export const useScrollBehavior = (
elementRef: RefObject<HTMLElement>,
Expand All @@ -10,13 +10,14 @@ export const useScrollBehavior = (
opened?: boolean
}
) => {
const wasAtBottomBeforeOpen = useRef(false)
const openedWasAtBottom = useRef(false)
const openedWasAtBottom = useRef(true) // before new messages
window.openedWasAtBottom = openedWasAtBottom

Check failure on line 14 in src/react/hooks/useScrollBehavior.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Property 'openedWasAtBottom' does not exist on type 'Window & typeof globalThis'.

const isAtBottom = () => {
if (!elementRef.current) return true
const { scrollTop, scrollHeight, clientHeight } = elementRef.current
return Math.abs(scrollHeight - clientHeight - scrollTop) < 1
const distanceFromBottom = Math.abs(scrollHeight - clientHeight - scrollTop)
return distanceFromBottom < 1
}

const scrollToBottom = () => {
Expand All @@ -39,31 +40,18 @@ export const useScrollBehavior = (
}, [])

// Handle opened state changes
useEffect(() => {
useLayoutEffect(() => {
if (opened) {
wasAtBottomBeforeOpen.current = isAtBottom()
if (wasAtBottomBeforeOpen.current) {
scrollToBottom()
}
} else if (elementRef.current) {
openedWasAtBottom.current = true
} else {
scrollToBottom()
}
}, [opened])

// Handle messages changes
useEffect(() => {
useLayoutEffect(() => {
if ((!opened || (opened && openedWasAtBottom.current)) && elementRef.current) {
openedWasAtBottom.current = false
if (isAtBottom()) {
scrollToBottom()
}
}
}, [messages])

// Update bottom state when messages change
useEffect(() => {
if (opened && elementRef.current) {
openedWasAtBottom.current = isAtBottom()
scrollToBottom()
}
}, [messages])

Expand Down

0 comments on commit 9a8451f

Please sign in to comment.