import React, { useEffect, useRef, useCallback, useState, useMemo } from 'react'; import { Message } from './Message'; import { useChatStore } from '../../stores/chatStore'; import { LoadingSpinner } from '../ui/LoadingSpinner'; import type { MessageWithUser } from '@opencord/shared'; const EMPTY_MESSAGES: MessageWithUser[] = []; interface MessageListProps { channelId: string; } function isSameGroup(prev: MessageWithUser, curr: MessageWithUser): boolean { if (prev.userId !== curr.userId) return false; const timeDiff = curr.createdAt - prev.createdAt; return timeDiff < 5 * 60 * 1000; // 5 minutes } function formatDateDivider(timestamp: number): string { const date = new Date(timestamp); return date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } function shouldShowDateDivider(prev: MessageWithUser | undefined, curr: MessageWithUser): boolean { if (!prev) return true; const prevDate = new Date(prev.createdAt).toDateString(); const currDate = new Date(curr.createdAt).toDateString(); return prevDate !== currDate; } export function MessageList({ channelId }: MessageListProps) { const messages = useChatStore((s) => s.messages.get(channelId)) ?? EMPTY_MESSAGES; const loadMessages = useChatStore((s) => s.loadMessages); const loadMoreMessages = useChatStore((s) => s.loadMoreMessages); const isLoading = useChatStore((s) => s.isLoading); const hasMore = useChatStore((s) => s.hasMore.get(channelId) ?? true); const bottomRef = useRef(null); const containerRef = useRef(null); const [isNearBottom, setIsNearBottom] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const prevMessagesLength = useRef(0); useEffect(() => { loadMessages(channelId); }, [channelId, loadMessages]); // Auto-scroll to bottom on new messages (if near bottom) useEffect(() => { if (messages.length > prevMessagesLength.current && isNearBottom) { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); } prevMessagesLength.current = messages.length; }, [messages.length, isNearBottom]); // Scroll to bottom on initial load useEffect(() => { if (messages.length > 0 && prevMessagesLength.current === 0) { bottomRef.current?.scrollIntoView(); } }, [messages.length]); const handleScroll = useCallback(async () => { const container = containerRef.current; if (!container) return; // Check if near bottom const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; setIsNearBottom(distanceFromBottom < 100); // Load more when scrolled to top if (container.scrollTop < 50 && hasMore && !isLoadingMore) { setIsLoadingMore(true); const prevScrollHeight = container.scrollHeight; const loaded = await loadMoreMessages(channelId); if (loaded) { // Maintain scroll position requestAnimationFrame(() => { container.scrollTop = container.scrollHeight - prevScrollHeight; }); } setIsLoadingMore(false); } }, [channelId, hasMore, isLoadingMore, loadMoreMessages]); if (isLoading && messages.length === 0) { return (
); } return (
{isLoadingMore && (
)} {!hasMore && (

Welcome to the channel!

This is the start of the conversation.

)}
{messages.map((msg, i) => { const prevMsg = messages[i - 1]; const showDate = shouldShowDateDivider(prevMsg, msg); const isFirstInGroup = !prevMsg || showDate || !isSameGroup(prevMsg, msg); return ( {showDate && (
{formatDateDivider(msg.createdAt)}
)} ); })}
); }