This commit is contained in:
Timothy Jaeryang Baek
2026-07-26 23:49:03 -04:00
parent 2196b4e1ff
commit 6f93ecd4fd
16 changed files with 550 additions and 62 deletions
@@ -26,6 +26,7 @@
getAllTags,
getChatById,
getChatListByTagName,
markChatUnreadById,
updateChatById,
updateChatFolderIdById
} from '$lib/apis/chats';
@@ -76,6 +77,7 @@
export let ownerName: string | null = null;
export let ownerUserId: string | null = null;
export let onReadStateChange = (data) => {};
export let onDragEnd = () => {};
@@ -139,6 +141,18 @@
}
};
const markUnreadHandler = async () => {
const res = await markChatUnreadById(localStorage.token, id).catch((error) => {
toast.error(`${error}`);
return null;
});
if (!res) return;
viewedAt = null;
lastReadAt = res.last_read_at ?? 0;
onReadStateChange(res);
};
let showShareChatModal = false;
let confirmEdit = false;
@@ -693,6 +707,7 @@
deleteHandler={() => {
showDeleteConfirm = true;
}}
{markUnreadHandler}
onClose={() => {
dispatch('unselect');
}}
@@ -28,6 +28,7 @@
import PinSlashIcon from './icons/PinSlash.svelte';
import ShareIcon from './icons/Share.svelte';
import TrashIcon from './icons/Trash.svelte';
import ChatCheckIcon from '$lib/components/icons/ChatCheck.svelte';
const i18n = getContext('i18n');
@@ -39,6 +40,7 @@
export let renameHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
export let markUnreadHandler: Function = () => {};
export let chatId = '';
@@ -369,6 +371,18 @@
<div class="flex items-center">{$i18n.t('Rename')}</div>
</button>
<button
draggable="false"
class="flex h-[1.6875rem] gap-2 items-center rounded-xl px-2 text-[13px] cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 w-full"
on:click={() => {
show = false;
markUnreadHandler();
}}
>
<ChatCheckIcon className="size-3.5" strokeWidth="1.5" />
<div class="flex items-center">{$i18n.t('Mark as unread')}</div>
</button>
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
<button
@@ -11,6 +11,7 @@
export let shiftKey = false;
export let onDelete = (folderId) => {};
export let onFolderUnreadCounts = (counts) => {};
let ownedList = [];
let sharedList = [];
@@ -52,6 +53,7 @@
{shiftKey}
{onDelete}
{onItemMove}
{onFolderUnreadCounts}
on:import={(e) => {
dispatch('import', e.detail);
}}
@@ -77,6 +79,7 @@
{shiftKey}
{onDelete}
{onItemMove}
{onFolderUnreadCounts}
on:import={(e) => {
dispatch('import', e.detail);
}}
@@ -12,6 +12,7 @@
import TrashIcon from '../icons/Trash.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Download from '../icons/Download.svelte';
import CheckIcon from '$lib/components/icons/Check.svelte';
export let align: 'start' | 'end' = 'start';
export let onEdit = () => {};
@@ -19,6 +20,7 @@
export let onShare = () => {};
export let onDelete = () => {};
export let onCreateSubFolder = () => {};
export let onMarkAllRead = () => {};
let show = false;
</script>
@@ -57,6 +59,19 @@
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
<button
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
show = false;
onMarkAllRead();
}}
>
<CheckIcon className="size-3.5" />
<div class="flex items-center">{$i18n.t('Mark all as read')}</div>
</button>
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
<button
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
@@ -20,7 +20,8 @@
updateFolderParentIdById,
getFolderById,
createNewFolder,
getSharedFolderChats
getSharedFolderChats,
markFolderChatsReadById
} from '$lib/apis/folders';
import {
getChatById,
@@ -60,6 +61,7 @@
export let onDelete = (e) => {};
export let onItemMove = (e) => {};
export let onFolderUnreadCounts = (counts) => {};
let folderElement;
@@ -83,6 +85,79 @@
compactDisplay: 'short'
}).format(count);
const isUnreadChat = (chat) =>
!(chat.active ?? false) &&
(chat.last_read_at == null ||
(typeof chat.updated_at === 'number' &&
typeof chat.last_read_at === 'number' &&
chat.updated_at > chat.last_read_at));
const sortFolderChats = (items) =>
[...items].sort(
(a, b) =>
Number(isUnreadChat(b)) - Number(isUnreadChat(a)) ||
Number(b.updated_at ?? 0) - Number(a.updated_at ?? 0)
);
const mergeFolderChats = (items, nextItems) => {
const merged = [...items];
const indexById = new Map(merged.map((chat, index) => [chat.id, index]));
for (const chat of nextItems) {
if (!chat?.id) {
continue;
}
const index = indexById.get(chat.id);
if (index === undefined) {
indexById.set(chat.id, merged.length);
merged.push(chat);
} else {
merged[index] = { ...merged[index], ...chat };
}
}
return sortFolderChats(merged);
};
const applyReadState = (data) => {
if (data?.folder_unread_counts) {
onFolderUnreadCounts(data.folder_unread_counts);
}
if (typeof data?.last_read_at === 'number') {
folderRegistry[folderId]?.setChatReadAt?.(data.chat_id, data.last_read_at);
}
};
const markAllReadHandler = async () => {
const res = await markFolderChatsReadById(localStorage.token, folderId).catch((error) => {
toast.error(`${error}`);
return null;
});
if (!res) return;
if (res.folder_unread_counts) {
onFolderUnreadCounts(res.folder_unread_counts);
}
for (const readFolderId of res.folder_ids ?? []) {
if (readFolderId !== folderId) {
folderRegistry[readFolderId]?.setFolderItems?.();
}
}
if (chats) {
chats = sortFolderChats(
chats.map((chat) =>
!chat.user_id || chat.user_id === $user?.id
? { ...chat, last_read_at: chat.updated_at }
: chat
)
);
}
};
const onDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
@@ -275,8 +350,48 @@
onMount(async () => {
open = folders[folderId].is_expanded;
folderRegistry[folderId] = {
setFolderItems: () => {
setFolderItems();
setFolderItems,
upsertChat: (chat) => {
if (chat.folder_id && chat.folder_id !== folderId) {
return;
}
pendingUpsertChats = mergeFolderChats(pendingUpsertChats, [chat]);
if (open || chats) {
chats = mergeFolderChats(chats ?? [], [chat]);
}
},
setChatActive: (chatId, active) => {
if (chats) {
let found = false;
chats = sortFolderChats(
chats.map((chat) => {
if (chat.id !== chatId) {
return chat;
}
found = true;
return { ...chat, active };
})
);
return found;
}
return false;
},
setChatReadAt: (chatId, lastReadAt) => {
if (chats) {
let found = false;
chats = sortFolderChats(
chats.map((chat) => {
if (chat.id !== chatId) {
return chat;
}
found = true;
return { ...chat, last_read_at: lastReadAt };
})
);
return found;
}
return false;
}
};
if (folderElement) {
@@ -396,10 +511,19 @@
let chatsPage = 1;
let hasMoreChats = false;
let chatsLoading = false;
let queuedReload = false;
let pendingUpsertChats = [];
export const setFolderItems = async (append = false) => {
await tick();
if (open && !chatsLoading) {
if (open && chatsLoading) {
if (!append) {
queuedReload = true;
}
return;
}
if (open) {
// Always use getSharedFolderChats so owners also see chats
// created by users who have write access to this folder.
const nextPage = append ? chatsPage + 1 : 1;
@@ -409,7 +533,11 @@
page: nextPage
});
const nextChats = res?.chats ?? [];
chats = append ? [...(chats ?? []), ...nextChats] : nextChats;
const merged = append ? mergeFolderChats(chats ?? [], nextChats) : nextChats;
chats = mergeFolderChats(merged, pendingUpsertChats);
pendingUpsertChats = pendingUpsertChats.filter(
(pendingChat) => !nextChats.some((chat) => chat.id === pendingChat.id)
);
chatsPage = nextPage;
hasMoreChats = res?.has_more ?? nextChats.length === SIDEBAR_CHATS_PAGE_SIZE;
} catch (error) {
@@ -420,16 +548,26 @@
return [];
}
);
chats = append ? [...(chats ?? []), ...(fallback ?? [])] : (fallback ?? []);
const fallbackChats = fallback ?? [];
const merged = append ? mergeFolderChats(chats ?? [], fallbackChats) : fallbackChats;
chats = mergeFolderChats(merged, pendingUpsertChats);
pendingUpsertChats = pendingUpsertChats.filter(
(pendingChat) => !fallbackChats.some((chat) => chat.id === pendingChat.id)
);
chatsPage = nextPage;
hasMoreChats = (fallback?.length ?? 0) === SIDEBAR_CHATS_PAGE_SIZE;
} finally {
chatsLoading = false;
if (queuedReload) {
queuedReload = false;
setFolderItems();
}
}
} else if (!open) {
chats = null;
chatsPage = 1;
hasMoreChats = false;
queuedReload = false;
}
};
@@ -710,6 +848,7 @@
createSubFolderParentId = folderId;
showCreateSubFolderModal = true;
}}
onMarkAllRead={markAllReadHandler}
>
<div
class="flex size-5 items-center justify-center self-center dark:hover:text-white transition m-0 touch-auto"
@@ -746,6 +885,7 @@
parentDragged={dragged}
{onItemMove}
{onDelete}
{onFolderUnreadCounts}
on:import={(e) => {
dispatch('import', e.detail);
}}
@@ -771,6 +911,7 @@
ownerUserId={folders[folderId]?.shared && chat.owner_name ? chat.user_id : null}
readonly={chat.user_id !== $user?.id}
{shiftKey}
onReadStateChange={applyReadState}
on:change={(e) => {
dispatch('change', e.detail);
}}