Error loading custom emojis
--
+ {matches.map((emoji) => (
+
-
+
{ + onSelectEmoji(`:${emoji.shortcode}:`); + }} + showCode + /> +
+ ))}
+
Error loading custom emojis
+diff --git a/package-lock.json b/package-lock.json index 8d3ce96f..ac84dd3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-equals": "~5.0.1", + "fuse.js": "~7.0.0", "html-prettify": "^1.0.7", "idb-keyval": "~6.2.1", "just-debounce-it": "~3.2.0", @@ -5130,6 +5131,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index c8485d51..907f901f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-equals": "~5.0.1", + "fuse.js": "~7.0.0", "html-prettify": "^1.0.7", "idb-keyval": "~6.2.1", "just-debounce-it": "~3.2.0", diff --git a/src/components/compose.css b/src/components/compose.css index 407f172f..220bcf4d 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -597,41 +597,123 @@ #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(1px); -} -#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); + + header { + .loader-container { + margin: 0; + } + + form { + margin: 8px 0 0; + + input { + width: 100%; + min-width: 0; + font-size: 0.8em; + } + } + } + + main { + mask-image: none; + min-height: 40vh; + padding-bottom: 88px; + } + + .custom-emojis-matches { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + } + + .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-color); + z-index: 1; + } + section { + display: flex; + flex-wrap: wrap; + } + button { + color: var(--text-color); + border-radius: 8px; + background-image: radial-gradient( + closest-side, + var(--img-bg-color), + transparent + ); + text-shadow: 0 1px 0 var(--bg-color); + position: relative; + min-width: 44px; + min-height: 44px; + font-variant-numeric: slashed-zero; + font-feature-settings: 'ss01'; + + &[data-title]:after { + max-width: 50vw; + pointer-events: none; + position: absolute; + content: attr(data-title); + left: 50%; + top: 0; + background-color: var(--bg-color); + padding: 2px 4px; + border-radius: 4px; + font-size: 12px; + border: 1px solid var(--text-color); + transform: translate(-50%, -110%); + opacity: 0; + transition: opacity 0.1s ease-out 0.1s; + font-family: var(--monospace-font); + line-height: 1; + } + &.edge-left[data-title]:after { + left: 0; + transform: translate(0, -110%); + } + &.edge-right[data-title]:after { + left: 100%; + transform: translate(-100%, -110%); + } + + &:is(:hover, :focus) { + z-index: 1; + filter: none; + background-color: var(--bg-faded-color); + + &[data-title]:after { + opacity: 1; + } + } + + img { + transition: transform 0.1s ease-out; + } + + &:is(:hover, :focus) img { + transform: scale(2); + } + &.edge-left img { + transform-origin: left center; + } + &.edge-right img { + transform-origin: right center; + } + + code { + font-size: 0.8em; + } + } + } } .compose-field-container { diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 49f9a847..12045bd8 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -3,8 +3,16 @@ import './compose.css'; import '@github/text-expander-element'; import { MenuItem } from '@szhsin/react-menu'; import { deepEqual } from 'fast-equals'; +import Fuse from 'fuse.js'; +import { memo } from 'preact/compat'; import { forwardRef } from 'preact/compat'; -import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import stringLength from 'string-length'; import { uid } from 'uid/single'; @@ -21,6 +29,7 @@ import db from '../utils/db'; import emojifyText from '../utils/emojify-text'; import localeMatch from '../utils/locale-match'; import openCompose from '../utils/open-compose'; +import pmem from '../utils/pmem'; import shortenNumber from '../utils/shorten-number'; import showToast from '../utils/show-toast'; import states, { saveStatus } from '../utils/states'; @@ -181,6 +190,8 @@ function highlightText(text, { maxCharacters = Infinity }) { const rtf = new Intl.RelativeTimeFormat(); +const CUSTOM_EMOJIS_COUNT = 100; + function Compose({ onClose, replyToStatus, @@ -1423,25 +1434,40 @@ function autoResizeTextarea(textarea) { } } +async function _getCustomEmojis(instance, masto) { + const emojis = await masto.v1.customEmojis.list(); + const visibleEmojis = emojis.filter((e) => e.visibleInPicker); + const searcher = new Fuse(visibleEmojis, { + keys: ['shortcode'], + findAllMatches: true, + }); + return [visibleEmojis, searcher]; +} +const getCustomEmojis = pmem(_getCustomEmojis, { + // Limit by time to reduce memory usage + // Cached by instance + matchesArg: (cacheKeyArg, keyArg) => cacheKeyArg.instance === keyArg.instance, + maxAge: 30 * 60 * 1000, // 30 minutes +}); + const Textarea = forwardRef((props, ref) => { - const { masto } = api(); + const { masto, instance } = api(); const [text, setText] = useState(ref.current?.value || ''); const { maxCharacters, performSearch = () => {}, ...textareaProps } = props; // const snapStates = useSnapshot(states); // const charCount = snapStates.composerCharacterCount; - const customEmojis = useRef(); + // const customEmojis = useRef(); + const searcherRef = useRef(); useEffect(() => { - (async () => { - try { - const emojis = await masto.v1.customEmojis.list(); - console.log({ emojis }); - customEmojis.current = emojis; - } catch (e) { - // silent fail + getCustomEmojis(instance, masto) + .then((r) => { + const [emojis, searcher] = r; + searcherRef.current = searcher; + }) + .catch((e) => { console.error(e); - } - })(); + }); }, []); const textExpanderRef = useRef(); @@ -1467,23 +1493,26 @@ const Textarea = forwardRef((props, ref) => { // const emojis = customEmojis.current.filter((emoji) => // emoji.shortcode.startsWith(text), // ); - const emojis = filterShortcodes(customEmojis.current, text); + // const emojis = filterShortcodes(customEmojis.current, text); + const results = searcherRef.current?.search(text, { + limit: 5, + }); let html = ''; - emojis.forEach((emoji) => { + results.forEach(({ item: emoji }) => { const { shortcode, url } = emoji; html += `
Error loading custom emojis
-Error loading custom emojis
+