enh/refac: channels message lazy load data

This commit is contained in:
Timothy Jaeryang Baek 2025-12-03 17:52:44 -05:00
parent 133618aaf0
commit 54b7ec56d6
4 changed files with 118 additions and 5 deletions

View file

@ -5,7 +5,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel from pydantic import BaseModel
from pydantic import field_validator
from open_webui.socket.main import ( from open_webui.socket.main import (
emit_to_users, emit_to_users,
@ -666,7 +666,16 @@ async def delete_channel_by_id(
class MessageUserResponse(MessageResponse): class MessageUserResponse(MessageResponse):
pass data: bool | None = None
@field_validator("data", mode="before")
def convert_data_to_bool(cls, v):
# No data or not a dict → False
if not isinstance(v, dict):
return False
# True if ANY value in the dict is non-empty
return any(bool(val) for val in v.values())
@router.get("/{id}/messages", response_model=list[MessageUserResponse]) @router.get("/{id}/messages", response_model=list[MessageUserResponse])
@ -1108,7 +1117,7 @@ async def post_new_message(
############################ ############################
@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageUserResponse]) @router.get("/{id}/messages/{message_id}", response_model=Optional[MessageResponse])
async def get_channel_message( async def get_channel_message(
id: str, message_id: str, user=Depends(get_verified_user) id: str, message_id: str, user=Depends(get_verified_user)
): ):
@ -1142,7 +1151,7 @@ async def get_channel_message(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
) )
return MessageUserResponse( return MessageResponse(
**{ **{
**message.model_dump(), **message.model_dump(),
"user": UserNameResponse( "user": UserNameResponse(
@ -1152,6 +1161,48 @@ async def get_channel_message(
) )
############################
# GetChannelMessageData
############################
@router.get("/{id}/messages/{message_id}/data", response_model=Optional[dict])
async def get_channel_message_data(
id: str, message_id: str, user=Depends(get_verified_user)
):
channel = Channels.get_channel_by_id(id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if channel.type in ["group", "dm"]:
if not Channels.is_user_channel_member(channel.id, user.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
else:
if user.role != "admin" and not has_access(
user.id, type="read", access_control=channel.access_control
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
)
message = Messages.get_message_by_id(message_id)
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
)
if message.channel_id != id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
return message.data
############################ ############################
# PinChannelMessage # PinChannelMessage
############################ ############################

View file

@ -491,6 +491,44 @@ export const getChannelThreadMessages = async (
return res; return res;
}; };
export const getMessageData = async (
token: string = '',
channel_id: string,
message_id: string
) => {
let error = null;
const res = await fetch(
`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/data`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
}
)
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
type MessageForm = { type MessageForm = {
temp_id?: string; temp_id?: string;
reply_to_id?: string; reply_to_id?: string;

View file

@ -126,6 +126,7 @@
{#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)} {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)}
<Message <Message
{message} {message}
{channel}
{thread} {thread}
replyToMessage={replyToMessage?.id === message.id} replyToMessage={replyToMessage?.id === message.id}
disabled={!channel?.write_access || message?.temp_id} disabled={!channel?.write_access || message?.temp_id}

View file

@ -17,6 +17,7 @@
import { settings, user, shortCodesToEmojis } from '$lib/stores'; import { settings, user, shortCodesToEmojis } from '$lib/stores';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { getMessageData } from '$lib/apis/channels';
import Markdown from '$lib/components/chat/Messages/Markdown.svelte'; import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte'; import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
@ -42,6 +43,8 @@
export let className = ''; export let className = '';
export let message; export let message;
export let channel;
export let showUserProfile = true; export let showUserProfile = true;
export let thread = false; export let thread = false;
@ -61,6 +64,21 @@
let edit = false; let edit = false;
let editedContent = null; let editedContent = null;
let showDeleteConfirmDialog = false; let showDeleteConfirmDialog = false;
const loadMessageData = async () => {
if (message && message?.data) {
const res = await getMessageData(localStorage.token, channel?.id, message.id);
if (res) {
message.data = res;
}
}
};
onMount(async () => {
if (message && message?.data) {
await loadMessageData();
}
});
</script> </script>
<ConfirmDialog <ConfirmDialog
@ -314,7 +332,12 @@
</Name> </Name>
{/if} {/if}
{#if (message?.data?.files ?? []).length > 0} {#if message?.data === true}
<!-- loading indicator -->
<div class=" my-2">
<Skeleton />
</div>
{:else if (message?.data?.files ?? []).length > 0}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap"> <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message?.data?.files as file} {#each message?.data?.files as file}
<div> <div>