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/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",

View File

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

View File

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

View File

@ -9,7 +9,7 @@ const EmojiGraphic: React.FC<IEmojiGraphic> = ({ emoji }) => {
return (
<div className='flex items-center justify-center'>
<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>
);

View File

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

View File

@ -41,7 +41,9 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
return (
<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>
);
};

View File

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

View File

@ -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<JSX.Element> {
class EmojiNode extends DecoratorNode<React.ReactNode> {
__emoji: Emoji;
@ -77,12 +76,12 @@ class EmojiNode extends DecoratorNode<JSX.Element> {
}
}
decorate(): JSX.Element {
decorate(): React.ReactNode {
const emoji = this.__emoji;
if (isNativeEmoji(emoji)) {
return <Component emoji={emoji.native} alt={emoji.colons} className='emojione size-4' />;
return emoji.native;
} 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 { 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();

View File

@ -234,7 +234,6 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
skin={handleSkinTone}
emojiSize={22}
emojiButtonSize={34}
set='twitter'
theme={theme}
i18n={getI18n()}
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 { 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<any> = (props) => {
const ref = useRef(null);
useEffect(() => {
const input = { ...props, data, ref, getImageURL, getSpritesheetURL };
const input = { ...props, data, ref };
new EmojiPicker(input);
}, []);

View File

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

View File

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

View File

@ -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"