1 import React
from 'react';
2 import CharacterCounter
from './character_counter';
3 import Button
from '../../../components/button';
4 import ImmutablePropTypes
from 'react-immutable-proptypes';
5 import PropTypes
from 'prop-types';
6 import ReplyIndicatorContainer
from '../containers/reply_indicator_container';
7 import AutosuggestTextarea
from '../../../components/autosuggest_textarea';
8 import { debounce
} from 'lodash';
9 import UploadButtonContainer
from '../containers/upload_button_container';
10 import { defineMessages
, injectIntl
} from 'react-intl';
11 import Collapsable
from '../../../components/collapsable';
12 import SpoilerButtonContainer
from '../containers/spoiler_button_container';
13 import PrivacyDropdownContainer
from '../containers/privacy_dropdown_container';
14 import SensitiveButtonContainer
from '../containers/sensitive_button_container';
15 import EmojiPickerDropdown
from '../containers/emoji_picker_dropdown_container';
16 import UploadFormContainer
from '../containers/upload_form_container';
17 import WarningContainer
from '../containers/warning_container';
18 import { isMobile
} from '../../../is_mobile';
19 import ImmutablePureComponent
from 'react-immutable-pure-component';
20 import { length
} from 'stringz';
21 import { countableText
} from '../util/counter';
23 const messages
= defineMessages({
24 placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
25 spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
26 publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
27 publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
31 export default class ComposeForm
extends ImmutablePureComponent
{
34 intl: PropTypes
.object
.isRequired
,
35 text: PropTypes
.string
.isRequired
,
36 suggestion_token: PropTypes
.string
,
37 suggestions: ImmutablePropTypes
.list
,
38 spoiler: PropTypes
.bool
,
39 privacy: PropTypes
.string
,
40 spoiler_text: PropTypes
.string
,
41 focusDate: PropTypes
.instanceOf(Date
),
42 preselectDate: PropTypes
.instanceOf(Date
),
43 is_submitting: PropTypes
.bool
,
44 is_uploading: PropTypes
.bool
,
46 onChange: PropTypes
.func
.isRequired
,
47 onSubmit: PropTypes
.func
.isRequired
,
48 onClearSuggestions: PropTypes
.func
.isRequired
,
49 onFetchSuggestions: PropTypes
.func
.isRequired
,
50 onSuggestionSelected: PropTypes
.func
.isRequired
,
51 onChangeSpoilerText: PropTypes
.func
.isRequired
,
52 onPaste: PropTypes
.func
.isRequired
,
53 onPickEmoji: PropTypes
.func
.isRequired
,
54 showSearch: PropTypes
.bool
,
57 static defaultProps
= {
61 handleChange
= (e
) => {
62 this.props
.onChange(e
.target
.value
);
65 handleKeyDown
= (e
) => {
66 if (e
.keyCode
=== 13 && (e
.ctrlKey
|| e
.metaKey
)) {
71 handleSubmit
= () => {
72 if (this.props
.text
!== this.autosuggestTextarea
.textarea
.value
) {
73 // Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
74 // Update the state to match the current text
75 this.props
.onChange(this.autosuggestTextarea
.textarea
.value
);
78 this.props
.onSubmit();
81 onSuggestionsClearRequested
= () => {
82 this.props
.onClearSuggestions();
85 onSuggestionsFetchRequested
= debounce((token
) => {
86 this.props
.onFetchSuggestions(token
);
87 }, 500, { trailing: true })
89 onSuggestionSelected
= (tokenStart
, token
, value
) => {
90 this._restoreCaret
= null;
91 this.props
.onSuggestionSelected(tokenStart
, token
, value
);
94 handleChangeSpoilerText
= (e
) => {
95 this.props
.onChangeSpoilerText(e
.target
.value
);
98 componentWillReceiveProps (nextProps
) {
99 // If this is the update where we've finished uploading,
100 // save the last caret position so we can restore it below!
101 if (!nextProps
.is_uploading
&& this.props
.is_uploading
) {
102 this._restoreCaret
= this.autosuggestTextarea
.textarea
.selectionStart
;
106 componentDidUpdate (prevProps
) {
107 // This statement does several things:
108 // - If we're beginning a reply, and,
109 // - Replying to zero or one users, places the cursor at the end of the textbox.
110 // - Replying to more than one user, selects any usernames past the first;
111 // this provides a convenient shortcut to drop everyone else from the conversation.
112 // - If we've just finished uploading an image, and have a saved caret position,
113 // restores the cursor to that position after the text changes!
114 if (this.props
.focusDate
!== prevProps
.focusDate
|| (prevProps
.is_uploading
&& !this.props
.is_uploading
&& typeof this._restoreCaret
=== 'number')) {
115 let selectionEnd
, selectionStart
;
117 if (this.props
.preselectDate
!== prevProps
.preselectDate
) {
118 selectionEnd
= this.props
.text
.length
;
119 selectionStart
= this.props
.text
.search(/\s/) + 1;
120 } else if (typeof this._restoreCaret
=== 'number') {
121 selectionStart
= this._restoreCaret
;
122 selectionEnd
= this._restoreCaret
;
124 selectionEnd
= this.props
.text
.length
;
125 selectionStart
= selectionEnd
;
128 this.autosuggestTextarea
.textarea
.setSelectionRange(selectionStart
, selectionEnd
);
129 this.autosuggestTextarea
.textarea
.focus();
130 } else if(prevProps
.is_submitting
&& !this.props
.is_submitting
) {
131 this.autosuggestTextarea
.textarea
.focus();
135 setAutosuggestTextarea
= (c
) => {
136 this.autosuggestTextarea
= c
;
139 handleEmojiPick
= (data
) => {
140 const position
= this.autosuggestTextarea
.textarea
.selectionStart
;
141 const emojiChar
= data
.native;
142 this._restoreCaret
= position
+ emojiChar
.length
+ 1;
143 this.props
.onPickEmoji(position
, data
);
147 const { intl
, onPaste
, showSearch
} = this.props
;
148 const disabled
= this.props
.is_submitting
;
149 const text
= [this.props
.spoiler_text
, countableText(this.props
.text
)].join('');
151 let publishText
= '';
153 if (this.props
.privacy
=== 'private' || this.props
.privacy
=== 'direct') {
154 publishText
= <span className
='compose-form__publish-private'><i className
='fa fa-lock' /> {intl
.formatMessage(messages
.publish
)}</span
>;
156 publishText
= this.props
.privacy
!== 'unlisted' ? intl
.formatMessage(messages
.publishLoud
, { publish: intl
.formatMessage(messages
.publish
) }) : intl
.formatMessage(messages
.publish
);
160 <div className
='compose-form'>
161 <Collapsable isVisible
={this.props
.spoiler
} fullHeight
={50}>
162 <div className
='spoiler-input'>
164 <span style
={{ display: 'none' }}>{intl
.formatMessage(messages
.spoiler_placeholder
)}</span
>
165 <input placeholder
={intl
.formatMessage(messages
.spoiler_placeholder
)} value
={this.props
.spoiler_text
} onChange
={this.handleChangeSpoilerText
} onKeyDown
={this.handleKeyDown
} type
='text' className
='spoiler-input__input' id
='cw-spoiler-input' />
172 <ReplyIndicatorContainer
/>
174 <div className
='compose-form__autosuggest-wrapper'>
176 ref
={this.setAutosuggestTextarea
}
177 placeholder
={intl
.formatMessage(messages
.placeholder
)}
179 value
={this.props
.text
}
180 onChange
={this.handleChange
}
181 suggestions
={this.props
.suggestions
}
182 onKeyDown
={this.handleKeyDown
}
183 onSuggestionsFetchRequested
={this.onSuggestionsFetchRequested
}
184 onSuggestionsClearRequested
={this.onSuggestionsClearRequested
}
185 onSuggestionSelected
={this.onSuggestionSelected
}
187 autoFocus
={!showSearch
&& !isMobile(window
.innerWidth
)}
190 <EmojiPickerDropdown onPickEmoji
={this.handleEmojiPick
} />
193 <div className
='compose-form__modifiers'>
194 <UploadFormContainer
/>
197 <div className
='compose-form__buttons-wrapper'>
198 <div className
='compose-form__buttons'>
199 <UploadButtonContainer
/>
200 <PrivacyDropdownContainer
/>
201 <SensitiveButtonContainer
/>
202 <SpoilerButtonContainer
/>
205 <div className
='compose-form__publish'>
206 <div className
='character-counter__wrapper'><CharacterCounter max
={500} text
={text
} /></div>
207 <div className
='compose-form__publish-button-wrapper'><Button text
={publishText
} onClick
={this.handleSubmit
} disabled
={disabled
|| this.props
.is_uploading
|| length(text
) > 500 || (text
.length
!== 0 && text
.trim().length
=== 0)} block
/></div>