diff --git a/src/renderer/src/components/Popups/UserPopup.tsx b/src/renderer/src/components/Popups/UserPopup.tsx index 410857e3..33bea081 100644 --- a/src/renderer/src/components/Popups/UserPopup.tsx +++ b/src/renderer/src/components/Popups/UserPopup.tsx @@ -5,13 +5,14 @@ import { useAppDispatch } from '@renderer/store' import { setAvatar } from '@renderer/store/runtime' import { setUserName } from '@renderer/store/settings' 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 { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { Center, HStack } from '../Layout' +import { Center, HStack, VStack } from '../Layout' import { TopView } from '../TopView' +import EmojiPicker from '../EmojiPicker' interface Props { resolve: (data: any) => void @@ -19,6 +20,8 @@ interface Props { const PopupContainer: React.FC = ({ resolve }) => { const [open, setOpen] = useState(true) + const [emojiPickerOpen, setEmojiPickerOpen] = useState(false) + const [dropdownOpen, setDropdownOpen] = useState(false) const { t } = useTranslation() const { userName } = useSettings() const dispatch = useAppDispatch() @@ -36,17 +39,28 @@ const PopupContainer: React.FC = ({ resolve }) => { resolve({}) } - return ( - -
+ const handleEmojiClick = async (emoji: string) => { + try { + // set emoji string + await ImageStorage.set('avatar', emoji) + // update avatar display + dispatch(setAvatar(emoji)) + setEmojiPickerOpen(false) + } catch (error: any) { + window.message.error(error.message) + } + } + + // 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: ( {}} accept="image/png, image/jpeg, image/gif" @@ -62,12 +76,72 @@ const PopupContainer: React.FC = ({ resolve }) => { await ImageStorage.set('avatar', compressedFile) } dispatch(setAvatar(await ImageStorage.get('avatar'))) + setDropdownOpen(false) } catch (error: any) { window.message.error(error.message) } }}> - +
{t('settings.general.image_upload')}
+ ) + }, + { + key: 'emoji', + label: ( +
{ + e.stopPropagation() + setEmojiPickerOpen(true) + setDropdownOpen(false) + }}> + {t('settings.general.emoji_picker')} +
+ ) + } + ] + + return ( + +
+ + { + setDropdownOpen(visible) + if (visible) { + setEmojiPickerOpen(false) + } + }}> + } + trigger="click" + open={emojiPickerOpen} + onOpenChange={(visible) => { + setEmojiPickerOpen(visible) + if (visible) { + setDropdownOpen(false) + } + }} + placement="bottom"> + {isEmoji(avatar) ? ( + {avatar} + ) : ( + + )} + + +
{ }) } + const isEmoji = (str: string) => { + return str && typeof str === 'string' && !str.startsWith('data:') && !str.startsWith('http'); + } + return ( { backgroundColor: sidebarBgColor, zIndex: minappShow ? 10000 : 'initial' }}> - + {isEmoji(avatar) ? ( + {avatar} + ) : ( + + )} @@ -220,6 +228,22 @@ const AvatarImg = styled(Avatar)` border: none; 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` display: flex; flex: 1; diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 58977755..a929a457 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -697,6 +697,8 @@ "general.title": "General Settings", "general.user_name": "User 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", "input.auto_translate_with_space": "Quickly translate with 3 spaces", "input.target_language": "Target language", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index f92212e3..5ac50f14 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -697,6 +697,8 @@ "general.title": "一般設定", "general.user_name": "ユーザー名", "general.user_name.placeholder": "ユーザー名を入力", + "general.image_upload": "画像アップロード", + "general.emoji_picker": "絵文字ピッカー", "general.view_webdav_settings": "WebDAV設定を表示", "input.auto_translate_with_space": "スペースを3回押して翻訳", "input.target_language": "目標言語", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index df88a529..65fc8dc9 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -697,6 +697,8 @@ "general.title": "Общие настройки", "general.user_name": "Имя пользователя", "general.user_name.placeholder": "Введите ваше имя", + "general.image_upload": "Загрузка изображений", + "general.emoji_picker": "Выбор эмодзи", "general.view_webdav_settings": "Просмотр настроек WebDAV", "input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов", "input.target_language": "Целевой язык", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a946d876..c771f8be 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -697,6 +697,8 @@ "general.title": "常规设置", "general.user_name": "用户名", "general.user_name.placeholder": "请输入用户名", + "general.image_upload": "图片上传", + "general.emoji_picker": "表情选择器", "general.view_webdav_settings": "查看 WebDAV 设置", "input.auto_translate_with_space": "快速敲击3次空格翻译", "input.target_language": "目标语言", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a88733de..e3cd412c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -696,6 +696,8 @@ "general.title": "一般設定", "general.user_name": "使用者名稱", "general.user_name.placeholder": "輸入您的名稱", + "general.image_upload": "圖片上傳", + "general.emoji_picker": "表情選擇器", "general.view_webdav_settings": "查看 WebDAV 設定", "input.auto_translate_with_space": "快速敲擊3次空格翻譯", "input.target_language": "目標語言", diff --git a/src/renderer/src/pages/home/Messages/MessageHeader.tsx b/src/renderer/src/pages/home/Messages/MessageHeader.tsx index f1b037c4..616367f0 100644 --- a/src/renderer/src/pages/home/Messages/MessageHeader.tsx +++ b/src/renderer/src/pages/home/Messages/MessageHeader.tsx @@ -15,6 +15,10 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +const isEmoji = (str: string) => { + return str && typeof str === 'string' && !str.startsWith('data:') && !str.startsWith('http'); +} + interface Props { message: Message assistant: Assistant @@ -81,12 +85,18 @@ const MessageHeader: FC = memo(({ assistant, model, message }) => { {avatarName} ) : ( - UserPopup.show()} - /> + <> + {isEmoji(avatar) ? ( + UserPopup.show()}>{avatar} + ) : ( + UserPopup.show()} + /> + )} + )} @@ -101,6 +111,19 @@ const MessageHeader: FC = memo(({ assistant, model, message }) => { 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` display: flex; flex-direction: row; diff --git a/src/renderer/src/services/ImageStorage.ts b/src/renderer/src/services/ImageStorage.ts index d620eb5f..bdc4c723 100644 --- a/src/renderer/src/services/ImageStorage.ts +++ b/src/renderer/src/services/ImageStorage.ts @@ -4,16 +4,26 @@ import { convertToBase64 } from '@renderer/utils' const IMAGE_PREFIX = 'image://' export default class ImageStorage { - static async set(key: string, file: File) { + static async set(key: string, value: File | string) { const id = IMAGE_PREFIX + key try { - const base64Image = await convertToBase64(file) - if (typeof base64Image === 'string') { + if (typeof value === 'string') { + // string(emoji) if (await db.settings.get(id)) { - db.settings.update(id, { value: base64Image }) + db.settings.update(id, { value }) return } - await db.settings.add({ id, value: base64Image }) + await db.settings.add({ id, value }) + } else { + // file image + const base64Image = await convertToBase64(value) + if (typeof base64Image === 'string') { + if (await db.settings.get(id)) { + db.settings.update(id, { value: base64Image }) + return + } + await db.settings.add({ id, value: base64Image }) + } } } catch (error) { console.error('Error storing the image', error)