2018-10-07 17:44:58 -04:00
import PropTypes from 'prop-types' ;
2024-01-25 09:34:26 -05:00
import { useCallback } from 'react' ;
2023-05-23 11:15:17 -04:00
2024-01-25 09:34:26 -05:00
import { defineMessages , useIntl , FormattedMessage } from 'react-intl' ;
2023-05-23 11:15:17 -04:00
import classNames from 'classnames' ;
2024-01-25 09:34:26 -05:00
import { Link , useHistory } from 'react-router-dom' ;
2023-05-23 11:15:17 -04:00
2024-01-25 09:34:26 -05:00
import { createSelector } from '@reduxjs/toolkit' ;
2018-10-07 17:44:58 -04:00
import ImmutablePropTypes from 'react-immutable-proptypes' ;
2024-01-25 09:34:26 -05:00
import { useDispatch , useSelector } from 'react-redux' ;
2023-05-23 11:15:17 -04:00
import { HotKeys } from 'react-hotkeys' ;
2024-01-16 05:27:26 -05:00
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react' ;
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react' ;
2024-01-25 09:34:26 -05:00
import { replyCompose } from 'mastodon/actions/compose' ;
import { markConversationRead , deleteConversation } from 'mastodon/actions/conversations' ;
import { openModal } from 'mastodon/actions/modal' ;
import { muteStatus , unmuteStatus , revealStatus , hideStatus } from 'mastodon/actions/statuses' ;
2019-09-21 14:01:16 -04:00
import AttachmentList from 'mastodon/components/attachment_list' ;
import AvatarComposite from 'mastodon/components/avatar_composite' ;
2023-05-08 21:11:56 -04:00
import { IconButton } from 'mastodon/components/icon_button' ;
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp' ;
2023-05-23 11:15:17 -04:00
import StatusContent from 'mastodon/components/status_content' ;
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container' ;
2019-10-01 11:11:14 -04:00
import { autoPlayGif } from 'mastodon/initial_state' ;
2024-01-25 09:34:26 -05:00
import { makeGetStatus } from 'mastodon/selectors' ;
2018-10-07 17:44:58 -04:00
2019-09-21 14:01:16 -04:00
const messages = defineMessages ( {
more : { id : 'status.more' , defaultMessage : 'More' } ,
open : { id : 'conversation.open' , defaultMessage : 'View conversation' } ,
reply : { id : 'status.reply' , defaultMessage : 'Reply' } ,
markAsRead : { id : 'conversation.mark_as_read' , defaultMessage : 'Mark as read' } ,
delete : { id : 'conversation.delete' , defaultMessage : 'Delete conversation' } ,
muteConversation : { id : 'status.mute_conversation' , defaultMessage : 'Mute conversation' } ,
unmuteConversation : { id : 'status.unmute_conversation' , defaultMessage : 'Unmute conversation' } ,
2024-01-25 09:34:26 -05:00
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?' } ,
2019-09-21 14:01:16 -04:00
} ) ;
2024-01-25 09:34:26 -05:00
const getAccounts = createSelector (
( state ) => state . get ( 'accounts' ) ,
( _ , accountIds ) => accountIds ,
( accounts , accountIds ) =>
accountIds . map ( id => accounts . get ( id ) )
) ;
const getStatus = makeGetStatus ( ) ;
export const Conversation = ( { conversation , scrollKey , onMoveUp , onMoveDown } ) => {
const id = conversation . get ( 'id' ) ;
const unread = conversation . get ( 'unread' ) ;
const lastStatusId = conversation . get ( 'last_status' ) ;
const accountIds = conversation . get ( 'accounts' ) ;
const intl = useIntl ( ) ;
const dispatch = useDispatch ( ) ;
const history = useHistory ( ) ;
const lastStatus = useSelector ( state => getStatus ( state , { id : lastStatusId } ) ) ;
const accounts = useSelector ( state => getAccounts ( state , accountIds ) ) ;
const handleMouseEnter = useCallback ( ( { currentTarget } ) => {
2021-01-31 15:25:31 -05:00
if ( autoPlayGif ) {
2019-10-01 11:11:14 -04:00
return ;
}
2021-01-31 15:25:31 -05:00
const emojis = currentTarget . querySelectorAll ( '.custom-emoji' ) ;
2019-10-01 11:11:14 -04:00
for ( var i = 0 ; i < emojis . length ; i ++ ) {
let emoji = emojis [ i ] ;
2021-01-31 15:25:31 -05:00
emoji . src = emoji . getAttribute ( 'data-original' ) ;
2019-10-01 11:11:14 -04:00
}
2024-01-25 09:34:26 -05:00
} , [ ] ) ;
2019-10-01 11:11:14 -04:00
2024-01-25 09:34:26 -05:00
const handleMouseLeave = useCallback ( ( { currentTarget } ) => {
2021-01-31 15:25:31 -05:00
if ( autoPlayGif ) {
return ;
}
2019-10-01 11:11:14 -04:00
2021-01-31 15:25:31 -05:00
const emojis = currentTarget . querySelectorAll ( '.custom-emoji' ) ;
2019-10-01 11:11:14 -04:00
2021-01-31 15:25:31 -05:00
for ( var i = 0 ; i < emojis . length ; i ++ ) {
let emoji = emojis [ i ] ;
emoji . src = emoji . getAttribute ( 'data-static' ) ;
}
2024-01-25 09:34:26 -05:00
} , [ ] ) ;
2018-10-18 19:47:29 -04:00
2024-01-25 09:34:26 -05:00
const handleClick = useCallback ( ( ) => {
2018-10-18 19:47:29 -04:00
if ( unread ) {
2024-01-25 09:34:26 -05:00
dispatch ( markConversationRead ( id ) ) ;
2018-10-18 19:47:29 -04:00
}
2024-01-25 09:34:26 -05:00
history . push ( ` /@ ${ lastStatus . getIn ( [ 'account' , 'acct' ] ) } / ${ lastStatus . get ( 'id' ) } ` ) ;
} , [ dispatch , history , unread , id , lastStatus ] ) ;
const handleMarkAsRead = useCallback ( ( ) => {
dispatch ( markConversationRead ( id ) ) ;
} , [ dispatch , id ] ) ;
const handleReply = useCallback ( ( ) => {
dispatch ( ( _ , getState ) => {
let state = getState ( ) ;
if ( state . getIn ( [ 'compose' , 'text' ] ) . trim ( ) . length !== 0 ) {
dispatch ( openModal ( {
modalType : 'CONFIRM' ,
modalProps : {
message : intl . formatMessage ( messages . replyMessage ) ,
confirm : intl . formatMessage ( messages . replyConfirm ) ,
onConfirm : ( ) => dispatch ( replyCompose ( lastStatus , history ) ) ,
} ,
} ) ) ;
} else {
dispatch ( replyCompose ( lastStatus , history ) ) ;
}
} ) ;
} , [ dispatch , lastStatus , history , intl ] ) ;
const handleDelete = useCallback ( ( ) => {
dispatch ( deleteConversation ( id ) ) ;
} , [ dispatch , id ] ) ;
const handleHotkeyMoveUp = useCallback ( ( ) => {
onMoveUp ( id ) ;
} , [ id , onMoveUp ] ) ;
const handleHotkeyMoveDown = useCallback ( ( ) => {
onMoveDown ( id ) ;
} , [ id , onMoveDown ] ) ;
const handleConversationMute = useCallback ( ( ) => {
if ( lastStatus . get ( 'muted' ) ) {
dispatch ( unmuteStatus ( lastStatus . get ( 'id' ) ) ) ;
} else {
dispatch ( muteStatus ( lastStatus . get ( 'id' ) ) ) ;
2018-10-07 17:44:58 -04:00
}
2024-01-25 09:34:26 -05:00
} , [ dispatch , lastStatus ] ) ;
2018-10-07 17:44:58 -04:00
2024-01-25 09:34:26 -05:00
const handleShowMore = useCallback ( ( ) => {
if ( lastStatus . get ( 'hidden' ) ) {
dispatch ( revealStatus ( lastStatus . get ( 'id' ) ) ) ;
} else {
dispatch ( hideStatus ( lastStatus . get ( 'id' ) ) ) ;
2019-09-21 14:01:16 -04:00
}
2024-01-25 09:34:26 -05:00
} , [ dispatch , lastStatus ] ) ;
if ( ! lastStatus ) {
return null ;
}
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
const menu = [
{ text : intl . formatMessage ( messages . open ) , action : handleClick } ,
null ,
{ text : intl . formatMessage ( lastStatus . get ( 'muted' ) ? messages . unmuteConversation : messages . muteConversation ) , action : handleConversationMute } ,
] ;
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
if ( unread ) {
menu . push ( { text : intl . formatMessage ( messages . markAsRead ) , action : handleMarkAsRead } ) ;
menu . push ( null ) ;
}
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
menu . push ( { text : intl . formatMessage ( messages . delete ) , action : handleDelete } ) ;
const names = accounts . map ( a => (
< Link to = { ` /@ ${ a . get ( 'acct' ) } ` } key = { a . get ( 'id' ) } title = { a . get ( 'acct' ) } >
< bdi >
< strong
className = 'display-name__html'
dangerouslySetInnerHTML = { { _ _html : a . get ( 'display_name_html' ) } }
/ >
< / bdi >
< / Link >
) ) . reduce ( ( prev , cur ) => [ prev , ', ' , cur ] ) ;
const handlers = {
reply : handleReply ,
open : handleClick ,
moveUp : handleHotkeyMoveUp ,
moveDown : handleHotkeyMoveDown ,
toggleHidden : handleShowMore ,
} ;
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
return (
< HotKeys handlers = { handlers } >
< div className = { classNames ( 'conversation focusable muted' , { 'conversation--unread' : unread } ) } tabIndex = { 0 } >
< div className = 'conversation__avatar' onClick = { handleClick } role = 'presentation' >
< AvatarComposite accounts = { accounts } size = { 48 } / >
< / div >
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
< div className = 'conversation__content' >
< div className = 'conversation__content__info' >
< div className = 'conversation__content__relative-time' >
{ unread && < span className = 'conversation__unread' / > } < RelativeTimestamp timestamp = { lastStatus . get ( 'created_at' ) } / >
< / div >
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
< div className = 'conversation__content__names' onMouseEnter = { handleMouseEnter } onMouseLeave = { handleMouseLeave } >
< FormattedMessage id = 'conversation.with' defaultMessage = 'With {names}' values = { { names : < span > { names } < / span > } } / >
2019-09-21 14:01:16 -04:00
< / div >
2024-01-25 09:34:26 -05:00
< / div >
2019-09-21 14:01:16 -04:00
2024-01-25 09:34:26 -05:00
< StatusContent
status = { lastStatus }
onClick = { handleClick }
expanded = { ! lastStatus . get ( 'hidden' ) }
onExpandedToggle = { handleShowMore }
collapsible
/ >
{ lastStatus . get ( 'media_attachments' ) . size > 0 && (
< AttachmentList
compact
media = { lastStatus . get ( 'media_attachments' ) }
2019-09-21 14:01:16 -04:00
/ >
2024-01-25 09:34:26 -05:00
) }
< div className = 'status__action-bar' >
< IconButton className = 'status__action-bar-button' title = { intl . formatMessage ( messages . reply ) } icon = 'reply' iconComponent = { ReplyIcon } onClick = { handleReply } / >
< div className = 'status__action-bar-dropdown' >
< DropdownMenuContainer
scrollKey = { scrollKey }
status = { lastStatus }
items = { menu }
icon = 'ellipsis-h'
iconComponent = { MoreHorizIcon }
size = { 18 }
direction = 'right'
title = { intl . formatMessage ( messages . more ) }
2019-09-21 14:01:16 -04:00
/ >
< / div >
< / div >
< / div >
2024-01-25 09:34:26 -05:00
< / div >
< / HotKeys >
) ;
} ;
Conversation . propTypes = {
conversation : ImmutablePropTypes . map . isRequired ,
scrollKey : PropTypes . string ,
onMoveUp : PropTypes . func ,
onMoveDown : PropTypes . func ,
} ;