2022-01-10 14:17:52 -08:00
import classNames from 'classnames' ;
2021-10-26 08:38:49 -07:00
import { List as ImmutableList , OrderedSet as ImmutableOrderedSet } from 'immutable' ;
2022-01-10 14:17:52 -08:00
import React from 'react' ;
import { HotKeys } from 'react-hotkeys' ;
2022-01-10 14:01:24 -08:00
import ImmutablePureComponent from 'react-immutable-pure-component' ;
2022-04-04 12:17:24 -07:00
import { defineMessages , injectIntl , FormattedMessage , WrappedComponentProps as IntlComponentProps } from 'react-intl' ;
2022-01-10 14:17:52 -08:00
import { connect } from 'react-redux' ;
2022-04-04 12:17:24 -07:00
import { withRouter , RouteComponentProps } from 'react-router-dom' ;
2022-01-10 14:01:24 -08:00
import { createSelector } from 'reselect' ;
2022-01-10 14:25:06 -08:00
2022-01-10 14:17:52 -08:00
import { launchChat } from 'soapbox/actions/chats' ;
2022-01-10 14:01:24 -08:00
import {
deactivateUserModal ,
deleteUserModal ,
deleteStatusModal ,
toggleStatusSensitivityModal ,
} from 'soapbox/actions/moderation' ;
2022-01-10 14:17:52 -08:00
import { getSettings } from 'soapbox/actions/settings' ;
import { getSoapboxConfig } from 'soapbox/actions/soapbox' ;
2022-04-23 10:20:25 -07:00
import ScrollableList from 'soapbox/components/scrollable_list' ;
2022-01-10 14:17:52 -08:00
import SubNavigation from 'soapbox/components/sub_navigation' ;
2022-05-16 19:30:30 -07:00
import Tombstone from 'soapbox/components/tombstone' ;
2022-05-11 12:35:56 -07:00
import { Column , Stack } from 'soapbox/components/ui' ;
2022-04-04 12:44:31 -07:00
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status' ;
2022-01-10 14:17:52 -08:00
import PendingStatus from 'soapbox/features/ui/components/pending_status' ;
2022-01-10 14:25:06 -08:00
2022-01-10 14:17:52 -08:00
import { blockAccount } from '../../actions/accounts' ;
import {
replyCompose ,
mentionCompose ,
directCompose ,
2022-01-23 09:44:17 -08:00
quoteCompose ,
2022-01-10 14:17:52 -08:00
} from '../../actions/compose' ;
import { simpleEmojiReact } from '../../actions/emoji_reacts' ;
2020-03-27 13:59:38 -07:00
import {
favourite ,
unfavourite ,
reblog ,
unreblog ,
2020-08-30 22:09:02 -07:00
bookmark ,
unbookmark ,
2020-03-27 13:59:38 -07:00
pin ,
unpin ,
} from '../../actions/interactions' ;
2022-02-02 05:33:12 -08:00
import { openModal } from '../../actions/modals' ;
2022-01-10 14:17:52 -08:00
import { initMuteModal } from '../../actions/mutes' ;
import { initReport } from '../../actions/reports' ;
2020-03-27 13:59:38 -07:00
import {
muteStatus ,
unmuteStatus ,
deleteStatus ,
hideStatus ,
revealStatus ,
2022-04-27 13:50:35 -07:00
editStatus ,
2020-03-27 13:59:38 -07:00
} from '../../actions/statuses' ;
2022-04-23 20:31:49 -07:00
import { fetchStatusWithContext , fetchNext } from '../../actions/statuses' ;
2022-01-10 14:17:52 -08:00
import MissingIndicator from '../../components/missing_indicator' ;
import { textForScreenReader , defaultMediaVisibility } from '../../components/status' ;
2020-03-27 13:59:38 -07:00
import { makeGetStatus } from '../../selectors' ;
import { attachFullscreenListener , detachFullscreenListener , isFullscreen } from '../ui/util/fullscreen' ;
2022-01-10 14:25:06 -08:00
2022-04-04 12:17:24 -07:00
import ActionBar from './components/action-bar' ;
2022-04-04 13:55:28 -07:00
import DetailedStatus from './components/detailed-status' ;
2022-05-11 12:35:56 -07:00
import ThreadLoginCta from './components/thread-login-cta' ;
2022-04-04 13:20:17 -07:00
import ThreadStatus from './components/thread-status' ;
2020-03-27 13:59:38 -07:00
2022-04-04 12:44:31 -07:00
import type { AxiosError } from 'axios' ;
2022-04-04 12:17:24 -07:00
import type { History } from 'history' ;
2022-05-13 18:23:03 -07:00
import type { VirtuosoHandle } from 'react-virtuoso' ;
2022-04-04 12:17:24 -07:00
import type { AnyAction } from 'redux' ;
import type { ThunkDispatch } from 'redux-thunk' ;
import type { RootState } from 'soapbox/store' ;
import type {
Account as AccountEntity ,
Attachment as AttachmentEntity ,
Status as StatusEntity ,
} from 'soapbox/types/entities' ;
2022-05-11 12:35:56 -07:00
import type { Me } from 'soapbox/types/soapbox' ;
2022-04-04 12:17:24 -07:00
2020-03-27 13:59:38 -07:00
const messages = defineMessages ( {
2022-03-21 11:09:01 -07:00
title : { id : 'status.title' , defaultMessage : '@{username}\'s Post' } ,
2021-10-14 06:49:33 -07:00
titleDirect : { id : 'status.title_direct' , defaultMessage : 'Direct message' } ,
2020-03-27 13:59:38 -07:00
deleteConfirm : { id : 'confirmations.delete.confirm' , defaultMessage : 'Delete' } ,
2022-01-06 07:51:34 -08:00
deleteHeading : { id : 'confirmations.delete.heading' , defaultMessage : 'Delete post' } ,
2020-03-27 13:59:38 -07:00
deleteMessage : { id : 'confirmations.delete.message' , defaultMessage : 'Are you sure you want to delete this post?' } ,
redraftConfirm : { id : 'confirmations.redraft.confirm' , defaultMessage : 'Delete & redraft' } ,
2021-12-30 08:38:57 -08:00
redraftHeading : { id : 'confirmations.redraft.heading' , defaultMessage : 'Delete & redraft' } ,
2020-03-27 13:59:38 -07:00
redraftMessage : { id : 'confirmations.redraft.message' , defaultMessage : 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' } ,
blockConfirm : { id : 'confirmations.block.confirm' , defaultMessage : 'Block' } ,
revealAll : { id : 'status.show_more_all' , defaultMessage : 'Show more for all' } ,
hideAll : { id : 'status.show_less_all' , defaultMessage : 'Show less for all' } ,
detailedStatus : { id : 'status.detailed_status' , defaultMessage : 'Detailed conversation view' } ,
replyConfirm : { id : 'confirmations.reply.confirm' , defaultMessage : 'Reply' } ,
replyMessage : { id : 'confirmations.reply.message' , defaultMessage : 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' } ,
blockAndReport : { id : 'confirmations.block.block_and_report' , defaultMessage : 'Block & Report' } ,
} ) ;
const makeMapStateToProps = ( ) = > {
const getStatus = makeGetStatus ( ) ;
2021-08-02 08:46:18 -07:00
const getAncestorsIds = createSelector ( [
2022-05-13 13:02:20 -07:00
( _ : RootState , statusId : string | undefined ) = > statusId ,
( state : RootState ) = > state . contexts . inReplyTos ,
2021-08-02 08:46:18 -07:00
] , ( statusId , inReplyTos ) = > {
2022-05-13 13:02:20 -07:00
let ancestorsIds = ImmutableOrderedSet < string > ( ) ;
let id : string | undefined = statusId ;
2021-08-02 08:46:18 -07:00
2021-11-23 12:55:40 -08:00
while ( id && ! ancestorsIds . includes ( id ) ) {
2021-09-12 09:25:44 -07:00
ancestorsIds = ImmutableOrderedSet ( [ id ] ) . union ( ancestorsIds ) ;
2021-08-03 01:50:08 -07:00
id = inReplyTos . get ( id ) ;
}
2020-03-27 13:59:38 -07:00
2021-08-02 08:46:18 -07:00
return ancestorsIds ;
} ) ;
const getDescendantsIds = createSelector ( [
2022-04-04 12:17:24 -07:00
( _ : RootState , statusId : string ) = > statusId ,
2022-05-13 13:02:20 -07:00
( state : RootState ) = > state . contexts . replies ,
2021-08-02 08:46:18 -07:00
] , ( statusId , contextReplies ) = > {
2021-09-12 09:25:44 -07:00
let descendantsIds = ImmutableOrderedSet ( ) ;
2021-08-03 01:50:08 -07:00
const ids = [ statusId ] ;
2020-03-27 13:59:38 -07:00
2021-08-03 01:50:08 -07:00
while ( ids . length > 0 ) {
2022-05-13 13:02:20 -07:00
const id = ids . shift ( ) ;
if ( ! id ) break ;
2021-08-03 01:50:08 -07:00
const replies = contextReplies . get ( id ) ;
2020-03-27 13:59:38 -07:00
2021-11-23 12:55:40 -08:00
if ( descendantsIds . includes ( id ) ) {
break ;
}
2021-08-03 01:50:08 -07:00
if ( statusId !== id ) {
descendantsIds = descendantsIds . union ( [ id ] ) ;
}
2020-03-27 13:59:38 -07:00
2021-08-03 01:50:08 -07:00
if ( replies ) {
2022-04-04 12:17:24 -07:00
replies . reverse ( ) . forEach ( ( reply : string ) = > {
2021-08-03 01:50:08 -07:00
ids . unshift ( reply ) ;
} ) ;
2021-08-02 08:46:18 -07:00
}
2021-08-03 01:50:08 -07:00
}
2020-03-27 13:59:38 -07:00
2021-08-02 08:46:18 -07:00
return descendantsIds ;
} ) ;
2020-03-27 13:59:38 -07:00
2022-04-04 12:17:24 -07:00
const mapStateToProps = ( state : RootState , props : { params : RouteParams } ) = > {
2021-08-02 08:46:18 -07:00
const status = getStatus ( state , { id : props.params.statusId } ) ;
2021-09-12 09:25:44 -07:00
let ancestorsIds = ImmutableOrderedSet ( ) ;
let descendantsIds = ImmutableOrderedSet ( ) ;
2020-03-27 13:59:38 -07:00
2021-08-02 08:46:18 -07:00
if ( status ) {
2022-04-04 12:17:24 -07:00
const statusId = status . id ;
2022-05-13 13:02:20 -07:00
ancestorsIds = getAncestorsIds ( state , state . contexts . inReplyTos . get ( statusId ) ) ;
2022-04-04 12:17:24 -07:00
descendantsIds = getDescendantsIds ( state , statusId ) ;
2021-11-23 12:55:40 -08:00
ancestorsIds = ancestorsIds . delete ( statusId ) . subtract ( descendantsIds ) ;
descendantsIds = descendantsIds . delete ( statusId ) . subtract ( ancestorsIds ) ;
2020-03-27 13:59:38 -07:00
}
2021-06-30 19:39:27 -07:00
const soapbox = getSoapboxConfig ( state ) ;
2020-03-27 13:59:38 -07:00
return {
status ,
ancestorsIds ,
descendantsIds ,
2022-04-04 12:17:24 -07:00
askReplyConfirmation : state.compose.get ( 'text' , '' ) . trim ( ) . length !== 0 ,
me : state.me ,
2020-10-29 07:41:43 -07:00
displayMedia : getSettings ( state ) . get ( 'displayMedia' ) ,
2022-04-04 12:17:24 -07:00
allowedEmoji : soapbox.allowedEmoji ,
2020-03-27 13:59:38 -07:00
} ;
} ;
return mapStateToProps ;
} ;
2022-04-04 12:17:24 -07:00
type DisplayMedia = 'default' | 'hide_all' | 'show_all' ;
type RouteParams = { statusId : string } ;
interface IStatus extends RouteComponentProps , IntlComponentProps {
params : RouteParams ,
dispatch : ThunkDispatch < RootState , void , AnyAction > ,
status : StatusEntity ,
ancestorsIds : ImmutableOrderedSet < string > ,
descendantsIds : ImmutableOrderedSet < string > ,
askReplyConfirmation : boolean ,
displayMedia : DisplayMedia ,
allowedEmoji : ImmutableList < string > ,
onOpenMedia : ( media : ImmutableList < AttachmentEntity > , index : number ) = > void ,
onOpenVideo : ( video : AttachmentEntity , time : number ) = > void ,
2022-05-11 12:35:56 -07:00
me : Me ,
2022-04-04 12:17:24 -07:00
}
interface IStatusState {
fullscreen : boolean ,
showMedia : boolean ,
loadedStatusId? : string ,
emojiSelectorFocused : boolean ,
2022-04-04 12:44:31 -07:00
isLoaded : boolean ,
error? : AxiosError ,
2022-04-23 20:31:49 -07:00
next? : string ,
2022-04-04 12:17:24 -07:00
}
class Status extends ImmutablePureComponent < IStatus , IStatusState > {
2020-03-27 13:59:38 -07:00
state = {
fullscreen : false ,
2020-10-29 07:41:43 -07:00
showMedia : defaultMediaVisibility ( this . props . status , this . props . displayMedia ) ,
2020-03-27 13:59:38 -07:00
loadedStatusId : undefined ,
2021-07-21 04:58:22 -07:00
emojiSelectorFocused : false ,
2022-04-04 12:44:31 -07:00
isLoaded : Boolean ( this . props . status ) ,
error : undefined ,
2022-04-23 20:31:49 -07:00
next : undefined ,
2020-03-27 13:59:38 -07:00
} ;
2022-04-04 12:17:24 -07:00
node : HTMLDivElement | null = null ;
status : HTMLDivElement | null = null ;
2022-05-13 18:23:03 -07:00
scroller : VirtuosoHandle | null = null ;
2022-04-04 12:17:24 -07:00
_scrolledIntoView : boolean = false ;
2022-04-23 20:31:49 -07:00
fetchData = async ( ) = > {
2021-11-04 10:34:22 -07:00
const { dispatch , params } = this . props ;
2021-11-04 11:16:28 -07:00
const { statusId } = params ;
2022-04-23 20:31:49 -07:00
const { next } = await dispatch ( fetchStatusWithContext ( statusId ) ) ;
this . setState ( { next } ) ;
2021-11-04 10:34:22 -07:00
}
2020-04-14 14:47:35 -07:00
componentDidMount() {
2022-04-04 12:44:31 -07:00
this . fetchData ( ) . then ( ( ) = > {
this . setState ( { isLoaded : true } ) ;
} ) . catch ( error = > {
this . setState ( { error , isLoaded : true } ) ;
} ) ;
2020-03-27 13:59:38 -07:00
attachFullscreenListener ( this . onFullScreenChange ) ;
}
handleToggleMediaVisibility = ( ) = > {
this . setState ( { showMedia : ! this . state . showMedia } ) ;
}
2022-04-04 12:17:24 -07:00
handleEmojiReactClick = ( status : StatusEntity , emoji : string ) = > {
2020-05-22 19:15:07 -07:00
this . props . dispatch ( simpleEmojiReact ( status , emoji ) ) ;
2020-05-20 13:52:46 -07:00
}
2022-04-04 12:17:24 -07:00
handleFavouriteClick = ( status : StatusEntity ) = > {
if ( status . favourited ) {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( unfavourite ( status ) ) ;
} else {
this . props . dispatch ( favourite ( status ) ) ;
}
}
2022-04-04 12:17:24 -07:00
handlePin = ( status : StatusEntity ) = > {
if ( status . pinned ) {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( unpin ( status ) ) ;
} else {
this . props . dispatch ( pin ( status ) ) ;
}
}
2022-04-04 12:17:24 -07:00
handleBookmark = ( status : StatusEntity ) = > {
if ( status . bookmarked ) {
2022-01-27 07:00:05 -08:00
this . props . dispatch ( unbookmark ( status ) ) ;
2020-08-30 22:09:02 -07:00
} else {
2022-01-27 07:00:05 -08:00
this . props . dispatch ( bookmark ( status ) ) ;
2020-08-30 22:09:02 -07:00
}
}
2022-04-04 12:17:24 -07:00
handleReplyClick = ( status : StatusEntity ) = > {
2021-08-03 10:10:42 -07:00
const { askReplyConfirmation , dispatch , intl } = this . props ;
2020-03-27 13:59:38 -07:00
if ( askReplyConfirmation ) {
dispatch ( openModal ( 'CONFIRM' , {
message : intl.formatMessage ( messages . replyMessage ) ,
confirm : intl.formatMessage ( messages . replyConfirm ) ,
2022-03-17 18:17:28 -07:00
onConfirm : ( ) = > dispatch ( replyCompose ( status , this . props . history ) ) ,
2020-03-27 13:59:38 -07:00
} ) ) ;
} else {
2022-03-17 18:17:28 -07:00
dispatch ( replyCompose ( status , this . props . history ) ) ;
2020-03-27 13:59:38 -07:00
}
}
2022-04-04 12:17:24 -07:00
handleModalReblog = ( status : StatusEntity ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( reblog ( status ) ) ;
}
2022-04-04 12:17:24 -07:00
handleReblogClick = ( status : StatusEntity , e? : React.MouseEvent ) = > {
2020-04-21 12:41:13 -07:00
this . props . dispatch ( ( _ , getState ) = > {
2020-04-28 11:49:39 -07:00
const boostModal = getSettings ( getState ( ) ) . get ( 'boostModal' ) ;
2022-04-04 12:17:24 -07:00
if ( status . reblogged ) {
2020-04-21 12:41:13 -07:00
this . props . dispatch ( unreblog ( status ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2020-04-21 12:41:13 -07:00
if ( ( e && e . shiftKey ) || ! boostModal ) {
this . handleModalReblog ( status ) ;
} else {
this . props . dispatch ( openModal ( 'BOOST' , { status , onReblog : this.handleModalReblog } ) ) ;
}
2020-03-27 13:59:38 -07:00
}
2020-04-21 12:41:13 -07:00
} ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
handleQuoteClick = ( status : StatusEntity ) = > {
2022-01-23 09:44:17 -08:00
const { askReplyConfirmation , dispatch , intl } = this . props ;
if ( askReplyConfirmation ) {
dispatch ( openModal ( 'CONFIRM' , {
message : intl.formatMessage ( messages . replyMessage ) ,
confirm : intl.formatMessage ( messages . replyConfirm ) ,
2022-03-17 18:17:28 -07:00
onConfirm : ( ) = > dispatch ( quoteCompose ( status , this . props . history ) ) ,
2022-01-23 09:44:17 -08:00
} ) ) ;
} else {
2022-03-17 18:17:28 -07:00
dispatch ( quoteCompose ( status , this . props . history ) ) ;
2022-01-23 09:44:17 -08:00
}
}
2022-04-04 12:17:24 -07:00
handleDeleteClick = ( status : StatusEntity , history : History , withRedraft = false ) = > {
2020-03-27 13:59:38 -07:00
const { dispatch , intl } = this . props ;
2020-04-21 12:41:13 -07:00
this . props . dispatch ( ( _ , getState ) = > {
2020-04-28 11:49:39 -07:00
const deleteModal = getSettings ( getState ( ) ) . get ( 'deleteModal' ) ;
2020-04-21 12:41:13 -07:00
if ( ! deleteModal ) {
2022-04-04 12:17:24 -07:00
dispatch ( deleteStatus ( status . id , history , withRedraft ) ) ;
2020-04-21 12:41:13 -07:00
} else {
dispatch ( openModal ( 'CONFIRM' , {
2021-12-30 08:38:57 -08:00
icon : withRedraft ? require ( '@tabler/icons/icons/edit.svg' ) : require ( '@tabler/icons/icons/trash.svg' ) ,
heading : intl.formatMessage ( withRedraft ? messages.redraftHeading : messages.deleteHeading ) ,
2020-04-21 12:41:13 -07:00
message : intl.formatMessage ( withRedraft ? messages.redraftMessage : messages.deleteMessage ) ,
confirm : intl.formatMessage ( withRedraft ? messages.redraftConfirm : messages.deleteConfirm ) ,
2022-04-04 12:17:24 -07:00
onConfirm : ( ) = > dispatch ( deleteStatus ( status . id , history , withRedraft ) ) ,
2020-04-21 12:41:13 -07:00
} ) ) ;
}
} ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-27 13:50:35 -07:00
handleEditClick = ( status : StatusEntity ) = > {
const { dispatch } = this . props ;
dispatch ( editStatus ( status . get ( 'id' ) ) ) ;
}
2022-04-04 12:17:24 -07:00
handleDirectClick = ( account : AccountEntity , router : History ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( directCompose ( account , router ) ) ;
}
2022-04-04 12:17:24 -07:00
handleChatClick = ( account : AccountEntity , router : History ) = > {
this . props . dispatch ( launchChat ( account . id , router ) ) ;
2021-10-13 11:55:02 -07:00
}
2022-04-04 12:17:24 -07:00
handleMentionClick = ( account : AccountEntity , router : History ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( mentionCompose ( account , router ) ) ;
}
2022-04-04 12:17:24 -07:00
handleOpenMedia = ( media : ImmutableList < AttachmentEntity > , index : number ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( openModal ( 'MEDIA' , { media , index } ) ) ;
}
2022-04-04 12:17:24 -07:00
handleOpenVideo = ( media : ImmutableList < AttachmentEntity > , time : number ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( openModal ( 'VIDEO' , { media , time } ) ) ;
}
2022-04-04 12:17:24 -07:00
handleHotkeyOpenMedia = ( e? : KeyboardEvent ) = > {
const { status , onOpenMedia , onOpenVideo } = this . props ;
const firstAttachment = status . media_attachments . get ( 0 ) ;
2021-08-28 05:17:14 -07:00
2022-04-04 12:17:24 -07:00
e ? . preventDefault ( ) ;
2021-08-28 05:17:14 -07:00
2022-04-04 12:17:24 -07:00
if ( status . media_attachments . size > 0 && firstAttachment ) {
if ( firstAttachment . type === 'video' ) {
onOpenVideo ( firstAttachment , 0 ) ;
2021-08-28 05:17:14 -07:00
} else {
2022-04-04 12:17:24 -07:00
onOpenMedia ( status . media_attachments , 0 ) ;
2021-08-28 05:17:14 -07:00
}
}
}
2022-04-04 12:17:24 -07:00
handleMuteClick = ( account : AccountEntity ) = > {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( initMuteModal ( account ) ) ;
}
2022-04-04 12:17:24 -07:00
handleConversationMuteClick = ( status : StatusEntity ) = > {
if ( status . muted ) {
this . props . dispatch ( unmuteStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-04-04 12:17:24 -07:00
this . props . dispatch ( muteStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
}
}
2022-04-04 12:17:24 -07:00
handleToggleHidden = ( status : StatusEntity ) = > {
if ( status . hidden ) {
this . props . dispatch ( revealStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-04-04 12:17:24 -07:00
this . props . dispatch ( hideStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
}
}
handleToggleAll = ( ) = > {
const { status , ancestorsIds , descendantsIds } = this . props ;
2022-04-04 12:17:24 -07:00
const statusIds = [ status . id ] . concat ( ancestorsIds . toArray ( ) , descendantsIds . toArray ( ) ) ;
2020-03-27 13:59:38 -07:00
2022-04-04 12:17:24 -07:00
if ( status . hidden ) {
2020-03-27 13:59:38 -07:00
this . props . dispatch ( revealStatus ( statusIds ) ) ;
} else {
this . props . dispatch ( hideStatus ( statusIds ) ) ;
}
}
2022-04-04 12:17:24 -07:00
handleBlockClick = ( status : StatusEntity ) = > {
2020-03-27 13:59:38 -07:00
const { dispatch , intl } = this . props ;
2022-04-04 12:17:24 -07:00
const { account } = status ;
if ( ! account || typeof account !== 'object' ) return ;
2020-03-27 13:59:38 -07:00
dispatch ( openModal ( 'CONFIRM' , {
2021-12-30 08:38:57 -08:00
icon : require ( '@tabler/icons/icons/ban.svg' ) ,
2022-04-04 12:17:24 -07:00
heading : < FormattedMessage id = 'confirmations.block.heading' defaultMessage = 'Block @{name}' values = { { name : account.acct } } / > ,
message : < FormattedMessage id = 'confirmations.block.message' defaultMessage = 'Are you sure you want to block {name}?' values = { { name : < strong > @ { account . acct } < / strong > } } / > ,
2020-03-27 13:59:38 -07:00
confirm : intl.formatMessage ( messages . blockConfirm ) ,
2022-04-04 12:17:24 -07:00
onConfirm : ( ) = > dispatch ( blockAccount ( account . id ) ) ,
2020-03-27 13:59:38 -07:00
secondary : intl.formatMessage ( messages . blockAndReport ) ,
onSecondary : ( ) = > {
2022-04-04 12:17:24 -07:00
dispatch ( blockAccount ( account . id ) ) ;
2020-03-27 13:59:38 -07:00
dispatch ( initReport ( account , status ) ) ;
} ,
} ) ) ;
}
2022-04-04 12:17:24 -07:00
handleReport = ( status : StatusEntity ) = > {
this . props . dispatch ( initReport ( status . account , status ) ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
handleEmbed = ( status : StatusEntity ) = > {
this . props . dispatch ( openModal ( 'EMBED' , { url : status.url } ) ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
handleDeactivateUser = ( status : StatusEntity ) = > {
2021-01-18 13:27:35 -08:00
const { dispatch , intl } = this . props ;
dispatch ( deactivateUserModal ( intl , status . getIn ( [ 'account' , 'id' ] ) ) ) ;
}
2022-04-04 12:17:24 -07:00
handleDeleteUser = ( status : StatusEntity ) = > {
2021-01-18 13:27:35 -08:00
const { dispatch , intl } = this . props ;
dispatch ( deleteUserModal ( intl , status . getIn ( [ 'account' , 'id' ] ) ) ) ;
}
2022-04-04 12:17:24 -07:00
handleToggleStatusSensitivity = ( status : StatusEntity ) = > {
2021-01-18 18:59:07 -08:00
const { dispatch , intl } = this . props ;
2022-04-04 12:17:24 -07:00
dispatch ( toggleStatusSensitivityModal ( intl , status . id , status . sensitive ) ) ;
2021-01-18 18:59:07 -08:00
}
2022-04-04 12:17:24 -07:00
handleDeleteStatus = ( status : StatusEntity ) = > {
2021-01-18 13:57:20 -08:00
const { dispatch , intl } = this . props ;
2022-04-04 12:17:24 -07:00
dispatch ( deleteStatusModal ( intl , status . id ) ) ;
2021-01-18 13:57:20 -08:00
}
2020-03-27 13:59:38 -07:00
handleHotkeyMoveUp = ( ) = > {
2022-04-04 12:17:24 -07:00
this . handleMoveUp ( this . props . status . id ) ;
2020-03-27 13:59:38 -07:00
}
handleHotkeyMoveDown = ( ) = > {
2022-04-04 12:17:24 -07:00
this . handleMoveDown ( this . props . status . id ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
handleHotkeyReply = ( e? : KeyboardEvent ) = > {
e ? . preventDefault ( ) ;
2020-03-27 13:59:38 -07:00
this . handleReplyClick ( this . props . status ) ;
}
handleHotkeyFavourite = ( ) = > {
this . handleFavouriteClick ( this . props . status ) ;
}
handleHotkeyBoost = ( ) = > {
this . handleReblogClick ( this . props . status ) ;
}
2022-04-04 12:17:24 -07:00
handleHotkeyMention = ( e? : KeyboardEvent ) = > {
e ? . preventDefault ( ) ;
const { account } = this . props . status ;
if ( ! account || typeof account !== 'object' ) return ;
this . handleMentionClick ( account , this . props . history ) ;
2020-03-27 13:59:38 -07:00
}
handleHotkeyOpenProfile = ( ) = > {
2022-03-17 18:17:28 -07:00
this . props . history . push ( ` /@ ${ this . props . status . getIn ( [ 'account' , 'acct' ] ) } ` ) ;
2020-03-27 13:59:38 -07:00
}
handleHotkeyToggleHidden = ( ) = > {
this . handleToggleHidden ( this . props . status ) ;
}
handleHotkeyToggleSensitive = ( ) = > {
this . handleToggleMediaVisibility ( ) ;
}
2021-07-21 04:58:22 -07:00
handleHotkeyReact = ( ) = > {
this . _expandEmojiSelector ( ) ;
}
2022-04-04 12:17:24 -07:00
handleMoveUp = ( id : string ) = > {
2020-03-27 13:59:38 -07:00
const { status , ancestorsIds , descendantsIds } = this . props ;
2022-04-04 12:17:24 -07:00
if ( id === status . id ) {
2020-03-27 13:59:38 -07:00
this . _selectChild ( ancestorsIds . size - 1 , true ) ;
} else {
2021-10-26 08:38:49 -07:00
let index = ImmutableList ( ancestorsIds ) . indexOf ( id ) ;
2020-03-27 13:59:38 -07:00
if ( index === - 1 ) {
2021-10-26 08:38:49 -07:00
index = ImmutableList ( descendantsIds ) . indexOf ( id ) ;
2020-03-27 13:59:38 -07:00
this . _selectChild ( ancestorsIds . size + index , true ) ;
} else {
this . _selectChild ( index - 1 , true ) ;
}
}
}
2022-04-04 12:17:24 -07:00
handleMoveDown = ( id : string ) = > {
2020-03-27 13:59:38 -07:00
const { status , ancestorsIds , descendantsIds } = this . props ;
2022-04-04 12:17:24 -07:00
if ( id === status . id ) {
2020-03-27 13:59:38 -07:00
this . _selectChild ( ancestorsIds . size + 1 , false ) ;
} else {
2021-10-26 08:38:49 -07:00
let index = ImmutableList ( ancestorsIds ) . indexOf ( id ) ;
2020-03-27 13:59:38 -07:00
if ( index === - 1 ) {
2021-10-26 08:38:49 -07:00
index = ImmutableList ( descendantsIds ) . indexOf ( id ) ;
2020-03-27 13:59:38 -07:00
this . _selectChild ( ancestorsIds . size + index + 2 , false ) ;
} else {
this . _selectChild ( index + 1 , false ) ;
}
}
}
2022-04-04 12:17:24 -07:00
handleEmojiSelectorExpand : React.EventHandler < React.KeyboardEvent > = e = > {
2021-07-21 04:58:22 -07:00
if ( e . key === 'Enter' ) {
this . _expandEmojiSelector ( ) ;
}
e . preventDefault ( ) ;
}
2022-04-04 12:17:24 -07:00
handleEmojiSelectorUnfocus : React.EventHandler < React.KeyboardEvent > = ( ) = > {
2021-07-21 04:58:22 -07:00
this . setState ( { emojiSelectorFocused : false } ) ;
}
_expandEmojiSelector = ( ) = > {
2022-04-04 12:17:24 -07:00
if ( ! this . status ) return ;
2021-07-21 04:58:22 -07:00
this . setState ( { emojiSelectorFocused : true } ) ;
2022-04-04 12:17:24 -07:00
const firstEmoji : HTMLButtonElement | null = this . status . querySelector ( '.emoji-react-selector .emoji-react-selector__emoji' ) ;
firstEmoji ? . focus ( ) ;
2021-07-21 04:58:22 -07:00
} ;
2022-04-04 12:17:24 -07:00
_selectChild ( index : number , align_top : boolean ) {
2020-03-27 13:59:38 -07:00
const container = this . node ;
2022-04-04 12:17:24 -07:00
if ( ! container ) return ;
const element = container . querySelectorAll ( '.focusable' ) [ index ] as HTMLButtonElement ;
2020-03-27 13:59:38 -07:00
if ( element ) {
if ( align_top && container . scrollTop > element . offsetTop ) {
element . scrollIntoView ( true ) ;
} else if ( ! align_top && container . scrollTop + container . clientHeight < element . offsetTop + element . offsetHeight ) {
element . scrollIntoView ( false ) ;
}
element . focus ( ) ;
}
}
2022-04-04 12:17:24 -07:00
renderTombstone ( id : string ) {
2021-04-21 12:47:39 -07:00
return (
2022-05-19 10:42:01 -07:00
< div className = 'py-4 pb-8' >
2022-05-16 19:30:30 -07:00
< Tombstone key = { id } / >
2021-04-21 12:47:39 -07:00
< / div >
) ;
}
2022-04-04 12:17:24 -07:00
renderStatus ( id : string ) {
2021-10-06 15:50:43 -07:00
const { status } = this . props ;
2021-04-21 12:47:39 -07:00
return (
2021-10-06 15:50:43 -07:00
< ThreadStatus
2020-03-27 13:59:38 -07:00
key = { id }
id = { id }
2022-04-04 12:17:24 -07:00
focusedStatusId = { status . id }
2022-04-04 13:20:17 -07:00
// @ts-ignore FIXME
2020-03-27 13:59:38 -07:00
onMoveUp = { this . handleMoveUp }
onMoveDown = { this . handleMoveDown }
/ >
2021-04-21 12:47:39 -07:00
) ;
}
2022-04-04 12:17:24 -07:00
renderPendingStatus ( id : string ) {
const { status } = this . props ;
2021-10-09 19:16:37 -07:00
const idempotencyKey = id . replace ( /^末pending-/ , '' ) ;
2021-10-09 15:47:25 -07:00
return (
< PendingStatus
2021-10-09 19:39:15 -07:00
className = 'thread__status'
2021-10-09 15:47:25 -07:00
key = { id }
idempotencyKey = { idempotencyKey }
2022-04-04 12:17:24 -07:00
focusedStatusId = { status . id }
2021-10-09 15:47:25 -07:00
onMoveUp = { this . handleMoveUp }
onMoveDown = { this . handleMoveDown }
contextType = 'thread'
/ >
) ;
}
2022-04-04 12:17:24 -07:00
renderChildren ( list : ImmutableOrderedSet < string > ) {
2021-04-21 12:47:39 -07:00
return list . map ( id = > {
2021-04-21 16:28:43 -07:00
if ( id . endsWith ( '-tombstone' ) ) {
2021-04-21 12:47:39 -07:00
return this . renderTombstone ( id ) ;
2021-10-09 19:16:37 -07:00
} else if ( id . startsWith ( '末pending-' ) ) {
2021-10-09 15:47:25 -07:00
return this . renderPendingStatus ( id ) ;
2021-04-21 12:47:39 -07:00
} else {
return this . renderStatus ( id ) ;
}
} ) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
setRef : React.RefCallback < HTMLDivElement > = c = > {
2020-03-27 13:59:38 -07:00
this . node = c ;
}
2022-04-04 12:17:24 -07:00
setStatusRef : React.RefCallback < HTMLDivElement > = c = > {
2021-07-21 04:58:22 -07:00
this . status = c ;
}
2022-04-04 12:17:24 -07:00
componentDidUpdate ( prevProps : IStatus , prevState : IStatusState ) {
2022-05-13 18:23:03 -07:00
const { params , status , displayMedia , ancestorsIds } = this . props ;
const { isLoaded } = this . state ;
2020-07-04 16:41:41 -07:00
2021-11-04 11:16:28 -07:00
if ( params . statusId !== prevProps . params . statusId ) {
this . fetchData ( ) ;
2020-06-24 14:02:14 -07:00
}
2022-04-04 12:17:24 -07:00
if ( status && status . id !== prevState . loadedStatusId ) {
this . setState ( { showMedia : defaultMediaVisibility ( status , displayMedia ) , loadedStatusId : status.id } ) ;
2020-06-24 14:02:14 -07:00
}
2022-05-13 18:23:03 -07:00
if ( params . statusId !== prevProps . params . statusId || status ? . id !== prevProps . status ? . id || ancestorsIds . size > prevProps . ancestorsIds . size || isLoaded !== prevState . isLoaded ) {
this . scroller ? . scrollToIndex ( {
index : this.props.ancestorsIds.size ,
offset : - 80 ,
2020-03-27 13:59:38 -07:00
} ) ;
}
}
2020-04-14 14:47:35 -07:00
componentWillUnmount() {
2020-03-27 13:59:38 -07:00
detachFullscreenListener ( this . onFullScreenChange ) ;
}
onFullScreenChange = ( ) = > {
this . setState ( { fullscreen : isFullscreen ( ) } ) ;
}
2021-11-04 10:34:22 -07:00
handleRefresh = ( ) = > {
2021-11-04 11:16:28 -07:00
return this . fetchData ( ) ;
2021-11-04 10:34:22 -07:00
}
2022-04-23 20:31:49 -07:00
handleLoadMore = ( ) = > {
2022-05-13 11:07:11 -07:00
const { status } = this . props ;
2022-04-23 20:31:49 -07:00
const { next } = this . state ;
if ( next ) {
2022-05-13 11:07:11 -07:00
this . props . dispatch ( fetchNext ( status . id , next ) ) . then ( ( { next } ) = > {
2022-04-23 20:31:49 -07:00
this . setState ( { next } ) ;
} ) . catch ( ( ) = > { } ) ;
}
}
2022-04-27 13:50:35 -07:00
handleOpenCompareHistoryModal = ( status : StatusEntity ) = > {
const { dispatch } = this . props ;
dispatch ( openModal ( 'COMPARE_HISTORY' , {
statusId : status.id ,
} ) ) ;
}
2022-05-13 18:23:03 -07:00
setScrollerRef = ( c : VirtuosoHandle ) = > {
this . scroller = c ;
}
2020-04-14 14:47:35 -07:00
render() {
2022-05-11 12:35:56 -07:00
const { me , status , ancestorsIds , descendantsIds , intl } = this . props ;
2020-03-27 13:59:38 -07:00
2022-04-23 10:20:25 -07:00
const hasAncestors = ancestorsIds && ancestorsIds . size > 0 ;
const hasDescendants = descendantsIds && descendantsIds . size > 0 ;
2022-04-04 12:44:31 -07:00
if ( ! status && this . state . isLoaded ) {
// TODO: handle errors other than 404 with `this.state.error?.response?.status`
2020-03-27 13:59:38 -07:00
return (
2022-03-21 11:09:01 -07:00
< MissingIndicator / >
2020-03-27 13:59:38 -07:00
) ;
2022-04-04 12:44:31 -07:00
} else if ( ! status ) {
return (
< PlaceholderStatus / >
) ;
2020-03-27 13:59:38 -07:00
}
2022-04-04 12:17:24 -07:00
type HotkeyHandlers = { [ key : string ] : ( keyEvent? : KeyboardEvent ) = > void } ;
const handlers : HotkeyHandlers = {
2020-03-27 13:59:38 -07:00
moveUp : this.handleHotkeyMoveUp ,
moveDown : this.handleHotkeyMoveDown ,
reply : this.handleHotkeyReply ,
favourite : this.handleHotkeyFavourite ,
boost : this.handleHotkeyBoost ,
mention : this.handleHotkeyMention ,
openProfile : this.handleHotkeyOpenProfile ,
toggleHidden : this.handleHotkeyToggleHidden ,
toggleSensitive : this.handleHotkeyToggleSensitive ,
2021-08-28 05:17:14 -07:00
openMedia : this.handleHotkeyOpenMedia ,
2021-07-21 04:58:22 -07:00
react : this.handleHotkeyReact ,
2020-03-27 13:59:38 -07:00
} ;
2022-04-04 12:17:24 -07:00
const username = String ( status . getIn ( [ 'account' , 'acct' ] ) ) ;
const titleMessage = status . visibility === 'direct' ? messages.titleDirect : messages.title ;
2021-10-14 06:49:33 -07:00
2022-04-23 10:20:25 -07:00
const focusedStatus = (
< div className = { classNames ( 'thread__detailed-status' , { 'pb-4' : hasDescendants } ) } key = { status . id } >
< HotKeys handlers = { handlers } >
< div
ref = { this . setStatusRef }
2022-05-17 06:09:53 -07:00
className = 'detailed-status__wrapper focusable'
2022-04-23 10:20:25 -07:00
tabIndex = { 0 }
// FIXME: no "reblogged by" text is added for the screen reader
aria - label = { textForScreenReader ( intl , status ) }
>
{ /* @ts-ignore */ }
< DetailedStatus
status = { status }
onOpenVideo = { this . handleOpenVideo }
onOpenMedia = { this . handleOpenMedia }
onToggleHidden = { this . handleToggleHidden }
showMedia = { this . state . showMedia }
onToggleMediaVisibility = { this . handleToggleMediaVisibility }
2022-04-27 13:50:35 -07:00
onOpenCompareHistoryModal = { this . handleOpenCompareHistoryModal }
2022-04-23 10:20:25 -07:00
/ >
< hr className = 'mb-2 dark:border-slate-600' / >
< ActionBar
status = { status }
onReply = { this . handleReplyClick }
onFavourite = { this . handleFavouriteClick }
onEmojiReact = { this . handleEmojiReactClick }
onReblog = { this . handleReblogClick }
onQuote = { this . handleQuoteClick }
onDelete = { this . handleDeleteClick }
2022-04-27 13:50:35 -07:00
onEdit = { this . handleEditClick }
2022-04-23 10:20:25 -07:00
onDirect = { this . handleDirectClick }
onChat = { this . handleChatClick }
onMention = { this . handleMentionClick }
onMute = { this . handleMuteClick }
onMuteConversation = { this . handleConversationMuteClick }
onBlock = { this . handleBlockClick }
onReport = { this . handleReport }
onPin = { this . handlePin }
onBookmark = { this . handleBookmark }
onEmbed = { this . handleEmbed }
onDeactivateUser = { this . handleDeactivateUser }
onDeleteUser = { this . handleDeleteUser }
onToggleStatusSensitivity = { this . handleToggleStatusSensitivity }
onDeleteStatus = { this . handleDeleteStatus }
allowedEmoji = { this . props . allowedEmoji }
emojiSelectorFocused = { this . state . emojiSelectorFocused }
handleEmojiSelectorExpand = { this . handleEmojiSelectorExpand }
handleEmojiSelectorUnfocus = { this . handleEmojiSelectorUnfocus }
/ >
< / div >
< / HotKeys >
{ hasDescendants && (
< hr className = 'mt-2 dark:border-slate-600' / >
) }
< / div >
) ;
const children : JSX.Element [ ] = [ ] ;
if ( hasAncestors ) {
children . push ( . . . this . renderChildren ( ancestorsIds ) . toArray ( ) ) ;
}
children . push ( focusedStatus ) ;
if ( hasDescendants ) {
children . push ( . . . this . renderChildren ( descendantsIds ) . toArray ( ) ) ;
}
2020-03-27 13:59:38 -07:00
return (
2022-03-21 11:09:01 -07:00
< Column label = { intl . formatMessage ( titleMessage , { username } ) } transparent >
< div className = 'px-4 pt-4 sm:p-0' >
< SubNavigation message = { intl . formatMessage ( titleMessage , { username } ) } / >
< / div >
2020-03-27 13:59:38 -07:00
2022-05-11 12:35:56 -07:00
< Stack space = { 2 } >
< div ref = { this . setRef } className = 'thread' >
< ScrollableList
2022-05-13 18:23:03 -07:00
ref = { this . setScrollerRef }
2022-05-11 12:35:56 -07:00
onRefresh = { this . handleRefresh }
hasMore = { ! ! this . state . next }
onLoadMore = { this . handleLoadMore }
placeholderComponent = { ( ) = > < PlaceholderStatus thread / > }
2022-05-13 18:23:03 -07:00
initialTopMostItemIndex = { ancestorsIds . size }
2022-05-11 12:35:56 -07:00
>
{ children }
< / ScrollableList >
< / div >
{ ! me && < ThreadLoginCta / > }
< / Stack >
2020-03-27 13:59:38 -07:00
< / Column >
) ;
}
}
2022-04-04 12:17:24 -07:00
const WrappedComponent = withRouter ( injectIntl ( Status ) ) ;
// @ts-ignore
export default connect ( makeMapStateToProps ) ( WrappedComponent ) ;