diff options
Diffstat (limited to 'app/javascript/mastodon/features/compose/components')
-rw-r--r-- | app/javascript/mastodon/features/compose/components/action_bar.jsx (renamed from app/javascript/mastodon/features/compose/components/action_bar.js) | 7 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/autosuggest_account.jsx (renamed from app/javascript/mastodon/features/compose/components/autosuggest_account.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/character_counter.jsx (renamed from app/javascript/mastodon/features/compose/components/character_counter.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/compose_form.jsx (renamed from app/javascript/mastodon/features/compose/components/compose_form.js) | 37 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx (renamed from app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js) | 46 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/language_dropdown.jsx (renamed from app/javascript/mastodon/features/compose/components/language_dropdown.js) | 37 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/navigation_bar.jsx (renamed from app/javascript/mastodon/features/compose/components/navigation_bar.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/poll_button.jsx (renamed from app/javascript/mastodon/features/compose/components/poll_button.js) | 6 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/poll_form.jsx (renamed from app/javascript/mastodon/features/compose/components/poll_form.js) | 29 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx (renamed from app/javascript/mastodon/features/compose/components/privacy_dropdown.js) | 35 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/reply_indicator.jsx (renamed from app/javascript/mastodon/features/compose/components/reply_indicator.js) | 7 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/search.js | 147 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/search.jsx | 335 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/search_results.jsx (renamed from app/javascript/mastodon/features/compose/components/search_results.js) | 5 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/text_icon_button.jsx (renamed from app/javascript/mastodon/features/compose/components/text_icon_button.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/upload.jsx (renamed from app/javascript/mastodon/features/compose/components/upload.js) | 15 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/upload_button.jsx (renamed from app/javascript/mastodon/features/compose/components/upload_button.js) | 10 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/upload_form.jsx (renamed from app/javascript/mastodon/features/compose/components/upload_form.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/upload_progress.jsx (renamed from app/javascript/mastodon/features/compose/components/upload_progress.js) | 0 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/warning.jsx (renamed from app/javascript/mastodon/features/compose/components/warning.js) | 0 |
20 files changed, 464 insertions, 252 deletions
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.jsx index ceed928bf..13760582e 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.js +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -11,6 +11,7 @@ const messages = defineMessages({ follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, @@ -19,7 +20,6 @@ const messages = defineMessages({ bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, }); -export default @injectIntl class ActionBar extends React.PureComponent { static propTypes = { @@ -30,7 +30,7 @@ class ActionBar extends React.PureComponent { handleLogout = () => { this.props.onLogout(); - } + }; render () { const { intl } = this.props; @@ -45,6 +45,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); @@ -63,3 +64,5 @@ class ActionBar extends React.PureComponent { } } + +export default injectIntl(ActionBar); diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.js b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx index 1451be0e6..1451be0e6 100644 --- a/app/javascript/mastodon/features/compose/components/autosuggest_account.js +++ b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx diff --git a/app/javascript/mastodon/features/compose/components/character_counter.js b/app/javascript/mastodon/features/compose/components/character_counter.jsx index 0ecfc9141..0ecfc9141 100644 --- a/app/javascript/mastodon/features/compose/components/character_counter.js +++ b/app/javascript/mastodon/features/compose/components/character_counter.jsx diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.jsx index ebdd55d33..a40eb87e3 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -32,7 +32,6 @@ const messages = defineMessages({ saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, }); -export default @injectIntl class ComposeForm extends ImmutablePureComponent { static contextTypes = { @@ -65,6 +64,7 @@ class ComposeForm extends ImmutablePureComponent { anyMedia: PropTypes.bool, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, + lang: PropTypes.string, }; static defaultProps = { @@ -73,17 +73,17 @@ class ComposeForm extends ImmutablePureComponent { handleChange = (e) => { this.props.onChange(e.target.value); - } + }; handleKeyDown = (e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { this.handleSubmit(); } - } + }; getFulltextForCharacterCounting = () => { return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join(''); - } + }; canSubmit = () => { const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; @@ -91,7 +91,7 @@ class ComposeForm extends ImmutablePureComponent { const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0; return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia)); - } + }; handleSubmit = (e) => { if (this.props.text !== this.autosuggestTextarea.textarea.value) { @@ -109,27 +109,27 @@ class ComposeForm extends ImmutablePureComponent { if (e) { e.preventDefault(); } - } + }; onSuggestionsClearRequested = () => { this.props.onClearSuggestions(); - } + }; onSuggestionsFetchRequested = (token) => { this.props.onFetchSuggestions(token); - } + }; onSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); - } + }; onSpoilerSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); - } + }; handleChangeSpoilerText = (e) => { this.props.onChangeSpoilerText(e.target.value); - } + }; handleFocus = () => { if (this.composeForm && !this.props.singleColumn) { @@ -138,7 +138,7 @@ class ComposeForm extends ImmutablePureComponent { this.composeForm.scrollIntoView(); } } - } + }; componentDidMount () { this._updateFocusAndSelection({ }); @@ -184,15 +184,15 @@ class ComposeForm extends ImmutablePureComponent { this.autosuggestTextarea.textarea.focus(); } } - } + }; setAutosuggestTextarea = (c) => { this.autosuggestTextarea = c; - } + }; setSpoilerText = (c) => { this.spoilerText = c; - } + }; setRef = c => { this.composeForm = c; @@ -204,7 +204,7 @@ class ComposeForm extends ImmutablePureComponent { const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); this.props.onPickEmoji(position, data, needsSpace); - } + }; render () { const { intl, onPaste, autoFocus } = this.props; @@ -241,6 +241,8 @@ class ComposeForm extends ImmutablePureComponent { searchTokens={[':']} id='cw-spoiler-input' className='spoiler-input__input' + lang={this.props.lang} + spellCheck /> </div> @@ -258,6 +260,7 @@ class ComposeForm extends ImmutablePureComponent { onSuggestionSelected={this.onSuggestionSelected} onPaste={onPaste} autoFocus={autoFocus} + lang={this.props.lang} > <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> @@ -296,3 +299,5 @@ class ComposeForm extends ImmutablePureComponent { } } + +export default injectIntl(ComposeForm); diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx index 76c9cda81..4fb131b47 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx @@ -57,7 +57,7 @@ class ModifierPickerMenu extends React.PureComponent { handleClick = e => { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - } + }; componentWillReceiveProps (nextProps) { if (nextProps.active) { @@ -75,7 +75,7 @@ class ModifierPickerMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; attachListeners () { document.addEventListener('click', this.handleDocumentClick, false); @@ -89,7 +89,7 @@ class ModifierPickerMenu extends React.PureComponent { setRef = c => { this.node = c; - } + }; render () { const { active } = this.props; @@ -124,12 +124,12 @@ class ModifierPicker extends React.PureComponent { } else { this.props.onOpen(); } - } + }; handleSelect = modifier => { this.props.onChange(modifier); this.props.onClose(); - } + }; render () { const { active, modifier } = this.props; @@ -144,8 +144,7 @@ class ModifierPicker extends React.PureComponent { } -@injectIntl -class EmojiPickerMenu extends React.PureComponent { +class EmojiPickerMenuImpl extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -174,7 +173,7 @@ class EmojiPickerMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); @@ -198,7 +197,7 @@ class EmojiPickerMenu extends React.PureComponent { setRef = c => { this.node = c; - } + }; getI18n = () => { const { intl } = this.props; @@ -219,7 +218,7 @@ class EmojiPickerMenu extends React.PureComponent { custom: intl.formatMessage(messages.custom), }, }; - } + }; handleClick = (emoji, event) => { if (!emoji.native) { @@ -229,19 +228,19 @@ class EmojiPickerMenu extends React.PureComponent { this.props.onClose(); } this.props.onPick(emoji); - } + }; handleModifierOpen = () => { this.setState({ modifierOpen: true }); - } + }; handleModifierClose = () => { this.setState({ modifierOpen: false }); - } + }; handleModifierChange = modifier => { this.props.onSkinTone(modifier); - } + }; render () { const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; @@ -305,7 +304,8 @@ class EmojiPickerMenu extends React.PureComponent { } -export default @injectIntl +const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl); + class EmojiPickerDropdown extends React.PureComponent { static propTypes = { @@ -325,7 +325,7 @@ class EmojiPickerDropdown extends React.PureComponent { setRef = (c) => { this.dropdown = c; - } + }; onShowDropdown = () => { this.setState({ active: true }); @@ -342,11 +342,11 @@ class EmojiPickerDropdown extends React.PureComponent { this.setState({ loading: false, active: false }); }); } - } + }; onHideDropdown = () => { this.setState({ active: false }); - } + }; onToggle = (e) => { if (!this.state.loading && (!e.key || e.key === 'Enter')) { @@ -356,21 +356,21 @@ class EmojiPickerDropdown extends React.PureComponent { this.onShowDropdown(e); } } - } + }; handleKeyDown = e => { if (e.key === 'Escape') { this.onHideDropdown(); } - } + }; setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; render () { const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; @@ -409,3 +409,5 @@ class EmojiPickerDropdown extends React.PureComponent { } } + +export default injectIntl(EmojiPickerDropdown); diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.js b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index 2dd406b4b..08542e3a9 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx @@ -40,7 +40,7 @@ class LanguageDropdownMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); @@ -63,15 +63,15 @@ class LanguageDropdownMenu extends React.PureComponent { setRef = c => { this.node = c; - } + }; setListRef = c => { this.listNode = c; - } + }; handleSearchChange = ({ target }) => { this.setState({ searchValue: target.value }); - } + }; search () { const { languages, value, frequentlyUsedLanguages } = this.props; @@ -122,7 +122,7 @@ class LanguageDropdownMenu extends React.PureComponent { this.props.onClose(); this.props.onChange(value); - } + }; handleKeyDown = e => { const { onClose } = this.props; @@ -163,7 +163,7 @@ class LanguageDropdownMenu extends React.PureComponent { e.preventDefault(); e.stopPropagation(); } - } + }; handleSearchKeyDown = e => { const { onChange, onClose } = this.props; @@ -199,21 +199,21 @@ class LanguageDropdownMenu extends React.PureComponent { break; } - } + }; handleClear = () => { this.setState({ searchValue: '' }); - } + }; renderItem = lang => { const { value } = this.props; return ( - <div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}> - <span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> + <div key={lang[0]} role='option' tabIndex={0} data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}> + <span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> </div> ); - } + }; render () { const { intl } = this.props; @@ -237,7 +237,6 @@ class LanguageDropdownMenu extends React.PureComponent { } -export default @injectIntl class LanguageDropdown extends React.PureComponent { static propTypes = { @@ -259,7 +258,7 @@ class LanguageDropdown extends React.PureComponent { } this.setState({ open: !this.state.open }); - } + }; handleClose = () => { const { value, onClose } = this.props; @@ -270,24 +269,24 @@ class LanguageDropdown extends React.PureComponent { this.setState({ open: false }); onClose(value); - } + }; handleChange = value => { const { onChange } = this.props; onChange(value); - } + }; setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; handleOverlayEnter = (state) => { this.setState({ placement: state.placement }); - } + }; render () { const { value, intl, frequentlyUsedLanguages } = this.props; @@ -325,3 +324,5 @@ class LanguageDropdown extends React.PureComponent { } } + +export default injectIntl(LanguageDropdown); diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx index be979af50..be979af50 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx diff --git a/app/javascript/mastodon/features/compose/components/poll_button.js b/app/javascript/mastodon/features/compose/components/poll_button.jsx index 76f96bfa4..6ad689ccc 100644 --- a/app/javascript/mastodon/features/compose/components/poll_button.js +++ b/app/javascript/mastodon/features/compose/components/poll_button.jsx @@ -13,8 +13,6 @@ const iconStyle = { lineHeight: '27px', }; -export default -@injectIntl class PollButton extends React.PureComponent { static propTypes = { @@ -27,7 +25,7 @@ class PollButton extends React.PureComponent { handleClick = () => { this.props.onClick(); - } + }; render () { const { intl, active, unavailable, disabled } = this.props; @@ -53,3 +51,5 @@ class PollButton extends React.PureComponent { } } + +export default injectIntl(PollButton); diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.jsx index 3aa527161..f81d7355a 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.jsx @@ -20,11 +20,11 @@ const messages = defineMessages({ days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, }); -@injectIntl -class Option extends React.PureComponent { +class OptionIntl extends React.PureComponent { static propTypes = { title: PropTypes.string.isRequired, + lang: PropTypes.string, index: PropTypes.number.isRequired, isPollMultiple: PropTypes.bool, autoFocus: PropTypes.bool, @@ -57,22 +57,22 @@ class Option extends React.PureComponent { if (e.key === 'Enter' || e.key === ' ') { this.handleToggleMultiple(e); } - } + }; onSuggestionsClearRequested = () => { this.props.onClearSuggestions(); - } + }; onSuggestionsFetchRequested = (token) => { this.props.onFetchSuggestions(token); - } + }; onSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); - } + }; render () { - const { isPollMultiple, title, index, autoFocus, intl } = this.props; + const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props; return ( <li> @@ -82,7 +82,7 @@ class Option extends React.PureComponent { onClick={this.handleToggleMultiple} onKeyPress={this.handleCheckboxKeypress} role='button' - tabIndex='0' + tabIndex={0} title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)} aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)} /> @@ -91,6 +91,8 @@ class Option extends React.PureComponent { placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} maxLength={100} value={title} + lang={lang} + spellCheck onChange={this.handleOptionTitleChange} suggestions={this.props.suggestions} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} @@ -110,12 +112,13 @@ class Option extends React.PureComponent { } -export default -@injectIntl +const Option = injectIntl(OptionIntl); + class PollForm extends ImmutablePureComponent { static propTypes = { options: ImmutablePropTypes.list, + lang: PropTypes.string, expiresIn: PropTypes.number, isMultiple: PropTypes.bool, onChangeOption: PropTypes.func.isRequired, @@ -142,7 +145,7 @@ class PollForm extends ImmutablePureComponent { }; render () { - const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; + const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; if (!options) { return null; @@ -153,7 +156,7 @@ class PollForm extends ImmutablePureComponent { return ( <div className='compose-form__poll-wrapper'> <ul> - {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)} + {options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)} </ul> <div className='poll__footer'> @@ -176,3 +179,5 @@ class PollForm extends ImmutablePureComponent { } } + +export default injectIntl(PollForm); diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx index 545b67eda..e65c9cb72 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx @@ -35,7 +35,7 @@ class PrivacyDropdownMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; handleKeyDown = e => { const { items } = this.props; @@ -79,7 +79,7 @@ class PrivacyDropdownMenu extends React.PureComponent { e.preventDefault(); e.stopPropagation(); } - } + }; handleClick = e => { const value = e.currentTarget.getAttribute('data-index'); @@ -88,7 +88,7 @@ class PrivacyDropdownMenu extends React.PureComponent { this.props.onClose(); this.props.onChange(value); - } + }; componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); @@ -103,11 +103,11 @@ class PrivacyDropdownMenu extends React.PureComponent { setRef = c => { this.node = c; - } + }; setFocusRef = c => { this.focusedItem = c; - } + }; render () { const { style, items, value } = this.props; @@ -115,7 +115,7 @@ class PrivacyDropdownMenu extends React.PureComponent { return ( <div style={{ ...style }} role='listbox' ref={this.setRef}> {items.map(item => ( - <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> + <div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div className='privacy-dropdown__option__icon'> <Icon id={item.icon} fixedWidth /> </div> @@ -132,7 +132,6 @@ class PrivacyDropdownMenu extends React.PureComponent { } -export default @injectIntl class PrivacyDropdown extends React.PureComponent { static propTypes = { @@ -168,7 +167,7 @@ class PrivacyDropdown extends React.PureComponent { } this.setState({ open: !this.state.open }); } - } + }; handleModalActionClick = (e) => { e.preventDefault(); @@ -177,7 +176,7 @@ class PrivacyDropdown extends React.PureComponent { this.props.onModalClose(); this.props.onChange(value); - } + }; handleKeyDown = e => { switch(e.key) { @@ -185,13 +184,13 @@ class PrivacyDropdown extends React.PureComponent { this.handleClose(); break; } - } + }; handleMouseDown = () => { if (!this.state.open) { this.activeElement = document.activeElement; } - } + }; handleButtonKeyDown = (e) => { switch(e.key) { @@ -200,18 +199,18 @@ class PrivacyDropdown extends React.PureComponent { this.handleMouseDown(); break; } - } + }; handleClose = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } this.setState({ open: false }); - } + }; handleChange = value => { this.props.onChange(value); - } + }; componentWillMount () { const { intl: { formatMessage } } = this.props; @@ -231,15 +230,15 @@ class PrivacyDropdown extends React.PureComponent { setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; handleOverlayEnter = (state) => { this.setState({ placement: state.placement }); - } + }; render () { const { value, container, disabled, intl } = this.props; @@ -285,3 +284,5 @@ class PrivacyDropdown extends React.PureComponent { } } + +export default injectIntl(PrivacyDropdown); diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx index fc236882a..81de4ea76 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.js +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx @@ -12,7 +12,6 @@ const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, }); -export default @injectIntl class ReplyIndicator extends ImmutablePureComponent { static contextTypes = { @@ -27,14 +26,14 @@ class ReplyIndicator extends ImmutablePureComponent { handleClick = () => { this.props.onCancel(); - } + }; handleAccountClick = (e) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); } - } + }; render () { const { status, intl } = this.props; @@ -69,3 +68,5 @@ class ReplyIndicator extends ImmutablePureComponent { } } + +export default injectIntl(ReplyIndicator); diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js deleted file mode 100644 index 5820f8ca2..000000000 --- a/app/javascript/mastodon/features/compose/components/search.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/Overlay'; -import { searchEnabled } from '../../../initial_state'; -import Icon from 'mastodon/components/icon'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, - placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, -}); - -class SearchPopout extends React.PureComponent { - - render () { - const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; - return ( - <div className='search-popout'> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - {extraInformation} - </div> - ); - } - -} - -export default @injectIntl -class Search extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - identity: PropTypes.object.isRequired, - }; - - static propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - openInRoute: PropTypes.bool, - intl: PropTypes.object.isRequired, - singleColumn: PropTypes.bool, - }; - - state = { - expanded: false, - }; - - setRef = c => { - this.searchForm = c; - } - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleClear = (e) => { - e.preventDefault(); - - if (this.props.value.length > 0 || this.props.submitted) { - this.props.onClear(); - } - } - - handleKeyUp = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - - this.props.onSubmit(); - - if (this.props.openInRoute) { - this.context.router.history.push('/search'); - } - } else if (e.key === 'Escape') { - document.querySelector('.ui').parentElement.focus(); - } - } - - handleFocus = () => { - this.setState({ expanded: true }); - this.props.onShow(); - - if (this.searchForm && !this.props.singleColumn) { - const { left, right } = this.searchForm.getBoundingClientRect(); - if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { - this.searchForm.scrollIntoView(); - } - } - } - - handleBlur = () => { - this.setState({ expanded: false }); - } - - findTarget = () => { - return this.searchForm; - } - - render () { - const { intl, value, submitted } = this.props; - const { expanded } = this.state; - const { signedIn } = this.context.identity; - const hasValue = value.length > 0 || submitted; - - return ( - <div className='search'> - <input - ref={this.setRef} - className='search__input' - type='text' - placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} - aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyUp} - onFocus={this.handleFocus} - onBlur={this.handleBlur} - /> - - <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <Icon id='search' className={hasValue ? '' : 'active'} /> - <Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} /> - </div> - <Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> - {({ props, placement }) => ( - <div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}> - <div className={`dropdown-animation ${placement}`}> - <SearchPopout /> - </div> - </div> - )} - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx new file mode 100644 index 000000000..46723f5cc --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -0,0 +1,335 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { searchEnabled } from 'mastodon/initial_state'; +import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; +import { HASHTAG_REGEX } from 'mastodon/utils/hashtags'; + +const messages = defineMessages({ + placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, + placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, +}); + +class Search extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, + }; + + static propTypes = { + value: PropTypes.string.isRequired, + recent: ImmutablePropTypes.orderedSet, + submitted: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onOpenURL: PropTypes.func.isRequired, + onClickSearchResult: PropTypes.func.isRequired, + onForgetSearchResult: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + onShow: PropTypes.func.isRequired, + openInRoute: PropTypes.bool, + intl: PropTypes.object.isRequired, + singleColumn: PropTypes.bool, + }; + + state = { + expanded: false, + selectedOption: -1, + options: [], + }; + + setRef = c => { + this.searchForm = c; + }; + + handleChange = ({ target }) => { + const { onChange } = this.props; + + onChange(target.value); + + this._calculateOptions(target.value); + }; + + handleClear = e => { + const { value, submitted, onClear } = this.props; + + e.preventDefault(); + + if (value.length > 0 || submitted) { + onClear(); + this.setState({ options: [], selectedOption: -1 }); + } + }; + + handleKeyDown = (e) => { + const { selectedOption } = this.state; + const options = this._getOptions(); + + switch(e.key) { + case 'Escape': + e.preventDefault(); + this._unfocus(); + + break; + case 'ArrowDown': + e.preventDefault(); + + if (options.length > 0) { + this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) }); + } + + break; + case 'ArrowUp': + e.preventDefault(); + + if (options.length > 0) { + this.setState({ selectedOption: Math.max(selectedOption - 1, -1) }); + } + + break; + case 'Enter': + e.preventDefault(); + + if (selectedOption === -1) { + this._submit(); + } else if (options.length > 0) { + options[selectedOption].action(); + } + + this._unfocus(); + + break; + case 'Delete': + if (selectedOption > -1 && options.length > 0) { + const search = options[selectedOption]; + + if (typeof search.forget === 'function') { + e.preventDefault(); + search.forget(e); + } + } + + break; + } + }; + + handleFocus = () => { + const { onShow, singleColumn } = this.props; + + this.setState({ expanded: true, selectedOption: -1 }); + onShow(); + + if (this.searchForm && !singleColumn) { + const { left, right } = this.searchForm.getBoundingClientRect(); + + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.searchForm.scrollIntoView(); + } + } + }; + + handleBlur = () => { + this.setState({ expanded: false, selectedOption: -1 }); + }; + + findTarget = () => { + return this.searchForm; + }; + + handleHashtagClick = () => { + const { router } = this.context; + const { value, onClickSearchResult } = this.props; + + const query = value.trim().replace(/^#/, ''); + + router.history.push(`/tags/${query}`); + onClickSearchResult(query, 'hashtag'); + }; + + handleAccountClick = () => { + const { router } = this.context; + const { value, onClickSearchResult } = this.props; + + const query = value.trim().replace(/^@/, ''); + + router.history.push(`/@${query}`); + onClickSearchResult(query, 'account'); + }; + + handleURLClick = () => { + const { router } = this.context; + const { onOpenURL } = this.props; + + onOpenURL(router.history); + }; + + handleStatusSearch = () => { + this._submit('statuses'); + }; + + handleAccountSearch = () => { + this._submit('accounts'); + }; + + handleRecentSearchClick = search => { + const { router } = this.context; + + if (search.get('type') === 'account') { + router.history.push(`/@${search.get('q')}`); + } else if (search.get('type') === 'hashtag') { + router.history.push(`/tags/${search.get('q')}`); + } + }; + + handleForgetRecentSearchClick = search => { + const { onForgetSearchResult } = this.props; + + onForgetSearchResult(search.get('q')); + }; + + _unfocus () { + document.querySelector('.ui').parentElement.focus(); + } + + _submit (type) { + const { onSubmit, openInRoute } = this.props; + const { router } = this.context; + + onSubmit(type); + + if (openInRoute) { + router.history.push('/search'); + } + } + + _getOptions () { + const { options } = this.state; + + if (options.length > 0) { + return options; + } + + const { recent } = this.props; + + return recent.toArray().map(search => ({ + label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`, + + action: () => this.handleRecentSearchClick(search), + + forget: e => { + e.stopPropagation(); + this.handleForgetRecentSearchClick(search); + }, + })); + } + + _calculateOptions (value) { + const trimmedValue = value.trim(); + const options = []; + + if (trimmedValue.length > 0) { + const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' '); + + if (couldBeURL) { + options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick }); + } + + const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX); + + if (couldBeHashtag) { + options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick }); + } + + const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i); + + if (couldBeUsername) { + options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick }); + } + + const couldBeStatusSearch = searchEnabled; + + if (couldBeStatusSearch) { + options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch }); + } + + const couldBeUserSearch = true; + + if (couldBeUserSearch) { + options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch }); + } + } + + this.setState({ options }); + } + + render () { + const { intl, value, submitted, recent } = this.props; + const { expanded, options, selectedOption } = this.state; + const { signedIn } = this.context.identity; + + const hasValue = value.length > 0 || submitted; + + return ( + <div className={classNames('search', { active: expanded })}> + <input + ref={this.setRef} + className='search__input' + type='text' + placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} + aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} + value={value} + onChange={this.handleChange} + onKeyDown={this.handleKeyDown} + onFocus={this.handleFocus} + onBlur={this.handleBlur} + /> + + <div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}> + <Icon id='search' className={hasValue ? '' : 'active'} /> + <Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} /> + </div> + + <div className='search__popout'> + {options.length === 0 && ( + <> + <h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4> + + <div className='search__popout__menu'> + {recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => ( + <button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}> + <span>{label}</span> + <button className='icon-button' onMouseDown={forget}><Icon id='times' /></button> + </button> + )) : ( + <div className='search__popout__menu__message'> + <FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' /> + </div> + )} + </div> + </> + )} + + {options.length > 0 && ( + <> + <h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4> + + <div className='search__popout__menu'> + {options.map(({ key, label, action }, i) => ( + <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}> + {label} + </button> + ))} + </div> + </> + )} + </div> + </div> + ); + } + +} + +export default injectIntl(Search); diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.jsx index 44ab43638..1dccd950c 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.jsx @@ -14,7 +14,6 @@ const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, }); -export default @injectIntl class SearchResults extends ImmutablePureComponent { static propTypes = { @@ -78,7 +77,7 @@ class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = ( <div className='search-results__section'> - <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5> {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} @@ -138,3 +137,5 @@ class SearchResults extends ImmutablePureComponent { } } + +export default injectIntl(SearchResults); diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.jsx index 73da32ad5..73da32ad5 100644 --- a/app/javascript/mastodon/features/compose/components/text_icon_button.js +++ b/app/javascript/mastodon/features/compose/components/text_icon_button.jsx diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.jsx index b08307ade..e3651c229 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.jsx @@ -22,31 +22,36 @@ export default class Upload extends ImmutablePureComponent { handleUndoClick = e => { e.stopPropagation(); this.props.onUndo(this.props.media.get('id')); - } + }; handleFocalPointClick = e => { e.stopPropagation(); this.props.onOpenFocalPoint(this.props.media.get('id')); - } + }; render () { const { media } = this.props; + + if (!media) { + return null; + } + const focusX = media.getIn(['meta', 'focus', 'x']); const focusY = media.getIn(['meta', 'focus', 'y']); const x = ((focusX / 2) + .5) * 100; const y = ((focusY / -2) + .5) * 100; return ( - <div className='compose-form__upload' tabIndex='0' role='button'> + <div className='compose-form__upload' tabIndex={0} role='button'> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> {({ scale }) => ( <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> <div className='compose-form__upload__actions'> <button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> - {!!media.get('unattached') && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)} + <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button> </div> - {(media.get('description') || '').length === 0 && !!media.get('unattached') && ( + {(media.get('description') || '').length === 0 && ( <div className='compose-form__upload__warning'> <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button> </div> diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.jsx index 9cb36167a..f2e6ff85c 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.jsx @@ -23,8 +23,6 @@ const iconStyle = { lineHeight: '27px', }; -export default @connect(makeMapStateToProps) -@injectIntl class UploadButton extends ImmutablePureComponent { static propTypes = { @@ -41,15 +39,15 @@ class UploadButton extends ImmutablePureComponent { if (e.target.files.length > 0) { this.props.onSelectFile(e.target.files); } - } + }; handleClick = () => { this.fileElement.click(); - } + }; setRef = (c) => { this.fileElement = c; - } + }; render () { const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props; @@ -81,3 +79,5 @@ class UploadButton extends ImmutablePureComponent { } } + +export default connect(makeMapStateToProps)(injectIntl(UploadButton)); diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.jsx index 9ff2aa0fa..9ff2aa0fa 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.jsx diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.jsx index cabf520fd..cabf520fd 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.js +++ b/app/javascript/mastodon/features/compose/components/upload_progress.jsx diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.jsx index 803b7f86a..803b7f86a 100644 --- a/app/javascript/mastodon/features/compose/components/warning.js +++ b/app/javascript/mastodon/features/compose/components/warning.jsx |