import classNames from 'classnames'; 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 Icon from 'soapbox/components/icon'; import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import { defaultMediaVisibility, textForScreenReader } from 'soapbox/utils/status'; import StatusMedia from './status-media'; import StatusReplyMentions from './status-reply-mentions'; import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; import { HStack, Text } from './ui'; import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import type { Account as AccountEntity, Attachment as AttachmentEntity, 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' }, }); interface IStatus { id?: string, contextType?: string, status: StatusEntity, account: AccountEntity, otherAccounts: ImmutableList, onClick: () => void, onReply: (status: StatusEntity) => void, onFavourite: (status: StatusEntity) => void, onReblog: (status: StatusEntity, e?: KeyboardEvent) => void, onQuote: (status: StatusEntity) => void, onDelete: (status: StatusEntity) => void, onEdit: (status: StatusEntity) => void, onDirect: (status: StatusEntity) => void, onChat: (status: StatusEntity) => void, onMention: (account: StatusEntity['account']) => void, onPin: (status: StatusEntity) => void, onOpenMedia: (media: ImmutableList, index: number) => void, onOpenVideo: (media: ImmutableMap | AttachmentEntity, startTime: number) => void, onOpenAudio: (media: ImmutableMap, startTime: number) => void, onBlock: (status: StatusEntity) => void, onEmbed: (status: StatusEntity) => void, onHeightChange: (status: StatusEntity) => void, onToggleHidden: (status: StatusEntity) => void, onShowHoverProfileCard: (status: StatusEntity) => void, muted: boolean, hidden: boolean, unread: boolean, onMoveUp: (statusId: string, featured?: boolean) => void, onMoveDown: (statusId: string, featured?: boolean) => void, getScrollPosition?: () => ScrollPosition | undefined, updateScrollBottom?: (bottom: number) => void, group: ImmutableMap, displayMedia: string, allowedEmoji: ImmutableList, focusable: boolean, featured?: boolean, withDismiss?: boolean, hideActionBar?: boolean, hoverable?: boolean, } const Status: React.FC = (props) => { const { status, focusable = true, hoverable = true, onToggleHidden, displayMedia, onOpenMedia, onOpenVideo, onClick, onReply, onFavourite, onReblog, onMention, onMoveUp, onMoveDown, muted, hidden, featured, unread, group, hideActionBar, } = props; const intl = useIntl(); const history = useHistory(); const didShowCard = useRef(false); const node = useRef(null); const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); const [emojiSelectorFocused, setEmojiSelectorFocused] = useState(false); // 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(`/@${_properStatus().getIn(['account', 'acct'])}/posts/${_properStatus().id}`); } }; const handleExpandedToggle = (): void => { onToggleHidden(_properStatus()); }; const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { const status = _properStatus(); const firstAttachment = status.media_attachments.first(); e?.preventDefault(); if (firstAttachment) { if (firstAttachment.type === 'video') { onOpenVideo(firstAttachment, 0); } else { onOpenMedia(status.media_attachments, 0); } } }; const handleHotkeyReply = (e?: KeyboardEvent): void => { e?.preventDefault(); onReply(_properStatus()); }; const handleHotkeyFavourite = (): void => { onFavourite(_properStatus()); }; const handleHotkeyBoost = (e?: KeyboardEvent): void => { onReblog(_properStatus(), e); }; const handleHotkeyMention = (e?: KeyboardEvent): void => { e?.preventDefault(); onMention(_properStatus().account); }; const handleHotkeyOpen = (): void => { history.push(`/@${_properStatus().getIn(['account', 'acct'])}/posts/${_properStatus().id}`); }; const handleHotkeyOpenProfile = (): void => { history.push(`/@${_properStatus().getIn(['account', 'acct'])}`); }; const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { onMoveUp(status.id, featured); }; const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { onMoveDown(status.id, featured); }; const handleHotkeyToggleHidden = (): void => { onToggleHidden(_properStatus()); }; const handleHotkeyToggleSensitive = (): void => { handleToggleMediaVisibility(); }; const handleHotkeyReact = (): void => { _expandEmojiSelector(); }; const handleEmojiSelectorUnfocus = (): void => { setEmojiSelectorFocused(false); }; const _expandEmojiSelector = (): void => { setEmojiSelectorFocused(true); const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); firstEmoji?.focus(); }; const _properStatus = (): StatusEntity => { if (status.reblog && typeof status.reblog === 'object') { return status.reblog; } else { return status; } }; if (!status) return null; const actualStatus = _properStatus(); let prepend, 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 (featured) { prepend = (
); } 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}`; return (
history.push(statusUrl)} role='link' > {prepend}
{reblogElementMobile}
{!group && actualStatus.group && (
Posted in {String(actualStatus.getIn(['group', 'title']))}
)} {quote} {!hideActionBar && ( // @ts-ignore )}
); }; export default Status;