add members from group, remove members from group, change title of group
This commit is contained in:
parent
7af96ca084
commit
17778e1736
9 changed files with 266 additions and 22 deletions
5
bun.lock
5
bun.lock
|
|
@ -37,6 +37,7 @@
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.45.6",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.4",
|
||||||
|
"svelte-sonner": "^1.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
|
@ -683,6 +684,8 @@
|
||||||
|
|
||||||
"svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="],
|
"svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="],
|
||||||
|
|
||||||
|
"svelte-sonner": ["svelte-sonner@1.0.7", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A=="],
|
||||||
|
|
||||||
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
|
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
|
||||||
|
|
||||||
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||||
|
|
@ -769,6 +772,8 @@
|
||||||
|
|
||||||
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||||
|
|
||||||
|
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
|
||||||
|
|
||||||
"vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
"vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.45.6",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.4",
|
||||||
|
"svelte-sonner": "^1.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,26 @@
|
||||||
import {
|
import {
|
||||||
GroupID,
|
GroupID,
|
||||||
ServerID,
|
ServerID,
|
||||||
|
type OverviewData,
|
||||||
type OverviewGroup,
|
type OverviewGroup,
|
||||||
type OverviewServer,
|
type OverviewServer,
|
||||||
type UserWithStatus
|
type UserWithStatus
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
import Button from './ui/button/button.svelte';
|
import Button from './ui/button/button.svelte';
|
||||||
|
import Input from './ui/input/input.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,
|
||||||
|
data,
|
||||||
currentEntity,
|
currentEntity,
|
||||||
currentEntityId = $bindable<string | null>(null)
|
currentEntityId = $bindable<string | null>(null)
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
members: UserWithStatus[];
|
members: UserWithStatus[];
|
||||||
|
data: OverviewData;
|
||||||
user: SessionValidationResult['user'];
|
user: SessionValidationResult['user'];
|
||||||
currentEntity: OverviewGroup | OverviewServer;
|
currentEntity: OverviewGroup | OverviewServer;
|
||||||
currentEntityId: string | null;
|
currentEntityId: string | null;
|
||||||
|
|
@ -104,15 +108,59 @@
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
{/if}
|
{/if}
|
||||||
<Tabs.Content value="users">
|
<Tabs.Content value="users">
|
||||||
<div class="space-y-4 p-2">
|
<div class="space-y-6 p-2">
|
||||||
{#if (currentEntity as OverviewGroup).permissions.addMembers || user.id == currentEntity.ownerId}
|
{#if (currentEntity as OverviewGroup).permissions.addMembers || user.id == currentEntity.ownerId}
|
||||||
<h1>you have permission to add members</h1>
|
{@const addableMembers = data.friends.filter(
|
||||||
|
(z) => !members.find((h) => h.id == z.id)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{#if addableMembers.length !== 0}
|
||||||
|
<form method="POST" action="?/addMembers" class="space-y-4">
|
||||||
|
<input type="hidden" name="groupId" value={currentEntityId} />
|
||||||
|
|
||||||
|
<h1>Add members</h1>
|
||||||
|
|
||||||
|
{#each addableMembers as friend (friend.id)}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="memberIds" value={friend.id} />
|
||||||
|
<User crown={false} user={friend} />
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<Button type="submit">Add selected members</Button>
|
||||||
|
</form>
|
||||||
|
<hr />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (currentEntity as OverviewGroup).permissions.changeTitle || user.id == currentEntity.ownerId}
|
{#if (currentEntity as OverviewGroup).permissions.changeTitle || user.id == currentEntity.ownerId}
|
||||||
<h1>you have permission to change title</h1>
|
<form method="POST" action="?/changeTitle" class="space-y-4">
|
||||||
|
<input type="hidden" name="groupId" value={currentEntityId} />
|
||||||
|
|
||||||
|
<h1>Change title</h1>
|
||||||
|
|
||||||
|
<Input name="title" placeholder="New group title" required />
|
||||||
|
|
||||||
|
<Button type="submit">Change title</Button>
|
||||||
|
</form>
|
||||||
|
<hr />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (currentEntity as OverviewGroup).permissions.removeMembers || user.id == currentEntity.ownerId}
|
{#if (currentEntity as OverviewGroup).permissions.removeMembers || user.id == currentEntity.ownerId}
|
||||||
<h1>you have permission to remove members</h1>
|
<form method="POST" action="?/removeMembers" class="space-y-4">
|
||||||
|
<input type="hidden" name="groupId" value={currentEntityId} />
|
||||||
|
|
||||||
|
<h1>Remove members</h1>
|
||||||
|
|
||||||
|
{#each members.filter((z) => z.id != currentEntity.ownerId) as member (member.id)}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="memberIds" value={member.id} />
|
||||||
|
<User crown={false} user={member} />
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<Button type="submit">Remove selected members</Button>
|
||||||
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
|
||||||
1
src/lib/components/ui/sonner/index.ts
Normal file
1
src/lib/components/ui/sonner/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Toaster } from "./sonner.svelte";
|
||||||
34
src/lib/components/ui/sonner/sonner.svelte
Normal file
34
src/lib/components/ui/sonner/sonner.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import CircleCheckIcon from "@lucide/svelte/icons/circle-check";
|
||||||
|
import InfoIcon from "@lucide/svelte/icons/info";
|
||||||
|
import Loader2Icon from "@lucide/svelte/icons/loader-2";
|
||||||
|
import OctagonXIcon from "@lucide/svelte/icons/octagon-x";
|
||||||
|
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
|
||||||
|
|
||||||
|
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||||
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
|
let { ...restProps }: SonnerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sonner
|
||||||
|
theme={mode.current}
|
||||||
|
class="toaster group"
|
||||||
|
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||||
|
{...restProps}
|
||||||
|
>{#snippet loadingIcon()}
|
||||||
|
<Loader2Icon class="size-4 animate-spin" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet successIcon()}
|
||||||
|
<CircleCheckIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet errorIcon()}
|
||||||
|
<OctagonXIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet infoIcon()}
|
||||||
|
<InfoIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet warningIcon()}
|
||||||
|
<TriangleAlertIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
</Sonner>
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ export function _sendToSubscribers(id: string, payload: unknown) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function _sendToUser(userId: string, payload: unknown) {
|
||||||
|
for (const [_, client] of _clients) {
|
||||||
|
if (client.userId == userId) {
|
||||||
|
client.controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function _isUserConnected(userId: string): boolean {
|
export function _isUserConnected(userId: string): boolean {
|
||||||
for (const client of _clients.values()) {
|
for (const client of _clients.values()) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +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';
|
import { _sendToSubscribers, _sendToUser } from '../api/updates/+server';
|
||||||
import { _findDmId } from '../api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server';
|
import { _findDmId } from '../api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server';
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
const user = requireLogin();
|
const user = requireLogin();
|
||||||
|
|
@ -245,6 +245,7 @@ export const actions = {
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
for await (const member of members) {
|
for await (const member of members) {
|
||||||
|
_sendToUser(member, { type: 'group', status: 'added-to-group' });
|
||||||
const user = await tx.select().from(table.user).where(eq(table.user.id, member)).limit(1);
|
const user = await tx.select().from(table.user).where(eq(table.user.id, member)).limit(1);
|
||||||
await tx
|
await tx
|
||||||
.update(table.user)
|
.update(table.user)
|
||||||
|
|
@ -293,6 +294,138 @@ export const actions = {
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
addMembers: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const groupId = data.get('groupId');
|
||||||
|
const memberIds = data.getAll('memberIds').map(String);
|
||||||
|
|
||||||
|
if (typeof groupId !== 'string') {
|
||||||
|
return fail(400, { error: 'Invalid group ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memberIds.length) {
|
||||||
|
return fail(400, { error: 'No members selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1);
|
||||||
|
if (!group.length) return fail(404, { error: 'Group not found' });
|
||||||
|
|
||||||
|
const g = group[0];
|
||||||
|
|
||||||
|
if (!(g.members as string[]).includes(locals.user!.id)) {
|
||||||
|
return fail(403, { error: 'You do not have permission to act on this group.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOwner = g.owner === locals.user!.id;
|
||||||
|
if (!isOwner && !g.addMembers) {
|
||||||
|
return fail(403, { error: 'No permission to add members' });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of memberIds) {
|
||||||
|
if (!locals.user!.friends.find((f) => f.id === id)) {
|
||||||
|
return fail(403, { error: 'Can only add friends' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMembers = [...new Set([...(g.members as string[]), ...memberIds])];
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.update(table.group).set({ members: newMembers }).where(eq(table.group.id, groupId));
|
||||||
|
|
||||||
|
for (const id of memberIds) {
|
||||||
|
const user = await tx.select().from(table.user).where(eq(table.user.id, id)).limit(1);
|
||||||
|
if (!user.length) continue;
|
||||||
|
if (newMembers.includes(id)) {
|
||||||
|
_sendToUser(id, { type: 'group', status: 'added-to-group' });
|
||||||
|
} else {
|
||||||
|
_sendToUser(id, { type: 'group', status: 'member-added-to-group' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(table.user)
|
||||||
|
.set({ groups: (user[0].groups as string[]).concat(groupId) })
|
||||||
|
.where(eq(table.user.id, id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
removeMembers: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const groupId = data.get('groupId');
|
||||||
|
const memberIds = data.getAll('memberIds').map(String);
|
||||||
|
|
||||||
|
if (typeof groupId !== 'string') {
|
||||||
|
return fail(400, { error: 'Invalid group ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1);
|
||||||
|
if (!group.length) return fail(404, { error: 'Group not found' });
|
||||||
|
|
||||||
|
const g = group[0];
|
||||||
|
|
||||||
|
if (!(g.members as string[]).includes(locals.user!.id)) {
|
||||||
|
return fail(403, { error: 'You do not have permission to act on this group.' });
|
||||||
|
}
|
||||||
|
const isOwner = g.owner === locals.user!.id;
|
||||||
|
if (!isOwner && !g.removeMembers) {
|
||||||
|
return fail(403, { error: 'No permission to remove members' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberIds.includes(g.owner)) {
|
||||||
|
return fail(400, { error: 'Cannot remove group owner' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = (g.members as string[]).filter((id) => !memberIds.includes(id));
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.update(table.group).set({ members: remaining }).where(eq(table.group.id, groupId));
|
||||||
|
|
||||||
|
for (const id of remaining) {
|
||||||
|
_sendToUser(id, { type: 'group', status: 'someone-was-removed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of memberIds) {
|
||||||
|
_sendToUser(id, { type: 'group', status: 'removed-from-group' });
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(table.user)
|
||||||
|
.set({
|
||||||
|
groups: locals.user!.groups.map((z) => z.id).filter((g) => g !== groupId)
|
||||||
|
})
|
||||||
|
.where(eq(table.user.id, id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
changeTitle: async ({ request, locals }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const groupId = data.get('groupId');
|
||||||
|
const title = data.get('title');
|
||||||
|
|
||||||
|
if (typeof groupId !== 'string' || typeof title !== 'string' || title.length < 1) {
|
||||||
|
return fail(400, { error: 'Invalid input' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1);
|
||||||
|
if (!group.length) return fail(404, { error: 'Group not found' });
|
||||||
|
|
||||||
|
const g = group[0];
|
||||||
|
if (!(g.members as string[]).includes(locals.user!.id)) {
|
||||||
|
return fail(403, { error: 'You do not have permission to act on this group.' });
|
||||||
|
}
|
||||||
|
const isOwner = g.owner === locals.user!.id;
|
||||||
|
|
||||||
|
if (!isOwner && !g.changeTitle) {
|
||||||
|
return fail(403, { error: 'No permission to change title' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.update(table.group).set({ name: title }).where(eq(table.group.id, groupId));
|
||||||
|
//@TODO if a user isn't in the group screen this doesnt get propogated
|
||||||
|
_sendToSubscribers(groupId, { type: 'group', status: 'name-changed' });
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
createServer: async ({ request, locals }) => {
|
createServer: async ({ request, locals }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,8 @@
|
||||||
import SendHorizontal from '@lucide/svelte/icons/send-horizontal';
|
import SendHorizontal from '@lucide/svelte/icons/send-horizontal';
|
||||||
import PersonStanding from '@lucide/svelte/icons/person-standing';
|
import PersonStanding from '@lucide/svelte/icons/person-standing';
|
||||||
import MemberSidebar from '$lib/components/member-sidebar.svelte';
|
import MemberSidebar from '$lib/components/member-sidebar.svelte';
|
||||||
import User from '$lib/components/extra/User.svelte';
|
import { invalidateAll } from '$app/navigation';
|
||||||
import { invalidate, invalidateAll } from '$app/navigation';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
let errorOpen = $state(true);
|
let errorOpen = $state(true);
|
||||||
|
|
||||||
let { form, data }: { form: ActionData; data: PageServerData } = $props();
|
let { form, data }: { form: ActionData; data: PageServerData } = $props();
|
||||||
|
|
@ -196,13 +195,25 @@
|
||||||
| { 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 };
|
| { type: 'friends'; status: string }
|
||||||
|
| { type: 'group'; status: string };
|
||||||
|
|
||||||
if (json.type == 'friends') {
|
if (json.type == 'friends') {
|
||||||
|
toast('Invalidation from friends updates, recieved ' + json.status);
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
await fill_overview_data();
|
await fill_overview_data();
|
||||||
}
|
}
|
||||||
|
if (json.type == 'group') {
|
||||||
|
toast('Invalidation from group updates, recieved ' + json.status);
|
||||||
|
if (json.status == 'removed-from-group' && GroupID.is(currentPageID)) {
|
||||||
|
currentPageID = null;
|
||||||
|
currentPage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await invalidateAll();
|
||||||
|
|
||||||
|
await fill_overview_data();
|
||||||
|
}
|
||||||
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;
|
||||||
|
|
@ -386,13 +397,16 @@
|
||||||
</Sidebar.Inset>
|
</Sidebar.Inset>
|
||||||
<Sidebar.Provider class="w-0">
|
<Sidebar.Provider class="w-0">
|
||||||
<Sidebar.Provider class="w-0">
|
<Sidebar.Provider class="w-0">
|
||||||
<MemberSidebar
|
{#if currentPage && currentPageID && overview_data && members && !UserID.is(currentPageID)}
|
||||||
bind:open={isMembersTabOpen}
|
<MemberSidebar
|
||||||
user={data.user}
|
bind:open={isMembersTabOpen}
|
||||||
{members}
|
user={data.user}
|
||||||
currentEntity={currentPage}
|
data={overview_data}
|
||||||
currentEntityId={currentPageID}
|
{members}
|
||||||
/>
|
currentEntity={currentPage}
|
||||||
|
currentEntityId={currentPageID}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Sidebar.Provider>
|
</Sidebar.Provider>
|
||||||
</Sidebar.Provider>
|
</Sidebar.Provider>
|
||||||
</Sidebar.Provider>
|
</Sidebar.Provider>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue