feat: Add emoji avatar support with image upload and picker & support i18n language (#2473)
This commit is contained in:
parent
ae11490f87
commit
92ab67eb3d
@ -5,13 +5,14 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
import { setUserName } from '@renderer/store/settings'
|
import { setUserName } from '@renderer/store/settings'
|
||||||
import { compressImage } from '@renderer/utils'
|
import { compressImage } from '@renderer/utils'
|
||||||
import { Avatar, Input, Modal, Upload } from 'antd'
|
import { Avatar , Input, Modal, Popover, Upload, Dropdown } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { Center, HStack } from '../Layout'
|
import { Center, HStack, VStack } from '../Layout'
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
|
import EmojiPicker from '../EmojiPicker'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resolve: (data: any) => void
|
resolve: (data: any) => void
|
||||||
@ -19,6 +20,8 @@ interface Props {
|
|||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userName } = useSettings()
|
const { userName } = useSettings()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -36,17 +39,28 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
resolve({})
|
resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const handleEmojiClick = async (emoji: string) => {
|
||||||
<Modal
|
try {
|
||||||
width="300px"
|
// set emoji string
|
||||||
open={open}
|
await ImageStorage.set('avatar', emoji)
|
||||||
footer={null}
|
// update avatar display
|
||||||
onOk={onOk}
|
dispatch(setAvatar(emoji))
|
||||||
onCancel={onCancel}
|
setEmojiPickerOpen(false)
|
||||||
afterClose={onClose}
|
} catch (error: any) {
|
||||||
transitionName="ant-move-down"
|
window.message.error(error.message)
|
||||||
centered>
|
}
|
||||||
<Center mt="30px">
|
}
|
||||||
|
|
||||||
|
// modify the judgment function, more accurately detect Emoji
|
||||||
|
const isEmoji = (str: string) => {
|
||||||
|
// check if it is a string and is not base64 or URL format
|
||||||
|
return str && typeof str === 'string' && !str.startsWith('data:') && !str.startsWith('http');
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'upload',
|
||||||
|
label: (
|
||||||
<Upload
|
<Upload
|
||||||
customRequest={() => {}}
|
customRequest={() => {}}
|
||||||
accept="image/png, image/jpeg, image/gif"
|
accept="image/png, image/jpeg, image/gif"
|
||||||
@ -62,12 +76,72 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
await ImageStorage.set('avatar', compressedFile)
|
await ImageStorage.set('avatar', compressedFile)
|
||||||
}
|
}
|
||||||
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
||||||
|
setDropdownOpen(false)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
window.message.error(error.message)
|
window.message.error(error.message)
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<UserAvatar src={avatar} />
|
<div>{t('settings.general.image_upload')}</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'emoji',
|
||||||
|
label: (
|
||||||
|
<div onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setEmojiPickerOpen(true)
|
||||||
|
setDropdownOpen(false)
|
||||||
|
}}>
|
||||||
|
{t('settings.general.emoji_picker')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width="300px"
|
||||||
|
open={open}
|
||||||
|
footer={null}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
centered>
|
||||||
|
<Center mt="30px">
|
||||||
|
<VStack alignItems="center" gap="10px">
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items }}
|
||||||
|
trigger={['click']}
|
||||||
|
open={dropdownOpen}
|
||||||
|
align={{ offset: [0, 4] }}
|
||||||
|
placement="bottom"
|
||||||
|
onOpenChange={(visible) => {
|
||||||
|
setDropdownOpen(visible)
|
||||||
|
if (visible) {
|
||||||
|
setEmojiPickerOpen(false)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Popover
|
||||||
|
content={<EmojiPicker onEmojiClick={handleEmojiClick} />}
|
||||||
|
trigger="click"
|
||||||
|
open={emojiPickerOpen}
|
||||||
|
onOpenChange={(visible) => {
|
||||||
|
setEmojiPickerOpen(visible)
|
||||||
|
if (visible) {
|
||||||
|
setDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placement="bottom">
|
||||||
|
{isEmoji(avatar) ? (
|
||||||
|
<EmojiAvatar>{avatar}</EmojiAvatar>
|
||||||
|
) : (
|
||||||
|
<UserAvatar src={avatar} />
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</Dropdown>
|
||||||
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
<HStack alignItems="center" gap="10px" p="20px">
|
<HStack alignItems="center" gap="10px" p="20px">
|
||||||
<Input
|
<Input
|
||||||
@ -92,6 +166,23 @@ const UserAvatar = styled(Avatar)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const EmojiAvatar = styled.div`
|
||||||
|
cursor: pointer;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 20%;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default class UserPopup {
|
export default class UserPopup {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
static hide() {
|
static hide() {
|
||||||
|
|||||||
@ -57,6 +57,10 @@ const Sidebar: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEmoji = (str: string) => {
|
||||||
|
return str && typeof str === 'string' && !str.startsWith('data:') && !str.startsWith('http');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
id="app-sidebar"
|
id="app-sidebar"
|
||||||
@ -64,7 +68,11 @@ const Sidebar: FC = () => {
|
|||||||
backgroundColor: sidebarBgColor,
|
backgroundColor: sidebarBgColor,
|
||||||
zIndex: minappShow ? 10000 : 'initial'
|
zIndex: minappShow ? 10000 : 'initial'
|
||||||
}}>
|
}}>
|
||||||
|
{isEmoji(avatar) ? (
|
||||||
|
<EmojiAvatarSidebar onClick={onEditUser}>{avatar}</EmojiAvatarSidebar>
|
||||||
|
) : (
|
||||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||||
|
)}
|
||||||
<MainMenusContainer>
|
<MainMenusContainer>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus onClick={MinApp.onClose}>
|
||||||
<MainMenus />
|
<MainMenus />
|
||||||
@ -220,6 +228,22 @@ const AvatarImg = styled(Avatar)`
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const EmojiAvatarSidebar = styled.div`
|
||||||
|
width: 31px;
|
||||||
|
height: 31px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
margin-bottom: ${isMac ? '12px' : '12px'};
|
||||||
|
margin-top: ${isMac ? '0px' : '2px'};
|
||||||
|
border-radius: 20%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
`
|
||||||
|
|
||||||
const MainMenusContainer = styled.div`
|
const MainMenusContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -697,6 +697,8 @@
|
|||||||
"general.title": "General Settings",
|
"general.title": "General Settings",
|
||||||
"general.user_name": "User Name",
|
"general.user_name": "User Name",
|
||||||
"general.user_name.placeholder": "Enter your name",
|
"general.user_name.placeholder": "Enter your name",
|
||||||
|
"general.image_upload": "Image Upload",
|
||||||
|
"general.emoji_picker": "Emoji Picker",
|
||||||
"general.view_webdav_settings": "View WebDAV settings",
|
"general.view_webdav_settings": "View WebDAV settings",
|
||||||
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
|
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
|
||||||
"input.target_language": "Target language",
|
"input.target_language": "Target language",
|
||||||
|
|||||||
@ -697,6 +697,8 @@
|
|||||||
"general.title": "一般設定",
|
"general.title": "一般設定",
|
||||||
"general.user_name": "ユーザー名",
|
"general.user_name": "ユーザー名",
|
||||||
"general.user_name.placeholder": "ユーザー名を入力",
|
"general.user_name.placeholder": "ユーザー名を入力",
|
||||||
|
"general.image_upload": "画像アップロード",
|
||||||
|
"general.emoji_picker": "絵文字ピッカー",
|
||||||
"general.view_webdav_settings": "WebDAV設定を表示",
|
"general.view_webdav_settings": "WebDAV設定を表示",
|
||||||
"input.auto_translate_with_space": "スペースを3回押して翻訳",
|
"input.auto_translate_with_space": "スペースを3回押して翻訳",
|
||||||
"input.target_language": "目標言語",
|
"input.target_language": "目標言語",
|
||||||
|
|||||||
@ -697,6 +697,8 @@
|
|||||||
"general.title": "Общие настройки",
|
"general.title": "Общие настройки",
|
||||||
"general.user_name": "Имя пользователя",
|
"general.user_name": "Имя пользователя",
|
||||||
"general.user_name.placeholder": "Введите ваше имя",
|
"general.user_name.placeholder": "Введите ваше имя",
|
||||||
|
"general.image_upload": "Загрузка изображений",
|
||||||
|
"general.emoji_picker": "Выбор эмодзи",
|
||||||
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
||||||
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
|
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
|
||||||
"input.target_language": "Целевой язык",
|
"input.target_language": "Целевой язык",
|
||||||
|
|||||||
@ -697,6 +697,8 @@
|
|||||||
"general.title": "常规设置",
|
"general.title": "常规设置",
|
||||||
"general.user_name": "用户名",
|
"general.user_name": "用户名",
|
||||||
"general.user_name.placeholder": "请输入用户名",
|
"general.user_name.placeholder": "请输入用户名",
|
||||||
|
"general.image_upload": "图片上传",
|
||||||
|
"general.emoji_picker": "表情选择器",
|
||||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||||
"input.auto_translate_with_space": "快速敲击3次空格翻译",
|
"input.auto_translate_with_space": "快速敲击3次空格翻译",
|
||||||
"input.target_language": "目标语言",
|
"input.target_language": "目标语言",
|
||||||
|
|||||||
@ -696,6 +696,8 @@
|
|||||||
"general.title": "一般設定",
|
"general.title": "一般設定",
|
||||||
"general.user_name": "使用者名稱",
|
"general.user_name": "使用者名稱",
|
||||||
"general.user_name.placeholder": "輸入您的名稱",
|
"general.user_name.placeholder": "輸入您的名稱",
|
||||||
|
"general.image_upload": "圖片上傳",
|
||||||
|
"general.emoji_picker": "表情選擇器",
|
||||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||||
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||||
"input.target_language": "目標語言",
|
"input.target_language": "目標語言",
|
||||||
|
|||||||
@ -15,6 +15,10 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const isEmoji = (str: string) => {
|
||||||
|
return str && typeof str === 'string' && !str.startsWith('data:') && !str.startsWith('http');
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -80,6 +84,10 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
onClick={showMiniApp}>
|
onClick={showMiniApp}>
|
||||||
{avatarName}
|
{avatarName}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isEmoji(avatar) ? (
|
||||||
|
<EmojiAvatar onClick={() => UserPopup.show()}>{avatar}</EmojiAvatar>
|
||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
src={avatar}
|
src={avatar}
|
||||||
@ -88,6 +96,8 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
onClick={() => UserPopup.show()}
|
onClick={() => UserPopup.show()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<UserWrap>
|
<UserWrap>
|
||||||
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
|
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
|
||||||
{username}
|
{username}
|
||||||
@ -101,6 +111,19 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
|
|
||||||
MessageHeader.displayName = 'MessageHeader'
|
MessageHeader.displayName = 'MessageHeader'
|
||||||
|
|
||||||
|
const EmojiAvatar = styled.div`
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
border-radius: 20%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@ -4,10 +4,19 @@ import { convertToBase64 } from '@renderer/utils'
|
|||||||
const IMAGE_PREFIX = 'image://'
|
const IMAGE_PREFIX = 'image://'
|
||||||
|
|
||||||
export default class ImageStorage {
|
export default class ImageStorage {
|
||||||
static async set(key: string, file: File) {
|
static async set(key: string, value: File | string) {
|
||||||
const id = IMAGE_PREFIX + key
|
const id = IMAGE_PREFIX + key
|
||||||
try {
|
try {
|
||||||
const base64Image = await convertToBase64(file)
|
if (typeof value === 'string') {
|
||||||
|
// string(emoji)
|
||||||
|
if (await db.settings.get(id)) {
|
||||||
|
db.settings.update(id, { value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await db.settings.add({ id, value })
|
||||||
|
} else {
|
||||||
|
// file image
|
||||||
|
const base64Image = await convertToBase64(value)
|
||||||
if (typeof base64Image === 'string') {
|
if (typeof base64Image === 'string') {
|
||||||
if (await db.settings.get(id)) {
|
if (await db.settings.get(id)) {
|
||||||
db.settings.update(id, { value: base64Image })
|
db.settings.update(id, { value: base64Image })
|
||||||
@ -15,6 +24,7 @@ export default class ImageStorage {
|
|||||||
}
|
}
|
||||||
await db.settings.add({ id, value: base64Image })
|
await db.settings.add({ id, value: base64Image })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error storing the image', error)
|
console.error('Error storing the image', error)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user