reset drizzle, get DMs working (partly, usernames aren't resolved)

This commit is contained in:
Soph :3 2026-01-04 23:13:39 +02:00
parent 3a0f096ade
commit bf679f9ee0
32 changed files with 1150 additions and 2867 deletions

View file

@ -1,312 +1,311 @@
<script lang="ts">
import { type Data, type OverviewUser } from '$lib';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
import UsersRound from '@lucide/svelte/icons/users-round';
import CirclePlus from '@lucide/svelte/icons/circle-plus';
import Input from './ui/input/input.svelte';
import Button, { buttonVariants } from './ui/button/button.svelte';
import User from './extra/User.svelte';
import type { SessionValidationResult } from '$lib/server/auth';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
import UsersRound from '@lucide/svelte/icons/users-round';
import CirclePlus from '@lucide/svelte/icons/circle-plus';
import Input from './ui/input/input.svelte';
import Button, { buttonVariants } from './ui/button/button.svelte';
import User from './extra/User.svelte';
import type { SessionValidationResult } from '$lib/server/auth';
let {
currentPage = $bindable<string | null>(),
data,
user,
...restProps
}: { currentPage: string | null; data: Data; user: SessionValidationResult['user'] } = $props();
let {
currentPage = $bindable<string | null>(),
data,
user,
...restProps
}: { currentPage: string | null; data: Data; user: SessionValidationResult['user'] } = $props();
</script>
<Sidebar.Root {...restProps}>
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton size="lg">
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<MessagesSquare class="size-4" />
</div>
<div class="flex flex-col gap-0.5 leading-none">
<span class="font-medium">chat.sad.ovh</span>
</div>
</Sidebar.MenuButton>
<div class="flex w-full justify-center gap-2">
<Dialog.Root>
<Dialog.Trigger>
<Button
variant={user!.friendRequests.length > 0 ? 'destructive' : 'outline'}
size="icon"
>
<UserRoundPlus />
</Button>
</Dialog.Trigger>
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton size="lg">
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<MessagesSquare class="size-4" />
</div>
<div class="flex flex-col gap-0.5 leading-none">
<span class="font-medium">chat.sad.ovh</span>
</div>
</Sidebar.MenuButton>
<div class="flex w-full justify-center gap-2">
<Dialog.Root>
<Dialog.Trigger>
<Button
variant={user!.friendRequests.length > 0 ? 'destructive' : 'outline'}
size="icon"
>
<UserRoundPlus />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Add a friend</Dialog.Title>
<Dialog.Description>
Add a friend using their username or manage pending requests.
</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Add a friend</Dialog.Title>
<Dialog.Description>
Add a friend using their username or manage pending requests.
</Dialog.Description>
</Dialog.Header>
<!-- input to add a new friend -->
<form method="POST" action="?/addFriend" class="mb-4">
<Input name="username" placeholder="username" required class="mb-2" />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Send request</Button>
</Dialog.Footer>
</form>
<!-- input to add a new friend -->
<form method="POST" action="?/addFriend" class="mb-4">
<Input name="username" placeholder="username" required class="mb-2" />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Send request</Button>
</Dialog.Footer>
</form>
<!-- Tabs for Friend Requests -->
<Tabs.Root value="outgoing">
<Tabs.List>
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
</Tabs.List>
<!-- Tabs for Friend Requests -->
<Tabs.Root value="outgoing">
<Tabs.List>
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
</Tabs.List>
<!-- Outgoing Requests -->
<Tabs.Content value="outgoing">
{#if user.friendRequests.filter((r) => r.fromUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No outgoing requests</p>
{:else}
{#each user.friendRequests.filter((r) => r.fromUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Request sent</Card.Description>
</Card.Header>
<Card.Footer>
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Cancel</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
<!-- Outgoing Requests -->
<Tabs.Content value="outgoing">
{#if user!.friendRequests.filter((r) => r.fromUser === user!.id).length === 0}
<p class="text-sm text-muted-foreground">No outgoing requests</p>
{:else}
{#each user!.friendRequests.filter((r) => r.fromUser === user!.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.toUsername}</Card.Title>
<Card.Description>Request sent</Card.Description>
</Card.Header>
<Card.Footer>
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Cancel</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
<!-- Incoming Requests -->
<Tabs.Content value="incoming">
{#if user.friendRequests.filter((r) => r.toUser === user.id).length === 0}
<p class="text-sm text-muted-foreground">No incoming requests</p>
{:else}
{#each user.friendRequests.filter((r) => r.toUser === user.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.username}</Card.Title>
<Card.Description>Sent you a friend request</Card.Description>
</Card.Header>
<Card.Footer class="flex gap-2">
<!-- accept friend -->
<form method="POST" action="?/addFriend">
<input type="hidden" name="userId" value={request.fromUser} />
<Button type="submit" size="sm">Accept</Button>
</form>
<!-- decline friend -->
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Decline</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
<!-- Incoming Requests -->
<Tabs.Content value="incoming">
{#if user!.friendRequests.filter((r) => r.toUser === user!.id).length === 0}
<p class="text-sm text-muted-foreground">No incoming requests</p>
{:else}
{#each user!.friendRequests.filter((r) => r.toUser === user!.id) as request (request.id)}
<Card.Root class="mb-2">
<Card.Header>
<Card.Title>{request.fromUsername}</Card.Title>
<Card.Description>Sent you a friend request</Card.Description>
</Card.Header>
<Card.Footer class="flex gap-2">
<!-- accept friend -->
<form method="POST" action="?/addFriend">
<input type="hidden" name="userId" value={request.fromUser} />
<Button type="submit" size="sm">Accept</Button>
</form>
<!-- decline friend -->
<form method="POST" action="?/cancelFriendRequest">
<input type="hidden" name="requestId" value={request.id} />
<Button type="submit" variant="outline" size="sm">Decline</Button>
</form>
</Card.Footer>
</Card.Root>
{/each}
{/if}
</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<UsersRound />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<UsersRound />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createGroup">
<Dialog.Header>
<Dialog.Title>Create a group</Dialog.Title>
<Dialog.Description>Add friends into your group!</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createGroup">
<Dialog.Header>
<Dialog.Title>Create a group</Dialog.Title>
<Dialog.Description>Add friends into your group!</Dialog.Description>
</Dialog.Header>
{#each data.friends as friend (friend.id)}
<label class="flex items-center gap-2">
<input type="checkbox" name="member" value={friend.id} />
<User user={friend} />
</label>
{/each}
{#each data.friends as friend (friend.id)}
<label class="flex items-center gap-2">
<input type="checkbox" name="member" value={friend.id} />
<User user={friend} />
</label>
{/each}
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create group</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create group</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<CirclePlus />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<CirclePlus />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/joinServer">
<Dialog.Header>
<Dialog.Title>Join a server</Dialog.Title>
<Dialog.Description>Enter an invite link.</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/joinServer">
<Dialog.Header>
<Dialog.Title>Join a server</Dialog.Title>
<Dialog.Description>Enter an invite link.</Dialog.Description>
</Dialog.Header>
<Input name="invite" placeholder="invite link" required />
<Input name="invite" placeholder="invite link" required />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Join</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Join</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createServer">
<Dialog.Header>
<Dialog.Title>Create a server</Dialog.Title>
<Dialog.Description>Name your new server.</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="sm:max-w-[425px]">
<form method="POST" action="?/createServer">
<Dialog.Header>
<Dialog.Title>Create a server</Dialog.Title>
<Dialog.Description>Name your new server.</Dialog.Description>
</Dialog.Header>
<Input name="name" placeholder="Server name" required />
<Input name="name" placeholder="Server name" required />
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</div>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.Menu>
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Friends
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.friends as friend (friend.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<User
onclick={(e) => {
e.preventDefault();
currentPage = friend.id;
}}
user={friend}
></User>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
<Dialog.Footer>
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
<Button type="submit">Create</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</div>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.Menu>
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Friends
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.friends as friend (friend.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<User
onclick={(e) => {
e.preventDefault();
currentPage = friend.id;
}}
user={friend}
></User>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Groups
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.groups as group (group.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a
onclick={(e) => {
e.preventDefault();
currentPage = group.id;
}}
href="##"
>
{group.name} ({group.members} members)
</a>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Groups
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.groups as group (group.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a
onclick={(e) => {
e.preventDefault();
currentPage = group.id;
}}
href="##"
>
{group.name} ({group.members} members)
</a>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Servers
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.servers as server (server.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a
onclick={(e) => {
e.preventDefault();
currentPage = server.id;
}}
href="##"
class="flex items-center gap-2"
>
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
alt={server.name}
class="size-6 rounded-full"
/>
{server.name}
</a>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Rail />
<Collapsible.Root open={true} class="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger>
<Sidebar.MenuButton>
Servers
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
</Sidebar.MenuButton>
</Collapsible.Trigger>
<Collapsible.Content>
<Sidebar.MenuSub>
{#each data.servers as server (server.id)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<a
onclick={(e) => {
e.preventDefault();
currentPage = server.id;
}}
href="##"
class="flex items-center gap-2"
>
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
alt={server.name}
class="size-6 rounded-full"
/>
{server.name}
</a>
</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
{/each}
</Sidebar.MenuSub>
</Collapsible.Content>
</Sidebar.MenuItem>
</Collapsible.Root>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Rail />
</Sidebar.Root>

View file

@ -1,9 +0,0 @@
import { MediaQuery } from "svelte/reactivity";
const DEFAULT_MOBILE_BREAKPOINT = 768;
export class IsMobile extends MediaQuery {
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
super(`max-width: ${breakpoint - 1}px`);
}
}

View file

@ -4,11 +4,12 @@ export const UserID = definePrefix('user');
export const GroupID = definePrefix('group');
export const ServerID = definePrefix('srv');
export const FriendRequestID = definePrefix('frq');
export const DirectMessageID = definePrefix('dmid');
export type UserId = Puuid<'user'>;
export type GroupId = Puuid<'group'>;
export type ServerId = Puuid<'srv'>;
export type FriendRequestID = Puuid<'frq'>;
export type DirectMessageId = Puuid<'dmid'>;
export const Status: Record<string, 1 | 2 | 3> = {
OFFLINE: 1,
@ -20,6 +21,7 @@ export type OverviewUser = {
id: string;
username: string;
image: string;
dmId?: string;
};
export type OverviewServer = {

View file

@ -4,6 +4,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { _findDmId } from '../../routes/api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server';
const DAY_IN_MS = 1000 * 60 * 60 * 24;
@ -63,15 +64,25 @@ export async function validateSessionToken(token: string) {
.set({ expiresAt: session.expiresAt })
.where(eq(table.session.id, session.id));
}
const friends = (user.friends as string[]).length
? await db
.select({
id: table.user.id,
username: table.user.username
})
.from(table.user)
.where(inArray(table.user.id, user.friends as string[]))
: [];
const friends = await Promise.all(
((user.friends as string[]).length
? await db
.select({
id: table.user.id,
username: table.user.username
})
.from(table.user)
.where(inArray(table.user.id, user.friends as string[]))
: []
).map(async (z) => {
const dmid = await _findDmId(z.id, user.id);
return {
...z,
dmId: dmid
};
})
);
const servers = (user.servers as string[]).length
? await db
@ -99,6 +110,8 @@ export async function validateSessionToken(token: string) {
.select({
id: table.friendRequest.id,
fromUser: table.friendRequest.fromUser,
fromUsername: table.friendRequest.fromUsername,
toUsername: table.friendRequest.toUsername,
toUser: table.friendRequest.toUser
})
.from(table.friendRequest)

View file

@ -37,7 +37,8 @@ export const group = sqliteTable('group', {
owner: text('owner')
.notNull()
.references(() => user.id),
members: text('members', { mode: 'json' }).default([]).notNull()
members: text('members', { mode: 'json' }).default([]).notNull(),
messages: text('messages', { mode: 'json' }).default([]).notNull()
});
export const channel = sqliteTable('channel', {
@ -51,10 +52,12 @@ export const channel = sqliteTable('channel', {
export const directMessage = sqliteTable('directMessage', {
id: text('id').primaryKey(),
name: text('name').notNull(),
serverId: text('server_id')
firstMember: text('first_member')
.notNull()
.references(() => server.id),
.references(() => user.id),
secondMember: text('second_member')
.notNull()
.references(() => user.id),
messages: text('messages', { mode: 'json' }).default([]).notNull()
});

View file

@ -4,6 +4,7 @@ import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatTimestamp(dateString: string) {
const date = new Date(dateString);
const now = new Date();