From edc20a252fffd887414400e331b8373621872fdf Mon Sep 17 00:00:00 2001 From: "dmitrii.pichenikin" Date: Wed, 25 Feb 2026 13:39:05 +0300 Subject: [PATCH] added the ability to select emojis via the Emoji Picker Overlay --- webapp/i18n/en.json | 24 ++++++- webapp/i18n/ru.json | 24 ++++++- .../components/badge_modal/badge_modal.scss | 51 ++++++++++++++ .../components/badge_modal/emoji_picker.tsx | 25 +++++++ webapp/src/components/badge_modal/index.tsx | 68 ++++++++++++++++--- webapp/src/components/icons/emoji_icon.tsx | 31 +++++++++ 6 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 webapp/src/components/badge_modal/emoji_picker.tsx create mode 100644 webapp/src/components/icons/emoji_icon.tsx diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 4c03c40..cdce4b2 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -94,5 +94,27 @@ "badges.error.cannot_create_type": "Failed to create type", "badges.error.cannot_update_badge": "Failed to update badge", "badges.error.cannot_delete_badge": "Failed to delete badge", - "badges.error.cannot_delete_type": "Failed to delete type" + "badges.error.cannot_delete_type": "Failed to delete type", + + "emoji_picker.activities": "Activities", + "emoji_picker.animals-nature": "Animals & Nature", + "emoji_picker.close": "Close", + "emoji_picker.custom": "Custom", + "emoji_picker.custom_emoji": "Custom Emoji", + "emoji_picker.emojiPicker.button.ariaLabel": "select an emoji", + "emoji_picker.emojiPicker.previewPlaceholder": "Select an Emoji", + "emoji_picker.flags": "Flags", + "emoji_picker.food-drink": "Food & Drink", + "emoji_picker.header": "Emoji Picker", + "emoji_picker.objects": "Objects", + "emoji_picker.people-body": "People & Body", + "emoji_picker.recent": "Recently Used", + "emoji_picker.search": "Search emojis", + "emoji_picker.searchResults": "Search Results", + "emoji_picker.search_emoji": "Search for an emoji", + "emoji_picker.skin_tone": "Skin tone", + "emoji_picker.smileys-emotion": "Smileys & Emotion", + "emoji_picker.symbols": "Symbols", + "emoji_picker.travel-places": "Travel Places", + "emoji_picker_item.emoji_aria_label": "{emojiName} emoji" } diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index a9902c6..72b6ed3 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -94,5 +94,27 @@ "badges.error.cannot_create_type": "Не удалось создать тип", "badges.error.cannot_update_badge": "Не удалось обновить значок", "badges.error.cannot_delete_badge": "Не удалось удалить значок", - "badges.error.cannot_delete_type": "Не удалось удалить тип" + "badges.error.cannot_delete_type": "Не удалось удалить тип", + + "emoji_picker.activities": "Мероприятия", + "emoji_picker.animals-nature": "Животные и природа", + "emoji_picker.close": "Закрыть", + "emoji_picker.custom": "Настраиваемое", + "emoji_picker.custom_emoji": "Пользовательские смайлики", + "emoji_picker.emojiPicker.button.ariaLabel": "выберите смайлик", + "emoji_picker.emojiPicker.previewPlaceholder": "Выберите смайлик", + "emoji_picker.flags": "Флаги", + "emoji_picker.food-drink": "Еда и напитки", + "emoji_picker.header": "Выбор смайликов", + "emoji_picker.objects": "Объекты", + "emoji_picker.people-body": "Люди и тело", + "emoji_picker.recent": "Недавно использованные", + "emoji_picker.search": "Поиск смайликов", + "emoji_picker.searchResults": "Результаты поиска", + "emoji_picker.search_emoji": "Поиск смайлика", + "emoji_picker.skin_tone": "Цвет кожи", + "emoji_picker.smileys-emotion": "Смайлы и эмоции", + "emoji_picker.symbols": "Символы", + "emoji_picker.travel-places": "Места путешествий", + "emoji_picker_item.emoji_aria_label": "смайлик {emojiName}" } diff --git a/webapp/src/components/badge_modal/badge_modal.scss b/webapp/src/components/badge_modal/badge_modal.scss index 4aed59d..3381ba5 100644 --- a/webapp/src/components/badge_modal/badge_modal.scss +++ b/webapp/src/components/badge_modal/badge_modal.scss @@ -101,6 +101,57 @@ resize: vertical; min-height: 60px; } + + .emoji-input { + display: flex; + align-items: center; + border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16); + border-radius: 4px; + background: var(--center-channel-bg, #fff); + + &:focus-within { + border-color: var(--button-bg, #166de0); + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 36px; + height: 36px; + padding: 0; + border: none; + background: none; + color: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.56); + cursor: pointer; + + &:hover { + color: var(--center-channel-color, #3d3c40); + } + + .emoticon { + display: block; + } + } + + .emojisprite, + .emoticon { + margin-right: 4px; + } + + input[type='text'] { + flex: 1; + border: none; + padding: 8px 12px 8px 0; + + &:focus { + outline: none; + border-color: transparent; + box-shadow: none; + } + } + } } .checkbox-group { diff --git a/webapp/src/components/badge_modal/emoji_picker.tsx b/webapp/src/components/badge_modal/emoji_picker.tsx new file mode 100644 index 0000000..96e301b --- /dev/null +++ b/webapp/src/components/badge_modal/emoji_picker.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface EmojiPickerOverlayProps { + target: () => HTMLElement | null; + container?: () => HTMLElement | null; + show: boolean; + onHide: () => void; + onEmojiClick: (emoji: any) => void; + rightOffset?: number; + defaultHorizontalPosition?: 'left' | 'right'; + onExited?: () => void; + hideCustomEmojiButton?: boolean; +} + +const EmojiPickerOverlay: React.FC = (props) => { + const Overlay = (window as any).Components?.EmojiPickerOverlay; + + if (!Overlay) { + return null; + } + + return ; +}; + +export default EmojiPickerOverlay; diff --git a/webapp/src/components/badge_modal/index.tsx b/webapp/src/components/badge_modal/index.tsx index e8565fe..61d8c02 100644 --- a/webapp/src/components/badge_modal/index.tsx +++ b/webapp/src/components/badge_modal/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {FormattedMessage, useIntl} from 'react-intl'; @@ -12,7 +12,10 @@ import {BadgeTypeDefinition} from 'types/badges'; import Client from 'client/api'; import {getServerErrorId} from 'utils/helpers'; import CloseIcon from 'components/icons/close_icon'; +import EmojiIcon from 'components/icons/emoji_icon'; +import RenderEmoji from 'components/utils/emoji'; +import EmojiPickerOverlay from './emoji_picker'; import TypeSelect from './type_select'; import './badge_modal.scss'; @@ -43,6 +46,14 @@ const BadgeModal: React.FC = () => { const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState(null); const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const dialogRef = useRef(null); + + const emojiData = (window as any)?.useGetEmojiSelectorData?.(); + const { + emojiButtonRef, + calculateRightOffSet, + } = emojiData || {}; useEffect(() => { if (!isOpen) { @@ -81,6 +92,7 @@ const BadgeModal: React.FC = () => { setConfirmDelete(false); setConfirmDeleteTypeId(null); setTypeDropdownOpen(false); + setShowEmojiPicker(false); setLoading(false); }, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps @@ -105,6 +117,15 @@ const BadgeModal: React.FC = () => { setConfirmDeleteTypeId(null); }, []); + const handleEmojiSelect = (emoji: any) => { + if (emoji.short_name) { + setImage(emoji.short_name); + } else if (emoji.name) { + setImage(emoji.name); + } + setShowEmojiPicker(false); + }; + const handleDeleteType = useCallback(async (typeId: string) => { if (confirmDeleteTypeId !== typeId) { setConfirmDeleteTypeId(typeId); @@ -210,7 +231,10 @@ const BadgeModal: React.FC = () => { className='BadgeModal__backdrop' onClick={handleClose} /> -
+

{title}

+ {image && ( + + )} + setImage(e.target.value)} + placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})} + /> +
+ {showEmojiPicker && ( + emojiButtonRef?.current} + container={() => dialogRef.current} + show={showEmojiPicker} + onHide={() => setShowEmojiPicker(false)} + onEmojiClick={handleEmojiSelect} + rightOffset={calculateRightOffSet?.(emojiButtonRef?.current)} + defaultHorizontalPosition='right' + hideCustomEmojiButton={true} + /> + )}