2017-04-21 14:05:35 -04:00
import PropTypes from 'prop-types' ;
2023-05-23 11:15:17 -04:00
2023-10-24 13:45:08 -04:00
import { FormattedDate , FormattedMessage } from 'react-intl' ;
2023-05-23 11:15:17 -04:00
import classNames from 'classnames' ;
2023-10-19 13:44:55 -04:00
import { Link , withRouter } from 'react-router-dom' ;
2023-05-23 11:15:17 -04:00
2016-09-25 08:20:29 -04:00
import ImmutablePropTypes from 'react-immutable-proptypes' ;
2023-05-23 11:15:17 -04:00
import ImmutablePureComponent from 'react-immutable-pure-component' ;
2024-01-16 05:27:26 -05:00
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react' ;
2023-05-23 11:15:17 -04:00
import { AnimatedNumber } from 'mastodon/components/animated_number' ;
import EditedTimestamp from 'mastodon/components/edited_timestamp' ;
2023-08-21 13:39:01 -04:00
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar' ;
2023-05-23 11:15:17 -04:00
import { Icon } from 'mastodon/components/icon' ;
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder' ;
2023-10-24 13:45:08 -04:00
import { VisibilityIcon } from 'mastodon/components/visibility_icon' ;
2023-10-19 13:44:55 -04:00
import { WithRouterPropTypes } from 'mastodon/utils/react_router' ;
2023-05-23 11:15:17 -04:00
2023-05-08 21:11:56 -04:00
import { Avatar } from '../../../components/avatar' ;
2023-05-09 17:08:54 -04:00
import { DisplayName } from '../../../components/display_name' ;
2016-11-16 11:20:52 -05:00
import MediaGallery from '../../../components/media_gallery' ;
2023-05-23 11:15:17 -04:00
import StatusContent from '../../../components/status_content' ;
2019-08-23 16:38:02 -04:00
import Audio from '../../audio' ;
2019-01-16 13:47:46 -05:00
import scheduleIdleTask from '../../ui/util/schedule_idle_task' ;
2023-05-23 11:15:17 -04:00
import Video from '../../video' ;
import Card from './card' ;
2016-09-25 08:20:29 -04:00
2020-06-25 16:43:59 -04:00
class DetailedStatus extends ImmutablePureComponent {
2016-09-25 08:20:29 -04:00
2017-05-12 08:44:10 -04:00
static propTypes = {
2019-03-12 12:34:00 -04:00
status : ImmutablePropTypes . map ,
2017-05-12 08:44:10 -04:00
onOpenMedia : PropTypes . func . isRequired ,
onOpenVideo : PropTypes . func . isRequired ,
2018-03-11 04:52:59 -04:00
onToggleHidden : PropTypes . func . isRequired ,
2022-09-23 17:00:12 -04:00
onTranslate : PropTypes . func . isRequired ,
2019-01-16 13:47:46 -05:00
measureHeight : PropTypes . bool ,
onHeightChange : PropTypes . func ,
domain : PropTypes . string . isRequired ,
2019-01-17 08:06:08 -05:00
compact : PropTypes . bool ,
2019-05-25 17:20:51 -04:00
showMedia : PropTypes . bool ,
2020-12-07 13:36:36 -05:00
pictureInPicture : ImmutablePropTypes . contains ( {
inUse : PropTypes . bool ,
available : PropTypes . bool ,
} ) ,
2019-05-25 17:20:51 -04:00
onToggleMediaVisibility : PropTypes . func ,
2023-10-19 13:44:55 -04:00
... WithRouterPropTypes ,
2019-01-16 13:47:46 -05:00
} ;
state = {
height : null ,
2017-05-12 08:44:10 -04:00
} ;
handleAccountClick = ( e ) => {
2023-10-19 13:44:55 -04:00
if ( e . button === 0 && ! ( e . ctrlKey || e . metaKey ) && this . props . history ) {
2016-09-25 08:20:29 -04:00
e . preventDefault ( ) ;
2023-10-27 10:03:21 -04:00
this . props . history . push ( ` /@ ${ this . props . status . getIn ( [ 'account' , 'acct' ] ) } ` ) ;
2016-09-25 08:20:29 -04:00
}
e . stopPropagation ( ) ;
2023-01-29 19:45:35 -05:00
} ;
2016-09-25 08:20:29 -04:00
2020-12-06 22:29:37 -05:00
handleOpenVideo = ( options ) => {
this . props . onOpenVideo ( this . props . status . getIn ( [ 'media_attachments' , 0 ] ) , options ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-09-13 21:39:10 -04:00
2018-03-11 04:52:59 -04:00
handleExpandedToggle = ( ) => {
this . props . onToggleHidden ( this . props . status ) ;
2023-01-29 19:45:35 -05:00
} ;
2018-03-11 04:52:59 -04:00
2019-01-16 13:47:46 -05:00
_measureHeight ( heightJustChanged ) {
if ( this . props . measureHeight && this . node ) {
2019-01-18 14:58:11 -05:00
scheduleIdleTask ( ( ) => this . node && this . setState ( { height : Math . ceil ( this . node . scrollHeight ) + 1 } ) ) ;
2019-01-16 13:47:46 -05:00
if ( this . props . onHeightChange && heightJustChanged ) {
this . props . onHeightChange ( ) ;
}
}
}
setRef = c => {
this . node = c ;
this . _measureHeight ( ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-01-16 13:47:46 -05:00
componentDidUpdate ( prevProps , prevState ) {
this . _measureHeight ( prevState . height !== this . state . height ) ;
}
handleModalLink = e => {
e . preventDefault ( ) ;
let href ;
if ( e . target . nodeName !== 'A' ) {
href = e . target . parentNode . href ;
} else {
href = e . target . href ;
}
window . open ( href , 'mastodon-intent' , 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes' ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-01-16 13:47:46 -05:00
2022-09-23 17:00:12 -04:00
handleTranslate = ( ) => {
const { onTranslate , status } = this . props ;
onTranslate ( status ) ;
2023-01-29 19:45:35 -05:00
} ;
2022-09-23 17:00:12 -04:00
2023-07-24 07:46:55 -04:00
_properStatus ( ) {
const { status } = this . props ;
if ( status . get ( 'reblog' , null ) !== null && typeof status . get ( 'reblog' ) === 'object' ) {
return status . get ( 'reblog' ) ;
} else {
return status ;
}
}
getAttachmentAspectRatio ( ) {
const attachments = this . _properStatus ( ) . get ( 'media_attachments' ) ;
if ( attachments . getIn ( [ 0 , 'type' ] ) === 'video' ) {
return ` ${ attachments . getIn ( [ 0 , 'meta' , 'original' , 'width' ] ) } / ${ attachments . getIn ( [ 0 , 'meta' , 'original' , 'height' ] ) } ` ;
} else if ( attachments . getIn ( [ 0 , 'type' ] ) === 'audio' ) {
return '16 / 9' ;
} else {
2023-10-09 07:38:29 -04:00
return ( attachments . size === 1 && attachments . getIn ( [ 0 , 'meta' , 'small' , 'aspect' ] ) ) ? attachments . getIn ( [ 0 , 'meta' , 'small' , 'aspect' ] ) : '3 / 2' ;
2023-07-24 07:46:55 -04:00
}
}
2016-09-25 08:20:29 -04:00
render ( ) {
2023-07-24 07:46:55 -04:00
const status = this . _properStatus ( ) ;
2019-01-16 13:47:46 -05:00
const outerStyle = { boxSizing : 'border-box' } ;
2023-10-24 13:45:08 -04:00
const { compact , pictureInPicture } = this . props ;
2019-01-16 13:47:46 -05:00
if ( ! status ) {
return null ;
}
2017-01-15 08:01:33 -05:00
let media = '' ;
let applicationLink = '' ;
2017-10-16 08:10:12 -04:00
let reblogLink = '' ;
2019-01-16 13:47:46 -05:00
let favouriteLink = '' ;
if ( this . props . measureHeight ) {
outerStyle . height = ` ${ this . state . height } px ` ;
}
2016-09-25 08:58:07 -04:00
2023-05-31 18:10:21 -04:00
const language = status . getIn ( [ 'translation' , 'language' ] ) || status . get ( 'language' ) ;
2020-12-07 13:36:36 -05:00
if ( pictureInPicture . get ( 'inUse' ) ) {
2023-07-24 07:46:55 -04:00
media = < PictureInPicturePlaceholder aspectRatio = { this . getAttachmentAspectRatio ( ) } / > ;
2020-09-28 07:29:43 -04:00
} else if ( status . get ( 'media_attachments' ) . size > 0 ) {
2019-08-23 16:38:02 -04:00
if ( status . getIn ( [ 'media_attachments' , 0 , 'type' ] ) === 'audio' ) {
const attachment = status . getIn ( [ 'media_attachments' , 0 ] ) ;
2023-05-31 18:10:21 -04:00
const description = attachment . getIn ( [ 'translation' , 'description' ] ) || attachment . get ( 'description' ) ;
2019-08-23 16:38:02 -04:00
media = (
< Audio
src = { attachment . get ( 'url' ) }
2023-05-31 18:10:21 -04:00
alt = { description }
lang = { language }
2019-08-23 16:38:02 -04:00
duration = { attachment . getIn ( [ 'meta' , 'original' , 'duration' ] , 0 ) }
2020-06-29 07:56:55 -04:00
poster = { attachment . get ( 'preview_url' ) || status . getIn ( [ 'account' , 'avatar_static' ] ) }
2020-07-05 12:28:25 -04:00
backgroundColor = { attachment . getIn ( [ 'meta' , 'colors' , 'background' ] ) }
foregroundColor = { attachment . getIn ( [ 'meta' , 'colors' , 'foreground' ] ) }
accentColor = { attachment . getIn ( [ 'meta' , 'colors' , 'accent' ] ) }
2022-08-13 09:39:05 -04:00
sensitive = { status . get ( 'sensitive' ) }
visible = { this . props . showMedia }
blurhash = { attachment . get ( 'blurhash' ) }
2020-06-23 06:20:14 -04:00
height = { 150 }
2022-08-13 09:39:05 -04:00
onToggleVisibility = { this . props . onToggleMediaVisibility }
2019-08-23 16:38:02 -04:00
/ >
) ;
} else if ( status . getIn ( [ 'media_attachments' , 0 , 'type' ] ) === 'video' ) {
2019-06-19 17:42:38 -04:00
const attachment = status . getIn ( [ 'media_attachments' , 0 ] ) ;
2023-05-31 18:10:21 -04:00
const description = attachment . getIn ( [ 'translation' , 'description' ] ) || attachment . get ( 'description' ) ;
2017-09-13 21:39:10 -04:00
media = (
< Video
2019-06-19 17:42:38 -04:00
preview = { attachment . get ( 'preview_url' ) }
2020-11-21 17:19:04 -05:00
frameRate = { attachment . getIn ( [ 'meta' , 'original' , 'frame_rate' ] ) }
2023-07-24 07:46:55 -04:00
aspectRatio = { ` ${ attachment . getIn ( [ 'meta' , 'original' , 'width' ] ) } / ${ attachment . getIn ( [ 'meta' , 'original' , 'height' ] ) } ` }
2019-06-19 17:42:38 -04:00
blurhash = { attachment . get ( 'blurhash' ) }
src = { attachment . get ( 'url' ) }
2023-05-31 18:10:21 -04:00
alt = { description }
lang = { language }
2017-09-13 21:39:10 -04:00
width = { 300 }
height = { 150 }
onOpenVideo = { this . handleOpenVideo }
sensitive = { status . get ( 'sensitive' ) }
2019-05-25 17:20:51 -04:00
visible = { this . props . showMedia }
onToggleVisibility = { this . props . onToggleMediaVisibility }
2017-09-13 21:39:10 -04:00
/ >
) ;
2016-09-25 08:58:07 -04:00
} else {
2017-09-23 23:58:30 -04:00
media = (
< MediaGallery
standalone
sensitive = { status . get ( 'sensitive' ) }
media = { status . get ( 'media_attachments' ) }
2023-05-31 18:10:21 -04:00
lang = { language }
2017-09-23 23:58:30 -04:00
height = { 300 }
onOpenMedia = { this . props . onOpenMedia }
2019-05-25 17:20:51 -04:00
visible = { this . props . showMedia }
onToggleVisibility = { this . props . onToggleMediaVisibility }
2017-09-23 23:58:30 -04:00
/ >
) ;
2016-09-25 08:58:07 -04:00
}
2017-04-19 08:28:00 -04:00
} else if ( status . get ( 'spoiler_text' ) . length === 0 ) {
2020-06-06 11:41:56 -04:00
media = < Card sensitive = { status . get ( 'sensitive' ) } onOpenMedia = { this . props . onOpenMedia } card = { status . get ( 'card' , null ) } / > ;
2016-09-25 08:58:07 -04:00
}
2016-09-25 08:20:29 -04:00
2017-01-15 08:01:33 -05:00
if ( status . get ( 'application' ) ) {
2024-03-14 05:18:24 -04:00
applicationLink = < > · < a className = 'detailed-status__application' href = { status . getIn ( [ 'application' , 'website' ] ) } target = '_blank' rel = 'noopener noreferrer' > { status . getIn ( [ 'application' , 'name' ] ) } < / a > < / > ;
2017-01-15 08:01:33 -05:00
}
2024-03-14 05:18:24 -04:00
const visibilityLink = < > · < VisibilityIcon visibility = { status . get ( 'visibility' ) } / > < / > ;
2017-10-16 08:10:12 -04:00
2020-03-10 13:39:47 -04:00
if ( [ 'private' , 'direct' ] . includes ( status . get ( 'visibility' ) ) ) {
2020-06-25 16:43:59 -04:00
reblogLink = '' ;
2023-10-19 13:44:55 -04:00
} else if ( this . props . history ) {
2019-01-16 13:47:46 -05:00
reblogLink = (
2024-03-14 05:18:24 -04:00
< Link to = { ` /@ ${ status . getIn ( [ 'account' , 'acct' ] ) } / ${ status . get ( 'id' ) } /reblogs ` } className = 'detailed-status__link' >
< span className = 'detailed-status__reblogs' >
< AnimatedNumber value = { status . get ( 'reblogs_count' ) } / >
< / span >
< FormattedMessage id = 'status.reblogs' defaultMessage = '{count, plural, one {boost} other {boosts}}' values = { { count : status . get ( 'reblogs_count' ) } } / >
< / Link >
2019-01-16 13:47:46 -05:00
) ;
} else {
reblogLink = (
2024-03-14 05:18:24 -04:00
< a href = { ` /interact/ ${ status . get ( 'id' ) } ?type=reblog ` } className = 'detailed-status__link' onClick = { this . handleModalLink } >
< span className = 'detailed-status__reblogs' >
< AnimatedNumber value = { status . get ( 'reblogs_count' ) } / >
< / span >
< FormattedMessage id = 'status.reblogs' defaultMessage = '{count, plural, one {boost} other {boosts}}' values = { { count : status . get ( 'reblogs_count' ) } } / >
< / a >
2019-01-16 13:47:46 -05:00
) ;
}
2023-10-19 13:44:55 -04:00
if ( this . props . history ) {
2019-01-16 13:47:46 -05:00
favouriteLink = (
2021-09-25 23:46:13 -04:00
< Link to = { ` /@ ${ status . getIn ( [ 'account' , 'acct' ] ) } / ${ status . get ( 'id' ) } /favourites ` } className = 'detailed-status__link' >
2019-01-16 13:47:46 -05:00
< span className = 'detailed-status__favorites' >
2020-01-24 23:23:05 -05:00
< AnimatedNumber value = { status . get ( 'favourites_count' ) } / >
2019-01-16 13:47:46 -05:00
< / span >
2024-03-14 05:18:24 -04:00
< FormattedMessage id = 'status.favourites' defaultMessage = '{count, plural, one {favorite} other {favorites}}' values = { { count : status . get ( 'favourites_count' ) } } / >
2019-01-16 13:47:46 -05:00
< / Link >
) ;
2017-10-16 08:10:12 -04:00
} else {
2019-01-16 13:47:46 -05:00
favouriteLink = (
< a href = { ` /interact/ ${ status . get ( 'id' ) } ?type=favourite ` } className = 'detailed-status__link' onClick = { this . handleModalLink } >
< span className = 'detailed-status__favorites' >
2020-01-24 23:23:05 -05:00
< AnimatedNumber value = { status . get ( 'favourites_count' ) } / >
2019-01-16 13:47:46 -05:00
< / span >
2024-03-14 05:18:24 -04:00
< FormattedMessage id = 'status.favourites' defaultMessage = '{count, plural, one {favorite} other {favorites}}' values = { { count : status . get ( 'favourites_count' ) } } / >
2019-01-16 13:47:46 -05:00
< / a >
) ;
2017-10-16 08:10:12 -04:00
}
2023-08-21 13:39:01 -04:00
const { statusContentProps , hashtagBar } = getHashtagBarForStatus ( status ) ;
2023-09-19 06:25:39 -04:00
const expanded = ! status . get ( 'hidden' ) || status . get ( 'spoiler_text' ) . length === 0 ;
2023-08-21 13:39:01 -04:00
2016-09-25 08:20:29 -04:00
return (
2019-01-17 08:06:08 -05:00
< div style = { outerStyle } >
2023-03-30 09:16:20 -04:00
< div ref = { this . setRef } className = { classNames ( 'detailed-status' , { compact } ) } >
{ status . get ( 'visibility' ) === 'direct' && (
< div className = 'status__prepend' >
2023-10-24 13:45:08 -04:00
< div className = 'status__prepend-icon-wrapper' > < Icon id = 'at' icon = { AlternateEmailIcon } className = 'status__prepend-icon' / > < / div >
2023-03-30 09:16:20 -04:00
< FormattedMessage id = 'status.direct_indicator' defaultMessage = 'Private mention' / >
< / div >
) }
2022-11-13 15:10:20 -05:00
< a href = { ` /@ ${ status . getIn ( [ 'account' , 'acct' ] ) } ` } onClick = { this . handleAccountClick } className = 'detailed-status__display-name' >
2022-10-25 13:02:21 -04:00
< div className = 'detailed-status__display-avatar' > < Avatar account = { status . get ( 'account' ) } size = { 46 } / > < / div >
2019-01-17 08:06:08 -05:00
< DisplayName account = { status . get ( 'account' ) } localDomain = { this . props . domain } / >
< / a >
2022-09-23 17:00:12 -04:00
< StatusContent
status = { status }
expanded = { ! status . get ( 'hidden' ) }
onExpandedToggle = { this . handleExpandedToggle }
onTranslate = { this . handleTranslate }
2023-08-21 13:39:01 -04:00
{ ... statusContentProps }
2022-09-23 17:00:12 -04:00
/ >
2019-01-17 08:06:08 -05:00
{ media }
2023-08-23 09:40:31 -04:00
{ expanded && hashtagBar }
2023-08-14 17:42:30 -04:00
2019-01-17 08:06:08 -05:00
< div className = 'detailed-status__meta' >
2024-03-14 05:18:24 -04:00
< div className = 'detailed-status__meta__line' >
< a className = 'detailed-status__datetime' href = { ` /@ ${ status . getIn ( [ 'account' , 'acct' ] ) } / ${ status . get ( 'id' ) } ` } target = '_blank' rel = 'noopener noreferrer' >
< FormattedDate value = { new Date ( status . get ( 'created_at' ) ) } year = 'numeric' month = 'short' day = '2-digit' hour = '2-digit' minute = '2-digit' / >
< / a >
{ visibilityLink }
{ applicationLink }
< / div >
{ status . get ( 'edited_at' ) && < div className = 'detailed-status__meta__line' > < EditedTimestamp statusId = { status . get ( 'id' ) } timestamp = { status . get ( 'edited_at' ) } / > < / div > }
< div className = 'detailed-status__meta__line' >
{ reblogLink }
·
{ favouriteLink }
< / div >
2019-01-17 08:06:08 -05:00
< / div >
2016-09-25 08:20:29 -04:00
< / div >
< / div >
) ;
}
2017-04-21 14:05:35 -04:00
}
2023-03-23 22:17:53 -04:00
2023-10-24 13:45:08 -04:00
export default withRouter ( DetailedStatus ) ;