bigbuffet-rw/packages/pl-fe/src/components/status-content.tsx

225 lines
6.9 KiB
TypeScript
Raw Normal View History

2023-02-06 10:01:03 -08:00
import clsx from 'clsx';
import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
2022-04-15 14:59:42 -07:00
import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses';
import Icon from 'pl-fe/components/icon';
import { Button, Stack, Text } from 'pl-fe/components/ui';
import { useAppDispatch, useSettings } from 'pl-fe/hooks';
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
2022-04-15 14:59:42 -07:00
2023-10-10 18:02:22 -07:00
import { getTextDirection } from '../utils/rtl';
2022-04-15 14:59:42 -07:00
import Markup from './markup';
import { ParsedContent } from './parsed-content';
2022-06-15 12:59:17 -07:00
import Poll from './polls/poll';
import type { Sizes } from 'pl-fe/components/ui/text';
import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
2022-04-15 14:59:42 -07:00
const BIG_EMOJI_LIMIT = 10;
interface IReadMoreButton {
onClick: React.MouseEventHandler;
quote?: boolean;
poll?: boolean;
}
/** Button to expand a truncated status (due to too much content) */
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick, quote, poll }) => (
<div className='relative -mt-4'>
<div
className={clsx('absolute -top-16 h-16 w-full bg-gradient-to-b from-transparent', {
'to-white black:to-black dark:to-primary-900': !poll,
'to-gray-100 dark:to-primary-800': poll,
'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote,
})}
/>
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
<Icon className='inline-block size-5' src={require('@tabler/icons/outline/chevron-right.svg')} />
</button>
</div>
);
2022-04-15 14:59:42 -07:00
interface IStatusContent {
status: MinifiedStatus;
onClick?: () => void;
collapsable?: boolean;
translatable?: boolean;
textSize?: Sizes;
quote?: boolean;
2022-04-15 14:59:42 -07:00
}
/** Renders the text content of a status */
const StatusContent: React.FC<IStatusContent> = React.memo(({
status,
onClick,
collapsable = false,
translatable,
2023-01-30 11:38:29 -08:00
textSize = 'md',
quote = false,
}) => {
const dispatch = useAppDispatch();
const { displaySpoilers } = useSettings();
2022-04-15 14:59:42 -07:00
const [collapsed, setCollapsed] = useState(false);
const [onlyEmoji, setOnlyEmoji] = useState(false);
const [lineClamp, setLineClamp] = useState(true);
2022-04-15 14:59:42 -07:00
const node = useRef<HTMLDivElement>(null);
const spoilerNode = useRef<HTMLSpanElement>(null);
2022-04-15 14:59:42 -07:00
const maybeSetCollapsed = (): void => {
if (!node.current) return;
if (collapsable && !collapsed) {
// 20px * x lines (+ 2px padding at the top)
if (node.current.clientHeight > (quote ? 202 : 282)) {
2022-04-15 14:59:42 -07:00
setCollapsed(true);
}
}
};
const maybeSetOnlyEmoji = (): void => {
if (!node.current) return;
const only = isOnlyEmoji(node.current, BIG_EMOJI_LIMIT, true);
if (only !== onlyEmoji) {
setOnlyEmoji(only);
}
};
const toggleExpanded: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
e.stopPropagation();
if (expanded) dispatch(collapseStatusSpoiler(status.id));
else dispatch(expandStatusSpoiler(status.id));
};
useLayoutEffect(() => {
2022-04-15 14:59:42 -07:00
maybeSetCollapsed();
maybeSetOnlyEmoji();
});
const parsedHtml = useMemo(
(): string => translatable && status.translation
? status.translation.content!
: (status.contentMapHtml && status.currentLanguage)
? (status.contentMapHtml[status.currentLanguage] || status.contentHtml)
: status.contentHtml,
[status.contentHtml, status.translation, status.currentLanguage],
);
2022-04-15 14:59:42 -07:00
useEffect(() => {
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);
}, [spoilerNode.current]);
const withSpoiler = status.spoiler_text.length > 0;
const spoilerText = status.spoilerMapHtml && status.currentLanguage
? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml
: status.spoilerHtml;
2023-10-10 18:02:22 -07:00
const direction = getTextDirection(status.search_index);
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
'cursor-pointer': onClick,
'overflow-hidden': collapsed,
'max-h-[200px]': collapsed && !quote,
'max-h-[120px]': collapsed && quote,
'leading-normal big-emoji': onlyEmoji,
2022-04-15 14:59:42 -07:00
});
const expandable = !displaySpoilers;
const expanded = !withSpoiler || status.expanded || false;
const output = [];
if (spoilerText) {
output.push(
<Text key='spoiler' size='2xl' weight='medium'>
<span
className={clsx({ 'line-clamp-3': !expanded && lineClamp })}
dangerouslySetInnerHTML={{ __html: spoilerText }}
ref={spoilerNode}
/>
{status.content && expandable && (
<Button
className='ml-2 align-middle'
type='button'
theme='muted'
size='xs'
onClick={toggleExpanded}
icon={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
>
{expanded
? <FormattedMessage id='status.spoiler.collapse' defaultMessage='Collapse' />
: <FormattedMessage id='status.spoiler.expand' defaultMessage='Expand' />}
</Button>
)}
</Text>,
);
}
if (expandable && !expanded) return <>{output}</>;
if (onClick) {
if (status.content) {
output.push(
<Markup
ref={node}
tabIndex={0}
key='content'
className={className}
direction={direction}
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
);
}
2022-04-15 14:59:42 -07:00
const hasPoll = !!status.poll_id;
2022-04-15 14:59:42 -07:00
if (collapsed) {
output.push(<ReadMoreButton onClick={onClick} key='read-more' quote={quote} poll={hasPoll} />);
2022-04-15 14:59:42 -07:00
}
if (status.poll_id) {
output.push(<Poll id={status.poll_id} key='poll' status={status} />);
2022-04-15 14:59:42 -07:00
}
return <Stack space={4} className={clsx({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</Stack>;
2022-04-15 14:59:42 -07:00
} else {
if (status.content) {
output.push(
<Markup
ref={node}
tabIndex={0}
key='content'
className={className}
direction={direction}
lang={status.language || undefined}
size={textSize}
>
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
</Markup>,
);
}
2022-04-15 14:59:42 -07:00
if (collapsed) {
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
}
if (status.poll_id) {
output.push(<Poll id={status.poll_id} key='poll' status={status} />);
2022-04-15 14:59:42 -07:00
}
return <>{output}</>;
}
});
2022-04-15 14:59:42 -07:00
export { StatusContent as default };