about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/compose/components
diff options
context:
space:
mode:
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.js147
-rw-r--r--app/javascript/mastodon/features/compose/components/search.jsx335
-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