From 2a85ad2f45bf0bf3672b195c45d8b32884bdafc3 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 24 Mar 2023 22:30:05 +0800 Subject: [PATCH] New feature: custom emoji picker --- src/components/compose.css | 40 +++++ src/components/compose.jsx | 308 ++++++++++++++++++++++++++++++------- src/components/icon.jsx | 1 + 3 files changed, 291 insertions(+), 58 deletions(-) diff --git a/src/components/compose.css b/src/components/compose.css index 7e09c82e..8e058118 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -523,3 +523,43 @@ height: auto; } } + +#custom-emojis-sheet { + max-height: 50vh; + max-height: 50dvh; +} +#custom-emojis-sheet main { + mask-image: none; +} +#custom-emojis-sheet .custom-emojis-list .section-header { + font-size: 80%; + text-transform: uppercase; + color: var(--text-insignificant-color); + padding: 8px 0 4px; + position: sticky; + top: 0; + background-color: var(--bg-blur-color); + backdrop-filter: blur(8px); +} +#custom-emojis-sheet .custom-emojis-list section { + display: flex; + flex-wrap: wrap; +} +#custom-emojis-sheet .custom-emojis-list button { + border-radius: 8px; + background-image: radial-gradient( + closest-side, + var(--img-bg-color), + transparent + ); +} +#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) { + filter: none; + background-color: var(--bg-faded-color); +} +#custom-emojis-sheet .custom-emojis-list button img { + transition: transform 0.1s ease-out; +} +#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img { + transform: scale(1.5); +} diff --git a/src/components/compose.jsx b/src/components/compose.jsx index d03e0681..ababec90 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -4,7 +4,7 @@ import { match } from '@formatjs/intl-localematcher'; import '@github/text-expander-element'; import equal from 'fast-deep-equal'; import { forwardRef } from 'preact/compat'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import stringLength from 'string-length'; import { uid } from 'uid/single'; @@ -497,6 +497,8 @@ function Compose({ }; }, [mediaAttachments]); + const [showEmoji2Picker, setShowEmoji2Picker] = useState(false); + return (
@@ -982,65 +984,77 @@ function Compose({ justifyContent: 'flex-end', }} > -
+ {showEmoji2Picker && ( + { + if (e.target === e.currentTarget) { + setShowEmoji2Picker(false); + } + }} + > + { + setShowEmoji2Picker(false); + }} + onSelect={(emoji) => { + const emojiWithSpace = ` ${emoji} `; + const textarea = textareaRef.current; + if (!textarea) return; + const { selectionStart, selectionEnd } = textarea; + const text = textarea.value; + const newText = + text.slice(0, selectionStart) + + emojiWithSpace + + text.slice(selectionEnd); + textarea.value = newText; + textarea.selectionStart = textarea.selectionEnd = + selectionEnd + emojiWithSpace.length; + textarea.focus(); + textarea.dispatchEvent(new Event('input')); + }} + /> + + )}
); } @@ -1287,6 +1335,7 @@ const Textarea = forwardRef((props, ref) => { value={text} onInput={(e) => { const { scrollHeight, offsetHeight, clientHeight, value } = e.target; + console.log('textarea input', value); setText(value); const offset = offsetHeight - clientHeight; e.target.style.height = value ? scrollHeight + offset + 'px' : null; @@ -1626,4 +1675,147 @@ function removeNullUndefined(obj) { return obj; } +function CustomEmojisModal({ + masto, + instance, + onClose = () => {}, + onSelect = () => {}, +}) { + const [uiState, setUIState] = useState('default'); + const customEmojisList = useRef([]); + const [customEmojis, setCustomEmojis] = useState({}); + const recentlyUsedCustomEmojis = useMemo( + () => store.account.get('recentlyUsedCustomEmojis') || [], + ); + useEffect(() => { + setUIState('loading'); + (async () => { + try { + const emojis = await masto.v1.customEmojis.list(); + // Group emojis by category + const emojisCat = { + '--recent--': recentlyUsedCustomEmojis.filter((emoji) => + emojis.find((e) => e.shortcode === emoji.shortcode), + ), + }; + const othersCat = []; + emojis.forEach((emoji) => { + if (!emoji.visibleInPicker) return; + customEmojisList.current?.push?.(emoji); + if (!emoji.category) { + othersCat.push(emoji); + return; + } + if (!emojisCat[emoji.category]) { + emojisCat[emoji.category] = []; + } + emojisCat[emoji.category].push(emoji); + }); + if (othersCat.length) { + emojisCat['--others--'] = othersCat; + } + setCustomEmojis(emojisCat); + setUIState('default'); + } catch (e) { + setUIState('error'); + console.error(e); + } + })(); + }, []); + + return ( +
+
+ Custom emojis{' '} + {uiState === 'loading' ? ( + + ) : ( + • {instance} + )} +
+
+
+ {uiState === 'error' && ( +
+

Error loading custom emojis

+
+ )} + {uiState === 'default' && + Object.entries(customEmojis).map( + ([category, emojis]) => + !!emojis?.length && ( + <> +
+ {{ + '--recent--': 'Recently used', + '--others--': 'Others', + }[category] || category} +
+
+ {emojis.map((emoji) => ( + + ))} +
+ + ), + )} +
+
+
+ ); +} + export default Compose; diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 9dc81fb7..8d6fcb10 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -73,6 +73,7 @@ const ICONS = { flag: 'mingcute:flag-4-line', time: 'mingcute:time-line', refresh: 'mingcute:refresh-2-line', + emoji2: 'mingcute:emoji-2-line', }; const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');