[RAPID-fix] 채팅방 스크롤 점프 제거: useEffect → useLayoutEffect (페인트 전 처리)
[RAPID-fix] 스크롤 sentinel 방식으로 교체: scrollIntoView useLayoutEffect (페인트 전 보장) [RAPID-fix] 스크롤 근본 원인 수정: isOpen deps 추가로 메신저 열릴 때마다 하단 스크롤 [RAPID-fix] 스크롤 ResizeObserver 추가: 이미지 로드 후 높이 변화 감지해 자동 하단 스크롤
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { MessageSquare, Pencil, Check, X, ChevronsDown } from "lucide-react";
|
||||
import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
@@ -16,14 +16,14 @@ interface ChatPanelProps {
|
||||
|
||||
export function ChatPanel({ room }: ChatPanelProps) {
|
||||
const { user } = useAuth();
|
||||
const { selectedRoomId } = useMessengerContext();
|
||||
const { selectedRoomId, isOpen } = useMessengerContext();
|
||||
const { data: messages } = useMessages(selectedRoomId);
|
||||
const markAsRead = useMarkAsRead();
|
||||
const updateRoom = useUpdateRoom();
|
||||
const { emitTypingStart, emitTypingStop, typingUsers } = useMessengerSocket();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [scrollReady, setScrollReady] = useState(false);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editName, setEditName] = useState("");
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -36,34 +36,41 @@ export function ChatPanel({ room }: ChatPanelProps) {
|
||||
|
||||
const lastMessageId = messages?.[messages.length - 1]?.id;
|
||||
|
||||
// Hide messages until scrolled to bottom (prevents visible jump on room open)
|
||||
useEffect(() => {
|
||||
setScrollReady(false);
|
||||
}, [selectedRoomId]);
|
||||
// Scroll to bottom: sentinel scrollIntoView before paint (no visible jump)
|
||||
// Scroll to bottom on room open / new message
|
||||
useLayoutEffect(() => {
|
||||
if (isOpen) bottomRef.current?.scrollIntoView();
|
||||
}, [selectedRoomId, lastMessageId, isOpen]);
|
||||
|
||||
// Scroll to bottom when messages load or room changes
|
||||
// Two-pass: immediate rAF + 600ms delayed (for async image loads)
|
||||
// ResizeObserver: re-scroll whenever content height changes (images loading, etc.)
|
||||
const shouldAutoScrollRef = useRef(true);
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
setScrollReady(true);
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const inner = el.firstElementChild as HTMLElement | null;
|
||||
if (!inner) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (shouldAutoScrollRef.current) {
|
||||
bottomRef.current?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
const t = setTimeout(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}, 600);
|
||||
return () => clearTimeout(t);
|
||||
}, [lastMessageId, selectedRoomId]);
|
||||
ro.observe(inner);
|
||||
return () => ro.disconnect();
|
||||
}, [selectedRoomId]);
|
||||
|
||||
// Track whether user has scrolled up (disable auto-scroll while reading old messages)
|
||||
useEffect(() => {
|
||||
shouldAutoScrollRef.current = true;
|
||||
}, [selectedRoomId, lastMessageId]);
|
||||
|
||||
// Re-attach scroll listener whenever room changes (scrollRef mounts after room is set)
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const onScroll = () => {
|
||||
setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 60);
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
|
||||
setIsAtBottom(atBottom);
|
||||
shouldAutoScrollRef.current = atBottom;
|
||||
};
|
||||
el.addEventListener("scroll", onScroll);
|
||||
return () => el.removeEventListener("scroll", onScroll);
|
||||
@@ -170,7 +177,7 @@ export function ChatPanel({ room }: ChatPanelProps) {
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div ref={scrollRef} className={`flex-1 min-h-0 overflow-y-auto relative${scrollReady ? "" : " invisible"}`}>
|
||||
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto relative">
|
||||
<div className="pt-2">
|
||||
{messages?.map((msg, idx) => (
|
||||
<div key={msg.id}>
|
||||
@@ -199,6 +206,7 @@ export function ChatPanel({ room }: ChatPanelProps) {
|
||||
<div className="px-4 h-5 flex items-center text-xs text-muted-foreground">
|
||||
{roomTyping && roomTyping.length > 0 ? `${roomTyping.join(", ")}님이 입력 중...` : ""}
|
||||
</div>
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user