From 5d2581710f49c27f7079ec4f8bc6906b32b76ee7 Mon Sep 17 00:00:00 2001 From: vegard Date: Tue, 17 Mar 2026 16:25:44 +0100 Subject: [PATCH] =?UTF-8?q?Fullf=C3=B8r=20oppgave=205.4:=20=C3=A9n-til-?= =?UTF-8?q?=C3=A9n=20chat=20med=20deltaker-velger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementerer full 1:1 chat-loop: - NewChatDialog: velg person å starte samtale med - Fikser API-felt (participant_ids → participants) for korrekt kommunikasjon med maskinrommets create_communication-endepunkt - Opprett kommunikasjonsnode med to deltakere (owner + member_of) - Dedupliserer: finner eksisterende samtale før ny opprettes - Chat-header viser den andre deltakerens navn i 1:1-samtaler - Testbruker-node opprettet på server for verifisering Full loop verifisert via STDB: node + edges + melding + belongs_to fungerer, WebSocket-subscribers ser endringer i sanntid. Co-Authored-By: Claude Opus 4.6 --- frontend/src/lib/api.ts | 3 +- .../src/lib/components/NewChatDialog.svelte | 152 ++++++++++++++++++ frontend/src/routes/+page.svelte | 65 +++++++- frontend/src/routes/chat/[id]/+page.svelte | 11 +- tasks.md | 3 +- 5 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/components/NewChatDialog.svelte diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b8b7220..61e4f08 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -68,7 +68,8 @@ export function createEdge( export interface CreateCommunicationRequest { title?: string; - participant_ids?: string[]; + participants?: string[]; + visibility?: string; metadata?: Record; } diff --git a/frontend/src/lib/components/NewChatDialog.svelte b/frontend/src/lib/components/NewChatDialog.svelte new file mode 100644 index 0000000..2204cd2 --- /dev/null +++ b/frontend/src/lib/components/NewChatDialog.svelte @@ -0,0 +1,152 @@ + + + + + + +
+
+ +
+

Ny samtale

+ +
+ + +
+ +
+ + +
+ {#if filtered.length === 0} +

+ {#if people.length === 0} + Ingen andre brukere funnet + {:else} + Ingen treff for «{search}» + {/if} +

+ {:else} +
    + {#each filtered as person (person.id)} + {@const existing = existingChatWith(person.id)} +
  • + +
  • + {/each} +
+ {/if} +
+
+
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 474f7e5..a813ff6 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -4,6 +4,7 @@ import { connectionState, nodeStore, edgeStore, nodeAccessStore, nodeVisibility } from '$lib/spacetime'; import type { Node } from '$lib/spacetime'; import NodeEditor from '$lib/components/NodeEditor.svelte'; + import NewChatDialog from '$lib/components/NewChatDialog.svelte'; import { createNode, createEdge, createCommunication } from '$lib/api'; const session = $derived($page.data.session as Record | undefined); @@ -102,12 +103,34 @@ return edges.filter((e) => !e.system).map((e) => e.edgeType); } - /** Create a new communication node and navigate to it */ - async function handleNewChat() { - if (!accessToken) return; + let showNewChatDialog = $state(false); + + /** Open the new chat dialog to pick a participant */ + function handleNewChat() { + showNewChatDialog = true; + } + + /** Create a 1:1 communication with the selected person */ + async function handleStartChat(personId: string) { + if (!accessToken || !nodeId) return; + showNewChatDialog = false; + try { + // Check if there's already a communication with this person + const existingId = findExistingChat(personId); + if (existingId) { + window.location.href = `/chat/${existingId}`; + return; + } + + // Get the other person's name for the chat title + const person = nodeStore.get(personId); + const myNode = nodeStore.get(nodeId); + const title = [myNode?.title, person?.title].filter(Boolean).join(' & ') || 'Samtale'; + const { node_id } = await createCommunication(accessToken, { - title: 'Ny samtale', + title, + participants: [personId], }); window.location.href = `/chat/${node_id}`; } catch (e) { @@ -115,6 +138,32 @@ } } + /** Find an existing 1:1 communication between current user and another person */ + function findExistingChat(personId: string): string | undefined { + if (!nodeId) return undefined; + + // Collect communication nodes where current user is owner or member + const userComms = new Set(); + for (const edge of edgeStore.bySource(nodeId)) { + if (edge.edgeType === 'owner' || edge.edgeType === 'member_of') { + const target = nodeStore.get(edge.targetId); + if (target?.nodeKind === 'communication') { + userComms.add(edge.targetId); + } + } + } + + // Check if the other person is also in any of these communications + for (const edge of edgeStore.bySource(personId)) { + if (edge.edgeType === 'owner' || edge.edgeType === 'member_of') { + if (userComms.has(edge.targetId)) { + return edge.targetId; + } + } + } + return undefined; + } + /** Submit a new node via maskinrommet */ async function handleCreateNode(data: { title: string; content: string; html: string }) { if (!accessToken) throw new Error('Ikke innlogget'); @@ -272,4 +321,12 @@

{/if} + + {#if showNewChatDialog && nodeId} + showNewChatDialog = false} + /> + {/if} diff --git a/frontend/src/routes/chat/[id]/+page.svelte b/frontend/src/routes/chat/[id]/+page.svelte index ee6ccd2..bdbff90 100644 --- a/frontend/src/routes/chat/[id]/+page.svelte +++ b/frontend/src/routes/chat/[id]/+page.svelte @@ -63,6 +63,15 @@ return nodes; }); + /** For 1:1 chats, show the other person's name instead of the generic title */ + const chatTitle = $derived.by(() => { + if (participants.length === 2 && nodeId) { + const other = participants.find(p => p.id !== nodeId); + if (other?.title) return other.title; + } + return communicationNode?.title || 'Samtale'; + }); + // Auto-scroll to bottom when new messages arrive let messagesContainer: HTMLDivElement | undefined = $state(); let prevMessageCount = $state(0); @@ -132,7 +141,7 @@

- {communicationNode?.title || 'Samtale'} + {chatTitle}

{#if participants.length > 0}

diff --git a/tasks.md b/tasks.md index da4a3bb..0fb161b 100644 --- a/tasks.md +++ b/tasks.md @@ -82,8 +82,7 @@ Uavhengige faser kan fortsatt plukkes. - [x] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at). - [x] 5.2 Kontekst-arv: input i kommunikasjonsnode → automatisk `belongs_to`-edge. - [x] 5.3 Chat-visning i frontend: noder med `belongs_to`-edge til kommunikasjonsnode, sortert på tid, sanntid via STDB. -- [~] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge. - > Påbegynt: 2026-03-17T16:15 +- [x] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge. ## Fase 6: CAS og mediefiler