add pin functionality

This commit is contained in:
Thomas G. Lopes 2025-06-17 13:12:22 +01:00
parent fae7798119
commit ef6aead37a
3 changed files with 84 additions and 5 deletions

View file

@ -124,3 +124,32 @@ export const updateTitle = mutation({
});
},
});
export const togglePin = mutation({
args: {
conversation_id: v.id('conversations'),
session_token: v.string(),
},
handler: async (ctx, args) => {
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
session_token: args.session_token,
});
if (!session) {
throw new Error('Unauthorized');
}
// Verify the conversation belongs to the user
const conversation = await ctx.db.get(args.conversation_id);
if (!conversation || conversation.user_id !== session.userId) {
throw new Error('Conversation not found or unauthorized');
}
await ctx.db.patch(args.conversation_id, {
pinned: !conversation.pinned,
updated_at: Date.now(),
});
return { pinned: !conversation.pinned };
},
});

View file

@ -45,6 +45,7 @@ export default defineSchema({
user_id: v.string(),
title: v.string(),
updated_at: v.optional(v.number()),
pinned: v.optional(v.boolean()),
}).index('by_user', ['user_id']),
messages: defineTable({
conversation_id: v.string(),

View file

@ -8,6 +8,7 @@
import { Avatar } from 'melt/components';
import PanelLeftIcon from '~icons/lucide/panel-left';
import PinIcon from '~icons/lucide/pin';
import PinOffIcon from '~icons/lucide/pin-off';
import XIcon from '~icons/lucide/x';
import SendIcon from '~icons/lucide/send';
import { callGenerateMessage } from '../api/generate-message/call.js';
@ -16,9 +17,12 @@
import { goto } from '$app/navigation';
import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
import { api } from '$lib/backend/convex/_generated/api.js';
import { type Doc } from '$lib/backend/convex/_generated/dataModel.js';
import { type Doc, type Id } from '$lib/backend/convex/_generated/dataModel.js';
import { TextareaAutosize } from '$lib/spells/textarea-autosize.svelte.js';
import Tooltip from '$lib/components/ui/tooltip.svelte';
import { useConvexClient } from 'convex-svelte';
const client = useConvexClient();
let { data, children } = $props();
@ -60,6 +64,7 @@
const thirtyDays = 30 * oneDay;
const groups = {
pinned: [] as Doc<'conversations'>[],
today: [] as Doc<'conversations'>[],
yesterday: [] as Doc<'conversations'>[],
lastWeek: [] as Doc<'conversations'>[],
@ -68,6 +73,12 @@
};
conversations.forEach((conversation) => {
// Pinned conversations go to pinned group regardless of time
if (conversation.pinned) {
groups.pinned.push(conversation);
return;
}
const updatedAt = conversation.updated_at ?? 0;
const timeDiff = now - updatedAt;
@ -84,11 +95,32 @@
}
});
// Sort pinned conversations by updated_at (most recent first)
groups.pinned.sort((a, b) => {
const aTime = a.updated_at ?? 0;
const bTime = b.updated_at ?? 0;
return bTime - aTime;
});
return groups;
}
const groupedConversations = $derived(groupConversationsByTime(conversationsQuery.data ?? []));
async function togglePin(conversationId: string) {
if (!session.current?.session.token) return;
try {
await client.mutation(api.conversations.togglePin, {
conversation_id: conversationId as Id<'conversations'>,
session_token: session.current.session.token,
});
} catch (error) {
console.error('Failed to toggle pin:', error);
}
}
const templateConversations = $derived([
{ key: 'pinned', label: 'Pinned', conversations: groupedConversations.pinned, icon: PinIcon },
{ key: 'today', label: 'Today', conversations: groupedConversations.today },
{ key: 'yesterday', label: 'Yesterday', conversations: groupedConversations.yesterday },
{ key: 'lastWeek', label: 'Last 7 days', conversations: groupedConversations.lastWeek },
@ -123,7 +155,12 @@
{#each templateConversations as group, index (group.key)}
{#if group.conversations.length > 0}
<div class="px-2 py-1" class:mt-2={index > 0}>
<h3 class="text-heading text-xs font-medium">{group.label}</h3>
<h3 class="text-heading text-xs font-medium">
{#if group.icon}
<svelte:component this={group.icon} class="inline size-3" />
{/if}
{group.label}
</h3>
</div>
{#each group.conversations as conversation (conversation._id)}
{@const isActive = page.params.id === conversation._id}
@ -145,11 +182,23 @@
>
<Tooltip>
{#snippet trigger(tooltip)}
<button {...tooltip.trigger} class="hover:bg-muted rounded-md p-1">
<PinIcon class="size-4" />
<button
{...tooltip.trigger}
class="hover:bg-muted rounded-md p-1"
onclick={(e) => {
e.preventDefault();
e.stopPropagation();
togglePin(conversation._id);
}}
>
{#if conversation.pinned}
<PinOffIcon class="size-4" />
{:else}
<PinIcon class="size-4" />
{/if}
</button>
{/snippet}
Pin thread
{conversation.pinned ? 'Unpin thread' : 'Pin thread'}
</Tooltip>
<Tooltip>
{#snippet trigger(tooltip)}