2019-04-21 17:07:48 +00:00
import PropTypes from 'prop-types' ;
2023-10-31 10:17:37 +00:00
import { createRef } from 'react' ;
2023-05-28 14:38:10 +00:00
2019-04-20 19:28:03 +00:00
import { defineMessages , injectIntl } from 'react-intl' ;
2023-05-28 14:38:10 +00:00
2023-12-02 18:21:43 +00:00
import classNames from 'classnames' ;
2023-05-28 14:38:10 +00:00
import ImmutablePropTypes from 'react-immutable-proptypes' ;
import ImmutablePureComponent from 'react-immutable-pure-component' ;
import { length } from 'stringz' ;
import { maxChars } from 'flavours/glitch/initial_state' ;
import { isMobile } from 'flavours/glitch/is_mobile' ;
2023-10-19 17:44:55 +00:00
import { WithOptionalRouterPropTypes , withOptionalRouter } from 'flavours/glitch/utils/react_router' ;
2023-05-28 14:38:10 +00:00
import AutosuggestInput from '../../../components/autosuggest_input' ;
import AutosuggestTextarea from '../../../components/autosuggest_textarea' ;
2022-10-11 11:33:21 +00:00
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container' ;
2023-05-28 14:38:10 +00:00
import OptionsContainer from '../containers/options_container' ;
2019-04-21 17:07:48 +00:00
import PollFormContainer from '../containers/poll_form_container' ;
2023-07-05 08:14:10 +00:00
import QuoteIndicatorContainer from '../containers/quote_indicator_container' ;
2023-05-28 14:38:10 +00:00
import ReplyIndicatorContainer from '../containers/reply_indicator_container' ;
2019-04-21 17:07:48 +00:00
import UploadFormContainer from '../containers/upload_form_container' ;
import WarningContainer from '../containers/warning_container' ;
2022-10-11 09:51:15 +00:00
import { countableText } from '../util/counter' ;
2023-05-28 14:38:10 +00:00
import CharacterCounter from './character_counter' ;
2019-04-21 16:48:33 +00:00
import Publisher from './publisher' ;
2019-04-21 13:57:06 +00:00
import TextareaIcons from './textarea_icons' ;
2017-12-24 06:16:45 +00:00
2018-08-29 13:26:24 +00:00
const messages = defineMessages ( {
2019-04-21 10:44:30 +00:00
placeholder : { id : 'compose_form.placeholder' , defaultMessage : 'What is on your mind?' } ,
2023-02-04 10:09:05 +00:00
missingDescriptionMessage : {
id : 'confirmations.missing_media_description.message' ,
defaultMessage : 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' ,
} ,
missingDescriptionConfirm : {
id : 'confirmations.missing_media_description.confirm' ,
defaultMessage : 'Send anyway' ,
} ,
2019-04-20 21:02:09 +00:00
spoiler _placeholder : { id : 'compose_form.spoiler_placeholder' , defaultMessage : 'Write your warning here' } ,
2018-08-29 13:26:24 +00:00
} ) ;
2019-04-20 19:28:03 +00:00
class ComposeForm extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes . object . isRequired ,
2023-11-16 16:57:13 +00:00
text : PropTypes . string . isRequired ,
2019-04-21 17:07:48 +00:00
suggestions : ImmutablePropTypes . list ,
spoiler : PropTypes . bool ,
privacy : PropTypes . string ,
spoilerText : PropTypes . string ,
2019-04-20 19:28:03 +00:00
focusDate : PropTypes . instanceOf ( Date ) ,
caretPosition : PropTypes . number ,
2019-04-21 17:07:48 +00:00
preselectDate : PropTypes . instanceOf ( Date ) ,
2019-04-20 19:28:03 +00:00
isSubmitting : PropTypes . bool ,
isChangingUpload : PropTypes . bool ,
2022-02-09 23:15:30 +00:00
isEditing : PropTypes . bool ,
2019-04-20 19:28:03 +00:00
isUploading : PropTypes . bool ,
2023-12-18 12:20:08 +00:00
onChange : PropTypes . func . isRequired ,
onSubmit : PropTypes . func . isRequired ,
onClearSuggestions : PropTypes . func . isRequired ,
onFetchSuggestions : PropTypes . func . isRequired ,
onSuggestionSelected : PropTypes . func . isRequired ,
onChangeSpoilerText : PropTypes . func . isRequired ,
onPaste : PropTypes . func . isRequired ,
onPickEmoji : PropTypes . func . isRequired ,
2019-04-21 17:07:48 +00:00
showSearch : PropTypes . bool ,
anyMedia : PropTypes . bool ,
2022-01-23 17:24:34 +00:00
isInReply : PropTypes . bool ,
2019-06-16 14:02:26 +00:00
singleColumn : PropTypes . bool ,
2023-01-24 17:49:21 +00:00
lang : PropTypes . string ,
2019-04-21 17:07:48 +00:00
advancedOptions : ImmutablePropTypes . map ,
2019-04-20 19:28:03 +00:00
media : ImmutablePropTypes . list ,
sideArm : PropTypes . string ,
sensitive : PropTypes . bool ,
spoilersAlwaysOn : PropTypes . bool ,
mediaDescriptionConfirmation : PropTypes . bool ,
preselectOnReply : PropTypes . bool ,
2023-12-18 12:20:08 +00:00
onChangeSpoilerness : PropTypes . func . isRequired ,
onChangeVisibility : PropTypes . func . isRequired ,
onMediaDescriptionConfirm : PropTypes . func . isRequired ,
2023-10-19 17:44:55 +00:00
... WithOptionalRouterPropTypes
2019-04-20 19:28:03 +00:00
} ;
2017-12-24 06:16:45 +00:00
2019-04-21 17:07:48 +00:00
static defaultProps = {
showSearch : false ,
} ;
2017-12-24 06:16:45 +00:00
2023-04-23 20:24:53 +00:00
state = {
highlighted : false ,
} ;
2023-10-31 10:17:37 +00:00
constructor ( props ) {
super ( props ) ;
this . textareaRef = createRef ( null ) ;
}
2019-04-21 17:07:48 +00:00
handleChange = ( e ) => {
this . props . onChange ( e . target . value ) ;
2023-02-03 19:52:07 +00:00
} ;
2017-12-24 06:16:45 +00:00
2023-12-18 12:20:08 +00:00
handleKeyDown = ( e ) => {
if ( e . keyCode === 13 && ( e . ctrlKey || e . metaKey ) ) {
this . handleSubmit ( ) ;
}
if ( e . keyCode === 13 && e . altKey ) {
this . handleSecondarySubmit ( ) ;
}
} ;
2020-11-30 11:09:34 +00:00
getFulltextForCharacterCounting = ( ) => {
return [
this . props . spoiler ? this . props . spoilerText : '' ,
countableText ( this . props . text ) ,
2023-02-03 19:52:07 +00:00
this . props . advancedOptions && this . props . advancedOptions . get ( 'do_not_federate' ) ? ' 👁️' : '' ,
2020-11-30 11:09:34 +00:00
] . join ( '' ) ;
2023-02-03 19:52:07 +00:00
} ;
2020-11-30 11:09:34 +00:00
canSubmit = ( ) => {
const { isSubmitting , isChangingUpload , isUploading , anyMedia } = this . props ;
const fulltext = this . getFulltextForCharacterCounting ( ) ;
return ! ( isSubmitting || isUploading || isChangingUpload || length ( fulltext ) > maxChars || ( ! fulltext . trim ( ) . length && ! anyMedia ) ) ;
2023-02-03 19:52:07 +00:00
} ;
2020-11-30 11:09:34 +00:00
2023-12-22 16:23:15 +00:00
handleSubmit = ( e , overriddenVisibility = null ) => {
2023-10-31 10:17:37 +00:00
if ( this . props . text !== this . textareaRef . current . value ) {
2020-11-30 11:09:34 +00:00
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
2023-10-31 10:17:37 +00:00
this . props . onChange ( this . textareaRef . current . value ) ;
2017-12-24 06:16:45 +00:00
}
2020-11-30 11:09:34 +00:00
if ( ! this . canSubmit ( ) ) {
2018-04-02 15:22:32 +00:00
return ;
}
2023-12-22 16:23:15 +00:00
if ( e ) {
e . preventDefault ( ) ;
2017-12-24 06:16:45 +00:00
}
2019-06-07 15:15:18 +00:00
2018-08-29 13:26:24 +00:00
// Submit unless there are media with missing descriptions
2023-12-18 12:20:08 +00:00
if ( this . props . mediaDescriptionConfirmation && this . props . media && this . props . media . some ( item => ! item . get ( 'description' ) ) ) {
const firstWithoutDescription = this . props . media . find ( item => ! item . get ( 'description' ) ) ;
this . props . onMediaDescriptionConfirm ( this . props . history || null , firstWithoutDescription . get ( 'id' ) , overriddenVisibility ) ;
} else {
if ( overriddenVisibility ) {
this . props . onChangeVisibility ( overriddenVisibility ) ;
2020-02-14 19:19:35 +00:00
}
2023-12-18 12:20:08 +00:00
this . props . onSubmit ( this . props . history || null ) ;
2019-04-21 17:07:48 +00:00
}
2023-02-03 19:52:07 +00:00
} ;
2019-04-21 17:07:48 +00:00
// Handles the secondary submit button.
handleSecondarySubmit = ( ) => {
2023-12-22 16:23:15 +00:00
const { sideArm } = this . props ;
this . handleSubmit ( null , sideArm === 'none' ? null : sideArm ) ;
2023-02-03 19:52:07 +00:00
} ;
2019-04-21 17:07:48 +00:00
2023-12-18 12:20:08 +00:00
onSuggestionsClearRequested = ( ) => {
this . props . onClearSuggestions ( ) ;
2023-02-03 19:52:07 +00:00
} ;
2019-04-11 15:18:55 +00:00
2023-12-18 12:20:08 +00:00
onSuggestionsFetchRequested = ( token ) => {
this . props . onFetchSuggestions ( token ) ;
2023-02-03 19:52:07 +00:00
} ;
2019-04-21 17:07:48 +00:00
2023-12-18 12:20:08 +00:00
onSuggestionSelected = ( tokenStart , token , value ) => {
this . props . onSuggestionSelected ( tokenStart , token , value , [ 'text' ] ) ;
2023-02-03 19:52:07 +00:00
} ;
2019-04-21 17:07:48 +00:00
2023-12-18 12:20:08 +00:00
onSpoilerSuggestionSelected = ( tokenStart , token , value ) => {
this . props . onSuggestionSelected ( tokenStart , token , value , [ 'spoiler_text' ] ) ;
2023-02-03 19:52:07 +00:00
} ;
2018-08-18 18:53:46 +00:00
2023-12-18 12:20:08 +00:00
handleChangeSpoilerText = ( e ) => {
this . props . onChangeSpoilerText ( e . target . value ) ;
2023-02-03 19:52:07 +00:00
} ;
2017-12-24 06:16:45 +00:00
2019-06-05 13:29:45 +00:00
handleFocus = ( ) => {
2019-06-16 14:02:26 +00:00
if ( this . composeForm && ! this . props . singleColumn ) {
2019-07-06 16:18:08 +00:00
const { left , right } = this . composeForm . getBoundingClientRect ( ) ;
if ( left < 0 || right > ( window . innerWidth || document . documentElement . clientWidth ) ) {
this . composeForm . scrollIntoView ( ) ;
}
2019-06-07 15:15:18 +00:00
}
2023-02-03 19:52:07 +00:00
} ;
2019-06-05 13:29:45 +00:00
2021-03-24 09:19:07 +00:00
componentDidMount ( ) {
this . _updateFocusAndSelection ( { } ) ;
}
2023-04-23 20:24:53 +00:00
componentWillUnmount ( ) {
if ( this . timeout ) clearTimeout ( this . timeout ) ;
}
2021-03-24 09:19:07 +00:00
componentDidUpdate ( prevProps ) {
this . _updateFocusAndSelection ( prevProps ) ;
}
2023-02-03 19:52:07 +00:00
_updateFocusAndSelection = ( prevProps ) => {
2023-12-18 12:20:08 +00:00
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
if ( this . props . focusDate && this . props . focusDate !== prevProps . focusDate ) {
let selectionEnd , selectionStart ;
if ( this . props . preselectDate !== prevProps . preselectDate && this . props . isInReply && this . props . preselectOnReply ) {
selectionEnd = this . props . text . length ;
selectionStart = this . props . text . search ( /\s/ ) + 1 ;
} else if ( typeof this . props . caretPosition === 'number' ) {
selectionStart = this . props . caretPosition ;
selectionEnd = this . props . caretPosition ;
} else {
selectionEnd = this . props . text . length ;
selectionStart = selectionEnd ;
2018-01-03 20:36:21 +00:00
}
2017-12-24 06:16:45 +00:00
2023-12-18 12:20:08 +00:00
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise . resolve ( ) . then ( ( ) => {
this . textareaRef . current . setSelectionRange ( selectionStart , selectionEnd ) ;
this . textareaRef . current . focus ( ) ;
if ( ! this . props . singleColumn ) this . textareaRef . current . scrollIntoView ( ) ;
this . setState ( { highlighted : true } ) ;
this . timeout = setTimeout ( ( ) => this . setState ( { highlighted : false } ) , 700 ) ;
} ) . catch ( console . error ) ;
} else if ( prevProps . isSubmitting && ! this . props . isSubmitting ) {
2023-10-31 10:17:37 +00:00
this . textareaRef . current . focus ( ) ;
2018-08-18 18:53:46 +00:00
} else if ( this . props . spoiler !== prevProps . spoiler ) {
if ( this . props . spoiler ) {
2023-12-18 12:20:08 +00:00
this . spoilerText . input . focus ( ) ;
} else if ( prevProps . spoiler ) {
this . textareaRef . current . focus ( ) ;
2018-08-18 18:53:46 +00:00
}
2017-12-24 06:16:45 +00:00
}
2023-02-03 19:52:07 +00:00
} ;
2017-12-24 06:16:45 +00:00
2023-12-18 12:20:08 +00:00
setSpoilerText = ( c ) => {
this . spoilerText = c ;
} ;
setRef = c => {
this . composeForm = c ;
} ;
handleEmojiPick = ( data ) => {
const position = this . textareaRef . current . selectionStart ;
this . props . onPickEmoji ( position , data ) ;
} ;
2019-04-21 10:44:30 +00:00
2017-12-24 06:16:45 +00:00
render ( ) {
const {
intl ,
2023-12-18 12:20:08 +00:00
advancedOptions ,
2018-01-03 20:36:21 +00:00
isSubmitting ,
onChangeSpoilerness ,
2019-04-21 17:07:48 +00:00
onPaste ,
2018-01-03 20:36:21 +00:00
privacy ,
sensitive ,
showSearch ,
sideArm ,
2018-08-22 13:58:57 +00:00
spoilersAlwaysOn ,
2022-02-09 23:15:30 +00:00
isEditing ,
2017-12-24 06:16:45 +00:00
} = this . props ;
2023-04-23 20:24:53 +00:00
const { highlighted } = this . state ;
2023-12-18 12:20:08 +00:00
const disabled = this . props . isSubmitting ;
2019-08-19 19:41:41 +00:00
2017-12-24 06:16:45 +00:00
return (
2023-12-18 12:20:08 +00:00
< form className = 'compose-form' onSubmit = { this . handleSubmit } >
2019-04-20 20:05:09 +00:00
< WarningContainer / >
2019-04-20 20:21:28 +00:00
< ReplyIndicatorContainer / >
2022-12-26 02:53:01 +00:00
< QuoteIndicatorContainer / >
2019-04-20 20:05:09 +00:00
2023-12-18 12:20:08 +00:00
< div className = { ` spoiler-input ${ this . props . spoiler ? 'spoiler-input--visible' : '' } ` } ref = { this . setRef } aria - hidden = { ! this . props . spoiler } >
2019-04-11 15:18:55 +00:00
< AutosuggestInput
placeholder = { intl . formatMessage ( messages . spoiler _placeholder ) }
2023-12-18 12:20:08 +00:00
value = { this . props . spoilerText }
onChange = { this . handleChangeSpoilerText }
2020-05-25 18:04:06 +00:00
onKeyDown = { this . handleKeyDown }
2023-12-18 12:20:08 +00:00
disabled = { ! this . props . spoiler }
ref = { this . setSpoilerText }
suggestions = { this . props . suggestions }
onSuggestionsFetchRequested = { this . onSuggestionsFetchRequested }
onSuggestionsClearRequested = { this . onSuggestionsClearRequested }
onSuggestionSelected = { this . onSpoilerSuggestionSelected }
2019-04-11 15:18:55 +00:00
searchTokens = { [ ':' ] }
2023-12-18 12:20:08 +00:00
id = 'cw-spoiler-input'
2019-05-03 16:54:06 +00:00
className = 'spoiler-input__input'
2023-01-24 17:49:21 +00:00
lang = { this . props . lang }
2019-06-06 10:14:11 +00:00
autoFocus = { false }
2023-02-04 15:34:21 +00:00
spellCheck
2019-04-11 15:18:55 +00:00
/ >
2019-04-20 21:02:09 +00:00
< / div >
2019-04-20 20:05:09 +00:00
2023-04-23 20:24:53 +00:00
< div className = { classNames ( 'compose-form__highlightable' , { active : highlighted } ) } >
< AutosuggestTextarea
2023-10-31 10:17:37 +00:00
ref = { this . textareaRef }
2023-04-23 20:24:53 +00:00
placeholder = { intl . formatMessage ( messages . placeholder ) }
2023-12-18 12:20:08 +00:00
disabled = { disabled }
2023-04-23 20:24:53 +00:00
value = { this . props . text }
onChange = { this . handleChange }
2023-12-18 12:20:08 +00:00
suggestions = { this . props . suggestions }
2023-04-23 20:24:53 +00:00
onFocus = { this . handleFocus }
2023-12-18 12:20:08 +00:00
onKeyDown = { this . handleKeyDown }
onSuggestionsFetchRequested = { this . onSuggestionsFetchRequested }
onSuggestionsClearRequested = { this . onSuggestionsClearRequested }
onSuggestionSelected = { this . onSuggestionSelected }
2023-04-23 20:24:53 +00:00
onPaste = { onPaste }
2024-02-08 18:12:02 +00:00
autoFocus = { ! showSearch && ! isMobile ( window . innerWidth ) }
2023-04-23 20:24:53 +00:00
lang = { this . props . lang }
>
< TextareaIcons advancedOptions = { advancedOptions } / >
< div className = 'compose-form__modifiers' >
< UploadFormContainer / >
< PollFormContainer / >
< / div >
< / AutosuggestTextarea >
2023-12-18 12:20:08 +00:00
< EmojiPickerDropdown onPickEmoji = { this . handleEmojiPick } / >
2019-04-21 10:44:30 +00:00
2023-06-06 09:05:25 +00:00
< div className = 'compose-form__buttons-wrapper' >
< OptionsContainer
advancedOptions = { advancedOptions }
disabled = { isSubmitting }
2023-12-18 12:20:08 +00:00
onToggleSpoiler = { this . props . spoilersAlwaysOn ? null : onChangeSpoilerness }
2023-06-06 09:05:25 +00:00
onUpload = { onPaste }
isEditing = { isEditing }
2023-12-18 12:20:08 +00:00
sensitive = { sensitive || ( spoilersAlwaysOn && this . props . spoilerText && this . props . spoilerText . length > 0 ) }
spoiler = { spoilersAlwaysOn ? ( this . props . spoilerText && this . props . spoilerText . length > 0 ) : this . props . spoiler }
2023-06-06 09:05:25 +00:00
/ >
< div className = 'character-counter__wrapper' >
2023-12-18 12:20:08 +00:00
< CharacterCounter max = { maxChars } text = { this . getFulltextForCharacterCounting ( ) } / >
2023-06-06 09:05:25 +00:00
< / div >
2019-08-19 19:41:41 +00:00
< / div >
< / div >
2019-04-21 10:44:30 +00:00
2019-04-21 16:48:33 +00:00
< Publisher
2020-11-30 11:09:34 +00:00
disabled = { ! this . canSubmit ( ) }
2022-02-09 23:15:30 +00:00
isEditing = { isEditing }
2023-12-18 12:20:08 +00:00
onSecondarySubmit = { this . handleSecondarySubmit }
2017-12-24 06:16:45 +00:00
privacy = { privacy }
sideArm = { sideArm }
/ >
2023-12-18 12:20:08 +00:00
< / form >
2017-12-24 06:16:45 +00:00
) ;
}
}
2023-03-24 22:15:25 +00:00
2023-10-19 17:44:55 +00:00
export default withOptionalRouter ( injectIntl ( ComposeForm ) ) ;