diff --git a/src/components/markup.css b/src/components/markup.css index 482b37237..4c7d6fb30 100644 --- a/src/components/markup.css +++ b/src/components/markup.css @@ -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; } diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 45ff497c8..3ee565f0e 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -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 = ({ textSize = 'md', }) => { const [collapsed, setCollapsed] = useState(false); - const [onlyEmoji, setOnlyEmoji] = useState(false); const node = useRef(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 = ({ } }; - 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 = ({ '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 = ({ tabIndex={0} key='content' className={clsx(baseClassName, { - 'leading-normal big-emoji': onlyEmoji, + 'leading-normal !text-4xl': isOnlyEmoji, })} direction={direction} lang={status.language || undefined} diff --git a/src/features/chats/components/chat-message.tsx b/src/features/chats/components/chat-message.tsx index 8e1f7e091..ad2b7694a 100644 --- a/src/features/chats/components/chat-message.tsx +++ b/src/features/chats/components/chat-message.tsx @@ -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} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 4dedec85e..d7742d727 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -130,15 +130,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; diff --git a/src/utils/rich-content.ts b/src/utils/rich-content.ts deleted file mode 100644 index 1fe3f5c79..000000000 --- a/src/utils/rich-content.ts +++ /dev/null @@ -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; - } -};