2022-09-08 14:25:02 -07:00
import { List as ImmutableList , OrderedSet as ImmutableOrderedSet } from 'immutable' ;
import debounce from 'lodash/debounce' ;
import React , { useCallback , useEffect , useRef , useState } from 'react' ;
2022-09-20 15:12:52 -07:00
import { FormattedMessage } from 'react-intl' ;
2022-09-08 14:25:02 -07:00
2022-09-16 12:02:15 -07:00
import { eventDiscussionCompose } from 'soapbox/actions/compose' ;
2022-09-08 14:25:02 -07:00
import { fetchStatusWithContext , fetchNext } from 'soapbox/actions/statuses' ;
2022-11-26 11:25:48 -08:00
import MissingIndicator from 'soapbox/components/missing-indicator' ;
import ScrollableList from 'soapbox/components/scrollable-list' ;
2022-09-08 14:25:02 -07:00
import Tombstone from 'soapbox/components/tombstone' ;
import { Stack } from 'soapbox/components/ui' ;
2022-11-26 11:25:48 -08:00
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status' ;
import PendingStatus from 'soapbox/features/ui/components/pending-status' ;
2022-09-08 14:25:02 -07:00
import { useAppDispatch , useAppSelector } from 'soapbox/hooks' ;
import { makeGetStatus } from 'soapbox/selectors' ;
2022-09-16 12:02:15 -07:00
import ComposeForm from '../compose/components/compose-form' ;
2022-09-30 14:20:58 -07:00
import { getDescendantsIds } from '../status' ;
2022-09-08 14:25:02 -07:00
import ThreadStatus from '../status/components/thread-status' ;
import type { VirtuosoHandle } from 'react-virtuoso' ;
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities' ;
type RouteParams = { statusId : string } ;
2022-09-16 12:02:15 -07:00
interface IEventDiscussion {
2022-09-08 14:25:02 -07:00
params : RouteParams ,
onOpenMedia : ( media : ImmutableList < AttachmentEntity > , index : number ) = > void ,
onOpenVideo : ( video : AttachmentEntity , time : number ) = > void ,
}
2022-09-16 12:02:15 -07:00
const EventDiscussion : React.FC < IEventDiscussion > = ( props ) = > {
2022-09-08 14:25:02 -07:00
const dispatch = useAppDispatch ( ) ;
2022-09-21 14:27:53 -07:00
const getStatus = useCallback ( makeGetStatus ( ) , [ ] ) ;
2022-09-08 14:25:02 -07:00
const status = useAppSelector ( state = > getStatus ( state , { id : props.params.statusId } ) ) ;
2022-09-21 14:27:53 -07:00
const me = useAppSelector ( ( state ) = > state . me ) ;
2022-09-08 14:25:02 -07:00
const descendantsIds = useAppSelector ( state = > {
let descendantsIds = ImmutableOrderedSet < string > ( ) ;
if ( status ) {
const statusId = status . id ;
descendantsIds = getDescendantsIds ( state , statusId ) ;
descendantsIds = descendantsIds . delete ( statusId ) ;
}
return descendantsIds ;
} ) ;
const [ isLoaded , setIsLoaded ] = useState < boolean > ( ! ! status ) ;
const [ next , setNext ] = useState < string > ( ) ;
const node = useRef < HTMLDivElement > ( null ) ;
const scroller = useRef < VirtuosoHandle > ( null ) ;
const fetchData = async ( ) = > {
const { params } = props ;
const { statusId } = params ;
const { next } = await dispatch ( fetchStatusWithContext ( statusId ) ) ;
setNext ( next ) ;
} ;
useEffect ( ( ) = > {
fetchData ( ) . then ( ( ) = > {
setIsLoaded ( true ) ;
} ) . catch ( ( ) = > {
setIsLoaded ( true ) ;
} ) ;
} , [ props . params . statusId ] ) ;
2022-09-16 12:02:15 -07:00
useEffect ( ( ) = > {
2022-09-21 14:27:53 -07:00
if ( isLoaded && me ) dispatch ( eventDiscussionCompose ( ` reply: ${ props . params . statusId } ` , status ! ) ) ;
} , [ isLoaded , me ] ) ;
2022-09-16 12:02:15 -07:00
2022-09-08 14:25:02 -07:00
const handleMoveUp = ( id : string ) = > {
const index = ImmutableList ( descendantsIds ) . indexOf ( id ) ;
_selectChild ( index - 1 ) ;
} ;
const handleMoveDown = ( id : string ) = > {
const index = ImmutableList ( descendantsIds ) . indexOf ( id ) ;
_selectChild ( index + 1 ) ;
} ;
const _selectChild = ( index : number ) = > {
scroller . current ? . scrollIntoView ( {
index ,
behavior : 'smooth' ,
done : ( ) = > {
const element = document . querySelector < HTMLDivElement > ( ` #thread [data-index=" ${ index } "] .focusable ` ) ;
if ( element ) {
element . focus ( ) ;
}
} ,
} ) ;
} ;
const renderTombstone = ( id : string ) = > {
return (
< div className = 'py-4 pb-8' >
< Tombstone
key = { id }
id = { id }
onMoveUp = { handleMoveUp }
onMoveDown = { handleMoveDown }
/ >
< / div >
) ;
} ;
const renderStatus = ( id : string ) = > {
return (
< ThreadStatus
key = { id }
id = { id }
focusedStatusId = { status ! . id }
onMoveUp = { handleMoveUp }
onMoveDown = { handleMoveDown }
/ >
) ;
} ;
const renderPendingStatus = ( id : string ) = > {
const idempotencyKey = id . replace ( /^末pending-/ , '' ) ;
return (
< PendingStatus
key = { id }
idempotencyKey = { idempotencyKey }
thread
/ >
) ;
} ;
const renderChildren = ( list : ImmutableOrderedSet < string > ) = > {
return list . map ( id = > {
if ( id . endsWith ( '-tombstone' ) ) {
return renderTombstone ( id ) ;
} else if ( id . startsWith ( '末pending-' ) ) {
return renderPendingStatus ( id ) ;
} else {
return renderStatus ( id ) ;
}
} ) ;
} ;
const handleLoadMore = useCallback ( debounce ( ( ) = > {
if ( next && status ) {
dispatch ( fetchNext ( status . id , next ) ) . then ( ( { next } ) = > {
setNext ( next ) ;
} ) . catch ( ( ) = > { } ) ;
}
} , 300 , { leading : true } ) , [ next , status ] ) ;
const hasDescendants = descendantsIds . size > 0 ;
if ( ! status && isLoaded ) {
return (
< MissingIndicator / >
) ;
} else if ( ! status ) {
return (
< PlaceholderStatus / >
) ;
}
const children : JSX.Element [ ] = [ ] ;
if ( hasDescendants ) {
children . push ( . . . renderChildren ( descendantsIds ) . toArray ( ) ) ;
}
return (
2022-09-16 12:02:15 -07:00
< Stack space = { 2 } >
2022-09-21 14:27:53 -07:00
{ me && < div className = 'sm:p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800' >
2022-09-30 14:20:58 -07:00
< ComposeForm id = { ` reply: ${ status . id } ` } autoFocus = { false } event = { status . id } / >
2022-09-21 14:27:53 -07:00
< / div > }
2022-09-16 12:02:15 -07:00
< div ref = { node } className = 'thread p-0 sm:p-2 shadow-none' >
< ScrollableList
id = 'thread'
ref = { scroller }
hasMore = { ! ! next }
onLoadMore = { handleLoadMore }
placeholderComponent = { ( ) = > < PlaceholderStatus thread / > }
initialTopMostItemIndex = { 0 }
2022-09-20 15:12:52 -07:00
emptyMessage = { < FormattedMessage id = 'event.discussion.empty' defaultMessage = 'No one has commented this event yet. When someone does, they will appear here.' / > }
2022-09-16 12:02:15 -07:00
>
{ children }
< / ScrollableList >
< / div >
< / Stack >
2022-09-08 14:25:02 -07:00
) ;
} ;
2022-09-16 12:02:15 -07:00
export default EventDiscussion ;