diff --git a/src/components/account.tsx b/src/components/account.tsx index fbd4359b7..d0003cafe 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -15,6 +15,7 @@ import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import ActionButton from 'soapbox/features/ui/components/action-button.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { getAcct } from 'soapbox/utils/accounts.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { displayFqn } from 'soapbox/utils/state.ts'; import Badge from './badge.tsx'; @@ -233,7 +234,7 @@ const Account = ({ - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {account.verified && } diff --git a/src/components/display-name-inline.tsx b/src/components/display-name-inline.tsx index b7391cbac..86af69f40 100644 --- a/src/components/display-name-inline.tsx +++ b/src/components/display-name-inline.tsx @@ -1,6 +1,7 @@ import HStack from 'soapbox/components/ui/hstack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { getAcct } from '../utils/accounts.ts'; @@ -10,7 +11,7 @@ import VerificationBadge from './verification-badge.tsx'; import type { Account } from 'soapbox/schemas/index.ts'; interface IDisplayName { - account: Pick; + account: Pick; withSuffix?: boolean; } @@ -25,7 +26,7 @@ const DisplayNameInline: React.FC = ({ account, withSuffix = true weight='normal' truncate > - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {verified && } diff --git a/src/components/display-name.tsx b/src/components/display-name.tsx index 6d87fa388..a2d46fb0b 100644 --- a/src/components/display-name.tsx +++ b/src/components/display-name.tsx @@ -2,16 +2,15 @@ import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; - -import { getAcct } from '../utils/accounts.ts'; - +import { getAcct } from 'soapbox/utils/accounts.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import VerificationBadge from './verification-badge.tsx'; import type { Account } from 'soapbox/schemas/index.ts'; interface IDisplayName { - account: Pick; + account: Pick; withSuffix?: boolean; children?: React.ReactNode; } @@ -27,7 +26,7 @@ const DisplayName: React.FC = ({ account, children, withSuffix = t weight='semibold' truncate > - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {verified && } diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 91ec91e25..96cf532d4 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -6,8 +6,8 @@ import { useState, useRef, useLayoutEffect, useMemo, memo } from 'react'; import { FormattedMessage } from 'react-intl'; import Icon from 'soapbox/components/icon.tsx'; - -import { getTextDirection } from '../utils/rtl.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; +import { getTextDirection } from 'soapbox/utils/rtl.ts'; import HashtagLink from './hashtag-link.tsx'; import Markup from './markup.tsx'; @@ -90,27 +90,7 @@ const StatusContent: React.FC = ({ } if (domNode instanceof DOMText) { - const parts: Array = []; - - const textNodes = domNode.data.split(/:\w+:/); - const shortcodes = [...domNode.data.matchAll(/:(\w+):/g)]; - - for (let i = 0; i < textNodes.length; i++) { - parts.push(textNodes[i]); - - if (shortcodes[i]) { - const [text, shortcode] = shortcodes[i]; - const customEmoji = status.emojis.find((e) => e.shortcode === shortcode); - - if (customEmoji) { - parts.push({shortcode}); - } else { - parts.push(text); - } - } - } - - return <>{parts}; + return emojifyText(domNode.data, status.emojis.toJS()); } if (domNode instanceof Element && domNode.name === 'a') { diff --git a/src/components/status.tsx b/src/components/status.tsx index 1cfbc287c..87c43456e 100644 --- a/src/components/status.tsx +++ b/src/components/status.tsx @@ -20,6 +20,7 @@ import QuotedStatus from 'soapbox/features/status/containers/quoted-status-conta import { HotKeys } from 'soapbox/features/ui/components/hotkeys.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status.ts'; import EventPreview from './event-preview.tsx'; @@ -232,7 +233,7 @@ const Status: React.FC = (props) => { > - {status.account.display_name} + {emojifyText(status.account.display_name, status.account.emojis)} @@ -263,7 +264,7 @@ const Status: React.FC = (props) => { - {status.account.display_name} + {emojifyText(status.account.display_name, status.account.emojis)} diff --git a/src/components/ui/custom-emoji.tsx b/src/components/ui/custom-emoji.tsx deleted file mode 100644 index b176096d0..000000000 --- a/src/components/ui/custom-emoji.tsx +++ /dev/null @@ -1,19 +0,0 @@ -interface ICustomEmoji { - /** Custom emoji URL. */ - url: string; - /** Image alt text, usually the shortcode. */ - alt?: string; - /** `img` tag className. Default: `h-[1em]` */ - className?: string; -} - -/** A single custom emoji image. */ -const CustomEmoji: React.FC = (props): JSX.Element | null => { - const { url, alt, className = 'h-[1em]' } = props; - - return ( - {alt} - ); -}; - -export default CustomEmoji; diff --git a/src/features/chats/components/chat-search/results.tsx b/src/features/chats/components/chat-search/results.tsx index 6474b74fb..91ace730e 100644 --- a/src/features/chats/components/chat-search/results.tsx +++ b/src/features/chats/components/chat-search/results.tsx @@ -8,6 +8,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import useAccountSearch from 'soapbox/queries/search.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import type { Account } from 'soapbox/types/entities.ts'; @@ -41,7 +42,10 @@ const Results = ({ accountSearchResult, onSelect }: IResults) => {
- {account.display_name} + + {emojifyText(account.display_name, account.emojis)} + + {account.verified && }
@{account.acct} {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} diff --git a/src/features/feed-suggestions/feed-suggestions.tsx b/src/features/feed-suggestions/feed-suggestions.tsx index d509889b2..79715bbe8 100644 --- a/src/features/feed-suggestions/feed-suggestions.tsx +++ b/src/features/feed-suggestions/feed-suggestions.tsx @@ -8,6 +8,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import ActionButton from '../ui/components/action-button.tsx'; import { HotKeys } from '../ui/components/hotkeys.tsx'; @@ -47,7 +48,7 @@ const SuggestionItem: React.FC = ({ accountId }) => { size='sm' className='max-w-[95%]' > - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {account.verified && } diff --git a/src/features/notifications/components/notification.tsx b/src/features/notifications/components/notification.tsx index 96692e4c1..c912c3ba4 100644 --- a/src/features/notifications/components/notification.tsx +++ b/src/features/notifications/components/notification.tsx @@ -37,6 +37,7 @@ import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useInstance } from 'soapbox/hooks/useInstance.ts'; import { makeGetNotification } from 'soapbox/selectors/index.ts'; import toast from 'soapbox/toast.tsx'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { NotificationType, validType } from 'soapbox/utils/notification.ts'; import type { ScrollPosition } from 'soapbox/components/status.tsx'; @@ -57,7 +58,7 @@ const buildLink = (account: AccountEntity): JSX.Element => ( title={account.acct} to={`/@${account.acct}`} > - {account.display_name} + {emojifyText(account.display_name, account.emojis)} ); diff --git a/src/features/ui/components/modals/familiar-followers-modal.tsx b/src/features/ui/components/modals/familiar-followers-modal.tsx index e4766ff54..63efbd34a 100644 --- a/src/features/ui/components/modals/familiar-followers-modal.tsx +++ b/src/features/ui/components/modals/familiar-followers-modal.tsx @@ -7,6 +7,7 @@ import Spinner from 'soapbox/components/ui/spinner.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { makeGetAccount } from 'soapbox/selectors/index.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; const getAccount = makeGetAccount(); @@ -32,7 +33,7 @@ const FamiliarFollowersModal = ({ accountId, onClose }: IFamiliarFollowersModal) ); @@ -57,7 +58,7 @@ const FamiliarFollowersModal = ({ accountId, onClose }: IFamiliarFollowersModal) )} onClose={onClickClose} diff --git a/src/features/ui/components/modals/zap-invoice.tsx b/src/features/ui/components/modals/zap-invoice.tsx index 3b3ebbc72..3622c5b9c 100644 --- a/src/features/ui/components/modals/zap-invoice.tsx +++ b/src/features/ui/components/modals/zap-invoice.tsx @@ -15,6 +15,7 @@ import Modal from 'soapbox/components/ui/modal.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { ZapSplitData } from 'soapbox/schemas/zap-split.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import type { Account as AccountEntity } from 'soapbox/types/entities.ts'; @@ -48,7 +49,13 @@ const ZapInvoiceModal: React.FC = ({ account, invoice, splitData, o }; const renderTitle = () => { - return ; + return ( + + ); }; const handleNext = () => { diff --git a/src/features/ui/components/modals/zap-split/zap-split.tsx b/src/features/ui/components/modals/zap-split/zap-split.tsx index 74ec92622..0abcaae0f 100644 --- a/src/features/ui/components/modals/zap-split/zap-split.tsx +++ b/src/features/ui/components/modals/zap-split/zap-split.tsx @@ -8,6 +8,7 @@ import Button from 'soapbox/components/ui/button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { ZapSplitData } from 'soapbox/schemas/zap-split.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; const messages = defineMessages({ zap_open_wallet: { id: 'zap.open_wallet', defaultMessage: 'Open Wallet' }, @@ -31,7 +32,11 @@ const ZapSplit = ({ zapData, zapAmount, invoice, onNext, isLastStep, onFinish }: const renderTitleQr = () => { return (
- +
); }; diff --git a/src/features/ui/components/profile-familiar-followers.tsx b/src/features/ui/components/profile-familiar-followers.tsx index 11eb769d4..1d241c724 100644 --- a/src/features/ui/components/profile-familiar-followers.tsx +++ b/src/features/ui/components/profile-familiar-followers.tsx @@ -14,6 +14,7 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; import { makeGetAccount } from 'soapbox/selectors/index.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import type { Account } from 'soapbox/schemas/index.ts'; @@ -51,7 +52,7 @@ const ProfileFamiliarFollowers: React.FC = ({ account - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {account.verified && } diff --git a/src/features/ui/components/profile-info-panel.tsx b/src/features/ui/components/profile-info-panel.tsx index 43fb51b24..26860b164 100644 --- a/src/features/ui/components/profile-info-panel.tsx +++ b/src/features/ui/components/profile-info-panel.tsx @@ -15,6 +15,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { capitalize } from 'soapbox/utils/strings.ts'; import ProfileFamiliarFollowers from './profile-familiar-followers.tsx'; @@ -151,7 +152,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => - {deactivated ? intl.formatMessage(messages.deactivated) : account.display_name} + {deactivated ? intl.formatMessage(messages.deactivated) : emojifyText(account.display_name, account.emojis)} {account.bot && } diff --git a/src/features/ui/components/user-panel.tsx b/src/features/ui/components/user-panel.tsx index 207dc84b8..f6b23caae 100644 --- a/src/features/ui/components/user-panel.tsx +++ b/src/features/ui/components/user-panel.tsx @@ -10,6 +10,7 @@ import Text from 'soapbox/components/ui/text.tsx'; import VerificationBadge from 'soapbox/components/verification-badge.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { getAcct } from 'soapbox/utils/accounts.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { shortNumberFormat } from 'soapbox/utils/numbers.tsx'; import { displayFqn } from 'soapbox/utils/state.ts'; @@ -59,7 +60,7 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain }) - {account.display_name} + {emojifyText(account.display_name, account.emojis)} {verified && } diff --git a/src/features/zap/components/zap-pay-request-form.tsx b/src/features/zap/components/zap-pay-request-form.tsx index 6dc0cb2ef..16e64c8d8 100644 --- a/src/features/zap/components/zap-pay-request-form.tsx +++ b/src/features/zap/components/zap-pay-request-form.tsx @@ -22,6 +22,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; import ZapButton from './zap-button/zap-button.tsx'; @@ -113,7 +114,11 @@ const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) => /> - + @@ -141,12 +146,15 @@ const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) => {hasZapSplit &&

sats

} - {hasZapSplit && - - } + {hasZapSplit && ( + + + + )}
@@ -162,7 +170,8 @@ const ZapPayRequestForm = ({ account, status, onClose }: IZapPayRequestForm) => diff --git a/src/utils/emojify.tsx b/src/utils/emojify.tsx new file mode 100644 index 000000000..aba82f5b7 --- /dev/null +++ b/src/utils/emojify.tsx @@ -0,0 +1,26 @@ +import { CustomEmoji } from 'soapbox/schemas/custom-emoji.ts'; + +/** Given text and a list of custom emojis, return JSX with the emojis rendered as `` elements. */ +export function emojifyText(text: string, emojis: CustomEmoji[]): JSX.Element { + const parts: Array = []; + + const textNodes = text.split(/:\w+:/); + const shortcodes = [...text.matchAll(/:(\w+):/g)]; + + for (let i = 0; i < textNodes.length; i++) { + parts.push(textNodes[i]); + + if (shortcodes[i]) { + const [match, shortcode] = shortcodes[i]; + const customEmoji = emojis.find((e) => e.shortcode === shortcode); + + if (customEmoji) { + parts.push({shortcode}); + } else { + parts.push(match); + } + } + } + + return <>{parts}; +} \ No newline at end of file