Files
vexplor_dev/frontend/components/messenger/ChatPanel.tsx

235 lines
9.3 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useEffect, useRef, useState } from "react";
import { MessageSquare, Pencil, Check, X, ChevronsDown, Bell, BellOff } from "lucide-react";
import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger";
import { useAuth } from "@/hooks/useAuth";
import { useMessengerContext } from "@/contexts/MessengerContext";
import { MessageItem } from "./MessageItem";
import { MessageInput } from "./MessageInput";
import type { Room } from "@/hooks/useMessenger";
interface ChatPanelProps {
room: Room | null;
typingUsers: Map<string, string[]>;
emitTypingStart: (roomId: string) => void;
emitTypingStop: (roomId: string) => void;
onNewRoom: () => void;
}
const formatDateLabel = (dateStr: string) => {
const d = new Date(dateStr);
const today = new Date();
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);
if (d.toDateString() === today.toDateString()) return "오늘";
if (d.toDateString() === yesterday.toDateString()) return "어제";
return d.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", weekday: "short" });
};
export function ChatPanel({ room, typingUsers, emitTypingStart, emitTypingStop, onNewRoom }: ChatPanelProps) {
const { user } = useAuth();
const { selectedRoomId, isOpen, mutedRooms, toggleRoomMute } = useMessengerContext();
const { data: messages } = useMessages(selectedRoomId);
const markAsRead = useMarkAsRead();
const updateRoom = useUpdateRoom();
const scrollRef = useRef<HTMLDivElement>(null);
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
const [isAtBottom, setIsAtBottom] = useState(true);
const [isEditingName, setIsEditingName] = useState(false);
const [editName, setEditName] = useState("");
const editInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen && selectedRoomId) {
markAsRead.mutate(selectedRoomId);
}
}, [isOpen, selectedRoomId, messages?.length]);
// Re-attach scroll listener whenever room changes
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
useEffect(() => {
const el = scrollRef.current;
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
if (!el) return;
const onScroll = () => {
// In flex-col-reverse, scrollTop=0 means bottom (newest messages)
setIsAtBottom(Math.abs(el.scrollTop) < 60);
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
};
el.addEventListener("scroll", onScroll);
return () => el.removeEventListener("scroll", onScroll);
}, [room?.id]);
if (!room) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-3">
<MessageSquare className="h-10 w-10" />
<p className="text-sm"> </p>
<button
onClick={onNewRoom}
className="px-4 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
</button>
</div>
);
}
const roomTyping = selectedRoomId ? typingUsers.get(selectedRoomId) : undefined;
// messages is oldest-first; flex-col-reverse CSS pins scroll to bottom (newest visible)
const isFirstInGroup = (idx: number) => {
if (!messages) return true;
if (idx === 0) return true;
const prev = messages[idx - 1];
const curr = messages[idx];
if (prev.senderId !== curr.senderId || curr.isDeleted || prev.isDeleted) return true;
const gap = new Date(curr.createdAt).getTime() - new Date(prev.createdAt).getTime();
return gap > 5 * 60 * 1000;
};
const isLastInGroup = (idx: number) => {
if (!messages) return true;
if (idx === messages.length - 1) return true;
const curr = messages[idx];
const next = messages[idx + 1];
if (curr.senderId !== next.senderId || next.isDeleted || curr.isDeleted) return true;
const gap = new Date(next.createdAt).getTime() - new Date(curr.createdAt).getTime();
return gap > 5 * 60 * 1000;
};
const shouldShowDate = (idx: number) => {
if (!messages) return false;
if (idx === 0) return true;
const prev = new Date(messages[idx - 1].createdAt).toDateString();
const curr = new Date(messages[idx].createdAt).toDateString();
return prev !== curr;
};
// Compute display name based on room type
const displayName = (() => {
if (room.type === "dm") {
const other = room.participants.find((p) => p.userId !== user?.userId);
return other?.userName ?? room.name;
}
if (room.name) return room.name;
const others = room.participants.filter((p) => p.userId !== user?.userId);
if (others.length <= 2) {
return others.map((p) => p.userName).join(", ");
}
return `${others[0].userName}, ${others[1].userName}${others.length - 2}`;
})();
return (
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
<div className="flex-1 flex flex-col min-w-0 min-h-0 overflow-hidden relative">
{/* Header */}
<div className="border-b px-4 py-2 flex items-center gap-2">
{isEditingName ? (
<form
className="flex items-center gap-1 flex-1 min-w-0"
onSubmit={(e) => {
e.preventDefault();
const trimmed = editName.trim();
if (trimmed && trimmed !== room.name) {
updateRoom.mutate({ roomId: room.id, name: trimmed });
}
setIsEditingName(false);
}}
>
<input
ref={editInputRef}
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="font-semibold text-sm bg-transparent border-b border-primary outline-none flex-1 min-w-0"
autoFocus
/>
<button type="submit" className="p-0.5 hover:bg-muted rounded">
<Check className="h-3.5 w-3.5 text-primary" />
</button>
<button type="button" onClick={() => setIsEditingName(false)} className="p-0.5 hover:bg-muted rounded">
<X className="h-3.5 w-3.5 text-muted-foreground" />
</button>
</form>
) : (
<>
<h3 className="font-semibold text-sm truncate">{displayName}</h3>
{room.type !== "dm" && (
<button
onClick={() => { setEditName(room.name); setIsEditingName(true); }}
className="p-0.5 hover:bg-muted rounded shrink-0"
>
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
<span className="text-xs text-muted-foreground">
{room.participants.length}
</span>
<button
onClick={() => selectedRoomId && toggleRoomMute(selectedRoomId)}
className="p-0.5 hover:bg-muted rounded shrink-0 ml-auto"
title={selectedRoomId && mutedRooms.has(selectedRoomId) ? "알림 켜기" : "알림 끄기"}
>
{selectedRoomId && mutedRooms.has(selectedRoomId)
? <BellOff className="h-3.5 w-3.5 text-muted-foreground" />
: <Bell className="h-3.5 w-3.5 text-muted-foreground" />
}
</button>
</>
)}
</div>
{/* Messages — flex-col-reverse keeps scroll pinned to bottom (newest visible) */}
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto relative flex flex-col-reverse">
<div className="pb-2">
{/* Typing indicator at top of inner div = visually just above messages */}
<div className="px-4 h-5 flex items-center text-xs text-muted-foreground gap-1">
{roomTyping && roomTyping.length > 0 && (
<>
<span>{roomTyping.join(", ")} </span>
<span className="flex gap-0.5 items-center">
<span className="w-1 h-1 bg-muted-foreground rounded-full animate-bounce [animation-delay:0ms]" />
<span className="w-1 h-1 bg-muted-foreground rounded-full animate-bounce [animation-delay:150ms]" />
<span className="w-1 h-1 bg-muted-foreground rounded-full animate-bounce [animation-delay:300ms]" />
</span>
</>
)}
</div>
{messages?.map((msg, idx) => (
<div key={msg.id}>
{shouldShowDate(idx) && (
<div className="flex items-center gap-2 px-4 py-2">
<div className="flex-1 h-px bg-border" />
<span className="text-[10px] text-muted-foreground">
{formatDateLabel(msg.createdAt)}
</span>
<div className="flex-1 h-px bg-border" />
</div>
)}
<MessageItem
message={msg}
isOwn={msg.senderId === user?.userId}
showAvatar={isFirstInGroup(idx)}
isLastInGroup={isLastInGroup(idx)}
/>
</div>
))}
</div>
</div>
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
{!isAtBottom && (
<button
onClick={() => { const el = scrollRef.current; if (el) el.scrollTop = 0; }}
[RAPID-fix] 캡처 이미지 크기 수정: CSS 픽셀 기준 scale factor로 정확한 영역 크롭 [RAPID-fix] 캡처 속도/화질 개선 + 드래그 커서 4방향 화살표로 변경 - 오버레이 마운트 시점에 미리 캡처 시작 → mouseup 즉시 크롭 - scale: max(dpr, 2)로 화질 2배 향상 - 캡처 준비 중 wait 커서 표시 - 메신저 헤더 드래그 커서 cursor-grab → cursor-move (4방향 화살표) [RAPID-micro] 캡처 버튼 헤더 → 입력창 첨부파일 좌측으로 이동 [RAPID-micro] 채팅방 선택 시 스크롤 하단 이동 수정 [RAPID] 메신저 3가지 수정: 스크롤 버튼, DM 상대방 이름, 캡처 속도 개선 [RAPID-fix] 스크롤/캡처 3가지 수정 - 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤) - 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결) - 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처) [RAPID-fix] 메신저 3가지 수정 - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) [RAPID-micro] 채팅방 열 때 스크롤 점프 제거 [RAPID-fix] 캡처 오버레이 렌더 후 domToPng 시작으로 mousedown 딜레이 개선 [RAPID-fix] 캡처 프리캐싱: 메신저 열릴 때 백그라운드 domToPng → 버튼 클릭 즉시 오버레이 [RAPID-fix] 캡처 worker 추가: 리소스 fetch를 Web Worker로 오프로드 [RAPID-fix] 캡처 방식 변경: domToPng 제거 → getDisplayMedia (즉시 캡처, 프리캐싱 제거) [RAPID-micro] 화면 캡처 버튼 제거 (Cmd+V 붙여넣기로 대체)
2026-03-31 15:13:46 +09:00
className="absolute bottom-14 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1 bg-primary text-primary-foreground text-xs px-3 py-1.5 rounded-full shadow-md hover:bg-primary/90"
>
<ChevronsDown className="h-3.5 w-3.5" />
</button>
)}
{/* Input */}
<MessageInput
roomId={room.id}
onTypingStart={() => emitTypingStart(room.id)}
onTypingStop={() => emitTypingStop(room.id)}
/>
</div>
);
}