glitchier-soc/app/javascript/flavours/glitch/components/status.js
fef 7250a4f6c2
make number of visible reactions a vanilla setting
Reactions will be backported to the vanilla
flavour, which requires all related settings to
be accessible from the vanilla settings page
rather than the glitch specific settings modal.
2022-12-20 18:27:46 +01:00

836 lines
29 KiB
JavaScript

import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import StatusPrepend from './status_prepend';
import StatusHeader from './status_header';
import StatusIcons from './status_icons';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import StatusReactions from './status_reactions';
import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
import classNames from 'classnames';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { displayMedia, visibleReactions } from 'flavours/glitch/initial_state';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
const displayName = status.getIn(['account', 'display_name']);
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
export const defaultMediaVisibility = (status, settings) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
if (settings.getIn(['media', 'reveal_behind_cw']) && !!status.get('spoiler_text')) {
return true;
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
}
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
containerId: PropTypes.string,
id: PropTypes.string,
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onBookmark: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onReactionAdd: PropTypes.func,
onReactionRemove: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onAddFilter: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
prepend: PropTypes.string,
withDismiss: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
expanded: PropTypes.bool,
intl: PropTypes.object.isRequired,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
onClick: PropTypes.func,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
settings: ImmutablePropTypes.map.isRequired,
emojiMap: ImmutablePropTypes.map.isRequired,
pictureInPicture: PropTypes.shape({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
state = {
isCollapsed: false,
autoCollapsed: false,
isExpanded: undefined,
showMedia: undefined,
statusId: undefined,
revealBehindCW: undefined,
showCard: false,
forceFilter: undefined,
}
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'account',
'settings',
'prepend',
'muted',
'notification',
'hidden',
'expanded',
'unread',
'pictureInPicture',
]
updateOnStates = [
'isExpanded',
'isCollapsed',
'showMedia',
'forceFilter',
]
// If our settings have changed to disable collapsed statuses, then we
// need to make sure that we uncollapse every one. We do that by watching
// for changes to `settings.collapsed.enabled` in
// `getderivedStateFromProps()`.
// We also need to watch for changes on the `collapse` prop---if this
// changes to anything other than `undefined`, then we need to collapse or
// uncollapse our status accordingly.
static getDerivedStateFromProps(nextProps, prevState) {
let update = {};
let updated = false;
// Make sure the state mirrors props we track…
if (nextProps.expanded !== prevState.expandedProp) {
update.expandedProp = nextProps.expanded;
updated = true;
}
if (nextProps.status?.get('hidden') !== prevState.statusPropHidden) {
update.statusPropHidden = nextProps.status?.get('hidden');
updated = true;
}
// Update state based on new props
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
if (prevState.isCollapsed) {
update.isCollapsed = false;
updated = true;
}
}
// Handle uncollapsing toots when the shared CW state is expanded
if (nextProps.settings.getIn(['content_warnings', 'shared_state']) &&
nextProps.status?.get('spoiler_text')?.length && nextProps.status?.get('hidden') === false &&
prevState.statusPropHidden !== false && prevState.isCollapsed
) {
update.isCollapsed = false;
updated = true;
}
// The “expanded” prop is used to one-off change the local state.
// It's used in the thread view when unfolding/re-folding all CWs at once.
if (nextProps.expanded !== prevState.expandedProp &&
nextProps.expanded !== undefined
) {
update.isExpanded = nextProps.expanded;
if (nextProps.expanded) update.isCollapsed = false;
updated = true;
}
if (prevState.isExpanded === undefined && update.isExpanded === undefined) {
update.isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status);
updated = true;
}
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
update.statusId = nextProps.status.get('id');
updated = true;
}
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
if (update.revealBehindCW) {
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
}
updated = true;
}
return updated ? update : null;
}
// When mounting, we just check to see if our status should be collapsed,
// and collapse it if so. We don't need to worry about whether collapsing
// is enabled here, because `setCollapsed()` already takes that into
// account.
// The cases where a status should be collapsed are:
//
// - The `collapse` prop has been set to `true`
// - The user has decided in local settings to collapse all statuses.
// - The user has decided to collapse all notifications ('muted'
// statuses).
// - The user has decided to collapse long statuses and the status is
// over 400px (without media, or 650px with).
// - The status is a reply and the user has decided to collapse all
// replies.
// - The status contains media and the user has decided to collapse all
// statuses with media.
// - The status is a reblog the user has decided to collapse all
// statuses which are reblogs.
componentDidMount () {
const { node } = this;
const {
status,
settings,
collapse,
muted,
prepend,
} = this.props;
// Prevent a crash when node is undefined. Not completely sure why this
// happens, might be because status === null.
if (node === undefined) return;
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
// Don't autocollapse if CW state is shared and status is explicitly revealed,
// as it could cause surprising changes when receiving notifications
if (settings.getIn(['content_warnings', 'shared_state']) && status.get('spoiler_text').length && !status.get('hidden')) return;
if (collapse ||
autoCollapseSettings.get('all') ||
(autoCollapseSettings.get('notifications') && muted) ||
(autoCollapseSettings.get('lengthy') && node.clientHeight > ((status.get('media_attachments').size && !muted) ? 650 : 400)) ||
(autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by') ||
(autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null) ||
(autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size > 0)
) {
this.setCollapsed(true);
// Hack to fix timeline jumps on second rendering when auto-collapsing
this.setState({ autoCollapsed: true });
}
// Hack to fix timeline jumps when a preview card is fetched
this.setState({
showCard: !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'),
});
}
// Hack to fix timeline jumps on second rendering when auto-collapsing
// or on subsequent rendering when a preview card has been fetched
getSnapshotBeforeUpdate (prevProps, prevState) {
if (!this.props.getScrollPosition) return null;
const { muted, hidden, status, settings } = this.props;
const doShowCard = !muted && !hidden && status && status.get('card') && settings.get('inline_preview_cards');
if (this.state.autoCollapsed || (doShowCard && !this.state.showCard)) {
if (doShowCard) this.setState({ showCard: true });
if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
return this.props.getScrollPosition();
} else {
return null;
}
}
componentDidUpdate (prevProps, prevState, snapshot) {
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => { this.props.updateScrollBottom(position.height - position.top); });
}
}
}
// `setCollapsed()` sets the value of `isCollapsed` in our state, that is,
// whether the toot is collapsed or not.
// `setCollapsed()` automatically checks for us whether toot collapsing
// is enabled, so we don't have to.
setCollapsed = (value) => {
if (this.props.settings.getIn(['collapsed', 'enabled'])) {
if (value) {
this.setExpansion(false);
}
this.setState({ isCollapsed: value });
} else {
this.setState({ isCollapsed: false });
}
}
setExpansion = (value) => {
if (this.props.settings.getIn(['content_warnings', 'shared_state']) && this.props.status.get('hidden') === value) {
this.props.onToggleHidden(this.props.status);
}
this.setState({ isExpanded: value });
if (value) {
this.setCollapsed(false);
}
}
// `parseClick()` takes a click event and responds appropriately.
// If our status is collapsed, then clicking on it should uncollapse it.
// If `Shift` is held, then clicking on it should collapse it.
// Otherwise, we open the url handed to us in `destination`, if
// applicable.
parseClick = (e, destination) => {
const { router } = this.context;
const { status } = this.props;
const { isCollapsed } = this.state;
if (!router) return;
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
if (isCollapsed) this.setCollapsed(false);
else if (e.shiftKey) {
this.setCollapsed(true);
document.getSelection().removeAllRanges();
} else if (this.props.onClick) {
this.props.onClick();
return;
} else {
if (destination === undefined) {
destination = `/@${
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
}/${
status.getIn(['reblog', 'id'], status.get('id'))
}`;
}
let state = {...router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
router.history.push(destination, state);
}
e.preventDefault();
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleExpandedToggle = () => {
if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
this.props.onToggleHidden(this.props.status);
} else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
};
handleOpenVideo = (options) => {
const { status } = this.props;
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
}
handleOpenMedia = (media, index) => {
this.props.onOpenMedia(this.props.status.get('id'), media, index);
}
handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props;
const statusId = status.get('id');
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
onOpenMedia(statusId, status.get('media_attachments'), 0);
}
}
}
handleDeployPictureInPicture = (type, mediaProps) => {
const { deployPictureInPicture, status } = this.props;
deployPictureInPicture(status, type, mediaProps);
}
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this.props.status, this.context.router.history);
}
handleHotkeyFavourite = (e) => {
this.props.onFavourite(this.props.status, e);
}
handleHotkeyBoost = e => {
this.props.onReblog(this.props.status, e);
}
handleHotkeyBookmark = e => {
this.props.onBookmark(this.props.status, e);
}
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
handleHotkeyOpen = () => {
let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
const status = this.props.status;
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
}
handleHotkeyOpenProfile = () => {
let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.containerId || this.props.id, e.target.getAttribute('data-featured'));
}
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured'));
}
handleHotkeyCollapse = e => {
if (!this.props.settings.getIn(['collapsed', 'enabled']))
return;
this.setCollapsed(!this.state.isCollapsed);
}
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
handleUnfilterClick = e => {
this.setState({ forceFilter: false });
e.preventDefault();
}
handleFilterClick = () => {
this.setState({ forceFilter: true });
}
handleRef = c => {
this.node = c;
}
handleTranslate = () => {
this.props.onTranslate(this.props.status);
}
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
}
renderLoadingVideoPlayer () {
return <div className='video-player' style={{ height: '110px' }} />;
}
renderLoadingAudioPlayer () {
return <div className='audio-player' style={{ height: '110px' }} />;
}
render () {
const {
handleRef,
parseClick,
setExpansion,
setCollapsed,
} = this;
const { router } = this.context;
const {
intl,
status,
account,
settings,
collapsed,
muted,
intersectionObserverWrapper,
onOpenVideo,
onOpenMedia,
notification,
hidden,
unread,
featured,
pictureInPicture,
...other
} = this.props;
const { isCollapsed, forceFilter } = this.state;
let background = null;
let attachments = null;
// Depending on user settings, some media are considered as parts of the
// contents (affected by CW) while other will be displayed outside of the
// CW.
let contentMedia = [];
let contentMediaIcons = [];
let extraMedia = [];
let extraMediaIcons = [];
let media = contentMedia;
let mediaIcons = contentMediaIcons;
if (settings.getIn(['content_warnings', 'media_outside'])) {
media = extraMedia;
mediaIcons = extraMediaIcons;
}
if (status === null) {
return null;
}
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
const handlers = {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleHotkeyMention,
open: this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleSpoiler: this.handleExpandedToggle,
bookmark: this.handleHotkeyBookmark,
toggleCollapse: this.handleHotkeyCollapse,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className='status focusable' tabIndex='0'>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
</HotKeys>
);
}
const matchedFilters = status.get('matched_filters');
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}
// If user backgrounds for collapsed statuses are enabled, then we
// initialize our background accordingly. This will only be rendered if
// the status is collapsed.
if (settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])) {
background = status.getIn(['account', 'header']);
}
// This handles our media attachments.
// If a media file is of unknwon type or if the status is muted
// (notification), we show a list of links instead of embedded media.
// After we have generated our appropriate media element and stored it in
// `media`, we snatch the thumbnail to use as our `background` if media
// backgrounds for collapsed statuses are enabled.
attachments = status.get('media_attachments');
if (pictureInPicture.inUse) {
media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
mediaIcons.push('video-camera');
} else if (attachments.size > 0) {
if (muted || attachments.some(item => item.get('type') === 'unknown')) {
media.push(
<AttachmentList
compact
media={status.get('media_attachments')}
/>,
);
} else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media.push(
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('music');
} else if (attachments.getIn([0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media.push(
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
inline
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
preventPlayback={isCollapsed || !isExpanded}
onOpenVideo={this.handleOpenVideo}
width={this.props.cachedMediaWidth}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>)}
</Bundle>,
);
mediaIcons.push('video-camera');
} else { // Media type is 'image' or 'gifv'
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={attachments}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('picture-o');
}
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
background = attachments.getIn([0, 'preview_url']);
}
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
media.push(
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/>,
);
mediaIcons.push('link');
}
if (status.get('poll')) {
contentMedia.push(<PollContainer pollId={status.get('poll')} />);
contentMediaIcons.push('tasks');
}
// Here we prepare extra data-* attributes for CSS selectors.
// Users can use those for theming, hiding avatars etc via UserStyle
const selectorAttribs = {
'data-status-by': `@${status.getIn(['account', 'acct'])}`,
};
let prepend;
if (this.props.prepend && account) {
const notifKind = {
favourite: 'favourited',
reaction: 'reacted',
reblog: 'boosted',
reblogged_by: 'boosted',
status: 'posted',
}[this.props.prepend];
selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
prepend = (
<StatusPrepend
type={this.props.prepend}
account={account}
parseClick={parseClick}
notificationId={this.props.notificationId}
/>
);
}
let rebloggedByText;
if (this.props.prepend === 'reblog') {
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
}
const computedClass = classNames('status', `status-${status.get('visibility')}`, {
collapsed: isCollapsed,
'has-background': isCollapsed && background,
'status__wrapper-reply': !!status.get('in_reply_to_id'),
unread,
muted,
}, 'focusable');
return (
<HotKeys handlers={handlers}>
<div
className={computedClass}
style={isCollapsed && background ? { backgroundImage: `url(${background})` } : null}
{...selectorAttribs}
ref={handleRef}
tabIndex='0'
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
>
{!muted && prepend}
<header className='status__info'>
<span>
{muted && prepend}
{!muted || !isCollapsed ? (
<StatusHeader
status={status}
friend={account}
collapsed={isCollapsed}
parseClick={parseClick}
/>
) : null}
</span>
<StatusIcons
status={status}
mediaIcons={contentMediaIcons.concat(extraMediaIcons)}
collapsible={settings.getIn(['collapsed', 'enabled'])}
collapsed={isCollapsed}
setCollapsed={setCollapsed}
settings={settings.get('status_icons')}
/>
</header>
<StatusContent
status={status}
media={contentMedia}
extraMedia={extraMedia}
mediaIcons={contentMediaIcons}
expanded={isExpanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
parseClick={parseClick}
disabled={!router}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
emojiMap={this.props.emojiMap}
/>
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
<StatusActionBar
status={status}
account={status.get('account')}
showReplyCount={settings.get('show_reply_count')}
onFilter={matchedFilters ? this.handleFilterClick : null}
{...other}
/>
) : null}
{notification ? (
<NotificationOverlayContainer
notification={notification}
/>
) : null}
</div>
</HotKeys>
);
}
}