enh: manage shared chats

This commit is contained in:
Timothy Jaeryang Baek
2026-01-29 18:51:02 +04:00
parent 26a5d8f75d
commit a10ac774ab
7 changed files with 362 additions and 18 deletions
+48 -17
View File
@@ -20,6 +20,9 @@
import XMark from '../icons/XMark.svelte';
import ChevronUp from '../icons/ChevronUp.svelte';
import ChevronDown from '../icons/ChevronDown.svelte';
import Link from '../icons/Link.svelte';
import LinkSlash from '../icons/LinkSlash.svelte';
import Clipboard from '../icons/Clipboard.svelte';
const i18n = getContext('i18n');
@@ -46,6 +49,7 @@
export let loadHandler: null | Function = null;
export let unarchiveHandler: null | Function = null;
export let unshareHandler: null | Function = null;
const setSortKey = (key) => {
if (orderBy === key) {
@@ -286,30 +290,57 @@
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Delete Chat')}>
{#if unshareHandler && chat.share_id}
<Tooltip content={$i18n.t('Copy Share Link')}>
<button
class="self-center w-fit px-1 text-sm rounded-xl"
on:click={async (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
const shareUrl = `${window.location.origin}/s/${chat.share_id}`;
await navigator.clipboard.writeText(shareUrl);
toast.success($i18n.t('Share link copied to clipboard.'));
}}
>
<Clipboard class="size-4" strokeWidth="1.5" />
</button>
</Tooltip>
{/if}
<Tooltip
content={unshareHandler ? $i18n.t('Unshare Chat') : $i18n.t('Delete Chat')}
>
<button
class="self-center w-fit px-1 text-sm rounded-xl"
on:click={async (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
selectedChatId = chat.id;
showDeleteConfirmDialog = true;
if (unshareHandler) {
unshareHandler(chat.id);
} else {
selectedChatId = chat.id;
showDeleteConfirmDialog = true;
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
{#if unshareHandler}
<LinkSlash />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
{/if}
</button>
</Tooltip>
</div>
@@ -0,0 +1,126 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import { toast } from 'svelte-sonner';
import { getContext } from 'svelte';
import { deleteSharedChatById, getSharedChatList } from '$lib/apis/chats';
import ChatsModal from './ChatsModal.svelte';
const i18n: Writable<any> = getContext('i18n');
export let show = false;
export let onUpdate = () => {};
let chatList: any[] | null = null;
let page = 1;
let query = '';
let orderBy = 'updated_at';
let direction = 'desc';
let allChatsLoaded = false;
let chatListLoading = false;
let searchDebounceTimeout: any;
let filter: any = {};
$: filter = {
...(query ? { query } : {}),
...(orderBy ? { order_by: orderBy } : {}),
...(direction ? { direction } : {})
};
$: if (filter !== null) {
searchHandler();
}
const searchHandler = async () => {
if (!show) {
return;
}
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
page = 1;
chatList = null;
if (query === '') {
chatList = await getSharedChatList(localStorage.token, page, filter);
} else {
searchDebounceTimeout = setTimeout(async () => {
chatList = await getSharedChatList(localStorage.token, page, filter);
}, 500);
}
if ((chatList ?? []).length === 0) {
allChatsLoaded = true;
} else {
allChatsLoaded = false;
}
};
const loadMoreChats = async () => {
chatListLoading = true;
page += 1;
let newChatList = [];
if (query) {
newChatList = await getSharedChatList(localStorage.token, page, filter);
} else {
newChatList = await getSharedChatList(localStorage.token, page, filter);
}
// once the bottom of the list has been reached (no results) there is no need to continue querying
allChatsLoaded = newChatList.length === 0;
if (newChatList.length > 0) {
chatList = [...(chatList || []), ...newChatList];
}
chatListLoading = false;
};
const unshareHandler = async (chatId: string) => {
const res = await deleteSharedChatById(localStorage.token, chatId).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res === true) {
toast.success($i18n.t('Chat unshared successfully.'));
onUpdate();
init();
} else if (res === false) {
toast.error($i18n.t('Failed to unshare chat.'));
}
};
const init = async () => {
chatList = await getSharedChatList(localStorage.token);
};
$: if (show) {
init();
}
</script>
<ChatsModal
bind:show
bind:query
bind:orderBy
bind:direction
title={$i18n.t('Shared Chats')}
emptyPlaceholder={$i18n.t('You have no shared conversations.')}
shareUrl={false}
{chatList}
{allChatsLoaded}
{chatListLoading}
onUpdate={() => {
onUpdate();
init();
}}
loadHandler={loadMoreChats}
{unshareHandler}
/>