diff --git a/src/components/compose.css b/src/components/compose.css index 220bcf4d..db013567 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -298,14 +298,20 @@ height: 2.2em; } #compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) { - color: var(--bg-color); - background-color: var(--link-color); -} -#compose-container - .text-expander-menu:hover - li[aria-selected]:not(:hover, :focus) { + background-color: var(--link-bg-color); color: var(--text-color); - background-color: var(--bg-color); +} +#compose-container .text-expander-menu li[aria-selected] { + box-shadow: inset 4px 0 0 0 var(--button-bg-color); +} +#compose-container .text-expander-menu li[data-more] { + &:not(:hover, :focus, [aria-selected]) { + color: var(--text-insignificant-color); + background-color: var(--bg-faded-color); + } + + font-size: 0.8em; + justify-content: center; } #compose-container .form-visibility-direct { diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 954d800e..7269e4ad 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -378,8 +378,11 @@ function Compose({ } // check for status and media attachments + const hasValue = (value || '') + .trim() + .replace(/^\p{White_Space}+|\p{White_Space}+$/gu, ''); const hasMediaAttachments = mediaAttachments.length > 0; - if (!value && !hasMediaAttachments) { + if (!hasValue && !hasMediaAttachments) { console.log('canClose', { value, mediaAttachments }); return true; } @@ -1119,6 +1122,13 @@ function Compose({ } return masto.v2.search.fetch(params); }} + onTrigger={(action) => { + if (action?.name === 'custom-emojis') { + setShowEmoji2Picker({ + defaultSearchTerm: action?.defaultSearchTerm || null, + }); + } + }} /> {mediaAttachments?.length > 0 && (
@@ -1342,19 +1352,29 @@ function Compose({ onClose={() => { setShowEmoji2Picker(false); }} - onSelect={(emoji) => { - const emojiWithSpace = ` ${emoji} `; + defaultSearchTerm={showEmoji2Picker?.defaultSearchTerm} + onSelect={(emojiShortcode) => { const textarea = textareaRef.current; if (!textarea) return; const { selectionStart, selectionEnd } = textarea; const text = textarea.value; + const textBeforeEmoji = text.slice(0, selectionStart); + const spaceBeforeEmoji = /[\s\t\n\r]$/.test(textBeforeEmoji) + ? '' + : ' '; + const textAfterEmoji = text.slice(selectionEnd); + const spaceAfterEmoji = /^[\s\t\n\r]/.test(textAfterEmoji) + ? '' + : ' '; const newText = - text.slice(0, selectionStart) + - emojiWithSpace + - text.slice(selectionEnd); + textBeforeEmoji + + spaceBeforeEmoji + + emojiShortcode + + spaceAfterEmoji + + textAfterEmoji; textarea.value = newText; textarea.selectionStart = textarea.selectionEnd = - selectionEnd + emojiWithSpace.length; + selectionEnd + emojiShortcode.length + spaceAfterEmoji.length; textarea.focus(); textarea.dispatchEvent(new Event('input')); }} @@ -1454,7 +1474,12 @@ const getCustomEmojis = pmem(_getCustomEmojis, { const Textarea = forwardRef((props, ref) => { const { masto, instance } = api(); const [text, setText] = useState(ref.current?.value || ''); - const { maxCharacters, performSearch = () => {}, ...textareaProps } = props; + const { + maxCharacters, + performSearch = () => {}, + onTrigger = () => {}, + ...textareaProps + } = props; // const snapStates = useSnapshot(states); // const charCount = snapStates.composerCharacterCount; @@ -1509,6 +1534,7 @@ const Textarea = forwardRef((props, ref) => { ${encodeHTML(shortcode)} `; }); + html += `
  • More…
  • `; // console.log({ emojis, html }); menu.innerHTML = html; provide( @@ -1600,10 +1626,22 @@ const Textarea = forwardRef((props, ref) => { handleValue = (e) => { const { key, item } = e.detail; + const { value, more } = item.dataset; if (key === ':') { - e.detail.value = `:${item.dataset.value}:`; + e.detail.value = value ? `:${value}:` : '​'; // zero-width space + if (more) { + // Prevent adding space after the above value + e.detail.continue = true; + + setTimeout(() => { + onTrigger?.({ + name: 'custom-emojis', + defaultSearchTerm: more, + }); + }, 300); + } } else { - e.detail.value = `${key}${item.dataset.value}`; + e.detail.value = `${key}${value}`; } }; @@ -1748,7 +1786,8 @@ const Textarea = forwardRef((props, ref) => { }} onInput={(e) => { const { target } = e; - const text = target.value; + // Replace zero-width space + const text = target.value.replace(/\u200b/g, ''); setText(text); autoResizeTextarea(target); props.onInput?.(e); @@ -2270,6 +2309,7 @@ function CustomEmojisModal({ instance, onClose = () => {}, onSelect = () => {}, + defaultSearchTerm, }) { const [uiState, setUIState] = useState('default'); const customEmojisList = useRef([]); @@ -2336,6 +2376,11 @@ function CustomEmojisModal({ }, [customEmojis], ); + useEffect(() => { + if (defaultSearchTerm && customEmojis?.length) { + onFind({ target: { value: defaultSearchTerm } }); + } + }, [defaultSearchTerm, onFind, customEmojis]); const onSelectEmoji = useCallback( (emoji) => { @@ -2371,6 +2416,18 @@ function CustomEmojisModal({ [onSelect], ); + const inputRef = useRef(); + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + // Put cursor at the end + if (inputRef.current.value) { + inputRef.current.selectionStart = inputRef.current.value.length; + inputRef.current.selectionEnd = inputRef.current.value.length; + } + } + }, []); + return (
    {!!onClose && ( @@ -2397,6 +2454,7 @@ function CustomEmojisModal({ }} >