added the ability to select emojis via the Emoji Picker Overlay
This commit is contained in:
parent
a88ce39a48
commit
edc20a252f
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
25
webapp/src/components/badge_modal/emoji_picker.tsx
Normal file
25
webapp/src/components/badge_modal/emoji_picker.tsx
Normal 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;
|
||||||
@ -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,12 +281,40 @@ const BadgeModal: React.FC = () => {
|
|||||||
defaultMessage='Эмодзи'
|
defaultMessage='Эмодзи'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div className='emoji-input'>
|
||||||
type='text'
|
<button
|
||||||
value={image}
|
type='button'
|
||||||
onChange={(e) => setImage(e.target.value)}
|
className='emoji-input__icon'
|
||||||
placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})}
|
onClick={() => setShowEmojiPicker((prev) => !prev)}
|
||||||
/>
|
ref={emojiButtonRef}
|
||||||
|
>
|
||||||
|
<EmojiIcon/>
|
||||||
|
</button>
|
||||||
|
{image && (
|
||||||
|
<RenderEmoji
|
||||||
|
emojiName={image}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={image}
|
||||||
|
onChange={(e) => setImage(e.target.value)}
|
||||||
|
placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
31
webapp/src/components/icons/emoji_icon.tsx
Normal file
31
webapp/src/components/icons/emoji_icon.tsx
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user