2017-04-22 03:05:35 +09:00
import PropTypes from 'prop-types' ;
2023-05-23 17:15:17 +02:00
2025-08-21 16:07:31 +02:00
import { defineMessages , injectIntl } from 'react-intl' ;
2023-05-23 17:15:17 +02:00
2017-11-07 22:24:55 +09:00
import classNames from 'classnames' ;
2023-05-23 17:15:17 +02:00
import { Helmet } from 'react-helmet' ;
2023-10-19 19:44:55 +02:00
import { withRouter } from 'react-router-dom' ;
2025-09-24 11:27:33 +02:00
import { difference } from 'lodash' ;
2023-05-23 17:15:17 +02:00
2017-02-18 02:37:59 +01:00
import ImmutablePropTypes from 'react-immutable-proptypes' ;
2023-05-23 17:15:17 +02:00
import ImmutablePureComponent from 'react-immutable-pure-component' ;
import { connect } from 'react-redux' ;
2024-01-16 11:27:26 +01:00
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react' ;
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react' ;
2025-07-21 16:43:38 +02:00
import { Hotkeys } from 'mastodon/components/hotkeys' ;
2023-05-23 17:15:17 +02:00
import { Icon } from 'mastodon/components/icon' ;
2023-06-14 02:26:25 +09:00
import { LoadingIndicator } from 'mastodon/components/loading_indicator' ;
2025-09-25 14:26:50 +02:00
import { ScrollContainer } from 'mastodon/containers/scroll_container' ;
2023-05-23 17:15:17 +02:00
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error' ;
2024-05-19 19:07:32 +02:00
import { identityContextPropShape , withIdentity } from 'mastodon/identity_context' ;
2023-10-19 19:44:55 +02:00
import { WithRouterPropTypes } from 'mastodon/utils/react_router' ;
2023-05-23 17:15:17 +02:00
2017-02-18 02:37:59 +01:00
import {
2023-05-23 17:15:17 +02:00
unblockAccount ,
unmuteAccount ,
} from '../../actions/accounts' ;
import { initBlockModal } from '../../actions/blocks' ;
2016-10-24 17:11:02 +02:00
import {
replyCompose ,
2017-05-21 00:31:47 +09:00
mentionCompose ,
2018-04-09 17:09:11 +02:00
directCompose ,
2017-02-18 02:37:59 +01:00
} from '../../actions/compose' ;
2019-11-19 21:24:16 +01:00
import {
2024-03-15 18:36:41 +01:00
initDomainBlockModal ,
2019-11-19 21:24:16 +01:00
unblockDomain ,
} from '../../actions/domain_blocks' ;
2023-05-23 17:15:17 +02:00
import {
2024-07-22 17:45:07 +02:00
toggleFavourite ,
2023-05-23 17:15:17 +02:00
bookmark ,
unbookmark ,
2024-07-22 17:45:07 +02:00
toggleReblog ,
2023-05-23 17:15:17 +02:00
pin ,
unpin ,
} from '../../actions/interactions' ;
import { openModal } from '../../actions/modal' ;
2017-12-25 14:56:05 -05:00
import { initMuteModal } from '../../actions/mutes' ;
2017-02-14 20:59:26 +01:00
import { initReport } from '../../actions/reports' ;
2023-05-23 17:15:17 +02:00
import {
fetchStatus ,
muteStatus ,
unmuteStatus ,
deleteStatus ,
editStatus ,
hideStatus ,
revealStatus ,
translateStatus ,
undoStatusTranslation ,
} from '../../actions/statuses' ;
2025-08-21 16:07:31 +02:00
import { setStatusQuotePolicy } from '../../actions/statuses_typed' ;
2018-03-11 09:52:59 +01:00
import ColumnHeader from '../../components/column_header' ;
2023-05-23 17:15:17 +02:00
import { textForScreenReader , defaultMediaVisibility } from '../../components/status' ;
2025-05-21 17:50:45 +02:00
import { StatusQuoteManager } from '../../components/status_quoted' ;
2024-07-22 17:45:07 +02:00
import { deleteModal } from '../../initial_state' ;
2023-05-23 17:15:17 +02:00
import { makeGetStatus , makeGetPictureInPicture } from '../../selectors' ;
2025-04-28 15:38:40 +02:00
import { getAncestorsIds , getDescendantsIds } from 'mastodon/selectors/contexts' ;
2023-05-23 17:15:17 +02:00
import Column from '../ui/components/column' ;
2018-07-29 23:52:06 +09:00
import { attachFullscreenListener , detachFullscreenListener , isFullscreen } from '../ui/util/fullscreen' ;
2023-05-23 17:15:17 +02:00
import ActionBar from './components/action_bar' ;
2024-09-12 11:41:19 +02:00
import { DetailedStatus } from './components/detailed_status' ;
2025-07-23 15:42:07 +02:00
import { RefreshController } from './components/refresh_controller' ;
2025-08-28 14:33:23 +02:00
import { quoteComposeById } from '@/mastodon/actions/compose_typed' ;
2023-10-19 19:44:55 +02:00
2017-04-23 04:39:50 +02:00
const messages = defineMessages ( {
2018-03-11 09:52:59 +01:00
revealAll : { id : 'status.show_more_all' , defaultMessage : 'Show more for all' } ,
hideAll : { id : 'status.show_less_all' , defaultMessage : 'Show less for all' } ,
2023-04-24 02:07:19 -04:00
statusTitleWithAttachments : { id : 'status.title.with_attachments' , defaultMessage : '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' } ,
2018-08-23 17:26:21 +02:00
detailedStatus : { id : 'status.detailed_status' , defaultMessage : 'Detailed conversation view' } ,
2017-04-23 04:39:50 +02:00
} ) ;
2016-09-16 00:21:51 +02:00
2016-10-24 17:11:02 +02:00
const makeMapStateToProps = ( ) => {
const getStatus = makeGetStatus ( ) ;
2020-12-07 19:36:36 +01:00
const getPictureInPicture = makeGetPictureInPicture ( ) ;
2016-10-24 17:11:02 +02:00
2019-07-02 16:03:54 +02:00
const mapStateToProps = ( state , props ) => {
2025-03-25 14:11:49 +01:00
const status = getStatus ( state , { id : props . params . statusId , contextType : 'detailed' } ) ;
2020-12-07 19:36:36 +01:00
2025-04-28 15:38:40 +02:00
let ancestorsIds = [ ] ;
let descendantsIds = [ ] ;
2018-06-04 04:19:16 +02:00
2019-07-02 16:03:54 +02:00
if ( status ) {
2025-04-28 15:38:40 +02:00
ancestorsIds = getAncestorsIds ( state , status . get ( 'in_reply_to_id' ) ) ;
descendantsIds = getDescendantsIds ( state , status . get ( 'id' ) ) ;
2018-05-26 01:46:28 +09:00
}
return {
2022-10-20 14:35:29 +02:00
isLoading : state . getIn ( [ 'statuses' , props . params . statusId , 'isLoading' ] ) ,
2018-05-26 01:46:28 +09:00
status ,
ancestorsIds ,
descendantsIds ,
2018-10-05 18:44:44 +02:00
askReplyConfirmation : state . getIn ( [ 'compose' , 'text' ] ) . trim ( ) . length !== 0 ,
2019-01-17 14:06:08 +01:00
domain : state . getIn ( [ 'meta' , 'domain' ] ) ,
2020-12-07 19:36:36 +01:00
pictureInPicture : getPictureInPicture ( state , { id : props . params . statusId } ) ,
2018-05-26 01:46:28 +09:00
} ;
} ;
2016-10-24 17:11:02 +02:00
return mapStateToProps ;
} ;
2016-09-16 00:21:51 +02:00
2022-09-29 04:39:33 +02:00
const truncate = ( str , num ) => {
2023-05-29 18:55:16 +09:00
const arr = Array . from ( str ) ;
if ( arr . length > num ) {
return arr . slice ( 0 , num ) . join ( '' ) + '…' ;
2022-09-29 04:39:33 +02:00
} else {
return str ;
}
} ;
2023-04-14 10:29:09 -05:00
const titleFromStatus = ( intl , status ) => {
2022-09-29 04:39:33 +02:00
const displayName = status . getIn ( [ 'account' , 'display_name' ] ) ;
const username = status . getIn ( [ 'account' , 'username' ] ) ;
2023-04-14 10:29:09 -05:00
const user = displayName . trim ( ) . length === 0 ? username : displayName ;
2022-09-29 04:39:33 +02:00
const text = status . get ( 'search_index' ) ;
2023-04-14 10:29:09 -05:00
const attachmentCount = status . get ( 'media_attachments' ) . size ;
2022-09-29 04:39:33 +02:00
2023-04-14 10:29:09 -05:00
return text ? ` ${ user } : " ${ truncate ( text , 30 ) } " ` : intl . formatMessage ( messages . statusTitleWithAttachments , { user , attachmentCount } ) ;
2022-09-29 04:39:33 +02:00
} ;
2018-09-15 00:59:48 +09:00
class Status extends ImmutablePureComponent {
2017-05-12 21:44:10 +09:00
static propTypes = {
2024-05-19 19:07:32 +02:00
identity : identityContextPropShape ,
2017-05-12 21:44:10 +09:00
params : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
status : ImmutablePropTypes . map ,
2022-10-20 14:35:29 +02:00
isLoading : PropTypes . bool ,
2025-04-28 15:38:40 +02:00
ancestorsIds : PropTypes . arrayOf ( PropTypes . string ) . isRequired ,
descendantsIds : PropTypes . arrayOf ( PropTypes . string ) . isRequired ,
2017-05-21 00:31:47 +09:00
intl : PropTypes . object . isRequired ,
2018-10-05 18:44:44 +02:00
askReplyConfirmation : PropTypes . bool ,
2019-08-01 12:26:58 +02:00
multiColumn : PropTypes . bool ,
2019-01-17 14:06:08 +01:00
domain : PropTypes . string . isRequired ,
2020-12-07 19:36:36 +01:00
pictureInPicture : ImmutablePropTypes . contains ( {
inUse : PropTypes . bool ,
available : PropTypes . bool ,
} ) ,
2023-10-19 19:44:55 +02:00
... WithRouterPropTypes
2017-05-12 21:44:10 +09:00
} ;
2016-09-16 00:21:51 +02:00
2017-11-07 22:24:55 +09:00
state = {
fullscreen : false ,
2019-05-26 13:48:16 +02:00
showMedia : defaultMediaVisibility ( this . props . status ) ,
2019-05-29 16:33:15 +02:00
loadedStatusId : undefined ,
2025-09-24 11:27:33 +02:00
/ * *
* Holds the ids of newly added replies , excluding the initial load .
* Used to highlight newly added replies in the UI
* /
newRepliesIds : [ ] ,
2017-11-07 22:24:55 +09:00
} ;
2023-05-10 03:05:32 -04:00
UNSAFE _componentWillMount ( ) {
2025-11-13 13:54:28 +01:00
this . props . dispatch ( fetchStatus ( this . props . params . statusId , { forceFetch : true } ) ) ;
2017-04-22 03:05:35 +09:00
}
2016-09-16 00:21:51 +02:00
2017-11-07 22:24:55 +09:00
componentDidMount ( ) {
attachFullscreenListener ( this . onFullScreenChange ) ;
}
2023-05-10 03:05:32 -04:00
UNSAFE _componentWillReceiveProps ( nextProps ) {
2016-09-16 00:21:51 +02:00
if ( nextProps . params . statusId !== this . props . params . statusId && nextProps . params . statusId ) {
2025-11-13 13:54:28 +01:00
this . props . dispatch ( fetchStatus ( nextProps . params . statusId , { forceFetch : true } ) ) ;
2016-09-16 00:21:51 +02:00
}
2019-05-26 13:48:16 +02:00
2019-05-29 16:33:15 +02:00
if ( nextProps . status && nextProps . status . get ( 'id' ) !== this . state . loadedStatusId ) {
this . setState ( { showMedia : defaultMediaVisibility ( nextProps . status ) , loadedStatusId : nextProps . status . get ( 'id' ) } ) ;
2019-05-25 23:20:51 +02:00
}
}
handleToggleMediaVisibility = ( ) => {
this . setState ( { showMedia : ! this . state . showMedia } ) ;
2023-01-29 19:45:35 -05:00
} ;
2016-09-16 00:21:51 +02:00
2017-05-12 21:44:10 +09:00
handleFavouriteClick = ( status ) => {
2022-10-07 10:14:31 +02:00
const { dispatch } = this . props ;
2024-05-19 19:07:32 +02:00
const { signedIn } = this . props . identity ;
2022-10-07 10:14:31 +02:00
if ( signedIn ) {
2024-07-22 17:45:07 +02:00
dispatch ( toggleFavourite ( status . get ( 'id' ) ) ) ;
2017-02-17 02:33:10 +01:00
} else {
2023-05-25 22:42:37 +09:00
dispatch ( openModal ( {
modalType : 'INTERACTION' ,
modalProps : {
accountId : status . getIn ( [ 'account' , 'id' ] ) ,
2023-07-27 16:11:17 +02:00
url : status . get ( 'uri' ) ,
2023-05-25 22:42:37 +09:00
} ,
2022-10-07 10:14:31 +02:00
} ) ) ;
2017-02-17 02:33:10 +01:00
}
2023-01-29 19:45:35 -05:00
} ;
2016-09-17 17:47:26 +02:00
2017-08-25 01:41:18 +02:00
handlePin = ( status ) => {
if ( status . get ( 'pinned' ) ) {
this . props . dispatch ( unpin ( status ) ) ;
} else {
this . props . dispatch ( pin ( status ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2017-08-25 01:41:18 +02:00
2017-05-12 21:44:10 +09:00
handleReplyClick = ( status ) => {
2024-07-25 19:05:54 +02:00
const { askReplyConfirmation , dispatch } = this . props ;
2024-05-19 19:07:32 +02:00
const { signedIn } = this . props . identity ;
2022-10-07 10:14:31 +02:00
if ( signedIn ) {
if ( askReplyConfirmation ) {
2024-07-25 19:05:54 +02:00
dispatch ( openModal ( { modalType : 'CONFIRM_REPLY' , modalProps : { status } } ) ) ;
2022-10-07 10:14:31 +02:00
} else {
2024-07-19 17:26:44 +02:00
dispatch ( replyCompose ( status ) ) ;
2022-10-07 10:14:31 +02:00
}
2018-10-05 18:44:44 +02:00
} else {
2023-05-25 22:42:37 +09:00
dispatch ( openModal ( {
modalType : 'INTERACTION' ,
modalProps : {
accountId : status . getIn ( [ 'account' , 'id' ] ) ,
2023-07-27 16:11:17 +02:00
url : status . get ( 'uri' ) ,
2023-05-25 22:42:37 +09:00
} ,
2022-10-07 10:14:31 +02:00
} ) ) ;
2018-10-05 18:44:44 +02:00
}
2023-01-29 19:45:35 -05:00
} ;
2016-09-17 17:47:26 +02:00
2017-05-12 21:44:10 +09:00
handleReblogClick = ( status , e ) => {
2022-10-07 10:14:31 +02:00
const { dispatch } = this . props ;
2024-05-19 19:07:32 +02:00
const { signedIn } = this . props . identity ;
2022-10-07 10:14:31 +02:00
if ( signedIn ) {
2024-07-22 17:45:07 +02:00
dispatch ( toggleReblog ( status . get ( 'id' ) , e && e . shiftKey ) ) ;
2022-10-07 10:14:31 +02:00
} else {
2023-05-25 22:42:37 +09:00
dispatch ( openModal ( {
modalType : 'INTERACTION' ,
modalProps : {
accountId : status . getIn ( [ 'account' , 'id' ] ) ,
2023-07-27 16:11:17 +02:00
url : status . get ( 'uri' ) ,
2023-05-25 22:42:37 +09:00
} ,
2022-10-07 10:14:31 +02:00
} ) ) ;
2017-02-17 02:33:10 +01:00
}
2023-01-29 19:45:35 -05:00
} ;
2016-09-17 17:47:26 +02:00
2019-11-13 23:02:10 +01:00
handleBookmarkClick = ( status ) => {
if ( status . get ( 'bookmarked' ) ) {
this . props . dispatch ( unbookmark ( status ) ) ;
} else {
this . props . dispatch ( bookmark ( status ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2019-11-13 23:02:10 +01:00
2024-07-19 17:26:44 +02:00
handleDeleteClick = ( status , withRedraft = false ) => {
2025-08-29 05:50:19 +09:00
const { dispatch , history } = this . props ;
const handleDeleteSuccess = ( ) => {
history . push ( '/' ) ;
} ;
2017-04-23 04:39:50 +02:00
2017-10-29 08:10:15 -07:00
if ( ! deleteModal ) {
2025-08-29 05:50:19 +09:00
dispatch ( deleteStatus ( status . get ( 'id' ) , withRedraft ) )
. then ( ( ) => {
if ( ! withRedraft ) {
handleDeleteSuccess ( ) ;
}
} )
. catch ( ( ) => {
// Error handling - could show error message
} ) ;
2017-05-29 11:56:13 -04:00
} else {
2025-08-29 05:50:19 +09:00
dispatch ( openModal ( {
modalType : 'CONFIRM_DELETE_STATUS' ,
modalProps : {
statusId : status . get ( 'id' ) ,
withRedraft ,
onDeleteSuccess : handleDeleteSuccess
}
} ) ) ;
2017-05-29 11:56:13 -04:00
}
2023-01-29 19:45:35 -05:00
} ;
2016-10-09 22:19:15 +02:00
2025-08-06 13:52:56 +02:00
handleRevokeQuoteClick = ( status ) => {
const { dispatch } = this . props ;
dispatch ( openModal ( { modalType : 'CONFIRM_REVOKE_QUOTE' , modalProps : { statusId : status . get ( 'id' ) , quotedStatusId : status . getIn ( [ 'quote' , 'quoted_status' ] ) } } ) ) ;
} ;
2025-08-14 17:04:32 +02:00
handleQuotePolicyChange = ( status ) => {
2025-08-21 16:07:31 +02:00
const statusId = status . get ( 'id' ) ;
2025-08-14 17:04:32 +02:00
const { dispatch } = this . props ;
2025-08-21 16:07:31 +02:00
const handleChange = ( _ , quotePolicy ) => {
dispatch (
setStatusQuotePolicy ( { policy : quotePolicy , statusId } ) ,
) ;
}
dispatch ( openModal ( { modalType : 'COMPOSE_PRIVACY' , modalProps : { statusId , onChange : handleChange } } ) ) ;
2025-08-14 17:04:32 +02:00
} ;
2025-11-04 12:01:25 +01:00
handleQuote = ( status ) => {
const { dispatch } = this . props ;
dispatch ( quoteComposeById ( status . get ( 'id' ) ) ) ;
} ;
2024-07-19 17:26:44 +02:00
handleEditClick = ( status ) => {
2024-07-25 19:05:54 +02:00
const { dispatch , askReplyConfirmation } = this . props ;
if ( askReplyConfirmation ) {
dispatch ( openModal ( { modalType : 'CONFIRM_EDIT_STATUS' , modalProps : { statusId : status . get ( 'id' ) } } ) ) ;
} else {
dispatch ( editStatus ( status . get ( 'id' ) ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2022-02-10 00:15:30 +01:00
2024-07-19 17:26:44 +02:00
handleDirectClick = ( account ) => {
this . props . dispatch ( directCompose ( account ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2018-04-09 17:09:11 +02:00
2024-07-19 17:26:44 +02:00
handleMentionClick = ( account ) => {
this . props . dispatch ( mentionCompose ( account ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2016-10-24 17:11:02 +02:00
2023-05-11 12:41:55 +02:00
handleOpenMedia = ( media , index , lang ) => {
2023-05-25 22:42:37 +09:00
this . props . dispatch ( openModal ( {
modalType : 'MEDIA' ,
modalProps : { statusId : this . props . status . get ( 'id' ) , media , index , lang } ,
} ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2016-10-24 18:07:40 +02:00
2023-05-11 12:41:55 +02:00
handleOpenVideo = ( media , lang , options ) => {
2023-05-25 22:42:37 +09:00
this . props . dispatch ( openModal ( {
modalType : 'VIDEO' ,
modalProps : { statusId : this . props . status . get ( 'id' ) , media , lang , options } ,
} ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-04-13 09:04:18 -04:00
2019-11-29 17:02:36 +01:00
handleHotkeyOpenMedia = e => {
2020-11-27 03:24:11 +01:00
const { status } = this . props ;
2019-11-29 17:02:36 +01:00
e . preventDefault ( ) ;
if ( status . get ( 'media_attachments' ) . size > 0 ) {
2020-11-27 03:24:11 +01:00
if ( status . getIn ( [ 'media_attachments' , 0 , 'type' ] ) === 'video' ) {
2020-04-25 12:16:05 +02:00
this . handleOpenVideo ( status . getIn ( [ 'media_attachments' , 0 ] ) , { startTime : 0 } ) ;
2019-11-29 17:02:36 +01:00
} else {
this . handleOpenMedia ( status . get ( 'media_attachments' ) , 0 ) ;
}
}
2023-01-29 19:45:35 -05:00
} ;
2019-11-29 17:02:36 +01:00
2017-12-25 14:56:05 -05:00
handleMuteClick = ( account ) => {
this . props . dispatch ( initMuteModal ( account ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-12-25 14:56:05 -05:00
handleConversationMuteClick = ( status ) => {
if ( status . get ( 'muted' ) ) {
this . props . dispatch ( unmuteStatus ( status . get ( 'id' ) ) ) ;
} else {
this . props . dispatch ( muteStatus ( status . get ( 'id' ) ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2017-12-25 14:56:05 -05:00
2018-03-11 09:52:59 +01:00
handleToggleHidden = ( status ) => {
if ( status . get ( 'hidden' ) ) {
this . props . dispatch ( revealStatus ( status . get ( 'id' ) ) ) ;
} else {
this . props . dispatch ( hideStatus ( status . get ( 'id' ) ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2018-03-11 09:52:59 +01:00
handleToggleAll = ( ) => {
const { status , ancestorsIds , descendantsIds } = this . props ;
2025-04-28 15:38:40 +02:00
const statusIds = [ status . get ( 'id' ) ] . concat ( ancestorsIds , descendantsIds ) ;
2018-03-11 09:52:59 +01:00
if ( status . get ( 'hidden' ) ) {
this . props . dispatch ( revealStatus ( statusIds ) ) ;
} else {
this . props . dispatch ( hideStatus ( statusIds ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2018-03-11 09:52:59 +01:00
2022-09-23 23:00:12 +02:00
handleTranslate = status => {
const { dispatch } = this . props ;
if ( status . get ( 'translation' ) ) {
2023-06-01 00:10:21 +02:00
dispatch ( undoStatusTranslation ( status . get ( 'id' ) , status . get ( 'poll' ) ) ) ;
2022-09-23 23:00:12 +02:00
} else {
dispatch ( translateStatus ( status . get ( 'id' ) ) ) ;
}
2023-01-29 19:45:35 -05:00
} ;
2022-09-23 23:00:12 +02:00
2019-03-26 17:34:02 +01:00
handleBlockClick = ( status ) => {
2019-09-29 21:46:05 +02:00
const { dispatch } = this . props ;
2019-03-26 17:34:02 +01:00
const account = status . get ( 'account' ) ;
2019-09-29 21:46:05 +02:00
dispatch ( initBlockModal ( account ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-12-25 14:56:05 -05:00
2017-05-12 21:44:10 +09:00
handleReport = ( status ) => {
2017-02-14 20:59:26 +01:00
this . props . dispatch ( initReport ( status . get ( 'account' ) , status ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-02-14 20:59:26 +01:00
2017-08-31 03:38:35 +02:00
handleEmbed = ( status ) => {
2023-05-25 22:42:37 +09:00
this . props . dispatch ( openModal ( {
modalType : 'EMBED' ,
2023-07-13 15:53:03 +02:00
modalProps : { id : status . get ( 'id' ) } ,
2023-05-25 22:42:37 +09:00
} ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-08-31 03:38:35 +02:00
2019-11-19 21:24:16 +01:00
handleUnmuteClick = account => {
this . props . dispatch ( unmuteAccount ( account . get ( 'id' ) ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-11-19 21:24:16 +01:00
handleUnblockClick = account => {
this . props . dispatch ( unblockAccount ( account . get ( 'id' ) ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-11-19 21:24:16 +01:00
2024-03-15 18:36:41 +01:00
handleBlockDomainClick = account => {
this . props . dispatch ( initDomainBlockModal ( account ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-11-19 21:24:16 +01:00
handleUnblockDomainClick = domain => {
this . props . dispatch ( unblockDomain ( domain ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-11-19 21:24:16 +01:00
2017-10-06 01:07:59 +02:00
handleHotkeyReply = e => {
e . preventDefault ( ) ;
this . handleReplyClick ( this . props . status ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
handleHotkeyFavourite = ( ) => {
this . handleFavouriteClick ( this . props . status ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
handleHotkeyBoost = ( ) => {
this . handleReblogClick ( this . props . status ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
2025-08-28 14:33:23 +02:00
handleHotkeyQuote = ( ) => {
this . props . dispatch ( quoteComposeById ( this . props . status . get ( 'id' ) ) ) ;
} ;
2017-10-06 01:07:59 +02:00
handleHotkeyMention = e => {
e . preventDefault ( ) ;
2018-07-03 02:17:18 +02:00
this . handleMentionClick ( this . props . status . get ( 'account' ) ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
handleHotkeyOpenProfile = ( ) => {
2023-10-19 19:44:55 +02:00
this . props . history . push ( ` /@ ${ this . props . status . getIn ( [ 'account' , 'acct' ] ) } ` ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
2018-04-17 21:33:59 -04:00
handleHotkeyToggleHidden = ( ) => {
this . handleToggleHidden ( this . props . status ) ;
2023-01-29 19:45:35 -05:00
} ;
2018-04-17 21:33:59 -04:00
2019-05-25 23:20:51 +02:00
handleHotkeyToggleSensitive = ( ) => {
this . handleToggleMediaVisibility ( ) ;
2023-01-29 19:45:35 -05:00
} ;
2019-05-25 23:20:51 +02:00
2025-01-03 10:24:50 +01:00
handleHotkeyTranslate = ( ) => {
this . handleTranslate ( this . props . status ) ;
} ;
2023-04-24 08:07:03 +02:00
renderChildren ( list , ancestors ) {
const { params : { statusId } } = this . props ;
return list . map ( ( id , i ) => (
2025-05-21 17:50:45 +02:00
< StatusQuoteManager
2017-10-06 01:07:59 +02:00
key = { id }
id = { id }
2018-06-29 15:34:36 +02:00
contextType = 'thread'
2025-04-28 15:38:40 +02:00
previousId = { i > 0 ? list [ i - 1 ] : undefined }
nextId = { list [ i + 1 ] || ( ancestors && statusId ) }
2023-04-24 08:07:03 +02:00
rootId = { statusId }
2025-09-24 11:27:33 +02:00
shouldHighlightOnMount = { this . state . newRepliesIds . includes ( id ) }
2017-10-06 01:07:59 +02:00
/ >
) ) ;
}
2024-01-04 15:17:38 +01:00
setContainerRef = c => {
2017-10-06 01:07:59 +02:00
this . node = c ;
2023-01-29 19:45:35 -05:00
} ;
2017-10-06 01:07:59 +02:00
2024-01-04 15:17:38 +01:00
setStatusRef = c => {
this . statusNode = c ;
} ;
2023-10-09 16:46:09 +02:00
componentDidUpdate ( prevProps ) {
2025-11-18 17:20:50 +01:00
const { status , descendantsIds } = this . props ;
2023-10-09 16:46:09 +02:00
2025-10-23 17:52:07 +02:00
const isSameStatus = status && ( prevProps . status ? . get ( 'id' ) === status . get ( 'id' ) ) ;
2025-09-24 11:27:33 +02:00
// Only highlight replies after the initial load
2025-10-23 17:52:07 +02:00
if ( prevProps . descendantsIds . length && isSameStatus ) {
2025-09-24 11:27:33 +02:00
const newRepliesIds = difference ( descendantsIds , prevProps . descendantsIds ) ;
if ( newRepliesIds . length ) {
this . setState ( { newRepliesIds } ) ;
}
}
2023-10-09 16:46:09 +02:00
}
2017-11-07 22:24:55 +09:00
componentWillUnmount ( ) {
detachFullscreenListener ( this . onFullScreenChange ) ;
}
onFullScreenChange = ( ) => {
this . setState ( { fullscreen : isFullscreen ( ) } ) ;
2023-01-29 19:45:35 -05:00
} ;
2017-11-07 22:24:55 +09:00
2025-09-25 14:26:50 +02:00
shouldUpdateScroll = ( prevLocation , location ) => {
2023-10-09 12:21:02 +02:00
// Do not change scroll when opening a modal
2025-09-25 14:26:50 +02:00
if ( location . state ? . mastodonModalKey !== prevLocation ? . state ? . mastodonModalKey ) {
2023-10-09 12:21:02 +02:00
return false ;
}
// Scroll to focused post if it is loaded
2024-01-04 15:17:38 +01:00
if ( this . statusNode ) {
return [ 0 , this . statusNode . offsetTop ] ;
2023-10-09 12:21:02 +02:00
}
// Do not scroll otherwise, `componentDidUpdate` will take care of that
return false ;
} ;
2016-09-16 00:21:51 +02:00
render ( ) {
2024-08-09 09:12:51 -07:00
let ancestors , descendants , remoteHint ;
2025-07-23 15:42:07 +02:00
const { isLoading , status , ancestorsIds , descendantsIds , refresh , intl , domain , multiColumn , pictureInPicture } = this . props ;
2017-11-07 22:24:55 +09:00
const { fullscreen } = this . state ;
2016-09-16 00:21:51 +02:00
2022-10-20 14:35:29 +02:00
if ( isLoading ) {
return (
< Column >
< LoadingIndicator / >
< / Column >
) ;
}
2016-09-16 00:21:51 +02:00
if ( status === null ) {
2016-10-07 16:00:11 +02:00
return (
2023-04-12 12:44:58 +02:00
< BundleColumnError multiColumn = { multiColumn } errorType = 'routing' / >
2016-10-07 16:00:11 +02:00
) ;
2016-09-16 00:21:51 +02:00
}
2025-04-28 15:38:40 +02:00
if ( ancestorsIds && ancestorsIds . length > 0 ) {
2023-04-24 08:07:03 +02:00
ancestors = < > { this . renderChildren ( ancestorsIds , true ) } < / > ;
2016-10-24 17:11:02 +02:00
}
2025-04-28 15:38:40 +02:00
if ( descendantsIds && descendantsIds . length > 0 ) {
2023-04-12 12:44:58 +02:00
descendants = < > { this . renderChildren ( descendantsIds ) } < / > ;
2016-10-24 17:11:02 +02:00
}
2022-10-20 14:35:29 +02:00
const isLocal = status . getIn ( [ 'account' , 'acct' ] , '' ) . indexOf ( '@' ) === - 1 ;
const isIndexable = ! status . getIn ( [ 'account' , 'noindex' ] ) ;
2017-10-06 01:07:59 +02:00
const handlers = {
reply : this . handleHotkeyReply ,
favourite : this . handleHotkeyFavourite ,
boost : this . handleHotkeyBoost ,
2025-08-28 14:33:23 +02:00
quote : this . handleHotkeyQuote ,
2017-10-06 01:07:59 +02:00
mention : this . handleHotkeyMention ,
openProfile : this . handleHotkeyOpenProfile ,
2018-04-17 21:33:59 -04:00
toggleHidden : this . handleHotkeyToggleHidden ,
2019-05-25 23:20:51 +02:00
toggleSensitive : this . handleHotkeyToggleSensitive ,
2019-11-29 17:02:36 +01:00
openMedia : this . handleHotkeyOpenMedia ,
2025-01-03 10:24:50 +01:00
onTranslate : this . handleHotkeyTranslate ,
2017-10-06 01:07:59 +02:00
} ;
2016-09-16 00:21:51 +02:00
return (
2019-08-01 19:17:17 +02:00
< Column bindToDocument = { ! multiColumn } label = { intl . formatMessage ( messages . detailedStatus ) } >
2018-03-11 09:52:59 +01:00
< ColumnHeader
showBackButton
2019-08-01 12:26:58 +02:00
multiColumn = { multiColumn }
2018-03-11 09:52:59 +01:00
extraButton = { (
2023-10-24 19:45:08 +02:00
< button type = 'button' className = 'column-header__button' title = { intl . formatMessage ( status . get ( 'hidden' ) ? messages . revealAll : messages . hideAll ) } aria - label = { intl . formatMessage ( status . get ( 'hidden' ) ? messages . revealAll : messages . hideAll ) } onClick = { this . handleToggleAll } > < Icon id = { status . get ( 'hidden' ) ? 'eye-slash' : 'eye' } icon = { status . get ( 'hidden' ) ? VisibilityOffIcon : VisibilityIcon } / > < / button >
2018-03-11 09:52:59 +01:00
) }
/ >
2016-09-18 13:03:37 +02:00
2025-09-25 18:14:49 +02:00
< ScrollContainer scrollKey = 'thread' shouldUpdateScroll = { this . shouldUpdateScroll } childRef = { this . setContainerRef } >
2025-10-07 18:43:40 +02:00
< div className = { classNames ( 'item-list scrollable scrollable--flex' , { fullscreen } ) } ref = { this . setContainerRef } >
2016-10-24 17:11:02 +02:00
{ ancestors }
2016-09-18 13:03:37 +02:00
2025-07-21 16:43:38 +02:00
< Hotkeys handlers = { handlers } >
2025-09-22 15:50:29 +02:00
< div className = { classNames ( 'focusable' , 'detailed-status__wrapper' , ` detailed-status__wrapper- ${ status . get ( 'visibility' ) } ` ) } tabIndex = { 0 } aria - label = { textForScreenReader ( { intl , status } ) } ref = { this . setStatusRef } >
2017-10-06 01:07:59 +02:00
< DetailedStatus
2020-01-06 18:22:17 +01:00
key = { ` details- ${ status . get ( 'id' ) } ` }
2017-10-06 01:07:59 +02:00
status = { status }
onOpenVideo = { this . handleOpenVideo }
onOpenMedia = { this . handleOpenMedia }
2018-03-11 09:52:59 +01:00
onToggleHidden = { this . handleToggleHidden }
2022-09-23 23:00:12 +02:00
onTranslate = { this . handleTranslate }
2019-01-17 14:06:08 +01:00
domain = { domain }
2019-05-25 23:20:51 +02:00
showMedia = { this . state . showMedia }
onToggleMediaVisibility = { this . handleToggleMediaVisibility }
2020-12-07 19:36:36 +01:00
pictureInPicture = { pictureInPicture }
2025-11-18 17:20:50 +01:00
ancestors = { this . props . ancestorsIds . length }
multiColumn = { multiColumn }
2017-10-06 01:07:59 +02:00
/ >
< ActionBar
2020-01-06 18:22:17 +01:00
key = { ` action-bar- ${ status . get ( 'id' ) } ` }
2017-10-06 01:07:59 +02:00
status = { status }
onReply = { this . handleReplyClick }
onFavourite = { this . handleFavouriteClick }
onReblog = { this . handleReblogClick }
2019-11-13 23:02:10 +01:00
onBookmark = { this . handleBookmarkClick }
2017-10-06 01:07:59 +02:00
onDelete = { this . handleDeleteClick }
2025-08-06 13:52:56 +02:00
onRevokeQuote = { this . handleRevokeQuoteClick }
2025-08-14 17:04:32 +02:00
onQuotePolicyChange = { this . handleQuotePolicyChange }
2025-11-04 12:01:25 +01:00
onQuote = { this . handleQuote }
2022-02-10 00:15:30 +01:00
onEdit = { this . handleEditClick }
2018-04-09 17:09:11 +02:00
onDirect = { this . handleDirectClick }
2017-10-06 01:07:59 +02:00
onMention = { this . handleMentionClick }
2017-12-25 14:56:05 -05:00
onMute = { this . handleMuteClick }
2019-11-19 21:24:16 +01:00
onUnmute = { this . handleUnmuteClick }
2017-12-25 14:56:05 -05:00
onMuteConversation = { this . handleConversationMuteClick }
onBlock = { this . handleBlockClick }
2019-11-19 21:24:16 +01:00
onUnblock = { this . handleUnblockClick }
onBlockDomain = { this . handleBlockDomainClick }
onUnblockDomain = { this . handleUnblockDomainClick }
2017-10-06 01:07:59 +02:00
onReport = { this . handleReport }
onPin = { this . handlePin }
onEmbed = { this . handleEmbed }
/ >
< / div >
2025-07-21 16:43:38 +02:00
< / Hotkeys >
2016-10-19 18:20:19 +02:00
2025-07-29 11:00:27 +02:00
{ descendants }
2025-10-22 11:43:03 +02:00
< RefreshController
isLocal = { isLocal }
statusId = { status . get ( 'id' ) }
statusCreatedAt = { status . get ( 'created_at' ) }
/ >
2016-10-19 18:20:19 +02:00
< / div >
< / ScrollContainer >
2022-09-29 04:39:33 +02:00
< Helmet >
2023-04-14 10:29:09 -05:00
< title > { titleFromStatus ( intl , status ) } < / title >
2022-10-20 14:35:29 +02:00
< meta name = 'robots' content = { ( isLocal && isIndexable ) ? 'all' : 'noindex' } / >
2023-07-05 11:25:27 +02:00
< link rel = 'canonical' href = { status . get ( 'url' ) } / >
2022-09-29 04:39:33 +02:00
< / Helmet >
2016-10-07 16:00:11 +02:00
< / Column >
2016-09-16 00:21:51 +02:00
) ;
}
2017-04-22 03:05:35 +09:00
}
2023-03-24 11:17:53 +09:00
2024-05-19 19:07:32 +02:00
export default withRouter ( injectIntl ( connect ( makeMapStateToProps ) ( withIdentity ( Status ) ) ) ) ;