feat/enh: group channel

This commit is contained in:
Timothy Jaeryang Baek
2025-11-30 08:24:27 -05:00
parent 696f356881
commit f589b7c189
17 changed files with 830 additions and 369 deletions
+15 -4
View File
@@ -181,9 +181,18 @@
};
const initChannels = async () => {
// default (none), group, dm type
await channels.set(
(await getChannels(localStorage.token)).sort((a, b) =>
a.type === b.type ? 0 : a.type === 'dm' ? 1 : -1
a.type === b.type
? 0
: a.type === 'dm'
? 1
: a.type === 'group'
? b.type === 'dm'
? -1
: 0
: -1
)
);
};
@@ -486,7 +495,7 @@
<ChannelModal
bind:show={showCreateChannel}
onSubmit={async ({ type, name, access_control, user_ids }) => {
onSubmit={async ({ type, name, is_private, access_control, group_ids, user_ids }) => {
name = name?.trim();
if (type === 'dm') {
@@ -504,7 +513,9 @@
const res = await createNewChannel(localStorage.token, {
type: type,
name: name,
is_private: is_private,
access_control: access_control,
group_ids: group_ids,
user_ids: user_ids
}).catch((error) => {
toast.error(`${error}`);
@@ -940,14 +951,14 @@
</Folder>
{/if}
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || ($user?.permissions?.features?.channels ?? true))}
<Folder
id="sidebar-channels"
className="px-2 mt-0.5"
name={$i18n.t('Channels')}
chevron={false}
dragAndDrop={false}
onAdd={$user?.role === 'admin'
onAdd={$user?.role === 'admin' || ($user?.permissions?.features?.channels ?? true)
? async () => {
await tick();
@@ -31,10 +31,13 @@
{channel}
edit={true}
{onUpdate}
onSubmit={async ({ name, access_control }) => {
onSubmit={async ({ name, is_private, access_control, group_ids, user_ids }) => {
const res = await updateChannelById(localStorage.token, channel.id, {
name,
access_control
is_private,
access_control,
group_ids,
user_ids
}).catch((error) => {
toast.error(error.message);
});
@@ -119,7 +122,7 @@
{/if}
{:else}
<div class=" size-4 justify-center flex items-center ml-1">
{#if channel?.access_control === null}
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
<Hashtag className="size-3.5" strokeWidth="2.5" />
{:else}
<Lock className="size-[15px]" strokeWidth="2" />
@@ -154,7 +157,7 @@
</div>
</a>
{#if channel?.type === 'dm'}
{#if ['dm'].includes(channel?.type)}
<div
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
>
@@ -181,7 +184,7 @@
<XMark className="size-3.5" />
</button>
</div>
{:else if $user?.role === 'admin'}
{:else if $user?.role === 'admin' || channel.user_id === $user?.id}
<div
class="ml-0.5 mr-1 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
>
@@ -1,18 +1,22 @@
<script lang="ts">
import { getContext, createEventDispatcher, onMount } from 'svelte';
const i18n = getContext('i18n');
import { toast } from 'svelte-sonner';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { createNewChannel, deleteChannelById } from '$lib/apis/channels';
import { user } from '$lib/stores';
import Spinner from '$lib/components/common/Spinner.svelte';
import Modal from '$lib/components/common/Modal.svelte';
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import { toast } from 'svelte-sonner';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import UserListSelector from '$lib/components/workspace/common/UserListSelector.svelte';
const i18n = getContext('i18n');
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
import Visibility from '$lib/components/workspace/common/Visibility.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let show = false;
export let onSubmit: Function = () => {};
@@ -21,10 +25,14 @@
export let channel = null;
export let edit = false;
let channelTypes = ['group', 'dm'];
let type = '';
let name = '';
let isPrivate = null;
let accessControl = {};
let groupIds = [];
let userIds = [];
let loading = false;
@@ -33,12 +41,26 @@
name = name.replace(/\s/g, '-').toLocaleLowerCase();
}
$: onTypeChange(type);
const onTypeChange = (type) => {
if (type === 'group') {
if (isPrivate === null) {
isPrivate = true;
}
} else {
isPrivate = null;
}
};
const submitHandler = async () => {
loading = true;
await onSubmit({
type: type,
name: name.replace(/\s/g, '-'),
access_control: accessControl,
is_private: type === 'group' ? isPrivate : null,
access_control: type === '' ? accessControl : {},
group_ids: groupIds,
user_ids: userIds
});
show = false;
@@ -46,16 +68,24 @@
};
const init = () => {
type = channel?.type ?? '';
name = channel?.name ?? '';
accessControl = channel.access_control;
userIds = channel?.user_ids ?? [];
if ($user?.role === 'admin') {
channelTypes = ['', 'group', 'dm'];
} else {
channelTypes = ['group', 'dm'];
}
type = channel?.type ?? channelTypes[0];
if (channel) {
name = channel?.name ?? '';
isPrivate = channel?.is_private ?? null;
accessControl = channel.access_control;
userIds = channel?.user_ids ?? [];
}
};
$: if (show) {
if (channel) {
init();
}
init();
} else {
resetHandler();
}
@@ -119,21 +149,51 @@
}}
>
{#if !edit}
<div class="flex flex-col w-full mt-2">
<div class="flex flex-col w-full mt-2 mb-1">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Channel Type')}</div>
<div class="flex-1">
<select
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={type}
<Tooltip
content={type === 'dm'
? $i18n.t('A private conversation between you and selected users')
: type === 'group'
? $i18n.t('A collaboration channel where people join as members')
: $i18n.t(
'A discussion channel where access is controlled by groups and permissions'
)}
placement="top-start"
>
<option value="">{$i18n.t('Channel')}</option>
<option value="dm">{$i18n.t('Direct Message')}</option>
</select>
<select
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={type}
>
{#each channelTypes as channelType, channelTypeIdx (channelType)}
<option value={channelType} selected={channelTypeIdx === 0}>
{#if channelType === 'group'}
{$i18n.t('Group Channel')}
{:else if channelType === 'dm'}
{$i18n.t('Direct Message')}
{:else if channelType === ''}
{$i18n.t('Channel')}
{/if}
</option>
{/each}
</select>
</Tooltip>
</div>
</div>
{/if}
<div class=" text-gray-300 dark:text-gray-700 text-xs">
{#if type === ''}
{$i18n.t('Discussion channel where access is based on groups and permissions')}
{:else if type === 'group'}
{$i18n.t('Collaboration channel where people join as members')}
{:else if type === 'dm'}
{$i18n.t('Private conversation between selected users')}
{/if}
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('Channel Name')}
@@ -154,17 +214,31 @@
</div>
</div>
<hr class=" border-gray-100/40 dark:border-gray-700/10 my-2.5 w-full" />
<div class="-mx-2">
{#if type === 'dm'}
<UserListSelector bind:userIds />
{:else}
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
{#if type !== 'dm'}
<div class="-mx-2 mb-1 mt-2.5 px-2">
{#if type === ''}
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
</div>
{/if}
</div>
{:else if type === 'group'}
<Visibility
state={isPrivate ? 'private' : 'public'}
onChange={(value) => {
if (value === 'private') {
isPrivate = true;
} else {
isPrivate = false;
}
console.log(value, isPrivate);
}}
/>
{/if}
</div>
{/if}
{#if type !== ''}
<div class="">
<MemberSelector bind:userIds includeGroups={type !== 'dm'} />
</div>
{/if}
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
{#if edit}