Switch to native emojis
This commit is contained in:
parent
dd9d171ade
commit
d1738273ed
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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' />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -234,7 +234,6 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
|||
skin={handleSkinTone}
|
||||
emojiSize={22}
|
||||
emojiButtonSize={34}
|
||||
set='twitter'
|
||||
theme={theme}
|
||||
i18n={getI18n()}
|
||||
skinTonePosition='search'
|
||||
|
|
|
@ -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);
|
||||
}, []);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -69,9 +69,6 @@ export default defineConfig(() => {
|
|||
}),
|
||||
viteStaticCopy({
|
||||
targets: [{
|
||||
src: './node_modules/@twemoji/svg/*',
|
||||
dest: 'packs/emoji/',
|
||||
}, {
|
||||
src: './src/instance',
|
||||
dest: '.',
|
||||
}, {
|
||||
|
|
10
yarn.lock
10
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"
|
||||
|
|
Loading…
Reference in New Issue