import classNames from 'clsx'; import React, { useEffect, useRef, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { NavLink, useHistory } from 'react-router-dom'; import { mentionCompose, replyComposeWithConfirmation } from 'soapbox/actions/compose'; import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { toggleStatusHidden } from 'soapbox/actions/statuses'; import Icon from 'soapbox/components/icon'; import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status'; import EventPreview from './event-preview'; import StatusActionBar from './status-action-bar'; import StatusMedia from './status-media'; import StatusReplyMentions from './status-reply-mentions'; import StatusContent from './status_content'; import { Card, HStack, Text } from './ui'; import type { Map as ImmutableMap } from 'immutable'; import type { Account as AccountEntity, Status as StatusEntity, } from 'soapbox/types/entities'; // Defined in components/scrollable_list export type ScrollPosition = { height: number, top: number }; const messages = defineMessages({ reblogged_by: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, }); export interface IStatus { id?: string, status: StatusEntity, onClick?: () => void, muted?: boolean, hidden?: boolean, unread?: boolean, onMoveUp?: (statusId: string, featured?: boolean) => void, onMoveDown?: (statusId: string, featured?: boolean) => void, group?: ImmutableMap, focusable?: boolean, featured?: boolean, hideActionBar?: boolean, hoverable?: boolean, variant?: 'default' | 'rounded', withDismiss?: boolean, accountAction?: React.ReactElement, } const Status: React.FC = (props) => { const { status, focusable = true, hoverable = true, onClick, onMoveUp, onMoveDown, muted, hidden, featured, unread, group, hideActionBar, variant = 'rounded', withDismiss, } = props; const intl = useIntl(); const history = useHistory(); const dispatch = useAppDispatch(); const settings = useSettings(); const displayMedia = settings.get('displayMedia') as string; const didShowCard = useRef(false); const node = useRef(null); const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); const actualStatus = getActualStatus(status); // Track height changes we know about to compensate scrolling. useEffect(() => { didShowCard.current = Boolean(!muted && !hidden && status?.card); }, []); useEffect(() => { setShowMedia(defaultMediaVisibility(status, displayMedia)); }, [status.id]); const handleToggleMediaVisibility = (): void => { setShowMedia(!showMedia); }; const handleClick = (): void => { if (onClick) { onClick(); } else { history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`); } }; const handleExpandedToggle = (): void => { dispatch(toggleStatusHidden(actualStatus)); }; const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { const status = actualStatus; const firstAttachment = status.media_attachments.first(); e?.preventDefault(); if (firstAttachment) { if (firstAttachment.type === 'video') { dispatch(openModal('VIDEO', { media: firstAttachment, time: 0 })); } else { dispatch(openModal('MEDIA', { media: status.media_attachments, index: 0 })); } } }; const handleHotkeyReply = (e?: KeyboardEvent): void => { e?.preventDefault(); dispatch(replyComposeWithConfirmation(actualStatus, intl)); }; const handleHotkeyFavourite = (): void => { toggleFavourite(actualStatus); }; const handleHotkeyBoost = (e?: KeyboardEvent): void => { const modalReblog = () => dispatch(toggleReblog(actualStatus)); const boostModal = settings.get('boostModal'); if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { dispatch(openModal('BOOST', { status: actualStatus, onReblog: modalReblog })); } }; const handleHotkeyMention = (e?: KeyboardEvent): void => { e?.preventDefault(); dispatch(mentionCompose(actualStatus.account as AccountEntity)); }; const handleHotkeyOpen = (): void => { history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`); }; const handleHotkeyOpenProfile = (): void => { history.push(`/@${actualStatus.getIn(['account', 'acct'])}`); }; const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { if (onMoveUp) { onMoveUp(status.id, featured); } }; const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { if (onMoveDown) { onMoveDown(status.id, featured); } }; const handleHotkeyToggleHidden = (): void => { dispatch(toggleStatusHidden(actualStatus)); }; const handleHotkeyToggleSensitive = (): void => { handleToggleMediaVisibility(); }; const handleHotkeyReact = (): void => { _expandEmojiSelector(); }; const _expandEmojiSelector = (): void => { const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); firstEmoji?.focus(); }; if (!status) return null; let rebloggedByText, reblogElement, reblogElementMobile; if (hidden) { return (
{actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])} {actualStatus.content}
); } if (status.filtered || actualStatus.filtered) { const minHandlers = muted ? undefined : { moveUp: handleHotkeyMoveUp, moveDown: handleHotkeyMoveDown, }; return (
); } if (status.reblog && typeof status.reblog === 'object') { const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) }; reblogElement = ( event.stopPropagation()} className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' > , }} /> ); reblogElementMobile = (
event.stopPropagation()} className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' > , }} />
); rebloggedByText = intl.formatMessage( messages.reblogged_by, { name: String(status.getIn(['account', 'acct'])) }, ); } let quote; if (actualStatus.quote) { if (actualStatus.pleroma.get('quote_visible', true) === false) { quote = (

); } else { quote = ; } } const handlers = muted ? undefined : { reply: handleHotkeyReply, favourite: handleHotkeyFavourite, boost: handleHotkeyBoost, mention: handleHotkeyMention, open: handleHotkeyOpen, openProfile: handleHotkeyOpenProfile, moveUp: handleHotkeyMoveUp, moveDown: handleHotkeyMoveDown, toggleHidden: handleHotkeyToggleHidden, toggleSensitive: handleHotkeyToggleSensitive, openMedia: handleHotkeyOpenMedia, react: handleHotkeyReact, }; const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; const accountAction = props.accountAction || reblogElement; return (
history.push(statusUrl)} role='link' > {featured && (
)} {reblogElementMobile}
{!group && actualStatus.group && (
Posted in {String(actualStatus.getIn(['group', 'title']))}
)} {actualStatus.event ? : ( <> {quote} )} {!hideActionBar && (
)}
); }; export default Status;