Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/apps/web/src/features/chat/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export type {
} from './websocket.type';
export { useMessages } from './use-messages';
export { useWebSocketClient } from './use-websocket-client';
export { useChatAutoScroll } from './use-chat-autoscroll';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useRef, useState } from 'react';
import { WebSocketResponsePayload } from './websocket.type';
import { useToast } from '@workspace/ui/hooks/Toast/use-toast';

export const useChatAutoScroll = (messages: WebSocketResponsePayload[]) => {
const bottomRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [prevMessageCount, setPrevMessageCount] = useState(0);
const [isUserScrollingUp, setIsUserScrollingUp] = useState(false);
const [newMessageCount, setNewMessageCount] = useState(0);
const { toast, dismiss } = useToast();

useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;

const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = container;
const isAtBottom = scrollHeight - scrollTop <= clientHeight + 100;

setIsUserScrollingUp(!isAtBottom);
if (isAtBottom && newMessageCount > 0) {
setNewMessageCount(0);
dismiss('new-message');
}
};

container.addEventListener('scroll', handleScroll);

if (messages.length > prevMessageCount) {
if (!isUserScrollingUp) {
setTimeout(() => {
bottomRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}, 0);
} else {
setNewMessageCount((prev) => prev + 1);
toast({
title: '새 메시지가 있습니다',
description: `${newMessageCount + 1}개의 새 메시지가 도착했습니다.`,
});
}
setPrevMessageCount(messages.length);
}

return () => container.removeEventListener('scroll', handleScroll);
}, [
messages,
isUserScrollingUp,
newMessageCount,
toast,
dismiss,
prevMessageCount,
]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 분리가 가능하다면 useEffect를 분리하여 의존성을 줄이는 것도 좋을 것 같습니다.
예를 들면 scoll 감지 / toast 기능 이런식으로 분리는 어려울까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 근데 제가 알기로 useEffect는 state가 들어가게 되었을 경우 재렌더링이 일어나는 것으로 알고 있습니다.
그런데 저희가 isUserScrollingUp, newMessageCount, prevMessageCount이렇게 3가지 state를 업데이트 하고 있는데 useEffect를 나뉘게 된다면 의존성을 관리하기에는 용이하겠지만 불필요한 재렌더링이 일어날 수 있지않을까요?


return { bottomRef, containerRef };
};
9 changes: 7 additions & 2 deletions src/frontend/apps/web/src/features/chat/ui/chat-content.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import ContentText from './content-text';
import ContentAvatar from './content-avatar';

import { MessageSquareText } from 'lucide-react';
import type { WebSocketResponsePayload } from '../model';
import { useChatAutoScroll, type WebSocketResponsePayload } from '../model';
import { processMessages } from '../lib';
import { Toaster } from '@workspace/ui/components';

export type ChatContentProps = {
type?: 'default' | 'live';
Expand All @@ -21,6 +21,8 @@ const ChatContent = ({
avatarUrls,
setIsThreadOpen,
}: ChatContentWithAvatarsProps) => {
const { bottomRef, containerRef } = useChatAutoScroll(messages);

if (!messages || messages.length === 0) return null;
console.log('🔗 ChatContent:', { messages });

Expand All @@ -32,6 +34,7 @@ const ChatContent = ({
return (
<>
<div
ref={containerRef}
className={`flex flex-col w-full pb-2 overflow-auto h-auto ${backgroundColor}`}
>
{processedMessages.map((messageData, index) => (
Expand Down Expand Up @@ -61,7 +64,9 @@ const ChatContent = ({
</div>
</div>
))}
<div ref={bottomRef} />
</div>
<Toaster />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ const ChatSectionContent = () => {

return (
<>
<div className="flex flex-col w-full h-full">
<div className="flex flex-1 flex-col w-full h-full overflow-y-auto">
<div className="flex flex-col w-full h-full relative">
<div className="flex flex-1 flex-col w-full min-h-0 overflow-y-auto pb-[64px]">
<ChatContent
setIsThreadOpen={setIsThreadOpen}
messages={messages}
/>
</div>
<div className="p-4">
<div className="pr-4 pl-4 pb-4 flex-shrink-0 sticky bottom-0 bg-white shadow-md">
<ChatTextarea onSend={handleSendMessage} />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/apps/web/src/features/chat/ui/header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Header = ({ children }: { children: React.ReactNode }) => {
return (
<header className="h-[54px] w-full flex items-center justify-between px-4 bg-white border-b border-gray-200">
<header className="h-[54px] flex-shrink-0 w-full flex items-center justify-between px-4 bg-white border-b border-gray-200">
{children}
</header>
);
Expand Down
13 changes: 12 additions & 1 deletion src/frontend/packages/ui/src/components/Toast/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction } from './toast';
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
} from './toast';
export { Toaster } from './toaster';
Loading