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({
}}
>