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
, FormattedMessage
} from 'react-intl';
11 import Toggle
from 'react-toggle';
12 import Collapsable
from '../../../components/collapsable';
13 import SpoilerButtonContainer
from '../containers/spoiler_button_container';
14 import PrivacyDropdownContainer
from '../containers/privacy_dropdown_container';
15 import SensitiveButtonContainer
from '../containers/sensitive_button_container';
16 import EmojiPickerDropdown
from './emoji_picker_dropdown';
17 import UploadFormContainer
from '../containers/upload_form_container';
18 import TextIconButton
from './text_icon_button';
19 import WarningContainer
from '../containers/warning_container';
20 import ImmutablePureComponent
from 'react-immutable-pure-component';
22 const messages
= defineMessages({
23 placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
24 spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
25 publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
28 class ComposeForm
extends ImmutablePureComponent
{
31 intl: PropTypes
.object
.isRequired
,
32 text: PropTypes
.string
.isRequired
,
33 suggestion_token: PropTypes
.string
,
34 suggestions: ImmutablePropTypes
.list
,
35 spoiler: PropTypes
.bool
,
36 privacy: PropTypes
.string
,
37 spoiler_text: PropTypes
.string
,
38 focusDate: PropTypes
.instanceOf(Date
),
39 preselectDate: PropTypes
.instanceOf(Date
),
40 is_submitting: PropTypes
.bool
,
41 is_uploading: PropTypes
.bool
,
43 onChange: PropTypes
.func
.isRequired
,
44 onSubmit: PropTypes
.func
.isRequired
,
45 onClearSuggestions: PropTypes
.func
.isRequired
,
46 onFetchSuggestions: PropTypes
.func
.isRequired
,
47 onSuggestionSelected: PropTypes
.func
.isRequired
,
48 onChangeSpoilerText: PropTypes
.func
.isRequired
,
49 onPaste: PropTypes
.func
.isRequired
,
50 onPickEmoji: PropTypes
.func
.isRequired
,
51 showSearch: PropTypes
.bool
,
54 static defaultProps
= {
58 handleChange
= (e
) => {
59 this.props
.onChange(e
.target
.value
);
62 handleKeyDown
= (e
) => {
63 if (e
.keyCode
=== 13 && (e
.ctrlKey
|| e
.metaKey
)) {
68 handleSubmit
= () => {
69 this.autosuggestTextarea
.reset();
70 this.props
.onSubmit();
73 onSuggestionsClearRequested
= () => {
74 this.props
.onClearSuggestions();
77 onSuggestionsFetchRequested
= (token
) => {
78 this.props
.onFetchSuggestions(token
);
81 onSuggestionSelected
= (tokenStart
, token
, value
) => {
82 this._restoreCaret
= null;
83 this.props
.onSuggestionSelected(tokenStart
, token
, value
);
86 handleChangeSpoilerText
= (e
) => {
87 this.props
.onChangeSpoilerText(e
.target
.value
);
90 componentWillReceiveProps (nextProps
) {
91 // If this is the update where we've finished uploading,
92 // save the last caret position so we can restore it below!
93 if (!nextProps
.is_uploading
&& this.props
.is_uploading
) {
94 this._restoreCaret
= this.autosuggestTextarea
.textarea
.selectionStart
;
98 componentDidUpdate (prevProps
) {
99 // This statement does several things:
100 // - If we're beginning a reply, and,
101 // - Replying to zero or one users, places the cursor at the end of the textbox.
102 // - Replying to more than one user, selects any usernames past the first;
103 // this provides a convenient shortcut to drop everyone else from the conversation.
104 // - If we've just finished uploading an image, and have a saved caret position,
105 // restores the cursor to that position after the text changes!
106 if (this.props
.focusDate
!== prevProps
.focusDate
|| (prevProps
.is_uploading
&& !this.props
.is_uploading
&& typeof this._restoreCaret
=== 'number')) {
107 let selectionEnd
, selectionStart
;
109 if (this.props
.preselectDate
!== prevProps
.preselectDate
) {
110 selectionEnd
= this.props
.text
.length
;
111 selectionStart
= this.props
.text
.search(/\s/) + 1;
112 } else if (typeof this._restoreCaret
=== 'number') {
113 selectionStart
= this._restoreCaret
;
114 selectionEnd
= this._restoreCaret
;
116 selectionEnd
= this.props
.text
.length
;
117 selectionStart
= selectionEnd
;
120 this.autosuggestTextarea
.textarea
.setSelectionRange(selectionStart
, selectionEnd
);
121 this.autosuggestTextarea
.textarea
.focus();
125 setAutosuggestTextarea
= (c
) => {
126 this.autosuggestTextarea
= c
;
129 handleEmojiPick
= (data
) => {
130 const position
= this.autosuggestTextarea
.textarea
.selectionStart
;
131 this._restoreCaret
= position
+ data
.shortname
.length
+ 1;
132 this.props
.onPickEmoji(position
, data
);
136 const { intl
, onPaste
, showSearch
} = this.props
;
137 const disabled
= this.props
.is_submitting
;
138 const text
= [this.props
.spoiler_text
, this.props
.text
].join('');
140 let publishText
= '';
141 let reply_to_other
= false;
143 if (this.props
.privacy
=== 'private' || this.props
.privacy
=== 'direct') {
144 publishText
= <span className
='compose-form__publish-private'><i className
='fa fa-lock' /> {intl
.formatMessage(messages
.publish
)}</span
>;
146 publishText
= intl
.formatMessage(messages
.publish
) + (this.props
.privacy
!== 'unlisted' ? '!' : '');
150 <div className
='compose-form'>
151 <Collapsable isVisible
={this.props
.spoiler
} fullHeight
={50}>
152 <div className
="spoiler-input">
153 <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'/>
159 <ReplyIndicatorContainer
/>
161 <div className
='compose-form__autosuggest-wrapper'>
163 ref
={this.setAutosuggestTextarea
}
164 placeholder
={intl
.formatMessage(messages
.placeholder
)}
166 value
={this.props
.text
}
167 onChange
={this.handleChange
}
168 suggestions
={this.props
.suggestions
}
169 onKeyDown
={this.handleKeyDown
}
170 onSuggestionsFetchRequested
={this.onSuggestionsFetchRequested
}
171 onSuggestionsClearRequested
={this.onSuggestionsClearRequested
}
172 onSuggestionSelected
={this.onSuggestionSelected
}
174 autoFocus
={!showSearch
}
177 <EmojiPickerDropdown onPickEmoji
={this.handleEmojiPick
} />
180 <div className
='compose-form__modifiers'>
181 <UploadFormContainer
/>
184 <div className
='compose-form__buttons-wrapper'>
185 <div className
='compose-form__buttons'>
186 <UploadButtonContainer
/>
187 <PrivacyDropdownContainer
/>
188 <SensitiveButtonContainer
/>
189 <SpoilerButtonContainer
/>
192 <div className
='compose-form__publish'>
193 <div className
='character-counter__wrapper'><CharacterCounter max
={500} text
={text
} /></div>
194 <div className
='compose-form__publish-button-wrapper'><Button text
={publishText
} onClick
={this.handleSubmit
} disabled
={disabled
|| this.props
.is_uploading
|| text
.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length
> 500 || (text
.length
!==0 && text
.trim().length
=== 0)} block
/></div>
203 export default injectIntl(ComposeForm
);