9c9568b80c
- Added GET /portal/conversation and GET /portal/conversation/messages endpoints - Created Communication.api.ts with typed fetchers and React hooks - Rewired Communication.tsx to use real API, removed mock data - Added composer-disabled bar with "Reply from your phone" tooltip - Added conversation route tests to portal.test.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
159 lines
4.0 KiB
TypeScript
159 lines
4.0 KiB
TypeScript
export interface Conversation {
|
|
id: string;
|
|
channel: string;
|
|
lastMessageAt: string | null;
|
|
status: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface Message {
|
|
id: string;
|
|
direction: "inbound" | "outbound";
|
|
body: string | null;
|
|
status: string;
|
|
createdAt: string;
|
|
deliveredAt: string | null;
|
|
}
|
|
|
|
export interface MessagesResponse {
|
|
messages: Message[];
|
|
nextCursor: string | null;
|
|
}
|
|
|
|
export async function fetchConversation(sessionId: string): Promise<Conversation | null> {
|
|
const res = await fetch("/api/portal/conversation", {
|
|
headers: { "X-Impersonation-Session-Id": sessionId },
|
|
});
|
|
if (res.status === 204) return null;
|
|
if (!res.ok) throw new Error("Failed to fetch conversation");
|
|
return res.json();
|
|
}
|
|
|
|
export async function fetchMessages(
|
|
sessionId: string,
|
|
cursor?: string,
|
|
limit?: number
|
|
): Promise<MessagesResponse> {
|
|
const params = new URLSearchParams();
|
|
if (cursor) params.set("cursor", cursor);
|
|
if (limit) params.set("limit", String(limit));
|
|
const query = params.toString();
|
|
|
|
const res = await fetch(`/api/portal/conversation/messages${query ? `?${query}` : ""}`, {
|
|
headers: { "X-Impersonation-Session-Id": sessionId },
|
|
});
|
|
if (res.status === 204) return { messages: [], nextCursor: null };
|
|
if (!res.ok) throw new Error("Failed to fetch messages");
|
|
return res.json();
|
|
}
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
export function useConversation(sessionId: string | null): {
|
|
conversation: Conversation | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
} {
|
|
const [conversation, setConversation] = useState<Conversation | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!sessionId) {
|
|
setLoading(false);
|
|
setConversation(null);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
fetchConversation(sessionId)
|
|
.then((conv) => {
|
|
if (!cancelled) {
|
|
setConversation(conv);
|
|
setLoading(false);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) {
|
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [sessionId]);
|
|
|
|
return { conversation, loading, error };
|
|
}
|
|
|
|
export function useMessages(sessionId: string | null): {
|
|
messages: Message[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
loadMore: () => void;
|
|
hasMore: boolean;
|
|
} {
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
|
const [hasMore, setHasMore] = useState(false);
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!sessionId) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(null);
|
|
setMessages([]);
|
|
setCursor(undefined);
|
|
setHasMore(false);
|
|
|
|
fetchMessages(sessionId)
|
|
.then((res) => {
|
|
if (!cancelled) {
|
|
setMessages(res.messages);
|
|
setCursor(res.nextCursor ?? undefined);
|
|
setHasMore(res.nextCursor !== null);
|
|
setLoading(false);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) {
|
|
setError(err instanceof Error ? err.message : "An error occurred");
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [sessionId]);
|
|
|
|
const loadMore = () => {
|
|
if (loadingMore || !hasMore || !sessionId) return;
|
|
setLoadingMore(true);
|
|
|
|
fetchMessages(sessionId, cursor)
|
|
.then((res) => {
|
|
setMessages((prev) => [...prev, ...res.messages]);
|
|
setCursor(res.nextCursor ?? undefined);
|
|
setHasMore(res.nextCursor !== null);
|
|
setLoadingMore(false);
|
|
})
|
|
.catch(() => {
|
|
setLoadingMore(false);
|
|
});
|
|
};
|
|
|
|
return { messages, loading, error, loadMore, hasMore };
|
|
} |