Private
Public Access
Fix chat view not landing on the latest message after switching rooms (#50)
MessageList auto-scrolled to the bottom in a useEffect keyed on messages.length, but .message-image has no reserved width/height (only max-width/max-height caps) -- unlike UserAvatar and LinkPreviewCard's thumbnail, which both reserve fixed pixel dimensions. If a message near the bottom of a room's history has an image attachment, that scroll ran before the image loaded; the image then grew the container a moment later, leaving the view scrolled short of the true bottom until the user scrolled down manually. Now tracks whether the view is pinned to the bottom (via a scroll listener) and re-runs the scroll whenever any image inside the list finishes loading, but only while still pinned -- a late-loading image in history you've deliberately scrolled up to read won't yank you back down. A single capture-phase 'load' listener on the container catches every image (load doesn't bubble, but capture-phase listeners on an ancestor still see it) without wiring an onLoad prop through each one. Verified with a direct A/B comparison against the pre-fix code: same scrolled-away state, same synthetic image load event -- old code never calls scrollIntoView, new code does and lands back at the bottom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,13 @@ interface MessageListProps {
|
||||
|
||||
export function MessageList({ roomId, messages, members, myRooms, onEdit, onReact }: MessageListProps) {
|
||||
const { user } = useAuth()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
// Whether the view should be pinned to the latest message -- true right
|
||||
// after a room switch/new message, flipped off if the user deliberately
|
||||
// scrolls away from the bottom. Read by the image-load handler below so a
|
||||
// late-loading image doesn't yank someone back down mid-scrollback.
|
||||
const pinnedToBottomRef = useRef(true)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [draft, setDraft] = useState('')
|
||||
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null)
|
||||
@@ -86,8 +92,35 @@ export function MessageList({ roomId, messages, members, myRooms, onEdit, onReac
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
pinnedToBottomRef.current = true
|
||||
bottomRef.current?.scrollIntoView({ block: 'end' })
|
||||
}, [messages.length])
|
||||
}, [roomId, messages.length])
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
function handleScroll() {
|
||||
if (!container) return
|
||||
// Within 48px of the true bottom counts as "at the bottom" -- an
|
||||
// exact-equality check would drop pinning from sub-pixel scroll
|
||||
// rounding alone.
|
||||
pinnedToBottomRef.current =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight < 48
|
||||
}
|
||||
// `load` doesn't bubble, but a capture-phase listener on an ancestor
|
||||
// still sees it fire on the way down -- lets one listener catch every
|
||||
// image in the list (message attachments and link-preview thumbnails
|
||||
// alike) without wiring an onLoad prop through each of them.
|
||||
function handleContentGrow() {
|
||||
if (pinnedToBottomRef.current) bottomRef.current?.scrollIntoView({ block: 'end' })
|
||||
}
|
||||
container.addEventListener('scroll', handleScroll, { passive: true })
|
||||
container.addEventListener('load', handleContentGrow, true)
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
container.removeEventListener('load', handleContentGrow, true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function startEdit(msg: Message | ChatMessageEnvelope) {
|
||||
setEditingId(msg.id)
|
||||
@@ -101,7 +134,7 @@ export function MessageList({ roomId, messages, members, myRooms, onEdit, onReac
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message-list">
|
||||
<div className="message-list" ref={containerRef}>
|
||||
{messages.map((msg, i) => {
|
||||
const mine = msg.user_id === user?.id
|
||||
const prev = messages[i - 1]
|
||||
|
||||
Reference in New Issue
Block a user