add message rendering, fix sse cancling, add user id copying

This commit is contained in:
Soph :3 2026-01-04 21:41:24 +02:00
parent f1539bdffa
commit 3a0f096ade
6 changed files with 155 additions and 42 deletions

View file

@ -1,22 +1,34 @@
<script lang="ts"> <script lang="ts">
import { Status, type UserWithStatus } from '$lib'; import { Status, type UserWithStatus } from '$lib';
const { const { onclick, user }: { onclick?: (e: MouseEvent) => void; user: UserWithStatus } = $props();
onclick,
user
}: { onclick?: (e: MouseEvent) => void, user: UserWithStatus } = $props();
</script> </script>
<a {onclick} href="##" class="flex items-center gap-2"> <a
<div class="relative"> {onclick}
<img src={"https://api.dicebear.com/7.x/pixel-art/svg?seed=" + user.username} alt={user.username} class="size-6 rounded-full" /> oncontextmenu={async (e) => {
{#if user.status === Status.OFFLINE} e.preventDefault();
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-gray-500 ring-1 ring-white"></span> await navigator.clipboard.writeText(user.id);
{:else if user.status === Status.DND} }}
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-red-500 ring-1 ring-white"></span> href="##"
{:else if user.status === Status.ONLINE} class="flex items-center gap-2"
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-green-500 ring-1 ring-white"></span> >
{/if} <div class="relative">
</div> <img
{user.username} src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + user.username}
alt={user.username}
class="size-6 rounded-full"
/>
{#if user.status === Status.OFFLINE}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-gray-500 ring-1 ring-white"
></span>
{:else if user.status === Status.DND}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-red-500 ring-1 ring-white"
></span>
{:else if user.status === Status.ONLINE}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-green-500 ring-1 ring-white"
></span>
{/if}
</div>
{user.username}
</a> </a>

View file

@ -4,6 +4,25 @@ import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function formatTimestamp(dateString: string) {
const date = new Date(dateString);
const now = new Date();
const isToday =
date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear();
const pad = (n: number) => n.toString().padStart(2, '0');
if (isToday) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
} else {
return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}, ${pad(
date.getHours()
)}:${pad(date.getMinutes())}`;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T; export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;

View file

@ -1,24 +0,0 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const { groupOrServerId, channelId } = params;
const isGroup = !channelId;
// fake messages
const messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: `user_${Math.floor(Math.random() * 10)}`,
content: isGroup ? `Group message #${i + 1}` : `Server message #${i + 1}`,
timestamp: Date.now() - Math.floor(Math.random() * 100000)
}));
return json({
type: isGroup ? 'group' : 'server',
groupId: isGroup ? groupOrServerId : null,
serverId: isGroup ? null : groupOrServerId,
channelId: channelId ?? null,
messages
});
};

View file

@ -0,0 +1,53 @@
import { fail, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { GroupID, ServerID, UserID } from '$lib';
export const GET: RequestHandler = async ({ params, locals }) => {
if (!locals.user) {
return new Response('No authentication', { status: 401 });
}
const { grp_srv_dm, channelId } = params;
if (!grp_srv_dm) {
return new Response('Missing group, server, or DM ID.', { status: 400 });
}
let messages = [];
let type = '';
if (GroupID.is(grp_srv_dm)) {
type = 'group';
messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: `user_${Math.floor(Math.random() * 10)}`,
content: 'group message ' + (i + 1),
timestamp: Date.now() - Math.floor(Math.random() * 100000)
}));
} else if (ServerID.is(grp_srv_dm)) {
type = 'server';
if (!channelId) {
return new Response('Missing channel ID.', { status: 400 });
}
messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: `user_${Math.floor(Math.random() * 10)}`,
content: 'server message ' + (i + 1),
timestamp: Date.now() - Math.floor(Math.random() * 100000)
}));
} else if (UserID.is(grp_srv_dm)) {
type = 'dms';
messages = Array.from({ length: 5 }, (_, i) => ({
id: crypto.randomUUID(),
authorId: Math.random() > 0.5 ? locals.user.id : grp_srv_dm,
content: 'dm message ' + (i + 1),
timestamp: Date.now() - Math.floor(Math.random() * 100000)
}));
}
return json({
type,
id: grp_srv_dm,
messages
});
};

View file

@ -66,6 +66,16 @@ export async function GET({ locals, request }) {
kvStore.set(`user-${userId}-state`, Status.OFFLINE); kvStore.set(`user-${userId}-state`, Status.OFFLINE);
_sendToSubscribers(userId, { type: 'status', id: userId, status: Status.OFFLINE }); _sendToSubscribers(userId, { type: 'status', id: userId, status: Status.OFFLINE });
}); });
},
cancel() {
console.log(`SSE Client cancelled. total: ${_clients.size}`);
if (_isUserConnected(userId)) return;
if (overwrite === Status.OFFLINE) return;
kvStore.set(`user-${userId}-state`, Status.OFFLINE);
_sendToSubscribers(userId, { type: 'status', id: userId, status: Status.OFFLINE });
} }
}); });

View file

@ -19,11 +19,14 @@
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js'; import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { ActionData } from './$types'; import type { ActionData } from './$types';
import { formatTimestamp } from '$lib/utils';
let errorOpen = $state(true); let errorOpen = $state(true);
let { form, data }: { form: ActionData; data: PageServerData } = $props(); let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null); let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state(); let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
let messages = $state([]);
const overview_data: OverviewData = $state({ const overview_data: OverviewData = $state({
friends: [], friends: [],
groups: [], groups: [],
@ -44,6 +47,20 @@
} }
}); });
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID)) return;
async function getMessages() {
const req = await fetch('/api/messages/' + currentPageID);
const data = await req.json();
messages = data.messages;
}
getMessages();
});
onMount(() => { onMount(() => {
async function run() { async function run() {
overview_data.servers = data.user.servers.map((z) => { overview_data.servers = data.user.servers.map((z) => {
@ -159,6 +176,32 @@
{/if} {/if}
{/if} {/if}
</header> </header>
<h1>this is like lowkirkounely the content, i should put messages and shi here</h1>
{#each messages as message, i (message.id)}
{#if i === 0 || messages[i - 1].authorId !== message.authorId}
<div class="flex gap-2 px-4 py-2">
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + message.authorId}
alt={message.authorId}
class="h-6 w-6 rounded-full"
/>
<div class="flex flex-col">
<div class="flex items-baseline gap-2">
<span class="font-semibold">{message.authorId}</span>
<span class="text-xs text-gray-400">
{formatTimestamp(message.timestamp)}
</span>
</div>
<div class="whitespace-pre-wrap">{message.content}</div>
</div>
</div>
{:else}
<div class="ml-8 flex gap-8 px-4 py-1">
<div class="whitespace-pre-wrap">{message.content}</div>
</div>
{/if}
{/each}
</Sidebar.Inset> </Sidebar.Inset>
</Sidebar.Provider> </Sidebar.Provider>