2017-12-24 07:16:45 +01:00
import React from 'react' ;
import ImmutablePropTypes from 'react-immutable-proptypes' ;
2019-04-21 19:07:48 +02:00
import PropTypes from 'prop-types' ;
import ReplyIndicatorContainer from '../containers/reply_indicator_container' ;
import AutosuggestTextarea from '../../../components/autosuggest_textarea' ;
2019-04-11 17:18:55 +02:00
import AutosuggestInput from '../../../components/autosuggest_input' ;
2019-04-20 21:28:03 +02:00
import { defineMessages , injectIntl } from 'react-intl' ;
2019-04-21 19:07:48 +02:00
import EmojiPicker from 'flavours/glitch/features/emoji_picker' ;
import PollFormContainer from '../containers/poll_form_container' ;
import UploadFormContainer from '../containers/upload_form_container' ;
import WarningContainer from '../containers/warning_container' ;
import { isMobile } from 'flavours/glitch/util/is_mobile' ;
2019-04-20 21:28:03 +02:00
import ImmutablePureComponent from 'react-immutable-pure-component' ;
2019-04-21 19:07:48 +02:00
import { countableText } from 'flavours/glitch/util/counter' ;
2019-04-21 18:31:26 +02:00
import OptionsContainer from '../containers/options_container' ;
2019-04-21 18:48:33 +02:00
import Publisher from './publisher' ;
2019-04-21 15:57:06 +02:00
import TextareaIcons from './textarea_icons' ;
2019-08-19 21:41:41 +02:00
import { maxChars } from 'flavours/glitch/util/initial_state' ;
import CharacterCounter from './character_counter' ;
2020-11-30 12:09:34 +01:00
import { length } from 'stringz' ;
2017-12-24 07:16:45 +01:00
2018-08-29 15:26:24 +02:00
const messages = defineMessages ( {
2019-04-21 12:44:30 +02:00
placeholder : { id : 'compose_form.placeholder' , defaultMessage : 'What is on your mind?' } ,
2018-08-29 15:26:24 +02: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 23:02:09 +02:00
spoiler _placeholder : { id : 'compose_form.spoiler_placeholder' , defaultMessage : 'Write your warning here' } ,
2018-08-29 15:26:24 +02:00
} ) ;
2019-04-20 21:28:03 +02:00
export default @ injectIntl
class ComposeForm extends ImmutablePureComponent {
2017-12-24 07:16:45 +01:00
2019-04-20 21:28:03 +02:00
static contextTypes = {
router : PropTypes . object ,
} ;
2017-12-24 07:16:45 +01:00
2019-04-20 21:28:03 +02:00
static propTypes = {
intl : PropTypes . object . isRequired ,
2019-04-21 19:07:48 +02:00
text : PropTypes . string ,
suggestions : ImmutablePropTypes . list ,
spoiler : PropTypes . bool ,
privacy : PropTypes . string ,
spoilerText : PropTypes . string ,
2019-04-20 21:28:03 +02:00
focusDate : PropTypes . instanceOf ( Date ) ,
caretPosition : PropTypes . number ,
2019-04-21 19:07:48 +02:00
preselectDate : PropTypes . instanceOf ( Date ) ,
2019-04-20 21:28:03 +02:00
isSubmitting : PropTypes . bool ,
isChangingUpload : PropTypes . bool ,
isUploading : PropTypes . bool ,
2019-04-21 19:07:48 +02:00
onChange : PropTypes . func ,
onSubmit : PropTypes . func ,
onClearSuggestions : PropTypes . func ,
onFetchSuggestions : PropTypes . func ,
onSuggestionSelected : PropTypes . func ,
onChangeSpoilerText : PropTypes . func ,
onPaste : PropTypes . func ,
onPickEmoji : PropTypes . func ,
showSearch : PropTypes . bool ,
anyMedia : PropTypes . bool ,
2019-06-16 16:02:26 +02:00
singleColumn : PropTypes . bool ,
2019-04-21 19:07:48 +02:00
advancedOptions : ImmutablePropTypes . map ,
2019-04-20 21:28:03 +02:00
layout : PropTypes . string ,
media : ImmutablePropTypes . list ,
sideArm : PropTypes . string ,
sensitive : PropTypes . bool ,
spoilersAlwaysOn : PropTypes . bool ,
mediaDescriptionConfirmation : PropTypes . bool ,
preselectOnReply : PropTypes . bool ,
onChangeSpoilerness : PropTypes . func ,
onChangeVisibility : PropTypes . func ,
2019-04-21 19:07:48 +02:00
onPaste : PropTypes . func ,
2019-04-20 21:28:03 +02:00
onMediaDescriptionConfirm : PropTypes . func ,
} ;
2017-12-24 07:16:45 +01:00
2019-04-21 19:07:48 +02:00
static defaultProps = {
showSearch : false ,
} ;
2017-12-24 07:16:45 +01:00
2019-04-21 19:07:48 +02:00
handleChange = ( e ) => {
this . props . onChange ( e . target . value ) ;
2019-04-20 21:28:03 +02:00
}
2017-12-24 07:16:45 +01:00
2020-11-30 12:09:34 +01:00
getFulltextForCharacterCounting = ( ) => {
return [
this . props . spoiler ? this . props . spoilerText : '' ,
countableText ( this . props . text ) ,
this . props . advancedOptions && this . props . advancedOptions . get ( 'do_not_federate' ) ? ' 👁️' : ''
] . join ( '' ) ;
}
canSubmit = ( ) => {
const { isSubmitting , isChangingUpload , isUploading , anyMedia } = this . props ;
const fulltext = this . getFulltextForCharacterCounting ( ) ;
return ! ( isSubmitting || isUploading || isChangingUpload || length ( fulltext ) > maxChars || ( ! fulltext . trim ( ) . length && ! anyMedia ) ) ;
}
2020-02-14 20:19:35 +01:00
handleSubmit = ( overriddenVisibility = null ) => {
2017-12-24 07:16:45 +01:00
const {
2018-01-03 21:36:21 +01:00
onSubmit ,
2018-08-29 15:26:24 +02:00
media ,
mediaDescriptionConfirmation ,
onMediaDescriptionConfirm ,
2020-02-14 20:19:35 +01:00
onChangeVisibility ,
2017-12-24 07:16:45 +01:00
} = this . props ;
2020-11-30 12:09:34 +01:00
if ( this . props . text !== this . textarea . value ) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
this . props . onChange ( this . textarea . value ) ;
2017-12-24 07:16:45 +01:00
}
2020-11-30 12:09:34 +01:00
if ( ! this . canSubmit ( ) ) {
2018-04-02 17:22:32 +02:00
return ;
}
2018-08-29 15:26:24 +02:00
// Submit unless there are media with missing descriptions
if ( mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media . some ( item => ! item . get ( 'description' ) ) ) {
2019-08-20 11:28:17 +02:00
const firstWithoutDescription = media . find ( item => ! item . get ( 'description' ) ) ;
2020-03-14 12:04:55 +01:00
onMediaDescriptionConfirm ( this . context . router ? this . context . router . history : null , firstWithoutDescription . get ( 'id' ) , overriddenVisibility ) ;
2018-08-29 15:26:24 +02:00
} else if ( onSubmit ) {
2020-02-14 20:19:35 +01:00
if ( onChangeVisibility && overriddenVisibility ) {
onChangeVisibility ( overriddenVisibility ) ;
}
2018-12-13 16:50:37 +01:00
onSubmit ( this . context . router ? this . context . router . history : null ) ;
2017-12-24 07:16:45 +01:00
}
2019-04-20 21:28:03 +02:00
}
2017-12-24 07:16:45 +01:00
2019-04-21 19:07:48 +02:00
// Changes the text value of the spoiler.
handleChangeSpoiler = ( { target : { value } } ) => {
const { onChangeSpoilerText } = this . props ;
if ( onChangeSpoilerText ) {
onChangeSpoilerText ( value ) ;
}
}
2019-06-07 17:15:18 +02:00
setRef = c => {
this . composeForm = c ;
} ;
2019-04-21 19:07:48 +02:00
// Inserts an emoji at the caret.
handleEmoji = ( data ) => {
const { textarea : { selectionStart } } = this ;
const { onPickEmoji } = this . props ;
if ( onPickEmoji ) {
onPickEmoji ( selectionStart , data ) ;
}
}
// Handles the secondary submit button.
handleSecondarySubmit = ( ) => {
const {
sideArm ,
} = this . props ;
2020-02-14 20:19:35 +01:00
this . handleSubmit ( sideArm === 'none' ? null : sideArm ) ;
2019-04-21 19:07:48 +02:00
}
// Selects a suggestion from the autofill.
onSuggestionSelected = ( tokenStart , token , value ) => {
2019-04-11 17:18:55 +02:00
this . props . onSuggestionSelected ( tokenStart , token , value , [ 'text' ] ) ;
}
onSpoilerSuggestionSelected = ( tokenStart , token , value ) => {
this . props . onSuggestionSelected ( tokenStart , token , value , [ 'spoiler_text' ] ) ;
2019-04-21 19:07:48 +02:00
}
2020-05-25 20:04:06 +02:00
handleKeyDown = ( e ) => {
if ( e . keyCode === 13 && ( e . ctrlKey || e . metaKey ) ) {
2020-03-14 12:40:07 +01:00
this . handleSubmit ( ) ;
}
2020-05-25 20:04:06 +02:00
if ( e . keyCode == 13 && e . altKey ) {
2020-03-14 12:40:07 +01:00
this . handleSecondarySubmit ( ) ;
}
2019-04-21 19:07:48 +02:00
}
2017-12-24 07:16:45 +01:00
// Sets a reference to the textarea.
2019-04-21 12:44:30 +02:00
setAutosuggestTextarea = ( textareaComponent ) => {
2018-01-03 21:36:21 +01:00
if ( textareaComponent ) {
this . textarea = textareaComponent . textarea ;
}
2019-04-20 21:28:03 +02:00
}
2018-08-18 20:53:46 +02:00
// Sets a reference to the CW field.
2019-04-20 21:28:03 +02:00
handleRefSpoilerText = ( spoilerComponent ) => {
2018-08-18 20:53:46 +02:00
if ( spoilerComponent ) {
2019-04-11 17:18:55 +02:00
this . spoilerText = spoilerComponent . input ;
2018-08-18 20:53:46 +02:00
}
}
2017-12-24 07:16:45 +01:00
2019-06-05 15:29:45 +02:00
handleFocus = ( ) => {
2019-06-16 16:02:26 +02:00
if ( this . composeForm && ! this . props . singleColumn ) {
2019-07-06 18:18:08 +02:00
const { left , right } = this . composeForm . getBoundingClientRect ( ) ;
if ( left < 0 || right > ( window . innerWidth || document . documentElement . clientWidth ) ) {
this . composeForm . scrollIntoView ( ) ;
}
2019-06-07 17:15:18 +02:00
}
2019-06-05 15:29:45 +02:00
}
2021-03-24 10:19:07 +01:00
componentDidMount ( ) {
this . _updateFocusAndSelection ( { } ) ;
}
componentDidUpdate ( prevProps ) {
this . _updateFocusAndSelection ( prevProps ) ;
}
2017-12-24 07:16:45 +01: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.
2021-03-24 10:19:07 +01:00
_updateFocusAndSelection = ( prevProps ) => {
2017-12-24 07:16:45 +01:00
const {
textarea ,
2018-08-18 20:53:46 +02:00
spoilerText ,
2017-12-24 07:16:45 +01:00
} = this ;
const {
2018-01-03 21:36:21 +01:00
focusDate ,
2018-05-22 21:09:07 +02:00
caretPosition ,
2018-01-03 21:36:21 +01:00
isSubmitting ,
preselectDate ,
text ,
2018-09-28 20:58:03 +02:00
preselectOnReply ,
2019-06-16 16:02:26 +02:00
singleColumn ,
2017-12-24 07:16:45 +01:00
} = this . props ;
let selectionEnd , selectionStart ;
// Caret/selection handling.
2018-05-22 20:21:09 +02:00
if ( focusDate !== prevProps . focusDate ) {
2017-12-24 07:16:45 +01:00
switch ( true ) {
2018-09-28 20:58:03 +02:00
case preselectDate !== prevProps . preselectDate && preselectOnReply :
2017-12-24 07:16:45 +01:00
selectionStart = text . search ( /\s/ ) + 1 ;
selectionEnd = text . length ;
break ;
2018-05-22 21:09:07 +02:00
case ! isNaN ( caretPosition ) && caretPosition !== null :
selectionStart = selectionEnd = caretPosition ;
2017-12-24 07:16:45 +01:00
break ;
default :
selectionStart = selectionEnd = text . length ;
}
2018-01-03 21:36:21 +01:00
if ( textarea ) {
textarea . setSelectionRange ( selectionStart , selectionEnd ) ;
textarea . focus ( ) ;
2019-06-16 16:02:26 +02:00
if ( ! singleColumn ) textarea . scrollIntoView ( ) ;
2018-01-03 21:36:21 +01:00
}
2017-12-24 07:16:45 +01:00
// Refocuses the textarea after submitting.
2018-01-03 21:36:21 +01:00
} else if ( textarea && prevProps . isSubmitting && ! isSubmitting ) {
2017-12-24 07:16:45 +01:00
textarea . focus ( ) ;
2018-08-18 20:53:46 +02:00
} else if ( this . props . spoiler !== prevProps . spoiler ) {
if ( this . props . spoiler ) {
if ( spoilerText ) {
spoilerText . focus ( ) ;
}
} else {
if ( textarea ) {
textarea . focus ( ) ;
}
}
2017-12-24 07:16:45 +01:00
}
}
2019-04-21 12:44:30 +02:00
2017-12-24 07:16:45 +01:00
render ( ) {
const {
2018-01-03 21:36:21 +01:00
handleEmoji ,
handleSecondarySubmit ,
handleSelect ,
handleSubmit ,
handleRefTextarea ,
2019-04-20 21:28:03 +02:00
} = this ;
2017-12-24 07:16:45 +01:00
const {
2018-01-06 03:23:06 +01:00
advancedOptions ,
2017-12-24 07:16:45 +01:00
intl ,
2018-01-03 21:36:21 +01:00
isSubmitting ,
layout ,
onChangeSpoilerness ,
onChangeVisibility ,
onClearSuggestions ,
onFetchSuggestions ,
2019-04-21 19:07:48 +02:00
onPaste ,
2018-01-03 21:36:21 +01:00
privacy ,
sensitive ,
showSearch ,
sideArm ,
spoiler ,
spoilerText ,
suggestions ,
2018-08-22 15:58:57 +02:00
spoilersAlwaysOn ,
2017-12-24 07:16:45 +01:00
} = this . props ;
2020-11-30 12:09:34 +01:00
const countText = this . getFulltextForCharacterCounting ( ) ;
2019-08-19 21:41:41 +02:00
2017-12-24 07:16:45 +01:00
return (
2019-06-25 22:57:56 +02:00
< div className = 'composer' >
2019-04-20 22:05:09 +02:00
< WarningContainer / >
2019-04-20 22:21:28 +02:00
< ReplyIndicatorContainer / >
2019-04-20 22:05:09 +02:00
2019-06-25 22:57:56 +02:00
< div className = { ` composer--spoiler ${ spoiler ? 'composer--spoiler--visible' : '' } ` } ref = { this . setRef } >
2019-04-11 17:18:55 +02:00
< AutosuggestInput
placeholder = { intl . formatMessage ( messages . spoiler _placeholder ) }
value = { spoilerText }
onChange = { this . handleChangeSpoiler }
2020-05-25 20:04:06 +02:00
onKeyDown = { this . handleKeyDown }
2019-04-11 17:18:55 +02:00
disabled = { ! spoiler }
ref = { this . handleRefSpoilerText }
suggestions = { this . props . suggestions }
onSuggestionsFetchRequested = { onFetchSuggestions }
onSuggestionsClearRequested = { onClearSuggestions }
onSuggestionSelected = { this . onSpoilerSuggestionSelected }
searchTokens = { [ ':' ] }
2019-05-03 18:54:06 +02:00
id = 'glitch.composer.spoiler.input'
className = 'spoiler-input__input'
2019-06-06 12:14:11 +02:00
autoFocus = { false }
2019-04-11 17:18:55 +02:00
/ >
2019-04-20 23:02:09 +02:00
< / d i v >
2019-04-20 22:05:09 +02:00
2019-06-02 10:05:54 +02:00
< AutosuggestTextarea
ref = { this . setAutosuggestTextarea }
placeholder = { intl . formatMessage ( messages . placeholder ) }
disabled = { isSubmitting }
value = { this . props . text }
onChange = { this . handleChange }
2020-05-25 20:04:06 +02:00
onKeyDown = { this . handleKeyDown }
2019-06-02 10:05:54 +02:00
suggestions = { this . props . suggestions }
onFocus = { this . handleFocus }
onSuggestionsFetchRequested = { onFetchSuggestions }
onSuggestionsClearRequested = { onClearSuggestions }
onSuggestionSelected = { this . onSuggestionSelected }
onPaste = { onPaste }
autoFocus = { ! showSearch && ! isMobile ( window . innerWidth , layout ) }
>
2019-06-13 17:07:43 +02:00
< EmojiPicker onPickEmoji = { handleEmoji } / >
2019-06-02 10:05:54 +02:00
< TextareaIcons advancedOptions = { advancedOptions } / >
< div className = 'compose-form__modifiers' >
< UploadFormContainer / >
< PollFormContainer / >
< / d i v >
< / A u t o s u g g e s t T e x t a r e a >
2019-04-21 12:44:30 +02:00
2019-08-19 21:41:41 +02:00
< div className = 'composer--options-wrapper' >
< OptionsContainer
advancedOptions = { advancedOptions }
disabled = { isSubmitting }
onChangeVisibility = { onChangeVisibility }
onToggleSpoiler = { spoilersAlwaysOn ? null : onChangeSpoilerness }
onUpload = { onPaste }
privacy = { privacy }
sensitive = { sensitive || ( spoilersAlwaysOn && spoilerText && spoilerText . length > 0 ) }
spoiler = { spoilersAlwaysOn ? ( spoilerText && spoilerText . length > 0 ) : spoiler }
/ >
< div className = 'compose--counter-wrapper' >
< CharacterCounter text = { countText } max = { maxChars } / >
< / d i v >
< / d i v >
2019-04-21 12:44:30 +02:00
2019-04-21 18:48:33 +02:00
< Publisher
2019-08-19 21:41:41 +02:00
countText = { countText }
2020-11-30 12:09:34 +01:00
disabled = { ! this . canSubmit ( ) }
2018-01-03 21:36:21 +01:00
onSecondarySubmit = { handleSecondarySubmit }
onSubmit = { handleSubmit }
2017-12-24 07:16:45 +01:00
privacy = { privacy }
sideArm = { sideArm }
/ >
< / d i v >
) ;
}
}