import clsx from 'clsx'; import React, { useEffect, useMemo, useRef } from 'react'; import { defineMessages, useIntl, FormattedList, FormattedMessage } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { unfilterStatus } from 'pl-fe/actions/statuses'; import Card from 'pl-fe/components/ui/card'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import AccountContainer from 'pl-fe/containers/account-container'; import Emojify from 'pl-fe/features/emoji/emojify'; import StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon'; import { Hotkeys } from 'pl-fe/features/ui/components/hotkeys'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { makeGetStatus, type SelectedStatus } from 'pl-fe/selectors'; import { useModalsStore } from 'pl-fe/stores/modals'; import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import { textForScreenReader } from 'pl-fe/utils/status'; import EventPreview from './event-preview'; import StatusActionBar from './status-action-bar'; import StatusContent from './status-content'; import StatusLanguagePicker from './status-language-picker'; import StatusReactionsBar from './status-reactions-bar'; import StatusReplyMentions from './status-reply-mentions'; import StatusInfo from './statuses/status-info'; import Tombstone from './tombstone'; const messages = defineMessages({ reblogged_by: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, }); interface IStatus { id?: string; avatarSize?: number; status: SelectedStatus; onClick?: () => void; muted?: boolean; unread?: boolean; onMoveUp?: (statusId: string, featured?: boolean) => void; onMoveDown?: (statusId: string, featured?: boolean) => void; focusable?: boolean; featured?: boolean; hideActionBar?: boolean; hoverable?: boolean; variant?: 'default' | 'rounded' | 'slim'; showGroup?: boolean; accountAction?: React.ReactElement; fromBookmarks?: boolean; className?: string; } const Status: React.FC = (props) => { const { status, accountAction, avatarSize = 42, focusable = true, hoverable = true, onClick, onMoveUp, onMoveDown, muted, featured, unread, hideActionBar, variant = 'rounded', showGroup = true, fromBookmarks = false, className, } = props; const intl = useIntl(); const history = useHistory(); const dispatch = useAppDispatch(); const { toggleStatusesMediaHidden } = useStatusMetaStore(); const { openModal } = useModalsStore(); const { boostModal } = useSettings(); const didShowCard = useRef(false); const node = useRef(null); const getStatus = useMemo(makeGetStatus, []); const actualStatus = useAppSelector(state => status.reblog_id && getStatus(state, { id: status.reblog_id }) || status)!; const { mutate: favouriteStatus } = useFavouriteStatus(actualStatus.id); const { mutate: unfavouriteStatus } = useUnfavouriteStatus(actualStatus.id); const { mutate: reblogStatus } = useReblogStatus(actualStatus.id); const { mutate: unreblogStatus } = useUnreblogStatus(actualStatus.id); const isReblog = status.reblog_id; const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`; const group = actualStatus.group; const filterResults = useMemo(() => [...status.filtered, ...actualStatus.filtered].filter(({ filter }) => filter.filter_action === 'warn'), [status.filtered, actualStatus.filtered]); const filtered = filterResults.length > 0; // Track height changes we know about to compensate scrolling. useEffect(() => { didShowCard.current = Boolean(!muted && status?.card); }, []); const handleClick = (e?: React.MouseEvent) => { e?.stopPropagation(); // If the user is selecting text, don't focus the status. if (getSelection()?.toString().length) { return; } if (!e || !(e.ctrlKey || e.metaKey)) { if (onClick) { onClick(); } else { history.push(statusUrl); } } else { window.open(statusUrl, '_blank'); } }; const handleHotkeyOpenMedia = (e?: KeyboardEvent) => { const status = actualStatus; const firstAttachment = status.media_attachments[0]; e?.preventDefault(); if (firstAttachment) { if (firstAttachment.type === 'video') { openModal('VIDEO', { statusId: status.id, media: firstAttachment, time: 0 }); } else { openModal('MEDIA', { statusId: status.id, media: status.media_attachments, index: 0 }); } } }; const handleHotkeyReply = (e?: KeyboardEvent) => { e?.preventDefault(); dispatch(replyCompose(actualStatus, status.reblog_id ? status.account : undefined)); }; const handleHotkeyFavourite = (e?: KeyboardEvent) => { e?.preventDefault(); if (status.favourited) unfavouriteStatus(); else favouriteStatus(); }; const handleHotkeyBoost = (e?: KeyboardEvent) => { const modalReblog = () => { if (status.reblogged) unreblogStatus(); else reblogStatus(undefined); }; if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { openModal('BOOST', { statusId: actualStatus.id, onReblog: modalReblog }); } }; const handleHotkeyMention = (e?: KeyboardEvent) => { e?.preventDefault(); dispatch(mentionCompose(actualStatus.account)); }; const handleHotkeyOpen = () => { history.push(statusUrl); }; const handleHotkeyOpenProfile = () => { history.push(`/@${actualStatus.account.acct}`); }; const handleHotkeyMoveUp = (e?: KeyboardEvent) => { if (onMoveUp) { onMoveUp(status.id, featured); } }; const handleHotkeyMoveDown = (e?: KeyboardEvent) => { if (onMoveDown) { onMoveDown(status.id, featured); } }; const handleHotkeyToggleSensitive = () => { toggleStatusesMediaHidden([actualStatus.id]); }; const handleHotkeyReact = () => { (node.current?.querySelector('.emoji-picker-dropdown') as HTMLButtonElement)?.click(); }; const handleUnfilter = () => { dispatch(unfilterStatus(actualStatus.id)); if (actualStatus.id !== status.id) dispatch(unfilterStatus(status.id)); }; const statusInfo = useMemo(() => { if (isReblog && showGroup && group) { return ( } text={ ), group: ( ), }} /> } /> ); } else if (isReblog) { const accounts = status.accounts || [status.account]; const renderedAccounts = accounts.slice(0, 2).map(account => !!account && ( )); if (accounts.length > 2) { renderedAccounts.push( , ); } const values = { name: , count: accounts.length, }; return ( } text={ status.visibility === 'private' ? ( ) : ( ) } /> ); } else if (featured) { return ( } text={ } /> ); } else if (showGroup && group) { return ( } text={ ), }} /> } /> ); } }, [status.accounts, group?.id]); if (!status) return null; if (status.deleted) return ( ); if (filtered && actualStatus.showFiltered !== true) { const body = (
: {filterResults.map(({ filter }) => filter.title).join(', ')}. {' '}
); if (muted) return body; const minHandlers = { moveUp: handleHotkeyMoveUp, moveDown: handleHotkeyMoveDown, }; return ( {body} ); } let rebloggedByText; if (status.reblog_id === 'object') { rebloggedByText = intl.formatMessage( messages.reblogged_by, { name: status.account.acct }, ); } const body = (
{statusInfo} )} />
{actualStatus.event ? : ( )} {!hideActionBar && (
)}
); if (muted) return body; const handlers = { reply: handleHotkeyReply, favourite: handleHotkeyFavourite, boost: handleHotkeyBoost, mention: handleHotkeyMention, open: handleHotkeyOpen, openProfile: handleHotkeyOpenProfile, moveUp: handleHotkeyMoveUp, moveDown: handleHotkeyMoveDown, toggleSensitive: handleHotkeyToggleSensitive, openMedia: handleHotkeyOpenMedia, react: handleHotkeyReact, }; return ( {body} ); }; export { type IStatus, Status as default, };