feat: message list implements virtual scrolling
This commit is contained in:
parent
89e20a65db
commit
c9388c744e
8 changed files with 45 additions and 29 deletions
|
@ -69,6 +69,7 @@
|
||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-use": "^17.5.0",
|
"react-use": "^17.5.0",
|
||||||
|
"react-virtuoso": "^4.10.4",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remesh": "^4.2.2",
|
"remesh": "^4.2.2",
|
||||||
|
|
|
@ -83,6 +83,9 @@ importers:
|
||||||
react-use:
|
react-use:
|
||||||
specifier: ^17.5.0
|
specifier: ^17.5.0
|
||||||
version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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:
|
remark-breaks:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
@ -4796,6 +4799,13 @@ packages:
|
||||||
react: '*'
|
react: '*'
|
||||||
react-dom: '*'
|
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:
|
react@18.3.1:
|
||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -11333,6 +11343,11 @@ snapshots:
|
||||||
ts-easing: 0.2.0
|
ts-easing: 0.2.0
|
||||||
tslib: 2.7.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:
|
react@18.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|
|
@ -24,10 +24,7 @@ const MessageItem: FC<MessageItemProps> = (props) => {
|
||||||
props.onHateChange?.(checked)
|
props.onHateChange?.(checked)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-index={props.index} className="box-border grid grid-cols-[auto_1fr] gap-x-2 px-4 first:pt-4 last:pb-4">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={props.data.userAvatar} alt="avatar" />
|
<AvatarImage src={props.data.userAvatar} alt="avatar" />
|
||||||
<AvatarFallback>{props.data.username.at(0)}</AvatarFallback>
|
<AvatarFallback>{props.data.username.at(0)}</AvatarFallback>
|
||||||
|
|
|
@ -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 { type MessageItemProps } from './MessageItem'
|
||||||
import { ScrollArea } from '@/components/ui/ScrollArea'
|
import { ScrollArea } from '@/components/ui/ScrollArea'
|
||||||
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
|
|
||||||
export interface MessageListProps {
|
export interface MessageListProps {
|
||||||
children?: Array<ReactElement<MessageItemProps>>
|
children?: Array<ReactElement<MessageItemProps>>
|
||||||
}
|
}
|
||||||
// [&>div>div]:!block fix word-break: break-word;
|
const MessageList: FC<MessageListProps> = ({ children }) => {
|
||||||
const MessageList = React.forwardRef<HTMLDivElement, MessageListProps>(({ children }, ref) => {
|
const scrollParentRef = useRef<HTMLDivElement | null>(null)
|
||||||
return (
|
return (
|
||||||
<ScrollArea ref={ref} className="[&>div>div]:!block">
|
<ScrollArea ref={scrollParentRef}>
|
||||||
{children}
|
<Virtuoso
|
||||||
|
followOutput={(isAtBottom: boolean) => (isAtBottom ? 'smooth' : 'auto')}
|
||||||
|
initialTopMostItemIndex={{ index: 'LAST', align: 'end' }}
|
||||||
|
data={children}
|
||||||
|
customScrollParent={scrollParentRef.current!}
|
||||||
|
itemContent={(_: any, item: ReactElement<MessageItemProps>) => item}
|
||||||
|
/>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
MessageList.displayName = 'MessageList'
|
MessageList.displayName = 'MessageList'
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,6 @@ const Main: FC = () => {
|
||||||
like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id),
|
like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id),
|
||||||
hate: message.hateUsers.some((hateUser) => hateUser.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) => {
|
const handleLikeChange = (messageId: string) => {
|
||||||
send(roomDomain.command.SendLikeMessageCommand(messageId))
|
send(roomDomain.command.SendLikeMessageCommand(messageId))
|
||||||
|
@ -29,20 +26,20 @@ const Main: FC = () => {
|
||||||
send(roomDomain.command.SendHateMessageCommand(messageId))
|
send(roomDomain.command.SendHateMessageCommand(messageId))
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const lastMessageRef = messageListRef.current?.querySelector('[data-index]:last-child')
|
// const lastMessageRef = messageListRef.current?.querySelector('[data-index]:last-child')
|
||||||
const timerId = setTimeout(() => {
|
// const timerId = setTimeout(() => {
|
||||||
requestAnimationFrame(() => {
|
// requestAnimationFrame(() => {
|
||||||
lastMessageRef?.scrollIntoView({ behavior: isUpdate.current ? 'smooth' : 'instant', block: 'end' })
|
// lastMessageRef?.scrollIntoView({ behavior: isUpdate.current ? 'smooth' : 'instant', block: 'end' })
|
||||||
isUpdate.current = true
|
// isUpdate.current = true
|
||||||
})
|
// })
|
||||||
}, 0)
|
// }, 0)
|
||||||
|
|
||||||
return () => clearTimeout(timerId)
|
// return () => clearTimeout(timerId)
|
||||||
}, [messageList.length])
|
// }, [messageList.length])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageList ref={messageListRef}>
|
<MessageList>
|
||||||
{messageList.map((message, index) => (
|
{messageList.map((message, index) => (
|
||||||
<MessageItem
|
<MessageItem
|
||||||
key={message.id}
|
key={message.id}
|
||||||
|
|
|
@ -50,7 +50,7 @@ const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
|
||||||
<Avatar
|
<Avatar
|
||||||
tabIndex={disabled ? -1 : 1}
|
tabIndex={disabled ? -1 : 1}
|
||||||
className={cn(
|
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,
|
'cursor-not-allowed': disabled,
|
||||||
'opacity-50': disabled
|
'opacity-50': disabled
|
||||||
|
|
|
@ -134,7 +134,7 @@ const ProfileForm = () => {
|
||||||
onClick={handleRandomAvatar}
|
onClick={handleRandomAvatar}
|
||||||
>
|
>
|
||||||
<RefreshCcwIcon size={14} />
|
<RefreshCcwIcon size={14} />
|
||||||
Random Avatar
|
Ugly Avatar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
@ -7,8 +7,8 @@ const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
|
<ScrollAreaPrimitive.Root className={cn('relative overflow-hidden', className)} {...props}>
|
||||||
<ScrollAreaPrimitive.Viewport className="size-full overscroll-none rounded-[inherit]">
|
<ScrollAreaPrimitive.Viewport ref={ref} className="size-full overscroll-none rounded-[inherit]">
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
|
|
Loading…
Reference in a new issue