implement channels fully, implement servers fully, make dms impossible

to send if no longer friends, update overview information on
invalidation (form response recieved, friends update)
This commit is contained in:
Soph :3 2026-01-11 13:47:46 +02:00
parent 92a95cb365
commit 7af96ca084
8 changed files with 245 additions and 104 deletions

View file

@ -18,11 +18,13 @@
let {
currentPage = $bindable<string | null>(),
subPage = $bindable<string | null>(),
data,
user,
...restProps
}: {
currentPage: string | null;
subPage: string | null;
data: OverviewData;
user: SessionValidationResult['user'];
} = $props();
@ -254,6 +256,7 @@
onclick={(e) => {
e.preventDefault();
currentPage = friend.id;
subPage = null;
}}
user={friend}
></User>
@ -283,6 +286,7 @@
onclick={(e) => {
e.preventDefault();
currentPage = group.id;
subPage = null;
}}
href="##"
>
@ -309,14 +313,12 @@
<Sidebar.MenuSub>
{#each data.servers as server (server.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a
<Sidebar.MenuSubButton
onclick={(e) => {
e.preventDefault();
currentPage = server.id;
subPage = null;
}}
href="##"
class="flex items-center gap-2"
>
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
@ -324,9 +326,21 @@
class="size-6 rounded-full"
/>
{server.name}
</a>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{#each server.channels as channel (channel.id)}
<a
onclick={(e) => {
e.preventDefault();
currentPage = server.id;
subPage = channel.id;
}}
href="##"
class="flex items-center gap-2"
>
{channel.name}
</a>
{/each}
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>

View file

@ -3,6 +3,8 @@ import { definePrefix, type Puuid } from './puuid';
export const UserID = definePrefix('user');
export const GroupID = definePrefix('group');
export const ServerID = definePrefix('srv');
export const ChannelID = definePrefix('ch');
export const FriendRequestID = definePrefix('frq');
export const DirectMessageID = definePrefix('dmid');
@ -10,6 +12,7 @@ export type UserId = Puuid<'user'>;
export type GroupId = Puuid<'group'>;
export type ServerId = Puuid<'srv'>;
export type DirectMessageId = Puuid<'dmid'>;
export type ChannelId = Puuid<'ch'>;
export const Status: Record<string, 1 | 2 | 3> = {
OFFLINE: 1,
@ -23,6 +26,11 @@ export type OverviewUser = {
image: string;
dmId?: string;
};
export interface Channel {
id: string;
name: string;
}
export interface Message {
id: string;
authorId: string;
@ -42,6 +50,7 @@ export type OverviewServer = {
name: string;
ownerId: string;
image: string;
channels: Channel[];
};
export type OverviewGroup = {
id: string;

View file

@ -10,7 +10,8 @@ export function definePrefix<const P extends string>(prefix: P) {
return {
prefix,
is(value: string): value is Puuid<P> {
is(value: string | undefined | null): value is Puuid<P> {
if (!value) return false;
return value.startsWith(prefix + '_');
},

View file

@ -84,16 +84,38 @@ export async function validateSessionToken(token: string) {
})
);
const servers = (user.servers as string[]).length
let servers = (user.servers as string[]).length
? await db
.select({
id: table.server.id,
name: table.server.name,
ownerId: table.server.owner
ownerId: table.server.owner,
channels: table.server.channels
})
.from(table.server)
.where(inArray(table.server.id, user.servers as string[]))
: [];
servers = await Promise.all(
servers.map(async (z) => {
return {
...z,
channels: (
await Promise.all(
(z.channels as string[]).map(async (m) => {
const channel = await db.select().from(table.channel).where(eq(table.channel.id, m));
if (!channel || channel.length == 0) return;
return {
name: channel[0].name,
id: channel[0].id
};
})
)
).filter(Boolean)
};
})
);
const groups = (user.groups as string[]).length
? await db
.select({

View file

@ -149,8 +149,19 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
messages = (c.messages as Message[]) ?? [];
messages.push(message);
_sendToSubscribers(c.id, {
type: 'message',
id: c.id,
message: {
...message,
author: {
id: locals.user.id,
name: locals.user.username
}
}
});
await db.update(table.channel).set({ messages }).where(eq(table.channel.id, channelId));
return json({ type, id: c.id, messages });
} else if (DirectMessageID.is(grp_srv_dm)) {
type = 'dms';
const dm = (
@ -158,7 +169,12 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
)[0];
if (!dm) return new Response('DM not found.', { status: 404 });
const firstMember = await db.select().from(table.user).where(eq(table.user.id, dm.firstMember));
if (!firstMember || firstMember.length == 0)
return new Response('First member is invalid.', { status: 404 });
if (!(firstMember[0].friends as string[]).includes(dm.secondMember))
return new Response('You are no longer friends.', { status: 404 });
messages = (dm.messages as Message[]) ?? [];
messages.push(message);

View file

@ -1,7 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { _clients } from '../+server';
import { DirectMessageID } from '$lib';
import { ChannelID, DirectMessageID, UserID } from '$lib';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { or } from 'drizzle-orm';
@ -33,7 +33,7 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
return json({ error: 'Invalid subscribeTo value' }, { status: 400 });
}
let isValidDmid = false;
let isValid = false;
if (DirectMessageID.is(subscribeTo)) {
const find = await db
@ -47,13 +47,35 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
);
if (find?.length != 0) {
isValidDmid = true;
isValid = true;
}
}
if (UserID.is(subscribeTo)) {
const find = await db
.select()
.from(table.user)
.where(or(eq(table.user.id, subscribeTo)));
if (find?.length != 0) {
isValid = true;
}
}
if (ChannelID.is(subscribeTo)) {
const find = await db
.select()
.from(table.channel)
.where(or(eq(table.channel.id, subscribeTo)));
if (find?.length != 0) {
isValid = true;
}
}
if (
!(
isValidDmid ||
isValid ||
locals.user.groups.find((z) => z.id === subscribeTo) ||
locals.user.servers.find((z) => z.id === subscribeTo)
)

View file

@ -8,6 +8,7 @@ import { eq } from 'drizzle-orm';
import { and } from 'drizzle-orm';
import { type User } from '$lib/server/db/schema';
import { _sendToSubscribers } from '../api/updates/+server';
import { _findDmId } from '../api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server';
export const load: PageServerLoad = async () => {
const user = requireLogin();
return { user };

View file

@ -12,7 +12,10 @@
type OverviewGroup,
type OverviewServer,
type UserWithStatus,
type ReturnMessage
type ReturnMessage,
type Channel,
type ChannelId,
ChannelID
} from '$lib';
import type { PageServerData } from './$types';
import AppSidebar from '$lib/components/app-sidebar.svelte';
@ -27,13 +30,17 @@
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 User from '$lib/components/extra/User.svelte';
import { invalidate, invalidateAll } from '$app/navigation';
let errorOpen = $state(true);
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 ownerId: string | null = $state(null);
let currentSubPage: Channel | null = $state(null);
let isMembersTabOpen = $state(true);
let members: UserWithStatus[] = $state([]);
@ -49,74 +56,14 @@
servers: []
});
$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);
}
} 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();
ownerId = (currentPage as OverviewGroup | OverviewServer).ownerId;
} else {
isMembersTabOpen = false;
}
});
$effect(() => {
if (!currentPageID || !currentPage) return;
if (ServerID.is(currentPageID)) return;
async function getMessages() {
const targetId = currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID;
const req = await fetch('/api/messages/' + targetId);
const data = await req.json();
messages = data.messages;
if (previousSubscription && previousSubscription != targetId) {
await fetch('/api/updates/' + sessionId, {
body: JSON.stringify({ subscribeTo: previousSubscription }),
method: 'DELETE'
});
}
if (previousSubscription != targetId) {
await fetch('/api/updates/' + sessionId, {
body: JSON.stringify({ subscribeTo: targetId }),
method: 'POST'
});
}
previousSubscription = currentPageID;
}
getMessages();
});
onMount(() => {
async function run() {
async function fill_overview_data() {
overview_data.servers = data.user.servers.map((z) => {
console.log(z);
return {
id: ServerID.parse(z.id),
name: z.name,
ownerId: z.ownerId,
channels: z.channels as Channel[],
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
};
});
@ -151,6 +98,92 @@
})
);
}
$effect(() => {
console.log(currentPageID, currentSubPageID);
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) {
console.log((currentPage as OverviewServer).channels.find((z) => z.id == 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 = '';
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;
}
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) {
await invalidateAll();
}
await fill_overview_data();
}
run();
});
@ -158,7 +191,7 @@
onMount(() => {
const sse = new EventSource('/api/updates');
sse.addEventListener('message', (e) => {
sse.addEventListener('message', async (e) => {
const json = JSON.parse(e.data) as
| { type: 'connected'; sessionId: string }
| { type: 'message'; message: ReturnMessage }
@ -166,9 +199,10 @@
| { type: 'friends'; status: string };
if (json.type == 'friends') {
alert(json.status);
location.reload();
await invalidateAll();
await fill_overview_data();
}
if (json.type == 'connected') {
console.log('SSE connected. We are sessionID ' + json.sessionId);
sessionId = json.sessionId;
@ -207,17 +241,27 @@
{/if}
<Sidebar.Provider>
<AppSidebar bind:currentPage={currentPageID} user={data.user} data={overview_data} />
<AppSidebar
bind:subPage={currentSubPageID}
bind:currentPage={currentPageID}
user={data.user}
data={overview_data}
/>
<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" />
<h1>{server!.name}</h1>
{#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 UserWithStatus}
@ -252,6 +296,11 @@
{/if}
{/if}
</header>
{#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID}
<h1>add invite creation, role creation, moderation, et cetera to this page.</h1>
{/if}
{#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))}
<div class="h-min shrink overflow-scroll">
{#each messages as message, i (message.id)}
{#if i === 0 || messages[i - 1].authorId !== message.authorId}
@ -280,18 +329,25 @@
{/if}
{/each}
</div>
{#if currentPageID && currentPage}
<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;
const targetId =
currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID;
inputValue = '';
const req = await fetch('/api/messages/' + targetId, {
const req = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: message })