- 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>
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { sql } from '$lib/server/db';
|
|
|
|
export const GET: RequestHandler = async ({ params, url, locals }) => {
|
|
if (!locals.workspace || !locals.user) error(401);
|
|
|
|
const channelId = params.id;
|
|
const after = url.searchParams.get('after');
|
|
const limit = Math.min(Number(url.searchParams.get('limit') ?? 50), 100);
|
|
|
|
// Verifiser at kanalen tilhører workspace
|
|
const [channel] = await sql`
|
|
SELECT c.id FROM channels c
|
|
JOIN nodes n ON n.id = c.id
|
|
WHERE c.id = ${channelId} AND n.workspace_id = ${locals.workspace.id}
|
|
`;
|
|
if (!channel) error(404, 'Kanal ikke funnet');
|
|
|
|
const currentUserId = locals.user.id;
|
|
|
|
const messages = after
|
|
? await sql`
|
|
SELECT m.id, m.channel_id, m.body, m.message_type, m.title,
|
|
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
|
|
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}
|
|
ORDER BY m.created_at ASC
|
|
LIMIT ${limit}
|
|
`
|
|
: await sql`
|
|
SELECT m.id, m.channel_id, m.body, m.message_type, m.title,
|
|
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
|
|
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}
|
|
ORDER BY m.created_at DESC
|
|
LIMIT ${limit}
|
|
`.then((rows) => rows.reverse());
|
|
|
|
return json(messages);
|
|
};
|
|
|
|
export const POST: RequestHandler = async ({ params, request, locals }) => {
|
|
if (!locals.workspace || !locals.user) error(401);
|
|
const workspace = locals.workspace;
|
|
const user = locals.user;
|
|
|
|
const channelId = params.id;
|
|
const { body, replyTo, mentions } = await request.json();
|
|
|
|
if (!body || typeof body !== 'string' || body.trim().length === 0) {
|
|
error(400, 'Melding kan ikke være tom');
|
|
}
|
|
|
|
// Verifiser at kanalen tilhører workspace
|
|
const [channel] = await sql`
|
|
SELECT c.id FROM channels c
|
|
JOIN nodes n ON n.id = c.id
|
|
WHERE c.id = ${channelId} AND n.workspace_id = ${workspace.id}
|
|
`;
|
|
if (!channel) error(404, 'Kanal ikke funnet');
|
|
|
|
// Opprett node + melding i PG
|
|
const [message] = await sql`
|
|
WITH new_node AS (
|
|
INSERT INTO nodes (workspace_id, node_type)
|
|
VALUES (${workspace.id}, 'melding')
|
|
RETURNING id
|
|
)
|
|
INSERT INTO messages (id, channel_id, author_id, body, reply_to)
|
|
SELECT new_node.id, ${channelId}, ${user.id}, ${body.trim()}, ${replyTo ?? null}
|
|
FROM new_node
|
|
RETURNING id, body, message_type, created_at, reply_to
|
|
`;
|
|
|
|
// Opprett MENTIONS-edges for hver #-mention
|
|
if (Array.isArray(mentions) && mentions.length > 0) {
|
|
const entityIds = mentions
|
|
.map((m: { id?: string }) => m.id)
|
|
.filter((id): id is string => typeof id === 'string' && id !== message.id);
|
|
|
|
if (entityIds.length > 0) {
|
|
// Verifiser at alle nevnte entiteter tilhører workspace
|
|
const validEntities = await sql`
|
|
SELECT id FROM nodes
|
|
WHERE id = ANY(${entityIds}) AND workspace_id = ${workspace.id}
|
|
`;
|
|
const validIds = new Set(validEntities.map((e) => (e as { id: string }).id));
|
|
|
|
for (const entityId of entityIds) {
|
|
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}, ${message.id}, ${entityId}, 'MENTIONS', ${user.id}, 'user')
|
|
ON CONFLICT (source_id, target_id, relation_type) DO NOTHING
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return json({
|
|
...message,
|
|
author_name: user.name,
|
|
author_id: user.id
|
|
}, { status: 201 });
|
|
};
|