From d9f5919b60ee1af34d149ea1238698abe7611882 Mon Sep 17 00:00:00 2001 From: fucksophie Date: Tue, 13 Jan 2026 00:20:10 +0200 Subject: [PATCH] username update, status message update, status overwriting, global status in client --- src/lib/components/app-sidebar.svelte | 65 +++++++++++++++++++- src/lib/components/extra/User.svelte | 45 ++++++++------ src/lib/components/member-sidebar.svelte | 14 ++--- src/lib/index.ts | 14 +++-- src/lib/server/auth.ts | 5 +- src/lib/server/db/index.ts | 5 ++ src/routes/api/members/[entityId]/+server.ts | 24 ++++++-- src/routes/api/status/[userId]/+server.ts | 29 ++++++--- src/routes/api/updates/+server.ts | 29 ++++++--- src/routes/app/+page.server.ts | 40 +++++++++++- src/routes/app/+page.svelte | 44 ++++++------- 11 files changed, 235 insertions(+), 79 deletions(-) diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index 17012ef..e9aab76 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -14,7 +14,8 @@ import Button, { buttonVariants } from './ui/button/button.svelte'; import User from './extra/User.svelte'; import type { SessionValidationResult } from '$lib/server/auth'; - import type { OverviewData } from '$lib'; + import { Status, statuses, type OverviewData, type OverviewUser } from '$lib'; + import Label from './ui/label/label.svelte'; let { currentPage = $bindable(), @@ -259,6 +260,7 @@ subPage = null; }} user={friend} + crown={false} > @@ -349,5 +351,66 @@ + + + + + + + +
+ + Edit profile + Update how others see you. + + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + + Cancel + + +
+
+
+
diff --git a/src/lib/components/extra/User.svelte b/src/lib/components/extra/User.svelte index 63ebfb7..d2a1242 100644 --- a/src/lib/components/extra/User.svelte +++ b/src/lib/components/extra/User.svelte @@ -1,12 +1,14 @@
@@ -16,20 +18,23 @@ alt={user.username} class="size-6 rounded-full" /> -
- {#if user.status === Status.OFFLINE} - - {:else if user.status === Status.DND} - - {:else if user.status === Status.ONLINE} - - {/if} -
+ {#if status} +
+ {#if status.status === Status.OFFLINE} + + {:else if status.status === Status.DND} + + {:else if status.status === Status.ONLINE} + + {/if} +
+ {/if}
{/if} -
- {user.statusMessage} -
+ {#if status} +
+ {status.statusMessage} +
+ {/if}
diff --git a/src/lib/components/member-sidebar.svelte b/src/lib/components/member-sidebar.svelte index e3a371d..de237b4 100644 --- a/src/lib/components/member-sidebar.svelte +++ b/src/lib/components/member-sidebar.svelte @@ -12,7 +12,7 @@ type OverviewData, type OverviewGroup, type OverviewServer, - type UserWithStatus + type OverviewUser } from '$lib'; import Button from './ui/button/button.svelte'; import Input from './ui/input/input.svelte'; @@ -20,14 +20,14 @@ // Props for the member sidebar. let { open = $bindable(true), - members = $bindable([]), + members = $bindable([]), user, data, currentEntity, currentEntityId = $bindable(null) }: { open: boolean; - members: UserWithStatus[]; + members: OverviewUser[]; data: OverviewData; user: SessionValidationResult['user']; currentEntity: OverviewGroup | OverviewServer; @@ -49,13 +49,13 @@ {#if user && currentEntityId} - + Group Settings Configure your group settings here. - + User Permissions {#if user.id == currentEntity.ownerId} @@ -194,9 +194,7 @@ {#each members as member (member.id)} - {#snippet child({ props })} - - {/snippet} + {/each} diff --git a/src/lib/index.ts b/src/lib/index.ts index a60e52b..bdd9888 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,4 +1,5 @@ import { definePrefix, type Puuid } from './puuid'; +import { SvelteMap } from 'svelte/reactivity'; export const UserID = definePrefix('user'); export const GroupID = definePrefix('group'); @@ -14,6 +15,12 @@ export type ServerId = Puuid<'srv'>; export type DirectMessageId = Puuid<'dmid'>; export type ChannelId = Puuid<'ch'>; +export interface Status { + statusMessage: string; + status: 1 | 2 | 3; +} +export const statuses: Map = new SvelteMap(); + export const Status: Record = { OFFLINE: 1, DND: 2, @@ -65,13 +72,8 @@ export type OverviewGroup = { }; }; -export interface UserWithStatus extends OverviewUser { - status: 1 | 2 | 3; - statusMessage: string; -} - export interface OverviewData { - friends: UserWithStatus[]; + friends: OverviewUser[]; groups: OverviewGroup[]; servers: OverviewServer[]; } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index f43edcb..8cd381e 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -2,7 +2,7 @@ import type { RequestEvent } from '@sveltejs/kit'; import { eq, inArray, or } from 'drizzle-orm'; import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; -import { db } from '$lib/server/db'; +import { db, kvStore } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; import { _findDmId } from '../../routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server'; @@ -159,7 +159,8 @@ export async function validateSessionToken(token: string) { } }; }), - friendRequests + friendRequests, + statusMessage: kvStore.get('user-' + user.id + '-message') } }; } diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index fc7ebcc..4984fcb 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -6,3 +6,8 @@ const sqlite = new Database('database.db'); export const db = drizzle(sqlite); export const kvStore = new BunSqliteKeyValue('./kvStore.db'); + +console.log('nuking kvstore'); +kvStore.getItems()?.forEach((z) => { + if (z.key.endsWith('state')) kvStore.set(z.key, 1); +}); diff --git a/src/routes/api/members/[entityId]/+server.ts b/src/routes/api/members/[entityId]/+server.ts index 39e9103..fa0638d 100644 --- a/src/routes/api/members/[entityId]/+server.ts +++ b/src/routes/api/members/[entityId]/+server.ts @@ -1,7 +1,7 @@ import { db, kvStore } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; import { eq, inArray } from 'drizzle-orm'; -import { GroupID, ServerID } from '$lib'; +import { GroupID, ServerID, Status } from '$lib'; import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; @@ -27,8 +27,15 @@ export const GET: RequestHandler = async ({ params }) => { return json({ members: members.map((member) => ({ id: member.id, - status: kvStore.get('user-' + member.id + '-state'), - statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : 'not vibing', + status: + member.statusOverwrite == Status.OFFLINE + ? Status.OFFLINE + : kvStore.get('user-' + member.id + '-state'), + statusMessage: + kvStore.get('user-' + member.id + '-state') != Status.OFFLINE && + member.statusOverwrite != Status.OFFLINE + ? kvStore.get('user-' + member.id + '-message') + : '', username: member.username, image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` })) @@ -50,8 +57,15 @@ export const GET: RequestHandler = async ({ params }) => { return json({ members: members.map((member) => ({ id: member.id, - status: kvStore.get('user-' + member.id + '-state'), - statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : 'not vibing', + status: + member.statusOverwrite == Status.OFFLINE + ? Status.OFFLINE + : kvStore.get('user-' + member.id + '-state'), + statusMessage: + kvStore.get('user-' + member.id + '-state') != Status.OFFLINE && + member.statusOverwrite != Status.OFFLINE + ? kvStore.get('user-' + member.id + '-message') + : '', username: member.username, image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` })) diff --git a/src/routes/api/status/[userId]/+server.ts b/src/routes/api/status/[userId]/+server.ts index 3cec0e6..f65e2db 100644 --- a/src/routes/api/status/[userId]/+server.ts +++ b/src/routes/api/status/[userId]/+server.ts @@ -1,19 +1,32 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { kvStore } from '$lib/server/db'; +import { db, kvStore } from '$lib/server/db'; import { Status } from '$lib'; +import * as table from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; export const GET: RequestHandler = async ({ params }) => { const { userId } = params; + const user = await db + .select({ + statusOverwrite: table.user.statusOverwrite + }) + .from(table.user) + .where(eq(table.user.id, userId)); + if (!user || user.length == 0) { + return new Response('User missing', { status: 404 }); + } + + let current = kvStore.get('user-' + userId + '-state'); + + if (user[0].statusOverwrite == Status.OFFLINE) { + current = Status.OFFLINE; + } + return json({ userId, - status: kvStore.get('user-' + userId + '-state'), - statusMessage: - kvStore.get('user-' + userId + '-state') != Status.OFFLINE - ? Math.random() > 0.5 - ? 'vibing 🟢' - : 'not vibing' - : '' + status: current, + statusMessage: current != Status.OFFLINE ? kvStore.get('user-' + userId + '-message') : '' }); }; diff --git a/src/routes/api/updates/+server.ts b/src/routes/api/updates/+server.ts index 7cc2493..a76c379 100644 --- a/src/routes/api/updates/+server.ts +++ b/src/routes/api/updates/+server.ts @@ -22,7 +22,7 @@ export function _sendToSubscribers(id: string, payload: unknown) { } } export function _sendToUser(userId: string, payload: unknown) { - for (const [_, client] of _clients) { + for (const [, client] of _clients) { if (client.userId == userId) { client.controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`); } @@ -59,10 +59,15 @@ export async function GET({ locals, request }) { if (overwrite === Status.DND) { kvStore.set(`user-${userId}-state`, Status.DND); - _sendToSubscribers(userId, { type: 'status', id: userId, status: Status.DND }); - } else { - kvStore.set(`user-${userId}-state`, Status.ONLINE); - _sendToSubscribers(userId, { type: 'status', id: userId, status: Status.ONLINE }); + _sendToSubscribers(userId, { + type: 'status', + id: userId, + status: Status.DND, + statusMessage: + kvStore.get('user-' + userId + '-state') != Status.OFFLINE + ? kvStore.get('user-' + userId + '-message') + : '' + }); } request.signal.addEventListener('abort', () => { @@ -74,7 +79,12 @@ export async function GET({ locals, request }) { if (overwrite === Status.OFFLINE) return; 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, + statusMessage: '' + }); }); }, cancel() { @@ -85,7 +95,12 @@ export async function GET({ locals, request }) { if (overwrite === Status.OFFLINE) return; 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, + statusMessage: '' + }); } }); diff --git a/src/routes/app/+page.server.ts b/src/routes/app/+page.server.ts index c30602f..7f84151 100644 --- a/src/routes/app/+page.server.ts +++ b/src/routes/app/+page.server.ts @@ -1,14 +1,15 @@ import { fail, redirect } from '@sveltejs/kit'; import { getRequestEvent } from '$app/server'; import type { Actions, PageServerLoad } from './$types'; -import { db } from '$lib/server/db'; +import { db, kvStore } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; import { DirectMessageID, FriendRequestID, GroupID, ServerID } from '$lib'; import { eq } from 'drizzle-orm'; import { and } from 'drizzle-orm'; import { type User } from '$lib/server/db/schema'; import { _sendToSubscribers, _sendToUser } from '../api/updates/+server'; -import { _findDmId } from '../api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server'; +import { validateUsername } from '$lib/server/auth'; + export const load: PageServerLoad = async () => { const user = requireLogin(); return { user }; @@ -196,6 +197,41 @@ export const actions = { return { success: true }; }, + updateProfile: async ({ request, locals }) => { + const user = locals.user; + if (!user) return fail(401); + + const data = await request.formData(); + + const userName = data.get('userName')?.toString().trim(); + const status = data.has('status') ? +data.get('status')! : undefined; + const statusMessage = data.get('statusMessage')?.toString().trim() || undefined; + + if (!validateUsername(userName)) { + return fail(400, { message: 'Invalid display name' }); + } + + if (status != 1 && status != 2 && status != 3) { + return fail(400, { message: 'Invalid status' }); + } + + kvStore.set('user-' + user.id + '-message', statusMessage); + await db + .update(table.user) + .set({ + username: userName, + statusOverwrite: status + }) + .where(eq(table.user.id, user.id)); + + if (statusMessage || status != user.statusOverwrite) + _sendToSubscribers(user.id, { type: 'status', id: user.id, status: status, statusMessage }); + + if (userName != user.username) + _sendToSubscribers(user.id, { type: 'username', status: 'name-changed' }); + + return { success: true }; + }, createGroup: async ({ request, locals }) => { const data = await request.formData(); const members = data.getAll('member').map((z) => z.toString()); diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index dfac08e..0861297 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -11,11 +11,11 @@ type OverviewUser, type OverviewGroup, type OverviewServer, - type UserWithStatus, type ReturnMessage, type Channel, type ChannelId, - ChannelID + ChannelID, + statuses } from '$lib'; import type { PageServerData } from './$types'; import AppSidebar from '$lib/components/app-sidebar.svelte'; @@ -43,7 +43,7 @@ let sse: EventSource | undefined; let messagesElement: HTMLDivElement | undefined = $state(); let isMembersTabOpen = $state(true); - let members: UserWithStatus[] = $state([]); + let members: OverviewUser[] = $state([]); let messages: ReturnMessage[] = $state([]); let inputValue = $state(); @@ -88,11 +88,14 @@ const status = await res.json(); + statuses.set(friend.id, { + status: status.status, + statusMessage: status.statusMessage + }); + 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 }; @@ -198,9 +201,10 @@ const json = JSON.parse(e.data) as | { type: 'connected'; sessionId: string } | { type: 'message'; message: ReturnMessage } - | { type: 'status'; id: string; status: 1 | 2 | 3 } + | { 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); @@ -216,25 +220,29 @@ toast('Invalidation from group updates, recieved ' + json.status); if (json.status == 'removed-from-group' && GroupID.is(currentPageID)) { currentPageID = null; - currentPage = null; + currentPage = undefined; } await invalidateAll(); await fill_overview_data(); } + if (json.type == 'username') { + toast('Invalidation from username updates, recieved ' + json.status); + await invalidateAll(); + + await fill_overview_data(); + } if (json.type == 'connected') { console.log('SSE connected. We are sessionID ' + json.sessionId); sessionId = json.sessionId; } if (json.type == 'status') { - //@TODO update everywhere where user is used - const friend = overview_data.friends.find((z) => z.id == json.id); - - if (friend) { - friend.status = json.status; - } + statuses.set(json.id, { + status: json.status, + statusMessage: json.statusMessage + }); } if (json.type == 'message') { @@ -294,18 +302,12 @@

{server!.name} - Server Info

{/if} {:else if UserID.is(currentPageID)} - {@const friend = currentPage as UserWithStatus} + {@const friend = currentPage as OverviewUser} {friend!.username}

- {friend!.username} [{friend.status == Status.ONLINE - ? 'Online!' - : friend.status == Status.DND - ? 'DND' - : friend.status == Status.OFFLINE - ? 'Offline' - : 'Unknown'}] + {friend!.username}

{:else if GroupID.is(currentPageID)} {@const group = currentPage as OverviewGroup}