diff --git a/web/src/lib/blocks/ChatBlock.svelte b/web/src/lib/blocks/ChatBlock.svelte index bca8b04..7af9b0b 100644 --- a/web/src/lib/blocks/ChatBlock.svelte +++ b/web/src/lib/blocks/ChatBlock.svelte @@ -7,6 +7,7 @@ import type { ChatConnection } from '$lib/chat/types'; import MessageBox from '$lib/components/MessageBox.svelte'; import Editor from '$lib/components/Editor.svelte'; + import ConvertDialog from '$lib/components/ConvertDialog.svelte'; let { props = {} }: { props?: Record } = $props(); @@ -15,9 +16,34 @@ let chat = $state(null); let sending = $state(false); let messagesEl: HTMLDivElement | undefined; + let replyingTo = $state(null); + let convertTarget = $state<{ messageId: string; type: 'kanban' | 'calendar' } | null>(null); + + let currentUserId = $derived($page.data.user?.id ?? undefined); const chatCallbacks = { + get currentUserId() { return currentUserId; }, onMentionClick: (entityId: string) => goto(`/entities/${entityId}`), + onEdit: async (messageId: string, newBody: string) => { + // Oppdater lokalt umiddelbart (optimistisk) + chat?.updateLocal?.(messageId, newBody); + try { + const res = await fetch(`/api/messages/${messageId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ body: newBody }) + }); + if (res.ok) await chat?.refresh(); + } catch { /* nettverksfeil — allerede oppdatert lokalt */ } + }, + onDelete: async (messageId: string) => { + // Fjern fra lokal state umiddelbart + chat?.removeLocal?.(messageId); + // Slett fra PG (ignorerer 404 — meldingen kan være usynket) + try { await fetch(`/api/messages/${messageId}`, { method: 'DELETE' }); } catch {} + // Slett fra SpacetimeDB slik at den ikke dukker opp igjen ved reconnect + chat?.deleteFromSpacetime?.(messageId); + }, onReaction: async (messageId: string, reaction: string) => { try { const msg = chat?.messages.find(m => m.id === messageId); @@ -39,6 +65,16 @@ }); if (res.ok) await chat?.refresh(); } catch { /* stille feil */ } + }, + onReply: (messageId: string) => { + const msg = chat?.messages.find(m => m.id === messageId); + if (msg) replyingTo = msg; + }, + onConvertToKanban: (messageId: string) => { + convertTarget = { messageId, type: 'kanban' }; + }, + onConvertToCalendar: (messageId: string) => { + convertTarget = { messageId, type: 'calendar' }; } }; @@ -46,13 +82,30 @@ if (!chat || sending) return; sending = true; try { - await chat.send(html, mentions.length > 0 ? mentions : undefined); + await chat.send(html, mentions.length > 0 ? mentions : undefined, replyingTo?.id); + replyingTo = null; scrollToBottom(); } finally { sending = false; } } + async function handleConvert(detail: { columnId?: string; calendarId?: string; startsAt?: string; endsAt?: string; allDay?: boolean }) { + if (!convertTarget) return; + const body = convertTarget.type === 'kanban' + ? { type: 'kanban', columnId: detail.columnId } + : { type: 'calendar', calendarId: detail.calendarId, startsAt: detail.startsAt, endsAt: detail.endsAt, allDay: detail.allDay }; + try { + const res = await fetch(`/api/messages/${convertTarget.messageId}/convert`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (res.ok) await chat?.refresh(); + } catch { /* stille feil */ } + convertTarget = null; + } + function scrollToBottom() { requestAnimationFrame(() => { messagesEl?.scrollTo(0, messagesEl.scrollHeight); @@ -97,10 +150,11 @@ onMount(() => { if (channelId) { const user = $page.data.user; + const workspaceId = $page.data.workspace?.id; chat = createChat(channelId, { id: user?.id ?? 'anonymous', name: user?.name ?? 'Ukjent' - }); + }, workspaceId); } return () => chat?.destroy(); }); @@ -131,15 +185,31 @@
{chat.error}
{/if} + {#if replyingTo} +
+ ↩ Svar til {replyingTo.author_name ?? 'Ukjent'} + +
+ {/if} +
+ + {#if convertTarget} + convertTarget = null} + /> + {/if} {/if} diff --git a/web/src/lib/components/MessageBox.svelte b/web/src/lib/components/MessageBox.svelte index a615219..be778cb 100644 --- a/web/src/lib/components/MessageBox.svelte +++ b/web/src/lib/components/MessageBox.svelte @@ -1,5 +1,6 @@ {#if mode === 'expanded'} -
+
+ {#if message.reply_to && message.parent_author_name} + + +
{ e.stopPropagation(); const el = document.getElementById(`msg-${message.reply_to}`); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }}> + ↩ {message.parent_author_name}: {truncate(stripHtml(message.parent_body ?? ''), 50)} +
+ {/if} {#if showAuthor || showTimestamp}
{#if showAuthor} @@ -79,19 +143,58 @@ {#if showTimestamp} {formatTime(message.created_at)} {/if} - {#if callbacks.onTogglePin} - - {:else if message.pinned} - 📌 - {/if} + + {#if isOwnMessage && !editing} + + {#if callbacks.onEdit} + + {/if} + {#if callbacks.onDelete} + + {/if} + + {/if} + + {#if callbacks.onReply} + + {/if} + {#if callbacks.onConvertToKanban} + + {/if} + {#if callbacks.onConvertToCalendar} + + {/if} + + {#if callbacks.onTogglePin} + + {:else if message.pinned} + 📌 + {/if} +
{/if} -
{@html message.body}
+ {#if editing} + +
{ if (e.key === 'Escape') cancelEdit(); }}> + submitEdit(html)} + /> +
+ Enter = lagre · Esc = avbryt + +
+
+ {:else} +
{@html message.body}
+ {/if} {#if hasReactions || callbacks.onReaction}
{#each message.reactions ?? [] as r} @@ -101,9 +204,9 @@ onclick={(e) => handleReaction(e, r.reaction)} >{r.reaction} {r.count} {/each} - {#if callbacks.onReaction} + {#if callbacks.onReaction && availableReactions.length > 0} - {#each QUICK_REACTIONS as emoji} + {#each availableReactions as emoji}