374 lines
12 KiB
Svelte
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 MainSidebar from '$lib/components/main-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 = subscribe;
|
|
}
|
|
|
|
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>
|
|
<MainSidebar
|
|
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>
|