Merge branch 'develop' into hooks-migration
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
39b595940b
37 changed files with 494 additions and 461 deletions
|
@ -24,14 +24,10 @@
|
|||
</body>
|
||||
<script>
|
||||
window.BIG_BUFFET_CONFIG = {
|
||||
baseUrl: 'https://pleroma.woodynet.net/',
|
||||
baseUrl: 'https://pl.fediverse.pl/',
|
||||
mountPoint: 'bigbuffet',
|
||||
styles: ['default', 'generated'],
|
||||
icons: {},
|
||||
homePage: {
|
||||
type: 'hashtag',
|
||||
hashtag: 'ahashtag',
|
||||
},
|
||||
locale: 'en',
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
"emoji-mart": "^5.5.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"graphemesplit": "^2.4.4",
|
||||
"html-react-parser": "^5.1.18",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^4.3.7",
|
||||
"intersection-observer": "^0.12.2",
|
||||
|
|
|
@ -14,6 +14,8 @@ import ActionButton from 'bigbuffet/features/ui/components/action-button';
|
|||
import { UserPanel } from 'bigbuffet/features/ui/util/async-components';
|
||||
import { useAccountHoverCardStore } from 'bigbuffet/stores/account-hover-card';
|
||||
|
||||
import { ParsedContent } from './parsed-content';
|
||||
|
||||
import type { Account } from 'bigbuffet/normalizers/account';
|
||||
|
||||
const showAccountHoverCard = debounce((openAccountHoverCard, ref, accountId) => {
|
||||
|
@ -93,7 +95,6 @@ export const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }
|
|||
};
|
||||
|
||||
if (!account) return null;
|
||||
const accountBio = { __html: account.note_emojified };
|
||||
// const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||
|
||||
return (
|
||||
|
@ -139,7 +140,9 @@ export const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }
|
|||
) : null} */}
|
||||
|
||||
{account.note.length > 0 && (
|
||||
<p className='account-hover-card__note-date' dangerouslySetInnerHTML={accountBio} />
|
||||
<p className='account-hover-card__note-date'>
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
|
|
|
@ -7,6 +7,7 @@ import RelativeTimestamp from 'bigbuffet/components/relative-timestamp';
|
|||
import Avatar from 'bigbuffet/components/ui/avatar';
|
||||
import VerificationBadge from 'bigbuffet/components/verification-badge';
|
||||
import bigBuffetConfig from 'bigbuffet/config';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import ActionButton from 'bigbuffet/features/ui/components/action-button';
|
||||
import { getAcct } from 'bigbuffet/utils/accounts';
|
||||
|
||||
|
@ -83,7 +84,9 @@ const Account = ({
|
|||
|
||||
const displayName = (
|
||||
<div className='account-card__account__name__display-name'>
|
||||
<p dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<p>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</p>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
|
@ -95,7 +98,11 @@ const Account = ({
|
|||
<div className='account-card__account'>
|
||||
<ProfilePopper
|
||||
condition={showAccountHoverCard}
|
||||
wrapper={(children) => <HoverAccountWrapper className='relative' accountId={account.id} inline>{children}</HoverAccountWrapper>}
|
||||
wrapper={(children) => (
|
||||
<HoverAccountWrapper className='relative' accountId={account.id} element='span'>
|
||||
{children}
|
||||
</HoverAccountWrapper>
|
||||
)}
|
||||
>
|
||||
{withLinkToProfile ? (account.acct.includes('@') ? (
|
||||
<a
|
||||
|
@ -123,7 +130,7 @@ const Account = ({
|
|||
<div className='account-card__account__name'>
|
||||
<ProfilePopper
|
||||
condition={showAccountHoverCard}
|
||||
wrapper={(children) => <HoverAccountWrapper accountId={account.id} inline>{children}</HoverAccountWrapper>}
|
||||
wrapper={(children) => <HoverAccountWrapper accountId={account.id} element='span'>{children}</HoverAccountWrapper>}
|
||||
>
|
||||
{withLinkToProfile ? (account.acct.includes('@') ? (
|
||||
<a
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
|
||||
import HoverAccountWrapper from 'bigbuffet/components/hover-account-wrapper';
|
||||
import bigBuffetConfig from 'bigbuffet/config';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
|
||||
import { getAcct } from '../utils/accounts';
|
||||
|
||||
|
@ -10,7 +11,7 @@ import VerificationBadge from './verification-badge';
|
|||
import type { Account } from 'bigbuffet/normalizers/account';
|
||||
|
||||
interface IDisplayName {
|
||||
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>;
|
||||
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name' | 'emojis'>;
|
||||
withSuffix?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
@ -21,7 +22,9 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
|
|||
|
||||
const displayName = (
|
||||
<div className='display-name__inner'>
|
||||
<p dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<p>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</p>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
</div>
|
||||
|
@ -31,7 +34,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
|
|||
|
||||
return (
|
||||
<span className='display-name' data-testid='display-name'>
|
||||
<HoverAccountWrapper accountId={account.id} inline>
|
||||
<HoverAccountWrapper accountId={account.id} element='span'>
|
||||
{displayName}
|
||||
</HoverAccountWrapper>
|
||||
{withSuffix && suffix}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import Icon from 'bigbuffet/components/icon';
|
||||
import VerificationBadge from 'bigbuffet/components/verification-badge';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import EventActionButton from 'bigbuffet/features/event/components/event-action-button';
|
||||
import EventDate from 'bigbuffet/features/event/components/event-date';
|
||||
|
||||
|
@ -56,7 +57,9 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
<div className='event-preview__detail'>
|
||||
<Icon icon='user' />
|
||||
<div className='display-name__inner'>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
15
src/components/hashtag-link.tsx
Normal file
15
src/components/hashtag-link.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import Link from './link';
|
||||
|
||||
interface IHashtagLink {
|
||||
hashtag: string;
|
||||
}
|
||||
|
||||
const HashtagLink: React.FC<IHashtagLink> = ({ hashtag }) => (
|
||||
<Link to={`/tags/${hashtag}`} onClick={(e) => e.stopPropagation()}>
|
||||
#{hashtag}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export { HashtagLink as default };
|
|
@ -17,17 +17,16 @@ const showAccountHoverCard = debounce((
|
|||
|
||||
interface IHoverAccountWrapper {
|
||||
accountId: string;
|
||||
inline?: boolean;
|
||||
element?: 'div' | 'span' | 'bdi';
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Makes a profile hover card appear when the wrapped element is hovered. */
|
||||
export const HoverAccountWrapper: React.FC<IHoverAccountWrapper> = ({ accountId, children, inline = false, className }) => {
|
||||
export const HoverAccountWrapper: React.FC<IHoverAccountWrapper> = ({ accountId, children, element: Elem = 'div', className }) => {
|
||||
const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardStore();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div';
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMobile(window.innerWidth) && ref.current) {
|
||||
|
|
118
src/components/parsed-content.tsx
Normal file
118
src/components/parsed-content.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
import HashtagLink from './hashtag-link';
|
||||
import HoverAccountWrapper from './hover-account-wrapper';
|
||||
import StatusMention from './status-mention';
|
||||
|
||||
import type { CustomEmoji, Mention } from 'pl-api';
|
||||
|
||||
const nodesToText = (nodes: Array<DOMNode>): string =>
|
||||
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
||||
|
||||
interface IParsedContent {
|
||||
/** HTML content to display. */
|
||||
html: string;
|
||||
/** Array of mentioned accounts. */
|
||||
mentions?: Array<Mention>;
|
||||
/** Whether it's a status which has a quote. */
|
||||
hasQuote?: boolean;
|
||||
/** Related custom emojis. */
|
||||
emojis?: Array<CustomEmoji>;
|
||||
}
|
||||
|
||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => useMemo(() => {
|
||||
if (html.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
||||
|
||||
const selectors: Array<string> = [];
|
||||
|
||||
// Quote posting
|
||||
if (hasQuote) selectors.push('quote-inline');
|
||||
|
||||
const options: HTMLReactParserOptions = {
|
||||
replace(domNode) {
|
||||
if (!(domNode instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['script', 'iframe'].includes(domNode.name)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (domNode.attribs.class?.split(' ').some(className => selectors.includes(className))) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (domNode.name === 'a') {
|
||||
const classes = domNode.attribs.class?.split(' ');
|
||||
|
||||
const fallback = (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
{...domNode.attribs}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel='nofollow noopener'
|
||||
target='_blank'
|
||||
title={domNode.attribs.href}
|
||||
>
|
||||
{domToReact(domNode.children as DOMNode[], options)}
|
||||
</a>
|
||||
);
|
||||
|
||||
if (classes?.includes('mention')) {
|
||||
if (mentions) {
|
||||
const mention = mentions.find(({ url }) => domNode.attribs.href === url);
|
||||
if (mention) {
|
||||
return (
|
||||
<HoverAccountWrapper accountId={mention.id} element='span'>
|
||||
<Link
|
||||
to={`/@${mention.acct}`}
|
||||
className='mention status-link'
|
||||
dir='ltr'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{mention.username}
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
}
|
||||
} else if (domNode.attribs['data-user']) {
|
||||
return (
|
||||
<StatusMention accountId={domNode.attribs['data-user']} fallback={fallback} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (classes?.includes('hashtag')) {
|
||||
const hashtag = nodesToText(domNode.children as Array<DOMNode>);
|
||||
if (hashtag) {
|
||||
return <HashtagLink hashtag={hashtag.replace(/^#/, '')} />;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}, [html, emojis]));
|
||||
|
||||
export { ParsedContent };
|
|
@ -5,7 +5,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import RelativeTimestamp from '../relative-timestamp';
|
||||
import Tooltip from '../ui/tooltip';
|
||||
|
||||
import type { Poll } from 'bigbuffet/normalizers/poll';
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Motion, presets, spring } from 'react-motion';
|
||||
|
||||
import type { Poll } from 'bigbuffet/normalizers/poll';
|
||||
import { ParsedContent } from '../parsed-content';
|
||||
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const PollPercentageBar: React.FC<{ percent: number }> = ({ percent }): JSX.Element => (
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
|
||||
|
@ -38,7 +40,9 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<PollPercentageBar percent={percent} />
|
||||
|
||||
<div className='poll-option__label'>
|
||||
<p dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
||||
<p>
|
||||
<ParsedContent html={option.title} emojis={poll.emojis} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='poll-option__percentage'>
|
||||
|
|
|
@ -74,6 +74,7 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
|
|||
}, ref) => {
|
||||
const { autoloadMore } = bigBuffetConfig;
|
||||
|
||||
const listRef = React.useRef<HTMLDivElement>(null);
|
||||
const parentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
/** Normalized children. */
|
||||
|
@ -92,6 +93,7 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
|
|||
count: data.length + (hasMore ? 1 : 0),
|
||||
overscan: 3,
|
||||
estimateSize: () => estimatedSize,
|
||||
scrollMargin: listRef.current ? listRef.current.getBoundingClientRect().top + window.scrollY : 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -156,6 +158,7 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
|
|||
style={style}
|
||||
>
|
||||
<div
|
||||
ref={listRef}
|
||||
className={listClassName}
|
||||
style={{
|
||||
height: !showLoading && data.length ? virtualizer.getTotalSize() : undefined,
|
||||
|
@ -174,7 +177,7 @@ const ScrollableList = React.forwardRef<Virtualizer<any, any>, IScrollableList &
|
|||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
transform: `translateY(${item.start}px)`,
|
||||
transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
|
||||
}}
|
||||
>
|
||||
{renderItem(item.index)}
|
||||
|
|
|
@ -4,8 +4,10 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import Icon from 'bigbuffet/components/icon';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { onlyEmoji as isOnlyEmoji } from 'bigbuffet/utils/rich-content';
|
||||
|
||||
import { ParsedContent } from './parsed-content';
|
||||
import Poll from './polls/poll';
|
||||
|
||||
import type { MinifiedStatus } from 'bigbuffet/reducers/statuses';
|
||||
|
@ -138,7 +140,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, collapsable = false }
|
|||
|
||||
const withSpoiler = status.spoiler_text.length > 0;
|
||||
|
||||
const content = { __html: status.contentHtml };
|
||||
const className = clsx('status-content', {
|
||||
'status-content--with-spoiler': withSpoiler,
|
||||
'status-content--collapsed': !!visibleElementsHeight,
|
||||
|
@ -150,7 +151,7 @@ const StatusContent: React.FC<IStatusContent> = ({ status, collapsable = false }
|
|||
if (status.spoiler_text) {
|
||||
output.push(
|
||||
<p key='status-title' className='status-title'>
|
||||
<span dangerouslySetInnerHTML={{ __html: status.spoiler_text }} />
|
||||
<Emojify text={status.spoiler_text} emojis={status.emojis} />
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
|
@ -163,13 +164,19 @@ const StatusContent: React.FC<IStatusContent> = ({ status, collapsable = false }
|
|||
className={clsx(className, {
|
||||
'big-emoji': onlyEmoji,
|
||||
})}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.language || undefined}
|
||||
data-markup
|
||||
style={{
|
||||
maxHeight: visibleElementsHeight,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<ParsedContent
|
||||
html={status.content}
|
||||
mentions={status.mentions}
|
||||
hasQuote={!!status.quote_id}
|
||||
emojis={status.emojis}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
||||
if (status.translation) {
|
||||
|
@ -181,9 +188,15 @@ const StatusContent: React.FC<IStatusContent> = ({ status, collapsable = false }
|
|||
tabIndex={0}
|
||||
key='status-content-translated'
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: status.translation.content! }}
|
||||
data-markup
|
||||
/>
|
||||
>
|
||||
<ParsedContent
|
||||
html={status.translation.content}
|
||||
mentions={status.mentions}
|
||||
hasQuote={!!status.quote_id}
|
||||
emojis={status.emojis}
|
||||
/>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
|
|
35
src/components/status-mention.tsx
Normal file
35
src/components/status-mention.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useAccount } from 'pl-hooks';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import HoverAccountWrapper from './hover-account-wrapper';
|
||||
|
||||
interface IStatusMention {
|
||||
accountId: string;
|
||||
fallback?: JSX.Element;
|
||||
}
|
||||
|
||||
const StatusMention: React.FC<IStatusMention> = ({ accountId, fallback }) => {
|
||||
const { data: account } = useAccount(accountId);
|
||||
|
||||
if (!account) return (
|
||||
<HoverAccountWrapper accountId={accountId} element='span'>
|
||||
{fallback}
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<HoverAccountWrapper accountId={accountId} element='span'>
|
||||
<Link
|
||||
to={`/@${account.acct}`}
|
||||
className='mention'
|
||||
dir='ltr'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{account.acct}
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusMention as default };
|
|
@ -51,7 +51,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
|
||||
if (hoverable) {
|
||||
return (
|
||||
<HoverAccountWrapper key={account.id} accountId={account.id} inline>
|
||||
<HoverAccountWrapper key={account.id} accountId={account.id} element='span'>
|
||||
{link}
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { toggleStatusHidden } from 'bigbuffet/actions/statuses';
|
|||
import Icon from 'bigbuffet/components/icon';
|
||||
import TranslateButton from 'bigbuffet/components/translate-button';
|
||||
import AccountContainer from 'bigbuffet/containers/account-container';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import StatusInteractionBar from 'bigbuffet/features/status/components/status-interaction-bar';
|
||||
import QuotedStatus from 'bigbuffet/features/status/containers/quoted-status-container';
|
||||
import { HotKeys } from 'bigbuffet/features/ui/components/hotkeys';
|
||||
|
@ -146,8 +147,6 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
let rebloggedByText, reblogElement, reblogElementMobile;
|
||||
|
||||
if (status.reblog && typeof status.reblog === 'object') {
|
||||
const displayNameHtml = { __html: String(status.account.display_name_html) };
|
||||
|
||||
reblogElement = (
|
||||
<NavLink
|
||||
to={`/@${status.account.acct}`}
|
||||
|
@ -161,9 +160,13 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
id='status.reblogged_by'
|
||||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: <bdi>
|
||||
<strong dangerouslySetInnerHTML={displayNameHtml} />
|
||||
</bdi>,
|
||||
name: (
|
||||
<bdi>
|
||||
<strong>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -183,7 +186,13 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
id='status.reblogged_by'
|
||||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi>,
|
||||
name: (
|
||||
<bdi>
|
||||
<strong>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { toggleStatusHidden } from 'bigbuffet/actions/statuses';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { useAppDispatch } from 'bigbuffet/hooks/useAppDispatch';
|
||||
|
||||
import Button from '../ui/button';
|
||||
|
@ -17,7 +18,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ISensitiveContentOverlay {
|
||||
status: Pick<Status, 'id' | 'sensitive' | 'hidden' | 'media_attachments' | 'currentLanguage' | 'spoilerHtml' | 'spoilerMapHtml' | 'spoiler_text'>;
|
||||
status: Pick<Status, 'id' | 'sensitive' | 'hidden' | 'media_attachments' | 'currentLanguage' | 'spoiler_text' | 'emojis'>;
|
||||
}
|
||||
|
||||
const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>(({ status }, ref) => {
|
||||
|
@ -65,7 +66,9 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
{status.spoiler_text && (
|
||||
<div className='sensitive-content-overlay__spoiler'>
|
||||
<p>
|
||||
“<span dangerouslySetInnerHTML={{ __html: status.spoilerHtml }} />”
|
||||
“<span>
|
||||
<Emojify text={status.spoiler_text} emojis={status.emojis} />
|
||||
</span>”
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Icon from './icon';
|
|||
import type { Status } from 'bigbuffet/normalizers/status';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Pick<Status, 'id' | 'account' | 'contentHtml' | 'contentMapHtml' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
status: Pick<Status, 'id' | 'account' | 'content' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
}
|
||||
|
||||
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
|
@ -28,7 +28,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
allow_unauthenticated: allowUnauthenticated,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = allowUnauthenticated && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
const renderTranslate = allowUnauthenticated && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.trim().length && status.language !== null && intl.locale !== status.language;
|
||||
|
||||
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import Account from 'bigbuffet/components/account';
|
||||
import Icon from 'bigbuffet/components/icon';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
|
||||
import type { Account as AccountEntity } from 'bigbuffet/normalizers/account';
|
||||
|
||||
interface IMovedNote {
|
||||
from: Pick<AccountEntity, 'display_name_html'>;
|
||||
from: Pick<AccountEntity, 'display_name' | 'emojis'>;
|
||||
to: AccountEntity;
|
||||
}
|
||||
|
||||
|
@ -21,7 +22,11 @@ const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
|||
id='notification.move'
|
||||
defaultMessage='{name} moved to {targetName}'
|
||||
values={{
|
||||
name: <span dangerouslySetInnerHTML={{ __html: from.display_name_html }} />,
|
||||
name: (
|
||||
<span>
|
||||
<Emojify text={from.display_name} emojis={from.emojis} />
|
||||
</span>
|
||||
),
|
||||
targetName: to.acct,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
export interface NativeEmoji {
|
||||
interface NativeEmoji {
|
||||
unified: string;
|
||||
native: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface CustomEmoji {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export interface Emoji<T> {
|
||||
interface Emoji<T> {
|
||||
id: string;
|
||||
name: string;
|
||||
keywords: string[];
|
||||
|
@ -17,27 +13,10 @@ export interface Emoji<T> {
|
|||
version?: number;
|
||||
}
|
||||
|
||||
export interface EmojiCategory {
|
||||
id: string;
|
||||
emojis: string[];
|
||||
}
|
||||
|
||||
export interface EmojiMap {
|
||||
interface EmojiMap {
|
||||
[s: string]: Emoji<NativeEmoji>;
|
||||
}
|
||||
|
||||
export interface EmojiAlias {
|
||||
[s: string]: string;
|
||||
}
|
||||
|
||||
export interface EmojiSheet {
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export interface EmojiData {
|
||||
categories: EmojiCategory[];
|
||||
emojis: EmojiMap;
|
||||
aliases: EmojiAlias;
|
||||
sheet: EmojiSheet;
|
||||
}
|
||||
|
|
|
@ -1,196 +0,0 @@
|
|||
import split from 'graphemesplit';
|
||||
|
||||
import * as BuildConfig from 'bigbuffet/build-config';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
/*
|
||||
* TODO: Consolate emoji object types
|
||||
*
|
||||
* There are five different emoji objects currently
|
||||
* - emoji-mart's "onPickEmoji" handler
|
||||
* - emoji-mart's custom emoji types
|
||||
* - an Emoji type that is either NativeEmoji or CustomEmoji
|
||||
* - a type inside redux's `store.custom_emoji` immutablejs
|
||||
*
|
||||
* there needs to be one type for the picker handler callback
|
||||
* and one type for the emoji-mart data
|
||||
* and one type that is used everywhere that the above two are converted into
|
||||
*/
|
||||
|
||||
export interface CustomEmoji {
|
||||
id: string;
|
||||
colons: string;
|
||||
custom: true;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface NativeEmoji {
|
||||
id: string;
|
||||
colons: string;
|
||||
custom?: false;
|
||||
unified: string;
|
||||
native: string;
|
||||
}
|
||||
|
||||
export type Emoji = CustomEmoji | NativeEmoji;
|
||||
|
||||
const isAlphaNumeric = (c: string) => {
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (!(code > 47 && code < 58) && // numeric (0-9)
|
||||
!(code > 64 && code < 91) && // upper alpha (A-Z)
|
||||
!(code > 96 && code < 123)) { // lower alpha (a-z)
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const validEmojiChar = (c: string) => isAlphaNumeric(c)
|
||||
|| c === '_'
|
||||
|| c === '-'
|
||||
|| c === '.';
|
||||
|
||||
const convertCustom = (shortname: string, filename: string) =>
|
||||
`<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
|
||||
const convertUnicode = (c: string) => {
|
||||
const { unified, shortcode } = unicodeMapping[c];
|
||||
|
||||
return `<img draggable="false" class="emojione" alt="${c}" title=":${shortcode}:" src="/${BuildConfig.ASSETS_DIR}/emoji/${unified}.svg" />`;
|
||||
};
|
||||
|
||||
const convertEmoji = (str: string, customEmojis: any) => {
|
||||
if (str.length < 3) return str;
|
||||
if (str in customEmojis) {
|
||||
const emoji = customEmojis[str];
|
||||
const filename = emoji.static_url;
|
||||
|
||||
if (filename?.length > 0) {
|
||||
return convertCustom(str, filename);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
export const emojifyText = (str: string, customEmojis = {}) => {
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
const clearStack = () => {
|
||||
buf += stack;
|
||||
open = false;
|
||||
stack = '';
|
||||
};
|
||||
|
||||
for (let c of split(str)) {
|
||||
// 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) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(c);
|
||||
} else if (unqualified in unicodeMapping) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(unqualified);
|
||||
} else if (c === ':') {
|
||||
stack += ':';
|
||||
|
||||
// we see another : we convert it and clear the stack buffer
|
||||
if (open) {
|
||||
buf += convertEmoji(stack, customEmojis);
|
||||
stack = '';
|
||||
}
|
||||
|
||||
open = !open;
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
|
||||
// if the stack is non-null and we see invalid chars it's a string not emoji
|
||||
// so we push it to the return result and clear it
|
||||
if (!validEmojiChar(c)) {
|
||||
clearStack();
|
||||
}
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// never found a closing colon so it's just a raw string
|
||||
if (open) {
|
||||
buf += stack;
|
||||
}
|
||||
|
||||
return buf;
|
||||
};
|
||||
|
||||
export const parseHTML = (str: string): { text: boolean; data: string }[] => {
|
||||
const tokens = [];
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
for (const c of str) {
|
||||
if (c === '<') {
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: stack });
|
||||
stack = '<';
|
||||
} else {
|
||||
tokens.push({ text: true, data: buf });
|
||||
stack = '<';
|
||||
open = true;
|
||||
}
|
||||
} else if (c === '>') {
|
||||
if (open) {
|
||||
open = false;
|
||||
tokens.push({ text: false, data: stack + '>' });
|
||||
stack = '';
|
||||
buf = '';
|
||||
} else {
|
||||
buf += '>';
|
||||
}
|
||||
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: buf + stack });
|
||||
} else if (buf !== '') {
|
||||
tokens.push({ text: true, data: buf });
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const emojify = (str: string, customEmojis = {}) => parseHTML(str)
|
||||
.map(({ text, data }) => {
|
||||
if (!text) return data;
|
||||
if (data.length === 0 || data === ' ') return data;
|
||||
|
||||
return emojifyText(data, customEmojis);
|
||||
})
|
||||
.join('');
|
||||
|
||||
export default emojify;
|
119
src/features/emoji/index.tsx
Normal file
119
src/features/emoji/index.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import split from 'graphemesplit';
|
||||
import React from 'react';
|
||||
|
||||
import * as BuildConfig from 'bigbuffet/build-config';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
const isAlphaNumeric = (c: string) => {
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (!(code > 47 && code < 58) && // numeric (0-9)
|
||||
!(code > 64 && code < 91) && // upper alpha (A-Z)
|
||||
!(code > 96 && code < 123)) { // lower alpha (a-z)
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const validEmojiChar = (c: string) => isAlphaNumeric(c)
|
||||
|| c === '_'
|
||||
|| c === '-'
|
||||
|| c === '.';
|
||||
|
||||
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={`/${BuildConfig.ASSETS_DIR}/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={`/${BuildConfig.ASSETS_DIR}/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 };
|
|
@ -8,6 +8,7 @@ import StillImage from 'bigbuffet/components/still-image';
|
|||
import IconButton from 'bigbuffet/components/ui/icon-button';
|
||||
import Menu, { MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'bigbuffet/components/ui/menu';
|
||||
import VerificationBadge from 'bigbuffet/components/verification-badge';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { useFeatures } from 'bigbuffet/hooks/useFeatures';
|
||||
import { useModalsStore } from 'bigbuffet/stores/modals';
|
||||
import copy from 'bigbuffet/utils/copy';
|
||||
|
@ -167,7 +168,9 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
name: (
|
||||
<Link className='mention' to={`/@${account.acct}`}>
|
||||
<div className='display-name__inner'>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
</Link>
|
||||
|
@ -184,8 +187,8 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
id='event.participants'
|
||||
defaultMessage='{count} {rawCount, plural, one {person} other {people}} going'
|
||||
values={{
|
||||
rawCount: event.participants_count || 0,
|
||||
count: shortNumberFormat(event.participants_count || 0),
|
||||
rawCount: event.participants_count,
|
||||
count: shortNumberFormat(event.participants_count),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -122,7 +122,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params: { statusId: sta
|
|||
|
||||
return (
|
||||
<div className='event-information'>
|
||||
{!!status.contentHtml.trim() && (
|
||||
{!!status.content.trim().length && (
|
||||
<div className='event-information__description'>
|
||||
<p className='event-information__heading'>
|
||||
<FormattedMessage id='event.description' defaultMessage='Description' />
|
||||
|
|
|
@ -4,8 +4,10 @@ import { FormattedDate, FormattedMessage } from 'react-intl';
|
|||
|
||||
import { fetchHistory } from 'bigbuffet/actions/history';
|
||||
import AttachmentThumbs from 'bigbuffet/components/attachment-thumbs';
|
||||
import { ParsedContent } from 'bigbuffet/components/parsed-content';
|
||||
import Modal from 'bigbuffet/components/ui/modal';
|
||||
import Spinner from 'bigbuffet/components/ui/spinner';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { useAppDispatch } from 'bigbuffet/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'bigbuffet/hooks/useAppSelector';
|
||||
|
||||
|
@ -29,7 +31,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
dispatch(fetchHistory(statusId));
|
||||
}, [statusId]);
|
||||
|
||||
let body;
|
||||
let body: JSX.Element;
|
||||
|
||||
if (loading) {
|
||||
body = <Spinner />;
|
||||
|
@ -37,26 +39,27 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
body = (
|
||||
<div className='compare-history'>
|
||||
{versions?.map((version) => {
|
||||
const content = { __html: version.contentHtml };
|
||||
const spoilerContent = { __html: version.spoilerHtml };
|
||||
|
||||
const poll = typeof version.poll !== 'string' && version.poll;
|
||||
|
||||
return (
|
||||
<div className='compare-history__version'>
|
||||
{version.spoiler_text?.length > 0 && (
|
||||
<>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||
<span>
|
||||
<Emojify text={version.spoiler_text} emojis={version.emojis} />
|
||||
</span>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='status__content' dangerouslySetInnerHTML={content} />
|
||||
<p className='status__content'>
|
||||
<ParsedContent html={version.content} emojis={version.emojis} />
|
||||
</p>
|
||||
|
||||
{poll && (
|
||||
<div className='poll'>
|
||||
<div>
|
||||
{poll.options.map((option: any) => (
|
||||
{poll.options.map((option) => (
|
||||
<div className='poll-option'>
|
||||
<span
|
||||
className={clsx('poll-option__check')}
|
||||
|
@ -64,7 +67,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
role='radio'
|
||||
/>
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
||||
<span>
|
||||
<ParsedContent html={option.title} emojis={version.emojis} />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,9 @@ import React from 'react';
|
|||
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
||||
|
||||
import Icon from 'bigbuffet/components/icon';
|
||||
import { ParsedContent } from 'bigbuffet/components/parsed-content';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { unescapeHTML } from 'bigbuffet/utils/html';
|
||||
|
||||
import type { Account } from 'bigbuffet/normalizers/account';
|
||||
|
||||
|
@ -24,10 +27,11 @@ const dateFormatOptions: FormatDateOptions = {
|
|||
|
||||
interface IProfileField {
|
||||
field: Account['fields'][number];
|
||||
emojis?: Account['emojis'];
|
||||
}
|
||||
|
||||
/** Renders a single profile field. */
|
||||
const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
||||
const ProfileField: React.FC<IProfileField> = ({ field, emojis }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
// if (isTicker(field.name)) {
|
||||
|
@ -42,10 +46,12 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
return (
|
||||
<dl className={clsx('profile-info__field', { 'profile-info__field--verified': field.verified_at })}>
|
||||
<dt title={field.name}>
|
||||
<span dangerouslySetInnerHTML={{ __html: field.name_emojified }} data-markup />
|
||||
<span data-markup>
|
||||
<Emojify text={field.name} emojis={emojis} />
|
||||
</span>
|
||||
</dt>
|
||||
|
||||
<dd title={field.value_plain}>
|
||||
<dd title={unescapeHTML(field.value)}>
|
||||
<div>
|
||||
{field.verified_at && (
|
||||
<span className='profile-info__field__verified' title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(field.verified_at, dateFormatOptions) })}>
|
||||
|
@ -53,7 +59,9 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: field.value_emojified }} data-markup />
|
||||
<span data-markup>
|
||||
<ParsedContent html={field.value} emojis={emojis} />
|
||||
</span>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -3,7 +3,9 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
|||
|
||||
import Badge from 'bigbuffet/components/badge';
|
||||
import Icon from 'bigbuffet/components/icon';
|
||||
import { ParsedContent } from 'bigbuffet/components/parsed-content';
|
||||
import bigBuffetConfig from 'bigbuffet/config';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { badgeToTag, getBadges as getAccountBadges } from 'bigbuffet/utils/badges';
|
||||
import { capitalize } from 'bigbuffet/utils/strings';
|
||||
|
||||
|
@ -100,8 +102,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 badges = getBadges();
|
||||
|
||||
return (
|
||||
|
@ -109,7 +109,11 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
<div className='profile-info'>
|
||||
<div className='profile-info__name'>
|
||||
<div className='profile-info__display-name'>
|
||||
<p dangerouslySetInnerHTML={displayNameHtml} />
|
||||
<p>
|
||||
{account.deactivated
|
||||
? <FormattedMessage id='account.deactivated' defaultMessage='Deactivated' />
|
||||
: <Emojify text={account.display_name} emojis={account.emojis} />}
|
||||
</p>
|
||||
|
||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||
|
||||
|
@ -135,7 +139,9 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
</div>
|
||||
|
||||
{account.note.length > 0 && (
|
||||
<p className='profile-info__note' dangerouslySetInnerHTML={{ __html: account.note_emojified }} data-markup />
|
||||
<p className='profile-info__note' data-markup>
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className='profile-info__details'>
|
||||
|
@ -156,7 +162,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
{account.fields.length > 0 && (
|
||||
<div className='profile-info__fields'>
|
||||
{account.fields.map((field, i) => (
|
||||
<ProfileField field={field} key={i} />
|
||||
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import StillImage from 'bigbuffet/components/still-image';
|
|||
import Avatar from 'bigbuffet/components/ui/avatar';
|
||||
import VerificationBadge from 'bigbuffet/components/verification-badge';
|
||||
import bigBuffetConfig from 'bigbuffet/config';
|
||||
import Emojify from 'bigbuffet/features/emoji';
|
||||
import { getAcct } from 'bigbuffet/utils/accounts';
|
||||
|
||||
interface IUserPanel {
|
||||
|
@ -20,7 +21,6 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
const { displayFqn } = bigBuffetConfig;
|
||||
|
||||
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;
|
||||
|
@ -53,7 +53,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
<div className='user-panel__name'>
|
||||
<Link to={`/@${account.acct}`}>
|
||||
<div className='user-panel__display-name'>
|
||||
<p dangerouslySetInnerHTML={displayNameHtml} />
|
||||
<p>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</p>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import bigBuffetConfig from 'bigbuffet/config';
|
|||
import defaultStyle from 'bigbuffet/styles/application.scss?url';
|
||||
import { generateThemeCss } from 'bigbuffet/utils/theme';
|
||||
|
||||
console.log(defaultStyle);
|
||||
|
||||
const { styles } = bigBuffetConfig;
|
||||
|
||||
const GeneratedTheme = () => {
|
||||
|
|
|
@ -1,59 +1,18 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
import emojify from 'bigbuffet/features/emoji';
|
||||
import { unescapeHTML } from 'bigbuffet/utils/html';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
import type { Account as BaseAccount } from 'pl-api';
|
||||
|
||||
const getDomainFromURL = (account: Pick<BaseAccount, 'url'>): string => {
|
||||
try {
|
||||
const url = account.url;
|
||||
return new URL(url).host;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const guessFqn = (account: Pick<BaseAccount, 'acct' | 'url'>): string => {
|
||||
const acct = account.acct;
|
||||
const [user, domain] = acct.split('@');
|
||||
|
||||
if (domain) {
|
||||
return acct;
|
||||
} else {
|
||||
return [user, getDomainFromURL(account)].join('@');
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeAccount = (account: BaseAccount) => {
|
||||
const missingAvatar: string = require('bigbuffet/assets/images/avatar-missing.png');
|
||||
const missingHeader: string = require('bigbuffet/assets/images/header-missing.png');
|
||||
|
||||
const fqn = account.fqn || guessFqn(account);
|
||||
const domain = fqn.split('@')[1] || '';
|
||||
const note = account.note === '<p></p>' ? '' : account.note;
|
||||
|
||||
const emojiMap = makeEmojiMap(account.emojis);
|
||||
|
||||
return {
|
||||
...account,
|
||||
avatar: account.avatar || account.avatar_static || missingAvatar,
|
||||
avatar_static: account.avatar_static || account.avatar || missingAvatar,
|
||||
header: account.header || account.header_static || missingHeader,
|
||||
header_static: account.header_static || account.header || missingHeader,
|
||||
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),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { Status as BaseStatus, StatusEdit as BaseStatusEdit, CustomEmoji } from 'pl-api';
|
||||
|
||||
import emojify from 'bigbuffet/features/emoji';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
const sanitizeTitle = (text: string, emojiMap: Record<string, CustomEmoji>) =>
|
||||
DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { ALLOWED_TAGS: [] });
|
||||
|
||||
const normalizePoll = (poll: Exclude<BaseStatus['poll'], null>) => {
|
||||
const emojiMap = makeEmojiMap(poll.emojis);
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
title_map_emojified: option.title_map
|
||||
? Object.fromEntries(Object.entries(option.title_map).map(([key, title]) => [key, sanitizeTitle(title, emojiMap)]))
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePollEdit = (poll: Exclude<BaseStatusEdit['poll'], null>, emojis: Array<CustomEmoji>) => {
|
||||
const emojiMap = makeEmojiMap(emojis);
|
||||
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
type Poll = ReturnType<typeof normalizePoll>;
|
||||
type PollEdit = ReturnType<typeof normalizePollEdit>;
|
||||
|
||||
export {
|
||||
normalizePoll,
|
||||
normalizePollEdit,
|
||||
type Poll,
|
||||
type PollEdit,
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* Status edit normalizer
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
import emojify from 'bigbuffet/features/emoji';
|
||||
import { makeEmojiMap } from 'bigbuffet/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: DOMPurify.sanitize(emojify(statusEdit.content, emojiMap), { ADD_ATTR: ['target'] }),
|
||||
spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap), { ADD_ATTR: ['target'] }),
|
||||
};
|
||||
};
|
||||
|
||||
type StatusEdit = ReturnType<typeof normalizeStatusEdit>
|
||||
|
||||
export { type StatusEdit, normalizeStatusEdit };
|
|
@ -3,17 +3,12 @@
|
|||
* Converts API statuses into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/status/}
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import emojify from 'bigbuffet/features/emoji';
|
||||
import { unescapeHTML } from 'bigbuffet/utils/html';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
import { normalizeAccount } from './account';
|
||||
import { normalizePoll } from './poll';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
@ -22,10 +17,6 @@ type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' |
|
|||
|
||||
type CalculatedValues = {
|
||||
search_index: string;
|
||||
contentHtml: string;
|
||||
spoilerHtml: string;
|
||||
contentMapHtml?: Record<string, string>;
|
||||
spoilerMapHtml?: Record<string, string>;
|
||||
hidden?: boolean;
|
||||
translation?: Translation | null | false;
|
||||
currentLanguage?: string;
|
||||
|
@ -61,32 +52,20 @@ const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoi
|
|||
return unescapeHTML(fields.join('\n\n')) || '';
|
||||
};
|
||||
|
||||
const calculateContent = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(text, emojiMap), { USE_PROFILES: { html: true } });
|
||||
const calculateSpoiler = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } });
|
||||
|
||||
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {
|
||||
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
|
||||
const {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, translation, currentLanguage,
|
||||
search_index, hidden, translation, currentLanguage,
|
||||
} = oldStatus;
|
||||
|
||||
return {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, translation, currentLanguage,
|
||||
search_index, hidden, translation, currentLanguage,
|
||||
};
|
||||
} else {
|
||||
const searchContent = buildSearchContent(status);
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
|
||||
return {
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: calculateContent(status.content, emojiMap),
|
||||
spoilerHtml: calculateSpoiler(status.spoiler_text, emojiMap),
|
||||
contentMapHtml: status.content_map
|
||||
? Object.fromEntries(Object.entries(status.content_map)?.map(([key, value]) => [key, calculateContent(value, emojiMap)]))
|
||||
: undefined,
|
||||
spoilerMapHtml: status.spoiler_text_map
|
||||
? Object.fromEntries(Object.entries(status.spoiler_text_map).map(([key, value]) => [key, calculateSpoiler(value, emojiMap)]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -124,8 +103,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
} | null) = null;
|
||||
let media_attachments = status.media_attachments;
|
||||
|
||||
// Normalize poll
|
||||
const poll = status.poll ? normalizePoll(status.poll) : null;
|
||||
|
||||
if (status.event) {
|
||||
const firstAttachment = status.media_attachments[0];
|
||||
|
@ -164,7 +141,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
content: status.content === '<p></p>' ? '' : status.content,
|
||||
filtered: status.filtered?.map(result => result.filter.title),
|
||||
event,
|
||||
poll,
|
||||
media_attachments,
|
||||
...calculated,
|
||||
translation: (status.translation || calculated.translation || null) as Translation | null | false,
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import emojify from 'bigbuffet/features/emoji';
|
||||
import { makeEmojiMap } from 'bigbuffet/utils/normalizers';
|
||||
|
||||
import type { Status, Translation as BaseTranslation } from 'pl-api';
|
||||
|
||||
const normalizeTranslation = (translation: BaseTranslation, status: Pick<Status, 'emojis'>) => {
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
const content = emojify(translation.content, emojiMap);
|
||||
|
||||
return {
|
||||
...translation,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
type Translation = ReturnType<typeof normalizeTranslation>;
|
||||
|
||||
export { normalizeTranslation, type Translation };
|
|
@ -1,7 +1,8 @@
|
|||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL, type HistoryAction } from 'bigbuffet/actions/history';
|
||||
import { normalizeStatusEdit, type StatusEdit } from 'bigbuffet/normalizers/status-edit';
|
||||
|
||||
import type { StatusEdit } from 'pl-api';
|
||||
|
||||
const HistoryRecord = ImmutableRecord({
|
||||
loading: false,
|
||||
|
@ -22,7 +23,7 @@ const history = (state: State = initialState, action: HistoryAction) => {
|
|||
case HISTORY_FETCH_SUCCESS:
|
||||
return state.update(action.statusId, HistoryRecord(), history => history!.withMutations(map => {
|
||||
map.set('loading', false);
|
||||
map.set('items', ImmutableList(action.history.map((x, i: number) => ({ ...x, original: i === 0 })).reverse().map(normalizeStatusEdit)));
|
||||
map.set('items', ImmutableList(action.history.map((x, i: number) => ({ ...x, original: i === 0 })).toReversed()));
|
||||
}));
|
||||
case HISTORY_FETCH_FAIL:
|
||||
return state.update(action.statusId, HistoryRecord(), history => history!.set('loading', false));
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { normalizeStatus } from 'bigbuffet/normalizers/status';
|
||||
import { normalizeTranslation } from 'bigbuffet/normalizers/translation';
|
||||
|
||||
import {
|
||||
STATUS_REVEAL,
|
||||
|
@ -21,15 +20,12 @@ type State = ImmutableMap<string, MinifiedStatus>;
|
|||
type MinifiedStatus = Omit<StatusRecord, 'reblog' | 'poll' | 'quote' | 'group'>;
|
||||
|
||||
/** Import translation from translation service into the store. */
|
||||
const importTranslation = (state: State, statusId: string, translation: Translation) => {
|
||||
const result = normalizeTranslation(translation, state.get(statusId)!);
|
||||
|
||||
return state.update(statusId, undefined as any, (status) => ({
|
||||
const importTranslation = (state: State, statusId: string, translation: Translation) =>
|
||||
state.update(statusId, undefined as any, (status) => ({
|
||||
...status,
|
||||
translation: result,
|
||||
translation,
|
||||
translating: false,
|
||||
}));
|
||||
};
|
||||
|
||||
/** Delete translation from the store. */
|
||||
const deleteTranslation = (state: State, statusId: string) => state.deleteIn([statusId, 'translation']);
|
||||
|
|
68
yarn.lock
68
yarn.lock
|
@ -2834,6 +2834,13 @@ domelementtype@^2.3.0:
|
|||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@5.0.3, domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domhandler@^4.2.0:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
|
||||
|
@ -2848,13 +2855,6 @@ domhandler@^4.3.1:
|
|||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.1.7:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.7.tgz#711a8c96479fb6ced93453732c160c3c72418a6a"
|
||||
|
@ -2869,7 +2869,7 @@ domutils@^2.8.0:
|
|||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
domutils@^3.0.1, domutils@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
|
||||
|
@ -4174,6 +4174,14 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-
|
|||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
html-dom-parser@5.0.10:
|
||||
version "5.0.10"
|
||||
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-5.0.10.tgz#bf46b05c50f35c2fcadfc8e91566c54d3caf9bd7"
|
||||
integrity sha512-GwArYL3V3V8yU/mLKoFF7HlLBv80BZ2Ey1BzfVNRpAci0cEKhFHI/Qh8o8oyt3qlAMLlK250wsxLdYX4viedvg==
|
||||
dependencies:
|
||||
domhandler "5.0.3"
|
||||
htmlparser2 "9.1.0"
|
||||
|
||||
html-encoding-sniffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
|
||||
|
@ -4194,11 +4202,31 @@ html-minifier-terser@^6.1.0:
|
|||
relateurl "^0.2.7"
|
||||
terser "^5.10.0"
|
||||
|
||||
html-react-parser@^5.1.18:
|
||||
version "5.1.18"
|
||||
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-5.1.18.tgz#a07ff6d95fcaa6de45244386a12dddb981434915"
|
||||
integrity sha512-65BwC0zzrdeW96jB2FRr5f1ovBhRMpLPJNvwkY5kA8Ay5xdL9t/RH2/uUTM7p+cl5iM88i6dDk4LXtfMnRmaJQ==
|
||||
dependencies:
|
||||
domhandler "5.0.3"
|
||||
html-dom-parser "5.0.10"
|
||||
react-property "2.0.2"
|
||||
style-to-js "1.1.16"
|
||||
|
||||
html-tags@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
|
||||
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
|
||||
|
||||
htmlparser2@9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
||||
integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.1.0"
|
||||
entities "^4.5.0"
|
||||
|
||||
http-link-header@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-1.1.3.tgz#b367b7a0ad1cf14027953f31aa1df40bb433da2a"
|
||||
|
@ -4313,6 +4341,11 @@ ini@^1.3.5:
|
|||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
inline-style-parser@0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz#f4af5fe72e612839fcd453d989a586566d695f22"
|
||||
integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==
|
||||
|
||||
internal-slot@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
|
||||
|
@ -6227,6 +6260,11 @@ react-motion@^0.5.2:
|
|||
prop-types "^15.5.8"
|
||||
raf "^3.1.0"
|
||||
|
||||
react-property@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6"
|
||||
integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==
|
||||
|
||||
react-redux@^9.0.4:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b"
|
||||
|
@ -7055,6 +7093,20 @@ style-loader@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-4.0.0.tgz#0ea96e468f43c69600011e0589cb05c44f3b17a5"
|
||||
integrity sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==
|
||||
|
||||
style-to-js@1.1.16:
|
||||
version "1.1.16"
|
||||
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a"
|
||||
integrity sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==
|
||||
dependencies:
|
||||
style-to-object "1.0.8"
|
||||
|
||||
style-to-object@1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292"
|
||||
integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==
|
||||
dependencies:
|
||||
inline-style-parser "0.2.4"
|
||||
|
||||
stylehacks@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738"
|
||||
|
|
Loading…
Reference in a new issue