2026-06-01 18:14:32 +07:00
|
|
|
import { useEffect, useRef } from "react";
|
2026-06-01 16:42:36 +07:00
|
|
|
import { ScrollArea } from "../../../shared/ui";
|
|
|
|
|
import type { MessageRecord } from "../../../shared/api/client";
|
|
|
|
|
import { MessageCard, MessageCardSkeleton } from "./MessageCard";
|
2026-05-14 21:01:57 +07:00
|
|
|
|
|
|
|
|
export interface MessageFeedProps {
|
|
|
|
|
messages: MessageRecord[];
|
2026-06-01 16:42:36 +07:00
|
|
|
onReanalyze: (id: string) => Promise<void>;
|
2026-05-16 19:17:34 +07:00
|
|
|
emptyText?: string;
|
2026-06-01 16:42:36 +07:00
|
|
|
loading?: boolean;
|
2026-06-01 18:14:32 +07:00
|
|
|
onLoadMore?: () => void;
|
|
|
|
|
hasMore?: boolean;
|
|
|
|
|
loadingMore?: boolean;
|
2026-05-14 21:01:57 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-01 18:14:32 +07:00
|
|
|
export function MessageFeed({ messages, onReanalyze, emptyText = "No messages found.", loading, onLoadMore, hasMore, loadingMore }: MessageFeedProps) {
|
|
|
|
|
// IntersectionObserver for infinite scroll — fires when sentinel becomes visible
|
|
|
|
|
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!onLoadMore || !hasMore) return;
|
|
|
|
|
const el = sentinelRef.current;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
|
(entries) => {
|
|
|
|
|
if (entries[0]?.isIntersecting) onLoadMore();
|
|
|
|
|
},
|
|
|
|
|
{ rootMargin: "400px" }, // preload before user reaches bottom
|
|
|
|
|
);
|
|
|
|
|
observer.observe(el);
|
|
|
|
|
return () => observer.disconnect();
|
|
|
|
|
}, [onLoadMore, hasMore]);
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{[1, 2, 3, 4, 5].map((i) => <MessageCardSkeleton key={i} />)}
|
|
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 21:01:57 +07:00
|
|
|
if (messages.length === 0) {
|
2026-05-16 19:17:34 +07:00
|
|
|
return <div className="rounded-2xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground">{emptyText}</div>;
|
2026-05-14 21:01:57 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-16 19:17:34 +07:00
|
|
|
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{messages.map((message) => (
|
|
|
|
|
<MessageCard key={message.id} message={message} onReanalyze={onReanalyze} />
|
|
|
|
|
))}
|
2026-06-01 18:14:32 +07:00
|
|
|
|
|
|
|
|
{/* Infinite-scroll sentinel */}
|
|
|
|
|
{hasMore && (
|
|
|
|
|
<div ref={sentinelRef} className="flex items-center justify-center py-4">
|
|
|
|
|
{loadingMore ? (
|
|
|
|
|
<MessageCardSkeleton />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-muted-foreground/40" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-16 19:17:34 +07:00
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
2026-05-14 21:01:57 +07:00
|
|
|
);
|
|
|
|
|
}
|