2026-06-01 18:14:32 +07:00
|
|
|
import { useCallback, useRef, useState } from "react";
|
2026-06-01 16:42:36 +07:00
|
|
|
import type { MessageRecord } from "../../../shared/api/client";
|
2026-06-01 21:44:29 +07:00
|
|
|
import { listMessages, reanalyzeMessage } from "../../../shared/api/client";
|
2026-05-16 19:17:34 +07:00
|
|
|
|
2026-06-01 18:14:32 +07:00
|
|
|
const PAGE_SIZE = 100;
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
export function mergeMessages(
|
|
|
|
|
current: MessageRecord[],
|
|
|
|
|
incoming: MessageRecord[],
|
|
|
|
|
): MessageRecord[] {
|
2026-05-16 19:17:34 +07:00
|
|
|
const byId = new Map(current.map((message) => [message.id, message]));
|
|
|
|
|
for (const message of incoming) {
|
|
|
|
|
byId.set(message.id, { ...byId.get(message.id), ...message });
|
|
|
|
|
}
|
2026-06-01 18:14:32 +07:00
|
|
|
// Removed .slice(0, 200) cap — let the message list grow unbounded.
|
|
|
|
|
// Infinite scroll handles the data volume via cursor pagination.
|
2026-06-01 21:44:29 +07:00
|
|
|
return Array.from(byId.values()).sort(
|
|
|
|
|
(a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id),
|
|
|
|
|
);
|
2026-05-16 19:17:34 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useMessages() {
|
|
|
|
|
const [messages, setMessages] = useState<MessageRecord[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
2026-06-01 18:14:32 +07:00
|
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
2026-05-16 19:17:34 +07:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-06-01 18:14:32 +07:00
|
|
|
const [cursor, setCursor] = useState<string | null>(null);
|
|
|
|
|
const [hasMore, setHasMore] = useState(false);
|
|
|
|
|
const currentChannel = useRef<string | null>(null);
|
2026-05-16 19:17:34 +07:00
|
|
|
|
|
|
|
|
const fetchMessages = useCallback(async (channelId?: string) => {
|
2026-05-16 23:11:46 +07:00
|
|
|
if (!channelId) {
|
|
|
|
|
setMessages([]);
|
2026-06-01 18:14:32 +07:00
|
|
|
setCursor(null);
|
|
|
|
|
setHasMore(false);
|
2026-05-16 23:11:46 +07:00
|
|
|
return [];
|
|
|
|
|
}
|
2026-06-01 18:14:32 +07:00
|
|
|
currentChannel.current = channelId;
|
2026-05-16 19:17:34 +07:00
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
try {
|
2026-06-01 21:44:29 +07:00
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
limit: String(PAGE_SIZE),
|
|
|
|
|
channelId,
|
|
|
|
|
});
|
2026-05-16 19:17:34 +07:00
|
|
|
const result = await listMessages(params);
|
2026-06-01 18:14:32 +07:00
|
|
|
// Only update state if we're still on the same channel (avoid race conditions)
|
|
|
|
|
if (currentChannel.current === channelId) {
|
|
|
|
|
setMessages(result.data);
|
|
|
|
|
setCursor(result.nextCursor);
|
|
|
|
|
setHasMore(!!result.nextCursor);
|
|
|
|
|
}
|
2026-05-16 19:17:34 +07:00
|
|
|
return result.data;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
|
|
|
setError(message);
|
|
|
|
|
throw err;
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-06-01 18:14:32 +07:00
|
|
|
const loadMore = useCallback(async () => {
|
|
|
|
|
if (!cursor || !currentChannel.current || loadingMore) return;
|
|
|
|
|
setLoadingMore(true);
|
|
|
|
|
try {
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
limit: String(PAGE_SIZE),
|
|
|
|
|
channelId: currentChannel.current,
|
|
|
|
|
cursor,
|
|
|
|
|
});
|
|
|
|
|
const result = await listMessages(params);
|
|
|
|
|
// Only update if still on the same channel
|
2026-06-01 21:44:29 +07:00
|
|
|
if (
|
|
|
|
|
currentChannel.current === result.data[0]?.channel_id ||
|
|
|
|
|
currentChannel.current
|
|
|
|
|
) {
|
2026-06-01 18:14:32 +07:00
|
|
|
setMessages((prev) => [...prev, ...result.data]);
|
|
|
|
|
setCursor(result.nextCursor);
|
|
|
|
|
setHasMore(!!result.nextCursor);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingMore(false);
|
|
|
|
|
}
|
|
|
|
|
}, [cursor, loadingMore]);
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
// BUG 5 FIX: reanalyze returns Promise<void> so callers can await it
|
|
|
|
|
const reanalyze = useCallback(async (id: string): Promise<void> => {
|
2026-05-16 19:17:34 +07:00
|
|
|
setMessages((prev) =>
|
|
|
|
|
prev.map((message) =>
|
|
|
|
|
message.id === id
|
2026-06-01 21:44:29 +07:00
|
|
|
? {
|
|
|
|
|
...message,
|
|
|
|
|
ai_status: "pending" as const,
|
|
|
|
|
ai_error: null,
|
|
|
|
|
ai_analysis: null,
|
|
|
|
|
}
|
2026-05-16 19:17:34 +07:00
|
|
|
: message,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await reanalyzeMessage(id);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-06-01 18:14:32 +07:00
|
|
|
return {
|
|
|
|
|
messages,
|
|
|
|
|
setMessages,
|
|
|
|
|
loading,
|
|
|
|
|
loadingMore,
|
|
|
|
|
error,
|
|
|
|
|
fetchMessages,
|
|
|
|
|
reanalyze,
|
|
|
|
|
loadMore,
|
|
|
|
|
hasMore,
|
|
|
|
|
};
|
2026-05-16 19:17:34 +07:00
|
|
|
}
|