pl-fe: WIP Move emojify to status content parser
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
a52dad864b
commit
5b6599f98d
39 changed files with 287 additions and 151 deletions
|
@ -9,13 +9,38 @@ import { coerceObject, datetimeSchema, filteredArray } from './utils';
|
||||||
const filterBadges = (tags?: string[]) =>
|
const filterBadges = (tags?: string[]) =>
|
||||||
tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));
|
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) => {
|
const preprocessAccount = v.transform((account: any) => {
|
||||||
if (!account?.acct) return null;
|
if (!account?.acct) return null;
|
||||||
|
|
||||||
const username = account.username || account.acct.split('@')[0];
|
const username = account.username || account.acct.split('@')[0];
|
||||||
|
|
||||||
|
const fqn = account.fqn || guessFqn(account);
|
||||||
|
const domain = fqn.split('@')[1] || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
|
fqn,
|
||||||
|
domain,
|
||||||
avatar_static: account.avatar_static || account.avatar,
|
avatar_static: account.avatar_static || account.avatar,
|
||||||
header_static: account.header_static || account.header,
|
header_static: account.header_static || account.header,
|
||||||
local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined,
|
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(), ''),
|
acct: v.fallback(v.string(), ''),
|
||||||
url: v.pipe(v.string(), v.url()),
|
url: v.pipe(v.string(), v.url()),
|
||||||
display_name: v.fallback(v.string(), ''),
|
display_name: v.fallback(v.string(), ''),
|
||||||
note: v.fallback(v.string(), ''),
|
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : note)), ''),
|
||||||
avatar: v.fallback(v.string(), ''),
|
avatar: v.fallback(v.string(), ''),
|
||||||
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
|
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||||
header: v.fallback(v.pipe(v.string(), v.url()), ''),
|
header: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||||
|
|
|
@ -160,7 +160,7 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
|
||||||
size='sm'
|
size='sm'
|
||||||
className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||||
>
|
>
|
||||||
<ParsedContent html={account.note_emojified} />
|
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
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 ActionButton from 'pl-fe/features/ui/components/action-button';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { getAcct } from 'pl-fe/utils/accounts';
|
import { getAcct } from 'pl-fe/utils/accounts';
|
||||||
|
@ -219,8 +220,9 @@ const Account = ({
|
||||||
size='sm'
|
size='sm'
|
||||||
weight='semibold'
|
weight='semibold'
|
||||||
truncate
|
truncate
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
>
|
||||||
/>
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
@ -281,8 +283,9 @@ const Account = ({
|
||||||
size='sm'
|
size='sm'
|
||||||
weight='semibold'
|
weight='semibold'
|
||||||
truncate
|
truncate
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
>
|
||||||
/>
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
@ -356,7 +359,7 @@ const Account = ({
|
||||||
truncate
|
truncate
|
||||||
size='sm'
|
size='sm'
|
||||||
>
|
>
|
||||||
<ParsedContent html={account.note_emojified} />
|
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
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 EventActionButton from 'pl-fe/features/event/components/event-action-button';
|
||||||
import EventDate from 'pl-fe/features/event/components/event-date';
|
import EventDate from 'pl-fe/features/event/components/event-date';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
|
@ -71,7 +72,9 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
<Icon src={require('@tabler/icons/outline/user.svg')} />
|
<Icon src={require('@tabler/icons/outline/user.svg')} />
|
||||||
<HStack space={1} alignItems='center' grow>
|
<HStack space={1} alignItems='center' grow>
|
||||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
<span>
|
||||||
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
|
</span>
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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 GroupHeaderImage from 'pl-fe/features/group/components/group-header-image';
|
||||||
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||||
|
@ -37,7 +38,9 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => (
|
||||||
{/* Group Info */}
|
{/* Group Info */}
|
||||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||||
<HStack alignItems='center' space={1.5}>
|
<HStack alignItems='center' space={1.5}>
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<Text size='lg' weight='bold'>
|
||||||
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Popover from 'pl-fe/components/ui/popover';
|
import Popover from 'pl-fe/components/ui/popover';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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 GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||||
|
|
||||||
|
@ -71,7 +72,9 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
|
||||||
|
|
||||||
{/* Group Info */}
|
{/* Group Info */}
|
||||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<Text size='lg' weight='bold'>
|
||||||
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||||
<GroupPrivacy group={group} />
|
<GroupPrivacy group={group} />
|
||||||
|
|
|
@ -3,11 +3,14 @@ import DOMPurify from 'isomorphic-dompurify';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 HashtagLink from './hashtag-link';
|
||||||
import HoverAccountWrapper from './hover-account-wrapper';
|
import HoverAccountWrapper from './hover-account-wrapper';
|
||||||
import StatusMention from './status-mention';
|
import StatusMention from './status-mention';
|
||||||
|
|
||||||
import type { Mention } from 'pl-api';
|
import type { CustomEmoji, Mention } from 'pl-api';
|
||||||
|
|
||||||
const nodesToText = (nodes: Array<DOMNode>): string =>
|
const nodesToText = (nodes: Array<DOMNode>): string =>
|
||||||
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
||||||
|
@ -19,14 +22,18 @@ interface IParsedContent {
|
||||||
mentions?: Array<Mention>;
|
mentions?: Array<Mention>;
|
||||||
/** Whether it's a status which has a quote. */
|
/** Whether it's a status which has a quote. */
|
||||||
hasQuote?: boolean;
|
hasQuote?: boolean;
|
||||||
|
/** Related custom emojis. */
|
||||||
|
emojis?: Array<CustomEmoji>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote }) => {
|
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (html.length === 0) {
|
if (html.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
||||||
|
|
||||||
const selectors: Array<string> = [];
|
const selectors: Array<string> = [];
|
||||||
|
|
||||||
// Explicit mentions
|
// Explicit mentions
|
||||||
|
@ -99,6 +106,14 @@ const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote })
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transform(reactNode) {
|
||||||
|
if (typeof reactNode === 'string') {
|
||||||
|
return <Emojify text={reactNode} emojis={emojiMap} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactNode as JSX.Element;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import RelativeTimestamp from '../relative-timestamp';
|
import RelativeTimestamp from '../relative-timestamp';
|
||||||
|
|
||||||
import type { Selected } from './poll';
|
import type { Selected } from './poll';
|
||||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
import type { Poll } from 'pl-api';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||||
|
|
|
@ -7,7 +7,9 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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({
|
const messages = defineMessages({
|
||||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
|
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
|
||||||
|
@ -65,8 +67,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
||||||
theme='inherit'
|
theme='inherit'
|
||||||
weight='medium'
|
weight='medium'
|
||||||
align='center'
|
align='center'
|
||||||
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
|
>
|
||||||
/>
|
<ParsedContent html={option.title} emojis={poll.emojis} />
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -133,9 +136,10 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
<Text
|
<Text
|
||||||
theme='inherit'
|
theme='inherit'
|
||||||
weight='medium'
|
weight='medium'
|
||||||
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
|
|
||||||
className='relative'
|
className='relative'
|
||||||
/>
|
>
|
||||||
|
<ParsedContent html={(language && option.title_map) && option.title_map[language] || option.title} emojis={poll.emojis} />
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HStack space={2} alignItems='center' className='relative'>
|
<HStack space={2} alignItems='center' className='relative'>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import AccountContainer from 'pl-fe/containers/account-container';
|
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 StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon';
|
||||||
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||||
|
@ -204,23 +205,17 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
className='hover:underline'
|
className='hover:underline'
|
||||||
>
|
>
|
||||||
<bdi className='truncate'>
|
<bdi className='truncate'>
|
||||||
<strong
|
<strong className='text-gray-800 dark:text-gray-200'>
|
||||||
className='text-gray-800 dark:text-gray-200'
|
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||||
dangerouslySetInnerHTML={{
|
</strong>
|
||||||
__html: status.account.display_name_html,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</bdi>
|
</bdi>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
group: (
|
group: (
|
||||||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||||
<strong
|
<strong className='text-gray-800 dark:text-gray-200'>
|
||||||
className='text-gray-800 dark:text-gray-200'
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
dangerouslySetInnerHTML={{
|
</strong>
|
||||||
__html: group.display_name_html,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
@ -234,12 +229,9 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
const renderedAccounts = accounts.slice(0, 2).map(account => !!account && (
|
const renderedAccounts = accounts.slice(0, 2).map(account => !!account && (
|
||||||
<Link key={account.acct} to={`/@${account.acct}`} className='hover:underline'>
|
<Link key={account.acct} to={`/@${account.acct}`} className='hover:underline'>
|
||||||
<bdi className='truncate'>
|
<bdi className='truncate'>
|
||||||
<strong
|
<strong className='text-gray-800 dark:text-gray-200'>
|
||||||
className='text-gray-800 dark:text-gray-200'
|
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||||
dangerouslySetInnerHTML={{
|
</strong>
|
||||||
__html: account.display_name_html,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</bdi>
|
</bdi>
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
|
@ -294,7 +286,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||||
<bdi className='truncate'>
|
<bdi className='truncate'>
|
||||||
<strong className='text-gray-800 dark:text-gray-200'>
|
<strong className='text-gray-800 dark:text-gray-200'>
|
||||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
</strong>
|
</strong>
|
||||||
</bdi>
|
</bdi>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Account from 'pl-fe/components/account';
|
||||||
import Icon from 'pl-fe/components/icon';
|
import Icon from 'pl-fe/components/icon';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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';
|
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
||||||
id='notification.move'
|
id='notification.move'
|
||||||
defaultMessage='{name} moved to {targetName}'
|
defaultMessage='{name} moved to {targetName}'
|
||||||
values={{
|
values={{
|
||||||
name: <span dangerouslySetInnerHTML={{ __html: from.display_name_html }} />,
|
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
|
||||||
targetName: to.acct,
|
targetName: to.acct,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Link from 'pl-fe/components/link';
|
import Link from 'pl-fe/components/link';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { makeGetStatus } from 'pl-fe/selectors';
|
import { makeGetStatus } from 'pl-fe/selectors';
|
||||||
|
|
||||||
|
@ -28,10 +29,11 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => {
|
||||||
id='compose.reply_group_indicator.message'
|
id='compose.reply_group_indicator.message'
|
||||||
defaultMessage='Posting to {groupLink}'
|
defaultMessage='Posting to {groupLink}'
|
||||||
values={{
|
values={{
|
||||||
groupLink: <Link
|
groupLink: (
|
||||||
to={`/groups/${group.id}`}
|
<Link to={`/groups/${group.id}`}>
|
||||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
/>,
|
</Link>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -70,13 +70,13 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
|
||||||
withRelationship={false}
|
withRelationship={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!!account.note_emojified && (
|
{!!account.note && (
|
||||||
<Text
|
<Text
|
||||||
truncate
|
truncate
|
||||||
align='left'
|
align='left'
|
||||||
className='line-clamp-2 inline text-ellipsis [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
className='line-clamp-2 inline text-ellipsis [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||||
>
|
>
|
||||||
<ParsedContent html={account.note_emojified} />
|
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
|
@ -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<string, CustomEmoji>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaybeEmoji: React.FC<IMaybeEmoji> = ({ 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 <img draggable={false} className='emojione' alt={text} title={text} src={filename} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IEmojify {
|
||||||
|
text: string;
|
||||||
|
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Emojify: React.FC<IEmojify> = ({ 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(
|
||||||
|
<img draggable={false} className='emojione' alt={c} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||||
|
);
|
||||||
|
} else if (unqualified in unicodeMapping) {
|
||||||
|
clearStack();
|
||||||
|
|
||||||
|
const { unified, shortcode } = unicodeMapping[unqualified];
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
<img draggable={false} className='emojione' alt={unqualified} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||||
|
);
|
||||||
|
} else if (c === ':') {
|
||||||
|
if (!open) {
|
||||||
|
clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
stack += ':';
|
||||||
|
|
||||||
|
// we see another : we convert it and clear the stack buffer
|
||||||
|
if (open) {
|
||||||
|
nodes.push(<MaybeEmoji text={stack} emojis={emojis} />);
|
||||||
|
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 };
|
|
@ -225,5 +225,6 @@ export {
|
||||||
isCustomEmoji,
|
isCustomEmoji,
|
||||||
isNativeEmoji,
|
isNativeEmoji,
|
||||||
buildCustomEmojis,
|
buildCustomEmojis,
|
||||||
|
validEmojiChar,
|
||||||
emojify as default,
|
emojify as default,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||||
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
||||||
|
@ -414,7 +415,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||||
name: (
|
name: (
|
||||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||||
<HStack space={1} alignItems='center' grow>
|
<HStack space={1} alignItems='center' grow>
|
||||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
<span><Emojify text={account.display_name} emojis={account.emojis} /></span>
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
</HStack>
|
</HStack>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
|
|
||||||
|
import Emojify from '../emoji/emojify';
|
||||||
import ActionButton from '../ui/components/action-button';
|
import ActionButton from '../ui/components/action-button';
|
||||||
import { HotKeys } from '../ui/components/hotkeys';
|
import { HotKeys } from '../ui/components/hotkeys';
|
||||||
|
|
||||||
|
@ -41,14 +42,9 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack alignItems='center' justifyContent='center' space={1}>
|
<HStack alignItems='center' justifyContent='center' space={1}>
|
||||||
<Text
|
<Text weight='semibold' truncate align='center' size='sm' className='max-w-[95%]'>
|
||||||
weight='semibold'
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
</Text>
|
||||||
truncate
|
|
||||||
align='center'
|
|
||||||
size='sm'
|
|
||||||
className='max-w-[95%]'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||||
import { isDefaultHeader } from 'pl-fe/utils/accounts';
|
import { isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||||
|
|
||||||
|
@ -141,9 +142,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
<Text
|
<Text
|
||||||
size='xl'
|
size='xl'
|
||||||
weight='bold'
|
weight='bold'
|
||||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
|
||||||
data-testid='group-name'
|
data-testid='group-name'
|
||||||
/>
|
>
|
||||||
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Stack data-testid='group-meta' space={1} alignItems='center'>
|
<Stack data-testid='group-meta' space={1} alignItems='center'>
|
||||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||||
|
@ -157,7 +159,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
align='center'
|
align='center'
|
||||||
className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||||
>
|
>
|
||||||
<ParsedContent html={group.note_emojified} />
|
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||||
import toast from 'pl-fe/toast';
|
import toast from 'pl-fe/toast';
|
||||||
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
|
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||||
|
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||||
|
|
||||||
import AvatarPicker from '../edit-profile/components/avatar-picker';
|
import AvatarPicker from '../edit-profile/components/avatar-picker';
|
||||||
import HeaderPicker from '../edit-profile/components/header-picker';
|
import HeaderPicker from '../edit-profile/components/header-picker';
|
||||||
|
@ -51,7 +52,7 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
|
||||||
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
|
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
|
||||||
|
|
||||||
const displayName = useTextField(group?.display_name);
|
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 maxName = Number(instance.configuration.groups.max_characters_name);
|
||||||
const maxNote = Number(instance.configuration.groups.max_characters_description);
|
const maxNote = Number(instance.configuration.groups.max_characters_description);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Text from 'pl-fe/components/ui/text';
|
||||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||||
import toast from 'pl-fe/toast';
|
import toast from 'pl-fe/toast';
|
||||||
|
|
||||||
|
import Emojify from '../emoji/emojify';
|
||||||
import ColumnForbidden from '../ui/components/column-forbidden';
|
import ColumnForbidden from '../ui/components/column-forbidden';
|
||||||
|
|
||||||
type RouteParams = { groupId: string };
|
type RouteParams = { groupId: string };
|
||||||
|
@ -86,7 +87,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/groups/${group.id}/manage/edit`}>
|
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/groups/${group.id}/manage/edit`}>
|
||||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
<span><Emojify text={group.display_name} emojis={group.emojis} /></span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,13 +7,14 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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 GroupActionButton from 'pl-fe/features/group/components/group-action-button';
|
||||||
import { shortNumberFormat } from 'pl-fe/utils/numbers';
|
import { shortNumberFormat } from 'pl-fe/utils/numbers';
|
||||||
|
|
||||||
import type { Group } from 'pl-fe/normalizers/group';
|
import type { Group } from 'pl-fe/normalizers/group';
|
||||||
|
|
||||||
interface IGroupListItem {
|
interface IGroupListItem {
|
||||||
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name_html' | 'locked' | 'members_count' | 'relationship'>;
|
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name' | 'emojis' | 'locked' | 'members_count' | 'relationship'>;
|
||||||
withJoinAction?: boolean;
|
withJoinAction?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,11 +35,9 @@ const GroupListItem = (props: IGroupListItem) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack className='overflow-hidden'>
|
<Stack className='overflow-hidden'>
|
||||||
<Text
|
<Text weight='bold' truncate>
|
||||||
weight='bold'
|
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
</Text>
|
||||||
truncate
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HStack className='text-gray-700 dark:text-gray-600' space={1} alignItems='center'>
|
<HStack className='text-gray-700 dark:text-gray-600' space={1} alignItems='center'>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
@ -13,6 +13,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import AccountContainer from 'pl-fe/containers/account-container';
|
import AccountContainer from 'pl-fe/containers/account-container';
|
||||||
import StatusContainer from 'pl-fe/containers/status-container';
|
import StatusContainer from 'pl-fe/containers/status-container';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
|
@ -37,14 +38,15 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
|
||||||
return output.join(', ');
|
return output.join(', ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLink = (account: Pick<Account, 'acct' | 'display_name_html' | 'id'>): JSX.Element => (
|
const buildLink = (account: Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>): JSX.Element => (
|
||||||
<HoverAccountWrapper key={account.acct} element='bdi' accountId={account.id}>
|
<HoverAccountWrapper key={account.acct} element='bdi' accountId={account.id}>
|
||||||
<Link
|
<Link
|
||||||
className='font-bold text-gray-800 hover:underline dark:text-gray-200'
|
className='font-bold text-gray-800 hover:underline dark:text-gray-200'
|
||||||
title={account.acct}
|
title={account.acct}
|
||||||
to={`/@${account.acct}`}
|
to={`/@${account.acct}`}
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
>
|
||||||
/>
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
|
</Link>
|
||||||
</HoverAccountWrapper>
|
</HoverAccountWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -153,7 +155,7 @@ const messages: Record<NotificationType | 'reply', MessageDescriptor> = defineMe
|
||||||
const buildMessage = (
|
const buildMessage = (
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
type: NotificationType | 'reply',
|
type: NotificationType | 'reply',
|
||||||
accounts: Array<Pick<Account, 'acct' | 'display_name_html' | 'id'>>,
|
accounts: Array<Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>>,
|
||||||
targetName: string,
|
targetName: string,
|
||||||
instanceTitle: string,
|
instanceTitle: string,
|
||||||
): React.ReactNode => {
|
): React.ReactNode => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
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 QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||||
|
|
||||||
import StatusInteractionBar from './status-interaction-bar';
|
import StatusInteractionBar from './status-interaction-bar';
|
||||||
|
@ -66,7 +67,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
<Link to={`/groups/${status.group.id}`} className='hover:underline'>
|
<Link to={`/groups/${status.group.id}`} className='hover:underline'>
|
||||||
<bdi className='truncate'>
|
<bdi className='truncate'>
|
||||||
<strong className='text-gray-800 dark:text-gray-200'>
|
<strong className='text-gray-800 dark:text-gray-200'>
|
||||||
<span dangerouslySetInnerHTML={{ __html: status.group.display_name_html }} />
|
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||||
</strong>
|
</strong>
|
||||||
</bdi>
|
</bdi>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Modal from 'pl-fe/components/ui/modal';
|
||||||
import Spinner from 'pl-fe/components/ui/spinner';
|
import Spinner from 'pl-fe/components/ui/spinner';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
|
|
||||||
|
@ -43,7 +44,6 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
||||||
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
|
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
|
||||||
{versions?.map((version) => {
|
{versions?.map((version) => {
|
||||||
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
|
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
|
||||||
const spoilerContent = { __html: version.spoilerHtml };
|
|
||||||
|
|
||||||
const poll = typeof version.poll !== 'string' && version.poll;
|
const poll = typeof version.poll !== 'string' && version.poll;
|
||||||
|
|
||||||
|
@ -51,7 +51,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
||||||
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
||||||
{version.spoiler_text?.length > 0 && (
|
{version.spoiler_text?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
<span>
|
||||||
|
<Emojify text={version.spoiler_text} emojis={version.emojis} />
|
||||||
|
</span>
|
||||||
<hr />
|
<hr />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -71,7 +73,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
||||||
role='radio'
|
role='radio'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
<span>
|
||||||
|
<ParsedContent html={option.title} emojis={version.emojis} />
|
||||||
|
</span>
|
||||||
</HStack>
|
</HStack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||||
import Modal from 'pl-fe/components/ui/modal';
|
import Modal from 'pl-fe/components/ui/modal';
|
||||||
import Spinner from 'pl-fe/components/ui/spinner';
|
import Spinner from 'pl-fe/components/ui/spinner';
|
||||||
import AccountContainer from 'pl-fe/containers/account-container';
|
import AccountContainer from 'pl-fe/containers/account-container';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { makeGetAccount } from 'pl-fe/selectors';
|
import { makeGetAccount } from 'pl-fe/selectors';
|
||||||
|
|
||||||
|
@ -31,7 +32,13 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
||||||
if (!account || !familiarFollowerIds) {
|
if (!account || !familiarFollowerIds) {
|
||||||
body = <Spinner />;
|
body = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
const emptyMessage = <FormattedMessage id='account.familiar_followers.empty' defaultMessage='No one you know follows {name}.' values={{ name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} /> }} />;
|
const emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.familiar_followers.empty'
|
||||||
|
defaultMessage='No one you know follows {name}.'
|
||||||
|
values={{ name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
body = (
|
body = (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
|
@ -54,7 +61,7 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='column.familiar_followers'
|
id='column.familiar_followers'
|
||||||
defaultMessage='People you know following {name}'
|
defaultMessage='People you know following {name}'
|
||||||
values={{ name: <span dangerouslySetInnerHTML={{ __html: account?.display_name_html || '' }} /> }}
|
values={{ name: !!account && <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClose={onClickClose}
|
onClose={onClickClose}
|
||||||
|
|
|
@ -64,7 +64,7 @@ const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
|
||||||
size='md'
|
size='md'
|
||||||
className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||||
>
|
>
|
||||||
<ParsedContent html={group.note_emojified} />
|
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import { fetchPinnedAccounts } from 'pl-fe/actions/accounts';
|
import { fetchPinnedAccounts } from 'pl-fe/actions/accounts';
|
||||||
import Widget from 'pl-fe/components/ui/widget';
|
import Widget from 'pl-fe/components/ui/widget';
|
||||||
import AccountContainer from 'pl-fe/containers/account-container';
|
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 { WhoToFollowPanel } from 'pl-fe/features/ui/util/async-components';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
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';
|
import type { Account } from 'pl-fe/normalizers/account';
|
||||||
|
|
||||||
interface IPinnedAccountsPanel {
|
interface IPinnedAccountsPanel {
|
||||||
account: Pick<Account, 'id' | 'display_name_html'>;
|
account: Pick<Account, 'id' | 'display_name' | 'emojis'>;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit })
|
||||||
id='pinned_accounts.title'
|
id='pinned_accounts.title'
|
||||||
defaultMessage='{name}’s choices'
|
defaultMessage='{name}’s choices'
|
||||||
values={{
|
values={{
|
||||||
name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />,
|
name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span>,
|
||||||
}}
|
}}
|
||||||
/>}
|
/>}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ProfileField from '../profile-field';
|
||||||
import type { Account } from 'pl-fe/normalizers/account';
|
import type { Account } from 'pl-fe/normalizers/account';
|
||||||
|
|
||||||
interface IProfileFieldsPanel {
|
interface IProfileFieldsPanel {
|
||||||
account: Pick<Account, 'fields'>;
|
account: Pick<Account, 'emojis' | 'fields'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Custom profile fields for sidebar. */
|
/** Custom profile fields for sidebar. */
|
||||||
|
@ -16,7 +16,7 @@ const ProfileFieldsPanel: React.FC<IProfileFieldsPanel> = ({ account }) => (
|
||||||
<Widget>
|
<Widget>
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
{account.fields.map((field, i) => (
|
{account.fields.map((field, i) => (
|
||||||
<ProfileField field={field} key={i} />
|
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||||
import { capitalize } from 'pl-fe/utils/strings';
|
import { capitalize } from 'pl-fe/utils/strings';
|
||||||
|
@ -122,8 +123,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ 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 memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||||
const badges = getBadges();
|
const badges = getBadges();
|
||||||
|
|
||||||
|
@ -132,7 +131,11 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
<Text size='lg' weight='bold' truncate>
|
||||||
|
{account.deactivated
|
||||||
|
? <FormattedMessage id='account.deactivated' defaultMessage='Deactivated' />
|
||||||
|
: <Emojify text={account.display_name} emojis={account.emojis} />}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||||
|
|
||||||
|
@ -160,9 +163,9 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||||
|
|
||||||
<ProfileStats account={account} />
|
<ProfileStats account={account} />
|
||||||
|
|
||||||
{!!account.note_emojified && (
|
{!!account.note && (
|
||||||
<Markup size='sm'>
|
<Markup size='sm'>
|
||||||
<ParsedContent html={account.note_emojified} />
|
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||||
</Markup>
|
</Markup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -208,7 +211,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||||
{account.fields.length > 0 && (
|
{account.fields.length > 0 && (
|
||||||
<Stack space={2} className='mt-4 xl:hidden'>
|
<Stack space={2} className='mt-4 xl:hidden'>
|
||||||
{account.fields.map((field, i) => (
|
{account.fields.map((field, i) => (
|
||||||
<ProfileField field={field} key={i} />
|
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||||
import { getAcct } from 'pl-fe/utils/accounts';
|
import { getAcct } from 'pl-fe/utils/accounts';
|
||||||
|
@ -29,7 +30,6 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||||
const fqn = useAppSelector((state) => displayFqn(state));
|
const fqn = useAppSelector((state) => displayFqn(state));
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const displayNameHtml = { __html: account.display_name_html };
|
|
||||||
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
||||||
const header = account.header;
|
const header = account.header;
|
||||||
const verified = account.verified;
|
const verified = account.verified;
|
||||||
|
@ -67,7 +67,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||||
<Stack>
|
<Stack>
|
||||||
<Link to={`/@${account.acct}`}>
|
<Link to={`/@${account.acct}`}>
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
<Text size='lg' weight='bold' truncate>
|
||||||
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
|
</Text>
|
||||||
|
|
||||||
{verified && <VerificationBadge />}
|
{verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||||
import PollOption from 'pl-fe/components/polls/poll-option';
|
import PollOption from 'pl-fe/components/polls/poll-option';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
|
|
||||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
import type { Poll } from 'pl-api';
|
||||||
|
|
||||||
interface IPollPreview {
|
interface IPollPreview {
|
||||||
poll: Poll;
|
poll: Poll;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||||
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||||
|
@ -51,12 +52,9 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
||||||
<HoverAccountWrapper accountId={account.id} key={account.id} element='span'>
|
<HoverAccountWrapper accountId={account.id} key={account.id} element='span'>
|
||||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||||
<HStack space={1} alignItems='center' grow>
|
<HStack space={1} alignItems='center' grow>
|
||||||
<Text
|
<Text size='sm' theme='primary' truncate>
|
||||||
size='sm'
|
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||||
theme='primary'
|
</Text>
|
||||||
truncate
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{account.verified && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
|
@ -3,9 +3,12 @@ import React from 'react';
|
||||||
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
||||||
|
|
||||||
import Markup from 'pl-fe/components/markup';
|
import Markup from 'pl-fe/components/markup';
|
||||||
|
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||||
import HStack from 'pl-fe/components/ui/hstack';
|
import HStack from 'pl-fe/components/ui/hstack';
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
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 { 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';
|
import type { Account } from 'pl-fe/normalizers/account';
|
||||||
|
|
||||||
|
@ -28,32 +31,35 @@ const dateFormatOptions: FormatDateOptions = {
|
||||||
|
|
||||||
interface IProfileField {
|
interface IProfileField {
|
||||||
field: Account['fields'][number];
|
field: Account['fields'][number];
|
||||||
|
emojis?: Account['emojis'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders a single profile field. */
|
/** Renders a single profile field. */
|
||||||
const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
const ProfileField: React.FC<IProfileField> = ({ field, emojis }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
if (isTicker(field.name)) {
|
if (isTicker(field.name)) {
|
||||||
return (
|
return (
|
||||||
<CryptoAddress
|
<CryptoAddress
|
||||||
ticker={getTicker(field.name).toLowerCase()}
|
ticker={getTicker(field.name).toLowerCase()}
|
||||||
address={field.value_plain}
|
address={unescapeHTML(field.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (isZapEmoji(field.name)) {
|
} else if (isZapEmoji(field.name)) {
|
||||||
return <LightningAddress address={field.value_plain} />;
|
return <LightningAddress address={unescapeHTML(field.value)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl>
|
<dl>
|
||||||
<dt title={field.name}>
|
<dt title={field.name}>
|
||||||
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
<Markup weight='bold' tag='span'>
|
||||||
|
<Emojify text={field.name} emojis={emojis} />
|
||||||
|
</Markup>
|
||||||
</dt>
|
</dt>
|
||||||
|
|
||||||
<dd
|
<dd
|
||||||
className={clsx({ 'text-success-500': field.verified_at })}
|
className={clsx({ 'text-success-500': field.verified_at })}
|
||||||
title={field.value_plain}
|
title={unescapeHTML(field.value)}
|
||||||
>
|
>
|
||||||
<HStack space={2} alignItems='center'>
|
<HStack space={2} alignItems='center'>
|
||||||
{field.verified_at && (
|
{field.verified_at && (
|
||||||
|
@ -62,7 +68,9 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Markup className='overflow-hidden break-words' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
<Markup className='overflow-hidden break-words' tag='span'>
|
||||||
|
<ParsedContent html={field.value} emojis={emojis} />
|
||||||
|
</Markup>
|
||||||
</HStack>
|
</HStack>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
@ -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';
|
import type { Account as BaseAccount } from 'pl-api';
|
||||||
|
|
||||||
const getDomainFromURL = (account: Pick<BaseAccount, 'url'>): string => {
|
const getDomainFromURL = (account: Pick<BaseAccount, 'url'>): string => {
|
||||||
|
@ -34,8 +28,6 @@ const normalizeAccount = (account: BaseAccount) => {
|
||||||
const domain = fqn.split('@')[1] || '';
|
const domain = fqn.split('@')[1] || '';
|
||||||
const note = account.note === '<p></p>' ? '' : account.note;
|
const note = account.note === '<p></p>' ? '' : account.note;
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(account.emojis);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mute_expires_at: null,
|
mute_expires_at: null,
|
||||||
...account,
|
...account,
|
||||||
|
@ -46,15 +38,6 @@ const normalizeAccount = (account: BaseAccount) => {
|
||||||
fqn,
|
fqn,
|
||||||
domain,
|
domain,
|
||||||
note,
|
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),
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import emojify from 'pl-fe/features/emoji';
|
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';
|
import type { AdminAnnouncement as BaseAdminAnnouncement, Announcement as BaseAnnouncement } from 'pl-api';
|
||||||
|
|
||||||
const normalizeAnnouncement = <T extends BaseAnnouncement = BaseAnnouncement>(announcement: T) => {
|
const normalizeAnnouncement = <T extends BaseAnnouncement = BaseAnnouncement>(announcement: T) => {
|
||||||
const emojiMap = makeCustomEmojiMap(announcement.emojis);
|
const emojiMap = makeEmojiMap(announcement.emojis);
|
||||||
|
|
||||||
const contentHtml = emojify(announcement.content, emojiMap);
|
const contentHtml = emojify(announcement.content, emojiMap);
|
||||||
|
|
||||||
|
|
|
@ -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';
|
import type { Group as BaseGroup } from 'pl-api';
|
||||||
|
|
||||||
const getDomainFromURL = (group: Pick<BaseGroup, 'url'>): string => {
|
const getDomainFromURL = (group: Pick<BaseGroup, 'url'>): string => {
|
||||||
|
@ -20,9 +14,6 @@ const normalizeGroup = (group: BaseGroup) => {
|
||||||
const missingHeader = require('pl-fe/assets/images/header-missing.png');
|
const missingHeader = require('pl-fe/assets/images/header-missing.png');
|
||||||
|
|
||||||
const domain = getDomainFromURL(group);
|
const domain = getDomainFromURL(group);
|
||||||
const note = group.note === '<p></p>' ? '' : group.note;
|
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(group.emojis);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
|
@ -31,10 +22,6 @@ const normalizeGroup = (group: BaseGroup) => {
|
||||||
header: group.header || group.header_static || missingHeader,
|
header: group.header || group.header_static || missingHeader,
|
||||||
header_static: group.header_static || group.header || missingHeader,
|
header_static: group.header_static || group.header || missingHeader,
|
||||||
domain,
|
domain,
|
||||||
note,
|
|
||||||
display_name_html: emojify(escapeTextContentForBrowser(group.display_name), emojiMap),
|
|
||||||
note_emojified: emojify(group.note, emojiMap),
|
|
||||||
note_plain: unescapeHTML(group.note),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,13 @@ import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from 'pl-fe/features/emoji';
|
import emojify from 'pl-fe/features/emoji';
|
||||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||||
|
|
||||||
import { normalizePollEdit } from './poll';
|
|
||||||
|
|
||||||
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
||||||
|
|
||||||
const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => {
|
const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => {
|
||||||
const emojiMap = makeEmojiMap(statusEdit.emojis);
|
const emojiMap = makeEmojiMap(statusEdit.emojis);
|
||||||
|
|
||||||
const poll = statusEdit.poll ? normalizePollEdit(statusEdit.poll, statusEdit.emojis) : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...statusEdit,
|
...statusEdit,
|
||||||
poll,
|
|
||||||
contentHtml: emojify(statusEdit.content, emojiMap),
|
contentHtml: emojify(statusEdit.content, emojiMap),
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap),
|
spoilerHtml: emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { POLLS_IMPORT } from 'pl-fe/actions/importer';
|
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';
|
import type { AnyAction } from 'redux';
|
||||||
|
|
||||||
type State = ImmutableMap<string, ReturnType<typeof normalizePoll>>;
|
type State = ImmutableMap<string, Poll>;
|
||||||
|
|
||||||
const importPolls = (state: State, polls: Array<Exclude<Status['poll'], null>>) =>
|
const importPolls = (state: State, polls: Array<Exclude<Status['poll'], null>>) =>
|
||||||
state.withMutations(map =>
|
state.withMutations(map =>
|
||||||
polls.forEach(poll => map.set(poll.id, normalizePoll(poll))),
|
polls.forEach(poll => map.set(poll.id, poll)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialState: State = ImmutableMap();
|
const initialState: State = ImmutableMap();
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
|
||||||
import type { CustomEmoji } from 'pl-api';
|
|
||||||
|
|
||||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||||
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
||||||
v.pipe(
|
v.pipe(
|
||||||
|
@ -14,13 +12,6 @@ const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Map a list of CustomEmoji to their shortcodes. */
|
|
||||||
const makeCustomEmojiMap = (customEmojis: CustomEmoji[]) =>
|
|
||||||
customEmojis.reduce<Record<string, CustomEmoji>>((result, emoji) => {
|
|
||||||
result[`:${emoji.shortcode}:`] = emoji;
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
/** valibot schema to force the value into an object, if it isn't already. */
|
/** valibot schema to force the value into an object, if it isn't already. */
|
||||||
const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
|
const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
|
||||||
v.pipe(
|
v.pipe(
|
||||||
|
@ -29,4 +20,4 @@ const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
|
||||||
v.object(shape),
|
v.object(shape),
|
||||||
);
|
);
|
||||||
|
|
||||||
export { filteredArray, makeCustomEmojiMap, coerceObject };
|
export { filteredArray, coerceObject };
|
||||||
|
|
Loading…
Reference in a new issue