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-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-08 17:31:19 -07:00
import { defineMessages , FormattedMessage , useIntl } from 'react-intl' ;
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-05-30 11:23:55 -07:00
import { blockAccount } from 'soapbox/actions/accounts' ;
2022-01-10 14:17:52 -08:00
import { launchChat } from 'soapbox/actions/chats' ;
import {
replyCompose ,
mentionCompose ,
directCompose ,
2022-01-23 09:44:17 -08:00
quoteCompose ,
2022-05-30 11:23:55 -07:00
} from 'soapbox/actions/compose' ;
import { simpleEmojiReact } from 'soapbox/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 ,
2022-05-30 11:23:55 -07:00
} from 'soapbox/actions/interactions' ;
import { openModal } from 'soapbox/actions/modals' ;
import {
deactivateUserModal ,
deleteUserModal ,
deleteStatusModal ,
toggleStatusSensitivityModal ,
} from 'soapbox/actions/moderation' ;
import { initMuteModal } from 'soapbox/actions/mutes' ;
import { initReport } from 'soapbox/actions/reports' ;
import { getSettings } from 'soapbox/actions/settings' ;
2020-03-27 13:59:38 -07:00
import {
muteStatus ,
unmuteStatus ,
deleteStatus ,
hideStatus ,
revealStatus ,
2022-04-27 13:50:35 -07:00
editStatus ,
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-06-07 12:26:12 -07:00
import { textForScreenReader } from 'soapbox/components/status' ;
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-08 17:31:19 -07:00
import { useAppDispatch , useAppSelector , useSettings , useSoapboxConfig } from 'soapbox/hooks' ;
2022-05-30 11:23:55 -07:00
import { makeGetStatus } from 'soapbox/selectors' ;
2022-06-07 12:26:12 -07:00
import { defaultMediaVisibility } from 'soapbox/utils/status' ;
2022-05-30 11:23:55 -07: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: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 { 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 getStatus = makeGetStatus ( ) ;
2020-03-27 13:59:38 -07:00
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 ( ) ;
const soapboxConfig = useSoapboxConfig ( ) ;
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 ;
const allowedEmoji = soapboxConfig . allowedEmoji ;
const askReplyConfirmation = useAppSelector ( state = > state . compose . text . trim ( ) . length !== 0 ) ;
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-08-08 17:31:19 -07:00
const [ showMedia , setShowMedia ] = useState < boolean > ( defaultMediaVisibility ( status , displayMedia ) ) ;
const [ emojiSelectorFocused , setEmojiSelectorFocused ] = useState ( false ) ;
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. */
const fetchData = async ( ) = > {
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-08 17:31:19 -07:00
const handleEmojiReactClick = ( status : StatusEntity , emoji : string ) = > {
dispatch ( simpleEmojiReact ( status , emoji ) ) ;
} ;
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 handlePin = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
if ( status . pinned ) {
2022-08-08 17:31:19 -07:00
dispatch ( unpin ( status ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( pin ( 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 handleBookmark = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
if ( status . bookmarked ) {
2022-08-08 17:31:19 -07:00
dispatch ( unbookmark ( status ) ) ;
2020-08-30 22:09:02 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( bookmark ( status ) ) ;
2020-08-30 22:09:02 -07:00
}
2022-08-08 17:31:19 -07:00
} ;
2020-08-30 22:09:02 -07:00
2022-08-08 17:31:19 -07:00
const handleReplyClick = ( status : StatusEntity ) = > {
2020-03-27 13:59:38 -07:00
if ( askReplyConfirmation ) {
dispatch ( openModal ( 'CONFIRM' , {
message : intl.formatMessage ( messages . replyMessage ) ,
confirm : intl.formatMessage ( messages . replyConfirm ) ,
2022-06-20 06:46:43 -07:00
onConfirm : ( ) = > dispatch ( replyCompose ( status ) ) ,
2020-03-27 13:59:38 -07:00
} ) ) ;
} else {
2022-06-20 06:46:43 -07:00
dispatch ( replyCompose ( 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 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 handleQuoteClick = ( status : StatusEntity ) = > {
2022-01-23 09:44:17 -08:00
if ( askReplyConfirmation ) {
dispatch ( openModal ( 'CONFIRM' , {
message : intl.formatMessage ( messages . replyMessage ) ,
confirm : intl.formatMessage ( messages . replyConfirm ) ,
2022-06-20 06:46:43 -07:00
onConfirm : ( ) = > dispatch ( quoteCompose ( status ) ) ,
2022-01-23 09:44:17 -08:00
} ) ) ;
} else {
2022-06-20 06:46:43 -07:00
dispatch ( quoteCompose ( status ) ) ;
2022-01-23 09:44:17 -08: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 handleDeleteClick = ( status : StatusEntity , withRedraft = false ) = > {
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-06-19 12:26:57 -07:00
dispatch ( deleteStatus ( status . id , withRedraft ) ) ;
2020-04-21 12:41:13 -07:00
} else {
dispatch ( openModal ( 'CONFIRM' , {
2022-07-09 09:20:02 -07:00
icon : withRedraft ? require ( '@tabler/icons/edit.svg' ) : require ( '@tabler/icons/trash.svg' ) ,
2021-12-30 08:38:57 -08:00
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-06-19 12:26:57 -07:00
onConfirm : ( ) = > dispatch ( deleteStatus ( status . id , withRedraft ) ) ,
2020-04-21 12:41:13 -07:00
} ) ) ;
}
} ) ;
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 handleEditClick = ( status : StatusEntity ) = > {
2022-06-04 00:22:36 -07:00
dispatch ( editStatus ( 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 handleDirectClick = ( account : AccountEntity ) = > {
dispatch ( directCompose ( account ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleChatClick = ( account : AccountEntity , router : History ) = > {
dispatch ( launchChat ( account . id , router ) ) ;
} ;
2021-10-13 11:55:02 -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 ) = > {
dispatch ( openModal ( 'MEDIA' , { media , index } ) ) ;
} ;
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 handleMuteClick = ( account : AccountEntity ) = > {
dispatch ( initMuteModal ( account ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleConversationMuteClick = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
if ( status . muted ) {
2022-08-08 17:31:19 -07:00
dispatch ( unmuteStatus ( status . id ) ) ;
2020-03-27 13:59:38 -07:00
} else {
2022-08-08 17:31:19 -07:00
dispatch ( muteStatus ( 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 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 handleBlockClick = ( status : StatusEntity ) = > {
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' , {
2022-07-09 09:20:02 -07:00
icon : require ( '@tabler/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-08-08 17:31:19 -07:00
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleReport = ( status : StatusEntity ) = > {
dispatch ( initReport ( status . account as AccountEntity , status ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleEmbed = ( status : StatusEntity ) = > {
dispatch ( openModal ( 'EMBED' , { url : status.url } ) ) ;
} ;
2020-03-27 13:59:38 -07:00
2022-08-08 17:31:19 -07:00
const handleDeactivateUser = ( status : StatusEntity ) = > {
2022-06-19 11:38:51 -07:00
dispatch ( deactivateUserModal ( intl , status . getIn ( [ 'account' , 'id' ] ) as string ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2021-01-18 13:27:35 -08:00
2022-08-08 17:31:19 -07:00
const handleDeleteUser = ( status : StatusEntity ) = > {
2022-06-19 11:38:51 -07:00
dispatch ( deleteUserModal ( intl , status . getIn ( [ 'account' , 'id' ] ) as string ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2021-01-18 13:27:35 -08:00
2022-08-08 17:31:19 -07:00
const handleToggleStatusSensitivity = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
dispatch ( toggleStatusSensitivityModal ( intl , status . id , status . sensitive ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2021-01-18 18:59:07 -08:00
2022-08-08 17:31:19 -07:00
const handleDeleteStatus = ( status : StatusEntity ) = > {
2022-04-04 12:17:24 -07:00
dispatch ( deleteStatusModal ( intl , status . id ) ) ;
2022-08-08 17:31:19 -07:00
} ;
2021-01-18 13:57:20 -08: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 handleHotkeyReact = ( ) = > {
_expandEmojiSelector ( ) ;
} ;
2020-03-27 13:59:38 -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 handleEmojiSelectorExpand : React.EventHandler < React.KeyboardEvent > = e = > {
2021-07-21 04:58:22 -07:00
if ( e . key === 'Enter' ) {
2022-08-08 17:31:19 -07:00
_expandEmojiSelector ( ) ;
2021-07-21 04:58:22 -07:00
}
e . preventDefault ( ) ;
2022-08-08 17:31:19 -07:00
} ;
2021-07-21 04:58:22 -07:00
2022-08-08 17:31:19 -07:00
const handleEmojiSelectorUnfocus : React.EventHandler < React.KeyboardEvent > = ( ) = > {
setEmojiSelectorFocused ( false ) ;
} ;
2021-07-21 04:58:22 -07:00
2022-08-08 17:31:19 -07:00
const _expandEmojiSelector = ( ) = > {
if ( statusRef . current ) {
setEmojiSelectorFocused ( true ) ;
const firstEmoji : HTMLButtonElement | null = statusRef . current . querySelector ( '.emoji-react-selector .emoji-react-selector__emoji' ) ;
firstEmoji ? . focus ( ) ;
}
2021-07-21 04:58:22 -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 }
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
2021-10-09 19:39:15 -07:00
className = 'thread__status'
2021-10-09 15:47:25 -07:00
key = { id }
idempotencyKey = { idempotencyKey }
/ >
) ;
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 ( ( ) = > {
setShowMedia ( defaultMediaVisibility ( status , displayMedia ) ) ;
} , [ 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-04-23 20:31:49 -07:00
} ) . catch ( ( ) = > { } ) ;
}
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 }
className = 'detailed-status__wrapper focusable'
tabIndex = { 0 }
// FIXME: no "reblogged by" text is added for the screen reader
aria - label = { textForScreenReader ( intl , status ) }
>
< DetailedStatus
status = { status }
onOpenVideo = { handleOpenVideo }
onOpenMedia = { handleOpenMedia }
onToggleHidden = { handleToggleHidden }
showMedia = { showMedia }
onToggleMediaVisibility = { handleToggleMediaVisibility }
onOpenCompareHistoryModal = { handleOpenCompareHistoryModal }
/ >
< hr className = 'mb-2 border-t-2 dark:border-primary-800' / >
< ActionBar
status = { status }
onReply = { handleReplyClick }
onFavourite = { handleFavouriteClick }
onEmojiReact = { handleEmojiReactClick }
onReblog = { handleReblogClick }
onQuote = { handleQuoteClick }
onDelete = { handleDeleteClick }
onEdit = { handleEditClick }
onDirect = { handleDirectClick }
onChat = { handleChatClick }
onMention = { handleMentionClick }
onMute = { handleMuteClick }
onMuteConversation = { handleConversationMuteClick }
onBlock = { handleBlockClick }
onReport = { handleReport }
onPin = { handlePin }
onBookmark = { handleBookmark }
onEmbed = { handleEmbed }
onDeactivateUser = { handleDeactivateUser }
onDeleteUser = { handleDeleteUser }
onToggleStatusSensitivity = { handleToggleStatusSensitivity }
onDeleteStatus = { handleDeleteStatus }
allowedEmoji = { allowedEmoji }
emojiSelectorFocused = { emojiSelectorFocused }
handleEmojiSelectorExpand = { handleEmojiSelectorExpand }
handleEmojiSelectorUnfocus = { handleEmojiSelectorUnfocus }
/ >
< / 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 ;