added the ability to select emojis via the Emoji Picker Overlay

This commit is contained in:
Дмитрий Пиченикин 2026-02-25 13:39:05 +03:00
parent a88ce39a48
commit edc20a252f
6 changed files with 213 additions and 10 deletions

View File

@ -94,5 +94,27 @@
"badges.error.cannot_create_type": "Failed to create type", "badges.error.cannot_create_type": "Failed to create type",
"badges.error.cannot_update_badge": "Failed to update badge", "badges.error.cannot_update_badge": "Failed to update badge",
"badges.error.cannot_delete_badge": "Failed to delete 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"
} }

View File

@ -94,5 +94,27 @@
"badges.error.cannot_create_type": "Не удалось создать тип", "badges.error.cannot_create_type": "Не удалось создать тип",
"badges.error.cannot_update_badge": "Не удалось обновить значок", "badges.error.cannot_update_badge": "Не удалось обновить значок",
"badges.error.cannot_delete_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}"
} }

View File

@ -101,6 +101,57 @@
resize: vertical; resize: vertical;
min-height: 60px; 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 { .checkbox-group {

View File

@ -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<EmojiPickerOverlayProps> = (props) => {
const Overlay = (window as any).Components?.EmojiPickerOverlay;
if (!Overlay) {
return null;
}
return <Overlay {...props}/>;
};
export default EmojiPickerOverlay;

View File

@ -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 {useDispatch, useSelector} from 'react-redux';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
@ -12,7 +12,10 @@ import {BadgeTypeDefinition} from 'types/badges';
import Client from 'client/api'; import Client from 'client/api';
import {getServerErrorId} from 'utils/helpers'; import {getServerErrorId} from 'utils/helpers';
import CloseIcon from 'components/icons/close_icon'; 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 TypeSelect from './type_select';
import './badge_modal.scss'; import './badge_modal.scss';
@ -43,6 +46,14 @@ const BadgeModal: React.FC = () => {
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null); const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);
const emojiData = (window as any)?.useGetEmojiSelectorData?.();
const {
emojiButtonRef,
calculateRightOffSet,
} = emojiData || {};
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@ -81,6 +92,7 @@ const BadgeModal: React.FC = () => {
setConfirmDelete(false); setConfirmDelete(false);
setConfirmDeleteTypeId(null); setConfirmDeleteTypeId(null);
setTypeDropdownOpen(false); setTypeDropdownOpen(false);
setShowEmojiPicker(false);
setLoading(false); setLoading(false);
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps }, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
@ -105,6 +117,15 @@ const BadgeModal: React.FC = () => {
setConfirmDeleteTypeId(null); 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) => { const handleDeleteType = useCallback(async (typeId: string) => {
if (confirmDeleteTypeId !== typeId) { if (confirmDeleteTypeId !== typeId) {
setConfirmDeleteTypeId(typeId); setConfirmDeleteTypeId(typeId);
@ -210,7 +231,10 @@ const BadgeModal: React.FC = () => {
className='BadgeModal__backdrop' className='BadgeModal__backdrop'
onClick={handleClose} onClick={handleClose}
/> />
<div className='BadgeModal__dialog'> <div
className='BadgeModal__dialog'
ref={dialogRef}
>
<div className='BadgeModal__header'> <div className='BadgeModal__header'>
<h4>{title}</h4> <h4>{title}</h4>
<button <button
@ -257,6 +281,21 @@ const BadgeModal: React.FC = () => {
defaultMessage='Эмодзи' defaultMessage='Эмодзи'
/> />
</label> </label>
<div className='emoji-input'>
<button
type='button'
className='emoji-input__icon'
onClick={() => setShowEmojiPicker((prev) => !prev)}
ref={emojiButtonRef}
>
<EmojiIcon/>
</button>
{image && (
<RenderEmoji
emojiName={image}
size={20}
/>
)}
<input <input
type='text' type='text'
value={image} value={image}
@ -264,6 +303,19 @@ const BadgeModal: React.FC = () => {
placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})} placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})}
/> />
</div> </div>
{showEmojiPicker && (
<EmojiPickerOverlay
target={() => emojiButtonRef?.current}
container={() => dialogRef.current}
show={showEmojiPicker}
onHide={() => setShowEmojiPicker(false)}
onEmojiClick={handleEmojiSelect}
rightOffset={calculateRightOffSet?.(emojiButtonRef?.current)}
defaultHorizontalPosition='right'
hideCustomEmojiButton={true}
/>
)}
</div>
<div className='form-group'> <div className='form-group'>
<label> <label>
<FormattedMessage <FormattedMessage

View File

@ -0,0 +1,31 @@
import React from 'react';
type Props = {
size?: number;
}
const EmojiIcon: React.FC<Props> = ({size = 20}) => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path
stroke='none'
d='M0 0h24v24H0z'
fill='none'
/>
<path d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0'/>
<path d='M9 10l.01 0'/>
<path d='M15 10l.01 0'/>
<path d='M9.5 15a3.5 3.5 0 0 0 5 0'/>
</svg>
);
export default EmojiIcon;