MessageBox: API-utvidelser, reaksjoner og pin-toggle
- GET messages returnerer alle felter + aggregerte reaksjoner via LATERAL join - Nytt PATCH/DELETE endepunkt for enkeltmeldinger (/api/messages/[id]) - Nye reaksjons-endepunkter (POST/DELETE /api/messages/[id]/reactions) - refresh() eksponert på ChatConnection (PG + SpacetimeDB) - MessageBox UI: reaksjonspills med toggle + hurtig-emojis ved hover - Pin-toggle i header med hover-synlighet - ChatBlock wirer onReaction og onTogglePin callbacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
568c385cd0
commit
5bc992272d
9 changed files with 342 additions and 7 deletions
|
|
@ -17,7 +17,25 @@
|
||||||
let messagesEl: HTMLDivElement | undefined;
|
let messagesEl: HTMLDivElement | undefined;
|
||||||
|
|
||||||
const chatCallbacks = {
|
const chatCallbacks = {
|
||||||
onMentionClick: (entityId: string) => goto(`/entities/${entityId}`)
|
onMentionClick: (entityId: string) => goto(`/entities/${entityId}`),
|
||||||
|
onReaction: async (messageId: string, reaction: string) => {
|
||||||
|
const msg = chat?.messages.find(m => m.id === messageId);
|
||||||
|
const existing = msg?.reactions?.find(r => r.reaction === reaction);
|
||||||
|
await fetch(`/api/messages/${messageId}/reactions`, {
|
||||||
|
method: existing?.user_reacted ? 'DELETE' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ reaction })
|
||||||
|
});
|
||||||
|
await chat?.refresh();
|
||||||
|
},
|
||||||
|
onTogglePin: async (messageId: string, pinned: boolean) => {
|
||||||
|
await fetch(`/api/messages/${messageId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ pinned })
|
||||||
|
});
|
||||||
|
await chat?.refresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleSubmit(html: string, json: Record<string, unknown>, mentions: Array<{ id: string; name: string; type: string; aliases: string[] }>) {
|
async function handleSubmit(html: string, json: Record<string, unknown>, mentions: Array<{ id: string; name: string; type: string; aliases: string[] }>) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export function createPgChat(channelId: string): ChatConnection {
|
||||||
visibility: (raw.visibility as 'workspace' | 'private') ?? 'workspace',
|
visibility: (raw.visibility as 'workspace' | 'private') ?? 'workspace',
|
||||||
created_at: raw.created_at as string,
|
created_at: raw.created_at as string,
|
||||||
updated_at: (raw.updated_at as string) ?? (raw.created_at as string),
|
updated_at: (raw.updated_at as string) ?? (raw.created_at as string),
|
||||||
|
reactions: (raw.reactions as MessageData['reactions']) ?? [],
|
||||||
kanban_view: null,
|
kanban_view: null,
|
||||||
calendar_view: null
|
calendar_view: null
|
||||||
};
|
};
|
||||||
|
|
@ -76,6 +77,7 @@ export function createPgChat(channelId: string): ChatConnection {
|
||||||
get error() { return error; },
|
get error() { return error; },
|
||||||
get connected() { return connected; },
|
get connected() { return connected; },
|
||||||
send,
|
send,
|
||||||
|
refresh,
|
||||||
destroy
|
destroy
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export function createSpacetimeChat(
|
||||||
visibility: (raw.visibility as 'workspace' | 'private') ?? 'workspace',
|
visibility: (raw.visibility as 'workspace' | 'private') ?? 'workspace',
|
||||||
created_at: raw.created_at as string,
|
created_at: raw.created_at as string,
|
||||||
updated_at: (raw.updated_at as string) ?? (raw.created_at as string),
|
updated_at: (raw.updated_at as string) ?? (raw.created_at as string),
|
||||||
|
reactions: (raw.reactions as MessageData['reactions']) ?? [],
|
||||||
kanban_view: null,
|
kanban_view: null,
|
||||||
calendar_view: null
|
calendar_view: null
|
||||||
};
|
};
|
||||||
|
|
@ -188,6 +189,7 @@ export function createSpacetimeChat(
|
||||||
get error() { return error; },
|
get error() { return error; },
|
||||||
get connected() { return connected; },
|
get connected() { return connected; },
|
||||||
send,
|
send,
|
||||||
|
refresh: loadFromPg,
|
||||||
destroy
|
destroy
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,6 @@ export interface ChatConnection {
|
||||||
readonly error: string;
|
readonly error: string;
|
||||||
readonly connected: boolean;
|
readonly connected: boolean;
|
||||||
send(body: string, mentions?: MentionRef[]): Promise<void>;
|
send(body: string, mentions?: MentionRef[]): Promise<void>;
|
||||||
|
refresh(): Promise<void>;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,27 @@
|
||||||
callbacks.onClick?.(message.id);
|
callbacks.onClick?.(message.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUICK_REACTIONS = ['👍', '❤️', '🔥', '👀', '💯'];
|
||||||
|
|
||||||
let hasBadges = $derived(
|
let hasBadges = $derived(
|
||||||
(mode === 'expanded' && (message.kanban_view || message.calendar_view))
|
(mode === 'expanded' && (message.kanban_view || message.calendar_view))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let hasReactions = $derived(
|
||||||
|
mode === 'expanded' && message.reactions && message.reactions.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
let bodyPreview = $derived(truncate(stripHtml(message.body), 80));
|
let bodyPreview = $derived(truncate(stripHtml(message.body), 80));
|
||||||
|
|
||||||
|
function handleReaction(e: MouseEvent, reaction: string) {
|
||||||
|
e.stopPropagation();
|
||||||
|
callbacks.onReaction?.(message.id, reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTogglePin(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
callbacks.onTogglePin?.(message.id, !message.pinned);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if mode === 'expanded'}
|
{#if mode === 'expanded'}
|
||||||
|
|
@ -63,12 +79,40 @@
|
||||||
{#if showTimestamp}
|
{#if showTimestamp}
|
||||||
<span class="messagebox__time">{formatTime(message.created_at)}</span>
|
<span class="messagebox__time">{formatTime(message.created_at)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if message.pinned}
|
{#if callbacks.onTogglePin}
|
||||||
<span class="messagebox__pinned" title="Festet">📌</span>
|
<button
|
||||||
|
class="messagebox__pin-toggle"
|
||||||
|
class:messagebox__pin-toggle--active={message.pinned}
|
||||||
|
title={message.pinned ? 'Løsne' : 'Fest'}
|
||||||
|
onclick={handleTogglePin}
|
||||||
|
>📌</button>
|
||||||
|
{:else if message.pinned}
|
||||||
|
<span class="messagebox__pinned" title="Festet">📌</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="messagebox__body">{@html message.body}</div>
|
<div class="messagebox__body">{@html message.body}</div>
|
||||||
|
{#if hasReactions || callbacks.onReaction}
|
||||||
|
<div class="messagebox__reactions">
|
||||||
|
{#each message.reactions ?? [] as r}
|
||||||
|
<button
|
||||||
|
class="messagebox__reaction-pill"
|
||||||
|
class:messagebox__reaction-pill--active={r.user_reacted}
|
||||||
|
onclick={(e) => handleReaction(e, r.reaction)}
|
||||||
|
>{r.reaction} {r.count}</button>
|
||||||
|
{/each}
|
||||||
|
{#if callbacks.onReaction}
|
||||||
|
<span class="messagebox__reaction-add">
|
||||||
|
{#each QUICK_REACTIONS as emoji}
|
||||||
|
<button
|
||||||
|
class="messagebox__reaction-quick"
|
||||||
|
onclick={(e) => handleReaction(e, emoji)}
|
||||||
|
>{emoji}</button>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if hasBadges}
|
{#if hasBadges}
|
||||||
<div class="messagebox__badges">
|
<div class="messagebox__badges">
|
||||||
{#if message.kanban_view}
|
{#if message.kanban_view}
|
||||||
|
|
@ -141,6 +185,83 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messagebox__pin-toggle {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
margin-left: auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__pin-toggle--active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox--expanded:hover .messagebox__pin-toggle {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox--expanded:hover .messagebox__pin-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Reactions === */
|
||||||
|
.messagebox__reactions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-pill {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: #e1e4e8;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-pill:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-pill--active {
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
background: rgba(125, 211, 252, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-add {
|
||||||
|
display: none;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox--expanded:hover .messagebox__reaction-add {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-quick {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.05rem 0.15rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messagebox__reaction-quick:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.messagebox__body {
|
.messagebox__body {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #e1e4e8;
|
color: #e1e4e8;
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,6 @@ export type MessageBoxMode = 'compact' | 'calendar' | 'expanded';
|
||||||
export interface MessageBoxCallbacks {
|
export interface MessageBoxCallbacks {
|
||||||
onClick?: (id: string) => void;
|
onClick?: (id: string) => void;
|
||||||
onMentionClick?: (entityId: string) => void;
|
onMentionClick?: (entityId: string) => void;
|
||||||
|
onReaction?: (messageId: string, reaction: string) => void;
|
||||||
|
onTogglePin?: (messageId: string, pinned: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,57 @@ export const GET: RequestHandler = async ({ params, url, locals }) => {
|
||||||
`;
|
`;
|
||||||
if (!channel) error(404, 'Kanal ikke funnet');
|
if (!channel) error(404, 'Kanal ikke funnet');
|
||||||
|
|
||||||
|
const currentUserId = locals.user.id;
|
||||||
|
|
||||||
const messages = after
|
const messages = after
|
||||||
? await sql`
|
? await sql`
|
||||||
SELECT m.id, m.body, m.message_type, m.created_at, m.reply_to,
|
SELECT m.id, m.channel_id, m.body, m.message_type, m.title,
|
||||||
u.display_name as author_name, u.authentik_id as author_id
|
m.pinned, m.visibility, m.created_at, m.updated_at, m.reply_to,
|
||||||
|
u.display_name as author_name, u.authentik_id as author_id,
|
||||||
|
COALESCE(r.reactions, '[]'::jsonb) as reactions
|
||||||
FROM messages m
|
FROM messages m
|
||||||
LEFT JOIN users u ON u.authentik_id = m.author_id
|
LEFT JOIN users u ON u.authentik_id = m.author_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT jsonb_agg(jsonb_build_object(
|
||||||
|
'reaction', sub.reaction,
|
||||||
|
'count', sub.cnt,
|
||||||
|
'user_reacted', sub.user_reacted
|
||||||
|
)) as reactions
|
||||||
|
FROM (
|
||||||
|
SELECT mr.reaction,
|
||||||
|
count(*)::int as cnt,
|
||||||
|
bool_or(mr.user_id = ${currentUserId}) as user_reacted
|
||||||
|
FROM message_reactions mr
|
||||||
|
WHERE mr.message_id = m.id
|
||||||
|
GROUP BY mr.reaction
|
||||||
|
) sub
|
||||||
|
) r ON true
|
||||||
WHERE m.channel_id = ${channelId} AND m.created_at > ${after}
|
WHERE m.channel_id = ${channelId} AND m.created_at > ${after}
|
||||||
ORDER BY m.created_at ASC
|
ORDER BY m.created_at ASC
|
||||||
LIMIT ${limit}
|
LIMIT ${limit}
|
||||||
`
|
`
|
||||||
: await sql`
|
: await sql`
|
||||||
SELECT m.id, m.body, m.message_type, m.created_at, m.reply_to,
|
SELECT m.id, m.channel_id, m.body, m.message_type, m.title,
|
||||||
u.display_name as author_name, u.authentik_id as author_id
|
m.pinned, m.visibility, m.created_at, m.updated_at, m.reply_to,
|
||||||
|
u.display_name as author_name, u.authentik_id as author_id,
|
||||||
|
COALESCE(r.reactions, '[]'::jsonb) as reactions
|
||||||
FROM messages m
|
FROM messages m
|
||||||
LEFT JOIN users u ON u.authentik_id = m.author_id
|
LEFT JOIN users u ON u.authentik_id = m.author_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT jsonb_agg(jsonb_build_object(
|
||||||
|
'reaction', sub.reaction,
|
||||||
|
'count', sub.cnt,
|
||||||
|
'user_reacted', sub.user_reacted
|
||||||
|
)) as reactions
|
||||||
|
FROM (
|
||||||
|
SELECT mr.reaction,
|
||||||
|
count(*)::int as cnt,
|
||||||
|
bool_or(mr.user_id = ${currentUserId}) as user_reacted
|
||||||
|
FROM message_reactions mr
|
||||||
|
WHERE mr.message_id = m.id
|
||||||
|
GROUP BY mr.reaction
|
||||||
|
) sub
|
||||||
|
) r ON true
|
||||||
WHERE m.channel_id = ${channelId}
|
WHERE m.channel_id = ${channelId}
|
||||||
ORDER BY m.created_at DESC
|
ORDER BY m.created_at DESC
|
||||||
LIMIT ${limit}
|
LIMIT ${limit}
|
||||||
|
|
|
||||||
94
web/src/routes/api/messages/[messageId]/+server.ts
Normal file
94
web/src/routes/api/messages/[messageId]/+server.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** Trekk ut entity-UUIDs fra Tiptap HTML mentions (data-id attributter) */
|
||||||
|
function extractMentionIds(html: string): string[] {
|
||||||
|
const ids: string[] = [];
|
||||||
|
const regex = /data-id="([0-9a-f-]{36})"/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
if (!ids.includes(match[1])) ids.push(match[1]);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PATCH /api/messages/:messageId — Oppdater melding (pin, visibility, title, body) */
|
||||||
|
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
const workspace = locals.workspace;
|
||||||
|
const user = locals.user;
|
||||||
|
const messageId = params.messageId;
|
||||||
|
|
||||||
|
const updates = await request.json();
|
||||||
|
|
||||||
|
// Verifiser at meldingen tilhører workspace
|
||||||
|
const [msg] = await sql`
|
||||||
|
SELECT m.id FROM messages m
|
||||||
|
JOIN nodes n ON n.id = m.id
|
||||||
|
WHERE m.id = ${messageId} AND n.workspace_id = ${workspace.id}
|
||||||
|
`;
|
||||||
|
if (!msg) error(404, 'Melding ikke funnet');
|
||||||
|
|
||||||
|
const bodyChanged = updates.body !== undefined;
|
||||||
|
|
||||||
|
const [updated] = await sql`
|
||||||
|
UPDATE messages SET
|
||||||
|
pinned = COALESCE(${updates.pinned ?? null}::boolean, pinned),
|
||||||
|
visibility = COALESCE(${updates.visibility ?? null}::text, visibility),
|
||||||
|
title = COALESCE(${updates.title ?? null}::text, title),
|
||||||
|
body = CASE WHEN ${bodyChanged} THEN ${updates.body ?? ''} ELSE body END,
|
||||||
|
edited_at = CASE WHEN ${bodyChanged} THEN now() ELSE edited_at END
|
||||||
|
WHERE id = ${messageId}
|
||||||
|
RETURNING id, channel_id, body, message_type, title, pinned, visibility,
|
||||||
|
created_at, updated_at, reply_to
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Synkroniser MENTIONS-edges ved body-endring
|
||||||
|
if (bodyChanged) {
|
||||||
|
const mentionIds = extractMentionIds(updates.body ?? '');
|
||||||
|
|
||||||
|
let validIds: Set<string> = new Set();
|
||||||
|
if (mentionIds.length > 0) {
|
||||||
|
const validEntities = await sql`
|
||||||
|
SELECT id FROM nodes
|
||||||
|
WHERE id = ANY(${mentionIds}) AND workspace_id = ${workspace.id}
|
||||||
|
`;
|
||||||
|
validIds = new Set(validEntities.map((e) => (e as { id: string }).id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
DELETE FROM graph_edges
|
||||||
|
WHERE source_id = ${messageId} AND relation_type = 'MENTIONS'
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const entityId of mentionIds) {
|
||||||
|
if (!validIds.has(entityId)) continue;
|
||||||
|
await sql`
|
||||||
|
INSERT INTO graph_edges (workspace_id, source_id, target_id, relation_type, created_by, origin)
|
||||||
|
VALUES (${workspace.id}, ${messageId}, ${entityId}, 'MENTIONS', ${user.id}, 'user')
|
||||||
|
ON CONFLICT (source_id, target_id, relation_type) DO NOTHING
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DELETE /api/messages/:messageId — Slett melding */
|
||||||
|
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
const messageId = params.messageId;
|
||||||
|
|
||||||
|
// Verifiser at meldingen tilhører workspace
|
||||||
|
const [msg] = await sql`
|
||||||
|
SELECT n.id FROM nodes n
|
||||||
|
WHERE n.id = ${messageId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!msg) error(404, 'Melding ikke funnet');
|
||||||
|
|
||||||
|
// Slett noden — kaskaderer til messages, reactions, edges
|
||||||
|
await sql`DELETE FROM nodes WHERE id = ${messageId}`;
|
||||||
|
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
};
|
||||||
59
web/src/routes/api/messages/[messageId]/reactions/+server.ts
Normal file
59
web/src/routes/api/messages/[messageId]/reactions/+server.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** Hent aggregerte reaksjoner for en melding */
|
||||||
|
async function getReactions(messageId: string, userId: string) {
|
||||||
|
return await sql`
|
||||||
|
SELECT reaction, count(*)::int as count,
|
||||||
|
bool_or(user_id = ${userId}) as user_reacted
|
||||||
|
FROM message_reactions
|
||||||
|
WHERE message_id = ${messageId}
|
||||||
|
GROUP BY reaction
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/messages/:messageId/reactions — Legg til reaksjon */
|
||||||
|
export const POST: RequestHandler = async ({ params, request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
const messageId = params.messageId;
|
||||||
|
const userId = locals.user.id;
|
||||||
|
|
||||||
|
const { reaction } = await request.json();
|
||||||
|
if (!reaction || typeof reaction !== 'string') error(400, 'Reaksjon mangler');
|
||||||
|
|
||||||
|
// Verifiser at meldingen tilhører workspace
|
||||||
|
const [msg] = await sql`
|
||||||
|
SELECT m.id FROM messages m
|
||||||
|
JOIN nodes n ON n.id = m.id
|
||||||
|
WHERE m.id = ${messageId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!msg) error(404, 'Melding ikke funnet');
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
INSERT INTO message_reactions (message_id, user_id, reaction)
|
||||||
|
VALUES (${messageId}, ${userId}, ${reaction})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
|
||||||
|
const reactions = await getReactions(messageId, userId);
|
||||||
|
return json(reactions, { status: 201 });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DELETE /api/messages/:messageId/reactions — Fjern reaksjon */
|
||||||
|
export const DELETE: RequestHandler = async ({ params, request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
const messageId = params.messageId;
|
||||||
|
const userId = locals.user.id;
|
||||||
|
|
||||||
|
const { reaction } = await request.json();
|
||||||
|
if (!reaction || typeof reaction !== 'string') error(400, 'Reaksjon mangler');
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
DELETE FROM message_reactions
|
||||||
|
WHERE message_id = ${messageId} AND user_id = ${userId} AND reaction = ${reaction}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const reactions = await getReactions(messageId, userId);
|
||||||
|
return json(reactions);
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue