import { useEffect, useMemo, useRef } from "react"; import { useSearchParams } from "react-router-dom"; import { useI18n } from "../../i18n"; import type { PostScope } from "../../types/post"; import { FilterChips } from "./FilterChips"; import { MessageBubble } from "./MessageBubble"; import { useGroupedByDay } from "./hooks/useGroupedByDay"; import { usePostStream } from "./hooks/usePostStream"; export type MessageStreamProps = { scope: PostScope; }; export function MessageStream({ scope }: MessageStreamProps) { const { t, lang } = useI18n(); const [sp, setSp] = useSearchParams(); const type = sp.get("type") || "all"; const language = sp.get("language") || ""; const params = useMemo( () => ({ scope, type, language, lang }), [scope, type, language, lang], ); const { items, isLoading, error, hasMore, loadMore, reset } = usePostStream(params); const groups = useGroupedByDay(items, lang); const retryLabel = lang === "zh-TW" ? "重試" : lang === "zh-CN" ? "重试" : "Retry"; const sentinelRef = useRef(null); const hasMoreRef = useRef(hasMore); const isLoadingRef = useRef(isLoading); useEffect(() => { hasMoreRef.current = hasMore; }, [hasMore]); useEffect(() => { isLoadingRef.current = isLoading; }, [isLoading]); useEffect(() => { const el = sentinelRef.current; if (!el) return; const io = new IntersectionObserver( (entries) => { for (const entry of entries) { if ( entry.isIntersecting && hasMoreRef.current && !isLoadingRef.current ) { loadMore(); } } }, { rootMargin: "200px" }, ); io.observe(el); return () => io.disconnect(); }, [loadMore]); const updateParam = (key: string, value: string) => { const n = new URLSearchParams(sp); if (!value || value === "all") n.delete(key); else n.set(key, value); setSp(n, { replace: true }); }; return (
updateParam("type", v)} onLanguageChange={(v) => updateParam("language", v)} />
{groups.map((group) => (
{group.items.map((post) => ( ))}
))} {!isLoading && !error && items.length === 0 ? (

{t("noResults")}

) : null} {error ? (
{error}
) : null} {isLoading ? (
) : null}
); }