diff --git a/apps/chat/apps/backend/src/conversation/conversation.controller.ts b/apps/chat/apps/backend/src/conversation/conversation.controller.ts index fe72319f7..6533ce439 100644 --- a/apps/chat/apps/backend/src/conversation/conversation.controller.ts +++ b/apps/chat/apps/backend/src/conversation/conversation.controller.ts @@ -165,6 +165,34 @@ export class ConversationController { return result.value; } + @Patch(':id/pin') + async pinConversation( + @Param('id') id: string, + @CurrentUser() user: CurrentUserData + ): Promise { + const result = await this.conversationService.pinConversation(id, user.userId); + + if (!isOk(result)) { + throw result.error; + } + + return result.value; + } + + @Patch(':id/unpin') + async unpinConversation( + @Param('id') id: string, + @CurrentUser() user: CurrentUserData + ): Promise { + const result = await this.conversationService.unpinConversation(id, user.userId); + + if (!isOk(result)) { + throw result.error; + } + + return result.value; + } + @Delete(':id') async deleteConversation( @Param('id') id: string, diff --git a/apps/chat/apps/backend/src/conversation/conversation.service.ts b/apps/chat/apps/backend/src/conversation/conversation.service.ts index 6a0bbe04b..3757ce712 100644 --- a/apps/chat/apps/backend/src/conversation/conversation.service.ts +++ b/apps/chat/apps/backend/src/conversation/conversation.service.ts @@ -28,7 +28,7 @@ export class ConversationService { .select() .from(conversations) .where(and(...conditions)) - .orderBy(desc(conversations.updatedAt)); + .orderBy(desc(conversations.isPinned), desc(conversations.updatedAt)); return ok(result); } catch (error) { @@ -267,4 +267,46 @@ export class ConversationService { return err(DatabaseError.queryFailed('Failed to get message count')); } } + + async pinConversation(conversationId: string, userId: string): AsyncResult { + try { + // First verify the conversation belongs to the user + const convResult = await this.getConversation(conversationId, userId); + if (!convResult.ok) { + return err(convResult.error); + } + + const result = await this.db + .update(conversations) + .set({ isPinned: true, updatedAt: new Date() }) + .where(eq(conversations.id, conversationId)) + .returning(); + + return ok(result[0]); + } catch (error) { + this.logger.error('Error pinning conversation', error); + return err(DatabaseError.queryFailed('Failed to pin conversation')); + } + } + + async unpinConversation(conversationId: string, userId: string): AsyncResult { + try { + // First verify the conversation belongs to the user + const convResult = await this.getConversation(conversationId, userId); + if (!convResult.ok) { + return err(convResult.error); + } + + const result = await this.db + .update(conversations) + .set({ isPinned: false, updatedAt: new Date() }) + .where(eq(conversations.id, conversationId)) + .returning(); + + return ok(result[0]); + } catch (error) { + this.logger.error('Error unpinning conversation', error); + return err(DatabaseError.queryFailed('Failed to unpin conversation')); + } + } } diff --git a/apps/chat/apps/backend/src/db/schema/conversations.schema.ts b/apps/chat/apps/backend/src/db/schema/conversations.schema.ts index 5b8cfa48c..2221235df 100644 --- a/apps/chat/apps/backend/src/db/schema/conversations.schema.ts +++ b/apps/chat/apps/backend/src/db/schema/conversations.schema.ts @@ -18,6 +18,7 @@ export const conversations = pgTable('conversations', { conversationMode: conversationModeEnum('conversation_mode').default('free').notNull(), documentMode: boolean('document_mode').default(false).notNull(), isArchived: boolean('is_archived').default(false).notNull(), + isPinned: boolean('is_pinned').default(false).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); diff --git a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte index 997501d13..61f23d4c1 100644 --- a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte +++ b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte @@ -4,7 +4,8 @@ import { conversationsStore } from '$lib/stores/conversations.svelte'; import { authStore } from '$lib/stores/auth.svelte'; import { isSidebarMode, isNavCollapsed } from '$lib/stores/navigation'; - import { MagnifyingGlass, X, Plus, ChatCircle, Archive, Trash } from '@manacore/shared-icons'; + import { MagnifyingGlass, X, Plus, ChatCircle, Archive, Trash, PushPin } from '@manacore/shared-icons'; + import { ConfirmationModal } from '@manacore/shared-ui'; import { goto } from '$app/navigation'; import type { Snippet } from 'svelte'; @@ -14,6 +15,11 @@ let { children }: Props = $props(); + // Delete confirmation modal state + let showDeleteModal = $state(false); + let deleteTargetId = $state(null); + let isDeleting = $state(false); + // Resizer state let leftColumnWidth = $state(320); let isResizing = $state(false); @@ -106,17 +112,48 @@ } } - // Delete conversation - async function handleDelete(e: MouseEvent, convId: string) { + // Pin/unpin conversation + async function handleTogglePin(e: MouseEvent, convId: string, isPinned: boolean) { e.preventDefault(); e.stopPropagation(); - if (confirm('Möchtest du diese Konversation wirklich löschen?')) { - const success = await conversationsStore.deleteConversation(convId); - if (success && isActive(convId)) { + if (isPinned) { + await conversationsStore.unpinConversation(convId); + } else { + await conversationsStore.pinConversation(convId); + } + } + + // Open delete confirmation modal + function handleDelete(e: MouseEvent, convId: string) { + e.preventDefault(); + e.stopPropagation(); + deleteTargetId = convId; + showDeleteModal = true; + } + + // Confirm delete action + async function confirmDelete() { + if (!deleteTargetId) return; + + isDeleting = true; + try { + const wasActive = isActive(deleteTargetId); + const success = await conversationsStore.deleteConversation(deleteTargetId); + if (success && wasActive) { goto('/chat'); } + } finally { + isDeleting = false; + showDeleteModal = false; + deleteTargetId = null; } } + + // Close delete modal + function closeDeleteModal() { + showDeleteModal = false; + deleteTargetId = null; + }
- + {#if conv.isPinned} + + {:else} + + {/if}

{conv.title || 'Neue Konversation'}

@@ -245,6 +290,13 @@ {/if}
+
+ + +