chat.sad.ovh/src/routes/app/+page.svelte
fucksophie f1dcd0cfc5 deleting of servers, invites, make a different file for dashboard, make
invites a new citizen, partly fix PSD type, switch to glass and 9x
dicebear
2026-01-16 21:24:35 +02:00

374 lines
12 KiB
Svelte

<script lang="ts">
import {
GroupID,
UserID,
ServerID,
type GroupId,
type ServerId,
type UserId,
type OverviewUser,
type OverviewGroup,
type OverviewServer,
type ReturnMessage,
type Channel,
type ChannelId,
ChannelID,
statuses
} from '$lib';
import type { PageServerData } from './$types';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { onMount } from 'svelte';
import type { ActionData } from './$types';
import { formatTimestamp } from '$lib/utils';
import Input from '$lib/components/ui/input/input.svelte';
import { Button } from '$lib/components/ui/button';
import ServerDashboard from './ServerDashboard.svelte';
import SendHorizontal from '@lucide/svelte/icons/send-horizontal';
import PersonStanding from '@lucide/svelte/icons/person-standing';
import MemberSidebar from '$lib/components/member-sidebar.svelte';
import { invalidateAll } from '$app/navigation';
import { toast } from 'svelte-sonner';
import { fill_overview_data, overview_data } from '$lib/state.svelte';
let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
let currentSubPageID: ChannelId | null = $state(null);
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
let currentSubPage: Channel | null = $state(null);
let sse: EventSource | undefined;
let messagesElement: HTMLDivElement | undefined = $state();
let isMembersTabOpen = $state(true);
let members: OverviewUser[] = $state([]);
let messages: ReturnMessage[] = $state([]);
let inputValue = $state();
let sessionId: string | undefined = $state();
let previousSubscription: UserId | GroupId | ServerId | null = $state(null);
$effect(() => {
if (currentPageID) {
if (UserID.is(currentPageID)) {
currentPage = overview_data.friends.find((friend) => friend.id === currentPageID);
} else if (GroupID.is(currentPageID)) {
currentPage = overview_data.groups.find((group) => group.id === currentPageID);
} else if (ServerID.is(currentPageID)) {
currentPage = overview_data.servers.find((server) => server.id === currentPageID);
}
if (currentSubPageID) {
currentSubPage = (currentPage as OverviewServer).channels.find(
(z) => z.id == currentSubPageID
) as Channel | null;
} else {
currentSubPage = null;
}
} else {
currentPage = undefined;
}
});
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID) || GroupID.is(currentPageID)) {
async function fetchMembers() {
const req = await fetch(`/api/members/${currentPageID}`);
const data = await req.json();
members = data.members;
}
fetchMembers();
} else {
isMembersTabOpen = false;
}
});
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID) && !ChannelID.is(currentSubPageID)) return;
async function getMessages() {
let path = '';
let subscribe: string | null | undefined = '';
if (ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) {
path = '/api/messages/' + currentPageID + '/' + currentSubPageID;
subscribe = currentSubPageID;
} else {
subscribe = currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID;
path = '/api/messages/' + subscribe;
}
if (!subscribe) return;
const req = await fetch(path);
const data = await req.json();
messages = data.messages;
if (previousSubscription && previousSubscription != subscribe) {
await fetch('/api/updates/' + sessionId, {
body: JSON.stringify({ subscribeTo: previousSubscription }),
method: 'DELETE'
});
}
if (previousSubscription != subscribe) {
await fetch('/api/updates/' + sessionId, {
body: JSON.stringify({ subscribeTo: subscribe }),
method: 'POST'
});
}
previousSubscription = currentPageID;
}
getMessages();
});
onMount(() => {
async function run() {
if (form) {
if (form.success) {
toast.success('Action succeeded.');
}
if (form.error) {
toast.error('Action had a error: ' + form.error);
}
await invalidateAll();
}
await fill_overview_data(data);
}
run();
});
onMount(() => {
if (!sse) {
sse = new EventSource('/api/updates');
}
sse.addEventListener('message', async (e) => {
const json = JSON.parse(e.data) as
| { type: 'connected'; sessionId: string }
| { type: 'message'; message: ReturnMessage }
| { type: 'status'; id: string; status: 1 | 2 | 3; statusMessage: string }
| { type: 'friends'; status: string }
| { type: 'group'; status: string }
| { type: 'username'; status: string }
| { type: 'server'; status: string };
if (json.type == 'friends') {
toast('Invalidation from friends updates, recieved ' + json.status);
await invalidateAll();
await fill_overview_data(data);
}
if (json.type == 'server') {
toast('Invalidation from servers updates, recieved ' + json.status);
await invalidateAll();
await fill_overview_data(data);
}
if (json.type == 'group') {
toast('Invalidation from group updates, recieved ' + json.status);
if (json.status == 'removed-from-group' && GroupID.is(currentPageID)) {
currentPageID = null;
currentPage = undefined;
}
await invalidateAll();
await fill_overview_data(data);
}
if (json.type == 'username') {
toast('Invalidation from username updates, recieved ' + json.status);
await invalidateAll();
await fill_overview_data(data);
}
if (json.type == 'connected') {
console.log('SSE connected. We are sessionID ' + json.sessionId);
sessionId = json.sessionId;
}
if (json.type == 'status') {
statuses.set(json.id, {
status: json.status,
statusMessage: json.statusMessage
});
}
if (json.type == 'message') {
if (!messagesElement) return;
if (
messagesElement.scrollTop ===
messagesElement.scrollHeight - messagesElement.offsetHeight
) {
setTimeout(() => {
if (!messagesElement) return;
messagesElement.scrollTop = messagesElement.scrollHeight;
}, 66.75); // FPS
}
messages.push(json.message);
}
});
});
</script>
<Sidebar.Provider>
<AppSidebar
psd={data}
bind:subPage={currentSubPageID}
bind:currentPage={currentPageID}
user={data.user}
/>
<Sidebar.Inset class="h-svh">
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<Sidebar.Trigger class="-ms-1" />
{#if currentPageID && currentPage}
{#if ServerID.is(currentPageID)}
{@const server = currentPage as OverviewServer}
{@const channel = currentSubPage as OverviewServer}
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
{#if currentSubPage}
<h1>{server!.name} - {channel.name}</h1>
{:else}
<h1>{server!.name} - Server Info</h1>
{/if}
{:else if UserID.is(currentPageID)}
{@const friend = currentPage as OverviewUser}
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
<h1>
{friend!.username}
</h1>
{:else if GroupID.is(currentPageID)}
{@const group = currentPage as OverviewGroup}
<h1>{group!.name} ({group.members} member{group.members > 1 ? 's' : ''})</h1>
{/if}
{#if ServerID.is(currentPageID) || GroupID.is(currentPageID)}
<Button
variant="outline"
onclick={() => {
isMembersTabOpen = !isMembersTabOpen;
}}
>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
<PersonStanding></PersonStanding>
</Button>
{/if}
{/if}
</header>
{#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID}
<ServerDashboard
bind:currentPage={currentPageID}
psd={data}
server={currentPage as OverviewServer}
></ServerDashboard>
{/if}
{#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))}
<div class="h-min shrink overflow-y-scroll" bind:this={messagesElement}>
{#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/9.x/glass/svg?seed=' + message.author.name}
alt={message.author.name}
class="h-6 w-6 rounded-full"
/>
<div class="flex flex-col">
<div class="flex items-baseline gap-2">
<span class="font-semibold">{message.author.name}</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}
</div>
<div class="flex shrink-0 gap-2 border-t p-2">
<Input
bind:value={inputValue}
onkeydown={async (e) => {
if (e.key == 'Enter') {
let path = '';
if (ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) {
path = '/api/messages/' + currentPageID + '/' + currentSubPageID;
} else {
path =
'/api/messages/' +
(currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID);
}
let message = inputValue;
inputValue = '';
const req = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: message })
});
if (!req.ok) {
console.error('Failed to send message');
}
}
}}
placeholder="input box for messages (ignore how ugly it is right now please)"
class="flex-1"
/>
<Button
onclick={async () => {
let message = inputValue;
const targetId =
currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID;
inputValue = '';
const req = await fetch('/api/messages/' + targetId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: message })
});
if (!req.ok) {
console.error('Failed to send message');
}
}}
>
<SendHorizontal></SendHorizontal>
</Button>
</div>
{/if}
</Sidebar.Inset>
<Sidebar.Provider class="w-0">
<Sidebar.Provider class="w-0">
{#if currentPage && currentPageID && overview_data && members && !UserID.is(currentPageID)}
<MemberSidebar
bind:open={isMembersTabOpen}
psd={data}
user={data.user}
{members}
currentEntity={currentPage as OverviewGroup | OverviewServer}
currentEntityId={currentPageID}
/>
{/if}
</Sidebar.Provider>
</Sidebar.Provider>
</Sidebar.Provider>