2023-02-06 10:01:03 -08:00
|
|
|
import clsx from 'clsx';
|
2024-09-05 16:06:25 -07:00
|
|
|
import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react';
|
2024-10-19 09:02:13 -07:00
|
|
|
import { FormattedMessage } from 'react-intl';
|
2022-04-15 14:59:42 -07:00
|
|
|
|
2024-09-05 15:47:26 -07:00
|
|
|
import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses';
|
2024-08-28 04:41:08 -07:00
|
|
|
import Icon from 'pl-fe/components/icon';
|
2024-10-19 05:34:03 -07:00
|
|
|
import Button from 'pl-fe/components/ui/button';
|
|
|
|
import Stack from 'pl-fe/components/ui/stack';
|
|
|
|
import Text from 'pl-fe/components/ui/text';
|
2024-10-21 14:51:16 -07:00
|
|
|
import Emojify from 'pl-fe/features/emoji/emojify';
|
2024-11-06 07:46:04 -08:00
|
|
|
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
2024-10-25 15:06:13 -07:00
|
|
|
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
|
|
|
import { useSettings } from 'pl-fe/hooks/use-settings';
|
2024-08-28 04:41:08 -07:00
|
|
|
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
|
|
|
|
2024-11-06 07:46:04 -08:00
|
|
|
import HashtagsBar from './hashtags-bar';
|
2022-11-19 16:13:27 -08:00
|
|
|
import Markup from './markup';
|
2024-11-06 07:46:04 -08:00
|
|
|
import { parseContent } from './parsed-content';
|
2022-06-15 12:59:17 -07:00
|
|
|
import Poll from './polls/poll';
|
2024-11-06 07:46:04 -08:00
|
|
|
import StatusMedia from './status-media';
|
|
|
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
|
|
|
import TranslateButton from './translate-button';
|
2022-06-15 12:59:17 -07:00
|
|
|
|
2024-10-18 15:53:07 -07:00
|
|
|
import type { Sizes } from 'pl-fe/components/ui/text';
|
2024-08-28 04:41:08 -07:00
|
|
|
import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
|
2022-04-15 14:59:42 -07:00
|
|
|
|
|
|
|
const BIG_EMOJI_LIMIT = 10;
|
|
|
|
|
2022-04-15 15:41:53 -07:00
|
|
|
interface IReadMoreButton {
|
2023-10-02 11:54:02 -07:00
|
|
|
onClick: React.MouseEventHandler;
|
2024-08-31 10:50:45 -07:00
|
|
|
quote?: boolean;
|
2024-09-11 09:01:14 -07:00
|
|
|
poll?: boolean;
|
2024-10-31 12:48:30 -07:00
|
|
|
preview?: boolean;
|
2022-04-15 15:41:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Button to expand a truncated status (due to too much content) */
|
2024-10-31 12:48:30 -07:00
|
|
|
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick, quote, poll, preview }) => (
|
|
|
|
<div
|
|
|
|
className={clsx('relative', {
|
|
|
|
'-mt-4': !preview,
|
|
|
|
'-mt-2': preview,
|
|
|
|
})}
|
|
|
|
>
|
2024-08-31 10:50:45 -07:00
|
|
|
<div
|
2024-09-12 11:42:38 -07:00
|
|
|
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,
|
2024-09-11 09:01:14 -07:00
|
|
|
'to-gray-100 dark:to-primary-800': poll,
|
2024-09-12 11:42:38 -07:00
|
|
|
'group-hover:to-gray-100 black:group-hover:to-gray-800 dark:group-hover:to-gray-800': quote,
|
2024-08-31 10:50:45 -07:00
|
|
|
})}
|
|
|
|
/>
|
2024-10-31 12:48:30 -07:00
|
|
|
{!preview && (
|
|
|
|
<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>
|
|
|
|
)}
|
2024-08-23 15:27:40 -07:00
|
|
|
</div>
|
2022-04-15 15:41:53 -07:00
|
|
|
);
|
|
|
|
|
2022-04-15 14:59:42 -07:00
|
|
|
interface IStatusContent {
|
2024-08-18 08:50:56 -07:00
|
|
|
status: MinifiedStatus;
|
2023-10-02 11:54:02 -07:00
|
|
|
onClick?: () => void;
|
|
|
|
collapsable?: boolean;
|
|
|
|
translatable?: boolean;
|
|
|
|
textSize?: Sizes;
|
2024-11-06 07:46:04 -08:00
|
|
|
isQuote?: boolean;
|
2024-10-31 12:48:30 -07:00
|
|
|
preview?: boolean;
|
2024-11-06 07:46:04 -08:00
|
|
|
withMedia?: boolean;
|
2022-04-15 14:59:42 -07:00
|
|
|
}
|
|
|
|
|
2022-04-15 15:41:53 -07:00
|
|
|
/** Renders the text content of a status */
|
2024-05-13 10:00:42 -07:00
|
|
|
const StatusContent: React.FC<IStatusContent> = React.memo(({
|
2023-01-27 10:36:49 -08:00
|
|
|
status,
|
|
|
|
onClick,
|
|
|
|
collapsable = false,
|
|
|
|
translatable,
|
2023-01-30 11:38:29 -08:00
|
|
|
textSize = 'md',
|
2024-11-06 07:46:04 -08:00
|
|
|
isQuote = false,
|
2024-10-31 12:48:30 -07:00
|
|
|
preview,
|
2024-11-06 07:46:04 -08:00
|
|
|
withMedia,
|
2023-01-27 10:36:49 -08:00
|
|
|
}) => {
|
2024-08-23 15:27:40 -07:00
|
|
|
const dispatch = useAppDispatch();
|
|
|
|
const { displaySpoilers } = useSettings();
|
|
|
|
|
2022-04-15 14:59:42 -07:00
|
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
2024-09-05 16:06:25 -07:00
|
|
|
const [lineClamp, setLineClamp] = useState(true);
|
2022-04-15 14:59:42 -07:00
|
|
|
|
|
|
|
const node = useRef<HTMLDivElement>(null);
|
2024-09-05 16:06:25 -07:00
|
|
|
const spoilerNode = useRef<HTMLSpanElement>(null);
|
2022-04-15 14:59:42 -07:00
|
|
|
|
|
|
|
const maybeSetCollapsed = (): void => {
|
|
|
|
if (!node.current) return;
|
|
|
|
|
2024-10-31 12:48:30 -07:00
|
|
|
if ((collapsable || preview) && !collapsed) {
|
2024-09-22 05:50:56 -07:00
|
|
|
// 20px * x lines (+ 2px padding at the top)
|
2024-11-06 07:46:04 -08:00
|
|
|
if (node.current.clientHeight > (preview ? 82 : isQuote ? 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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-08-23 15:27:40 -07:00
|
|
|
const toggleExpanded: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
|
2024-09-05 15:47:26 -07:00
|
|
|
if (expanded) dispatch(collapseStatusSpoiler(status.id));
|
|
|
|
else dispatch(expandStatusSpoiler(status.id));
|
2024-08-23 15:27:40 -07:00
|
|
|
};
|
|
|
|
|
2023-01-12 08:57:39 -08:00
|
|
|
useLayoutEffect(() => {
|
2022-04-15 14:59:42 -07:00
|
|
|
maybeSetCollapsed();
|
|
|
|
maybeSetOnlyEmoji();
|
|
|
|
});
|
|
|
|
|
2024-10-21 14:51:16 -07:00
|
|
|
const content = useMemo(
|
2024-05-27 15:11:28 -07:00
|
|
|
(): string => translatable && status.translation
|
2024-08-11 01:48:58 -07:00
|
|
|
? status.translation.content!
|
2024-10-21 14:51:16 -07:00
|
|
|
: (status.content_map && status.currentLanguage)
|
|
|
|
? (status.content_map[status.currentLanguage] || status.content)
|
|
|
|
: status.content,
|
|
|
|
[status.content, status.translation, status.currentLanguage],
|
2024-05-12 16:18:04 -07:00
|
|
|
);
|
2022-04-15 14:59:42 -07:00
|
|
|
|
2024-11-06 07:46:04 -08:00
|
|
|
const { content: parsedContent, hashtags } = useMemo(() => parseContent({
|
|
|
|
html: content,
|
|
|
|
mentions: status.mentions,
|
|
|
|
hasQuote: !!status.quote_id,
|
|
|
|
emojis: status.emojis,
|
|
|
|
}, true), [content]);
|
|
|
|
|
2024-09-05 16:06:25 -07:00
|
|
|
useEffect(() => {
|
|
|
|
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);
|
|
|
|
}, [spoilerNode.current]);
|
|
|
|
|
2024-09-02 03:35:47 -07:00
|
|
|
const withSpoiler = status.spoiler_text.length > 0;
|
2023-10-13 20:07:57 -07:00
|
|
|
|
2024-10-21 14:51:16 -07:00
|
|
|
const spoilerText = status.spoiler_text_map && status.currentLanguage
|
|
|
|
? status.spoiler_text_map[status.currentLanguage] || status.spoiler_text
|
|
|
|
: status.spoiler_text;
|
2024-08-24 03:41:08 -07:00
|
|
|
|
2023-10-10 18:02:22 -07:00
|
|
|
const direction = getTextDirection(status.search_index);
|
2024-10-02 08:25:39 -07:00
|
|
|
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
|
2022-09-30 10:59:19 -07:00
|
|
|
'cursor-pointer': onClick,
|
2024-10-02 08:25:39 -07:00
|
|
|
'overflow-hidden': collapsed,
|
2024-11-06 07:46:04 -08:00
|
|
|
'max-h-[200px]': collapsed && !isQuote && !preview,
|
|
|
|
'max-h-[120px]': collapsed && isQuote,
|
2024-10-31 12:48:30 -07:00
|
|
|
'max-h-[80px]': collapsed && preview,
|
2022-09-30 10:59:19 -07:00
|
|
|
'leading-normal big-emoji': onlyEmoji,
|
2022-04-15 14:59:42 -07:00
|
|
|
});
|
|
|
|
|
2024-08-23 15:27:40 -07:00
|
|
|
const expandable = !displaySpoilers;
|
|
|
|
const expanded = !withSpoiler || status.expanded || false;
|
|
|
|
|
2024-08-23 14:50:00 -07:00
|
|
|
const output = [];
|
|
|
|
|
|
|
|
if (spoilerText) {
|
|
|
|
output.push(
|
2024-08-25 11:17:52 -07:00
|
|
|
<Text key='spoiler' size='2xl' weight='medium'>
|
2024-10-21 14:51:16 -07:00
|
|
|
<span className={clsx({ 'line-clamp-3': !expanded && lineClamp })} ref={spoilerNode}>
|
|
|
|
<Emojify text={spoilerText} emojis={status.emojis} />
|
|
|
|
</span>
|
2024-10-03 03:09:35 -07:00
|
|
|
{status.content && expandable && (
|
2024-08-23 15:27:40 -07:00
|
|
|
<Button
|
|
|
|
className='ml-2 align-middle'
|
|
|
|
type='button'
|
|
|
|
theme='muted'
|
|
|
|
size='xs'
|
|
|
|
onClick={toggleExpanded}
|
2024-08-28 05:48:35 -07:00
|
|
|
icon={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
|
2024-08-23 15:27:40 -07:00
|
|
|
>
|
2024-09-01 14:54:28 -07:00
|
|
|
{expanded
|
|
|
|
? <FormattedMessage id='status.spoiler.collapse' defaultMessage='Collapse' />
|
|
|
|
: <FormattedMessage id='status.spoiler.expand' defaultMessage='Expand' />}
|
2024-08-23 15:27:40 -07:00
|
|
|
</Button>
|
|
|
|
)}
|
2024-08-23 14:50:00 -07:00
|
|
|
</Text>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-23 15:27:40 -07:00
|
|
|
if (expandable && !expanded) return <>{output}</>;
|
|
|
|
|
2024-11-06 07:46:04 -08:00
|
|
|
let quote;
|
|
|
|
|
|
|
|
if (withMedia && status.quote_id) {
|
|
|
|
if ((status.quote_visible ?? true) === false) {
|
|
|
|
quote = (
|
|
|
|
<div className='quoted-status-tombstone'>
|
|
|
|
<p><FormattedMessage id='statuses.quote_tombstone' defaultMessage='Post is unavailable.' /></p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
quote = <QuotedStatus statusId={status.quote_id} />;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const media = withMedia && ((quote || status.card || status.media_attachments.length > 0)) && (
|
|
|
|
<Stack space={4}>
|
|
|
|
{(status.media_attachments.length > 0 || (status.card && !quote)) && (
|
|
|
|
<div className='relative'>
|
|
|
|
<SensitiveContentOverlay status={status} />
|
|
|
|
<StatusMedia status={status} />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{quote}
|
|
|
|
</Stack>
|
|
|
|
);
|
|
|
|
|
2022-10-31 13:18:40 -07:00
|
|
|
if (onClick) {
|
2024-10-03 03:09:35 -07:00
|
|
|
if (status.content) {
|
2024-09-14 08:57:50 -07:00
|
|
|
output.push(
|
|
|
|
<Markup
|
|
|
|
ref={node}
|
|
|
|
tabIndex={0}
|
|
|
|
key='content'
|
|
|
|
className={className}
|
|
|
|
direction={direction}
|
|
|
|
lang={status.language || undefined}
|
|
|
|
size={textSize}
|
|
|
|
>
|
2024-11-06 07:46:04 -08:00
|
|
|
{parsedContent}
|
2024-09-14 08:57:50 -07:00
|
|
|
</Markup>,
|
|
|
|
);
|
|
|
|
}
|
2022-04-15 14:59:42 -07:00
|
|
|
|
2024-09-11 09:01:14 -07:00
|
|
|
const hasPoll = !!status.poll_id;
|
|
|
|
|
2022-04-15 14:59:42 -07:00
|
|
|
if (collapsed) {
|
2024-11-06 07:46:04 -08:00
|
|
|
output.push(<ReadMoreButton onClick={onClick} key='read-more' quote={isQuote} poll={hasPoll} />);
|
2022-04-15 14:59:42 -07:00
|
|
|
}
|
|
|
|
|
2024-08-18 08:50:56 -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
|
|
|
}
|
|
|
|
|
2024-11-06 07:46:04 -08:00
|
|
|
if (translatable) {
|
|
|
|
output.push(<TranslateButton status={status} />);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (media) {
|
|
|
|
output.push(media);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hashtags.length) {
|
|
|
|
output.push(<HashtagsBar key='hashtags' hashtags={hashtags} />);
|
|
|
|
}
|
|
|
|
|
2024-08-29 15:09:46 -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 {
|
2024-10-03 03:09:35 -07:00
|
|
|
if (status.content) {
|
2024-09-14 08:57:50 -07:00
|
|
|
output.push(
|
|
|
|
<Markup
|
|
|
|
ref={node}
|
|
|
|
tabIndex={0}
|
|
|
|
key='content'
|
|
|
|
className={className}
|
|
|
|
direction={direction}
|
|
|
|
lang={status.language || undefined}
|
|
|
|
size={textSize}
|
|
|
|
>
|
2024-11-06 07:46:04 -08:00
|
|
|
{parsedContent}
|
2024-09-14 08:57:50 -07:00
|
|
|
</Markup>,
|
|
|
|
);
|
|
|
|
}
|
2022-04-15 14:59:42 -07:00
|
|
|
|
2024-08-31 10:50:45 -07:00
|
|
|
if (collapsed) {
|
2024-11-06 07:46:04 -08:00
|
|
|
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={isQuote} preview={preview} />);
|
2024-08-31 10:50:45 -07:00
|
|
|
}
|
|
|
|
|
2024-08-18 08:50:56 -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
|
|
|
}
|
|
|
|
|
2024-11-06 07:46:04 -08:00
|
|
|
if (translatable) {
|
|
|
|
output.push(<TranslateButton status={status} />);
|
|
|
|
}
|
|
|
|
|
2022-04-15 14:59:42 -07:00
|
|
|
return <>{output}</>;
|
|
|
|
}
|
2024-05-13 10:00:42 -07:00
|
|
|
});
|
2022-04-15 14:59:42 -07:00
|
|
|
|
2024-05-13 10:00:42 -07:00
|
|
|
export { StatusContent as default };
|