feat: implement chat archiving functionality in LayoutDataProvider

- Added the ability to archive and unarchive chat threads, enhancing user interaction with chat items.
- Introduced a new `handleChatArchive` function to manage the archiving state and provide user feedback via toast notifications.
- Updated the `ChatItem` type to include an `archived` property for tracking the archive status.
- Modified relevant components to support the new archiving feature, including `ChatListItem`, `Sidebar`, and `MobileSidebar`.
This commit is contained in:
Anish Sarkar 2026-01-24 15:17:35 +05:30
parent 7ccb52dc76
commit a51fe2ee69
7 changed files with 81 additions and 14 deletions

View file

@ -7,6 +7,7 @@ import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms";
@ -21,7 +22,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useInbox } from "@/hooks/use-inbox"; import { useInbox } from "@/hooks/use-inbox";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence"; import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence";
import { cleanupElectric } from "@/lib/electric/client"; import { cleanupElectric } from "@/lib/electric/client";
import { resetUser, trackLogout } from "@/lib/posthog/events"; import { resetUser, trackLogout } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
@ -56,6 +57,7 @@ export function LayoutDataProvider({
}: LayoutDataProviderProps) { }: LayoutDataProviderProps) {
const t = useTranslations("dashboard"); const t = useTranslations("dashboard");
const tCommon = useTranslations("common"); const tCommon = useTranslations("common");
const tSidebar = useTranslations("sidebar");
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const pathname = usePathname(); const pathname = usePathname();
@ -154,6 +156,7 @@ export function LayoutDataProvider({
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`, url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
visibility: thread.visibility, visibility: thread.visibility,
isOwnThread: thread.is_own_thread, isOwnThread: thread.is_own_thread,
archived: thread.archived,
}; };
// Split based on visibility, not ownership: // Split based on visibility, not ownership:
@ -304,6 +307,28 @@ export function LayoutDataProvider({
setShowDeleteChatDialog(true); setShowDeleteChatDialog(true);
}, []); }, []);
const handleChatArchive = useCallback(
async (chat: ChatItem) => {
const newArchivedState = !chat.archived;
const successMessage = newArchivedState
? tSidebar("chat_archived") || "Chat archived"
: tSidebar("chat_unarchived") || "Chat restored";
try {
await updateThread(chat.id, { archived: newArchivedState });
toast.success(successMessage);
// Invalidate queries to refresh UI (React Query will only refetch active queries)
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
} catch (error) {
console.error("Error archiving thread:", error);
toast.error(tSidebar("error_archiving_chat") || "Failed to archive chat");
}
},
[queryClient, searchSpaceId, tSidebar]
);
const handleSettings = useCallback(() => { const handleSettings = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/settings`); router.push(`/dashboard/${searchSpaceId}/settings`);
}, [router, searchSpaceId]); }, [router, searchSpaceId]);
@ -391,6 +416,7 @@ export function LayoutDataProvider({
onNewChat={handleNewChat} onNewChat={handleNewChat}
onChatSelect={handleChatSelect} onChatSelect={handleChatSelect}
onChatDelete={handleChatDelete} onChatDelete={handleChatDelete}
onChatArchive={handleChatArchive}
onViewAllSharedChats={handleViewAllSharedChats} onViewAllSharedChats={handleViewAllSharedChats}
onViewAllPrivateChats={handleViewAllPrivateChats} onViewAllPrivateChats={handleViewAllPrivateChats}
user={{ user={{

View file

@ -30,6 +30,7 @@ export interface ChatItem {
isActive?: boolean; isActive?: boolean;
visibility?: "PRIVATE" | "SEARCH_SPACE"; visibility?: "PRIVATE" | "SEARCH_SPACE";
isOwnThread?: boolean; isOwnThread?: boolean;
archived?: boolean;
} }
export interface PageUsage { export interface PageUsage {

View file

@ -26,6 +26,7 @@ interface LayoutShellProps {
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllSharedChats?: () => void;
onViewAllPrivateChats?: () => void; onViewAllPrivateChats?: () => void;
user: User; user: User;
@ -59,6 +60,7 @@ export function LayoutShell({
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatDelete, onChatDelete,
onChatArchive,
onViewAllSharedChats, onViewAllSharedChats,
onViewAllPrivateChats, onViewAllPrivateChats,
user, user,
@ -107,6 +109,7 @@ export function LayoutShell({
onNewChat={onNewChat} onNewChat={onNewChat}
onChatSelect={onChatSelect} onChatSelect={onChatSelect}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive}
onViewAllSharedChats={onViewAllSharedChats} onViewAllSharedChats={onViewAllSharedChats}
onViewAllPrivateChats={onViewAllPrivateChats} onViewAllPrivateChats={onViewAllPrivateChats}
user={user} user={user}
@ -155,6 +158,7 @@ export function LayoutShell({
onNewChat={onNewChat} onNewChat={onNewChat}
onChatSelect={onChatSelect} onChatSelect={onChatSelect}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive}
onViewAllSharedChats={onViewAllSharedChats} onViewAllSharedChats={onViewAllSharedChats}
onViewAllPrivateChats={onViewAllPrivateChats} onViewAllPrivateChats={onViewAllPrivateChats}
user={user} user={user}

View file

@ -1,12 +1,13 @@
"use client"; "use client";
import { MessageSquare, MoreHorizontal } from "lucide-react"; import { ArchiveIcon, MessageSquare, MoreHorizontal, RotateCcwIcon, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -14,11 +15,13 @@ import { cn } from "@/lib/utils";
interface ChatListItemProps { interface ChatListItemProps {
name: string; name: string;
isActive?: boolean; isActive?: boolean;
archived?: boolean;
onClick?: () => void; onClick?: () => void;
onArchive?: () => void;
onDelete?: () => void; onDelete?: () => void;
} }
export function ChatListItem({ name, isActive, onClick, onDelete }: ChatListItemProps) { export function ChatListItem({ name, isActive, archived, onClick, onArchive, onDelete }: ChatListItemProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
return ( return (
@ -48,15 +51,39 @@ export function ChatListItem({ name, isActive, onClick, onDelete }: ChatListItem
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right"> <DropdownMenuContent align="end" side="right">
<DropdownMenuItem {onArchive && (
onClick={(e) => { <DropdownMenuItem
e.stopPropagation(); onClick={(e) => {
onDelete?.(); e.stopPropagation();
}} onArchive();
className="text-destructive focus:text-destructive" }}
> >
{t("delete")} {archived ? (
</DropdownMenuItem> <>
<RotateCcwIcon className="mr-2 h-4 w-4" />
<span>{t("unarchive") || "Restore"}</span>
</>
) : (
<>
<ArchiveIcon className="mr-2 h-4 w-4" />
<span>{t("archive") || "Archive"}</span>
</>
)}
</DropdownMenuItem>
)}
{onArchive && onDelete && <DropdownMenuSeparator />}
{onDelete && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete")}</span>
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>

View file

@ -25,6 +25,7 @@ interface MobileSidebarProps {
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllSharedChats?: () => void;
onViewAllPrivateChats?: () => void; onViewAllPrivateChats?: () => void;
user: User; user: User;
@ -64,6 +65,7 @@ export function MobileSidebar({
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatDelete, onChatDelete,
onChatArchive,
onViewAllSharedChats, onViewAllSharedChats,
onViewAllPrivateChats, onViewAllPrivateChats,
user, user,
@ -141,6 +143,7 @@ export function MobileSidebar({
}} }}
onChatSelect={handleChatSelect} onChatSelect={handleChatSelect}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive}
onViewAllSharedChats={onViewAllSharedChats} onViewAllSharedChats={onViewAllSharedChats}
onViewAllPrivateChats={onViewAllPrivateChats} onViewAllPrivateChats={onViewAllPrivateChats}
user={user} user={user}

View file

@ -27,6 +27,7 @@ interface SidebarProps {
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllSharedChats?: () => void;
onViewAllPrivateChats?: () => void; onViewAllPrivateChats?: () => void;
user: User; user: User;
@ -52,6 +53,7 @@ export function Sidebar({
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatDelete, onChatDelete,
onChatArchive,
onViewAllSharedChats, onViewAllSharedChats,
onViewAllPrivateChats, onViewAllPrivateChats,
user, user,
@ -175,7 +177,9 @@ export function Sidebar({
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
isActive={chat.id === activeChatId} isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)} onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)} onDelete={() => onChatDelete?.(chat)}
/> />
))} ))}
@ -216,7 +220,9 @@ export function Sidebar({
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
isActive={chat.id === activeChatId} isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)} onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)} onDelete={() => onChatDelete?.(chat)}
/> />
))} ))}

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { ChevronsUpDown, ScrollText, Settings, Users } from "lucide-react"; import { ChevronsUpDown, Logs, Settings, Users } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -57,7 +57,7 @@ export function SidebarHeader({
{t("manage_members")} {t("manage_members")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/dashboard/${searchSpaceId}/logs`)}> <DropdownMenuItem onClick={() => router.push(`/dashboard/${searchSpaceId}/logs`)}>
<ScrollText className="mr-2 h-4 w-4" /> <Logs className="mr-2 h-4 w-4" />
{t("logs")} {t("logs")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />