Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/core/chorus/api/ChatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export type Chat = {
projectContextSummaryIsStale: boolean;
replyToId: string | null;
gcPrototype: boolean;

pinned: boolean; // deprecated
pinned: boolean;
};

type ChatDBRow = {
Expand Down Expand Up @@ -98,7 +97,7 @@ export async function fetchChats(): Promise<Chat[]> {
project_context_summary, project_context_summary_is_stale, reply_to_id, gc_prototype_chat
FROM chats
WHERE reply_to_id IS NULL
ORDER BY updated_at DESC`,
ORDER BY pinned DESC, updated_at DESC`,
)
.then((rows) => rows.map(readChat));
}
Expand Down Expand Up @@ -391,3 +390,25 @@ export function useRenameChat() {
},
});
}

export function useTogglePinChat() {
const queryClient = useQueryClient();
const cacheUpdateChat = useCacheUpdateChat();

return useMutation({
mutationKey: ["togglePinChat"] as const,
mutationFn: async ({ chatId, pinned }: { chatId: string; pinned: boolean }) => {
await db.execute("UPDATE chats SET pinned = $1 WHERE id = $2", [
pinned ? 1 : 0,
chatId,
]);
return { chatId, pinned };
},
onSuccess: async (_data, variables) => {
cacheUpdateChat(variables.chatId, (chat) => {
chat.pinned = variables.pinned;
});
await queryClient.invalidateQueries(chatQueries.list());
},
});
}
35 changes: 35 additions & 0 deletions src/ui/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
SquarePlusIcon,
ArrowBigUpIcon,
EllipsisIcon,
Pin,
PinOff,
} from "lucide-react";
import {
Sidebar,
Expand Down Expand Up @@ -810,6 +812,7 @@ function ChatListItem({ chat, isActive }: { chat: Chat; isActive: boolean }) {
mutateAsync: deleteChatMutateAsync,
isPending: deleteChatIsPending,
} = ChatAPI.useDeleteChat();
const { mutate: togglePinChat } = ChatAPI.useTogglePinChat();
const { data: parentChat } = useQuery(
ChatAPI.chatQueries.detail(chat.parentChatId ?? undefined),
);
Expand Down Expand Up @@ -871,18 +874,32 @@ function ChatListItem({ chat, isActive }: { chat: Chat; isActive: boolean }) {
[chat.id, renameChatMutateAsync],
);

const handleTogglePin = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
togglePinChat({
chatId: chat.id,
pinned: !chat.pinned,
});
},
[chat.id, chat.pinned, togglePinChat],
);

return (
<ChatListItemView
chatId={chat.id}
chatTitle={chat.title || ""}
isNewChat={chat.isNewChat}
isPinned={chat.pinned}
parentChatId={parentChat?.id ?? null}
parentChatTitle={parentChat?.title || null}
isActive={isActive}
isEditingTitle={isEditingTitle}
onStartEdit={handleStartEdit}
onStopEdit={handleStopEdit}
onSubmitEdit={handleSubmitEdit}
onTogglePin={handleTogglePin}
onDelete={handleOpenDeleteDialog}
onConfirmDelete={handleConfirmDelete}
deleteIsPending={deleteChatIsPending}
Expand All @@ -896,13 +913,15 @@ type ChatListItemViewProps = {
chatId: string;
chatTitle: string;
isNewChat: boolean;
isPinned: boolean;
parentChatId: string | null;
parentChatTitle: string | null;
isActive: boolean;
isEditingTitle: boolean;
onStartEdit: () => void;
onStopEdit: () => void;
onSubmitEdit: (newTitle: string) => Promise<void>;
onTogglePin: (e: React.MouseEvent) => void;
onDelete: () => void;
onConfirmDelete: () => void;
deleteIsPending: boolean;
Expand All @@ -915,13 +934,15 @@ const ChatListItemView = React.memo(
chatId,
chatTitle,
isNewChat,
isPinned,
parentChatId,
parentChatTitle,
isActive,
isEditingTitle,
onStartEdit,
onStopEdit,
onSubmitEdit,
onTogglePin,
onDelete,
onConfirmDelete,
deleteIsPending,
Expand Down Expand Up @@ -996,6 +1017,20 @@ const ChatListItemView = React.memo(

{/* chat actions */}
<div className="flex items-center gap-2 absolute right-3 z-10">
<Tooltip>
<TooltipTrigger asChild>
<div onClick={onTogglePin}>
{isPinned ? (
<PinOff className="h-[13px] w-[13px] opacity-100 transition-opacity text-muted-foreground hover:text-foreground" />
) : (
<Pin className="h-[13px] w-[13px] opacity-0 group-hover/chat-button:opacity-100 transition-opacity text-muted-foreground hover:text-foreground" />
)}
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{isPinned ? "Unpin chat" : "Pin chat"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<PencilOptimized
Expand Down