diff --git a/package-lock.json b/package-lock.json index 040f1b3c68..041e85ceb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@szhsin/react-menu": "~4.2.3", "chroma-js": "~3.1.2", "compare-versions": "~6.1.1", + "emojibase-data": "^15.3.2", "fast-blurhash": "~1.1.4", "fast-equals": "~5.0.1", "fuse.js": "~7.0.0", @@ -5538,6 +5539,28 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emojibase": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-15.3.1.tgz", + "integrity": "sha512-GNsjHnG2J3Ktg684Fs/vZR/6XpOSkZPMAv85EHrr6br2RN2cJNwdS4am/3YSK3y+/gOv2kmoK3GGdahXdMxg2g==", + "peer": true, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + } + }, + "node_modules/emojibase-data": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-15.3.2.tgz", + "integrity": "sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==", + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + }, + "peerDependencies": { + "emojibase": "*" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", diff --git a/package.json b/package.json index 8ec05e59ed..c22ce26776 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@szhsin/react-menu": "~4.2.3", "chroma-js": "~3.1.2", "compare-versions": "~6.1.1", + "emojibase-data": "^15.3.2", "fast-blurhash": "~1.1.4", "fast-equals": "~5.0.1", "fuse.js": "~7.0.0", diff --git a/src/components/compose.jsx b/src/components/compose.jsx index d25d1ed9ea..d9a8148811 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -1786,11 +1786,21 @@ function autoResizeTextarea(textarea) { } } -async function _getCustomEmojis(instance, masto) { - const emojis = await masto.v1.customEmojis.list(); - const visibleEmojis = emojis.filter((e) => e.visibleInPicker); +async function _getCustomEmojis(lang, instance, masto) { + const [unicodeEmojis, customEmojis] = await Promise.all([ + fetch(`./assets/emojis/${lang}.json`).then((r) => r.json()), + masto.v1.customEmojis.list(), + ]); + + for (let emoji of unicodeEmojis.emojis) + if (emoji.group in unicodeEmojis.groups) + emoji.category = unicodeEmojis.groups[emoji.group].message; + + const visibleEmojis = customEmojis + .filter((e) => e.visibleInPicker) + .concat(unicodeEmojis.emojis); const searcher = new Fuse(visibleEmojis, { - keys: ['shortcode'], + keys: ['shortcode', 'label', 'tags'], findAllMatches: true, }); return [visibleEmojis, searcher]; @@ -1827,7 +1837,7 @@ const Textarea = forwardRef((props, ref) => { // const customEmojis = useRef(); const searcherRef = useRef(); useEffect(() => { - getCustomEmojis(instance, masto) + getCustomEmojis(i18n.locale, instance, masto) .then((r) => { const [emojis, searcher] = r; searcherRef.current = searcher; @@ -1866,13 +1876,18 @@ const Textarea = forwardRef((props, ref) => { }); let html = ''; results.forEach(({ item: emoji }) => { - const { shortcode, url } = emoji; - html += ` -
  • - - ${encodeHTML(shortcode)} + )}" width="16" height="16" alt="" loading="lazy" />`; + html += ` +
  • + ${presentation} + ${encodeHTML(shortcode || '')}
  • `; }); html += `
  • ${t`More…`}
  • `; @@ -1974,7 +1989,7 @@ const Textarea = forwardRef((props, ref) => { const { key, item } = e.detail; const { value, more } = item.dataset; if (key === ':') { - e.detail.value = value ? `:${value}:` : '​'; // zero-width space + e.detail.value = value || '​'; // zero-width space if (more) { // Prevent adding space after the above value e.detail.continue = true; @@ -3115,7 +3130,11 @@ function CustomEmojisModal({ setUIState('loading'); (async () => { try { - const [emojis, searcher] = await getCustomEmojis(instance, masto); + const [emojis, searcher] = await getCustomEmojis( + i18n.locale, + instance, + masto, + ); console.log('emojis', emojis); searcherRef.current = searcher; setCustomEmojis(emojis); @@ -3244,7 +3263,9 @@ function CustomEmojisModal({ e.preventDefault(); const emoji = matches[0]; if (emoji) { - onSelectEmoji(`:${emoji.shortcode}:`); + onSelectEmoji( + emoji.unicode ? emoji.unicode : `:${emoji.shortcode}:`, + ); } }} > @@ -3270,7 +3291,9 @@ function CustomEmojisModal({ { - onSelectEmoji(`:${emoji.shortcode}:`); + onSelectEmoji( + emoji.unicode ? emoji.unicode : `:${emoji.shortcode}:`, + ); }} showCode /> @@ -3370,23 +3393,25 @@ const CustomEmojiButton = memo(({ emoji, onClick, showCode }) => { onPointerEnter={addEdges} onFocus={addEdges} > - - {!!emoji.staticUrl && ( - {emoji.unicode}) || ( + + {!!emoji.staticUrl && ( + + )} + {emoji.shortcode} - )} - {emoji.shortcode} - + + )} {showCode && ( <> {' '} diff --git a/src/locales/en.po b/src/locales/en.po index 238dc806d1..34375472e9 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -104,7 +104,7 @@ msgstr "" #: src/components/account-info.jsx:429 #: src/components/account-info.jsx:1120 -#: src/components/compose.jsx:2591 +#: src/components/compose.jsx:2606 #: src/components/media-alt-modal.jsx:45 #: src/components/media-modal.jsx:357 #: src/components/status.jsx:1737 @@ -400,10 +400,10 @@ msgstr "" #: src/components/account-info.jsx:2094 #: src/components/account-sheet.jsx:37 #: src/components/compose.jsx:859 -#: src/components/compose.jsx:2547 -#: src/components/compose.jsx:3020 -#: src/components/compose.jsx:3228 -#: src/components/compose.jsx:3458 +#: src/components/compose.jsx:2562 +#: src/components/compose.jsx:3035 +#: src/components/compose.jsx:3247 +#: src/components/compose.jsx:3483 #: src/components/drafts.jsx:58 #: src/components/embed-modal.jsx:12 #: src/components/generic-accounts.jsx:142 @@ -681,7 +681,7 @@ msgid "Mark media as sensitive" msgstr "" #: src/components/compose.jsx:1381 -#: src/components/compose.jsx:3078 +#: src/components/compose.jsx:3093 #: src/components/shortcuts-settings.jsx:715 #: src/pages/list.jsx:359 msgid "Add" @@ -713,172 +713,172 @@ msgstr "" msgid "Failed to download GIF" msgstr "" -#: src/components/compose.jsx:1878 -#: src/components/compose.jsx:1955 +#: src/components/compose.jsx:1893 +#: src/components/compose.jsx:1970 #: src/components/nav-menu.jsx:238 msgid "More…" msgstr "" -#: src/components/compose.jsx:2360 +#: src/components/compose.jsx:2375 msgid "Uploaded" msgstr "" -#: src/components/compose.jsx:2373 +#: src/components/compose.jsx:2388 msgid "Image description" msgstr "" -#: src/components/compose.jsx:2374 +#: src/components/compose.jsx:2389 msgid "Video description" msgstr "" -#: src/components/compose.jsx:2375 +#: src/components/compose.jsx:2390 msgid "Audio description" msgstr "" -#: src/components/compose.jsx:2411 -#: src/components/compose.jsx:2431 +#: src/components/compose.jsx:2426 +#: src/components/compose.jsx:2446 msgid "File size too large. Uploading might encounter issues. Try reduce the file size from {0} to {1} or lower." msgstr "" -#: src/components/compose.jsx:2423 -#: src/components/compose.jsx:2443 +#: src/components/compose.jsx:2438 +#: src/components/compose.jsx:2458 msgid "Dimension too large. Uploading might encounter issues. Try reduce dimension from {0}×{1}px to {2}×{3}px." msgstr "" -#: src/components/compose.jsx:2451 +#: src/components/compose.jsx:2466 msgid "Frame rate too high. Uploading might encounter issues." msgstr "" -#: src/components/compose.jsx:2511 -#: src/components/compose.jsx:2761 +#: src/components/compose.jsx:2526 +#: src/components/compose.jsx:2776 #: src/components/shortcuts-settings.jsx:726 #: src/pages/catchup.jsx:1074 #: src/pages/filters.jsx:412 msgid "Remove" msgstr "" -#: src/components/compose.jsx:2528 +#: src/components/compose.jsx:2543 #: src/compose.jsx:83 msgid "Error" msgstr "" -#: src/components/compose.jsx:2553 +#: src/components/compose.jsx:2568 msgid "Edit image description" msgstr "" -#: src/components/compose.jsx:2554 +#: src/components/compose.jsx:2569 msgid "Edit video description" msgstr "" -#: src/components/compose.jsx:2555 +#: src/components/compose.jsx:2570 msgid "Edit audio description" msgstr "" -#: src/components/compose.jsx:2600 -#: src/components/compose.jsx:2649 +#: src/components/compose.jsx:2615 +#: src/components/compose.jsx:2664 msgid "Generating description. Please wait…" msgstr "" -#: src/components/compose.jsx:2620 +#: src/components/compose.jsx:2635 msgid "Failed to generate description: {0}" msgstr "" -#: src/components/compose.jsx:2621 +#: src/components/compose.jsx:2636 msgid "Failed to generate description" msgstr "" -#: src/components/compose.jsx:2633 -#: src/components/compose.jsx:2639 -#: src/components/compose.jsx:2685 +#: src/components/compose.jsx:2648 +#: src/components/compose.jsx:2654 +#: src/components/compose.jsx:2700 msgid "Generate description…" msgstr "" -#: src/components/compose.jsx:2672 +#: src/components/compose.jsx:2687 msgid "Failed to generate description{0}" msgstr "" -#: src/components/compose.jsx:2687 +#: src/components/compose.jsx:2702 msgid "({0}) <0>— experimental" msgstr "" -#: src/components/compose.jsx:2706 +#: src/components/compose.jsx:2721 msgid "Done" msgstr "" -#: src/components/compose.jsx:2742 +#: src/components/compose.jsx:2757 msgid "Choice {0}" msgstr "" -#: src/components/compose.jsx:2789 +#: src/components/compose.jsx:2804 msgid "Multiple choices" msgstr "" -#: src/components/compose.jsx:2792 +#: src/components/compose.jsx:2807 msgid "Duration" msgstr "" -#: src/components/compose.jsx:2823 +#: src/components/compose.jsx:2838 msgid "Remove poll" msgstr "" -#: src/components/compose.jsx:3037 +#: src/components/compose.jsx:3052 msgid "Search accounts" msgstr "" -#: src/components/compose.jsx:3091 +#: src/components/compose.jsx:3106 #: src/components/generic-accounts.jsx:227 msgid "Error loading accounts" msgstr "" -#: src/components/compose.jsx:3234 +#: src/components/compose.jsx:3253 msgid "Custom emojis" msgstr "" -#: src/components/compose.jsx:3254 +#: src/components/compose.jsx:3275 msgid "Search emoji" msgstr "" -#: src/components/compose.jsx:3285 +#: src/components/compose.jsx:3308 msgid "Error loading custom emojis" msgstr "" -#: src/components/compose.jsx:3296 +#: src/components/compose.jsx:3319 msgid "Recently used" msgstr "" -#: src/components/compose.jsx:3297 +#: src/components/compose.jsx:3320 msgid "Others" msgstr "" -#: src/components/compose.jsx:3335 +#: src/components/compose.jsx:3358 msgid "{0} more…" msgstr "" -#: src/components/compose.jsx:3473 +#: src/components/compose.jsx:3498 msgid "Search GIFs" msgstr "" -#: src/components/compose.jsx:3488 +#: src/components/compose.jsx:3513 msgid "Powered by GIPHY" msgstr "" -#: src/components/compose.jsx:3496 +#: src/components/compose.jsx:3521 msgid "Type to search GIFs" msgstr "" -#: src/components/compose.jsx:3594 +#: src/components/compose.jsx:3619 #: src/components/media-modal.jsx:461 #: src/components/timeline.jsx:889 msgid "Previous" msgstr "" -#: src/components/compose.jsx:3612 +#: src/components/compose.jsx:3637 #: src/components/media-modal.jsx:480 #: src/components/timeline.jsx:906 msgid "Next" msgstr "" -#: src/components/compose.jsx:3629 +#: src/components/compose.jsx:3654 msgid "Error loading GIFs" msgstr "" diff --git a/vite.config.js b/vite.config.js index d39a49f58d..f4cd113465 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ import { execSync } from 'child_process'; import fs from 'fs'; -import { resolve } from 'path'; +import { createRequire } from 'module'; +import { dirname, join, resolve } from 'path'; import { lingui } from '@lingui/vite-plugin'; import preact from '@preact/preset-vite'; @@ -40,6 +41,66 @@ const rollbarCode = fs.readFileSync( 'utf-8', ); +let emojiData; +{ + // import.meta.resolve requires Node 20.6.0/18.19.0, cannot use that yet. + const require = createRequire(import.meta.url); + const basePath = dirname(dirname( + require.resolve("emojibase-data/en/data.json") + )); + + emojiData = ALL_LOCALES.map((lang) => { + const emojiLang = [ + lang.toLowerCase(), + lang.replace(/-.*/, '').toLowerCase(), + 'en', + ].find((lang) => { + try { + return fs.statSync(join(basePath, lang)).isDirectory(); + } catch (error) { + return false; + } + }); + + const messages = JSON.parse(fs.readFileSync( + join(basePath, emojiLang, 'messages.json') + )); + const emojis = JSON.parse(fs.readFileSync( + join(basePath, emojiLang, 'compact.json') + )); + const shortcodes = JSON.parse(fs.readFileSync( + join(basePath, emojiLang, 'shortcodes', 'cldr.json') + )); + + const data = { + groups: messages.groups, + subgroups: messages.subgroups, + skinTones: messages.skinTones, + emojis: emojis.sort((emoji1, emoji2) => emoji1.order - emoji2.order).map((emoji) => { + if (emoji.hexcode in shortcodes) + emoji.shortcode = shortcodes[emoji.hexcode]; + + // Remove unused fields to keep the data compact + delete emoji.order; + delete emoji.hexcode; + + if (emoji.skins) { + emoji.skins = emoji.skins.sort((skin1, skin2) => skin1.order - skin2.order) + .map((skin) => skin.unicode); + } + + return emoji; + }), + }; + + return generateFile({ + type: 'json', + output: `./assets/emojis/${lang}.json`, + data, + }); + }); +} + // https://vitejs.dev/config/ export default defineConfig({ base: './', @@ -154,6 +215,7 @@ export default defineConfig({ detailed: true, brotli: true, }), + ...emojiData, ], build: { sourcemap: true,