New feature: custom emoji picker

This commit is contained in:
Lim Chee Aun 2023-03-24 22:30:05 +08:00
parent f623ccd856
commit 2a85ad2f45
3 changed files with 291 additions and 58 deletions

View file

@ -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);
}

View file

@ -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 (
<div id="compose-container-outer">
<div id="compose-container" class={standalone ? 'standalone' : ''}>
@ -982,6 +984,7 @@ function Compose({
justifyContent: 'flex-end',
}}
>
<span>
<label class="toolbar-button">
<input
type="file"
@ -1041,6 +1044,17 @@ function Compose({
>
<Icon icon="poll" alt="Add poll" />
</button>{' '}
<button
type="button"
class="toolbar-button"
disabled={uiState === 'loading'}
onClick={() => {
setShowEmoji2Picker(true);
}}
>
<Icon icon="emoji2" />
</button>
</span>
<div class="spacer" />
{uiState === 'loading' ? (
<Loader abrupt />
@ -1089,6 +1103,40 @@ function Compose({
</div>
</form>
</div>
{showEmoji2Picker && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowEmoji2Picker(false);
}
}}
>
<CustomEmojisModal
masto={masto}
instance={instance}
onClose={() => {
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'));
}}
/>
</Modal>
)}
</div>
);
}
@ -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 (
<div id="custom-emojis-sheet" class="sheet">
<header>
<b>Custom emojis</b>{' '}
{uiState === 'loading' ? (
<Loader />
) : (
<small class="insignificant"> {instance}</small>
)}
</header>
<main>
<div class="custom-emojis-list">
{uiState === 'error' && (
<div class="ui-state">
<p>Error loading custom emojis</p>
</div>
)}
{uiState === 'default' &&
Object.entries(customEmojis).map(
([category, emojis]) =>
!!emojis?.length && (
<>
<div class="section-header">
{{
'--recent--': 'Recently used',
'--others--': 'Others',
}[category] || category}
</div>
<section>
{emojis.map((emoji) => (
<button
key={emoji}
type="button"
class="plain4"
onClick={() => {
onClose();
requestAnimationFrame(() => {
onSelect(`:${emoji.shortcode}:`);
});
let recentlyUsedCustomEmojis =
store.account.get('recentlyUsedCustomEmojis') ||
[];
const recentlyUsedEmojiIndex =
recentlyUsedCustomEmojis.findIndex(
(e) => e.shortcode === emoji.shortcode,
);
if (recentlyUsedEmojiIndex !== -1) {
// Move emoji to index 0
recentlyUsedCustomEmojis.splice(
recentlyUsedEmojiIndex,
1,
);
recentlyUsedCustomEmojis.unshift(emoji);
} else {
recentlyUsedCustomEmojis.unshift(emoji);
// Remove unavailable ones
recentlyUsedCustomEmojis =
recentlyUsedCustomEmojis.filter((e) =>
customEmojisList.current?.find?.(
(emoji) => emoji.shortcode === e.shortcode,
),
);
// Limit to 10
recentlyUsedCustomEmojis =
recentlyUsedCustomEmojis.slice(0, 10);
}
// Store back
store.account.set(
'recentlyUsedCustomEmojis',
recentlyUsedCustomEmojis,
);
}}
title={`:${emoji.shortcode}:`}
>
<img
src={emoji.url || emoji.staticUrl}
alt={emoji.shortcode}
width="16"
height="16"
loading="lazy"
decoding="async"
/>
</button>
))}
</section>
</>
),
)}
</div>
</main>
</div>
);
}
export default Compose;

View file

@ -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');