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",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"@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",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
|
|
|||
|
|
@ -9,22 +9,26 @@
|
|||
import {
|
||||
GroupID,
|
||||
ServerID,
|
||||
type OverviewData,
|
||||
type OverviewGroup,
|
||||
type OverviewServer,
|
||||
type UserWithStatus
|
||||
} from '$lib';
|
||||
import Button from './ui/button/button.svelte';
|
||||
import Input from './ui/input/input.svelte';
|
||||
|
||||
// Props for the member sidebar.
|
||||
let {
|
||||
open = $bindable(true),
|
||||
members = $bindable<UserWithStatus[]>([]),
|
||||
user,
|
||||
data,
|
||||
currentEntity,
|
||||
currentEntityId = $bindable<string | null>(null)
|
||||
}: {
|
||||
open: boolean;
|
||||
members: UserWithStatus[];
|
||||
data: OverviewData;
|
||||
user: SessionValidationResult['user'];
|
||||
currentEntity: OverviewGroup | OverviewServer;
|
||||
currentEntityId: string | null;
|
||||
|
|
@ -104,15 +108,59 @@
|
|||
</Tabs.Content>
|
||||
{/if}
|
||||
<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}
|
||||
<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 (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 (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}
|
||||
</div>
|
||||
</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">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
|
||||
let { children } = $props();
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
|
||||
<Toaster />
|
||||
<ModeWatcher />
|
||||
{@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 {
|
||||
for (const client of _clients.values()) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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 } from '../api/updates/+server';
|
||||
import { _sendToSubscribers, _sendToUser } from '../api/updates/+server';
|
||||
import { _findDmId } from '../api/messages/[[grp_srv_dm]]/[[channelId]]/[[channelId]]/+server';
|
||||
export const load: PageServerLoad = async () => {
|
||||
const user = requireLogin();
|
||||
|
|
@ -245,6 +245,7 @@ export const actions = {
|
|||
|
||||
await db.transaction(async (tx) => {
|
||||
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);
|
||||
await tx
|
||||
.update(table.user)
|
||||
|
|
@ -293,6 +294,138 @@ export const actions = {
|
|||
|
||||
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 }) => {
|
||||
const data = await request.formData();
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@
|
|||
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';
|
||||
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
let errorOpen = $state(true);
|
||||
|
||||
let { form, data }: { form: ActionData; data: PageServerData } = $props();
|
||||
|
|
@ -196,13 +195,25 @@
|
|||
| { type: 'connected'; sessionId: string }
|
||||
| { type: 'message'; message: ReturnMessage }
|
||||
| { type: 'status'; id: string; status: 1 | 2 | 3 }
|
||||
| { type: 'friends'; status: string };
|
||||
| { type: 'friends'; status: string }
|
||||
| { type: 'group'; status: string };
|
||||
|
||||
if (json.type == 'friends') {
|
||||
toast('Invalidation from friends updates, recieved ' + json.status);
|
||||
await invalidateAll();
|
||||
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') {
|
||||
console.log('SSE connected. We are sessionID ' + json.sessionId);
|
||||
sessionId = json.sessionId;
|
||||
|
|
@ -386,13 +397,16 @@
|
|||
</Sidebar.Inset>
|
||||
<Sidebar.Provider class="w-0">
|
||||
<Sidebar.Provider class="w-0">
|
||||
<MemberSidebar
|
||||
bind:open={isMembersTabOpen}
|
||||
user={data.user}
|
||||
{members}
|
||||
currentEntity={currentPage}
|
||||
currentEntityId={currentPageID}
|
||||
/>
|
||||
{#if currentPage && currentPageID && overview_data && members && !UserID.is(currentPageID)}
|
||||
<MemberSidebar
|
||||
bind:open={isMembersTabOpen}
|
||||
user={data.user}
|
||||
data={overview_data}
|
||||
{members}
|
||||
currentEntity={currentPage}
|
||||
currentEntityId={currentPageID}
|
||||
/>
|
||||
{/if}
|
||||
</Sidebar.Provider>
|
||||
</Sidebar.Provider>
|
||||
</Sidebar.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue