Add <Markup> component to style markup from the API
This commit is contained in:
parent
39b4ee9f09
commit
7e32f0d992
7 changed files with 66 additions and 78 deletions
|
@ -1,77 +1,77 @@
|
|||
.status-content p {
|
||||
[data-markup] p {
|
||||
@apply mb-4 whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
.status-content p:last-child {
|
||||
[data-markup] p:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.status-content a {
|
||||
[data-markup] a {
|
||||
@apply text-primary-600 dark:text-accent-blue hover:underline;
|
||||
}
|
||||
|
||||
.status-content strong {
|
||||
[data-markup] strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.status-content em {
|
||||
[data-markup] em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.status-content ul,
|
||||
.status-content ol {
|
||||
[data-markup] ul,
|
||||
[data-markup] ol {
|
||||
@apply pl-10 mb-4;
|
||||
}
|
||||
|
||||
.status-content ul {
|
||||
[data-markup] ul {
|
||||
@apply list-disc list-outside;
|
||||
}
|
||||
|
||||
.status-content ol {
|
||||
[data-markup] ol {
|
||||
@apply list-decimal list-outside;
|
||||
}
|
||||
|
||||
.status-content blockquote {
|
||||
[data-markup] blockquote {
|
||||
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.status-content code {
|
||||
[data-markup] code {
|
||||
@apply cursor-text font-mono;
|
||||
}
|
||||
|
||||
.status-content p > code,
|
||||
.status-content pre {
|
||||
[data-markup] p > code,
|
||||
[data-markup] pre {
|
||||
@apply bg-gray-100 dark:bg-primary-800;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
.status-content p > code {
|
||||
[data-markup] p > code {
|
||||
@apply py-0.5 px-1 rounded-sm;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.status-content pre {
|
||||
[data-markup] pre {
|
||||
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
|
||||
}
|
||||
|
||||
.status-content pre:last-child {
|
||||
[data-markup] pre:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
/* Markdown images */
|
||||
.status-content img:not(.emojione):not([width][height]) {
|
||||
[data-markup] img:not(.emojione):not([width][height]) {
|
||||
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
|
||||
}
|
||||
|
||||
/* User setting to underline links */
|
||||
body.underline-links .status-content a {
|
||||
body.underline-links [data-markup] a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
.status-content .big-emoji img.emojione {
|
||||
[data-markup] .big-emoji img.emojione {
|
||||
@apply inline w-9 h-9 p-1;
|
||||
}
|
||||
|
||||
.status-content .status-link {
|
||||
[data-markup] .status-link {
|
||||
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
|
||||
}
|
16
app/soapbox/components/markup.tsx
Normal file
16
app/soapbox/components/markup.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import Text, { IText } from './ui/text/text';
|
||||
import './markup.css';
|
||||
|
||||
interface IMarkup extends IText {
|
||||
}
|
||||
|
||||
/** Styles HTML markup returned by the API, such as in account bios and statuses. */
|
||||
const Markup = React.forwardRef<any, IMarkup>((props, ref) => {
|
||||
return (
|
||||
<Text ref={ref} {...props} data-markup />
|
||||
);
|
||||
});
|
||||
|
||||
export default Markup;
|
|
@ -10,8 +10,8 @@ import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content';
|
|||
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
import Markup from './markup';
|
||||
import Poll from './polls/poll';
|
||||
import './status-content.css';
|
||||
import StopPropagation from './stop-propagation';
|
||||
|
||||
import type { Status, Mention } from 'soapbox/types/entities';
|
||||
|
@ -19,11 +19,6 @@ import type { Status, Mention } from 'soapbox/types/entities';
|
|||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||
const BIG_EMOJI_LIMIT = 10;
|
||||
|
||||
type Point = [
|
||||
x: number,
|
||||
y: number,
|
||||
]
|
||||
|
||||
interface IReadMoreButton {
|
||||
onClick: React.MouseEventHandler,
|
||||
}
|
||||
|
@ -52,7 +47,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
||||
|
||||
const startXY = useRef<Point>();
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { greentext } = useSoapboxConfig();
|
||||
|
@ -138,29 +132,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
updateStatusLinks();
|
||||
});
|
||||
|
||||
const handleMouseDown: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
startXY.current = [e.clientX, e.clientY];
|
||||
};
|
||||
|
||||
const handleMouseUp: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (!startXY.current) return;
|
||||
const target = e.target as HTMLElement;
|
||||
const parentNode = target.parentNode as HTMLElement;
|
||||
|
||||
const [startX, startY] = startXY.current;
|
||||
const [deltaX, deltaY] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
|
||||
|
||||
if (target.localName === 'button' || target.localName === 'a' || (parentNode && (parentNode.localName === 'button' || parentNode.localName === 'a'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) {
|
||||
onClick();
|
||||
}
|
||||
|
||||
startXY.current = undefined;
|
||||
};
|
||||
|
||||
const parsedHtml = useMemo((): string => {
|
||||
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||
|
||||
|
@ -180,30 +151,24 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
|
||||
|
||||
const content = { __html: parsedHtml };
|
||||
const directionStyle: React.CSSProperties = { direction: 'ltr' };
|
||||
const className = classNames(baseClassName, 'status-content', {
|
||||
const direction = isRtl(status.search_index) ? 'rtl' : 'ltr';
|
||||
const className = classNames(baseClassName, {
|
||||
'cursor-pointer': onClick,
|
||||
'whitespace-normal': withSpoiler,
|
||||
'max-h-[300px]': collapsed,
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
});
|
||||
|
||||
if (isRtl(status.search_index)) {
|
||||
directionStyle.direction = 'rtl';
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
const output = [
|
||||
<div
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
key='content'
|
||||
className={className}
|
||||
style={directionStyle}
|
||||
direction={direction}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.language || undefined}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
/>,
|
||||
];
|
||||
|
||||
|
@ -219,14 +184,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
return <div className={classNames({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
} else {
|
||||
const output = [
|
||||
<div
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
key='content'
|
||||
className={classNames(baseClassName, 'status-content', {
|
||||
className={classNames(baseClassName, {
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
})}
|
||||
style={directionStyle}
|
||||
direction={direction}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.language || undefined}
|
||||
/>,
|
||||
|
|
|
@ -54,7 +54,9 @@ export type Sizes = keyof typeof sizes
|
|||
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
|
||||
type Directions = 'ltr' | 'rtl'
|
||||
|
||||
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML'> {
|
||||
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML' | 'tabIndex' | 'lang'> {
|
||||
/** Text content. */
|
||||
children?: React.ReactNode,
|
||||
/** How to align the text. */
|
||||
align?: keyof typeof alignments,
|
||||
/** Extra class names for the outer element. */
|
||||
|
@ -84,8 +86,8 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'danger
|
|||
}
|
||||
|
||||
/** UI-friendly text container with dark mode support. */
|
||||
const Text: React.FC<IText> = React.forwardRef(
|
||||
(props: IText, ref: React.LegacyRef<any>) => {
|
||||
const Text = React.forwardRef<any, IText>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
align,
|
||||
className,
|
||||
|
|
|
@ -2,7 +2,8 @@ import classNames from 'clsx';
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage, FormatDateOptions } from 'react-intl';
|
||||
|
||||
import { Widget, Stack, HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Widget, Stack, HStack, Icon } from 'soapbox/components/ui';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||
import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
|
@ -51,7 +52,7 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
return (
|
||||
<dl>
|
||||
<dt title={field.name}>
|
||||
<Text weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
</dt>
|
||||
|
||||
<dd
|
||||
|
@ -65,7 +66,7 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<Text className='break-words overflow-hidden' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
<Markup className='break-words overflow-hidden' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
</HStack>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Icon, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
@ -139,13 +140,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
return (
|
||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
||||
<Stack space={2}>
|
||||
{/* Not sure if this is actual used. */}
|
||||
{/* <div className='profile-info-panel-content__deactivated'>
|
||||
<FormattedMessage
|
||||
id='account.deactivated_description' defaultMessage='This account has been deactivated.'
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<Stack>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
|
||||
|
@ -178,8 +172,8 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
<ProfileStats account={account} />
|
||||
|
||||
{account.note.length > 0 && account.note !== '<p></p>' && (
|
||||
<Text size='sm' dangerouslySetInnerHTML={content} />
|
||||
{account.note.length > 0 && (
|
||||
<Markup size='sm' dangerouslySetInnerHTML={content} />
|
||||
)}
|
||||
|
||||
<div className='flex flex-col md:flex-row items-start md:flex-wrap md:items-center gap-2'>
|
||||
|
|
|
@ -269,6 +269,15 @@ const fixBirthday = (account: ImmutableMap<string, any>) => {
|
|||
return account.set('birthday', birthday || '');
|
||||
};
|
||||
|
||||
/** Rewrite `<p></p>` to empty string. */
|
||||
const fixNote = (account: ImmutableMap<string, any>) => {
|
||||
if (account.get('note') === '<p></p>') {
|
||||
return account.set('note', '');
|
||||
} else {
|
||||
return account;
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeAccount = (account: Record<string, any>) => {
|
||||
return AccountRecord(
|
||||
ImmutableMap(fromJS(account)).withMutations(account => {
|
||||
|
@ -289,6 +298,7 @@ export const normalizeAccount = (account: Record<string, any>) => {
|
|||
fixUsername(account);
|
||||
fixDisplayName(account);
|
||||
fixBirthday(account);
|
||||
fixNote(account);
|
||||
addInternalFields(account);
|
||||
}),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue