2022-08-31 02:35:06 -07:00
import classNames from 'clsx' ;
2021-10-26 08:38:49 -07:00
import { List as ImmutableList , OrderedSet as ImmutableOrderedSet } from 'immutable' ;
2022-08-08 17:31:19 -07:00
import { debounce } from 'lodash' ;
import React , { useCallback , useEffect , useRef , useState } from 'react' ;
2022-01-10 14:17:52 -08:00
import { HotKeys } from 'react-hotkeys' ;
2022-08-09 12:34:08 -07:00
import { defineMessages , useIntl } from 'react-intl' ;
2022-08-08 17:31:19 -07:00
import { useHistory } 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 {
replyCompose ,
mentionCompose ,
2022-05-30 11:23:55 -07:00
} from 'soapbox/actions/compose' ;
2020-03-27 13:59:38 -07:00
import {
favourite ,
unfavourite ,
reblog ,
unreblog ,
2022-05-30 11:23:55 -07:00
} from 'soapbox/actions/interactions' ;
import { openModal } from 'soapbox/actions/modals' ;
import { getSettings } from 'soapbox/actions/settings' ;
2020-03-27 13:59:38 -07:00
import {
hideStatus ,
revealStatus ,
2022-06-07 13:21:18 -07:00
fetchStatusWithContext ,
fetchNext ,
2022-05-30 11:23:55 -07:00
} from 'soapbox/actions/statuses' ;
import MissingIndicator from 'soapbox/components/missing_indicator' ;
2022-07-13 18:13:37 -07:00
import PullToRefresh from 'soapbox/components/pull-to-refresh' ;
2022-05-30 11:23:55 -07:00
import ScrollableList from 'soapbox/components/scrollable_list' ;
2022-08-09 16:22:53 -07:00
import StatusActionBar from 'soapbox/components/status-action-bar' ;
2022-05-30 11:23:55 -07:00
import SubNavigation from 'soapbox/components/sub_navigation' ;
import Tombstone from 'soapbox/components/tombstone' ;
import { Column , Stack } from 'soapbox/components/ui' ;
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status' ;
import PendingStatus from 'soapbox/features/ui/components/pending_status' ;
2022-08-09 12:34:08 -07:00
import { useAppDispatch , useAppSelector , useSettings } from 'soapbox/hooks' ;
2022-05-30 11:23:55 -07:00
import { makeGetStatus } from 'soapbox/selectors' ;
2022-08-08 19:42:07 -07:00
import { defaultMediaVisibility , textForScreenReader } from 'soapbox/utils/status' ;
2022-05-30 11:23:55 -07:00
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-05-13 18:23:03 -07:00
import type { VirtuosoHandle } from 'react-virtuoso' ;
2022-04-04 12:17:24 -07:00
import type { RootState } from 'soapbox/store' ;
import type {
Account as AccountEntity ,
Attachment as AttachmentEntity ,
Status as StatusEntity ,
} from 'soapbox/types/entities' ;
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' } ,
} ) ;
2022-08-08 17:31:19 -07:00
const getAncestorsIds = createSelector ( [
( _ : RootState , statusId : string | undefined ) = > statusId ,
( state : RootState ) = > state . contexts . inReplyTos ,
] , ( statusId , inReplyTos ) = > {
let ancestorsIds = ImmutableOrderedSet < string > ( ) ;
let id : string | undefined = statusId ;
2021-08-02 08:46:18 -07:00
2022-08-08 17:31:19 -07:00
while ( id && ! ancestorsIds . includes ( id ) ) {
ancestorsIds = ImmutableOrderedSet ( [ id ] ) . union ( ancestorsIds ) ;
id = inReplyTos . get ( id ) ;
}
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
return ancestorsIds ;
} ) ;
2021-08-02 08:46:18 -07:00
2022-08-08 17:31:19 -07:00
const getDescendantsIds = createSelector ( [
( _ : RootState , statusId : string ) = > statusId ,
( state : RootState ) = > state . contexts . replies ,
] , ( statusId , contextReplies ) = > {
let descendantsIds = ImmutableOrderedSet < string > ( ) ;
const ids = [ statusId ] ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
while ( ids . length > 0 ) {
const id = ids . shift ( ) ;
if ( ! id ) break ;
2022-05-13 13:02:20 -07:00
2022-08-08 17:31:19 -07:00
const replies = contextReplies . get ( id ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
if ( descendantsIds . includes ( id ) ) {
break ;
}
2021-11-23 12:55:40 -08:00
2022-08-08 17:31:19 -07:00
if ( statusId !== id ) {
descendantsIds = descendantsIds . union ( [ id ] ) ;
}
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
if ( replies ) {
replies . reverse ( ) . forEach ( ( reply : string ) = > {
ids . unshift ( reply ) ;
} ) ;
2021-08-03 01:50:08 -07:00
}
2022-08-08 17:31:19 -07:00
}
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
return descendantsIds ;
} ) ;
type DisplayMedia = 'default' | 'hide_all' | 'show_all' ;
type RouteParams = { statusId : string } ;
2022-08-08 17:54:27 -07:00
interface IThread {
2022-08-08 17:31:19 -07:00
params : RouteParams ,
onOpenMedia : ( media : ImmutableList < AttachmentEntity > , index : number ) = > void ,
onOpenVideo : ( video : AttachmentEntity , time : number ) = > void ,
}
2022-08-08 17:54:27 -07:00
const Thread : React.FC < IThread > = ( props ) = > {
2022-08-08 17:31:19 -07:00
const intl = useIntl ( ) ;
const history = useHistory ( ) ;
const dispatch = useAppDispatch ( ) ;
const settings = useSettings ( ) ;
2022-09-13 01:21:56 -07:00
const getStatus = useCallback ( makeGetStatus ( ) , [ ] ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const me = useAppSelector ( state = > state . me ) ;
const status = useAppSelector ( state = > getStatus ( state , { id : props.params.statusId } ) ) ;
const displayMedia = settings . get ( 'displayMedia' ) as DisplayMedia ;
2022-11-09 11:03:38 -08:00
const isUnderReview = status ? . visibility === 'self' ;
2022-08-08 17:31:19 -07:00
const { ancestorsIds , descendantsIds } = useAppSelector ( state = > {
let ancestorsIds = ImmutableOrderedSet < string > ( ) ;
let descendantsIds = ImmutableOrderedSet < string > ( ) ;
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
}
return {
status ,
ancestorsIds ,
descendantsIds ,
} ;
2022-08-08 17:31:19 -07:00
} ) ;
2020-03-27 13:59:38 -07:00
2022-10-20 07:48:41 -07:00
const [ showMedia , setShowMedia ] = useState < boolean > ( status ? . visibility === 'self' ? false : defaultMediaVisibility ( status , displayMedia ) ) ;
2022-08-08 17:31:19 -07:00
const [ isLoaded , setIsLoaded ] = useState < boolean > ( ! ! status ) ;
const [ next , setNext ] = useState < string > ( ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const node = useRef < HTMLDivElement > ( null ) ;
const statusRef = useRef < HTMLDivElement > ( null ) ;
const scroller = useRef < VirtuosoHandle > ( null ) ;
2022-04-04 12:17:24 -07:00
2022-08-08 17:31:19 -07:00
/** Fetch the status (and context) from the API. */
2022-10-20 07:48:41 -07:00
const fetchData = async ( ) = > {
2022-08-08 17:31:19 -07:00
const { params } = 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 ) ) ;
2022-08-08 17:31:19 -07:00
setNext ( next ) ;
} ;
2021-11-04 10:34:22 -07:00
2022-08-08 17:31:19 -07:00
// Load data.
useEffect ( ( ) = > {
fetchData ( ) . then ( ( ) = > {
setIsLoaded ( true ) ;
2022-04-04 12:44:31 -07:00
} ) . catch ( error = > {
2022-08-08 17:31:19 -07:00
setIsLoaded ( true ) ;
2022-04-04 12:44:31 -07:00
} ) ;
2022-08-08 17:31:19 -07:00
} , [ props . params . statusId ] ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleToggleMediaVisibility = ( ) = > {
setShowMedia ( ! showMedia ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-09 12:34:08 -07:00
const handleHotkeyReact = ( ) = > {
if ( statusRef . current ) {
const firstEmoji : HTMLButtonElement | null = statusRef . current . querySelector ( '.emoji-react-selector .emoji-react-selector__emoji' ) ;
firstEmoji ? . focus ( ) ;
}
2022-08-08 17:31:19 -07:00
} ;
2020-05-20 13:52:46 -07:00
2022-08-08 17:31:19 -07:00
const handleFavouriteClick = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
if ( status . favourited ) {
2022-08-08 17:31:19 -07:00
dispatch ( unfavourite ( status ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( favourite ( status ) ) ;
2020-03-27 13:59:38 -07:00
}
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleReplyClick = ( status : StatusEntity ) = > {
2022-09-10 14:52:06 -07:00
dispatch ( replyCompose ( status ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleModalReblog = ( status : StatusEntity ) = > {
dispatch ( reblog ( status ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleReblogClick = ( status : StatusEntity , e? : React.MouseEvent ) = > {
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 ) {
2022-08-08 17:31:19 -07:00
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 ) {
2022-08-08 17:31:19 -07:00
handleModalReblog ( status ) ;
2020-04-21 12:41:13 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( openModal ( 'BOOST' , { status , onReblog : handleModalReblog } ) ) ;
2020-04-21 12:41:13 -07:00
}
2020-03-27 13:59:38 -07:00
}
2020-04-21 12:41:13 -07:00
} ) ;
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleMentionClick = ( account : AccountEntity ) = > {
dispatch ( mentionCompose ( account ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleOpenMedia = ( media : ImmutableList < AttachmentEntity > , index : number ) = > {
2022-10-18 17:10:16 -07:00
dispatch ( openModal ( 'MEDIA' , { media , status , index } ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleOpenVideo = ( media : ImmutableList < AttachmentEntity > , time : number ) = > {
dispatch ( openModal ( 'VIDEO' , { media , time } ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyOpenMedia = ( e? : KeyboardEvent ) = > {
const { onOpenMedia , onOpenVideo } = 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-08-08 17:31:19 -07:00
if ( status && firstAttachment ) {
2022-04-04 12:17:24 -07:00
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-08-08 17:31:19 -07:00
} ;
2021-08-28 05:17:14 -07:00
2022-08-08 17:31:19 -07:00
const handleToggleHidden = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
if ( status . hidden ) {
2022-08-08 17:31:19 -07:00
dispatch ( revealStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( hideStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
}
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyMoveUp = ( ) = > {
handleMoveUp ( status ! . id ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyMoveDown = ( ) = > {
handleMoveDown ( status ! . id ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyReply = ( e? : KeyboardEvent ) = > {
2022-04-04 12:17:24 -07:00
e ? . preventDefault ( ) ;
2022-08-08 17:31:19 -07:00
handleReplyClick ( status ! ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyFavourite = ( ) = > {
handleFavouriteClick ( status ! ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyBoost = ( ) = > {
handleReblogClick ( status ! ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyMention = ( e? : KeyboardEvent ) = > {
2022-04-04 12:17:24 -07:00
e ? . preventDefault ( ) ;
2022-08-08 17:31:19 -07:00
const { account } = status ! ;
2022-04-04 12:17:24 -07:00
if ( ! account || typeof account !== 'object' ) return ;
2022-08-08 17:31:19 -07:00
handleMentionClick ( account ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyOpenProfile = ( ) = > {
history . push ( ` /@ ${ status ! . getIn ( [ 'account' , 'acct' ] ) } ` ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyToggleHidden = ( ) = > {
handleToggleHidden ( status ! ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleHotkeyToggleSensitive = ( ) = > {
handleToggleMediaVisibility ( ) ;
} ;
2021-07-21 04:58:22 -07:00
2022-08-08 17:31:19 -07:00
const handleMoveUp = ( id : string ) = > {
if ( id === status ? . id ) {
_selectChild ( ancestorsIds . size - 1 ) ;
2020-03-27 13:59:38 -07:00
} 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 ) ;
2022-08-08 17:31:19 -07:00
_selectChild ( ancestorsIds . size + index ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
_selectChild ( index - 1 ) ;
2020-03-27 13:59:38 -07:00
}
}
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleMoveDown = ( id : string ) = > {
if ( id === status ? . id ) {
_selectChild ( ancestorsIds . size + 1 ) ;
2020-03-27 13:59:38 -07:00
} 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 ) ;
2022-08-08 17:31:19 -07:00
_selectChild ( ancestorsIds . size + index + 2 ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
_selectChild ( index + 1 ) ;
2020-03-27 13:59:38 -07:00
}
}
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const _selectChild = ( index : number ) = > {
scroller . current ? . scrollIntoView ( {
2022-05-27 07:29:54 -07:00
index ,
behavior : 'smooth' ,
done : ( ) = > {
const element = document . querySelector < HTMLDivElement > ( ` #thread [data-index=" ${ index } "] .focusable ` ) ;
2020-03-27 13:59:38 -07:00
2022-05-27 07:29:54 -07:00
if ( element ) {
element . focus ( ) ;
}
} ,
} ) ;
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const 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-06-04 06:20:19 -07:00
< Tombstone
key = { id }
id = { id }
2022-08-08 17:31:19 -07:00
onMoveUp = { handleMoveUp }
onMoveDown = { handleMoveDown }
2022-06-04 06:20:19 -07:00
/ >
2021-04-21 12:47:39 -07:00
< / div >
) ;
2022-08-08 17:31:19 -07:00
} ;
2021-10-06 15:50:43 -07:00
2022-08-08 17:31:19 -07:00
const renderStatus = ( id : string ) = > {
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-08-08 17:31:19 -07:00
focusedStatusId = { status ! . id }
2022-08-12 14:21:44 -07:00
onMoveUp = { handleMoveUp }
onMoveDown = { handleMoveDown }
2020-03-27 13:59:38 -07:00
/ >
2021-04-21 12:47:39 -07:00
) ;
2022-08-08 17:31:19 -07:00
} ;
2021-04-21 12:47:39 -07:00
2022-08-08 17:31:19 -07:00
const renderPendingStatus = ( id : string ) = > {
2021-10-09 19:16:37 -07:00
const idempotencyKey = id . replace ( /^末pending-/ , '' ) ;
2021-10-09 15:47:25 -07:00
return (
< PendingStatus
key = { id }
idempotencyKey = { idempotencyKey }
2022-08-22 09:11:01 -07:00
thread
2021-10-09 15:47:25 -07:00
/ >
) ;
2022-08-08 17:31:19 -07:00
} ;
2021-10-09 15:47:25 -07:00
2022-08-08 17:31:19 -07:00
const 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' ) ) {
2022-08-08 17:31:19 -07:00
return renderTombstone ( id ) ;
2021-10-09 19:16:37 -07:00
} else if ( id . startsWith ( '末pending-' ) ) {
2022-08-08 17:31:19 -07:00
return renderPendingStatus ( id ) ;
2021-04-21 12:47:39 -07:00
} else {
2022-08-08 17:31:19 -07:00
return renderStatus ( id ) ;
2021-04-21 12:47:39 -07:00
}
} ) ;
2022-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
// Reset media visibility if status changes.
useEffect ( ( ) = > {
2022-10-20 07:48:41 -07:00
setShowMedia ( status ? . visibility === 'self' ? false : defaultMediaVisibility ( status , displayMedia ) ) ;
2022-08-08 17:31:19 -07:00
} , [ status ? . id ] ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
// Scroll focused status into view when thread updates.
useEffect ( ( ) = > {
scroller . current ? . scrollToIndex ( {
index : ancestorsIds.size ,
offset : - 80 ,
} ) ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
setImmediate ( ( ) = > statusRef . current ? . querySelector < HTMLDivElement > ( '.detailed-status' ) ? . focus ( ) ) ;
} , [ props . params . statusId , status ? . id , ancestorsIds . size , isLoaded ] ) ;
2021-11-04 10:34:22 -07:00
2022-08-08 17:31:19 -07:00
const handleRefresh = ( ) = > {
return fetchData ( ) ;
} ;
2022-04-23 20:31:49 -07:00
2022-08-08 17:31:19 -07:00
const handleLoadMore = useCallback ( debounce ( ( ) = > {
if ( next && status ) {
dispatch ( fetchNext ( status . id , next ) ) . then ( ( { next } ) = > {
setNext ( next ) ;
2022-11-09 11:03:38 -08:00
} ) . catch ( ( ) = > { } ) ;
2022-04-23 20:31:49 -07:00
}
2022-08-08 17:31:19 -07:00
} , 300 , { leading : true } ) , [ next , status ] ) ;
2022-04-27 13:50:35 -07:00
2022-08-08 17:31:19 -07:00
const handleOpenCompareHistoryModal = ( status : StatusEntity ) = > {
2022-04-27 13:50:35 -07:00
dispatch ( openModal ( 'COMPARE_HISTORY' , {
statusId : status.id ,
} ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2022-04-27 13:50:35 -07:00
2022-08-08 17:31:19 -07:00
const hasAncestors = ancestorsIds . size > 0 ;
const hasDescendants = descendantsIds . size > 0 ;
2022-05-13 18:23:03 -07:00
2022-08-08 17:31:19 -07:00
if ( ! status && isLoaded ) {
return (
< MissingIndicator / >
) ;
} else if ( ! status ) {
return (
< PlaceholderStatus / >
) ;
}
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
type HotkeyHandlers = { [ key : string ] : ( keyEvent? : KeyboardEvent ) = > void } ;
const handlers : HotkeyHandlers = {
moveUp : handleHotkeyMoveUp ,
moveDown : handleHotkeyMoveDown ,
reply : handleHotkeyReply ,
favourite : handleHotkeyFavourite ,
boost : handleHotkeyBoost ,
mention : handleHotkeyMention ,
openProfile : handleHotkeyOpenProfile ,
toggleHidden : handleHotkeyToggleHidden ,
toggleSensitive : handleHotkeyToggleSensitive ,
openMedia : handleHotkeyOpenMedia ,
react : handleHotkeyReact ,
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const username = String ( status . getIn ( [ 'account' , 'acct' ] ) ) ;
const titleMessage = status . visibility === 'direct' ? messages.titleDirect : messages.title ;
const focusedStatus = (
< div className = { classNames ( 'thread__detailed-status' , { 'pb-4' : hasDescendants } ) } key = { status . id } >
< HotKeys handlers = { handlers } >
< div
ref = { statusRef }
2022-10-20 09:32:19 -07:00
className = 'detailed-status__wrapper focusable relative'
2022-08-08 17:31:19 -07:00
tabIndex = { 0 }
// FIXME: no "reblogged by" text is added for the screen reader
aria - label = { textForScreenReader ( intl , status ) }
>
2022-09-29 07:44:06 -07:00
2022-08-08 17:31:19 -07:00
< DetailedStatus
status = { status }
onOpenVideo = { handleOpenVideo }
onOpenMedia = { handleOpenMedia }
onToggleHidden = { handleToggleHidden }
showMedia = { showMedia }
onToggleMediaVisibility = { handleToggleMediaVisibility }
onOpenCompareHistoryModal = { handleOpenCompareHistoryModal }
/ >
2022-11-09 11:03:38 -08:00
{ ! isUnderReview ? (
< >
< hr className = 'mb-2 border-t-2 dark:border-primary-800' / >
< StatusActionBar
status = { status }
expandable = { false }
space = 'expand'
withLabels
/ >
< / >
) : null }
2022-08-08 17:31:19 -07:00
< / div >
< / HotKeys >
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
{ hasDescendants && (
< hr className = 'mt-2 border-t-2 dark:border-primary-800' / >
) }
< / div >
) ;
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
const children : JSX.Element [ ] = [ ] ;
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
if ( hasAncestors ) {
children . push ( . . . renderChildren ( ancestorsIds ) . toArray ( ) ) ;
}
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
children . push ( focusedStatus ) ;
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
if ( hasDescendants ) {
children . push ( . . . renderChildren ( descendantsIds ) . toArray ( ) ) ;
}
2022-04-23 10:20:25 -07:00
2022-08-08 17:31:19 -07:00
return (
< Column label = { intl . formatMessage ( titleMessage , { username } ) } transparent withHeader = { false } >
< 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-08-08 17:31:19 -07:00
< PullToRefresh onRefresh = { handleRefresh } >
< Stack space = { 2 } >
< div ref = { node } className = 'thread' >
< ScrollableList
id = 'thread'
ref = { scroller }
hasMore = { ! ! next }
onLoadMore = { handleLoadMore }
placeholderComponent = { ( ) = > < PlaceholderStatus thread / > }
initialTopMostItemIndex = { ancestorsIds . size }
>
{ children }
< / ScrollableList >
< / div >
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
{ ! me && < ThreadLoginCta / > }
< / Stack >
< / PullToRefresh >
< / Column >
) ;
} ;
2022-04-04 12:17:24 -07:00
2022-08-08 17:54:27 -07:00
export default Thread ;