Merge branch 'main' into css-to-tailwind

This commit is contained in:
danidfra 2024-11-16 18:16:48 -03:00
commit 864a377c8c
32 changed files with 143 additions and 284 deletions

View File

@ -69,7 +69,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",
@ -97,7 +96,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

@ -102,10 +102,6 @@ body.underline-links [data-markup] a {
@apply underline;
}
[data-markup].big-emoji img.emojione {
@apply inline w-9 h-9 p-1;
}
[data-markup] .status-link {
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
}

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

@ -1,11 +1,11 @@
import chevronRightIcon from '@tabler/icons/outline/chevron-right.svg';
import clsx from 'clsx';
import graphemesplit from 'graphemesplit';
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
import { useState, useRef, useLayoutEffect, useMemo, memo } from 'react';
import { FormattedMessage } from 'react-intl';
import Icon from 'soapbox/components/icon.tsx';
import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content.ts';
import { getTextDirection } from '../utils/rtl.ts';
@ -49,10 +49,14 @@ const StatusContent: React.FC<IStatusContent> = ({
textSize = 'md',
}) => {
const [collapsed, setCollapsed] = useState(false);
const [onlyEmoji, setOnlyEmoji] = useState(false);
const node = useRef<HTMLDivElement>(null);
const isOnlyEmoji = useMemo(() => {
const textContent = new DOMParser().parseFromString(status.contentHtml, 'text/html').body.firstChild?.textContent;
return Boolean(textContent && /^\p{Extended_Pictographic}+$/u.test(textContent) && (graphemesplit(textContent).length <= BIG_EMOJI_LIMIT));
}, [status.contentHtml]);
const maybeSetCollapsed = (): void => {
if (!node.current) return;
@ -63,18 +67,8 @@ const StatusContent: React.FC<IStatusContent> = ({
}
};
const maybeSetOnlyEmoji = (): void => {
if (!node.current) return;
const only = isOnlyEmoji(node.current, BIG_EMOJI_LIMIT, true);
if (only !== onlyEmoji) {
setOnlyEmoji(only);
}
};
useLayoutEffect(() => {
maybeSetCollapsed();
maybeSetOnlyEmoji();
});
const parsedHtml = useMemo((): string => {
@ -149,7 +143,7 @@ const StatusContent: React.FC<IStatusContent> = ({
'cursor-pointer': onClick,
'whitespace-normal': withSpoiler,
'max-h-[300px]': collapsed,
'leading-normal big-emoji': onlyEmoji,
'leading-normal !text-4xl': isOnlyEmoji,
});
if (onClick) {
@ -184,7 +178,7 @@ const StatusContent: React.FC<IStatusContent> = ({
tabIndex={0}
key='content'
className={clsx(baseClassName, {
'leading-normal big-emoji': onlyEmoji,
'leading-normal !text-4xl': isOnlyEmoji,
})}
direction={direction}
lang={status.language || undefined}

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

@ -51,24 +51,28 @@ const BookmarkFolders: React.FC = () => {
</HStack>
}
/>
{bookmarkFolders?.map((folder) => (
<ListItem
key={folder.id}
to={`/bookmarks/${folder.id}`}
label={
<HStack alignItems='center' space={2}>
{folder.emoji ? (
<Emoji
emoji={folder.emoji}
src={folder.emoji_url || undefined}
className='size-5 flex-none'
/>
) : <Icon src={folderIcon} size={20} />}
<span>{folder.name}</span>
</HStack>
}
/>
))}
{bookmarkFolders?.map((folder) => {
let icon = <Icon src={folderIcon} size={20} />;
if (folder.emoji_url) {
icon = <img src={folder.emoji_url} alt={folder.emoji} className='size-5' />;
} else if (folder.emoji) {
icon = <Emoji size={20} emoji={folder.emoji} />;
}
return (
<ListItem
key={folder.id}
to={`/bookmarks/${folder.id}`}
label={
<HStack alignItems='center' space={2}>
<div className='flex-none'>{icon}</div>
<span>{folder.name}</span>
</HStack>
}
/>
);
})}
</List>
</Stack>
</Column>

View File

@ -6,12 +6,12 @@ import moodSmileIcon from '@tabler/icons/outline/mood-smile.svg';
import trashIcon from '@tabler/icons/outline/trash.svg';
import { useMutation } from '@tanstack/react-query';
import clsx from 'clsx';
import graphemesplit from 'graphemesplit';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import escape from 'lodash/escape';
import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals.ts';
import { initReport, ReportableEntities } from 'soapbox/actions/reports.ts';
import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts';
@ -27,7 +27,6 @@ import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats.ts';
import { queryClient } from 'soapbox/queries/client.ts';
import { stripHTML } from 'soapbox/utils/html.ts';
import { onlyEmoji } from 'soapbox/utils/rich-content.ts';
import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-message-reaction-wrapper.tsx';
import ChatMessageReaction from './chat-message-reaction.tsx';
@ -100,10 +99,9 @@ const ChatMessage = (props: IChatMessage) => {
&& lastReadMessageTimestamp >= new Date(chatMessage.created_at);
const isOnlyEmoji = useMemo(() => {
const hiddenEl = document.createElement('div');
hiddenEl.innerHTML = content;
return onlyEmoji(hiddenEl, BIG_EMOJI_LIMIT, false);
}, []);
const textContent = new DOMParser().parseFromString(content, 'text/html').body.firstChild?.textContent;
return Boolean(textContent && /^\p{Extended_Pictographic}+$/u.test(textContent) && (graphemesplit(textContent).length <= BIG_EMOJI_LIMIT));
}, [content]);
const emojiReactionRows = useMemo(() => {
if (!chatMessage.emoji_reactions) {
@ -302,7 +300,7 @@ const ChatMessage = (props: IChatMessage) => {
'[&_.mention]:text-white dark:[&_.mention]:white': isMyMessage,
'bg-primary-500 text-white': isMyMessage,
'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100': !isMyMessage,
'!bg-transparent !p-0 emoji-lg': isOnlyEmoji,
'!bg-transparent !p-0 text-4xl': isOnlyEmoji,
})
}
ref={setBubbleRef}

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

@ -15,11 +15,8 @@ const LightningAddress: React.FC<ILightningAddress> = (props): JSX.Element => {
return (
<Stack>
<HStack alignItems='center' className='mb-1'>
<Emoji
className='mr-2.5 flex w-6 items-start justify-center rtl:ml-2.5 rtl:mr-0'
emoji='⚡'
/>
<HStack space={2.5} alignItems='center' className='mb-1'>
<Emoji size={24} emoji='⚡' />
<Text weight='bold'>
<FormattedMessage id='crypto.lightning' defaultMessage='Lightning' />

View File

@ -129,7 +129,7 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
{(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && (
<Tooltip text={intl.formatMessage(messages.unverified)}>
<div>
<Emoji className='size-4' emoji='⚠️' />
<Emoji emoji='⚠️' />
</div>
</Tooltip>
)}

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

@ -63,13 +63,11 @@ const validEmojiChar = (c: string) => {
};
const convertCustom = (shortname: string, filename: string) => {
return `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
return `<img draggable="false" class="inline-block w-4 h-4" alt="${shortname}" title="${shortname}" src="${filename}" />`;
};
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

@ -315,13 +315,11 @@ const Notification: React.FC<INotification> = (props) => {
const renderIcon = (): React.ReactNode => {
if (type === 'pleroma:emoji_reaction' && notification.emoji) {
return (
<Emoji
emoji={notification.emoji}
src={notification.emoji_url || undefined}
className='size-4 flex-none'
/>
);
if (notification.emoji_url) {
return <img src={notification.emoji_url} alt={notification.emoji} className='size-4 flex-none' />;
} else {
return <Emoji emoji={notification.emoji} />;
}
} else if (validType(type)) {
return (
<Icon

View File

@ -24,7 +24,7 @@ const SearchPage = () => {
>
<div className='space-y-4'>
<div className='px-4 sm:py-0'>
<Search autoFocus autoSubmit />
<Search autoSubmit />
</div>
<SearchResults />
</div>

View File

@ -182,15 +182,12 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
return (
<InteractionCounter count={count} onClick={features.exposableReactions ? handleClick : undefined}>
<HStack space={0.5} alignItems='center'>
{emojiReacts.take(3).map((e, i) => {
return (
<Emoji
key={i}
className='size-4.5 flex-none'
emoji={e.name}
src={e.url}
/>
);
{emojiReacts.take(3).map((emoji, i) => {
if (emoji.url) {
return <img key={i} src={emoji.url} alt={emoji.name} className='size-4.5 flex-none' />;
} else {
return <div key={i} className='flex-none'><Emoji size={18} emoji={emoji.name} /></div>;
}
})}
</HStack>
</InteractionCounter>

View File

@ -63,7 +63,7 @@ const EmojiPicker: React.FC<IEmojiPicker> = ({ emoji, emojiUrl, ...props }) => {
tabIndex={0}
>
{emoji
? <Emoji height={20} width={20} emoji={emoji} />
? <Emoji size={20} emoji={emoji} />
: <Icon className='size-5 text-gray-600 hover:text-gray-700 dark:hover:text-gray-500' src={moodHappyIcon} />}
</button>

View File

@ -62,10 +62,16 @@ const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, reaction
reactions!.forEach(reaction => items.push(
{
text: <div className='flex items-center gap-1'>
<Emoji className='size-4' emoji={reaction.name} src={reaction.url || undefined} />
{reaction.count}
</div>,
text: (
<div className='flex items-center gap-1'>
{reaction.url ? (
<img src={reaction.url} alt='' className='size-4' />
) : (
<Emoji emoji={reaction.name} />
)}
{reaction.count}
</div>
),
action: () => setReaction(reaction.name),
name: reaction.name,
},

View File

@ -60,25 +60,29 @@ const SelectBookmarkFolderModal: React.FC<ISelectBookmarkFolderModal> = ({ statu
];
if (!isFetching) {
items.push(...(bookmarkFolders.map((folder) => (
<RadioItem
key={folder.id}
label={
<HStack alignItems='center' space={2}>
{folder.emoji ? (
<Emoji
emoji={folder.emoji}
src={folder.emoji_url || undefined}
className='size-5 flex-none'
/>
) : <Icon src={folderIcon} size={20} />}
<span>{folder.name}</span>
</HStack>
}
checked={selectedFolder === folder.id}
value={folder.id}
/>
))));
items.push(...(bookmarkFolders.map((folder) => {
let icon = <Icon src={folderIcon} size={20} />;
if (folder.emoji_url) {
icon = <img src={folder.emoji_url} alt={folder.emoji} className='size-5' />;
} else if (folder.emoji) {
icon = <Emoji size={20} emoji={folder.emoji} />;
}
return (
<RadioItem
key={folder.id}
label={
<HStack alignItems='center' space={2}>
<div className='flex-none'>{icon}</div>
<span>{folder.name}</span>
</HStack>
}
checked={selectedFolder === folder.id}
value={folder.id}
/>
);
})));
}
const body = isFetching ? <Spinner /> : (

View File

@ -131,15 +131,6 @@
padding-left: 4px;
}
.emoji-lg img.emojione {
width: 36px !important;
height: 36px !important;
}
.emojione {
@apply w-4 h-4 -mt-[0.2ex] mb-[0.2ex] inline-block align-middle object-contain;
}
.compose-form-warning {
strong {
@apply font-medium;

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

@ -1,24 +0,0 @@
/** Returns `true` if the node contains only emojis, up to a limit */
export const onlyEmoji = (node: HTMLElement, limit = 1, ignoreMentions = true): boolean => {
if (!node) return false;
try {
// Remove mentions before checking content
if (ignoreMentions) {
node = node.cloneNode(true) as HTMLElement;
node.querySelectorAll('a.mention').forEach(m => m.parentNode?.removeChild(m));
}
if (node.textContent?.replace(new RegExp(' ', 'g'), '') !== '') return false;
const emojis = Array.from(node.querySelectorAll('img.emojione'));
if (emojis.length === 0) return false;
if (emojis.length > limit) return false;
const images = Array.from(node.querySelectorAll('img'));
if (images.length > emojis.length) return false;
return true;
} catch (e) {
// If anything in here crashes, skipping it is inconsequential.
console.error(e);
return false;
}
};

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"