From b7489bbc6c4e376c017edffd8da3c2eb4e6c1c8e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 26 Jul 2026 23:16:58 -0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 2 +- backend/open_webui/models/chats.py | 11 +++--- backend/open_webui/socket/main.py | 31 ++++++++++++++-- src/lib/components/layout/Sidebar.svelte | 35 +++++++++++++++++-- .../components/layout/Sidebar/Folders.svelte | 11 ------ .../layout/Sidebar/RecursiveFolder.svelte | 8 ++++- src/lib/stores/chatList.ts | 15 ++++++++ 7 files changed, 91 insertions(+), 22 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 6b9269e8fe..ca6272427f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1402,7 +1402,7 @@ async def chat_completion( updated['files'] = chat_files if selected_chat_models: updated['models'] = selected_chat_models - await Chats.update_chat_by_id(chat_id, updated) + await Chats.update_chat_by_id(chat_id, updated, touch=False) await Chats.update_chat_variables_by_id(chat_id, chat_variables) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index cd73c99142..65d80ba170 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -614,17 +614,18 @@ class ChatTable: except Exception: return None - async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: + async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> int | None: try: async with get_async_db_context(db) as session: chat = await session.get(Chat, id) if chat and chat.user_id == user_id: - chat.last_read_at = int(time.time()) + last_read_at = int(time.time()) + chat.last_read_at = last_read_at await session.commit() - return True - return False + return last_read_at + return None except Exception: - return False + return None async def update_chat_title_by_id(self, id: str, title: str) -> ChatModel | None: try: diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index b5a0b53e38..03c4ad15ff 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -31,6 +31,7 @@ from open_webui.env import ( from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import Channels from open_webui.models.chats import Chats +from open_webui.models.folders import Folders from open_webui.models.notes import Notes, NoteUpdateForm from open_webui.models.users import UserNameResponse, Users from open_webui.socket.utils import RedisDict, RedisLock, YdocManager @@ -508,6 +509,24 @@ async def channel_events(sid, data): await Channels.update_member_last_read_at(data['channel_id'], user['id']) +async def get_folder_unread_counts(user_id: str) -> dict[str, int]: + folder_list = await Folders.get_folders_by_user_id(user_id) + parent_by_id = {folder.id: folder.parent_id for folder in folder_list} + unread_counts = dict.fromkeys(parent_by_id.keys(), 0) + + direct_unread_counts = await Chats.count_unread_by_folder_ids(user_id, list(parent_by_id.keys())) + for unread_folder_id, unread_count in direct_unread_counts.items(): + current_id = unread_folder_id + seen = set() + while current_id and current_id not in seen: + seen.add(current_id) + if current_id in unread_counts: + unread_counts[current_id] += unread_count + current_id = parent_by_id.get(current_id) + + return unread_counts + + @sio.on('events:chat') async def chat_events(sid, data): try: @@ -526,13 +545,21 @@ async def chat_events(sid, data): event_type = event_data.get('type') if event_type == 'last_read_at': - if not await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id']): + last_read_at = await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id']) + if not last_read_at: return await sio.emit( 'events', { 'chat_id': data['chat_id'], - 'data': {'type': 'chat:list'}, + 'data': { + 'type': 'chat:list', + 'data': { + 'chat_id': data['chat_id'], + 'last_read_at': last_read_at, + 'folder_unread_counts': await get_folder_unread_counts(user['id']), + }, + }, }, room=f'user:{user["id"]}', ) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index edca875dc0..9702cb7e2b 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -31,7 +31,8 @@ loadNextChatListPage, refreshChatList, registerFolderRefreshHandler, - setChatActive + setChatActive, + setChatReadAt } from '$lib/stores/chatList'; import { onMount, getContext, tick, onDestroy } from 'svelte'; @@ -691,7 +692,14 @@ const chatActiveEventHandler = async (event: { chat_id: string; message_id: string; - data: { type: string; data: { active?: boolean } }; + data: { + type: string; + data: { + active?: boolean; + last_read_at?: number; + folder_unread_counts?: Record; + }; + }; }) => { if (event.data?.type === 'chat:active') { const active = event.data.data.active ?? false; @@ -700,6 +708,29 @@ await refreshChatRows(); } } else if (event.data?.type === 'chat:list') { + const eventData = event.data.data ?? {}; + const folderUnreadCounts = eventData.folder_unread_counts; + if (folderUnreadCounts) { + folders = Object.fromEntries( + Object.entries(folders).map(([id, folder]) => [ + id, + id in folderUnreadCounts ? { ...folder, unread_count: folderUnreadCounts[id] } : folder + ]) + ); + _folders.update((folderList) => + folderList.map((folder) => + folder.id in folderUnreadCounts + ? { ...folder, unread_count: folderUnreadCounts[folder.id] } + : folder + ) + ); + } + + if (typeof eventData.last_read_at === 'number') { + setChatReadAt(event.chat_id, eventData.last_read_at); + return; + } + refreshChatRows(); } }; diff --git a/src/lib/components/layout/Sidebar/Folders.svelte b/src/lib/components/layout/Sidebar/Folders.svelte index 2aedba22ef..516f3eaa12 100644 --- a/src/lib/components/layout/Sidebar/Folders.svelte +++ b/src/lib/components/layout/Sidebar/Folders.svelte @@ -4,7 +4,6 @@ const dispatch = createEventDispatcher(); import RecursiveFolder from './RecursiveFolder.svelte'; - import { chatId } from '$lib/stores'; export let folderRegistry = {}; @@ -42,16 +41,6 @@ folderRegistry[e.originFolderId]?.setFolderItems(); } }; - - const loadFolderItems = () => { - for (const folderId of Object.keys(folders)) { - folderRegistry[folderId]?.setFolderItems(); - } - }; - - $: if (folders || $chatId) { - loadFolderItems(); - } {#each ownedList as folderId (folderId)} diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index 0d0e90b24a..0e666dc15d 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -433,10 +433,16 @@ } }; - $: if (open) { + $: if (open && chats === null) { setFolderItems(); } + $: if (!open && chats !== null) { + chats = null; + chatsPage = 1; + hasMoreChats = false; + } + const renameHandler = async () => { console.log('Edit'); await tick(); diff --git a/src/lib/stores/chatList.ts b/src/lib/stores/chatList.ts index 7baddd2262..94ab76d5e5 100644 --- a/src/lib/stores/chatList.ts +++ b/src/lib/stores/chatList.ts @@ -123,6 +123,21 @@ export const setChatActive = (chatId: string, active: boolean): boolean => { return found; }; +export const setChatReadAt = (chatId: string, lastReadAt: number): boolean => { + let found = false; + const updateChat = (chat: ChatListItem) => { + if (chat.id !== chatId) { + return chat; + } + found = true; + return { ...chat, last_read_at: lastReadAt }; + }; + + chatsStore.update((items) => (items ? items.map(updateChat) : items)); + pinnedChatsStore.update((items) => items.map(updateChat)); + return found; +}; + export const resetChatListState = () => { requestGeneration += 1; currentPage = 1;