refac
This commit is contained in:
1 parent
2196b4e1ff
commit
6f93ecd4fd
16 files changed
+550
-62
No files matched your search
@@ -275,7 +275,8 @@ async def emit_chat_list_event(metadata: dict, chat_id: str):
|
|||||||
|
|
||||||
event_emitter = await get_event_emitter(metadata, update_db=False)
|
event_emitter = await get_event_emitter(metadata, update_db=False)
|
||||||
if event_emitter:
|
if event_emitter:
|
||||||
await event_emitter({'type': 'chat:list', 'data': {'chat_id': chat_id}})
|
folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(chat_id, metadata.get('user_id'))
|
||||||
|
await event_emitter({'type': 'chat:list', 'data': {'chat_id': chat_id, 'folder_id': folder_id}})
|
||||||
|
|
||||||
|
|
||||||
class SPAStaticFiles(StaticFiles):
|
class SPAStaticFiles(StaticFiles):
|
||||||
@@ -1642,7 +1643,17 @@ async def chat_completion(
|
|||||||
event_emitter = await get_event_emitter(metadata, update_db=False)
|
event_emitter = await get_event_emitter(metadata, update_db=False)
|
||||||
if event_emitter:
|
if event_emitter:
|
||||||
try:
|
try:
|
||||||
await asyncio.shield(event_emitter({'type': 'chat:active', 'data': {'active': False}}))
|
folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(
|
||||||
|
chat_id, user.id
|
||||||
|
)
|
||||||
|
await asyncio.shield(
|
||||||
|
event_emitter(
|
||||||
|
{
|
||||||
|
'type': 'chat:active',
|
||||||
|
'data': {'active': False, 'folder_id': folder_id},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1748,7 +1759,8 @@ async def chat_completion(
|
|||||||
update_db=False,
|
update_db=False,
|
||||||
)
|
)
|
||||||
if event_emitter:
|
if event_emitter:
|
||||||
await event_emitter({'type': 'chat:active', 'data': {'active': True}})
|
folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(chat_id, user.id)
|
||||||
|
await event_emitter({'type': 'chat:active', 'data': {'active': True, 'folder_id': folder_id}})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': True,
|
'status': True,
|
||||||
|
|||||||
@@ -36,13 +36,37 @@ from sqlalchemy import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.sql import exists
|
from sqlalchemy.sql import case, exists
|
||||||
from sqlalchemy.sql.expression import bindparam
|
from sqlalchemy.sql.expression import bindparam
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
ACTIVE_CHAT_GAP_SECONDS = 30 * 60
|
ACTIVE_CHAT_GAP_SECONDS = 30 * 60
|
||||||
|
|
||||||
|
|
||||||
|
def chat_list_order(sort_by: str = 'updated_at', sort_dir: str = 'desc', user_id: str | None = None):
|
||||||
|
if sort_by != 'unread_updated_at':
|
||||||
|
sort_column = Chat.title if sort_by == 'title' else Chat.updated_at
|
||||||
|
order_clause = sort_column.asc() if sort_dir == 'asc' else sort_column.desc()
|
||||||
|
return order_clause, Chat.id
|
||||||
|
|
||||||
|
unfinished_assistant = (
|
||||||
|
select(ChatMessage.id)
|
||||||
|
.where(ChatMessage.chat_id == Chat.id)
|
||||||
|
.where(ChatMessage.role == 'assistant')
|
||||||
|
.where(ChatMessage.done.is_(False))
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
conditions = [Chat.updated_at > func.coalesce(Chat.last_read_at, 0), ~unfinished_assistant]
|
||||||
|
if user_id is not None:
|
||||||
|
conditions.append(Chat.user_id == user_id)
|
||||||
|
|
||||||
|
unread = case(
|
||||||
|
(and_(*conditions), 1),
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
return unread.desc(), Chat.updated_at.desc(), Chat.id
|
||||||
|
|
||||||
|
|
||||||
class Chat(Base): # database table mapping for chat entity
|
class Chat(Base): # database table mapping for chat entity
|
||||||
__tablename__ = 'chat'
|
__tablename__ = 'chat'
|
||||||
|
|
||||||
@@ -614,19 +638,62 @@ class ChatTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> int | None:
|
async def update_chat_last_read_at_by_id(
|
||||||
|
self, id: str, user_id: str, db: AsyncSession | None = None
|
||||||
|
) -> tuple[int, bool] | None:
|
||||||
try:
|
try:
|
||||||
async with get_async_db_context(db) as session:
|
async with get_async_db_context(db) as session:
|
||||||
chat = await session.get(Chat, id)
|
chat = await session.get(Chat, id)
|
||||||
if chat and chat.user_id == user_id:
|
if chat and chat.user_id == user_id:
|
||||||
last_read_at = int(time.time())
|
last_read_at = int(time.time())
|
||||||
|
was_unread = chat.last_read_at is None or chat.updated_at > chat.last_read_at
|
||||||
chat.last_read_at = last_read_at
|
chat.last_read_at = last_read_at
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return last_read_at
|
return last_read_at, was_unread
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def mark_chat_unread_by_id(
|
||||||
|
self, id: str, user_id: str, db: AsyncSession | None = None
|
||||||
|
) -> ChatTitleIdResponse | 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 = 0
|
||||||
|
await session.commit()
|
||||||
|
return ChatTitleIdResponse(
|
||||||
|
id=chat.id,
|
||||||
|
title=chat.title,
|
||||||
|
updated_at=chat.updated_at,
|
||||||
|
created_at=chat.created_at,
|
||||||
|
last_read_at=chat.last_read_at,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def mark_chats_read_by_folder_ids(
|
||||||
|
self, user_id: str, folder_ids: list[str], db: AsyncSession | None = None
|
||||||
|
) -> int:
|
||||||
|
if not folder_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async with get_async_db_context(db) as session:
|
||||||
|
result = await session.execute(
|
||||||
|
update(Chat)
|
||||||
|
.where(
|
||||||
|
Chat.user_id == user_id,
|
||||||
|
Chat.folder_id.in_(folder_ids),
|
||||||
|
Chat.archived == False,
|
||||||
|
Chat.meta['internal'].as_boolean().is_not(True),
|
||||||
|
)
|
||||||
|
.values(last_read_at=Chat.updated_at)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount or 0
|
||||||
|
|
||||||
async def update_chat_title_by_id(self, id: str, title: str) -> ChatModel | None:
|
async def update_chat_title_by_id(self, id: str, title: str) -> ChatModel | None:
|
||||||
try:
|
try:
|
||||||
async with get_async_db_context() as session:
|
async with get_async_db_context() as session:
|
||||||
@@ -1212,6 +1279,8 @@ class ChatTable:
|
|||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
include_folders: bool = False,
|
include_folders: bool = False,
|
||||||
include_pinned: bool = False,
|
include_pinned: bool = False,
|
||||||
|
sort_by: str = 'updated_at',
|
||||||
|
sort_dir: str = 'desc',
|
||||||
skip: int | None = None,
|
skip: int | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
db: AsyncSession | None = None,
|
db: AsyncSession | None = None,
|
||||||
@@ -1231,7 +1300,7 @@ class ChatTable:
|
|||||||
if not include_archived:
|
if not include_archived:
|
||||||
stmt = stmt.filter_by(archived=False)
|
stmt = stmt.filter_by(archived=False)
|
||||||
|
|
||||||
stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id)
|
stmt = stmt.order_by(*chat_list_order(sort_by, sort_dir))
|
||||||
|
|
||||||
if skip:
|
if skip:
|
||||||
stmt = stmt.offset(skip)
|
stmt = stmt.offset(skip)
|
||||||
@@ -1802,6 +1871,8 @@ class ChatTable:
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 60,
|
limit: int = 60,
|
||||||
|
sort_by: str = 'updated_at',
|
||||||
|
sort_dir: str = 'desc',
|
||||||
db: AsyncSession | None = None,
|
db: AsyncSession | None = None,
|
||||||
) -> list[ChatTitleIdResponse]:
|
) -> list[ChatTitleIdResponse]:
|
||||||
async with get_async_db_context(db) as session:
|
async with get_async_db_context(db) as session:
|
||||||
@@ -1811,8 +1882,8 @@ class ChatTable:
|
|||||||
.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||||
.filter_by(archived=False)
|
.filter_by(archived=False)
|
||||||
.where(Chat.meta['internal'].as_boolean().is_not(True))
|
.where(Chat.meta['internal'].as_boolean().is_not(True))
|
||||||
.order_by(Chat.updated_at.desc(), Chat.id)
|
|
||||||
)
|
)
|
||||||
|
stmt = stmt.order_by(*chat_list_order(sort_by, sort_dir))
|
||||||
|
|
||||||
if skip:
|
if skip:
|
||||||
stmt = stmt.offset(skip)
|
stmt = stmt.offset(skip)
|
||||||
@@ -1841,20 +1912,19 @@ class ChatTable:
|
|||||||
limit: int = 60,
|
limit: int = 60,
|
||||||
sort_by: str = 'updated_at',
|
sort_by: str = 'updated_at',
|
||||||
sort_dir: str = 'desc',
|
sort_dir: str = 'desc',
|
||||||
|
unread_for_user_id: str | None = None,
|
||||||
db: AsyncSession | None = None,
|
db: AsyncSession | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Get chats in a folder across ALL users. Returns dicts with user_id."""
|
"""Get chats in a folder across ALL users. Returns dicts with user_id."""
|
||||||
async with get_async_db_context(db) as session:
|
async with get_async_db_context(db) as session:
|
||||||
sort_column = Chat.title if sort_by == 'title' else Chat.updated_at
|
|
||||||
order_clause = sort_column.asc() if sort_dir == 'asc' else sort_column.desc()
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Chat.id, Chat.title, Chat.user_id, Chat.updated_at, Chat.created_at, Chat.last_read_at)
|
select(Chat.id, Chat.title, Chat.user_id, Chat.updated_at, Chat.created_at, Chat.last_read_at)
|
||||||
.filter_by(folder_id=folder_id)
|
.filter_by(folder_id=folder_id)
|
||||||
.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
.filter(or_(Chat.pinned == False, Chat.pinned == None))
|
||||||
.filter_by(archived=False)
|
.filter_by(archived=False)
|
||||||
.where(Chat.meta['internal'].as_boolean().is_not(True))
|
.where(Chat.meta['internal'].as_boolean().is_not(True))
|
||||||
.order_by(order_clause, Chat.id)
|
|
||||||
)
|
)
|
||||||
|
stmt = stmt.order_by(*chat_list_order(sort_by, sort_dir, unread_for_user_id))
|
||||||
|
|
||||||
if skip:
|
if skip:
|
||||||
stmt = stmt.offset(skip)
|
stmt = stmt.offset(skip)
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ async def add_active_state_to_chat_list(
|
|||||||
return chat_list
|
return chat_list
|
||||||
|
|
||||||
|
|
||||||
|
async def get_folder_unread_counts(user_id: str, db: AsyncSession | None = None) -> dict[str, int]:
|
||||||
|
user_folders = await Folders.get_folders_by_user_id(user_id, db=db)
|
||||||
|
parent_by_id = {folder.id: folder.parent_id for folder in user_folders}
|
||||||
|
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()), db=db)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class ChatConfigForm(BaseModel):
|
class ChatConfigForm(BaseModel):
|
||||||
ENABLE_CONTEXT_COMPACTION: bool
|
ENABLE_CONTEXT_COMPACTION: bool
|
||||||
CONTEXT_COMPACTION_TOKEN_THRESHOLD: int
|
CONTEXT_COMPACTION_TOKEN_THRESHOLD: int
|
||||||
@@ -203,6 +221,8 @@ async def get_session_user_chat_list(
|
|||||||
page: int | None = None,
|
page: int | None = None,
|
||||||
include_pinned: bool | None = False,
|
include_pinned: bool | None = False,
|
||||||
include_folders: bool | None = False,
|
include_folders: bool | None = False,
|
||||||
|
sort_by: str = 'updated_at',
|
||||||
|
sort_dir: str = 'desc',
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
@@ -214,6 +234,8 @@ async def get_session_user_chat_list(
|
|||||||
user.id,
|
user.id,
|
||||||
include_folders=include_folders,
|
include_folders=include_folders,
|
||||||
include_pinned=include_pinned,
|
include_pinned=include_pinned,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_dir=sort_dir,
|
||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
db=db,
|
db=db,
|
||||||
@@ -223,6 +245,8 @@ async def get_session_user_chat_list(
|
|||||||
user.id,
|
user.id,
|
||||||
include_folders=include_folders,
|
include_folders=include_folders,
|
||||||
include_pinned=include_pinned,
|
include_pinned=include_pinned,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_dir=sort_dir,
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
return await add_active_state_to_chat_list(request, chats)
|
return await add_active_state_to_chat_list(request, chats)
|
||||||
@@ -869,6 +893,8 @@ async def get_chat_list_by_folder_id(
|
|||||||
request: Request,
|
request: Request,
|
||||||
folder_id: str,
|
folder_id: str,
|
||||||
page: int | None = 1,
|
page: int | None = 1,
|
||||||
|
sort_by: str = 'unread_updated_at',
|
||||||
|
sort_dir: str = 'desc',
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
@@ -876,7 +902,15 @@ async def get_chat_list_by_folder_id(
|
|||||||
limit = 10
|
limit = 10
|
||||||
skip = (page - 1) * limit
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
chats = await Chats.get_chats_by_folder_id_and_user_id(folder_id, user.id, skip=skip, limit=limit, db=db)
|
chats = await Chats.get_chats_by_folder_id_and_user_id(
|
||||||
|
folder_id,
|
||||||
|
user.id,
|
||||||
|
skip=skip,
|
||||||
|
limit=limit,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_dir=sort_dir,
|
||||||
|
db=db,
|
||||||
|
)
|
||||||
return await add_active_state_to_chat_list(request, chats)
|
return await add_active_state_to_chat_list(request, chats)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2033,6 +2067,28 @@ class ChatFolderIdForm(BaseModel):
|
|||||||
folder_id: str | None = None
|
folder_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/{id}/unread')
|
||||||
|
async def mark_chat_unread_by_id(
|
||||||
|
id: str,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
db: AsyncSession = Depends(get_async_session),
|
||||||
|
):
|
||||||
|
chat = await Chats.mark_chat_unread_by_id(id, user.id, db=db)
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
folder_id = await Chats.get_chat_folder_id(id, user.id, db=db)
|
||||||
|
return {
|
||||||
|
'chat_id': id,
|
||||||
|
'last_read_at': chat.last_read_at,
|
||||||
|
'folder_id': folder_id,
|
||||||
|
'folder_unread_counts': await get_folder_unread_counts(user.id, db=db),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post('/{id}/folder', response_model=ChatResponse | None)
|
@router.post('/{id}/folder', response_model=ChatResponse | None)
|
||||||
async def update_chat_folder_id_by_id(
|
async def update_chat_folder_id_by_id(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ router = APIRouter()
|
|||||||
from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access
|
from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access
|
||||||
|
|
||||||
|
|
||||||
|
async def get_folder_unread_counts(user_id: str, db: AsyncSession | None = None) -> dict[str, int]:
|
||||||
|
folders = await Folders.get_folders_by_user_id(user_id, db=db)
|
||||||
|
parent_by_id = {folder.id: folder.parent_id for folder in folders}
|
||||||
|
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()), db=db)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
async def check_folders_permission(request: Request, user, db=None):
|
async def check_folders_permission(request: Request, user, db=None):
|
||||||
"""Verify the folders feature is enabled and the user has permission."""
|
"""Verify the folders feature is enabled and the user has permission."""
|
||||||
config = await Config.get_many('folders.enable', 'user.permissions')
|
config = await Config.get_many('folders.enable', 'user.permissions')
|
||||||
@@ -97,19 +115,7 @@ async def get_folders(
|
|||||||
|
|
||||||
folder_list.append(folder)
|
folder_list.append(folder)
|
||||||
|
|
||||||
direct_unread_counts = await Chats.count_unread_by_folder_ids(
|
unread_counts = await get_folder_unread_counts(user.id, db=db)
|
||||||
user.id, [folder.id for folder in folder_list], db=db
|
|
||||||
)
|
|
||||||
parent_by_id = {folder.id: folder.parent_id for folder in folder_list}
|
|
||||||
unread_counts = dict.fromkeys(parent_by_id.keys(), 0)
|
|
||||||
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 [
|
return [
|
||||||
FolderNameIdResponse(**folder.model_dump(), unread_count=unread_counts.get(folder.id, 0))
|
FolderNameIdResponse(**folder.model_dump(), unread_count=unread_counts.get(folder.id, 0))
|
||||||
@@ -504,7 +510,7 @@ async def get_shared_folder_chats(
|
|||||||
request: Request,
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
page: int | None = Query(None, ge=1),
|
page: int | None = Query(None, ge=1),
|
||||||
sort_by: str = Query('updated_at'),
|
sort_by: str = Query('unread_updated_at'),
|
||||||
sort_dir: str = Query('desc'),
|
sort_dir: str = Query('desc'),
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
@@ -537,6 +543,7 @@ async def get_shared_folder_chats(
|
|||||||
limit=limit if page is not None else 60,
|
limit=limit if page is not None else 60,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
sort_dir=sort_dir,
|
sort_dir=sort_dir,
|
||||||
|
unread_for_user_id=user.id,
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
total = await Chats.count_all_chats_by_folder_id(id, db=db) if page is not None else len(chats)
|
total = await Chats.count_all_chats_by_folder_id(id, db=db) if page is not None else len(chats)
|
||||||
@@ -564,6 +571,44 @@ async def get_shared_folder_chats(
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/{id}/read')
|
||||||
|
async def mark_folder_chats_read_by_id(
|
||||||
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
db: AsyncSession = Depends(get_async_session),
|
||||||
|
):
|
||||||
|
await check_folders_permission(request, user, db=db)
|
||||||
|
folder = await Folders.get_folder_by_id(id, db=db)
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_owner = user.id == folder.user_id
|
||||||
|
is_admin = user.role == 'admin'
|
||||||
|
if not (is_owner or is_admin or await _has_folder_access(user.id, folder, 'read', db)):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
folder_ids = (
|
||||||
|
await Folders.get_folder_ids_by_id_and_user_id_in_subtree(id, folder.user_id, db=db)
|
||||||
|
if is_owner or is_admin
|
||||||
|
else [id]
|
||||||
|
)
|
||||||
|
updated_count = await Chats.mark_chats_read_by_folder_ids(user.id, folder_ids, db=db)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'folder_id': id,
|
||||||
|
'folder_ids': folder_ids,
|
||||||
|
'updated_count': updated_count,
|
||||||
|
'folder_unread_counts': await get_folder_unread_counts(user.id, db=db),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Delete Folder By Id
|
# Delete Folder By Id
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -545,20 +545,24 @@ async def chat_events(sid, data):
|
|||||||
event_type = event_data.get('type')
|
event_type = event_data.get('type')
|
||||||
|
|
||||||
if event_type == 'last_read_at':
|
if event_type == 'last_read_at':
|
||||||
last_read_at = await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id'])
|
read_update = await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id'])
|
||||||
if not last_read_at:
|
if not read_update:
|
||||||
return
|
return
|
||||||
|
last_read_at, was_unread = read_update
|
||||||
|
response_data = {
|
||||||
|
'chat_id': data['chat_id'],
|
||||||
|
'last_read_at': last_read_at,
|
||||||
|
}
|
||||||
|
if was_unread:
|
||||||
|
response_data['folder_unread_counts'] = await get_folder_unread_counts(user['id'])
|
||||||
|
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
'events',
|
'events',
|
||||||
{
|
{
|
||||||
'chat_id': data['chat_id'],
|
'chat_id': data['chat_id'],
|
||||||
'data': {
|
'data': {
|
||||||
'type': 'chat:list',
|
'type': 'chat:list',
|
||||||
'data': {
|
'data': response_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"]}',
|
room=f'user:{user["id"]}',
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ async def publish_chat_finished_event(
|
|||||||
)
|
)
|
||||||
event_emitter = await get_event_emitter(metadata, update_db=False)
|
event_emitter = await get_event_emitter(metadata, update_db=False)
|
||||||
if event_emitter:
|
if event_emitter:
|
||||||
await event_emitter({'type': 'chat:list', 'data': {'chat_id': chat_id}})
|
folder_id = metadata.get('folder_id') or await Chats.get_chat_folder_id(chat_id, metadata.get('user_id'))
|
||||||
|
await event_emitter({'type': 'chat:list', 'data': {'chat_id': chat_id, 'folder_id': folder_id}})
|
||||||
|
|
||||||
|
|
||||||
# We believe in one maker of all models, seen and unseen,
|
# We believe in one maker of all models, seen and unseen,
|
||||||
|
|||||||
@@ -885,6 +885,34 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const markChatUnreadById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/unread`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = 'detail' in err ? err.detail : err;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const cloneChatById = async (token: string, id: string, title?: string) => {
|
export const cloneChatById = async (token: string, id: string, title?: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,34 @@ export const deleteFolderById = async (token: string, id: string, deleteContents
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const markFolderChatsReadById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/read`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateFolderAccessById = async (token: string, id: string, accessGrants: any[]) => {
|
export const updateFolderAccessById = async (token: string, id: string, accessGrants: any[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -3470,6 +3470,7 @@
|
|||||||
|
|
||||||
const initChatHandler = async (history) => {
|
const initChatHandler = async (history) => {
|
||||||
let _chatId = $chatId;
|
let _chatId = $chatId;
|
||||||
|
const selectedFolderId = $selectedFolder?.id;
|
||||||
|
|
||||||
if (!$temporaryChatEnabled) {
|
if (!$temporaryChatEnabled) {
|
||||||
chat = await createNewChat(
|
chat = await createNewChat(
|
||||||
@@ -3502,6 +3503,10 @@
|
|||||||
await refreshChatList(localStorage.token);
|
await refreshChatList(localStorage.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedFolderId) {
|
||||||
|
await refreshFolderChatLists(selectedFolderId, chat);
|
||||||
|
}
|
||||||
|
|
||||||
selectedFolder.set(null);
|
selectedFolder.set(null);
|
||||||
} else {
|
} else {
|
||||||
_chatId = createTemporaryChatId($socket?.id);
|
_chatId = createTemporaryChatId($socket?.id);
|
||||||
|
|||||||
@@ -123,7 +123,15 @@
|
|||||||
let showSharedFolders = false;
|
let showSharedFolders = false;
|
||||||
|
|
||||||
let folders = {};
|
let folders = {};
|
||||||
let folderRegistry: Record<string, { setFolderItems?: () => unknown }> = {};
|
let folderRegistry: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
setFolderItems?: () => unknown;
|
||||||
|
upsertChat?: (chat: Record<string, unknown>) => unknown;
|
||||||
|
setChatActive?: (chatId: string, active: boolean) => boolean;
|
||||||
|
setChatReadAt?: (chatId: string, lastReadAt: number) => boolean;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
let newFolderId = null;
|
let newFolderId = null;
|
||||||
|
|
||||||
@@ -403,6 +411,35 @@
|
|||||||
chatListLoading = false;
|
chatListLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyFolderUnreadCounts = (folderUnreadCounts: Record<string, number>) => {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyChatReadState = (data) => {
|
||||||
|
if (data?.folder_unread_counts) {
|
||||||
|
applyFolderUnreadCounts(data.folder_unread_counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.chat_id && typeof data?.last_read_at === 'number') {
|
||||||
|
setChatReadAt(data.chat_id, data.last_read_at);
|
||||||
|
for (const folder of Object.values(folderRegistry)) {
|
||||||
|
folder?.setChatReadAt?.(data.chat_id, data.last_read_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const importChatHandler = async (items, pinned = false, folderId = null) => {
|
const importChatHandler = async (items, pinned = false, folderId = null) => {
|
||||||
if (!canImportChats) {
|
if (!canImportChats) {
|
||||||
toast.error($i18n.t('Access prohibited'));
|
toast.error($i18n.t('Access prohibited'));
|
||||||
@@ -656,9 +693,17 @@
|
|||||||
socketInstance?.on('events', chatActiveEventHandler);
|
socketInstance?.on('events', chatActiveEventHandler);
|
||||||
socketInstance?.on('connect', refreshChatRows);
|
socketInstance?.on('connect', refreshChatRows);
|
||||||
|
|
||||||
const unregisterFolderRefreshHandler = registerFolderRefreshHandler(() =>
|
const unregisterFolderRefreshHandler = registerFolderRefreshHandler((folderId, chat) => {
|
||||||
Promise.all(Object.values(folderRegistry).map((folder) => folder?.setFolderItems?.()))
|
if (folderId) {
|
||||||
);
|
if (chat) {
|
||||||
|
return folderRegistry[folderId]?.upsertChat?.(chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderRegistry[folderId]?.setFolderItems?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(Object.values(folderRegistry).map((folder) => folder?.setFolderItems?.()));
|
||||||
|
});
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
initPinnedMenuSortable();
|
initPinnedMenuSortable();
|
||||||
@@ -696,14 +741,23 @@
|
|||||||
type: string;
|
type: string;
|
||||||
data: {
|
data: {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
folder_id?: string | null;
|
||||||
last_read_at?: number;
|
last_read_at?: number;
|
||||||
folder_unread_counts?: Record<string, number>;
|
folder_unread_counts?: Record<string, number>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
if (event.data?.type === 'chat:active') {
|
if (event.data?.type === 'chat:active') {
|
||||||
const active = event.data.data.active ?? false;
|
const eventData = event.data.data ?? {};
|
||||||
|
const active = eventData.active ?? false;
|
||||||
const found = setChatActive(event.chat_id, active);
|
const found = setChatActive(event.chat_id, active);
|
||||||
|
let foundInFolder = false;
|
||||||
|
for (const folder of Object.values(folderRegistry)) {
|
||||||
|
foundInFolder = folder?.setChatActive?.(event.chat_id, active) || foundInFolder;
|
||||||
|
}
|
||||||
|
if (!foundInFolder && active && eventData.folder_id) {
|
||||||
|
await folderRegistry[eventData.folder_id]?.setFolderItems?.();
|
||||||
|
}
|
||||||
if (!found && active) {
|
if (!found && active) {
|
||||||
await refreshChatRows();
|
await refreshChatRows();
|
||||||
}
|
}
|
||||||
@@ -711,27 +765,21 @@
|
|||||||
const eventData = event.data.data ?? {};
|
const eventData = event.data.data ?? {};
|
||||||
const folderUnreadCounts = eventData.folder_unread_counts;
|
const folderUnreadCounts = eventData.folder_unread_counts;
|
||||||
if (folderUnreadCounts) {
|
if (folderUnreadCounts) {
|
||||||
folders = Object.fromEntries(
|
applyFolderUnreadCounts(folderUnreadCounts);
|
||||||
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') {
|
if (typeof eventData.last_read_at === 'number') {
|
||||||
setChatReadAt(event.chat_id, eventData.last_read_at);
|
setChatReadAt(event.chat_id, eventData.last_read_at);
|
||||||
|
for (const folder of Object.values(folderRegistry)) {
|
||||||
|
folder?.setChatReadAt?.(event.chat_id, eventData.last_read_at);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshChatRows();
|
await refreshChatRows();
|
||||||
|
if (eventData.folder_id) {
|
||||||
|
await folderRegistry[eventData.folder_id]?.setFolderItems?.();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1295,6 +1343,7 @@
|
|||||||
bind:folderRegistry
|
bind:folderRegistry
|
||||||
{folders}
|
{folders}
|
||||||
{shiftKey}
|
{shiftKey}
|
||||||
|
onFolderUnreadCounts={applyFolderUnreadCounts}
|
||||||
onDelete={(folderId) => {
|
onDelete={(folderId) => {
|
||||||
selectedFolder.set(null);
|
selectedFolder.set(null);
|
||||||
initChatList();
|
initChatList();
|
||||||
@@ -1465,6 +1514,7 @@
|
|||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
initChatList();
|
initChatList();
|
||||||
}}
|
}}
|
||||||
|
onReadStateChange={applyChatReadState}
|
||||||
on:tag={(e) => {
|
on:tag={(e) => {
|
||||||
const { type, name } = e.detail;
|
const { type, name } = e.detail;
|
||||||
tagEventHandler(type, name, chat.id);
|
tagEventHandler(type, name, chat.id);
|
||||||
@@ -1529,6 +1579,7 @@
|
|||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
initChatList();
|
initChatList();
|
||||||
}}
|
}}
|
||||||
|
onReadStateChange={applyChatReadState}
|
||||||
on:tag={(e) => {
|
on:tag={(e) => {
|
||||||
const { type, name } = e.detail;
|
const { type, name } = e.detail;
|
||||||
tagEventHandler(type, name, chat.id);
|
tagEventHandler(type, name, chat.id);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
getAllTags,
|
getAllTags,
|
||||||
getChatById,
|
getChatById,
|
||||||
getChatListByTagName,
|
getChatListByTagName,
|
||||||
|
markChatUnreadById,
|
||||||
updateChatById,
|
updateChatById,
|
||||||
updateChatFolderIdById
|
updateChatFolderIdById
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
|
|
||||||
export let ownerName: string | null = null;
|
export let ownerName: string | null = null;
|
||||||
export let ownerUserId: string | null = null;
|
export let ownerUserId: string | null = null;
|
||||||
|
export let onReadStateChange = (data) => {};
|
||||||
|
|
||||||
export let onDragEnd = () => {};
|
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 showShareChatModal = false;
|
||||||
let confirmEdit = false;
|
let confirmEdit = false;
|
||||||
|
|
||||||
@@ -693,6 +707,7 @@
|
|||||||
deleteHandler={() => {
|
deleteHandler={() => {
|
||||||
showDeleteConfirm = true;
|
showDeleteConfirm = true;
|
||||||
}}
|
}}
|
||||||
|
{markUnreadHandler}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
dispatch('unselect');
|
dispatch('unselect');
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
import PinSlashIcon from './icons/PinSlash.svelte';
|
import PinSlashIcon from './icons/PinSlash.svelte';
|
||||||
import ShareIcon from './icons/Share.svelte';
|
import ShareIcon from './icons/Share.svelte';
|
||||||
import TrashIcon from './icons/Trash.svelte';
|
import TrashIcon from './icons/Trash.svelte';
|
||||||
|
import ChatCheckIcon from '$lib/components/icons/ChatCheck.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
export let renameHandler: Function;
|
export let renameHandler: Function;
|
||||||
export let deleteHandler: Function;
|
export let deleteHandler: Function;
|
||||||
export let onClose: Function;
|
export let onClose: Function;
|
||||||
|
export let markUnreadHandler: Function = () => {};
|
||||||
|
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
|
|
||||||
@@ -369,6 +371,18 @@
|
|||||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||||
</button>
|
</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" />
|
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
export let shiftKey = false;
|
export let shiftKey = false;
|
||||||
|
|
||||||
export let onDelete = (folderId) => {};
|
export let onDelete = (folderId) => {};
|
||||||
|
export let onFolderUnreadCounts = (counts) => {};
|
||||||
|
|
||||||
let ownedList = [];
|
let ownedList = [];
|
||||||
let sharedList = [];
|
let sharedList = [];
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
{shiftKey}
|
{shiftKey}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
{onItemMove}
|
{onItemMove}
|
||||||
|
{onFolderUnreadCounts}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
dispatch('import', e.detail);
|
dispatch('import', e.detail);
|
||||||
}}
|
}}
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
{shiftKey}
|
{shiftKey}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
{onItemMove}
|
{onItemMove}
|
||||||
|
{onFolderUnreadCounts}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
dispatch('import', e.detail);
|
dispatch('import', e.detail);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import TrashIcon from '../icons/Trash.svelte';
|
import TrashIcon from '../icons/Trash.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Download from '../icons/Download.svelte';
|
import Download from '../icons/Download.svelte';
|
||||||
|
import CheckIcon from '$lib/components/icons/Check.svelte';
|
||||||
|
|
||||||
export let align: 'start' | 'end' = 'start';
|
export let align: 'start' | 'end' = 'start';
|
||||||
export let onEdit = () => {};
|
export let onEdit = () => {};
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
export let onShare = () => {};
|
export let onShare = () => {};
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
export let onCreateSubFolder = () => {};
|
export let onCreateSubFolder = () => {};
|
||||||
|
export let onMarkAllRead = () => {};
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
</script>
|
</script>
|
||||||
@@ -57,6 +59,19 @@
|
|||||||
|
|
||||||
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
|
<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
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
updateFolderParentIdById,
|
updateFolderParentIdById,
|
||||||
getFolderById,
|
getFolderById,
|
||||||
createNewFolder,
|
createNewFolder,
|
||||||
getSharedFolderChats
|
getSharedFolderChats,
|
||||||
|
markFolderChatsReadById
|
||||||
} from '$lib/apis/folders';
|
} from '$lib/apis/folders';
|
||||||
import {
|
import {
|
||||||
getChatById,
|
getChatById,
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
|
|
||||||
export let onDelete = (e) => {};
|
export let onDelete = (e) => {};
|
||||||
export let onItemMove = (e) => {};
|
export let onItemMove = (e) => {};
|
||||||
|
export let onFolderUnreadCounts = (counts) => {};
|
||||||
|
|
||||||
let folderElement;
|
let folderElement;
|
||||||
|
|
||||||
@@ -83,6 +85,79 @@
|
|||||||
compactDisplay: 'short'
|
compactDisplay: 'short'
|
||||||
}).format(count);
|
}).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) => {
|
const onDragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -275,8 +350,48 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
open = folders[folderId].is_expanded;
|
open = folders[folderId].is_expanded;
|
||||||
folderRegistry[folderId] = {
|
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) {
|
if (folderElement) {
|
||||||
@@ -396,10 +511,19 @@
|
|||||||
let chatsPage = 1;
|
let chatsPage = 1;
|
||||||
let hasMoreChats = false;
|
let hasMoreChats = false;
|
||||||
let chatsLoading = false;
|
let chatsLoading = false;
|
||||||
|
let queuedReload = false;
|
||||||
|
let pendingUpsertChats = [];
|
||||||
|
|
||||||
export const setFolderItems = async (append = false) => {
|
export const setFolderItems = async (append = false) => {
|
||||||
await tick();
|
await tick();
|
||||||
if (open && !chatsLoading) {
|
if (open && chatsLoading) {
|
||||||
|
if (!append) {
|
||||||
|
queuedReload = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
// Always use getSharedFolderChats so owners also see chats
|
// Always use getSharedFolderChats so owners also see chats
|
||||||
// created by users who have write access to this folder.
|
// created by users who have write access to this folder.
|
||||||
const nextPage = append ? chatsPage + 1 : 1;
|
const nextPage = append ? chatsPage + 1 : 1;
|
||||||
@@ -409,7 +533,11 @@
|
|||||||
page: nextPage
|
page: nextPage
|
||||||
});
|
});
|
||||||
const nextChats = res?.chats ?? [];
|
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;
|
chatsPage = nextPage;
|
||||||
hasMoreChats = res?.has_more ?? nextChats.length === SIDEBAR_CHATS_PAGE_SIZE;
|
hasMoreChats = res?.has_more ?? nextChats.length === SIDEBAR_CHATS_PAGE_SIZE;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -420,16 +548,26 @@
|
|||||||
return [];
|
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;
|
chatsPage = nextPage;
|
||||||
hasMoreChats = (fallback?.length ?? 0) === SIDEBAR_CHATS_PAGE_SIZE;
|
hasMoreChats = (fallback?.length ?? 0) === SIDEBAR_CHATS_PAGE_SIZE;
|
||||||
} finally {
|
} finally {
|
||||||
chatsLoading = false;
|
chatsLoading = false;
|
||||||
|
if (queuedReload) {
|
||||||
|
queuedReload = false;
|
||||||
|
setFolderItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (!open) {
|
} else if (!open) {
|
||||||
chats = null;
|
chats = null;
|
||||||
chatsPage = 1;
|
chatsPage = 1;
|
||||||
hasMoreChats = false;
|
hasMoreChats = false;
|
||||||
|
queuedReload = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -710,6 +848,7 @@
|
|||||||
createSubFolderParentId = folderId;
|
createSubFolderParentId = folderId;
|
||||||
showCreateSubFolderModal = true;
|
showCreateSubFolderModal = true;
|
||||||
}}
|
}}
|
||||||
|
onMarkAllRead={markAllReadHandler}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex size-5 items-center justify-center self-center dark:hover:text-white transition m-0 touch-auto"
|
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}
|
parentDragged={dragged}
|
||||||
{onItemMove}
|
{onItemMove}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
|
{onFolderUnreadCounts}
|
||||||
on:import={(e) => {
|
on:import={(e) => {
|
||||||
dispatch('import', e.detail);
|
dispatch('import', e.detail);
|
||||||
}}
|
}}
|
||||||
@@ -771,6 +911,7 @@
|
|||||||
ownerUserId={folders[folderId]?.shared && chat.owner_name ? chat.user_id : null}
|
ownerUserId={folders[folderId]?.shared && chat.owner_name ? chat.user_id : null}
|
||||||
readonly={chat.user_id !== $user?.id}
|
readonly={chat.user_id !== $user?.id}
|
||||||
{shiftKey}
|
{shiftKey}
|
||||||
|
onReadStateChange={applyReadState}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
dispatch('change', e.detail);
|
dispatch('change', e.detail);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const refreshChatList = async (
|
|||||||
// through a registry of per-folder callbacks that only the sidebar can reach.
|
// through a registry of per-folder callbacks that only the sidebar can reach.
|
||||||
// Handlers registered here let other components (e.g. the open chat's menu)
|
// Handlers registered here let other components (e.g. the open chat's menu)
|
||||||
// request that refresh without a reference to the sidebar.
|
// request that refresh without a reference to the sidebar.
|
||||||
type FolderRefreshHandler = () => unknown;
|
type FolderRefreshHandler = (folderId?: string | null, chat?: ChatListItem | null) => unknown;
|
||||||
const folderRefreshHandlers = new Set<FolderRefreshHandler>();
|
const folderRefreshHandlers = new Set<FolderRefreshHandler>();
|
||||||
|
|
||||||
export const registerFolderRefreshHandler = (handler: FolderRefreshHandler) => {
|
export const registerFolderRefreshHandler = (handler: FolderRefreshHandler) => {
|
||||||
@@ -75,8 +75,8 @@ export const registerFolderRefreshHandler = (handler: FolderRefreshHandler) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshFolderChatLists = async () => {
|
export const refreshFolderChatLists = async (folderId?: string | null, chat?: ChatListItem | null) => {
|
||||||
await Promise.all([...folderRefreshHandlers].map((handler) => handler()));
|
await Promise.all([...folderRefreshHandlers].map((handler) => handler(folderId, chat)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadNextChatListPage = async (token: string = ''): Promise<ChatListResult> => {
|
export const loadNextChatListPage = async (token: string = ''): Promise<ChatListResult> => {
|
||||||
|
|||||||
Reference in new issue
Block a user