diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 3ba66f76d2..ba56b74ece 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -170,7 +170,13 @@ class UserGroupIdsListResponse(BaseModel): total: int -class UserInfoResponse(BaseModel): +class UserStatus(BaseModel): + status_emoji: Optional[str] = None + status_message: Optional[str] = None + status_expires_at: Optional[int] = None + + +class UserInfoResponse(UserStatus): id: str name: str email: str @@ -493,6 +499,21 @@ class UsersTable: except Exception: return None + def update_user_status_by_id( + self, id: str, form_data: UserStatus + ) -> Optional[UserModel]: + try: + with get_db() as db: + db.query(User).filter_by(id=id).update( + {**form_data.model_dump(exclude_none=True)} + ) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except Exception: + return None + def update_user_profile_image_url_by_id( self, id: str, profile_image_url: str ) -> Optional[UserModel]: diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 1b79d84cfd..42302043ed 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -17,7 +17,12 @@ from open_webui.models.auths import ( SignupForm, UpdatePasswordForm, ) -from open_webui.models.users import UserProfileImageResponse, Users, UpdateProfileForm +from open_webui.models.users import ( + UserProfileImageResponse, + Users, + UpdateProfileForm, + UserStatus, +) from open_webui.models.groups import Groups from open_webui.models.oauth_sessions import OAuthSessions @@ -82,7 +87,7 @@ class SessionUserResponse(Token, UserProfileImageResponse): permissions: Optional[dict] = None -class SessionUserInfoResponse(SessionUserResponse): +class SessionUserInfoResponse(SessionUserResponse, UserStatus): bio: Optional[str] = None gender: Optional[str] = None date_of_birth: Optional[datetime.date] = None @@ -139,6 +144,9 @@ async def get_session_user( "bio": user.bio, "gender": user.gender, "date_of_birth": user.date_of_birth, + "status_emoji": user.status_emoji, + "status_message": user.status_message, + "status_expires_at": user.status_expires_at, "permissions": user_permissions, } diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index c51916422f..3c1bbb72a8 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -21,6 +21,7 @@ from open_webui.models.users import ( UserInfoListResponse, UserInfoListResponse, UserRoleUpdateForm, + UserStatus, Users, UserSettings, UserUpdateForm, @@ -299,6 +300,43 @@ async def update_user_settings_by_session_user( ) +############################ +# GetUserStatusBySessionUser +############################ + + +@router.get("/user/status") +async def get_user_status_by_session_user(user=Depends(get_verified_user)): + user = Users.get_user_by_id(user.id) + if user: + return user + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# UpdateUserStatusBySessionUser +############################ + + +@router.post("/user/status/update") +async def update_user_status_by_session_user( + form_data: UserStatus, user=Depends(get_verified_user) +): + user = Users.get_user_by_id(user.id) + if user: + user = Users.update_user_status_by_id(user.id, form_data) + return user + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + ############################ # GetUserInfoBySessionUser ############################ @@ -350,9 +388,10 @@ async def update_user_info_by_session_user( ############################ -class UserActiveResponse(BaseModel): +class UserActiveResponse(UserStatus): name: str profile_image_url: Optional[str] = None + is_active: bool model_config = ConfigDict(extra="allow") @@ -377,8 +416,7 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): if user: return UserActiveResponse( **{ - "id": user.id, - "name": user.name, + **user.model_dump(), "is_active": Users.is_user_active(user_id), } ) diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index 89e2daa104..d6da54bbf9 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -327,6 +327,36 @@ export const getUserById = async (token: string, userId: string) => { return res; }; +export const updateUserStatus = async (token: string, formData: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/status/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...formData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getUserInfo = async (token: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, { diff --git a/src/lib/components/channel/Messages/Message/UserStatus.svelte b/src/lib/components/channel/Messages/Message/UserStatus.svelte index 07efa89c43..d04c9eb291 100644 --- a/src/lib/components/channel/Messages/Message/UserStatus.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatus.svelte @@ -11,6 +11,8 @@ import ChatBubble from '$lib/components/icons/ChatBubble.svelte'; import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte'; import { goto } from '$app/navigation'; + import Emoji from '$lib/components/common/Emoji.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; export let user = null; @@ -71,6 +73,25 @@ + {#if user?.status_emoji || user?.status_message} +
+ +
+ {#if user?.status_emoji} +
+ +
+ {/if} +
+ {user?.status_message} +
+
+
+
+ {/if} + {#if $_user?.id !== user.id}
diff --git a/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte index ab0c65fbe3..74b2029266 100644 --- a/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte @@ -26,7 +26,7 @@ {#if user} { if (e.detail === 'archived-chat') { @@ -1281,6 +1282,7 @@ {#if $user !== undefined && $user !== null} { if (e.detail === 'archived-chat') { diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index 2200da7a6f..41a14a1d37 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -476,6 +476,14 @@ on:mouseenter={() => { ignoreBlur = true; }} + on:click={(e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + + generateTitleHandler(); + ignoreBlur = false; + }} > diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte index ef6609c40b..1bfff6751e 100644 --- a/src/lib/components/layout/Sidebar/UserMenu.svelte +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -7,10 +7,12 @@ import { fade, slide } from 'svelte/transition'; import { getUsage } from '$lib/apis'; - import { userSignOut } from '$lib/apis/auths'; + import { getSessionUser, userSignOut } from '$lib/apis/auths'; import { showSettings, mobile, showSidebar, showShortcuts, user } from '$lib/stores'; + import { WEBUI_API_BASE_URL } from '$lib/constants'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte'; @@ -21,16 +23,27 @@ import Code from '$lib/components/icons/Code.svelte'; import UserGroup from '$lib/components/icons/UserGroup.svelte'; import SignOut from '$lib/components/icons/SignOut.svelte'; + import FaceSmile from '$lib/components/icons/FaceSmile.svelte'; + import UserStatusModal from './UserStatusModal.svelte'; + import Emoji from '$lib/components/common/Emoji.svelte'; + import XMark from '$lib/components/icons/XMark.svelte'; + import { updateUserStatus } from '$lib/apis/users'; + import { toast } from 'svelte-sonner'; const i18n = getContext('i18n'); export let show = false; export let role = ''; + + export let profile = false; export let help = false; + export let className = 'max-w-[240px]'; export let showActiveUsers = true; + let showUserStatusModal = false; + const dispatch = createEventDispatcher(); let usage = null; @@ -52,6 +65,12 @@ + { + user.set(await getSessionUser(localStorage.token)); + }} +/> fade(e, { duration: 100 })} > + {#if profile} +
+
+ profile +
+ +
+
+ {$user.name} +
+ +
+ {#if $user?.is_active ?? true} +
+ + + + +
+ + {$i18n.t('Active')} + {:else} +
+ + + +
+ + {$i18n.t('Away')} + {/if} +
+
+
+ + {#if $user?.status_emoji || $user?.status_message} +
+ + +
+ + + {:else} +
+ +
+ {/if} + +
+ {/if} + { diff --git a/src/lib/components/layout/Sidebar/UserStatusModal.svelte b/src/lib/components/layout/Sidebar/UserStatusModal.svelte new file mode 100644 index 0000000000..25e0f5f1c1 --- /dev/null +++ b/src/lib/components/layout/Sidebar/UserStatusModal.svelte @@ -0,0 +1,157 @@ + + + +
+
+
+ {$i18n.t('Set your status')} +
+ +
+ +
+
+
{ + submitHandler(); + }} + > +
+
+ {$i18n.t('Status')} +
+ +
+ {}} + onSubmit={(name) => { + console.log(name); + emoji = name; + }} + > +
+ {#if emoji} + + {:else} + + {/if} +
+
+ + + + +
+
+ +
+ +
+
+
+
+
+