diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index b96f15b..17012ef 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -18,11 +18,13 @@ let { currentPage = $bindable(), + subPage = $bindable(), 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} > @@ -283,6 +286,7 @@ onclick={(e) => { e.preventDefault(); currentPage = group.id; + subPage = null; }} href="##" > @@ -309,24 +313,34 @@ {#each data.servers as server (server.id)} - - { - e.preventDefault(); - currentPage = server.id; - }} - href="##" - class="flex items-center gap-2" - > - {server.name} - {server.name} - + { + e.preventDefault(); + currentPage = server.id; + subPage = null; + }} + > + {server.name} + {server.name} + {#each server.channels as channel (channel.id)} + { + e.preventDefault(); + currentPage = server.id; + subPage = channel.id; + }} + href="##" + class="flex items-center gap-2" + > + {channel.name} + + {/each} {/each} diff --git a/src/lib/index.ts b/src/lib/index.ts index 7a96895..a60e52b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -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 = { 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; diff --git a/src/lib/puuid.ts b/src/lib/puuid.ts index 5504ccf..31020b3 100644 --- a/src/lib/puuid.ts +++ b/src/lib/puuid.ts @@ -10,7 +10,8 @@ export function definePrefix(prefix: P) { return { prefix, - is(value: string): value is Puuid

{ + is(value: string | undefined | null): value is Puuid

{ + if (!value) return false; return value.startsWith(prefix + '_'); }, diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index b25e23d..f43edcb 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -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({ diff --git a/src/routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server.ts b/src/routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server.ts index 1ba3abe..760a783 100644 --- a/src/routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server.ts +++ b/src/routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server.ts @@ -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); diff --git a/src/routes/api/updates/[sessionId]/+server.ts b/src/routes/api/updates/[sessionId]/+server.ts index 2393d9f..a56c160 100644 --- a/src/routes/api/updates/[sessionId]/+server.ts +++ b/src/routes/api/updates/[sessionId]/+server.ts @@ -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) ) diff --git a/src/routes/app/+page.server.ts b/src/routes/app/+page.server.ts index 068cceb..33fcecc 100644 --- a/src/routes/app/+page.server.ts +++ b/src/routes/app/+page.server.ts @@ -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 }; diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 64824a2..ea1b426 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -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,7 +56,50 @@ servers: [] }); + 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 + }; + }); + overview_data.groups = data.user.groups.map((z) => { + return { + id: GroupID.parse(z.id), + name: z.name, + ownerId: z.ownerId, + members: z.members, + permissions: z.permissions, + image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name + }; + }); + overview_data.friends = await Promise.all( + data.user.friends.map(async (friend) => { + const res = await fetch(`/api/status/${friend.id}`); + + if (!res.ok) { + throw new Error(`Failed to fetch status for ${friend.id}`); + } + + const status = await res.json(); + + return { + id: UserID.parse(friend.id), + username: friend.username, + status: status.status, + statusMessage: status.statusMessage, + dmId: friend.dmId, + image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username + }; + }) + ); + } $effect(() => { + console.log(currentPageID, currentSubPageID); if (currentPageID) { if (UserID.is(currentPageID)) { currentPage = overview_data.friends.find((friend) => friend.id === currentPageID); @@ -58,6 +108,15 @@ } 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; } @@ -72,7 +131,6 @@ members = data.members; } fetchMembers(); - ownerId = (currentPage as OverviewGroup | OverviewServer).ownerId; } else { isMembersTabOpen = false; } @@ -80,26 +138,35 @@ $effect(() => { if (!currentPageID || !currentPage) return; - if (ServerID.is(currentPageID)) return; + if (ServerID.is(currentPageID) && !ChannelID.is(currentSubPageID)) return; async function getMessages() { - const targetId = currentPage && 'dmId' in currentPage ? currentPage.dmId : currentPageID; + let path = ''; + let subscribe = ''; - const req = await fetch('/api/messages/' + targetId); + 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 != targetId) { + if (previousSubscription && previousSubscription != subscribe) { await fetch('/api/updates/' + sessionId, { body: JSON.stringify({ subscribeTo: previousSubscription }), method: 'DELETE' }); } - if (previousSubscription != targetId) { + if (previousSubscription != subscribe) { await fetch('/api/updates/' + sessionId, { - body: JSON.stringify({ subscribeTo: targetId }), + body: JSON.stringify({ subscribeTo: subscribe }), method: 'POST' }); } @@ -112,44 +179,10 @@ onMount(() => { async function run() { - overview_data.servers = data.user.servers.map((z) => { - return { - id: ServerID.parse(z.id), - name: z.name, - ownerId: z.ownerId, - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name - }; - }); - overview_data.groups = data.user.groups.map((z) => { - return { - id: GroupID.parse(z.id), - name: z.name, - ownerId: z.ownerId, - members: z.members, - permissions: z.permissions, - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name - }; - }); - overview_data.friends = await Promise.all( - data.user.friends.map(async (friend) => { - const res = await fetch(`/api/status/${friend.id}`); - - if (!res.ok) { - throw new Error(`Failed to fetch status for ${friend.id}`); - } - - const status = await res.json(); - - return { - id: UserID.parse(friend.id), - username: friend.username, - status: status.status, - statusMessage: status.statusMessage, - dmId: friend.dmId, - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username - }; - }) - ); + 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} - +

{#if currentPageID && currentPage} {#if ServerID.is(currentPageID)} {@const server = currentPage as OverviewServer} + {@const channel = currentSubPage as OverviewServer} {server!.name} -

{server!.name}

+ {#if currentSubPage} +

{server!.name} - {channel.name}

+ {:else} +

{server!.name} - Server Info

+ {/if} {:else if UserID.is(currentPageID)} {@const friend = currentPage as UserWithStatus} @@ -252,46 +296,58 @@ {/if} {/if}
-
- {#each messages as message, i (message.id)} - {#if i === 0 || messages[i - 1].authorId !== message.authorId} -
- {message.author.name} -
-
- {message.author.name} - - {formatTimestamp(message.timestamp)} - + {#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID} +

add invite creation, role creation, moderation, et cetera to this page.

+ {/if} + {#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))} +
+ {#each messages as message, i (message.id)} + {#if i === 0 || messages[i - 1].authorId !== message.authorId} +
+ {message.author.name} + +
+
+ {message.author.name} + + {formatTimestamp(message.timestamp)} + +
+ +
{message.content}
- +
+ {:else} +
{message.content}
-
- {:else} -
-
{message.content}
-
- {/if} - {/each} -
- {#if currentPageID && currentPage} + {/if} + {/each} +
{ 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 })