From 5b6599f98dac2f3b781a1ea9c41d616d78af3fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 21 Oct 2024 23:23:48 +0200 Subject: [PATCH] pl-fe: WIP Move emojify to status content parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-api/lib/entities/account.ts | 27 ++++- .../src/components/account-hover-card.tsx | 2 +- packages/pl-fe/src/components/account.tsx | 13 ++- .../pl-fe/src/components/event-preview.tsx | 5 +- packages/pl-fe/src/components/group-card.tsx | 5 +- .../groups/popover/group-popover.tsx | 5 +- .../pl-fe/src/components/parsed-content.tsx | 19 +++- .../src/components/polls/poll-footer.tsx | 2 +- .../src/components/polls/poll-option.tsx | 14 ++- packages/pl-fe/src/components/status.tsx | 30 ++--- .../components/moved-note.tsx | 3 +- .../components/reply-group-indicator.tsx | 10 +- .../directory/components/account-card.tsx | 4 +- packages/pl-fe/src/features/emoji/emojify.tsx | 103 ++++++++++++++++++ packages/pl-fe/src/features/emoji/index.ts | 1 + .../event/components/event-header.tsx | 3 +- .../feed-suggestions/feed-suggestions.tsx | 12 +- .../group/components/group-header.tsx | 8 +- .../pl-fe/src/features/group/edit-group.tsx | 3 +- .../pl-fe/src/features/group/manage-group.tsx | 3 +- .../components/discover/group-list-item.tsx | 11 +- .../notifications/components/notification.tsx | 10 +- .../status/components/detailed-status.tsx | 3 +- .../modals/compare-history-modal.tsx | 10 +- .../modals/familiar-followers-modal.tsx | 11 +- .../steps/confirmation-step.tsx | 2 +- .../panels/pinned-accounts-panel.tsx | 5 +- .../panels/profile-fields-panel.tsx | 4 +- .../components/panels/profile-info-panel.tsx | 15 ++- .../ui/components/panels/user-panel.tsx | 6 +- .../features/ui/components/poll-preview.tsx | 2 +- .../components/profile-familiar-followers.tsx | 10 +- .../features/ui/components/profile-field.tsx | 20 +++- packages/pl-fe/src/normalizers/account.ts | 17 --- .../pl-fe/src/normalizers/announcement.ts | 4 +- packages/pl-fe/src/normalizers/group.ts | 13 --- packages/pl-fe/src/normalizers/status-edit.ts | 5 - packages/pl-fe/src/reducers/polls.ts | 7 +- packages/pl-fe/src/schemas/utils.ts | 11 +- 39 files changed, 287 insertions(+), 151 deletions(-) create mode 100644 packages/pl-fe/src/features/emoji/emojify.tsx diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 9651842c9..37c37b004 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -9,13 +9,38 @@ import { coerceObject, datetimeSchema, filteredArray } from './utils'; const filterBadges = (tags?: string[]) => tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') })); +const getDomainFromURL = (account: any): string => { + try { + const url = account.url; + return new URL(url).host; + } catch { + return ''; + } +}; + +const guessFqn = (account: any): string => { + const acct = account.acct; + const [user, domain] = acct.split('@'); + + if (domain) { + return acct; + } else { + return [user, getDomainFromURL(account)].join('@'); + } +}; + const preprocessAccount = v.transform((account: any) => { if (!account?.acct) return null; const username = account.username || account.acct.split('@')[0]; + const fqn = account.fqn || guessFqn(account); + const domain = fqn.split('@')[1] || ''; + return { username, + fqn, + domain, avatar_static: account.avatar_static || account.avatar, header_static: account.header_static || account.header, local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined, @@ -73,7 +98,7 @@ const baseAccountSchema = v.object({ acct: v.fallback(v.string(), ''), url: v.pipe(v.string(), v.url()), display_name: v.fallback(v.string(), ''), - note: v.fallback(v.string(), ''), + note: v.fallback(v.pipe(v.string(), v.transform(note => note === '

' ? '' : note)), ''), avatar: v.fallback(v.string(), ''), avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''), header: v.fallback(v.pipe(v.string(), v.url()), ''), diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index 01b6a22d3..0426fba86 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -160,7 +160,7 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { size='sm' className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden' > - + )} diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index fcb716158..affda2e66 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -11,6 +11,7 @@ import IconButton from 'pl-fe/components/ui/icon-button'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; +import Emojify from 'pl-fe/features/emoji/emojify'; import ActionButton from 'pl-fe/features/ui/components/action-button'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { getAcct } from 'pl-fe/utils/accounts'; @@ -219,8 +220,9 @@ const Account = ({ size='sm' weight='semibold' truncate - dangerouslySetInnerHTML={{ __html: account.display_name_html }} - /> + > + + {account.verified && } @@ -281,8 +283,9 @@ const Account = ({ size='sm' weight='semibold' truncate - dangerouslySetInnerHTML={{ __html: account.display_name_html }} - /> + > + + {account.verified && } @@ -356,7 +359,7 @@ const Account = ({ truncate size='sm' > - + )} diff --git a/packages/pl-fe/src/components/event-preview.tsx b/packages/pl-fe/src/components/event-preview.tsx index 115d08512..c952cbdb7 100644 --- a/packages/pl-fe/src/components/event-preview.tsx +++ b/packages/pl-fe/src/components/event-preview.tsx @@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; +import Emojify from 'pl-fe/features/emoji/emojify'; import EventActionButton from 'pl-fe/features/event/components/event-action-button'; import EventDate from 'pl-fe/features/event/components/event-date'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; @@ -71,7 +72,9 @@ const EventPreview: React.FC = ({ status, className, hideAction, - + + + {account.verified && } diff --git a/packages/pl-fe/src/components/group-card.tsx b/packages/pl-fe/src/components/group-card.tsx index 66c604e56..1a384169a 100644 --- a/packages/pl-fe/src/components/group-card.tsx +++ b/packages/pl-fe/src/components/group-card.tsx @@ -3,6 +3,7 @@ import React from 'react'; import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import GroupHeaderImage from 'pl-fe/features/group/components/group-header-image'; import GroupMemberCount from 'pl-fe/features/group/components/group-member-count'; import GroupPrivacy from 'pl-fe/features/group/components/group-privacy'; @@ -37,7 +38,9 @@ const GroupCard: React.FC = ({ group }) => ( {/* Group Info */} - + + + diff --git a/packages/pl-fe/src/components/groups/popover/group-popover.tsx b/packages/pl-fe/src/components/groups/popover/group-popover.tsx index 88daf39e8..c2f5f5bed 100644 --- a/packages/pl-fe/src/components/groups/popover/group-popover.tsx +++ b/packages/pl-fe/src/components/groups/popover/group-popover.tsx @@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Popover from 'pl-fe/components/ui/popover'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import GroupMemberCount from 'pl-fe/features/group/components/group-member-count'; import GroupPrivacy from 'pl-fe/features/group/components/group-privacy'; @@ -71,7 +72,9 @@ const GroupPopover = (props: IGroupPopoverContainer) => { {/* Group Info */} - + + + diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index 234a86c85..2573a3c4b 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -3,11 +3,14 @@ import DOMPurify from 'isomorphic-dompurify'; import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; +import Emojify from 'pl-fe/features/emoji/emojify'; +import { makeEmojiMap } from 'pl-fe/utils/normalizers'; + import HashtagLink from './hashtag-link'; import HoverAccountWrapper from './hover-account-wrapper'; import StatusMention from './status-mention'; -import type { Mention } from 'pl-api'; +import type { CustomEmoji, Mention } from 'pl-api'; const nodesToText = (nodes: Array): string => nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array) : '').join(''); @@ -19,14 +22,18 @@ interface IParsedContent { mentions?: Array; /** Whether it's a status which has a quote. */ hasQuote?: boolean; + /** Related custom emojis. */ + emojis?: Array; } -const ParsedContent: React.FC = (({ html, mentions, hasQuote }) => { +const ParsedContent: React.FC = (({ html, mentions, hasQuote, emojis }) => { return useMemo(() => { if (html.length === 0) { return null; } + const emojiMap = emojis ? makeEmojiMap(emojis) : undefined; + const selectors: Array = []; // Explicit mentions @@ -99,6 +106,14 @@ const ParsedContent: React.FC = (({ html, mentions, hasQuote }) return fallback; } }, + + transform(reactNode) { + if (typeof reactNode === 'string') { + return ; + } + + return reactNode as JSX.Element; + }, }; return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options); diff --git a/packages/pl-fe/src/components/polls/poll-footer.tsx b/packages/pl-fe/src/components/polls/poll-footer.tsx index d619a4bc4..b63aaa4a5 100644 --- a/packages/pl-fe/src/components/polls/poll-footer.tsx +++ b/packages/pl-fe/src/components/polls/poll-footer.tsx @@ -12,7 +12,7 @@ import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import RelativeTimestamp from '../relative-timestamp'; import type { Selected } from './poll'; -import type { Poll } from 'pl-fe/normalizers/poll'; +import type { Poll } from 'pl-api'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, diff --git a/packages/pl-fe/src/components/polls/poll-option.tsx b/packages/pl-fe/src/components/polls/poll-option.tsx index 99cf9c14b..caef4e4fa 100644 --- a/packages/pl-fe/src/components/polls/poll-option.tsx +++ b/packages/pl-fe/src/components/polls/poll-option.tsx @@ -7,7 +7,9 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Text from 'pl-fe/components/ui/text'; -import type { Poll } from 'pl-fe/normalizers/poll'; +import { ParsedContent } from '../parsed-content'; + +import type { Poll } from 'pl-api'; const messages = defineMessages({ voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' }, @@ -65,8 +67,9 @@ const PollOptionText: React.FC = ({ poll, option, index, active theme='inherit' weight='medium' align='center' - dangerouslySetInnerHTML={{ __html: option.title_emojified }} - /> + > + + @@ -133,9 +136,10 @@ const PollOption: React.FC = (props): JSX.Element | null => { + > + + diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 3593aec1c..67812204d 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -12,6 +12,7 @@ import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import AccountContainer from 'pl-fe/containers/account-container'; +import Emojify from 'pl-fe/features/emoji/emojify'; import StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon'; import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container'; import { HotKeys } from 'pl-fe/features/ui/components/hotkeys'; @@ -204,23 +205,17 @@ const Status: React.FC = (props) => { className='hover:underline' > - + + + ), group: ( - + + + ), }} @@ -234,12 +229,9 @@ const Status: React.FC = (props) => { const renderedAccounts = accounts.slice(0, 2).map(account => !!account && ( - + + + )); @@ -294,7 +286,7 @@ const Status: React.FC = (props) => { - + diff --git a/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx b/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx index c98f39a91..da8b93228 100644 --- a/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx +++ b/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx @@ -5,6 +5,7 @@ import Account from 'pl-fe/components/account'; import Icon from 'pl-fe/components/icon'; import HStack from 'pl-fe/components/ui/hstack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import type { Account as AccountEntity } from 'pl-fe/normalizers/account'; @@ -27,7 +28,7 @@ const MovedNote: React.FC = ({ from, to }) => ( id='notification.move' defaultMessage='{name} moved to {targetName}' values={{ - name: , + name: , targetName: to.acct, }} /> diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index 82edc5bc5..d67bae5b8 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl'; import Link from 'pl-fe/components/link'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { makeGetStatus } from 'pl-fe/selectors'; @@ -28,10 +29,11 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { id='compose.reply_group_indicator.message' defaultMessage='Posting to {groupLink}' values={{ - groupLink: , + groupLink: ( + + + + ), }} /> diff --git a/packages/pl-fe/src/features/directory/components/account-card.tsx b/packages/pl-fe/src/features/directory/components/account-card.tsx index bfca9ddf1..c31c150be 100644 --- a/packages/pl-fe/src/features/directory/components/account-card.tsx +++ b/packages/pl-fe/src/features/directory/components/account-card.tsx @@ -70,13 +70,13 @@ const AccountCard: React.FC = ({ id }) => { withRelationship={false} /> - {!!account.note_emojified && ( + {!!account.note && ( - + )} diff --git a/packages/pl-fe/src/features/emoji/emojify.tsx b/packages/pl-fe/src/features/emoji/emojify.tsx new file mode 100644 index 000000000..ac159816a --- /dev/null +++ b/packages/pl-fe/src/features/emoji/emojify.tsx @@ -0,0 +1,103 @@ +import split from 'graphemesplit'; +import React from 'react'; + +import { makeEmojiMap } from 'pl-fe/utils/normalizers'; + +import unicodeMapping from './mapping'; + +import { validEmojiChar } from '.'; + +import type { CustomEmoji } from 'pl-api'; + +interface IMaybeEmoji { + text: string; + emojis: Record; +} + +const MaybeEmoji: React.FC = ({ text, emojis }) => { + if (text.length < 3) return text; + if (text in emojis) { + const emoji = emojis[text]; + const filename = emoji.static_url; + + if (filename?.length > 0) { + return {text}; + } + } + + return text; +}; + +interface IEmojify { + text: string; + emojis?: Array | Record; +} + +const Emojify: React.FC = ({ text, emojis = {} }) => React.useMemo(() => { + if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis); + + const nodes = []; + + let stack = ''; + let open = false; + + const clearStack = () => { + if (stack.length) nodes.push(stack); + open = false; + stack = ''; + }; + + for (let c of split(text)) { + // convert FE0E selector to FE0F so it can be found in unimap + if (c.codePointAt(c.length - 1) === 65038) { + c = c.slice(0, -1) + String.fromCodePoint(65039); + } + + // unqualified emojis aren't in emoji-mart's mappings so we just add FEOF + const unqualified = c + String.fromCodePoint(65039); + + if (c in unicodeMapping) { + clearStack(); + + const { unified, shortcode } = unicodeMapping[c]; + + nodes.push( + {c}, + ); + } else if (unqualified in unicodeMapping) { + clearStack(); + + const { unified, shortcode } = unicodeMapping[unqualified]; + + nodes.push( + {unqualified}, + ); + } else if (c === ':') { + if (!open) { + clearStack(); + } + + stack += ':'; + + // we see another : we convert it and clear the stack buffer + if (open) { + nodes.push(); + stack = ''; + } + + open = !open; + } else { + stack += c; + + if (open && !validEmojiChar(c)) { + clearStack(); + } + } + } + + if (stack.length) nodes.push(stack); + + return nodes; +}, [text, emojis]); + +export { Emojify as default }; diff --git a/packages/pl-fe/src/features/emoji/index.ts b/packages/pl-fe/src/features/emoji/index.ts index aafe331d6..dcd14a060 100644 --- a/packages/pl-fe/src/features/emoji/index.ts +++ b/packages/pl-fe/src/features/emoji/index.ts @@ -225,5 +225,6 @@ export { isCustomEmoji, isNativeEmoji, buildCustomEmojis, + validEmojiChar, emojify as default, }; diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index a1bc20ba8..07cdde057 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -19,6 +19,7 @@ import IconButton from 'pl-fe/components/ui/icon-button'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import { useFeatures } from 'pl-fe/hooks/useFeatures'; import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount'; @@ -414,7 +415,7 @@ const EventHeader: React.FC = ({ status }) => { name: ( - + {account.verified && } diff --git a/packages/pl-fe/src/features/feed-suggestions/feed-suggestions.tsx b/packages/pl-fe/src/features/feed-suggestions/feed-suggestions.tsx index ff009530f..30229ff7e 100644 --- a/packages/pl-fe/src/features/feed-suggestions/feed-suggestions.tsx +++ b/packages/pl-fe/src/features/feed-suggestions/feed-suggestions.tsx @@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; +import Emojify from '../emoji/emojify'; import ActionButton from '../ui/components/action-button'; import { HotKeys } from '../ui/components/hotkeys'; @@ -41,14 +42,9 @@ const SuggestionItem: React.FC = ({ accountId }) => { - + + + {account.verified && } diff --git a/packages/pl-fe/src/features/group/components/group-header.tsx b/packages/pl-fe/src/features/group/components/group-header.tsx index d43ccac9c..b520c4896 100644 --- a/packages/pl-fe/src/features/group/components/group-header.tsx +++ b/packages/pl-fe/src/features/group/components/group-header.tsx @@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useModalsStore } from 'pl-fe/stores/modals'; import { isDefaultHeader } from 'pl-fe/utils/accounts'; @@ -141,9 +142,10 @@ const GroupHeader: React.FC = ({ group }) => { + > + + @@ -157,7 +159,7 @@ const GroupHeader: React.FC = ({ group }) => { align='center' className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue' > - + diff --git a/packages/pl-fe/src/features/group/edit-group.tsx b/packages/pl-fe/src/features/group/edit-group.tsx index 2ea0a1be0..e7c4ca078 100644 --- a/packages/pl-fe/src/features/group/edit-group.tsx +++ b/packages/pl-fe/src/features/group/edit-group.tsx @@ -18,6 +18,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { useInstance } from 'pl-fe/hooks/useInstance'; import toast from 'pl-fe/toast'; import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts'; +import { unescapeHTML } from 'pl-fe/utils/html'; import AvatarPicker from '../edit-profile/components/avatar-picker'; import HeaderPicker from '../edit-profile/components/header-picker'; @@ -51,7 +52,7 @@ const EditGroup: React.FC = ({ params: { groupId } }) => { const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) }); const displayName = useTextField(group?.display_name); - const note = useTextField(group?.note_plain); + const note = useTextField(unescapeHTML(group?.note)); const maxName = Number(instance.configuration.groups.max_characters_name); const maxNote = Number(instance.configuration.groups.max_characters_description); diff --git a/packages/pl-fe/src/features/group/manage-group.tsx b/packages/pl-fe/src/features/group/manage-group.tsx index aa63d4bcb..de8637df6 100644 --- a/packages/pl-fe/src/features/group/manage-group.tsx +++ b/packages/pl-fe/src/features/group/manage-group.tsx @@ -13,6 +13,7 @@ import Text from 'pl-fe/components/ui/text'; import { useModalsStore } from 'pl-fe/stores/modals'; import toast from 'pl-fe/toast'; +import Emojify from '../emoji/emojify'; import ColumnForbidden from '../ui/components/column-forbidden'; type RouteParams = { groupId: string }; @@ -86,7 +87,7 @@ const ManageGroup: React.FC = ({ params }) => { - + diff --git a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx index 6201f591d..88e5f540b 100644 --- a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx +++ b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx @@ -7,13 +7,14 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import GroupActionButton from 'pl-fe/features/group/components/group-action-button'; import { shortNumberFormat } from 'pl-fe/utils/numbers'; import type { Group } from 'pl-fe/normalizers/group'; interface IGroupListItem { - group: Pick; + group: Pick; withJoinAction?: boolean; } @@ -34,11 +35,9 @@ const GroupListItem = (props: IGroupListItem) => { /> - + + + ): JSX.Element => ( +const buildLink = (account: Pick): JSX.Element => ( + > + + ); @@ -153,7 +155,7 @@ const messages: Record = defineMe const buildMessage = ( intl: IntlShape, type: NotificationType | 'reply', - accounts: Array>, + accounts: Array>, targetName: string, instanceTitle: string, ): React.ReactNode => { diff --git a/packages/pl-fe/src/features/status/components/detailed-status.tsx b/packages/pl-fe/src/features/status/components/detailed-status.tsx index f2cb80d94..6e218db4c 100644 --- a/packages/pl-fe/src/features/status/components/detailed-status.tsx +++ b/packages/pl-fe/src/features/status/components/detailed-status.tsx @@ -15,6 +15,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container'; import StatusInteractionBar from './status-interaction-bar'; @@ -66,7 +67,7 @@ const DetailedStatus: React.FC = ({ - + diff --git a/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx index 02c972250..aabc3559f 100644 --- a/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx @@ -9,6 +9,7 @@ import Modal from 'pl-fe/components/ui/modal'; import Spinner from 'pl-fe/components/ui/spinner'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; @@ -43,7 +44,6 @@ const CompareHistoryModal: React.FC =
{versions?.map((version) => { const content = ; - const spoilerContent = { __html: version.spoilerHtml }; const poll = typeof version.poll !== 'string' && version.poll; @@ -51,7 +51,9 @@ const CompareHistoryModal: React.FC =
{version.spoiler_text?.length > 0 && ( <> - + + +
)} @@ -71,7 +73,9 @@ const CompareHistoryModal: React.FC = role='radio' /> - + + + ))} diff --git a/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx index f3725273c..4c5271839 100644 --- a/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx @@ -6,6 +6,7 @@ import ScrollableList from 'pl-fe/components/scrollable-list'; import Modal from 'pl-fe/components/ui/modal'; import Spinner from 'pl-fe/components/ui/spinner'; import AccountContainer from 'pl-fe/containers/account-container'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { makeGetAccount } from 'pl-fe/selectors'; @@ -31,7 +32,13 @@ const FamiliarFollowersModal: React.FC; } else { - const emptyMessage = }} />; + const emptyMessage = ( + }} + /> + ); body = ( }} + values={{ name: !!account && }} /> } onClose={onClickClose} diff --git a/packages/pl-fe/src/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx b/packages/pl-fe/src/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx index 0acc3e4db..9d9ba62dc 100644 --- a/packages/pl-fe/src/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx @@ -64,7 +64,7 @@ const ConfirmationStep: React.FC = ({ group }) => { size='md' className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue' > - + diff --git a/packages/pl-fe/src/features/ui/components/panels/pinned-accounts-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/pinned-accounts-panel.tsx index 3814bcd47..0ae284fe3 100644 --- a/packages/pl-fe/src/features/ui/components/panels/pinned-accounts-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/pinned-accounts-panel.tsx @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import { fetchPinnedAccounts } from 'pl-fe/actions/accounts'; import Widget from 'pl-fe/components/ui/widget'; import AccountContainer from 'pl-fe/containers/account-container'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { WhoToFollowPanel } from 'pl-fe/features/ui/util/async-components'; import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; @@ -12,7 +13,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import type { Account } from 'pl-fe/normalizers/account'; interface IPinnedAccountsPanel { - account: Pick; + account: Pick; limit: number; } @@ -36,7 +37,7 @@ const PinnedAccountsPanel: React.FC = ({ account, limit }) id='pinned_accounts.title' defaultMessage='{name}’s choices' values={{ - name: , + name: , }} />} > diff --git a/packages/pl-fe/src/features/ui/components/panels/profile-fields-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/profile-fields-panel.tsx index dc6712505..dd2be80b9 100644 --- a/packages/pl-fe/src/features/ui/components/panels/profile-fields-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/profile-fields-panel.tsx @@ -8,7 +8,7 @@ import ProfileField from '../profile-field'; import type { Account } from 'pl-fe/normalizers/account'; interface IProfileFieldsPanel { - account: Pick; + account: Pick; } /** Custom profile fields for sidebar. */ @@ -16,7 +16,7 @@ const ProfileFieldsPanel: React.FC = ({ account }) => ( {account.fields.map((field, i) => ( - + ))} diff --git a/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx index 8ffc01e9d..65805e1c5 100644 --- a/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx @@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig'; import { capitalize } from 'pl-fe/utils/strings'; @@ -122,8 +123,6 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => ); } - const deactivated = account.deactivated ?? false; - const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html }; const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' }); const badges = getBadges(); @@ -132,7 +131,11 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => - + + {account.deactivated + ? + : } + {account.bot && } @@ -160,9 +163,9 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => - {!!account.note_emojified && ( + {!!account.note && ( - + )} @@ -208,7 +211,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => {account.fields.length > 0 && ( {account.fields.map((field, i) => ( - + ))} )} diff --git a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx index 6d4cb889d..27a30185c 100644 --- a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx @@ -9,6 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { useSettings } from 'pl-fe/hooks/useSettings'; import { getAcct } from 'pl-fe/utils/accounts'; @@ -29,7 +30,6 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain }) const fqn = useAppSelector((state) => displayFqn(state)); if (!account) return null; - const displayNameHtml = { __html: account.display_name_html }; const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct; const header = account.header; const verified = account.verified; @@ -67,7 +67,9 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain }) - + + + {verified && } diff --git a/packages/pl-fe/src/features/ui/components/poll-preview.tsx b/packages/pl-fe/src/features/ui/components/poll-preview.tsx index b6bd4cfc5..0cf89614d 100644 --- a/packages/pl-fe/src/features/ui/components/poll-preview.tsx +++ b/packages/pl-fe/src/features/ui/components/poll-preview.tsx @@ -4,7 +4,7 @@ import React from 'react'; import PollOption from 'pl-fe/components/polls/poll-option'; import Stack from 'pl-fe/components/ui/stack'; -import type { Poll } from 'pl-fe/normalizers/poll'; +import type { Poll } from 'pl-api'; interface IPollPreview { poll: Poll; diff --git a/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx b/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx index 50984d8d7..fe3db1fed 100644 --- a/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx @@ -9,6 +9,7 @@ import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; import HStack from 'pl-fe/components/ui/hstack'; import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch'; import { useAppSelector } from 'pl-fe/hooks/useAppSelector'; import { useFeatures } from 'pl-fe/hooks/useFeatures'; @@ -51,12 +52,9 @@ const ProfileFamiliarFollowers: React.FC = ({ account - + + + {account.verified && } diff --git a/packages/pl-fe/src/features/ui/components/profile-field.tsx b/packages/pl-fe/src/features/ui/components/profile-field.tsx index 1e0cae751..18ea25f59 100644 --- a/packages/pl-fe/src/features/ui/components/profile-field.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-field.tsx @@ -3,9 +3,12 @@ import React from 'react'; import { defineMessages, useIntl, FormatDateOptions } from 'react-intl'; import Markup from 'pl-fe/components/markup'; +import { ParsedContent } from 'pl-fe/components/parsed-content'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; +import Emojify from 'pl-fe/features/emoji/emojify'; import { CryptoAddress, LightningAddress } from 'pl-fe/features/ui/util/async-components'; +import { unescapeHTML } from 'pl-fe/utils/html'; import type { Account } from 'pl-fe/normalizers/account'; @@ -28,32 +31,35 @@ const dateFormatOptions: FormatDateOptions = { interface IProfileField { field: Account['fields'][number]; + emojis?: Account['emojis']; } /** Renders a single profile field. */ -const ProfileField: React.FC = ({ field }) => { +const ProfileField: React.FC = ({ field, emojis }) => { const intl = useIntl(); if (isTicker(field.name)) { return ( ); } else if (isZapEmoji(field.name)) { - return ; + return ; } return (
- + + +
{field.verified_at && ( @@ -62,7 +68,9 @@ const ProfileField: React.FC = ({ field }) => { )} - + + +
diff --git a/packages/pl-fe/src/normalizers/account.ts b/packages/pl-fe/src/normalizers/account.ts index 0a8d96353..9ee10a773 100644 --- a/packages/pl-fe/src/normalizers/account.ts +++ b/packages/pl-fe/src/normalizers/account.ts @@ -1,9 +1,3 @@ -import escapeTextContentForBrowser from 'escape-html'; - -import emojify from 'pl-fe/features/emoji'; -import { unescapeHTML } from 'pl-fe/utils/html'; -import { makeEmojiMap } from 'pl-fe/utils/normalizers'; - import type { Account as BaseAccount } from 'pl-api'; const getDomainFromURL = (account: Pick): string => { @@ -34,8 +28,6 @@ const normalizeAccount = (account: BaseAccount) => { const domain = fqn.split('@')[1] || ''; const note = account.note === '

' ? '' : account.note; - const emojiMap = makeEmojiMap(account.emojis); - return { mute_expires_at: null, ...account, @@ -46,15 +38,6 @@ const normalizeAccount = (account: BaseAccount) => { fqn, domain, note, - display_name_html: emojify(escapeTextContentForBrowser(account.display_name), emojiMap), - note_emojified: emojify(account.note, emojiMap), - note_plain: unescapeHTML(account.note), - fields: account.fields.map(field => ({ - ...field, - name_emojified: emojify(escapeTextContentForBrowser(field.name), emojiMap), - value_emojified: emojify(field.value, emojiMap), - value_plain: unescapeHTML(field.value), - })), }; }; diff --git a/packages/pl-fe/src/normalizers/announcement.ts b/packages/pl-fe/src/normalizers/announcement.ts index ffb1ad4c8..9574a0827 100644 --- a/packages/pl-fe/src/normalizers/announcement.ts +++ b/packages/pl-fe/src/normalizers/announcement.ts @@ -1,10 +1,10 @@ import emojify from 'pl-fe/features/emoji'; -import { makeCustomEmojiMap } from 'pl-fe/schemas/utils'; +import { makeEmojiMap } from 'pl-fe/utils/normalizers'; import type { AdminAnnouncement as BaseAdminAnnouncement, Announcement as BaseAnnouncement } from 'pl-api'; const normalizeAnnouncement = (announcement: T) => { - const emojiMap = makeCustomEmojiMap(announcement.emojis); + const emojiMap = makeEmojiMap(announcement.emojis); const contentHtml = emojify(announcement.content, emojiMap); diff --git a/packages/pl-fe/src/normalizers/group.ts b/packages/pl-fe/src/normalizers/group.ts index 9d564e0c5..856cb33b8 100644 --- a/packages/pl-fe/src/normalizers/group.ts +++ b/packages/pl-fe/src/normalizers/group.ts @@ -1,9 +1,3 @@ -import escapeTextContentForBrowser from 'escape-html'; - -import emojify from 'pl-fe/features/emoji'; -import { unescapeHTML } from 'pl-fe/utils/html'; -import { makeEmojiMap } from 'pl-fe/utils/normalizers'; - import type { Group as BaseGroup } from 'pl-api'; const getDomainFromURL = (group: Pick): string => { @@ -20,9 +14,6 @@ const normalizeGroup = (group: BaseGroup) => { const missingHeader = require('pl-fe/assets/images/header-missing.png'); const domain = getDomainFromURL(group); - const note = group.note === '

' ? '' : group.note; - - const emojiMap = makeEmojiMap(group.emojis); return { ...group, @@ -31,10 +22,6 @@ const normalizeGroup = (group: BaseGroup) => { header: group.header || group.header_static || missingHeader, header_static: group.header_static || group.header || missingHeader, domain, - note, - display_name_html: emojify(escapeTextContentForBrowser(group.display_name), emojiMap), - note_emojified: emojify(group.note, emojiMap), - note_plain: unescapeHTML(group.note), }; }; diff --git a/packages/pl-fe/src/normalizers/status-edit.ts b/packages/pl-fe/src/normalizers/status-edit.ts index 80809dc79..e5aa79149 100644 --- a/packages/pl-fe/src/normalizers/status-edit.ts +++ b/packages/pl-fe/src/normalizers/status-edit.ts @@ -6,18 +6,13 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from 'pl-fe/features/emoji'; import { makeEmojiMap } from 'pl-fe/utils/normalizers'; -import { normalizePollEdit } from './poll'; - import type { StatusEdit as BaseStatusEdit } from 'pl-api'; const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => { const emojiMap = makeEmojiMap(statusEdit.emojis); - const poll = statusEdit.poll ? normalizePollEdit(statusEdit.poll, statusEdit.emojis) : null; - return { ...statusEdit, - poll, contentHtml: emojify(statusEdit.content, emojiMap), spoilerHtml: emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap), }; diff --git a/packages/pl-fe/src/reducers/polls.ts b/packages/pl-fe/src/reducers/polls.ts index 27102d118..f5015dc8d 100644 --- a/packages/pl-fe/src/reducers/polls.ts +++ b/packages/pl-fe/src/reducers/polls.ts @@ -1,16 +1,15 @@ import { Map as ImmutableMap } from 'immutable'; import { POLLS_IMPORT } from 'pl-fe/actions/importer'; -import { normalizePoll } from 'pl-fe/normalizers/poll'; -import type { Status } from 'pl-api'; +import type { Poll, Status } from 'pl-api'; import type { AnyAction } from 'redux'; -type State = ImmutableMap>; +type State = ImmutableMap; const importPolls = (state: State, polls: Array>) => state.withMutations(map => - polls.forEach(poll => map.set(poll.id, normalizePoll(poll))), + polls.forEach(poll => map.set(poll.id, poll)), ); const initialState: State = ImmutableMap(); diff --git a/packages/pl-fe/src/schemas/utils.ts b/packages/pl-fe/src/schemas/utils.ts index ed75567ee..1cc70c64e 100644 --- a/packages/pl-fe/src/schemas/utils.ts +++ b/packages/pl-fe/src/schemas/utils.ts @@ -1,7 +1,5 @@ import * as v from 'valibot'; -import type { CustomEmoji } from 'pl-api'; - /** Validates individual items in an array, dropping any that aren't valid. */ const filteredArray = (schema: v.BaseSchema>) => v.pipe( @@ -14,13 +12,6 @@ const filteredArray = (schema: v.BaseSchema>) => )), ); -/** Map a list of CustomEmoji to their shortcodes. */ -const makeCustomEmojiMap = (customEmojis: CustomEmoji[]) => - customEmojis.reduce>((result, emoji) => { - result[`:${emoji.shortcode}:`] = emoji; - return result; - }, {}); - /** valibot schema to force the value into an object, if it isn't already. */ const coerceObject = (shape: T) => v.pipe( @@ -29,4 +20,4 @@ const coerceObject = (shape: T) => v.object(shape), ); -export { filteredArray, makeCustomEmojiMap, coerceObject }; +export { filteredArray, coerceObject };