From d1738273ed9195eb338e5eea67c06ff3b3ff083c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 15 Nov 2024 17:52:02 -0600 Subject: [PATCH] Switch to native emojis --- package.json | 2 - src/actions/soapbox.ts | 7 ---- src/components/account.tsx | 12 +++--- src/components/autosuggest-emoji.tsx | 22 ++++------ src/components/emoji-graphic.tsx | 2 +- src/components/status-action-button.tsx | 8 +++- src/components/ui/emoji-selector.tsx | 4 +- src/components/ui/emoji.tsx | 29 ++++--------- .../compose/editor/nodes/emoji-node.tsx | 9 ++-- .../editor/plugins/autosuggest-plugin.tsx | 7 +++- .../components/emoji-picker-dropdown.tsx | 1 - .../emoji/components/emoji-picker.tsx | 9 +--- src/features/emoji/index.ts | 4 +- src/utils/emoji.test.ts | 41 ------------------- src/utils/emoji.ts | 35 ---------------- tailwind.config.ts | 13 +++++- vite.config.ts | 3 -- yarn.lock | 10 ----- 18 files changed, 58 insertions(+), 160 deletions(-) delete mode 100644 src/utils/emoji.test.ts delete mode 100644 src/utils/emoji.ts diff --git a/package.json b/package.json index 8fae41ee1..b49929f42 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tanstack/react-query": "^5.59.13", - "@twemoji/svg": "^15.0.0", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", @@ -98,7 +97,6 @@ "comlink": "^4.4.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", - "emoji-datasource": "15.0.1", "emoji-mart": "^5.6.0", "escape-html": "^1.0.3", "eslint-plugin-formatjs": "^5.2.2", diff --git a/src/actions/soapbox.ts b/src/actions/soapbox.ts index cee62ab4f..cd0170c90 100644 --- a/src/actions/soapbox.ts +++ b/src/actions/soapbox.ts @@ -3,7 +3,6 @@ import { createSelector } from 'reselect'; import { getHost } from 'soapbox/actions/instance.ts'; import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts'; import KVStore from 'soapbox/storage/kv-store.ts'; -import { removeVS16s } from 'soapbox/utils/emoji.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; import api from '../api/index.ts'; @@ -29,12 +28,6 @@ const getSoapboxConfig = createSelector([ if (soapbox.get('displayFqn') === undefined) { soapboxConfig.set('displayFqn', features.federating); } - - // If RGI reacts aren't supported, strip VS16s - // https://git.pleroma.social/pleroma/pleroma/-/issues/2355 - if (features.emojiReactsNonRGI) { - soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s)); - } }); }); diff --git a/src/components/account.tsx b/src/components/account.tsx index 4dba320de..f9ffc41cf 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -214,11 +214,13 @@ const Account = ({ {emoji && ( - +
+ {emojiUrl ? ( + {emoji} + ) : ( + + )} +
)}
diff --git a/src/components/autosuggest-emoji.tsx b/src/components/autosuggest-emoji.tsx index fb4a4b4fc..411526b9c 100644 --- a/src/components/autosuggest-emoji.tsx +++ b/src/components/autosuggest-emoji.tsx @@ -1,3 +1,5 @@ +import EmojiComponent from 'soapbox/components/ui/emoji.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; import { isCustomEmoji } from 'soapbox/features/emoji/index.ts'; import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; @@ -8,11 +10,10 @@ interface IAutosuggestEmoji { } const AutosuggestEmoji: React.FC = ({ emoji }) => { - let url, alt; + let elem: React.ReactNode; if (isCustomEmoji(emoji)) { - url = emoji.imageUrl; - alt = emoji.colons; + elem = {emoji.colons}; } else { const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; @@ -20,19 +21,14 @@ const AutosuggestEmoji: React.FC = ({ emoji }) => { return null; } - url = `/packs/emoji/${mapping.unified}.svg`; - alt = emoji.native; + elem = ; } return ( -
- {alt} - {emoji.colons} -
+ + {elem} + {emoji.colons} + ); }; diff --git a/src/components/emoji-graphic.tsx b/src/components/emoji-graphic.tsx index 7facaa4ad..207fb409d 100644 --- a/src/components/emoji-graphic.tsx +++ b/src/components/emoji-graphic.tsx @@ -9,7 +9,7 @@ const EmojiGraphic: React.FC = ({ emoji }) => { return (
- +
); diff --git a/src/components/status-action-button.tsx b/src/components/status-action-button.tsx index b72218b4d..2819adaa3 100644 --- a/src/components/status-action-button.tsx +++ b/src/components/status-action-button.tsx @@ -46,8 +46,12 @@ const StatusActionButton = forwardRef((p const renderIcon = () => { if (emoji) { return ( - - + + {emoji.url ? ( + {emoji.name} + ) : ( + + )} ); } else { diff --git a/src/components/ui/emoji-selector.tsx b/src/components/ui/emoji-selector.tsx index 2487bb127..6d4b716e0 100644 --- a/src/components/ui/emoji-selector.tsx +++ b/src/components/ui/emoji-selector.tsx @@ -41,7 +41,9 @@ const EmojiButton: React.FC = ({ emoji, className, onClick, tabInd return ( ); }; diff --git a/src/components/ui/emoji.tsx b/src/components/ui/emoji.tsx index da3449ccb..75cc4d111 100644 --- a/src/components/ui/emoji.tsx +++ b/src/components/ui/emoji.tsx @@ -1,30 +1,19 @@ -import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji.ts'; - -interface IEmoji extends React.ImgHTMLAttributes { +interface IEmoji { /** Unicode emoji character. */ - emoji?: string; + emoji: string; + /** Size to render the emoji. */ + size?: number; } /** A single emoji image. */ const Emoji: React.FC = (props): JSX.Element | null => { - const { emoji, alt, src, ...rest } = props; - - let filename; - - if (emoji) { - const codepoints = toCodePoints(removeVS16s(emoji)); - filename = codepoints.join('-'); - } - - if (!filename && !src) return null; + const { emoji, size = 16 } = props; + const px = `${size}px`; return ( - {alt +
+ {emoji} +
); }; diff --git a/src/features/compose/editor/nodes/emoji-node.tsx b/src/features/compose/editor/nodes/emoji-node.tsx index 4c7155d00..d29f24af2 100644 --- a/src/features/compose/editor/nodes/emoji-node.tsx +++ b/src/features/compose/editor/nodes/emoji-node.tsx @@ -1,6 +1,5 @@ import { $applyNodeReplacement, DecoratorNode } from 'lexical'; -import Component from 'soapbox/components/ui/emoji.tsx'; import { isNativeEmoji, type Emoji } from 'soapbox/features/emoji/index.ts'; import type { @@ -17,7 +16,7 @@ type SerializedEmojiNode = Spread<{ version: 1; }, SerializedLexicalNode>; -class EmojiNode extends DecoratorNode { +class EmojiNode extends DecoratorNode { __emoji: Emoji; @@ -77,12 +76,12 @@ class EmojiNode extends DecoratorNode { } } - decorate(): JSX.Element { + decorate(): React.ReactNode { const emoji = this.__emoji; if (isNativeEmoji(emoji)) { - return ; + return emoji.native; } else { - return ; + return {emoji.colons}; } } diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 6efc85369..af5eb3e66 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -36,6 +36,7 @@ import ReactDOM from 'react-dom'; import { clearComposeSuggestions, fetchComposeSuggestions } from 'soapbox/actions/compose.ts'; import { chooseEmoji } from 'soapbox/actions/emojis.ts'; import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji.tsx'; +import { isNativeEmoji } from 'soapbox/features/emoji/index.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useCompose } from 'soapbox/hooks/useCompose.ts'; import { selectAccount } from 'soapbox/selectors/index.ts'; @@ -316,7 +317,11 @@ const AutosuggestPlugin = ({ if (typeof suggestion === 'object') { if (!suggestion.id) return; dispatch(chooseEmoji(suggestion)); - replaceMatch($createEmojiNode(suggestion)); + if (isNativeEmoji(suggestion)) { + replaceMatch(new TextNode(suggestion.native)); + } else { + replaceMatch($createEmojiNode(suggestion)); + } } else if (suggestion[0] === '#') { (node as TextNode).setTextContent(`${suggestion} `); node.select(); diff --git a/src/features/emoji/components/emoji-picker-dropdown.tsx b/src/features/emoji/components/emoji-picker-dropdown.tsx index bdae364b3..3a528c267 100644 --- a/src/features/emoji/components/emoji-picker-dropdown.tsx +++ b/src/features/emoji/components/emoji-picker-dropdown.tsx @@ -234,7 +234,6 @@ const EmojiPickerDropdown: React.FC = ({ skin={handleSkinTone} emojiSize={22} emojiButtonSize={34} - set='twitter' theme={theme} i18n={getI18n()} skinTonePosition='search' diff --git a/src/features/emoji/components/emoji-picker.tsx b/src/features/emoji/components/emoji-picker.tsx index a6dc860ff..3407c76c8 100644 --- a/src/features/emoji/components/emoji-picker.tsx +++ b/src/features/emoji/components/emoji-picker.tsx @@ -1,20 +1,13 @@ -import spriteSheet from 'emoji-datasource/img/twitter/sheets/32.png'; import { Picker as EmojiPicker } from 'emoji-mart'; import { useRef, useEffect } from 'react'; import data from '../data.ts'; -const getSpritesheetURL = () => spriteSheet; - -const getImageURL = (_set: string, name: string) => { - return `/packs/emoji/${name}.svg`; -}; - const Picker: React.FC = (props) => { const ref = useRef(null); useEffect(() => { - const input = { ...props, data, ref, getImageURL, getSpritesheetURL }; + const input = { ...props, data, ref }; new EmojiPicker(input); }, []); diff --git a/src/features/emoji/index.ts b/src/features/emoji/index.ts index 2c4995dae..1ff9f555d 100644 --- a/src/features/emoji/index.ts +++ b/src/features/emoji/index.ts @@ -67,9 +67,7 @@ const convertCustom = (shortname: string, filename: string) => { }; const convertUnicode = (c: string) => { - const { unified, shortcode } = unicodeMapping[c]; - - return `${c}`; + return c; }; const convertEmoji = (str: string, customEmojis: any) => { diff --git a/src/utils/emoji.test.ts b/src/utils/emoji.test.ts deleted file mode 100644 index 7587e57a5..000000000 --- a/src/utils/emoji.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - removeVS16s, - toCodePoints, -} from './emoji.ts'; - -const ASCII_HEART = '❀'; // '\u2764\uFE0F' -const RED_HEART_RGI = '❀️'; // '\u2764' -const JOY = 'πŸ˜‚'; - -describe('removeVS16s()', () => { - it('removes Variation Selector-16 characters from emoji', () => { - // Sanity check - expect(ASCII_HEART).not.toBe(RED_HEART_RGI); - - // It normalizes an emoji with VS16s - expect(removeVS16s(RED_HEART_RGI)).toBe(ASCII_HEART); - - // Leaves a regular emoji alone - expect(removeVS16s(JOY)).toBe(JOY); - }); -}); - -describe('toCodePoints()', () => { - it('converts a plain emoji', () => { - expect(toCodePoints('πŸ˜‚')).toEqual(['1f602']); - }); - - it('converts a VS16 emoji', () => { - expect(toCodePoints(RED_HEART_RGI)).toEqual(['2764', 'fe0f']); - }); - - it('converts an ASCII character', () => { - expect(toCodePoints(ASCII_HEART)).toEqual(['2764']); - }); - - it('converts a sequence emoji', () => { - expect(toCodePoints('πŸ‡ΊπŸ‡Έ')).toEqual(['1f1fa', '1f1f8']); - }); -}); diff --git a/src/utils/emoji.ts b/src/utils/emoji.ts deleted file mode 100644 index 1d6da69d1..000000000 --- a/src/utils/emoji.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Taken from twemoji-parser -// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js - -/** Remove Variation Selector-16 characters from emoji */ -// https://emojipedia.org/variation-selector-16/ -const removeVS16s = (rawEmoji: string): string => { - const vs16RegExp = /\uFE0F/g; - const zeroWidthJoiner = String.fromCharCode(0x200d); - return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji; -}; - -/** Convert emoji into an array of Unicode codepoints */ -const toCodePoints = (unicodeSurrogates: string): string[] => { - const points = []; - let char = 0; - let previous = 0; - let i = 0; - while (i < unicodeSurrogates.length) { - char = unicodeSurrogates.charCodeAt(i++); - if (previous) { - points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16)); - previous = 0; - } else if (char > 0xd800 && char <= 0xdbff) { - previous = char; - } else { - points.push(char.toString(16)); - } - } - return points; -}; - -export { - removeVS16s, - toCodePoints, -}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 50c0d9e3d..8e197193e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -27,7 +27,7 @@ const config: Config = { base: '0.9375rem', }, fontFamily: { - 'sans': [ + sans: [ 'Soapbox i18n', 'Inter', 'ui-sans-serif', @@ -45,11 +45,20 @@ const config: Config = { 'Segoe UI Symbol', 'Noto Color Emoji', ], - 'mono': [ + mono: [ 'Roboto Mono', 'ui-monospace', 'mono', ], + emoji: [ + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Segoe UI', + 'Apple Color Emoji', + 'Twemoji Mozilla', + 'Noto Color Emoji', + 'Android Emoji', + ], }, spacing: { '4.5': '1.125rem', diff --git a/vite.config.ts b/vite.config.ts index c056a0a4d..7ca8ca508 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,9 +69,6 @@ export default defineConfig(() => { }), viteStaticCopy({ targets: [{ - src: './node_modules/@twemoji/svg/*', - dest: 'packs/emoji/', - }, { src: './src/instance', dest: '.', }, { diff --git a/yarn.lock b/yarn.lock index e1add8d3e..9a05530f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2525,11 +2525,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@twemoji/svg@^15.0.0": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@twemoji/svg/-/svg-15.0.0.tgz#0e3828c654726f1848fe11f31ef4e8a75854cc7f" - integrity sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w== - "@types/aria-query@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" @@ -4132,11 +4127,6 @@ electron-to-chromium@^1.5.28: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576" integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== -emoji-datasource@15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-15.0.1.tgz#6cc7676e4d48d7559c2e068ffcacf84ec653584c" - integrity sha512-aF5Q6LCKXzJzpG4K0ETiItuzz0xLYxNexR9qWw45/shuuEDWZkOIbeGHA23uopOSYA/LmeZIXIFsySCx+YKg2g== - emoji-mart@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"