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 && (
+
+ )}
+
- )}
-
-
+
+ )}
{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>— experimental0>"
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,