Switch to native emojis

This commit is contained in:
Alex Gleason 2024-11-15 17:52:02 -06:00
parent dd9d171ade
commit d1738273ed
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
18 changed files with 58 additions and 160 deletions

View File

@ -70,7 +70,6 @@
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.59.13", "@tanstack/react-query": "^5.59.13",
"@twemoji/svg": "^15.0.0",
"@types/escape-html": "^1.0.1", "@types/escape-html": "^1.0.1",
"@types/http-link-header": "^1.0.3", "@types/http-link-header": "^1.0.3",
"@types/leaflet": "^1.8.0", "@types/leaflet": "^1.8.0",
@ -98,7 +97,6 @@
"comlink": "^4.4.1", "comlink": "^4.4.1",
"cssnano": "^6.0.0", "cssnano": "^6.0.0",
"detect-passive-events": "^2.0.0", "detect-passive-events": "^2.0.0",
"emoji-datasource": "15.0.1",
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"eslint-plugin-formatjs": "^5.2.2", "eslint-plugin-formatjs": "^5.2.2",

View File

@ -3,7 +3,6 @@ import { createSelector } from 'reselect';
import { getHost } from 'soapbox/actions/instance.ts'; import { getHost } from 'soapbox/actions/instance.ts';
import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts'; import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts';
import KVStore from 'soapbox/storage/kv-store.ts'; import KVStore from 'soapbox/storage/kv-store.ts';
import { removeVS16s } from 'soapbox/utils/emoji.ts';
import { getFeatures } from 'soapbox/utils/features.ts'; import { getFeatures } from 'soapbox/utils/features.ts';
import api from '../api/index.ts'; import api from '../api/index.ts';
@ -29,12 +28,6 @@ const getSoapboxConfig = createSelector([
if (soapbox.get('displayFqn') === undefined) { if (soapbox.get('displayFqn') === undefined) {
soapboxConfig.set('displayFqn', features.federating); 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));
}
}); });
}); });

View File

@ -214,11 +214,13 @@ const Account = ({
<LinkEl className='rounded-full' {...linkProps}> <LinkEl className='rounded-full' {...linkProps}>
<Avatar src={account.avatar} size={avatarSize} /> <Avatar src={account.avatar} size={avatarSize} />
{emoji && ( {emoji && (
<Emoji <div className='absolute -right-1.5 bottom-0'>
className='absolute -right-1.5 bottom-0 size-5' {emojiUrl ? (
emoji={emoji} <img className='size-5' src={emojiUrl} alt={emoji} />
src={emojiUrl} ) : (
/> <Emoji size={20} emoji={emoji} />
)}
</div>
)} )}
</LinkEl> </LinkEl>
</ProfilePopper> </ProfilePopper>

View File

@ -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 { isCustomEmoji } from 'soapbox/features/emoji/index.ts';
import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; import unicodeMapping from 'soapbox/features/emoji/mapping.ts';
@ -8,11 +10,10 @@ interface IAutosuggestEmoji {
} }
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => { const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
let url, alt; let elem: React.ReactNode;
if (isCustomEmoji(emoji)) { if (isCustomEmoji(emoji)) {
url = emoji.imageUrl; elem = <img className='emojione mr-2 block size-4' src={emoji.imageUrl} alt={emoji.colons} />;
alt = emoji.colons;
} else { } else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
@ -20,19 +21,14 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
return null; return null;
} }
url = `/packs/emoji/${mapping.unified}.svg`; elem = <EmojiComponent emoji={emoji.native} size={16} />;
alt = emoji.native;
} }
return ( return (
<div className='flex flex-row items-center justify-start text-[14px] leading-[18px]' data-testid='emoji'> <HStack space={2} alignItems='center' justifyContent='start' className='text-[14px] leading-[18px]' data-testid='emoji'>
<img {elem}
className='emojione mr-2 block size-4' <span>{emoji.colons}</span>
src={url} </HStack>
alt={alt}
/>
{emoji.colons}
</div>
); );
}; };

View File

@ -9,7 +9,7 @@ const EmojiGraphic: React.FC<IEmojiGraphic> = ({ emoji }) => {
return ( return (
<div className='flex items-center justify-center'> <div className='flex items-center justify-center'>
<div className='rounded-full bg-gray-100 p-8 dark:bg-gray-800'> <div className='rounded-full bg-gray-100 p-8 dark:bg-gray-800'>
<Emoji className='size-24' emoji={emoji} /> <Emoji size={96} emoji={emoji} />
</div> </div>
</div> </div>
); );

View File

@ -46,8 +46,12 @@ const StatusActionButton = forwardRef<HTMLButtonElement, IStatusActionButton>((p
const renderIcon = () => { const renderIcon = () => {
if (emoji) { if (emoji) {
return ( return (
<span className='flex size-6 items-center justify-center'> <span className='flex size-6 items-center justify-center p-0.5'>
<Emoji className='size-full p-0.5' emoji={emoji.name} src={emoji.url} /> {emoji.url ? (
<img src={emoji.url} alt={emoji.name} className='w-full' />
) : (
<Emoji size={18} emoji={emoji.name} />
)}
</span> </span>
); );
} else { } else {

View File

@ -41,7 +41,9 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
return ( return (
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}> <button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
<EmojiComponent className='size-6 duration-100 hover:scale-110' emoji={emoji} /> <div className='flex items-center justify-center duration-100 hover:scale-110'>
<EmojiComponent size={24} emoji={emoji} />
</div>
</button> </button>
); );
}; };

View File

@ -1,30 +1,19 @@
import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji.ts'; interface IEmoji {
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
/** Unicode emoji character. */ /** Unicode emoji character. */
emoji?: string; emoji: string;
/** Size to render the emoji. */
size?: number;
} }
/** A single emoji image. */ /** A single emoji image. */
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => { const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
const { emoji, alt, src, ...rest } = props; const { emoji, size = 16 } = props;
const px = `${size}px`;
let filename;
if (emoji) {
const codepoints = toCodePoints(removeVS16s(emoji));
filename = codepoints.join('-');
}
if (!filename && !src) return null;
return ( return (
<img <div className='inline-flex items-center justify-center font-emoji leading-[0]' style={{ width: px, height: px, fontSize: px }}>
draggable='false' {emoji}
alt={alt || emoji} </div>
src={src || `/packs/emoji/${filename}.svg`}
{...rest}
/>
); );
}; };

View File

@ -1,6 +1,5 @@
import { $applyNodeReplacement, DecoratorNode } from 'lexical'; import { $applyNodeReplacement, DecoratorNode } from 'lexical';
import Component from 'soapbox/components/ui/emoji.tsx';
import { isNativeEmoji, type Emoji } from 'soapbox/features/emoji/index.ts'; import { isNativeEmoji, type Emoji } from 'soapbox/features/emoji/index.ts';
import type { import type {
@ -17,7 +16,7 @@ type SerializedEmojiNode = Spread<{
version: 1; version: 1;
}, SerializedLexicalNode>; }, SerializedLexicalNode>;
class EmojiNode extends DecoratorNode<JSX.Element> { class EmojiNode extends DecoratorNode<React.ReactNode> {
__emoji: Emoji; __emoji: Emoji;
@ -77,12 +76,12 @@ class EmojiNode extends DecoratorNode<JSX.Element> {
} }
} }
decorate(): JSX.Element { decorate(): React.ReactNode {
const emoji = this.__emoji; const emoji = this.__emoji;
if (isNativeEmoji(emoji)) { if (isNativeEmoji(emoji)) {
return <Component emoji={emoji.native} alt={emoji.colons} className='emojione size-4' />; return emoji.native;
} else { } else {
return <Component src={emoji.imageUrl} alt={emoji.colons} className='emojione size-4' />; return <img src={emoji.imageUrl} alt={emoji.colons} className='emojione size-4' />;
} }
} }

View File

@ -36,6 +36,7 @@ import ReactDOM from 'react-dom';
import { clearComposeSuggestions, fetchComposeSuggestions } from 'soapbox/actions/compose.ts'; import { clearComposeSuggestions, fetchComposeSuggestions } from 'soapbox/actions/compose.ts';
import { chooseEmoji } from 'soapbox/actions/emojis.ts'; import { chooseEmoji } from 'soapbox/actions/emojis.ts';
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji.tsx'; import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji.tsx';
import { isNativeEmoji } from 'soapbox/features/emoji/index.ts';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useCompose } from 'soapbox/hooks/useCompose.ts'; import { useCompose } from 'soapbox/hooks/useCompose.ts';
import { selectAccount } from 'soapbox/selectors/index.ts'; import { selectAccount } from 'soapbox/selectors/index.ts';
@ -316,7 +317,11 @@ const AutosuggestPlugin = ({
if (typeof suggestion === 'object') { if (typeof suggestion === 'object') {
if (!suggestion.id) return; if (!suggestion.id) return;
dispatch(chooseEmoji(suggestion)); dispatch(chooseEmoji(suggestion));
if (isNativeEmoji(suggestion)) {
replaceMatch(new TextNode(suggestion.native));
} else {
replaceMatch($createEmojiNode(suggestion)); replaceMatch($createEmojiNode(suggestion));
}
} else if (suggestion[0] === '#') { } else if (suggestion[0] === '#') {
(node as TextNode).setTextContent(`${suggestion} `); (node as TextNode).setTextContent(`${suggestion} `);
node.select(); node.select();

View File

@ -234,7 +234,6 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
skin={handleSkinTone} skin={handleSkinTone}
emojiSize={22} emojiSize={22}
emojiButtonSize={34} emojiButtonSize={34}
set='twitter'
theme={theme} theme={theme}
i18n={getI18n()} i18n={getI18n()}
skinTonePosition='search' skinTonePosition='search'

View File

@ -1,20 +1,13 @@
import spriteSheet from 'emoji-datasource/img/twitter/sheets/32.png';
import { Picker as EmojiPicker } from 'emoji-mart'; import { Picker as EmojiPicker } from 'emoji-mart';
import { useRef, useEffect } from 'react'; import { useRef, useEffect } from 'react';
import data from '../data.ts'; import data from '../data.ts';
const getSpritesheetURL = () => spriteSheet;
const getImageURL = (_set: string, name: string) => {
return `/packs/emoji/${name}.svg`;
};
const Picker: React.FC<any> = (props) => { const Picker: React.FC<any> = (props) => {
const ref = useRef(null); const ref = useRef(null);
useEffect(() => { useEffect(() => {
const input = { ...props, data, ref, getImageURL, getSpritesheetURL }; const input = { ...props, data, ref };
new EmojiPicker(input); new EmojiPicker(input);
}, []); }, []);

View File

@ -67,9 +67,7 @@ const convertCustom = (shortname: string, filename: string) => {
}; };
const convertUnicode = (c: string) => { const convertUnicode = (c: string) => {
const { unified, shortcode } = unicodeMapping[c]; return c;
return `<img draggable="false" class="emojione" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
}; };
const convertEmoji = (str: string, customEmojis: any) => { const convertEmoji = (str: string, customEmojis: any) => {

View File

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

View File

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

View File

@ -27,7 +27,7 @@ const config: Config = {
base: '0.9375rem', base: '0.9375rem',
}, },
fontFamily: { fontFamily: {
'sans': [ sans: [
'Soapbox i18n', 'Soapbox i18n',
'Inter', 'Inter',
'ui-sans-serif', 'ui-sans-serif',
@ -45,11 +45,20 @@ const config: Config = {
'Segoe UI Symbol', 'Segoe UI Symbol',
'Noto Color Emoji', 'Noto Color Emoji',
], ],
'mono': [ mono: [
'Roboto Mono', 'Roboto Mono',
'ui-monospace', 'ui-monospace',
'mono', 'mono',
], ],
emoji: [
'Segoe UI Emoji',
'Segoe UI Symbol',
'Segoe UI',
'Apple Color Emoji',
'Twemoji Mozilla',
'Noto Color Emoji',
'Android Emoji',
],
}, },
spacing: { spacing: {
'4.5': '1.125rem', '4.5': '1.125rem',

View File

@ -69,9 +69,6 @@ export default defineConfig(() => {
}), }),
viteStaticCopy({ viteStaticCopy({
targets: [{ targets: [{
src: './node_modules/@twemoji/svg/*',
dest: 'packs/emoji/',
}, {
src: './src/instance', src: './src/instance',
dest: '.', dest: '.',
}, { }, {

View File

@ -2525,11 +2525,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== 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": "@types/aria-query@^5.0.1":
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" 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" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576"
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== 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: emoji-mart@^5.6.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"