about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js5
-rw-r--r--app/javascript/flavours/glitch/components/link.js2
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js378
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js138
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js (renamed from app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js)17
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/index.js221
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js25
-rw-r--r--app/javascript/flavours/glitch/features/composer/publisher/index.js7
-rw-r--r--app/javascript/flavours/glitch/features/composer/reply/index.js12
-rw-r--r--app/javascript/flavours/glitch/features/composer/spoiler/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/index.js42
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/index.js39
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/item/index.js40
-rw-r--r--app/javascript/flavours/glitch/features/drawer/account/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/drawer/header/index.js3
-rw-r--r--app/javascript/flavours/glitch/features/drawer/index.js88
-rw-r--r--app/javascript/flavours/glitch/features/drawer/results/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/index.js34
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/popout/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/actions_modal.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js4
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js12
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js5
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss153
-rw-r--r--app/javascript/flavours/glitch/styles/components/drawer.scss335
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss43
-rw-r--r--app/javascript/flavours/glitch/theme.yml2
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js4
-rw-r--r--app/javascript/flavours/glitch/util/react_helpers.js4
-rw-r--r--app/javascript/flavours/glitch/util/redux_helpers.js10
32 files changed, 868 insertions, 790 deletions
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index 519941dd6..706390c92 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -134,11 +134,12 @@ export default class Dropdown extends React.PureComponent {
       this.props.onModalOpen({
         status,
         actions: items.map(
-          (item, i) => ({
+          (item, i) => item ? {
             ...item,
             name: `${item.text}-${i}`,
             onClick: this.handleItemClick.bind(i),
-          }),
+          } : null
+        ),
       });
 
       return;
diff --git a/app/javascript/flavours/glitch/components/link.js b/app/javascript/flavours/glitch/components/link.js
index c49fc487c..de99f7d42 100644
--- a/app/javascript/flavours/glitch/components/link.js
+++ b/app/javascript/flavours/glitch/components/link.js
@@ -45,7 +45,7 @@ export default class Link extends React.PureComponent {
       title,
       ...rest
     } = this.props;
-    const computedClass = classNames('link', className, role);
+    const computedClass = classNames('link', className, `role-${role}`);
 
     //  We assume that our `onClick` is a routing function and give it
     //  the qualities of a link even if no `href` is provided. However,
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index c3e6c987c..d64bee7ee 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -52,6 +52,7 @@ function mapStateToProps (state) {
     focusDate: state.getIn(['compose', 'focusDate']),
     isSubmitting: state.getIn(['compose', 'is_submitting']),
     isUploading: state.getIn(['compose', 'is_uploading']),
+    layout: state.getIn(['local_settings', 'layout']),
     media: state.getIn(['compose', 'media_attachments']),
     preselectDate: state.getIn(['compose', 'preselectDate']),
     privacy: state.getIn(['compose', 'privacy']),
@@ -71,132 +72,96 @@ function mapStateToProps (state) {
 };
 
 //  Dispatch mapping.
-const mapDispatchToProps = dispatch => ({
-  cancelReply () {
-    dispatch(cancelReplyCompose());
-  },
-  changeDescription (mediaId, description) {
-    dispatch(changeUploadCompose(mediaId, description));
-  },
-  changeSensitivity () {
-    dispatch(changeComposeSensitivity());
-  },
-  changeSpoilerText (checked) {
-    dispatch(changeComposeSpoilerText(checked));
-  },
-  changeSpoilerness () {
-    dispatch(changeComposeSpoilerness());
-  },
-  changeText (text) {
-    dispatch(changeCompose(text));
-  },
-  changeVisibility (value) {
-    dispatch(changeComposeVisibility(value));
-  },
-  clearSuggestions () {
-    dispatch(clearComposeSuggestions());
-  },
-  closeModal () {
-    dispatch(closeModal());
-  },
-  fetchSuggestions (token) {
-    dispatch(fetchComposeSuggestions(token));
-  },
-  insertEmoji (position, data) {
-    dispatch(insertEmojiCompose(position, data));
-  },
-  openActionsModal (data) {
-    dispatch(openModal('ACTIONS', data));
-  },
-  openDoodleModal () {
-    dispatch(openModal('DOODLE', { noEsc: true }));
-  },
-  selectSuggestion (position, token, accountId) {
-    dispatch(selectComposeSuggestion(position, token, accountId));
-  },
-  submit () {
-    dispatch(submitCompose());
-  },
-  toggleAdvancedOption (option) {
-    dispatch(toggleComposeAdvancedOption(option));
-  },
-  undoUpload (mediaId) {
-    dispatch(undoUploadCompose(mediaId));
-  },
-  upload (files) {
-    dispatch(uploadCompose(files));
-  },
-});
+const mapDispatchToProps = {
+  onCancelReply: cancelReplyCompose,
+  onChangeDescription: changeUploadCompose,
+  onChangeSensitivity: changeComposeSensitivity,
+  onChangeSpoilerText: changeComposeSpoilerText,
+  onChangeSpoilerness: changeComposeSpoilerness,
+  onChangeText: changeCompose,
+  onChangeVisibility: changeComposeVisibility,
+  onClearSuggestions: clearComposeSuggestions,
+  onCloseModal: closeModal,
+  onFetchSuggestions: fetchComposeSuggestions,
+  onInsertEmoji: insertEmojiCompose,
+  onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
+  onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
+  onSelectSuggestion: selectComposeSuggestion,
+  onSubmit: submitCompose,
+  onToggleAdvancedOption: toggleComposeAdvancedOption,
+  onUndoUpload: undoUploadCompose,
+  onUpload: uploadCompose,
+};
 
 //  Handlers.
 const handlers = {
 
   //  Changes the text value of the spoiler.
-  changeSpoiler ({ target: { value } }) {
-    const { dispatch: { changeSpoilerText } } = this.props;
-    if (changeSpoilerText) {
-      changeSpoilerText(value);
+  handleChangeSpoiler ({ target: { value } }) {
+    const { onChangeSpoilerText } = this.props;
+    if (onChangeSpoilerText) {
+      onChangeSpoilerText(value);
     }
   },
 
   //  Inserts an emoji at the caret.
-  emoji (data) {
+  handleEmoji (data) {
     const { textarea: { selectionStart } } = this;
-    const { dispatch: { insertEmoji } } = this.props;
+    const { onInsertEmoji } = this.props;
     this.caretPos = selectionStart + data.native.length + 1;
-    if (insertEmoji) {
-      insertEmoji(selectionStart, data);
+    if (onInsertEmoji) {
+      onInsertEmoji(selectionStart, data);
     }
   },
 
   //  Handles the secondary submit button.
-  secondarySubmit () {
-    const { submit } = this.handlers;
+  handleSecondarySubmit () {
+    const { handleSubmit } = this.handlers;
     const {
-      dispatch: { changeVisibility },
-      side_arm,
+      onChangeVisibility,
+      sideArm,
     } = this.props;
-    if (changeVisibility) {
-      changeVisibility(side_arm);
+    if (sideArm !== 'none' && onChangeVisibility) {
+      onChangeVisibility(sideArm);
     }
-    submit();
+    handleSubmit();
   },
 
   //  Selects a suggestion from the autofill.
-  select (tokenStart, token, value) {
-    const { dispatch: { selectSuggestion } } = this.props;
+  handleSelect (tokenStart, token, value) {
+    const { onSelectSuggestion } = this.props;
     this.caretPos = null;
-    if (selectSuggestion) {
-      selectSuggestion(tokenStart, token, value);
+    if (onSelectSuggestion) {
+      onSelectSuggestion(tokenStart, token, value);
     }
   },
 
   //  Submits the status.
-  submit () {
+  handleSubmit () {
     const { textarea: { value } } = this;
     const {
-      dispatch: {
-        changeText,
-        submit,
-      },
-      state: { text },
+      onChangeText,
+      onSubmit,
+      text,
     } = this.props;
 
     //  If something changes inside the textarea, then we update the
     //  state before submitting.
-    if (changeText && text !== value) {
-      changeText(value);
+    if (onChangeText && text !== value) {
+      onChangeText(value);
     }
 
     //  Submits the status.
-    if (submit) {
-      submit();
+    if (onSubmit) {
+      onSubmit();
     }
   },
 
   //  Sets a reference to the textarea.
-  refTextarea ({ textarea }) {
-    this.textarea = textarea;
+  handleRefTextarea (textareaComponent) {
+    if (textareaComponent) {
+      this.textarea = textareaComponent.textarea;
+    }
   },
 };
 
@@ -216,10 +181,10 @@ class Composer extends React.Component {
   //  If this is the update where we've finished uploading,
   //  save the last caret position so we can restore it below!
   componentWillReceiveProps (nextProps) {
-    const { textarea: { selectionStart } } = this;
-    const { state: { isUploading } } = this.props;
-    if (isUploading && !nextProps.state.isUploading) {
-      this.caretPos = selectionStart;
+    const { textarea } = this;
+    const { isUploading } = this.props;
+    if (textarea && isUploading && !nextProps.isUploading) {
+      this.caretPos = textarea.selectionStart;
     }
   }
 
@@ -239,20 +204,18 @@ class Composer extends React.Component {
       textarea,
     } = this;
     const {
-      state: {
-        focusDate,
-        isUploading,
-        isSubmitting,
-        preselectDate,
-        text,
-      },
+      focusDate,
+      isUploading,
+      isSubmitting,
+      preselectDate,
+      text,
     } = this.props;
     let selectionEnd, selectionStart;
 
     //  Caret/selection handling.
-    if (focusDate !== prevProps.state.focusDate || (prevProps.state.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) {
+    if (focusDate !== prevProps.focusDate || (prevProps.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) {
       switch (true) {
-      case preselectDate !== prevProps.state.preselectDate:
+      case preselectDate !== prevProps.preselectDate:
         selectionStart = text.search(/\s/) + 1;
         selectionEnd = text.length;
         break;
@@ -262,71 +225,71 @@ class Composer extends React.Component {
       default:
         selectionStart = selectionEnd = text.length;
       }
-      textarea.setSelectionRange(selectionStart, selectionEnd);
-      textarea.focus();
+      if (textarea) {
+        textarea.setSelectionRange(selectionStart, selectionEnd);
+        textarea.focus();
+      }
 
     //  Refocuses the textarea after submitting.
-    } else if (prevProps.state.isSubmitting && !isSubmitting) {
+    } else if (textarea && prevProps.isSubmitting && !isSubmitting) {
       textarea.focus();
     }
   }
 
   render () {
     const {
-      changeSpoiler,
-      emoji,
-      secondarySubmit,
-      select,
-      submit,
-      refTextarea,
+      handleChangeSpoiler,
+      handleEmoji,
+      handleSecondarySubmit,
+      handleSelect,
+      handleSubmit,
+      handleRefTextarea,
     } = this.handlers;
     const { history } = this.context;
     const {
-      dispatch: {
-        cancelReply,
-        changeDescription,
-        changeSensitivity,
-        changeText,
-        changeVisibility,
-        clearSuggestions,
-        closeModal,
-        fetchSuggestions,
-        openActionsModal,
-        openDoodleModal,
-        toggleAdvancedOption,
-        undoUpload,
-        upload,
-      },
+      acceptContentTypes,
+      amUnlocked,
+      doNotFederate,
       intl,
-      state: {
-        acceptContentTypes,
-        amUnlocked,
-        doNotFederate,
-        isSubmitting,
-        isUploading,
-        media,
-        privacy,
-        progress,
-        replyAccount,
-        replyContent,
-        resetFileKey,
-        sensitive,
-        showSearch,
-        sideArm,
-        spoiler,
-        spoilerText,
-        suggestions,
-        text,
-      },
+      isSubmitting,
+      isUploading,
+      layout,
+      media,
+      onCancelReply,
+      onChangeDescription,
+      onChangeSensitivity,
+      onChangeSpoilerness,
+      onChangeText,
+      onChangeVisibility,
+      onClearSuggestions,
+      onCloseModal,
+      onFetchSuggestions,
+      onOpenActionsModal,
+      onOpenDoodleModal,
+      onToggleAdvancedOption,
+      onUndoUpload,
+      onUpload,
+      privacy,
+      progress,
+      replyAccount,
+      replyContent,
+      resetFileKey,
+      sensitive,
+      showSearch,
+      sideArm,
+      spoiler,
+      spoilerText,
+      suggestions,
+      text,
     } = this.props;
 
     return (
-      <div className='compose'>
+      <div className='composer'>
         <ComposerSpoiler
           hidden={!spoiler}
           intl={intl}
-          onChange={changeSpoiler}
-          onSubmit={submit}
+          onChange={handleChangeSpoiler}
+          onSubmit={handleSubmit}
           text={spoilerText}
         />
         {privacy === 'private' && amUnlocked ? <ComposerWarning /> : null}
@@ -336,32 +299,32 @@ class Composer extends React.Component {
             content={replyContent}
             history={history}
             intl={intl}
-            onCancel={cancelReply}
+            onCancel={onCancelReply}
           />
         ) : null}
         <ComposerTextarea
-          autoFocus={!showSearch && !isMobile(window.innerWidth)}
+          autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
           disabled={isSubmitting}
           intl={intl}
-          onChange={changeText}
-          onPaste={upload}
-          onPickEmoji={emoji}
-          onSubmit={submit}
-          onSuggestionsClearRequested={clearSuggestions}
-          onSuggestionsFetchRequested={fetchSuggestions}
-          onSuggestionSelected={select}
-          ref={refTextarea}
+          onChange={onChangeText}
+          onPaste={onUpload}
+          onPickEmoji={handleEmoji}
+          onSubmit={handleSubmit}
+          onSuggestionsClearRequested={onClearSuggestions}
+          onSuggestionsFetchRequested={onFetchSuggestions}
+          onSuggestionSelected={handleSelect}
+          ref={handleRefTextarea}
           suggestions={suggestions}
           value={text}
         />
-        {media && media.size ? (
+        {isUploading || media && media.size ? (
           <ComposerUploadForm
-            active={isUploading}
             intl={intl}
             media={media}
-            onChangeDescription={changeDescription}
-            onRemove={undoUpload}
+            onChangeDescription={onChangeDescription}
+            onRemove={onUndoUpload}
             progress={progress}
+            uploading={isUploading}
           />
         ) : null}
         <ComposerOptions
@@ -373,13 +336,14 @@ class Composer extends React.Component {
           )}
           hasMedia={!!media.size}
           intl={intl}
-          onChangeSensitivity={changeSensitivity}
-          onChangeVisibility={changeVisibility}
-          onDoodleOpen={openDoodleModal}
-          onModalClose={closeModal}
-          onModalOpen={openActionsModal}
-          onToggleAdvancedOption={toggleAdvancedOption}
-          onUpload={upload}
+          onChangeSensitivity={onChangeSensitivity}
+          onChangeVisibility={onChangeVisibility}
+          onDoodleOpen={onOpenDoodleModal}
+          onModalClose={onCloseModal}
+          onModalOpen={onOpenActionsModal}
+          onToggleAdvancedOption={onToggleAdvancedOption}
+          onToggleSpoiler={onChangeSpoilerness}
+          onUpload={onUpload}
           privacy={privacy}
           resetFileKey={resetFileKey}
           sensitive={sensitive}
@@ -387,10 +351,10 @@ class Composer extends React.Component {
         />
         <ComposerPublisher
           countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
-          disabled={isSubmitting || isUploading || text.length && text.trim().length === 0}
+          disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
           intl={intl}
-          onSecondarySubmit={secondarySubmit}
-          onSubmit={submit}
+          onSecondarySubmit={handleSecondarySubmit}
+          onSubmit={handleSubmit}
           privacy={privacy}
           sideArm={sideArm}
         />
@@ -407,37 +371,51 @@ Composer.contextTypes = {
 
 //  Props.
 Composer.propTypes = {
-  dispatch: PropTypes.objectOf(PropTypes.func).isRequired,
   intl: PropTypes.object.isRequired,
-  state: PropTypes.shape({
-    acceptContentTypes: PropTypes.string,
-    amUnlocked: PropTypes.bool,
-    doNotFederate: PropTypes.bool,
-    focusDate: PropTypes.instanceOf(Date),
-    isSubmitting: PropTypes.bool,
-    isUploading: PropTypes.bool,
-    media: PropTypes.list,
-    preselectDate: PropTypes.instanceOf(Date),
-    privacy: PropTypes.string,
-    progress: PropTypes.number,
-    replyAccount: ImmutablePropTypes.map,
-    replyContent: PropTypes.string,
-    resetFileKey: PropTypes.string,
-    sideArm: PropTypes.string,
-    sensitive: PropTypes.bool,
-    showSearch: PropTypes.bool,
-    spoiler: PropTypes.bool,
-    spoilerText: PropTypes.string,
-    suggestionToken: PropTypes.string,
-    suggestions: ImmutablePropTypes.list,
-    text: PropTypes.string,
-  }).isRequired,
-};
 
-//  Default props.
-Composer.defaultProps = {
-  dispatch: {},
-  state: {},
+  //  State props.
+  acceptContentTypes: PropTypes.string,
+  amUnlocked: PropTypes.bool,
+  doNotFederate: PropTypes.bool,
+  focusDate: PropTypes.instanceOf(Date),
+  isSubmitting: PropTypes.bool,
+  isUploading: PropTypes.bool,
+  layout: PropTypes.string,
+  media: ImmutablePropTypes.list,
+  preselectDate: PropTypes.instanceOf(Date),
+  privacy: PropTypes.string,
+  progress: PropTypes.number,
+  replyAccount: ImmutablePropTypes.map,
+  replyContent: PropTypes.string,
+  resetFileKey: PropTypes.number,
+  sideArm: PropTypes.string,
+  sensitive: PropTypes.bool,
+  showSearch: PropTypes.bool,
+  spoiler: PropTypes.bool,
+  spoilerText: PropTypes.string,
+  suggestionToken: PropTypes.string,
+  suggestions: ImmutablePropTypes.list,
+  text: PropTypes.string,
+
+  //  Dispatch props.
+  onCancelReply: PropTypes.func,
+  onChangeDescription: PropTypes.func,
+  onChangeSensitivity: PropTypes.func,
+  onChangeSpoilerText: PropTypes.func,
+  onChangeSpoilerness: PropTypes.func,
+  onChangeText: PropTypes.func,
+  onChangeVisibility: PropTypes.func,
+  onClearSuggestions: PropTypes.func,
+  onCloseModal: PropTypes.func,
+  onFetchSuggestions: PropTypes.func,
+  onInsertEmoji: PropTypes.func,
+  onOpenActionsModal: PropTypes.func,
+  onOpenDoodleModal: PropTypes.func,
+  onSelectSuggestion: PropTypes.func,
+  onSubmit: PropTypes.func,
+  onToggleAdvancedOption: PropTypes.func,
+  onUndoUpload: PropTypes.func,
+  onUpload: PropTypes.func,
 };
 
 //  Connecting and export.
diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js
new file mode 100644
index 000000000..28bdfc0db
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js
@@ -0,0 +1,138 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import spring from 'react-motion/lib/spring';
+
+//  Components.
+import ComposerOptionsDropdownContentItem from './item';
+
+//  Utils.
+import { withPassive } from 'flavours/glitch/util/dom_helpers';
+import Motion from 'flavours/glitch/util/optional_motion';
+import { assignHandlers } from 'flavours/glitch/util/react_helpers';
+
+//  Handlers.
+const handlers = {
+
+  //  When the document is clicked elsewhere, we close the dropdown.
+  handleDocumentClick ({ target }) {
+    const { node } = this;
+    const { onClose } = this.props;
+    if (onClose && node && !node.contains(target)) {
+      onClose();
+    }
+  },
+
+  //  Stores our node in `this.node`.
+  handleRef (node) {
+    this.node = node;
+  },
+};
+
+//  The spring to use with our motion.
+const springMotion = spring(1, {
+  damping: 35,
+  stiffness: 400,
+});
+
+//  The component.
+export default class ComposerOptionsDropdownContent extends React.PureComponent {
+
+  //  Constructor.
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+
+    //  Instance variables.
+    this.node = null;
+  }
+
+  //  On mounting, we add our listeners.
+  componentDidMount () {
+    const { handleDocumentClick } = this.handlers;
+    document.addEventListener('click', handleDocumentClick, false);
+    document.addEventListener('touchend', handleDocumentClick, withPassive);
+  }
+
+  //  On unmounting, we remove our listeners.
+  componentWillUnmount () {
+    const { handleDocumentClick } = this.handlers;
+    document.removeEventListener('click', handleDocumentClick, false);
+    document.removeEventListener('touchend', handleDocumentClick, withPassive);
+  }
+
+  //  Rendering.
+  render () {
+    const { handleRef } = this.handlers;
+    const {
+      items,
+      onChange,
+      onClose,
+      style,
+      value,
+    } = this.props;
+
+    //  The result.
+    return (
+      <Motion
+        defaultStyle={{
+          opacity: 0,
+          scaleX: 0.85,
+          scaleY: 0.75,
+        }}
+        style={{
+          opacity: springMotion,
+          scaleX: springMotion,
+          scaleY: springMotion,
+        }}
+      >
+        {({ opacity, scaleX, scaleY }) => (
+          <div
+            className='composer--options--dropdown--content'
+            ref={handleRef}
+            style={{
+              ...style,
+              opacity: opacity,
+              transform: `scale(${scaleX}, ${scaleY})`,
+            }}
+          >
+            {items.map(
+              ({
+                name,
+                ...rest
+              }) => (
+                <ComposerOptionsDropdownContentItem
+                  active={name === value}
+                  key={name}
+                  name={name}
+                  onChange={onChange}
+                  onClose={onClose}
+                  options={rest}
+                />
+              )
+            )}
+          </div>
+        )}
+      </Motion>
+    );
+  }
+
+}
+
+//  Props.
+ComposerOptionsDropdownContent.propTypes = {
+  items: PropTypes.arrayOf(PropTypes.shape({
+    icon: PropTypes.string,
+    meta: PropTypes.node,
+    name: PropTypes.string.isRequired,
+    on: PropTypes.bool,
+    text: PropTypes.node,
+  })).isRequired,
+  onChange: PropTypes.func,
+  onClose: PropTypes.func,
+  style: PropTypes.object,
+  value: PropTypes.string,
+};
+
+//  Default props.
+ComposerOptionsDropdownContent.defaultProps = { style: {} };
diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js
index e9047dc50..605c945bd 100644
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js
@@ -14,7 +14,7 @@ import { assignHandlers } from 'flavours/glitch/util/react_helpers';
 const handlers = {
 
   //  This function activates the dropdown item.
-  activate (e) {
+  handleActivate (e) {
     const {
       name,
       onChange,
@@ -35,11 +35,10 @@ const handlers = {
       onChange(name);
     }
   },
-
 };
 
 //  The component.
-export default class ComposerOptionsDropdownItem extends React.PureComponent {
+export default class ComposerOptionsDropdownContentItem extends React.PureComponent {
 
   //  Constructor.
   constructor (props) {
@@ -49,7 +48,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
 
   //  Rendering.
   render () {
-    const { activate } = this.handlers;
+    const { handleActivate } = this.handlers;
     const {
       active,
       options: {
@@ -59,7 +58,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
         text,
       },
     } = this.props;
-    const computedClass = classNames('composer--options--dropdown_item', {
+    const computedClass = classNames('composer--options--dropdown--content--item', {
       active,
       lengthy: meta,
       'toggled-off': !on && on !== null && typeof on !== 'undefined',
@@ -71,8 +70,8 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
     return (
       <div
         className={computedClass}
-        onClick={activate}
-        onKeyDown={activate}
+        onClick={handleActivate}
+        onKeyDown={handleActivate}
         role='button'
         tabIndex='0'
       >
@@ -85,7 +84,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
             return (
               <Toggle
                 checked={on}
-                onChange={activate}
+                onChange={handleActivate}
               />
             );
           case !!icon:
@@ -113,7 +112,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
 };
 
 //  Props.
-ComposerOptionsDropdownItem.propTypes = {
+ComposerOptionsDropdownContentItem.propTypes = {
   active: PropTypes.bool,
   name: PropTypes.string,
   onChange: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
index daed4ec8a..d63d90a9f 100644
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
@@ -2,108 +2,120 @@
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React from 'react';
-import spring from 'react-motion/lib/spring';
 import Overlay from 'react-overlays/lib/Overlay';
 
 //  Components.
 import IconButton from 'flavours/glitch/components/icon_button';
-import ComposerOptionsDropdownItem from './item';
+import ComposerOptionsDropdownContent from './content';
 
 //  Utils.
-import { withPassive } from 'flavours/glitch/util/dom_helpers';
 import { isUserTouching } from 'flavours/glitch/util/is_mobile';
-import Motion from 'flavours/glitch/util/optional_motion';
 import { assignHandlers } from 'flavours/glitch/util/react_helpers';
 
-//  We'll use this to define our various transitions.
-const springMotion = spring(1, {
-  damping: 35,
-  stiffness: 400,
-});
-
 //  Handlers.
 const handlers = {
 
   //  Closes the dropdown.
-  close () {
+  handleClose () {
     this.setState({ open: false });
   },
 
-  //  When the document is clicked elsewhere, we close the dropdown.
-  documentClick ({ target }) {
-    const { node } = this;
-    const { onClose } = this.props;
-    if (onClose && node && !node.contains(target)) {
-      onClose();
-    }
-  },
-
   //  The enter key toggles the dropdown's open state, and the escape
   //  key closes it.
-  keyDown ({ key }) {
+  handleKeyDown ({ key }) {
     const {
-      close,
-      toggle,
+      handleClose,
+      handleToggle,
     } = this.handlers;
     switch (key) {
     case 'Enter':
-      toggle();
+      handleToggle();
       break;
     case 'Escape':
-      close();
+      handleClose();
       break;
     }
   },
 
-  //  Toggles opening and closing the dropdown.
-  toggle () {
+  //  Creates an action modal object.
+  handleMakeModal () {
+    const component = this;
     const {
       items,
       onChange,
-      onModalClose,
       onModalOpen,
+      onModalClose,
       value,
     } = this.props;
+
+    //  Required props.
+    if (!(onChange && onModalOpen && onModalClose && items)) {
+      return null;
+    }
+
+    //  The object.
+    return {
+      actions: items.map(
+        ({
+          name,
+          ...rest
+        }) => ({
+          ...rest,
+          active: value && name === value,
+          name,
+          onClick (e) {
+            e.preventDefault();  //  Prevents focus from changing
+            onModalClose();
+            onChange(name);
+          },
+          onPassiveClick (e) {
+            e.preventDefault();  //  Prevents focus from changing
+            onChange(name);
+            component.setState({ needsModalUpdate: true });
+          },
+        })
+      ),
+    };
+  },
+
+  //  Toggles opening and closing the dropdown.
+  handleToggle () {
+    const { handleMakeModal } = this.handlers;
+    const { onModalOpen } = this.props;
     const { open } = this.state;
 
     //  If this is a touch device, we open a modal instead of the
     //  dropdown.
-    if (onModalClose && isUserTouching()) {
-      if (open) {
-        onModalClose();
-      } else if (onChange && onModalOpen) {
-        onModalOpen({
-          actions: items.map(
-            ({
-              name,
-              ...rest
-            }) => ({
-              ...rest,
-              active: value && name === value,
-              name,
-              onClick (e) {
-                e.preventDefault();  //  Prevents focus from changing
-                onModalClose();
-                onChange(name);
-              },
-              onPassiveClick (e) {
-                e.preventDefault();  //  Prevents focus from changing
-                onChange(name);
-              },
-            })
-          ),
-        });
+    if (isUserTouching()) {
+
+      //  This gets the modal to open.
+      const modal = handleMakeModal();
+
+      //  If we can, we then open the modal.
+      if (modal && onModalOpen) {
+        onModalOpen(modal);
+        return;
       }
+    }
 
     //  Otherwise, we just set our state to open.
-    } else {
-      this.setState({ open: !open });
-    }
+    this.setState({ open: !open });
   },
 
-  //  Stores our node in `this.node`.
-  ref (node) {
-    this.node = node;
+  //  If our modal is open and our props update, we need to also update
+  //  the modal.
+  handleUpdate () {
+    const { handleMakeModal } = this.handlers;
+    const { onModalOpen } = this.props;
+    const { needsModalUpdate } = this.state;
+
+    //  Gets our modal object.
+    const modal = handleMakeModal();
+
+    //  Reopens the modal with the new object.
+    if (needsModalUpdate && modal && onModalOpen) {
+      onModalOpen(modal);
+    }
   },
 };
 
@@ -114,33 +126,31 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
   constructor (props) {
     super(props);
     assignHandlers(this, handlers);
-    this.state = { open: false };
-
-    //  Instance variables.
-    this.node = null;
+    this.state = {
+      needsModalUpdate: false,
+      open: false,
+    };
   }
 
-  //  On mounting, we add our listeners.
-  componentDidMount () {
-    const { documentClick } = this.handlers;
-    document.addEventListener('click', documentClick, false);
-    document.addEventListener('touchend', documentClick, withPassive);
-  }
-
-  //  On unmounting, we remove our listeners.
-  componentWillUnmount () {
-    const { documentClick } = this.handlers;
-    document.removeEventListener('click', documentClick, false);
-    document.removeEventListener('touchend', documentClick, withPassive);
+  //  Updates our modal as necessary.
+  componentDidUpdate (prevProps) {
+    const { handleUpdate } = this.handlers;
+    const { items } = this.props;
+    const { needsModalUpdate } = this.state;
+    if (needsModalUpdate && items.find(
+      (item, i) => item.on !== prevProps.items[i].on
+    )) {
+      handleUpdate();
+      this.setState({ needsModalUpdate: false });
+    }
   }
 
   //  Rendering.
   render () {
     const {
-      close,
-      keyDown,
-      ref,
-      toggle,
+      handleClose,
+      handleKeyDown,
+      handleToggle,
     } = this.handlers;
     const {
       active,
@@ -154,22 +164,21 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
     const { open } = this.state;
     const computedClass = classNames('composer--options--dropdown', {
       active,
-      open: open || active,
+      open,
     });
 
     //  The result.
     return (
       <div
         className={computedClass}
-        onKeyDown={keyDown}
-        ref={ref}
+        onKeyDown={handleKeyDown}
       >
         <IconButton
           active={open || active}
           className='value'
           disabled={disabled}
           icon={icon}
-          onClick={toggle}
+          onClick={handleToggle}
           size={18}
           style={{
             height: null,
@@ -178,49 +187,17 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
           title={title}
         />
         <Overlay
+          containerPadding={20}
           placement='bottom'
           show={open}
           target={this}
         >
-          <Motion
-            defaultStyle={{
-              opacity: 0,
-              scaleX: 0.85,
-              scaleY: 0.75,
-            }}
-            style={{
-              opacity: springMotion,
-              scaleX: springMotion,
-              scaleY: springMotion,
-            }}
-          >
-            {({ opacity, scaleX, scaleY }) => (
-              <div
-                className='composer--options--dropdown__dropdown'
-                ref={this.setRef}
-                style={{
-                  opacity: opacity,
-                  transform: `scale(${scaleX}, ${scaleY})`,
-                }}
-              >
-                {items.map(
-                  ({
-                    name,
-                    ...rest
-                  }) => (
-                    <ComposerOptionsDropdownItem
-                      active={name === value}
-                      key={name}
-                      name={name}
-                      onChange={onChange}
-                      onClose={close}
-                      options={rest}
-                    />
-                  )
-                )}
-              </div>
-            )}
-          </Motion>
+          <ComposerOptionsDropdownContent
+            items={items}
+            onChange={onChange}
+            onClose={handleClose}
+            value={value}
+          />
         </Overlay>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
index ea998a421..e805372ab 100644
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/index.js
@@ -95,7 +95,7 @@ const messages = defineMessages({
 const handlers = {
 
   //  Handles file selection.
-  changeFiles ({ target: { files } }) {
+  handleChangeFiles ({ target: { files } }) {
     const { onUpload } = this.props;
     if (files.length && onUpload) {
       onUpload(files);
@@ -103,7 +103,7 @@ const handlers = {
   },
 
   //  Handles attachment clicks.
-  clickAttach (name) {
+  handleClickAttach (name) {
     const { fileElement } = this;
     const { onDoodleOpen } = this.props;
 
@@ -123,7 +123,7 @@ const handlers = {
   },
 
   //  Handles a ref to the file input.
-  refFileElement (fileElement) {
+  handleRefFileElement (fileElement) {
     this.fileElement = fileElement;
   },
 };
@@ -143,9 +143,9 @@ export default class ComposerOptions extends React.PureComponent {
   //  Rendering.
   render () {
     const {
-      changeFiles,
-      clickAttach,
-      refFileElement,
+      handleChangeFiles,
+      handleClickAttach,
+      handleRefFileElement,
     } = this.handlers;
     const {
       acceptContentTypes,
@@ -159,6 +159,7 @@ export default class ComposerOptions extends React.PureComponent {
       onModalClose,
       onModalOpen,
       onToggleAdvancedOption,
+      onToggleSpoiler,
       privacy,
       resetFileKey,
       sensitive,
@@ -201,8 +202,8 @@ export default class ComposerOptions extends React.PureComponent {
           accept={acceptContentTypes}
           disabled={disabled || full}
           key={resetFileKey}
-          onChange={changeFiles}
-          ref={refFileElement}
+          onChange={handleChangeFiles}
+          ref={handleRefFileElement}
           type='file'
           {...hiddenComponent}
         />
@@ -221,10 +222,10 @@ export default class ComposerOptions extends React.PureComponent {
               text: <FormattedMessage {...messages.doodle} />,
             },
           ]}
-          onChange={clickAttach}
+          onChange={handleClickAttach}
           onModalClose={onModalClose}
           onModalOpen={onModalOpen}
-          title={messages.attach}
+          title={intl.formatMessage(messages.attach)}
         />
         <Motion
           defaultStyle={{ scale: 0.87 }}
@@ -279,6 +280,7 @@ export default class ComposerOptions extends React.PureComponent {
           active={spoiler}
           ariaControls='glitch.composer.spoiler.input'
           label='CW'
+          onClick={onToggleSpoiler}
           title={intl.formatMessage(messages.spoiler)}
         />
         <Dropdown
@@ -318,9 +320,10 @@ ComposerOptions.propTypes = {
   onModalClose: PropTypes.func,
   onModalOpen: PropTypes.func,
   onToggleAdvancedOption: PropTypes.func,
+  onToggleSpoiler: PropTypes.func,
   onUpload: PropTypes.func,
   privacy: PropTypes.string,
-  resetFileKey: PropTypes.string,
+  resetFileKey: PropTypes.number,
   sensitive: PropTypes.bool,
   spoiler: PropTypes.bool,
 };
diff --git a/app/javascript/flavours/glitch/features/composer/publisher/index.js b/app/javascript/flavours/glitch/features/composer/publisher/index.js
index 79337100f..f54fd68b7 100644
--- a/app/javascript/flavours/glitch/features/composer/publisher/index.js
+++ b/app/javascript/flavours/glitch/features/composer/publisher/index.js
@@ -46,10 +46,13 @@ export default function ComposerPublisher ({
   //  The result.
   return (
     <div className={computedClass}>
-      <span class='count'>{diff}</span>
+      <span className='count'>{diff}</span>
       {sideArm && sideArm !== 'none' ? (
         <Button
           className='side_arm'
+          disabled={disabled || diff < 0}
+          onClick={onSecondarySubmit}
+          style={{ padding: null }}
           text={
             <span>
               <Icon
@@ -63,8 +66,6 @@ export default function ComposerPublisher ({
             </span>
           }
           title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
-          onClick={onSecondarySubmit}
-          disabled={disabled || diff < 0}
         />
       ) : null}
       <Button
diff --git a/app/javascript/flavours/glitch/features/composer/reply/index.js b/app/javascript/flavours/glitch/features/composer/reply/index.js
index 4a52cddb4..568705aff 100644
--- a/app/javascript/flavours/glitch/features/composer/reply/index.js
+++ b/app/javascript/flavours/glitch/features/composer/reply/index.js
@@ -25,7 +25,7 @@ const messages = defineMessages({
 const handlers = {
 
   //  Handles a click on the "close" button.
-  click () {
+  handleClick () {
     const { onCancel } = this.props;
     if (onCancel) {
       onCancel();
@@ -33,7 +33,7 @@ const handlers = {
   },
 
   //  Handles a click on the status's account.
-  clickAccount () {
+  handleClickAccount () {
     const {
       account,
       history,
@@ -56,8 +56,8 @@ export default class ComposerReply extends React.PureComponent {
   //  Rendering.
   render () {
     const {
-      click,
-      clickAccount,
+      handleClick,
+      handleClickAccount,
     } = this.handlers;
     const {
       account,
@@ -72,14 +72,14 @@ export default class ComposerReply extends React.PureComponent {
           <IconButton
             className='cancel'
             icon='times'
-            onClick={click}
+            onClick={handleClick}
             title={intl.formatMessage(messages.cancel)}
           />
           {account ? (
             <a
               className='account'
               href={account.get('url')}
-              onClick={clickAccount}
+              onClick={handleClickAccount}
             >
               <Avatar
                 account={account}
diff --git a/app/javascript/flavours/glitch/features/composer/spoiler/index.js b/app/javascript/flavours/glitch/features/composer/spoiler/index.js
index 730ab2205..a49b0e10f 100644
--- a/app/javascript/flavours/glitch/features/composer/spoiler/index.js
+++ b/app/javascript/flavours/glitch/features/composer/spoiler/index.js
@@ -24,7 +24,7 @@ const messages = defineMessages({
 const handlers = {
 
   //  Handles a keypress.
-  keyDown ({
+  handleKeyDown ({
     ctrlKey,
     keyCode,
     metaKey,
@@ -49,7 +49,7 @@ export default class ComposerSpoiler extends React.PureComponent {
 
   //  Rendering.
   render () {
-    const { keyDown } = this.handlers;
+    const { handleKeyDown } = this.handlers;
     const {
       hidden,
       intl,
@@ -70,7 +70,7 @@ export default class ComposerSpoiler extends React.PureComponent {
           <input
             id='glitch.composer.spoiler.input'
             onChange={onChange}
-            onKeyDown={keyDown}
+            onKeyDown={handleKeyDown}
             placeholder={intl.formatMessage(messages.placeholder)}
             type='text'
             value={text}
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js
index 2299757df..1b6f79bba 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/index.js
@@ -31,14 +31,14 @@ const messages = defineMessages({
 const handlers = {
 
   //  When blurring the textarea, suggestions are hidden.
-  blur () {
+  handleBlur () {
     this.setState({ suggestionsHidden: true });
   },
 
   //  When the contents of the textarea change, we have to pull up new
   //  autosuggest suggestions if applicable, and also change the value
   //  of the textarea in our store.
-  change ({
+  handleChange ({
     target: {
       selectionStart,
       value,
@@ -91,7 +91,7 @@ const handlers = {
   },
 
   //  Handles a click on an autosuggestion.
-  clickSuggestion (index) {
+  handleClickSuggestion (index) {
     const { textarea } = this;
     const {
       onSuggestionSelected,
@@ -107,7 +107,7 @@ const handlers = {
 
   //  Handles a keypress.  If the autosuggestions are visible, we need
   //  to allow keypresses to navigate and sleect them.
-  keyDown (e) {
+  handleKeyDown (e) {
     const {
       disabled,
       onSubmit,
@@ -165,7 +165,7 @@ const handlers = {
 
   //  When the escape key is released, we either close the suggestions
   //  window or focus the UI.
-  keyUp ({ key }) {
+  handleKeyUp ({ key }) {
     const { suggestionsHidden } = this.state;
     if (key === 'Escape') {
       if (!suggestionsHidden) {
@@ -177,7 +177,7 @@ const handlers = {
   },
 
   //  Handles the pasting of images into the composer.
-  paste (e) {
+  handlePaste (e) {
     const { onPaste } = this.props;
     let d;
     if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) {
@@ -187,7 +187,7 @@ const handlers = {
   },
 
   //  Saves a reference to the textarea.
-  refTextarea (textarea) {
+  handleRefTextarea (textarea) {
     this.textarea = textarea;
   },
 };
@@ -223,13 +223,13 @@ export default class ComposerTextarea extends React.Component {
   //  Rendering.
   render () {
     const {
-      blur,
-      change,
-      clickSuggestion,
-      keyDown,
-      keyUp,
-      paste,
-      refTextarea,
+      handleBlur,
+      handleChange,
+      handleClickSuggestion,
+      handleKeyDown,
+      handleKeyUp,
+      handlePaste,
+      handleRefTextarea,
     } = this.handlers;
     const {
       autoFocus,
@@ -254,12 +254,12 @@ export default class ComposerTextarea extends React.Component {
             autoFocus={autoFocus}
             className='textarea'
             disabled={disabled}
-            inputRef={refTextarea}
-            onBlur={blur}
-            onChange={change}
-            onKeyDown={keyDown}
-            onKeyUp={keyUp}
-            onPaste={paste}
+            inputRef={handleRefTextarea}
+            onBlur={handleBlur}
+            onChange={handleChange}
+            onKeyDown={handleKeyDown}
+            onKeyUp={handleKeyUp}
+            onPaste={handlePaste}
             placeholder={intl.formatMessage(messages.placeholder)}
             value={value}
             style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }}
@@ -268,7 +268,7 @@ export default class ComposerTextarea extends React.Component {
         <EmojiPicker onPickEmoji={onPickEmoji} />
         <ComposerTextareaSuggestions
           hidden={suggestionsHidden}
-          onSuggestionClick={clickSuggestion}
+          onSuggestionClick={handleClickSuggestion}
           suggestions={suggestions}
           value={selectedSuggestion}
         />
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js
index 65ed2c18a..dc72585f2 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js
@@ -18,9 +18,9 @@ export default function ComposerTextareaSuggestions ({
   return (
     <div
       className='composer--textarea--suggestions'
-      hidden={hidden || suggestions.isEmpty()}
+      hidden={hidden || !suggestions || suggestions.isEmpty()}
     >
-      {!hidden ? suggestions.map(
+      {!hidden && suggestions ? suggestions.map(
         (suggestion, index) => (
           <ComposerTextareaSuggestionsItem
             index={index}
@@ -39,5 +39,5 @@ ComposerTextareaSuggestions.propTypes = {
   hidden: PropTypes.bool,
   onSuggestionClick: PropTypes.func,
   suggestions: ImmutablePropTypes.list,
-  value: PropTypes.string,
+  value: PropTypes.number,
 };
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
index b78f99ee9..dc057e679 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
@@ -17,7 +17,7 @@ const assetHost = ((process || {}).env || {}).CDN_HOST || '';
 const handlers = {
 
   //  Handles a click on a suggestion.
-  click (e) {
+  handleClick (e) {
     const {
       index,
       onClick,
@@ -40,7 +40,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
 
   //  Rendering.
   render () {
-    const { click } = this.handlers;
+    const { handleClick } = this.handlers;
     const {
       selected,
       suggestion,
@@ -51,7 +51,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
     return (
       <div
         className={computedClass}
-        onMouseDown={click}
+        onMouseDown={handleClick}
         role='button'
         tabIndex='0'
       >
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
index ab46a3046..53b14acc7 100644
--- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
@@ -10,45 +10,44 @@ import ComposerUploadFormProgress from './progress';
 
 //  The component.
 export default function ComposerUploadForm ({
-  active,
   intl,
   media,
   onChangeDescription,
   onRemove,
   progress,
+  uploading,
 }) {
-  const computedClass = classNames('composer--upload_form', { uploading: active });
-
-  //  We need `media` in order to be able to render.
-  if (!media) {
-    return null;
-  }
+  const computedClass = classNames('composer--upload_form', { uploading });
 
   //  The result.
   return (
     <div className={computedClass}>
-      {active ? <ComposerUploadFormProgress progress={progress} /> : null}
-      {media.map(item => (
-        <ComposerUploadFormItem
-          description={item.get('description')}
-          key={item.get('id')}
-          id={item.get('id')}
-          intl={intl}
-          preview={item.get('preview_url')}
-          onChangeDescription={onChangeDescription}
-          onRemove={onRemove}
-        />
-      ))}
+      {uploading ? <ComposerUploadFormProgress progress={progress} /> : null}
+      {media ? (
+        <div className='content'>
+          {media.map(item => (
+            <ComposerUploadFormItem
+              description={item.get('description')}
+              key={item.get('id')}
+              id={item.get('id')}
+              intl={intl}
+              preview={item.get('preview_url')}
+              onChangeDescription={onChangeDescription}
+              onRemove={onRemove}
+            />
+          ))}
+        </div>
+      ) : null}
     </div>
   );
 }
 
 //  Props.
 ComposerUploadForm.propTypes = {
-  active: PropTypes.bool,
   intl: PropTypes.object.isRequired,
   media: ImmutablePropTypes.list,
   onChangeDescription: PropTypes.func,
   onRemove: PropTypes.func,
   progress: PropTypes.number,
+  uploading: PropTypes.bool,
 };
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
index cbec5ecd9..ec67b8ef8 100644
--- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
@@ -31,7 +31,7 @@ const messages = defineMessages({
 const handlers = {
 
   //  On blur, we save the description for the media item.
-  blur () {
+  handleBlur () {
     const {
       id,
       onChangeDescription,
@@ -48,27 +48,27 @@ const handlers = {
 
   //  When the value of our description changes, we store it in the
   //  temp value `dirtyDescription` in our state.
-  change ({ target: { value } }) {
+  handleChange ({ target: { value } }) {
     this.setState({ dirtyDescription: value });
   },
 
   //  Records focus on the media item.
-  focus () {
+  handleFocus () {
     this.setState({ focused: true });
   },
 
   //  Records the start of a hover over the media item.
-  mouseEnter () {
+  handleMouseEnter () {
     this.setState({ hovered: true });
   },
 
   //  Records the end of a hover over the media item.
-  mouseLeave () {
+  handleMouseLeave () {
     this.setState({ hovered: false });
   },
 
   //  Removes the media item.
-  remove () {
+  handleRemove () {
     const {
       id,
       onRemove,
@@ -85,7 +85,7 @@ export default class ComposerUploadFormItem extends React.PureComponent {
   //  Constructor.
   constructor (props) {
     super(props);
-    assignHandlers(handlers);
+    assignHandlers(this, handlers);
     this.state = {
       hovered: false,
       focused: false,
@@ -96,12 +96,12 @@ export default class ComposerUploadFormItem extends React.PureComponent {
   //  Rendering.
   render () {
     const {
-      blur,
-      change,
-      focus,
-      mouseEnter,
-      mouseLeave,
-      remove,
+      handleBlur,
+      handleChange,
+      handleFocus,
+      handleMouseEnter,
+      handleMouseLeave,
+      handleRemove,
     } = this.handlers;
     const {
       description,
@@ -119,8 +119,8 @@ export default class ComposerUploadFormItem extends React.PureComponent {
     return (
       <div
         className={computedClass}
-        onMouseEnter={mouseEnter}
-        onMouseLeave={mouseLeave}
+        onMouseEnter={handleMouseEnter}
+        onMouseLeave={handleMouseLeave}
       >
         <Motion
           defaultStyle={{ scale: 0.8 }}
@@ -141,7 +141,7 @@ export default class ComposerUploadFormItem extends React.PureComponent {
               <IconButton
                 className='close'
                 icon='times'
-                onClick={remove}
+                onClick={handleRemove}
                 size={36}
                 title={intl.formatMessage(messages.undo)}
               />
@@ -149,9 +149,9 @@ export default class ComposerUploadFormItem extends React.PureComponent {
                 <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
                 <input
                   maxLength={420}
-                  onBlur={blur}
-                  onChange={change}
-                  onFocus={focus}
+                  onBlur={handleBlur}
+                  onChange={handleChange}
+                  onFocus={handleFocus}
                   placeholder={intl.formatMessage(messages.description)}
                   type='text'
                   value={dirtyDescription || description || ''}
@@ -169,7 +169,7 @@ export default class ComposerUploadFormItem extends React.PureComponent {
 //  Props.
 ComposerUploadFormItem.propTypes = {
   description: PropTypes.string,
-  id: PropTypes.number,
+  id: PropTypes.string,
   intl: PropTypes.object.isRequired,
   onChangeDescription: PropTypes.func,
   onRemove: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/drawer/account/index.js b/app/javascript/flavours/glitch/features/drawer/account/index.js
index 9afe8ba3e..168d0c2cf 100644
--- a/app/javascript/flavours/glitch/features/drawer/account/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/account/index.js
@@ -27,7 +27,7 @@ export default function DrawerAccount ({ account }) {
   //  We need an account to render.
   if (!account) {
     return (
-      <div className='drawer--pager--account'>
+      <div className='drawer--account'>
         <a
           className='edit'
           href='/settings/profile'
@@ -40,7 +40,7 @@ export default function DrawerAccount ({ account }) {
 
   //  The result.
   return (
-    <div className='drawer--pager--account'>
+    <div className='drawer--account'>
       <Permalink
         className='avatar'
         href={account.get('url')}
@@ -67,4 +67,5 @@ export default function DrawerAccount ({ account }) {
   );
 }
 
+//  Props.
 DrawerAccount.propTypes = { account: ImmutablePropTypes.map };
diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js
index fd79b6e18..6949cd028 100644
--- a/app/javascript/flavours/glitch/features/drawer/header/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/header/index.js
@@ -51,7 +51,7 @@ export default function DrawerHeader ({
 }) {
 
   //  Only renders the component if the column isn't being shown.
-  const renderForColumn = conditionalRender.bind(
+  const renderForColumn = conditionalRender.bind(null,
     columnId => !columns || !columns.some(
       column => column.get('id') === columnId
     )
@@ -110,6 +110,7 @@ export default function DrawerHeader ({
   );
 }
 
+//  Props.
 DrawerHeader.propTypes = {
   columns: ImmutablePropTypes.list,
   intl: PropTypes.object,
diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js
index d184dfd9b..9ade1f87a 100644
--- a/app/javascript/flavours/glitch/features/drawer/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/index.js
@@ -34,23 +34,13 @@ const mapStateToProps = state => ({
 });
 
 //  Dispatch mapping.
-const mapDispatchToProps = dispatch => ({
-  change (value) {
-    dispatch(changeSearch(value));
-  },
-  clear () {
-    dispatch(clearSearch());
-  },
-  show () {
-    dispatch(showSearch());
-  },
-  submit () {
-    dispatch(submitSearch());
-  },
-  openSettings () {
-    dispatch(openModal('SETTINGS', {}));
-  },
-});
+const mapDispatchToProps = {
+  onChange: changeSearch,
+  onClear: clearSearch,
+  onShow: showSearch,
+  onSubmit: submitSearch,
+  onOpenSettings: openModal.bind(null, 'SETTINGS', {}),
+};
 
 //  The component.
 class Drawer extends React.Component {
@@ -63,23 +53,19 @@ class Drawer extends React.Component {
   //  Rendering.
   render () {
     const {
-      dispatch: {
-        change,
-        clear,
-        openSettings,
-        show,
-        submit,
-      },
+      account,
+      columns,
       intl,
       multiColumn,
-      state: {
-        account,
-        columns,
-        results,
-        searchHidden,
-        searchValue,
-        submitted,
-      },
+      onChange,
+      onClear,
+      onOpenSettings,
+      onShow,
+      onSubmit,
+      results,
+      searchHidden,
+      searchValue,
+      submitted,
     } = this.props;
 
     //  The result.
@@ -89,15 +75,15 @@ class Drawer extends React.Component {
           <DrawerHeader
             columns={columns}
             intl={intl}
-            onSettingsClick={openSettings}
+            onSettingsClick={onOpenSettings}
           />
         ) : null}
         <DrawerSearch
           intl={intl}
-          onChange={change}
-          onClear={clear}
-          onShow={show}
-          onSubmit={submit}
+          onChange={onChange}
+          onClear={onClear}
+          onShow={onShow}
+          onSubmit={onSubmit}
           submitted={submitted}
           value={searchValue}
         />
@@ -117,23 +103,23 @@ class Drawer extends React.Component {
 
 //  Props.
 Drawer.propTypes = {
-  dispatch: PropTypes.func.isRequired,
   intl: PropTypes.object.isRequired,
   multiColumn: PropTypes.bool,
-  state: PropTypes.shape({
-    account: ImmutablePropTypes.map,
-    columns: ImmutablePropTypes.list,
-    results: ImmutablePropTypes.map,
-    searchHidden: PropTypes.bool,
-    searchValue: PropTypes.string,
-    submitted: PropTypes.bool,
-  }).isRequired,
-};
 
-//  Default props.
-Drawer.defaultProps = {
-  dispatch: {},
-  state: {},
+  //  State props.
+  account: ImmutablePropTypes.map,
+  columns: ImmutablePropTypes.list,
+  results: ImmutablePropTypes.map,
+  searchHidden: PropTypes.bool,
+  searchValue: PropTypes.string,
+  submitted: PropTypes.bool,
+
+  //  Dispatch props.
+  onChange: PropTypes.func,
+  onClear: PropTypes.func,
+  onShow: PropTypes.func,
+  onSubmit: PropTypes.func,
+  onOpenSettings: PropTypes.func,
 };
 
 //  Connecting and export.
diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js
index 559d56da5..f2a79eb59 100644
--- a/app/javascript/flavours/glitch/features/drawer/results/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/results/index.js
@@ -25,7 +25,7 @@ const messages = defineMessages({
 });
 
 //  The component.
-export default function DrawerPager ({
+export default function DrawerResults ({
   results,
   visible,
 }) {
@@ -33,6 +33,7 @@ export default function DrawerPager ({
   const statuses = results ? results.get('statuses') : null;
   const hashtags = results ? results.get('hashtags') : null;
 
+  //  This gets the total number of items.
   const count = [accounts, statuses, hashtags].reduce(function (size, item) {
     if (item && item.size) {
       return size + item.size;
@@ -108,7 +109,8 @@ export default function DrawerPager ({
   );
 }
 
-DrawerPager.propTypes = {
+//  Props.
+DrawerResults.propTypes = {
   results: ImmutablePropTypes.map,
   visible: PropTypes.bool,
 };
diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js
index ed69f71ed..2d739349c 100644
--- a/app/javascript/flavours/glitch/features/drawer/search/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/search/index.js
@@ -30,18 +30,18 @@ const messages = defineMessages({
 //  Handlers.
 const handlers = {
 
-  blur () {
+  handleBlur () {
     this.setState({ expanded: false });
   },
 
-  change ({ target: { value } }) {
+  handleChange ({ target: { value } }) {
     const { onChange } = this.props;
     if (onChange) {
       onChange(value);
     }
   },
 
-  clear (e) {
+  handleClear (e) {
     const {
       onClear,
       submitted,
@@ -53,7 +53,7 @@ const handlers = {
     }
   },
 
-  focus () {
+  handleFocus () {
     const { onShow } = this.props;
     this.setState({ expanded: true });
     if (onShow) {
@@ -61,7 +61,7 @@ const handlers = {
     }
   },
 
-  keyUp (e) {
+  handleKeyUp (e) {
     const { onSubmit } = this.props;
     switch (e.key) {
     case 'Enter':
@@ -78,19 +78,21 @@ const handlers = {
 //  The component.
 export default class DrawerSearch extends React.PureComponent {
 
+  //  Constructor.
   constructor (props) {
     super(props);
     assignHandlers(this, handlers);
     this.state = { expanded: false };
   }
 
+  //  Rendering.
   render () {
     const {
-      blur,
-      change,
-      clear,
-      focus,
-      keyUp,
+      handleBlur,
+      handleChange,
+      handleClear,
+      handleFocus,
+      handleKeyUp,
     } = this.handlers;
     const {
       intl,
@@ -110,23 +112,22 @@ export default class DrawerSearch extends React.PureComponent {
             type='text'
             placeholder={intl.formatMessage(messages.placeholder)}
             value={value || ''}
-            onChange={change}
-            onKeyUp={keyUp}
-            onFocus={focus}
-            onBlur={blur}
+            onChange={handleChange}
+            onKeyUp={handleKeyUp}
+            onFocus={handleFocus}
+            onBlur={handleBlur}
           />
         </label>
         <div
           aria-label={intl.formatMessage(messages.placeholder)}
           className='icon'
-          onClick={clear}
+          onClick={handleClear}
           role='button'
           tabIndex='0'
         >
           <Icon icon='search' />
           <Icon icon='fa-times-circle' />
         </div>
-
         <Overlay
           placement='bottom'
           show={expanded && !(value || '').length && !submitted}
@@ -138,6 +139,7 @@ export default class DrawerSearch extends React.PureComponent {
 
 }
 
+//  Props.
 DrawerSearch.propTypes = {
   value: PropTypes.string,
   submitted: PropTypes.bool,
diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
index bd36275f5..b5ea86ff1 100644
--- a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
@@ -34,9 +34,13 @@ const messages = defineMessages({
   },
 });
 
+//  The spring used by our motion.
 const motionSpring = spring(1, { damping: 35, stiffness: 400 });
 
+//  The component.
 export default function DrawerSearchPopout ({ style }) {
+
+  //  The result.
   return (
     <Motion
       defaultStyle={{
diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
index 020cc0dd6..c8b040f95 100644
--- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
@@ -50,7 +50,7 @@ export default class ActionsModal extends ImmutablePureComponent {
         <Link
           className={classNames('link', { active })}
           href={href}
-          onClick={onClick}
+          onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
           role={onClick ? 'button' : null}
         >
           {function () {
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 91d4df93f..e4556899d 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -11,13 +11,13 @@ import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
+import { Drawer, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
 
 import detectPassiveEvents from 'detect-passive-events';
 import { scrollRight } from 'flavours/glitch/util/scroll';
 
 const componentMap = {
-  'COMPOSE': Compose,
+  'COMPOSE': Drawer,
   'HOME': HomeTimeline,
   'NOTIFICATIONS': Notifications,
   'PUBLIC': PublicTimeline,
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 5c80ea07b..fae705deb 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -17,7 +17,7 @@ import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
 import classNames from 'classnames';
 import {
-  Compose,
+  Drawer,
   Status,
   GettingStarted,
   KeyboardShortcuts,
@@ -56,7 +56,6 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = state => ({
-  isComposing: state.getIn(['compose', 'is_composing']),
   hasComposingText: state.getIn(['compose', 'text']) !== '',
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
@@ -120,9 +119,9 @@ export default class UI extends React.Component {
   };
 
   handleBeforeUnload = (e) => {
-    const { intl, isComposing, hasComposingText } = this.props;
+    const { intl, hasComposingText } = this.props;
 
-    if (isComposing && hasComposingText) {
+    if (hasComposingText) {
       // Setting returnValue to any string causes confirmation dialog.
       // Many browsers no longer display this text to users,
       // but we set user-friendly message for other browsers, e.g. Edge.
@@ -227,9 +226,8 @@ export default class UI extends React.Component {
   }
 
   shouldComponentUpdate (nextProps) {
-    if (nextProps.isComposing !== this.props.isComposing) {
+    if (nextProps.navbarUnder !== this.props.navbarUnder) {
       // Avoid expensive update just to toggle a class
-      this.node.classList.toggle('is-composing', nextProps.isComposing);
       this.node.classList.toggle('navbar-under', nextProps.navbarUnder);
 
       return false;
@@ -427,7 +425,7 @@ export default class UI extends React.Component {
               <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
               <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 
-              <WrappedRoute path='/statuses/new' component={Compose} content={children} />
+              <WrappedRoute path='/statuses/new' component={Drawer} content={children} />
               <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
               <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
               <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 17c5e3a3a..c5d7b03ac 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -47,7 +47,6 @@ const initialState = ImmutableMap({
   focusDate: null,
   preselectDate: null,
   in_reply_to: null,
-  is_composing: false,
   is_submitting: false,
   is_uploading: false,
   progress: 0,
@@ -180,9 +179,7 @@ export default function compose(state = initialState, action) {
   case COMPOSE_MOUNT:
     return state.set('mounted', true);
   case COMPOSE_UNMOUNT:
-    return state
-      .set('mounted', false)
-      .set('is_composing', false);
+    return state.set('mounted', false)
   case COMPOSE_ADVANCED_OPTIONS_CHANGE:
     return state
       .set('advanced_options',
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index ae9114644..4b09d80d6 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -1,22 +1,24 @@
 .composer { padding: 10px }
 
 .composer--spoiler {
-  display: block;
-  box-sizing: border-box;
-  margin: 0;
-  border: none;
-  border-radius: 4px;
-  padding: 10px;
-  width: 100%;
-  outline: 0;
-  color: $ui-base-color;
-  background: $simple-background-color;
-  font-size: 14px;
-  font-family: inherit;
-  resize: vertical;
+  input {
+    display: block;
+    box-sizing: border-box;
+    margin: 0;
+    border: none;
+    border-radius: 4px;
+    padding: 10px;
+    width: 100%;
+    outline: 0;
+    color: $ui-base-color;
+    background: $simple-background-color;
+    font-size: 14px;
+    font-family: inherit;
+    resize: vertical;
 
-  &:focus { outline: 0 }
-  @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+    &:focus { outline: 0 }
+    @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+  }
 }
 
 .composer--warning {
@@ -116,33 +118,33 @@
 }
 
 .composer--textarea {
-  background: $simple-background-color;
   position: relative;
 
-  &:disabled { background: $ui-secondary-color }
-
-  & > .textarea {
-    display: block;
-    box-sizing: border-box;
-    margin: 0;
-    border: none;
-    border-radius: 4px 4px 0 0;
-    padding: 10px 32px 0 10px;
-    width: 100%;
-    min-height: 100px;
-    outline: 0;
-    color: $ui-base-color;
-    background: $simple-background-color;
-    font-size: 14px;
-    font-family: inherit;
-    resize: none;
+  & > label {
+    .textarea {
+      display: block;
+      box-sizing: border-box;
+      margin: 0;
+      border: none;
+      border-radius: 4px 4px 0 0;
+      padding: 10px 32px 0 10px;
+      width: 100%;
+      min-height: 100px;
+      outline: 0;
+      color: $ui-base-color;
+      background: $simple-background-color;
+      font-size: 14px;
+      font-family: inherit;
+      resize: none;
 
-    &:focus { outline: 0 }
-    @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+      &:disabled { background: $ui-secondary-color }
+      &:focus { outline: 0 }
+      @include single-column('screen and (max-width: 630px)') { font-size: 16px }
 
-    @include limited-single-column('screen and (max-width: 600px)') {
-      height: 100px !important; // prevent auto-resize textarea
-      resize: vertical;
+      @include limited-single-column('screen and (max-width: 600px)') {
+        height: 100px !important; // prevent auto-resize textarea
+        resize: vertical;
+      }
     }
   }
 }
@@ -192,15 +194,18 @@
 }
 
 .composer--upload_form {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
   padding: 5px;
   color: $ui-base-color;
   background: $simple-background-color;
   font-size: 14px;
-  font-family: inherit;
-  overflow: hidden;
+
+  & > .content {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    font-family: inherit;
+    overflow: hidden;
+  }
 }
 
 .composer--upload_form--item {
@@ -254,17 +259,61 @@
   }
 }
 
+.composer--upload_form--progress {
+  display: flex;
+  padding: 10px;
+  color: $ui-base-lighter-color;
+  overflow: hidden;
+
+  & > .fa {
+    font-size: 34px;
+    margin-right: 10px;
+  }
+
+  & > .message {
+    flex: 1 1 auto;
+
+    & > span {
+      display: block;
+      font-size: 12px;
+      font-weight: 500;
+      text-transform: uppercase;
+    }
+
+    & > .backdrop {
+      position: relative;
+      margin-top: 5px;
+      border-radius: 6px;
+      width: 100%;
+      height: 6px;
+      background: $ui-base-lighter-color;
+
+      & > .tracker {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 6px;
+        border-radius: 6px;
+        background: $ui-highlight-color;
+      }
+    }
+  }
+}
+
 .composer--options {
   padding: 10px;
   background: darken($simple-background-color, 8%);
   box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
   border-radius: 0 0 4px 4px;
+  height: 27px;
 
   & > * {
     display: inline-block;
     box-sizing: content-box;
     padding: 0 3px;
+    height: 27px;
     line-height: 27px;
+    vertical-align: bottom;
   }
 
   & > hr {
@@ -274,26 +323,26 @@
     border-style: none none none solid;
     border-color: transparent transparent transparent darken($simple-background-color, 24%);
     padding: 0;
+    width: 0;
+    height: 27px;
     background: transparent;
   }
 }
 
 .composer--options--dropdown {
-  & > .value { transition: none }
-
-  &.active {
+  &.open {
     & > .value {
       border-radius: 4px 4px 0 0;
       box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
       color: $primary-text-color;
       background: $ui-highlight-color;
+      transition: none;
     }
   }
 }
 
-.composer--options--dropdown__dropdown {
+.composer--options--dropdown--content {
   position: absolute;
-  margin-left: 40px;
   border-radius: 4px;
   box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
   background: $simple-background-color;
@@ -301,11 +350,12 @@
   transform-origin: 50% 0;
 }
 
-.composer--options--dropdown--item {
-  color: $ui-base-color;
+.composer--options--dropdown--content--item {
+  display: flex;
+  align-items: center;
   padding: 10px;
+  color: $ui-base-color;
   cursor: pointer;
-  display: flex;
 
   & > .content {
     flex: 1 1 auto;
@@ -344,7 +394,6 @@
   & > .count {
     display: inline-block;
     margin: 0 16px 0 8px;
-    padding-top: 10px;
     font-size: 16px;
     line-height: 36px;
   }
diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss
index 8ad55c79b..ebf996907 100644
--- a/app/javascript/flavours/glitch/styles/components/drawer.scss
+++ b/app/javascript/flavours/glitch/styles/components/drawer.scss
@@ -4,7 +4,7 @@
   box-sizing: border-box;
   padding: 10px 5px;
   width: 300px;
-  flex: 1 1 100%;
+  flex: none;
   contain: strict;
 
   &:first-child {
@@ -15,10 +15,10 @@
     padding-right: 10px;
   }
 
-  @include multi-columns('screen and (max-width: 630px)') {
-    &, &:first-child, &:last-child {
-      padding: 0;
-    }
+  @include single-column('screen and (max-width: 630px)') { flex: auto }
+
+  @include limited-single-column('screen and (max-width: 630px)') {
+    &, &:first-child, &:last-child { padding: 0 }
   }
 
   .wide & {
@@ -27,207 +27,196 @@
     flex: 1 1 200px;
   }
 
-  .react-swipeable-view-container & {
+  @include single-column('screen and (max-width: 630px)') {
+    :root & {  //  Overrides `.wide` for single-column view
+      flex: auto;
+      width: 100%;
+      min-width: 0;
+      max-width: none;
+      padding: 0;
+    }
+  }
+
+  .react-swipeable-view-container & { height: 100% }
+
+  & > .contents {
+    position: relative;
+    padding: 0;
+    width: 100%;
     height: 100%;
+    background: lighten($ui-base-color, 13%);
+    overflow-x: hidden;
+    overflow-y: auto;
+    contain: strict;
+  }
+}
+
+.drawer--header {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 10px;
+  flex: none;
+  background: lighten($ui-base-color, 8%);
+  font-size: 16px;
+
+  & > * {
+    display: block;
+    box-sizing: border-box;
+    border-bottom: 2px solid transparent;
+    padding: 15px 5px 13px;
+    height: 48px;
+    flex: 1 1 auto;
+    color: $ui-primary-color;
+    text-align: center;
+    text-decoration: none;
+    cursor: pointer;
   }
 
-  .drawer--header {
-    display: flex;
-    flex-direction: row;
-    margin-bottom: 10px;
-    flex: none;
-    background: lighten($ui-base-color, 8%);
-    font-size: 16px;
+  a {
+    transition: background 100ms ease-in;
 
-    & > * {
-      display: block;
-      box-sizing: border-box;
-      border-bottom: 2px solid transparent;
-      padding: 15px 5px 13px;
-      height: 48px;
-      flex: 1 1 auto;
-      color: $ui-primary-color;
-      text-align: center;
-      text-decoration: none;
-      cursor: pointer;
+    &:focus,
+    &:hover {
+      outline: none;
+      background: lighten($ui-base-color, 3%);
+      transition: background 200ms ease-out;
     }
+  }
+}
 
-    a {
-      transition: background 100ms ease-in;
+.drawer--search {
+  position: relative;
+  margin-bottom: 10px;
+  flex: none;
 
-      &:focus,
-      &:hover {
-        outline: none;
-        background: lighten($ui-base-color, 3%);
-        transition: background 200ms ease-out;
-      }
+  @include limited-single-column('screen and (max-width: 360px)') { margin-bottom: 0 }
+  @include single-column('screen and (max-width: 630px)') { font-size: 16px }
+
+  input {
+    display: block;
+    box-sizing: border-box;
+    margin: 0;
+    border: none;
+    padding: 10px 30px 10px 10px;
+    width: 100%;
+    height: 36px;
+    outline: 0;
+    color: $ui-primary-color;
+    background: $ui-base-color;
+    font-size: 14px;
+    font-family: inherit;
+    line-height: 16px;
+
+    &:focus {
+      outline: 0;
+      background: lighten($ui-base-color, 4%);
     }
   }
 
-  .drawer--search {
-    position: relative;
-    margin-bottom: 10px;
-    flex: none;
+  & > .icon {
+    .fa {
+      display: inline-block;
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      width: 18px;
+      height: 18px;
+      color: $ui-secondary-color;
+      font-size: 18px;
+      opacity: 0;
+      cursor: default;
+      pointer-events: none;
+      z-index: 2;
+      transition: all 100ms linear;
+    }
 
-    @include limited-single-column('screen and (max-width: 360px)') {
-      margin-bottom: 0;
+    .fa-search {
+      opacity: 0.3;
+      transform: rotate(0deg);
     }
 
-    input {
-      display: block;
-      box-sizing: border-box;
-      margin: 0;
-      border: none;
-      padding: 10px 30px 10px 10px;
-      width: 100%;
-      height: 36px;
-      outline: 0;
-      color: $ui-primary-color;
-      background: $ui-base-color;
-      font-size: 14px;
-      font-family: inherit;
-      line-height: 16px;
+    .fa-times-circle {
+      top: 11px;
+      transform: rotate(-90deg);
+      cursor: pointer;
 
-      &:focus {
-        outline: 0;
-        background: lighten($ui-base-color, 4%);
-      }
+      &:hover { color: $primary-text-color }
     }
 
-    & > .icon {
-      .fa {
-        display: inline-block;
-        position: absolute;
-        top: 10px;
-        right: 10px;
-        width: 18px;
-        height: 18px;
-        color: $ui-secondary-color;
-        font-size: 18px;
+    &.active {
+      .fa-search {
         opacity: 0;
-        cursor: default;
-        pointer-events: none;
-        z-index: 2;
-        transition: all 100ms linear;
+        transform: rotate(90deg);
       }
 
-      .fa-search {
+      .fa-times-circle {
         opacity: 0.3;
+        pointer-events: auto;
         transform: rotate(0deg);
       }
-
-      .fa-times-circle {
-        top: 11px;
-        transform: rotate(-90deg);
-        cursor: pointer;
-
-        &:hover {
-          color: $primary-text-color;
-        }
-      }
-
-      &.active {
-        .fa-search {
-          opacity: 0;
-          transform: rotate(90deg);
-        }
-
-        .fa-times-circle {
-          opacity: 0.3;
-          pointer-events: auto;
-          transform: rotate(0deg);
-        }
-      }
     }
   }
+}
 
-  & > .contents {
-    position: relative;
-    padding: 0;
-    width: 100%;
-    height: 100%;
-    background: lighten($ui-base-color, 13%);
-    overflow-x: hidden;
-    overflow-y: auto;
-    contain: strict;
-
-    .drawer--account {
-      padding: 10px;
-      color: $ui-primary-color;
+.drawer--account {
+  padding: 10px;
+  color: $ui-primary-color;
 
-      & > a {
-        color: inherit;
-        text-decoration: none;
-      }
+  & > a {
+    color: inherit;
+    text-decoration: none;
+  }
 
-      & > .avatar {
-        float: left;
-        margin-right: 10px;
-      }
+  & > .avatar {
+    float: left;
+    margin-right: 10px;
+  }
 
-      & > .acct {
-        display: block;
-        color: $primary-text-color;
-        font-weight: 500;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-      }
-    }
+  & > .acct {
+    display: block;
+    color: $primary-text-color;
+    font-weight: 500;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
 
-    .drawer--results {
-      position: absolute;
-      top: 0;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      padding: 0;
-      background: $ui-base-color;
-      overflow-x: hidden;
-      overflow-y: auto;
-      contain: strict;
-
-      & > header {
-        border-bottom: 1px solid darken($ui-base-color, 4%);
-        padding: 15px 10px;
-        color: $ui-base-lighter-color;
-        background: lighten($ui-base-color, 2%);
-        font-size: 14px;
-        font-weight: 500;
-      }
+.drawer--results {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 0;
+  background: $ui-base-color;
+  overflow-x: hidden;
+  overflow-y: auto;
+  contain: strict;
 
-      & > section {
-        background: $ui-base-color;
-
-        & > .hashtag {
-          display: block;
-          padding: 10px;
-          color: $ui-secondary-color;
-          text-decoration: none;
-
-          &:hover,
-          &:active,
-          &:focus {
-            color: lighten($ui-secondary-color, 4%);
-            text-decoration: underline;
-          }
-        }
-      }
-    }
+  & > header {
+    border-bottom: 1px solid darken($ui-base-color, 4%);
+    padding: 15px 10px;
+    color: $ui-base-lighter-color;
+    background: lighten($ui-base-color, 2%);
+    font-size: 14px;
+    font-weight: 500;
   }
-}
 
-:root {  //  Overrides .wide stylings for mobile view
-  @include single-column('screen and (max-width: 630px)', $parent: null) {
-    .drawer {
-      flex: auto;
-      width: 100%;
-      min-width: 0;
-      max-width: none;
-      padding: 0;
+  & > section {
+    background: $ui-base-color;
 
-      .drawer--search input {
-        font-size: 16px;
+    & > .hashtag {
+      display: block;
+      padding: 10px;
+      color: $ui-secondary-color;
+      text-decoration: none;
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: lighten($ui-secondary-color, 4%);
+        text-decoration: underline;
       }
     }
   }
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 98ed5d24a..0ce0dafc9 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -2704,47 +2704,6 @@
   border-radius: 4px;
 }
 
-.upload-progress {
-  padding: 10px;
-  color: $ui-base-lighter-color;
-  overflow: hidden;
-  display: flex;
-
-  .fa {
-    font-size: 34px;
-    margin-right: 10px;
-  }
-
-  span {
-    font-size: 12px;
-    text-transform: uppercase;
-    font-weight: 500;
-    display: block;
-  }
-}
-
-.upload-progess__message {
-  flex: 1 1 auto;
-}
-
-.upload-progress__backdrop {
-  width: 100%;
-  height: 6px;
-  border-radius: 6px;
-  background: $ui-base-lighter-color;
-  position: relative;
-  margin-top: 5px;
-}
-
-.upload-progress__tracker {
-  position: absolute;
-  left: 0;
-  top: 0;
-  height: 6px;
-  background: $ui-highlight-color;
-  border-radius: 6px;
-}
-
 .emoji-button {
   display: block;
   font-size: 24px;
@@ -3339,6 +3298,7 @@
   max-width: 80vw;
 
   strong {
+    display: block;
     font-weight: 500;
   }
 
@@ -3368,6 +3328,7 @@
           color: $primary-text-color;
         }
 
+        & > .react-toggle,
         & > .icon {
           margin-right: 10px;
         }
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
index 435fa2329..8ccd8fa65 100644
--- a/app/javascript/flavours/glitch/theme.yml
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -11,8 +11,8 @@ pack:
   home:
     filename: packs/home.js
     preload:
+    - flavours/glitch/async/drawer
     - flavours/glitch/async/getting_started
-    - flavours/glitch/async/compose
     - flavours/glitch/async/home_timeline
     - flavours/glitch/async/notifications
   modal:
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 5d21ccca2..b90f1b8c8 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -2,8 +2,8 @@ export function EmojiPicker () {
   return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/util/emoji/emoji_picker');
 }
 
-export function Compose () {
-  return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose');
+export function Drawer () {
+  return import(/* webpackChunkName: "flavours/glitch/async/drawer" */'flavours/glitch/features/drawer');
 }
 
 export function Notifications () {
diff --git a/app/javascript/flavours/glitch/util/react_helpers.js b/app/javascript/flavours/glitch/util/react_helpers.js
index 087e3969d..082a58e62 100644
--- a/app/javascript/flavours/glitch/util/react_helpers.js
+++ b/app/javascript/flavours/glitch/util/react_helpers.js
@@ -6,8 +6,8 @@ export function assignHandlers (target, handlers) {
 
   //  We just bind each handler to the `target`.
   const handle = target.handlers = {};
-  handlers.keys().forEach(
-    key => handle.key = key.bind(target)
+  Object.keys(handlers).forEach(
+    key => handle[key] = handlers[key].bind(target)
   );
 }
 
diff --git a/app/javascript/flavours/glitch/util/redux_helpers.js b/app/javascript/flavours/glitch/util/redux_helpers.js
index c0f5eeb28..8eb338da7 100644
--- a/app/javascript/flavours/glitch/util/redux_helpers.js
+++ b/app/javascript/flavours/glitch/util/redux_helpers.js
@@ -1,16 +1,8 @@
 import { injectIntl } from 'react-intl';
 import { connect } from 'react-redux';
 
-//  Merges react-redux props.
-export function mergeProps (stateProps, dispatchProps, ownProps) {
-  Object.assign({}, ownProps, {
-    dispatch: Object.assign({}, dispatchProps, ownProps.dispatch || {}),
-    state: Object.assign({}, stateProps, ownProps.state || {}),
-  });
-}
-
 //  Connects a component.
 export function wrap (Component, mapStateToProps, mapDispatchToProps, options) {
   const withIntl = typeof options === 'object' ? options.withIntl : !!options;
-  return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component));
+  return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps)(Component));
 }