| <script lang="ts"> |
| import { toast } from 'svelte-sonner'; |
| import { onMount, getContext, tick, onDestroy } from 'svelte'; |
| const i18n = getContext('i18n'); |
| |
| import { page } from '$app/stores'; |
| import { channels, mobile, showSidebar, user } from '$lib/stores'; |
| import { getUserActiveStatusById } from '$lib/apis/users'; |
| import { updateChannelById, updateChannelMemberActiveStatusById } from '$lib/apis/channels'; |
| import { WEBUI_API_BASE_URL } from '$lib/constants'; |
| |
| import Cog6 from '$lib/components/icons/Cog6.svelte'; |
| import ChannelModal from './ChannelModal.svelte'; |
| import Lock from '$lib/components/icons/Lock.svelte'; |
| import Hashtag from '$lib/components/icons/Hashtag.svelte'; |
| import Users from '$lib/components/icons/Users.svelte'; |
| import XMark from '$lib/components/icons/XMark.svelte'; |
| import Emoji from '$lib/components/common/Emoji.svelte'; |
| |
| export let onUpdate: Function = () => {}; |
| |
| export let className = ''; |
| export let channel; |
| |
| let showEditChannelModal = false; |
| |
| let itemElement; |
| |
| const hasPublicReadGrant = (grants: any) => |
| Array.isArray(grants) && |
| grants.some( |
| (grant) => |
| grant?.principal_type === 'user' && |
| grant?.principal_id === '*' && |
| grant?.permission === 'read' |
| ); |
| |
| const isPublicChannel = (channel: any): boolean => { |
| if (channel?.type === 'group') { |
| if (typeof channel?.is_private === 'boolean') { |
| return !channel.is_private; |
| } |
| return hasPublicReadGrant(channel?.access_grants); |
| } |
| return hasPublicReadGrant(channel?.access_grants); |
| }; |
| </script> |
|
|
| <ChannelModal |
| bind:show={showEditChannelModal} |
| {channel} |
| edit={true} |
| {onUpdate} |
| onSubmit={async (payload: any) => { |
| const { name, is_private, access_grants, group_ids, user_ids } = payload ?? {}; |
| const res = await updateChannelById(localStorage.token, channel.id, { |
| name, |
| is_private, |
| access_grants, |
| group_ids, |
| user_ids |
| }).catch((error) => { |
| toast.error(error.message); |
| }); |
|
|
| if (res) { |
| toast.success($i18n.t('Channel updated successfully')); |
| } |
|
|
| onUpdate(); |
| }} |
| /> |
|
|
| <div |
| id="sidebar-channel-item" |
| bind:this={itemElement} |
| class=" w-full {className} rounded-xl flex relative group hover:bg-gray-100 dark:hover:bg-gray-900 {$page |
| .url.pathname === `/channels/${channel.id}` |
| ? 'bg-gray-100 dark:bg-gray-900 selected' |
| : ''} {channel?.type === 'dm' ? 'px-1 py-[3px]' : 'p-1'} {channel?.unread_count > 0 |
| ? 'font-medium dark:text-white text-black' |
| : ' dark:text-gray-400 text-gray-600'} cursor-pointer select-none" |
| > |
| <a |
| class=" w-full flex justify-between" |
| href="/channels/{channel.id}" |
| on:click={() => { |
| console.log(channel); |
|
|
| if ($channels) { |
| channels.set( |
| $channels.map((ch) => { |
| if (ch.id === channel.id) { |
| ch.unread_count = 0; |
| } |
| return ch; |
| }) |
| ); |
| } |
|
|
| if ($mobile) { |
| showSidebar.set(false); |
| } |
| }} |
| draggable="false" |
| > |
| <div class="flex items-center gap-1"> |
| <div> |
| {#if channel?.type === 'dm'} |
| {#if channel?.users} |
| {@const channelMembers = channel.users.filter((u) => u.id !== $user?.id)} |
| <div class="flex ml-[1px] mr-0.5 relative"> |
| {#each channelMembers.slice(0, 2) as u, index} |
| <img |
| src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`} |
| alt={u.name} |
| class=" size-5.5 rounded-full border-2 border-white dark:border-gray-900 {index === |
| 1 |
| ? '-ml-2.5' |
| : ''}" |
| /> |
| {/each} |
| |
| {#if channelMembers.length === 1} |
| <div class="absolute bottom-0 right-0"> |
| <span class="relative flex size-2"> |
| {#if channelMembers[0]?.is_active} |
| <span |
| class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" |
| ></span> |
| {/if} |
| <span |
| class="relative inline-flex size-2 rounded-full {channelMembers[0]?.is_active |
| ? 'bg-green-500' |
| : 'bg-gray-300 dark:bg-gray-700'} border-[1.5px] border-white dark:border-gray-900" |
| ></span> |
| </span> |
| </div> |
| {/if} |
| </div> |
| {:else} |
| <Users className="size-4 ml-1 mr-0.5" strokeWidth="2" /> |
| {/if} |
| {:else} |
| <div class=" size-4 justify-center flex items-center ml-1"> |
| {#if isPublicChannel(channel)} |
| <Hashtag className="size-3.5" strokeWidth="2.5" /> |
| {:else} |
| <Lock className="size-[15px]" strokeWidth="2" /> |
| {/if} |
| </div> |
| {/if} |
| </div> |
|
|
| <div |
| class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1 pr-1 flex items-center gap-2.5" |
| > |
| {#if channel?.name} |
| <span class="line-clamp-1"> |
| {channel.name} |
| </span> |
| {:else} |
| <span class="shrink-0 line-clamp-1"> |
| {channel?.users |
| ?.filter((u) => u.id !== $user?.id) |
| .map((u) => u.name) |
| .join(', ')} |
| </span> |
|
|
| {#if channel?.users?.length === 2} |
| {@const dmUser = channel.users.find((u) => u.id !== $user?.id)} |
|
|
| {#if dmUser?.status_emoji || dmUser?.status_message} |
| <span class="flex gap-1.5 line-clamp-1"> |
| {#if dmUser?.status_emoji} |
| <div class=" self-center shrink-0"> |
| <Emoji className="size-3.5" shortCode={dmUser?.status_emoji} /> |
| </div> |
| {/if} |
| |
| <div class="line-clamp-1 italic"> |
| {dmUser?.status_message} |
| </div> |
| </span> |
| {/if} |
| {/if} |
| {/if} |
| </div> |
| </div> |
|
|
| <div class="flex items-center"> |
| {#if channel?.unread_count > 0} |
| <div |
| class="text-xs py-[1px] px-2 rounded-xl bg-gray-100 text-black dark:bg-gray-800 dark:text-white font-medium whitespace-nowrap" |
| > |
| {new Intl.NumberFormat($i18n.locale, { |
| notation: 'compact', |
| compactDisplay: 'short' |
| }).format(channel.unread_count)} |
| </div> |
| {/if} |
| </div> |
| </a> |
|
|
| {#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" |
| > |
| <button |
| type="button" |
| class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" |
| on:click={async (e) => { |
| e.stopImmediatePropagation(); |
| e.stopPropagation(); |
|
|
| channels.update((chs) => |
| chs.filter((ch) => { |
| return ch.id !== channel.id; |
| }) |
| ); |
|
|
| await updateChannelMemberActiveStatusById(localStorage.token, channel.id, false).catch( |
| (error) => { |
| toast.error(`${error}`); |
| } |
| ); |
| }} |
| > |
| <XMark className="size-3.5" /> |
| </button> |
| </div> |
| {: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" |
| > |
| <button |
| type="button" |
| class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" |
| on:click={(e) => { |
| e.stopImmediatePropagation(); |
| e.stopPropagation(); |
| showEditChannelModal = true; |
| }} |
| > |
| <Cog6 className="size-3.5" /> |
| </button> |
| </div> |
| {/if} |
| </div> |
|
|