almost finish status support everywhere else, group setting start impl

This commit is contained in:
Soph :3 2026-01-08 14:47:07 +02:00
parent 6b47888514
commit 92a95cb365
14 changed files with 770 additions and 116 deletions

View file

@ -0,0 +1,3 @@
ALTER TABLE `group` ADD `change_title` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE `group` ADD `add_members` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE `group` ADD `remove_members` integer DEFAULT 0 NOT NULL;

View file

@ -0,0 +1,556 @@
{
"version": "6",
"dialect": "sqlite",
"id": "e21369b9-d475-4c0f-8a6d-1c5f92ab8948",
"prevId": "bce65872-fa2f-4adc-b86f-af9880038bc8",
"tables": {
"channel": {
"name": "channel",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"channel_server_id_server_id_fk": {
"name": "channel_server_id_server_id_fk",
"tableFrom": "channel",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"directMessage": {
"name": "directMessage",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"first_member": {
"name": "first_member",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"second_member": {
"name": "second_member",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"directMessage_first_member_user_id_fk": {
"name": "directMessage_first_member_user_id_fk",
"tableFrom": "directMessage",
"tableTo": "user",
"columnsFrom": [
"first_member"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"directMessage_second_member_user_id_fk": {
"name": "directMessage_second_member_user_id_fk",
"tableFrom": "directMessage",
"tableTo": "user",
"columnsFrom": [
"second_member"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"friendRequest": {
"name": "friendRequest",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"from_user": {
"name": "from_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"from_username": {
"name": "from_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_username": {
"name": "to_username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"to_user": {
"name": "to_user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"friendRequest_from_user_user_id_fk": {
"name": "friendRequest_from_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_from_username_user_id_fk": {
"name": "friendRequest_from_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"from_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_username_user_id_fk": {
"name": "friendRequest_to_username_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_username"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"friendRequest_to_user_user_id_fk": {
"name": "friendRequest_to_user_user_id_fk",
"tableFrom": "friendRequest",
"tableTo": "user",
"columnsFrom": [
"to_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"group": {
"name": "group",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"change_title": {
"name": "change_title",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"add_members": {
"name": "add_members",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"remove_members": {
"name": "remove_members",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"messages": {
"name": "messages",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"group_owner_user_id_fk": {
"name": "group_owner_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"invite": {
"name": "invite",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"server_id": {
"name": "server_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"invite_server_id_server_id_fk": {
"name": "invite_server_id_server_id_fk",
"tableFrom": "invite",
"tableTo": "server",
"columnsFrom": [
"server_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"server": {
"name": "server",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"members": {
"name": "members",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"channels": {
"name": "channels",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"server_owner_user_id_fk": {
"name": "server_owner_user_id_fk",
"tableFrom": "server",
"tableTo": "user",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status_overwrite": {
"name": "status_overwrite",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 3
},
"friends": {
"name": "friends",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"servers": {
"name": "servers",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"groups": {
"name": "groups",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
}
},
"indexes": {
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -8,6 +8,13 @@
"when": 1767559403688, "when": 1767559403688,
"tag": "0000_amusing_shatterstar", "tag": "0000_amusing_shatterstar",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1767860870240,
"tag": "0001_tiresome_stature",
"breakpoints": true
} }
] ]
} }

View file

@ -168,45 +168,10 @@
</Dialog.Header> </Dialog.Header>
{#each data.friends as friend (friend.id)} {#each data.friends as friend (friend.id)}
<Sidebar.MenuSubItem class="flex items-center gap-2"> <label class="flex items-center gap-2">
<Sidebar.MenuSubButton> <input type="checkbox" name="member" value={friend.id} />
<User <User crown={false} user={friend} />
onclick={(e) => { </label>
e.preventDefault();
currentPage = friend.id;
}}
user={friend}
></User>
</Sidebar.MenuSubButton>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="destructive" size="icon">
<MinusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-106.25">
<Dialog.Header>
<Dialog.Title>Remove Friend</Dialog.Title>
<Dialog.Description>
Are you sure you want to remove {friend.username} from your friends?
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="flex gap-2">
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>
Cancel
</Dialog.Close>
<form method="POST" action="?/removeFriend">
<input type="hidden" name="userId" value={friend.id} />
<Button type="submit" variant="destructive">Remove</Button>
</form>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Sidebar.MenuSubItem>
{/each} {/each}
<Dialog.Footer> <Dialog.Footer>

View file

@ -1,10 +1,38 @@
<script lang="ts"> <script lang="ts">
import { Status, type UserWithStatus } from '$lib'; import { Status, type UserWithStatus } from '$lib';
import Crown from '@lucide/svelte/icons/crown';
const { onclick, user }: { onclick?: (e: MouseEvent) => void; user: UserWithStatus } = $props(); const {
onclick,
user,
crown
}: { crown: boolean; onclick?: (e: MouseEvent) => void; user: UserWithStatus } = $props();
</script> </script>
<a <div class="flex flex-row gap-2">
<div>
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + user.username}
alt={user.username}
class="size-6 rounded-full"
/>
<div class="relative">
{#if user.status === Status.OFFLINE}
<span
class="absolute end-0 bottom-0 block size-2 rounded-full bg-gray-500 ring-1 ring-white"
></span>
{:else if user.status === Status.DND}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-red-500 ring-1 ring-white"
></span>
{:else if user.status === Status.ONLINE}
<span
class="absolute end-0 bottom-0 block size-2 rounded-full bg-green-500 ring-1 ring-white"
></span>
{/if}
</div>
</div>
<div>
<a
{onclick} {onclick}
oncontextmenu={async (e) => { oncontextmenu={async (e) => {
e.preventDefault(); e.preventDefault();
@ -12,23 +40,14 @@
}} }}
href="##" href="##"
class="flex items-center gap-2" class="flex items-center gap-2"
> >
<div class="relative">
<img
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + user.username}
alt={user.username}
class="size-6 rounded-full"
/>
{#if user.status === Status.OFFLINE}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-gray-500 ring-1 ring-white"
></span>
{:else if user.status === Status.DND}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-red-500 ring-1 ring-white"
></span>
{:else if user.status === Status.ONLINE}
<span class="absolute end-0 bottom-0 block size-2 rounded-full bg-green-500 ring-1 ring-white"
></span>
{/if}
</div>
{user.username} {user.username}
</a> {#if crown}
<Crown></Crown>
{/if}
</a>
<div class="pl-2 text-xs text-gray-400 italic">
{user.statusMessage}
</div>
</div>
</div>

View file

@ -1,36 +1,128 @@
<script lang="ts"> <script lang="ts">
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus'; import * as Dialog from '$lib/components/ui/dialog/index.js';
import UserRoundMinus from '@lucide/svelte/icons/user-round-minus'; import * as Tabs from '$lib/components/ui/tabs/index.js';
import Cog from '@lucide/svelte/icons/cog';
import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js'; import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import { onMount } from 'svelte';
import User from './extra/User.svelte'; import User from './extra/User.svelte';
import type { SessionValidationResult } from '$lib/server/auth'; import type { SessionValidationResult } from '$lib/server/auth';
import { ServerID, type UserWithStatus } from '$lib'; import {
GroupID,
ServerID,
type OverviewGroup,
type OverviewServer,
type UserWithStatus
} from '$lib';
import Button from './ui/button/button.svelte';
// Props for the member sidebar. // Props for the member sidebar.
let { let {
open = $bindable(true), open = $bindable(true),
members = $bindable<UserWithStatus[]>([]), members = $bindable<UserWithStatus[]>([]),
user, user,
currentEntity,
currentEntityId = $bindable<string | null>(null) currentEntityId = $bindable<string | null>(null)
}: { }: {
open: boolean; open: boolean;
members: UserWithStatus[]; members: UserWithStatus[];
user: SessionValidationResult['user']; user: SessionValidationResult['user'];
currentEntity: OverviewGroup | OverviewServer;
currentEntityId: string | null; currentEntityId: string | null;
} = $props(); } = $props();
let sidebar_probably = useSidebar(); let this_sidebar = useSidebar();
$effect(() => { $effect(() => {
if (sidebar_probably.open != open) { if (this_sidebar.open != open) {
sidebar_probably.setOpen(open); this_sidebar.setOpen(open);
} }
}); });
</script> </script>
<Sidebar.Root side="right"> <Sidebar.Root side="right">
<Sidebar.Header>
<div class="align-center flex w-full justify-center">
{#if user && currentEntityId}
<Dialog.Root>
<Dialog.Trigger><Button variant="outline"><Cog></Cog></Button></Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Group Settings</Dialog.Title>
<Dialog.Description>Configure your group settings here.</Dialog.Description>
</Dialog.Header>
<Tabs.Root value="users" class="w-[400px]">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="users">User Permissions</Tabs.Trigger>
{#if user.id == currentEntity.ownerId}
<Tabs.Trigger value="admins">Admin Settings</Tabs.Trigger>
{/if}
</Tabs.List>
{#if ServerID.is(currentEntityId)}
<h1>not done yet for later</h1>
{:else if GroupID.is(currentEntityId)}
{#if user.id == currentEntity.ownerId}
<Tabs.Content value="admins">
<form method="POST" action="?/configureGroup" class="space-y-4 p-2">
<input type="hidden" name="groupId" value={currentEntityId} />
<div class="flex items-center justify-between">
<label for="addUsers">Allow everyone to add users</label>
<input
type="checkbox"
id="addMembers"
name="addMembers"
checked={(currentEntity as OverviewGroup).permissions.addMembers}
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</div>
<div class="flex items-center justify-between">
<label for="removeUsers">Allow everyone to remove users</label>
<input
type="checkbox"
id="removeUsers"
name="removeUsers"
checked={(currentEntity as OverviewGroup).permissions.removeMembers}
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</div>
<div class="flex items-center justify-between">
<label for="changeTitle">Allow everyone to change title</label>
<input
type="checkbox"
id="changeTitle"
name="changeTitle"
checked={(currentEntity as OverviewGroup).permissions.changeTitle}
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</div>
<Button type="submit" class="w-full">Save Changes</Button>
</form>
</Tabs.Content>
{/if}
<Tabs.Content value="users">
<div class="space-y-4 p-2">
{#if (currentEntity as OverviewGroup).permissions.addMembers || user.id == currentEntity.ownerId}
<h1>you have permission to add members</h1>
{/if}
{#if (currentEntity as OverviewGroup).permissions.changeTitle || user.id == currentEntity.ownerId}
<h1>you have permission to change title</h1>
{/if}
{#if (currentEntity as OverviewGroup).permissions.removeMembers || user.id == currentEntity.ownerId}
<h1>you have permission to remove members</h1>
{/if}
</div>
</Tabs.Content>
{/if}
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
{/if}
</div>
</Sidebar.Header>
<Sidebar.Content> <Sidebar.Content>
<Sidebar.Group> <Sidebar.Group>
<Sidebar.GroupLabel>Members</Sidebar.GroupLabel> <Sidebar.GroupLabel>Members</Sidebar.GroupLabel>
@ -41,7 +133,7 @@
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton> <Sidebar.MenuButton>
{#snippet child({ props })} {#snippet child({ props })}
<User user={member} /> <User user={member} crown={member.id == currentEntity.ownerId} />
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
@ -49,45 +141,5 @@
</Sidebar.Menu> </Sidebar.Menu>
</Sidebar.GroupContent> </Sidebar.GroupContent>
</Sidebar.Group> </Sidebar.Group>
{#if currentEntityId && ServerID.is(currentEntityId) && user}
<Sidebar.Group>
<Sidebar.GroupLabel>Server Actions</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<form method="POST" action="?/inviteUser" class="w-full">
<input type="hidden" name="serverId" value={currentEntityId} />
<button type="submit" class="flex w-full items-center gap-2" {...props}>
<UserRoundPlus class="size-4" />
<span>Invite User</span>
</button>
</form>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{#if user.id === members.find((m) => m.id === currentEntityId)?.ownerId}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<form method="POST" action="?/leaveServer" class="w-full">
<input type="hidden" name="serverId" value={currentEntityId} />
<button type="submit" class="flex w-full items-center gap-2" {...props}>
<UserRoundMinus class="size-4" />
<span>Leave Server</span>
</button>
</form>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/if}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}
</Sidebar.Content> </Sidebar.Content>
</Sidebar.Root> </Sidebar.Root>

View file

@ -49,6 +49,11 @@ export type OverviewGroup = {
ownerId: string; ownerId: string;
members: number; members: number;
image: string; image: string;
permissions: {
changeTitle: boolean;
addMembers: boolean;
removeMembers: boolean;
};
}; };
export interface UserWithStatus extends OverviewUser { export interface UserWithStatus extends OverviewUser {

View file

@ -100,7 +100,10 @@ export async function validateSessionToken(token: string) {
id: table.group.id, id: table.group.id,
name: table.group.name, name: table.group.name,
ownerId: table.group.owner, ownerId: table.group.owner,
members: table.group.members members: table.group.members,
changeTitle: table.group.changeTitle,
addMembers: table.group.addMembers,
removeMembers: table.group.removeMembers
}) })
.from(table.group) .from(table.group)
.where(inArray(table.group.id, user.groups as string[])) .where(inArray(table.group.id, user.groups as string[]))
@ -124,7 +127,15 @@ export async function validateSessionToken(token: string) {
servers, servers,
friends, friends,
groups: groups.map((z) => { groups: groups.map((z) => {
return { ...z, members: (z.members as string[]).length }; return {
...z,
members: (z.members as string[]).length,
permissions: {
changeTitle: !!z.changeTitle,
addMembers: !!z.addMembers,
removeMembers: !!z.removeMembers
}
};
}), }),
friendRequests friendRequests
} }

View file

@ -1,3 +1,4 @@
import { boolean } from 'drizzle-orm/singlestore-core';
import { Status } from '../../index.ts'; import { Status } from '../../index.ts';
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'; import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
@ -37,6 +38,9 @@ export const group = sqliteTable('group', {
owner: text('owner') owner: text('owner')
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
changeTitle: integer('change_title').default(1).notNull(),
addMembers: integer('add_members').default(1).notNull(),
removeMembers: integer('remove_members').default(0).notNull(),
members: text('members', { mode: 'json' }).default([]).notNull(), members: text('members', { mode: 'json' }).default([]).notNull(),
messages: text('messages', { mode: 'json' }).default([]).notNull() messages: text('messages', { mode: 'json' }).default([]).notNull()
}); });

View file

@ -1,4 +1,4 @@
import { db } from '$lib/server/db'; import { db, kvStore } from '$lib/server/db';
import * as table from '$lib/server/db/schema'; import * as table from '$lib/server/db/schema';
import { eq, inArray } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
import { GroupID, ServerID } from '$lib'; import { GroupID, ServerID } from '$lib';
@ -27,6 +27,9 @@ export const GET: RequestHandler = async ({ params }) => {
return json({ return json({
members: members.map((member) => ({ members: members.map((member) => ({
id: member.id, id: member.id,
status: kvStore.get('user-' + member.id + '-state'),
//@TODO Implement statusmessage
statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : 'not vibing',
username: member.username, username: member.username,
image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}`
})) }))
@ -48,6 +51,9 @@ export const GET: RequestHandler = async ({ params }) => {
return json({ return json({
members: members.map((member) => ({ members: members.map((member) => ({
id: member.id, id: member.id,
status: kvStore.get('user-' + member.id + '-state'),
//@TODO Implement statusmessage
statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : 'not vibing',
username: member.username, username: member.username,
image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}`
})) }))

View file

@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { kvStore } from '$lib/server/db'; import { kvStore } from '$lib/server/db';
import { Status } from '$lib';
export const GET: RequestHandler = async ({ params }) => { export const GET: RequestHandler = async ({ params }) => {
const { userId } = params; const { userId } = params;
@ -9,6 +10,11 @@ export const GET: RequestHandler = async ({ params }) => {
userId, userId,
status: kvStore.get('user-' + userId + '-state'), status: kvStore.get('user-' + userId + '-state'),
//@TODO Implement statusmessage //@TODO Implement statusmessage
statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : null statusMessage:
kvStore.get('user-' + userId + '-state') != Status.OFFLINE
? Math.random() > 0.5
? 'vibing 🟢'
: 'not vibing'
: ''
}); });
}; };

View file

@ -37,6 +37,8 @@ export async function GET({ locals, request }) {
const userId = locals.user.id; const userId = locals.user.id;
//@TODO add more to subscribed eventually, server members, et cetera //@TODO add more to subscribed eventually, server members, et cetera
const subscribed = locals.user.friends.map((f) => f.id); const subscribed = locals.user.friends.map((f) => f.id);
subscribed.push(userId); // shit such as friend requests
const overwrite = locals.user.statusOverwrite; const overwrite = locals.user.statusOverwrite;
const sessionId = crypto.randomUUID(); const sessionId = crypto.randomUUID();

View file

@ -7,6 +7,7 @@ import { DirectMessageID, FriendRequestID, GroupID, ServerID } from '$lib';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { and } from 'drizzle-orm'; import { and } from 'drizzle-orm';
import { type User } from '$lib/server/db/schema'; import { type User } from '$lib/server/db/schema';
import { _sendToSubscribers } from '../api/updates/+server';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const user = requireLogin(); const user = requireLogin();
return { user }; return { user };
@ -92,6 +93,8 @@ export const actions = {
.where(eq(table.user.id, user[0].id)); .where(eq(table.user.id, user[0].id));
}); });
_sendToSubscribers(locals.user!.id, { type: 'friends', status: 'accepted' });
_sendToSubscribers(user[0].id, { type: 'friends', status: 'accepted' });
return { success: true }; return { success: true };
} }
@ -111,6 +114,8 @@ export const actions = {
fromUsername: locals.user!.username fromUsername: locals.user!.username
}); });
_sendToSubscribers(locals.user!.id, { type: 'friends', status: 'sent-request' });
_sendToSubscribers(user[0].id, { type: 'friends', status: 'new-request' });
return { success: true }; return { success: true };
}, },
removeFriend: async ({ request, locals }) => { removeFriend: async ({ request, locals }) => {
@ -185,6 +190,9 @@ export const actions = {
// delete the request // delete the request
await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1); await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1);
_sendToSubscribers(fr.fromUser, { type: 'friends', status: 'request-cancelled' });
_sendToSubscribers(fr.toUser, { type: 'friends', status: 'request-cancelled' });
return { success: true }; return { success: true };
}, },
createGroup: async ({ request, locals }) => { createGroup: async ({ request, locals }) => {

View file

@ -33,7 +33,7 @@
let { form, data }: { form: ActionData; data: PageServerData } = $props(); let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null); let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state(); let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
let ownerId: string | null = $state(null);
let isMembersTabOpen = $state(true); let isMembersTabOpen = $state(true);
let members: UserWithStatus[] = $state([]); let members: UserWithStatus[] = $state([]);
@ -72,6 +72,7 @@
members = data.members; members = data.members;
} }
fetchMembers(); fetchMembers();
ownerId = (currentPage as OverviewGroup | OverviewServer).ownerId;
} else { } else {
isMembersTabOpen = false; isMembersTabOpen = false;
} }
@ -125,6 +126,7 @@
name: z.name, name: z.name,
ownerId: z.ownerId, ownerId: z.ownerId,
members: z.members, members: z.members,
permissions: z.permissions,
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
}; };
}); });
@ -160,15 +162,22 @@
const json = JSON.parse(e.data) as const json = JSON.parse(e.data) as
| { type: 'connected'; sessionId: string } | { type: 'connected'; sessionId: string }
| { type: 'message'; message: ReturnMessage } | { type: 'message'; message: ReturnMessage }
| { type: 'status'; id: string; status: 1 | 2 | 3 }; | { type: 'status'; id: string; status: 1 | 2 | 3 }
| { type: 'friends'; status: string };
if (json.type == 'friends') {
alert(json.status);
location.reload();
}
if (json.type == 'connected') { if (json.type == 'connected') {
console.log('SSE connected. We are sessionID ' + json.sessionId); console.log('SSE connected. We are sessionID ' + json.sessionId);
sessionId = json.sessionId; sessionId = json.sessionId;
} }
if (json.type == 'status') { if (json.type == 'status') {
//@TODO update everywhere where user is used
const friend = overview_data.friends.find((z) => z.id == json.id); const friend = overview_data.friends.find((z) => z.id == json.id);
if (friend) { if (friend) {
friend.status = json.status; friend.status = json.status;
} }
@ -325,6 +334,7 @@
bind:open={isMembersTabOpen} bind:open={isMembersTabOpen}
user={data.user} user={data.user}
{members} {members}
currentEntity={currentPage}
currentEntityId={currentPageID} currentEntityId={currentPageID}
/> />
</Sidebar.Provider> </Sidebar.Provider>