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 @@
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