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 StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from 'themes/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container'; // 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 default class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, }; static propTypes = { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, onBlock: PropTypes.func, onEmbed: PropTypes.func, onHeightChange: PropTypes.func, muted: PropTypes.bool, collapse: PropTypes.bool, hidden: PropTypes.bool, prepend: PropTypes.string, withDismiss: PropTypes.bool, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, }; state = { isExpanded: null, markedForDelete: false, } // 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', 'boostModal', 'muted', 'collapse', 'notification', ] updateOnStates = [ 'isExpanded', 'markedForDelete', ] // 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 // `componentWillReceiveProps()`. // 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. componentWillReceiveProps (nextProps) { if (!nextProps.settings.getIn(['collapsed', 'enabled'])) { if (this.state.isExpanded === false) { this.setExpansion(null); } } else if ( nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined ) this.setExpansion(nextProps.collapse ? false : 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 `setExpansion()` 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; const autoCollapseSettings = settings.getIn(['collapsed', 'auto']); if (function () { switch (true) { case collapse: case autoCollapseSettings.get('all'): case autoCollapseSettings.get('notifications') && muted: case autoCollapseSettings.get('lengthy') && node.clientHeight > ( status.get('media_attachments').size && !muted ? 650 : 400 ): case autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by': case autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null: case autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size: return true; default: return false; } }()) this.setExpansion(false); } // `setExpansion()` sets the value of `isExpanded` in our state. It takes // one argument, `value`, which gives the desired value for `isExpanded`. // The default for this argument is `null`. // `setExpansion()` automatically checks for us whether toot collapsing // is enabled, so we don't have to. setExpansion = (value) => { switch (true) { case value === undefined || value === null: this.setState({ isExpanded: null }); break; case !value && this.props.settings.getIn(['collapsed', 'enabled']): this.setState({ isExpanded: false }); break; case !!value: this.setState({ isExpanded: true }); break; } } // `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 { isExpanded } = this.state; if (!router) return; if (destination === undefined) { destination = `/statuses/${ status.getIn(['reblog', 'id'], status.get('id')) }`; } if (e.button === 0) { if (isExpanded === false) this.setExpansion(null); else if (e.shiftKey) { this.setExpansion(false); document.getSelection().removeAllRanges(); } else router.history.push(destination); e.preventDefault(); } } handleAccountClick = (e) => { if (this.context.router && e.button === 0) { const id = e.currentTarget.getAttribute('data-id'); e.preventDefault(); this.context.router.history.push(`/accounts/${id}`); } } handleExpandedToggle = () => { this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true); }; handleOpenVideo = startTime => { this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); } handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this.props.status, this.context.router.history); } handleHotkeyFavourite = () => { this.props.onFavourite(this.props.status); } handleHotkeyBoost = e => { this.props.onReblog(this.props.status, e); } handleHotkeyMention = e => { e.preventDefault(); this.props.onMention(this.props.status.get('account'), this.context.router.history); } handleHotkeyOpen = () => { this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); } handleHotkeyOpenProfile = () => { this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); } handleHotkeyMoveUp = () => { this.props.onMoveUp(this.props.status.get('id')); } handleHotkeyMoveDown = () => { this.props.onMoveDown(this.props.status.get('id')); } handleRef = c => { this.node = c; } renderLoadingMediaGallery () { return
; } renderLoadingVideoPlayer () { return ; } render () { const { handleRef, parseClick, setExpansion, } = this; const { router } = this.context; const { status, account, settings, collapsed, muted, prepend, intersectionObserverWrapper, onOpenVideo, onOpenMedia, notification, hidden, ...other } = this.props; const { isExpanded } = this.state; let background = null; let attachments = null; let media = null; let mediaIcon = null; if (status === null) { return null; } if (hidden) { return (