feat: message list implements virtual scrolling

This commit is contained in:
molvqingtai 2024-09-19 22:52:19 +08:00
parent 89e20a65db
commit c9388c744e
8 changed files with 45 additions and 29 deletions

View file

@ -69,6 +69,7 @@
"react-hook-form": "^7.51.0",
"react-markdown": "^9.0.1",
"react-use": "^17.5.0",
"react-virtuoso": "^4.10.4",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"remesh": "^4.2.2",

View file

@ -83,6 +83,9 @@ importers:
react-use:
specifier: ^17.5.0
version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-virtuoso:
specifier: ^4.10.4
version: 4.10.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
@ -4796,6 +4799,13 @@ packages:
react: '*'
react-dom: '*'
react-virtuoso@4.10.4:
resolution: {integrity: sha512-G/gprhTbK+lzMxoo/iStcZxVEGph/cIhc3WANEpt92RuMw+LiCZOmBfKoeoZOHlm/iyftTrDJhGaTCpxyucnkQ==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16 || >=17 || >= 18'
react-dom: '>=16 || >=17 || >= 18'
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@ -11333,6 +11343,11 @@ snapshots:
ts-easing: 0.2.0
tslib: 2.7.0
react-virtuoso@4.10.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react@18.3.1:
dependencies:
loose-envify: 1.4.0

View file

@ -24,10 +24,7 @@ const MessageItem: FC<MessageItemProps> = (props) => {
props.onHateChange?.(checked)
}
return (
<div
data-index={props.index}
className="box-border grid grid-cols-[auto_1fr] gap-x-2 px-4 [content-visibility:auto] first:pt-4 last:pb-4"
>
<div data-index={props.index} className="box-border grid grid-cols-[auto_1fr] gap-x-2 px-4 first:pt-4 last:pb-4">
<Avatar>
<AvatarImage src={props.data.userAvatar} alt="avatar" />
<AvatarFallback>{props.data.username.at(0)}</AvatarFallback>

View file

@ -1,20 +1,26 @@
import { type ReactElement } from 'react'
import { FC, useRef, type ReactElement } from 'react'
import React from 'react'
import { type MessageItemProps } from './MessageItem'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { Virtuoso } from 'react-virtuoso'
export interface MessageListProps {
children?: Array<ReactElement<MessageItemProps>>
}
// [&>div>div]:!block fix word-break: break-word;
const MessageList = React.forwardRef<HTMLDivElement, MessageListProps>(({ children }, ref) => {
const MessageList: FC<MessageListProps> = ({ children }) => {
const scrollParentRef = useRef<HTMLDivElement | null>(null)
return (
<ScrollArea ref={ref} className="[&>div>div]:!block">
{children}
<ScrollArea ref={scrollParentRef}>
<Virtuoso
followOutput={(isAtBottom: boolean) => (isAtBottom ? 'smooth' : 'auto')}
initialTopMostItemIndex={{ index: 'LAST', align: 'end' }}
data={children}
customScrollParent={scrollParentRef.current!}
itemContent={(_: any, item: ReactElement<MessageItemProps>) => item}
/>
</ScrollArea>
)
})
}
MessageList.displayName = 'MessageList'

View file

@ -17,9 +17,6 @@ const Main: FC = () => {
like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id),
hate: message.hateUsers.some((hateUser) => hateUser.userId === userInfo?.id)
}))
const messageListRef = useRef<HTMLDivElement>(null)
const isUpdate = useRef(false)
const handleLikeChange = (messageId: string) => {
send(roomDomain.command.SendLikeMessageCommand(messageId))
@ -29,20 +26,20 @@ const Main: FC = () => {
send(roomDomain.command.SendHateMessageCommand(messageId))
}
useEffect(() => {
const lastMessageRef = messageListRef.current?.querySelector('[data-index]:last-child')
const timerId = setTimeout(() => {
requestAnimationFrame(() => {
lastMessageRef?.scrollIntoView({ behavior: isUpdate.current ? 'smooth' : 'instant', block: 'end' })
isUpdate.current = true
})
}, 0)
// useEffect(() => {
// const lastMessageRef = messageListRef.current?.querySelector('[data-index]:last-child')
// const timerId = setTimeout(() => {
// requestAnimationFrame(() => {
// lastMessageRef?.scrollIntoView({ behavior: isUpdate.current ? 'smooth' : 'instant', block: 'end' })
// isUpdate.current = true
// })
// }, 0)
return () => clearTimeout(timerId)
}, [messageList.length])
// return () => clearTimeout(timerId)
// }, [messageList.length])
return (
<MessageList ref={messageListRef}>
<MessageList>
{messageList.map((message, index) => (
<MessageItem
key={message.id}

View file

@ -50,7 +50,7 @@ const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
<Avatar
tabIndex={disabled ? -1 : 1}
className={cn(
'group h-20 w-20 cursor-pointer border-4 border-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'group h-24 w-24 cursor-pointer border-4 border-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
{
'cursor-not-allowed': disabled,
'opacity-50': disabled

View file

@ -134,7 +134,7 @@ const ProfileForm = () => {
onClick={handleRandomAvatar}
>
<RefreshCcwIcon size={14} />
Random Avatar
Ugly Avatar
</Button>
</div>
</FormControl>

View file

@ -7,8 +7,8 @@ const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport className="size-full overscroll-none rounded-[inherit]">
<ScrollAreaPrimitive.Root className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport ref={ref} className="size-full overscroll-none rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />