Merge branch 'markup-css' into 'develop'

Add <Markup> component to style markup from the API

See merge request soapbox-pub/soapbox!1922
environments/review-develop-3zknud/deployments/1487
Alex Gleason 2 years ago
commit 5f9ce5776e

@ -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;
}

@ -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,19 +10,14 @@ 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 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,
}
@ -49,7 +44,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();
@ -131,29 +125,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;
@ -173,30 +144,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}
/>,
];
@ -212,14 +177,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,14 +2,13 @@ import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { prepareRequest } from 'soapbox/actions/consumer-auth';
import Markup from 'soapbox/components/markup';
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification-badge';
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
import { capitalize } from 'soapbox/utils/strings';
import './instance-description.css';
const LandingPage = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
@ -114,12 +113,10 @@ const LandingPage = () => {
{instance.title}
</h1>
<Text size='lg'>
<span
className='instance-description'
<Markup
size='lg'
dangerouslySetInnerHTML={{ __html: instance.short_description || instance.description }}
/>
</Text>
</Stack>
</div>
</div>

@ -1,14 +0,0 @@
/* Instance HTML from the API. */
.instance-description a {
@apply underline;
}
.instance-description b,
.instance-description strong {
@apply font-bold;
}
.instance-description i,
.instance-description em {
@apply italic;
}

@ -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…
Cancel
Save