Files
simbarag/raggr-frontend/src/hooks/useConversations.ts
T
ryan 09dd983c4e Replace inline mobile menu with slide-in drawer and paginate conversations
Rework mobile navigation into a proper left slide-in drawer with a dimmed
backdrop, sharing a new SidebarContent component with the desktop sidebar so
mobile users can reach Admin/Scheduler/Sign out. The sidebar now loads only the
10 most recent conversations; a dedicated /conversations page browses the full
list with server-side search and "Load more" pagination.

- Backend: limit/offset/search query params on GET /api/conversation/
- SidebarContent shared by desktop sidebar and mobile drawer
- useConversationBrowser hook powers the dedicated page
- Remove duplicated inline conversation lists from ChatScreen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 22:32:19 -04:00

100 lines
3.2 KiB
TypeScript

import { useState, useCallback, useEffect, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { conversationService } from "../api/conversationService";
export type Conversation = {
title: string;
id: string;
};
// The sidebar/drawer only shows the most recent conversations; the full list
// lives on the dedicated /conversations page.
const SIDEBAR_LIMIT = 10;
export function useConversations() {
const navigate = useNavigate();
const { conversationId } = useParams();
const [conversations, setConversations] = useState<Conversation[]>([]);
const refreshConversations = useCallback(async () => {
try {
const fetched = await conversationService.getAllConversations({
limit: SIDEBAR_LIMIT,
});
setConversations(fetched.map((c) => ({ id: c.id, title: c.name })));
} catch (err) {
console.error("Failed to load conversations:", err);
}
}, []);
useEffect(() => {
refreshConversations();
}, [refreshConversations]);
// The active conversation is derived from the URL. Fall back to a minimal
// object while the list is still loading (e.g. on a deep-link refresh).
const selectedConversation = useMemo<Conversation | null>(() => {
if (!conversationId) return null;
return (
conversations.find((c) => c.id === conversationId) ?? {
id: conversationId,
title: "",
}
);
}, [conversationId, conversations]);
const createConversation = useCallback(async (): Promise<Conversation> => {
const newConv = await conversationService.createConversation();
const conversation = { title: newConv.name, id: newConv.id };
setConversations((prev) => [conversation, ...prev]);
navigate(`/c/${conversation.id}`);
return conversation;
}, [navigate]);
const renameConversation = useCallback(
async (id: string, title: string): Promise<void> => {
const trimmed = title.trim();
if (!trimmed) return;
// Optimistic update, roll back on failure. selectedConversation is
// derived from `conversations`, so it updates automatically.
const previous = conversations;
setConversations((prev) =>
prev.map((c) => (c.id === id ? { ...c, title: trimmed } : c)),
);
try {
await conversationService.renameConversation(id, trimmed);
} catch (err) {
console.error("Failed to rename conversation:", err);
setConversations(previous);
}
},
[conversations],
);
const deleteConversation = useCallback(
async (id: string): Promise<void> => {
const previous = conversations;
const wasActive = conversationId === id;
setConversations((prev) => prev.filter((c) => c.id !== id));
// Navigating home clears the message view via the route change.
if (wasActive) navigate("/");
try {
await conversationService.deleteConversation(id);
} catch (err) {
console.error("Failed to delete conversation:", err);
setConversations(previous);
}
},
[conversations, conversationId, navigate],
);
return {
conversations,
selectedConversation,
createConversation,
renameConversation,
deleteConversation,
refreshConversations,
};
}