Merge branch 'main' into css-to-tailwind
This commit is contained in:
commit
864a377c8c
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
}, []);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 /> : (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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