about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/composer
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer')
-rw-r--r--app/javascript/flavours/glitch/features/composer/direct_warning/index.js55
-rw-r--r--app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js49
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js600
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js146
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js129
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/index.js229
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js377
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js135
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/index.js29
-rw-r--r--app/javascript/flavours/glitch/features/composer/publisher/index.js122
-rw-r--r--app/javascript/flavours/glitch/features/composer/reply/index.js96
-rw-r--r--app/javascript/flavours/glitch/features/composer/spoiler/index.js107
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/icons/index.js60
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/index.js312
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js43
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js118
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/index.js60
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/item/index.js202
-rw-r--r--app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js52
-rw-r--r--app/javascript/flavours/glitch/features/composer/warning/index.js59
20 files changed, 0 insertions, 2980 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js
deleted file mode 100644
index 3b1369acd..000000000
--- a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import Motion from 'flavours/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import { defineMessages, FormattedMessage } from 'react-intl';
-import { termsLink} from 'flavours/glitch/util/backend_links';
-
-//  This is the spring used with our motion.
-const motionSpring = spring(1, { damping: 35, stiffness: 400 });
-
-//  Messages.
-const messages = defineMessages({
-  disclaimer: {
-    defaultMessage: 'This toot will only be sent to all the mentioned users.',
-    id: 'compose_form.direct_message_warning',
-  },
-  learn_more: {
-    defaultMessage: 'Learn more',
-    id: 'compose_form.direct_message_warning_learn_more'
-  }
-});
-
-//  The component.
-export default function ComposerDirectWarning () {
-  return (
-    <Motion
-      defaultStyle={{
-        opacity: 0,
-        scaleX: 0.85,
-        scaleY: 0.75,
-      }}
-      style={{
-        opacity: motionSpring,
-        scaleX: motionSpring,
-        scaleY: motionSpring,
-      }}
-    >
-      {({ opacity, scaleX, scaleY }) => (
-        <div
-          className='composer--warning'
-          style={{
-            opacity: opacity,
-            transform: `scale(${scaleX}, ${scaleY})`,
-          }}
-        >
-          <span>
-            <FormattedMessage {...messages.disclaimer} />
-            { termsLink !== undefined && <a href={termsLink} target='_blank'><FormattedMessage {...messages.learn_more} /></a> }
-          </span>
-        </div>
-      )}
-    </Motion>
-  );
-}
-
-ComposerDirectWarning.propTypes = {};
diff --git a/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js b/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js
deleted file mode 100644
index 716028e4c..000000000
--- a/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import Motion from 'flavours/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import { defineMessages, FormattedMessage } from 'react-intl';
-
-//  This is the spring used with our motion.
-const motionSpring = spring(1, { damping: 35, stiffness: 400 });
-
-//  Messages.
-const messages = defineMessages({
-  disclaimer: {
-    defaultMessage: 'This toot won\'t be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.',
-    id: 'compose_form.hashtag_warning',
-  },
-});
-
-//  The component.
-export default function ComposerHashtagWarning () {
-  return (
-    <Motion
-      defaultStyle={{
-        opacity: 0,
-        scaleX: 0.85,
-        scaleY: 0.75,
-      }}
-      style={{
-        opacity: motionSpring,
-        scaleX: motionSpring,
-        scaleY: motionSpring,
-      }}
-    >
-      {({ opacity, scaleX, scaleY }) => (
-        <div
-          className='composer--warning'
-          style={{
-            opacity: opacity,
-            transform: `scale(${scaleX}, ${scaleY})`,
-          }}
-        >
-          <FormattedMessage
-            {...messages.disclaimer}
-          />
-        </div>
-      )}
-    </Motion>
-  );
-}
-
-ComposerHashtagWarning.propTypes = {};
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
deleted file mode 100644
index 9d2e0b3da..000000000
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ /dev/null
@@ -1,600 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { defineMessages } from 'react-intl';
-
-const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i;
-
-//  Actions.
-import {
-  cancelReplyCompose,
-  changeCompose,
-  changeComposeAdvancedOption,
-  changeComposeSensitivity,
-  changeComposeSpoilerText,
-  changeComposeSpoilerness,
-  changeComposeVisibility,
-  changeUploadCompose,
-  clearComposeSuggestions,
-  fetchComposeSuggestions,
-  insertEmojiCompose,
-  mountCompose,
-  selectComposeSuggestion,
-  submitCompose,
-  undoUploadCompose,
-  unmountCompose,
-  uploadCompose,
-} from 'flavours/glitch/actions/compose';
-import {
-  closeModal,
-  openModal,
-} from 'flavours/glitch/actions/modal';
-import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { addPoll, removePoll } from 'flavours/glitch/actions/compose';
-
-//  Components.
-import ComposerOptions from './options';
-import ComposerPublisher from './publisher';
-import ComposerReply from './reply';
-import ComposerSpoiler from './spoiler';
-import ComposerTextarea from './textarea';
-import ComposerUploadForm from './upload_form';
-import ComposerPollForm from './poll_form';
-import ComposerWarning from './warning';
-import ComposerHashtagWarning from './hashtag_warning';
-import ComposerDirectWarning from './direct_warning';
-
-//  Utils.
-import { countableText } from 'flavours/glitch/util/counter';
-import { me } from 'flavours/glitch/util/initial_state';
-import { isMobile } from 'flavours/glitch/util/is_mobile';
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-import { wrap } from 'flavours/glitch/util/redux_helpers';
-import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
-
-const messages = defineMessages({
-  missingDescriptionMessage: {  id: 'confirmations.missing_media_description.message',
-                                defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
-  missingDescriptionConfirm: {  id: 'confirmations.missing_media_description.confirm',
-                                defaultMessage: 'Send anyway' },
-});
-
-//  State mapping.
-function mapStateToProps (state) {
-  const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
-  const inReplyTo = state.getIn(['compose', 'in_reply_to']);
-  const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null;
-  const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']);
-  const sideArmRestrictedPrivacy = replyPrivacy ? privacyPreference(replyPrivacy, sideArmBasePrivacy) : null;
-  let sideArmPrivacy = null;
-  switch (state.getIn(['local_settings', 'side_arm_reply_mode'])) {
-    case 'copy':
-      sideArmPrivacy = replyPrivacy;
-      break;
-    case 'restrict':
-      sideArmPrivacy = sideArmRestrictedPrivacy;
-      break;
-  }
-  sideArmPrivacy = sideArmPrivacy || sideArmBasePrivacy;
-  return {
-    acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
-    advancedOptions: state.getIn(['compose', 'advanced_options']),
-    amUnlocked: !state.getIn(['accounts', me, 'locked']),
-    focusDate: state.getIn(['compose', 'focusDate']),
-    caretPosition: state.getIn(['compose', 'caretPosition']),
-    isSubmitting: state.getIn(['compose', 'is_submitting']),
-    isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
-    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']),
-    progress: state.getIn(['compose', 'progress']),
-    inReplyTo: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null,
-    replyAccount: inReplyTo ? state.getIn(['statuses', inReplyTo, 'account']) : null,
-    replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
-    resetFileKey: state.getIn(['compose', 'resetFileKey']),
-    sideArm: sideArmPrivacy,
-    sensitive: state.getIn(['compose', 'sensitive']),
-    showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
-    spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']),
-    spoilerText: state.getIn(['compose', 'spoiler_text']),
-    suggestionToken: state.getIn(['compose', 'suggestion_token']),
-    suggestions: state.getIn(['compose', 'suggestions']),
-    text: state.getIn(['compose', 'text']),
-    anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
-    poll: state.getIn(['compose', 'poll']),
-    spoilersAlwaysOn: spoilersAlwaysOn,
-    mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
-    preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
-  };
-};
-
-//  Dispatch mapping.
-const mapDispatchToProps = (dispatch, { intl }) => ({
-  onCancelReply() {
-    dispatch(cancelReplyCompose());
-  },
-  onChangeAdvancedOption(option, value) {
-    dispatch(changeComposeAdvancedOption(option, value));
-  },
-  onChangeDescription(id, description) {
-    dispatch(changeUploadCompose(id, { description }));
-  },
-  onChangeSensitivity() {
-    dispatch(changeComposeSensitivity());
-  },
-  onChangeSpoilerText(text) {
-    dispatch(changeComposeSpoilerText(text));
-  },
-  onChangeSpoilerness() {
-    dispatch(changeComposeSpoilerness());
-  },
-  onChangeText(text) {
-    dispatch(changeCompose(text));
-  },
-  onChangeVisibility(value) {
-    dispatch(changeComposeVisibility(value));
-  },
-  onTogglePoll() {
-    dispatch((_, getState) => {
-      if (getState().getIn(['compose', 'poll'])) {
-        dispatch(removePoll());
-      } else {
-        dispatch(addPoll());
-      }
-    });
-  },
-  onClearSuggestions() {
-    dispatch(clearComposeSuggestions());
-  },
-  onCloseModal() {
-    dispatch(closeModal());
-  },
-  onFetchSuggestions(token) {
-    dispatch(fetchComposeSuggestions(token));
-  },
-  onInsertEmoji(position, emoji) {
-    dispatch(insertEmojiCompose(position, emoji));
-  },
-  onMount() {
-    dispatch(mountCompose());
-  },
-  onOpenActionsModal(props) {
-    dispatch(openModal('ACTIONS', props));
-  },
-  onOpenDoodleModal() {
-    dispatch(openModal('DOODLE', { noEsc: true }));
-  },
-  onOpenFocalPointModal(id) {
-    dispatch(openModal('FOCAL_POINT', { id }));
-  },
-  onSelectSuggestion(position, token, suggestion) {
-    dispatch(selectComposeSuggestion(position, token, suggestion));
-  },
-  onMediaDescriptionConfirm(routerHistory) {
-    dispatch(openModal('CONFIRM', {
-      message: intl.formatMessage(messages.missingDescriptionMessage),
-      confirm: intl.formatMessage(messages.missingDescriptionConfirm),
-      onConfirm: () => dispatch(submitCompose(routerHistory)),
-      onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
-    }));
-  },
-  onSubmit(routerHistory) {
-    dispatch(submitCompose(routerHistory));
-  },
-  onUndoUpload(id) {
-    dispatch(undoUploadCompose(id));
-  },
-  onUnmount() {
-    dispatch(unmountCompose());
-  },
-  onUpload(files) {
-    dispatch(uploadCompose(files));
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  Changes the text value of the spoiler.
-  handleChangeSpoiler ({ target: { value } }) {
-    const { onChangeSpoilerText } = this.props;
-    if (onChangeSpoilerText) {
-      onChangeSpoilerText(value);
-    }
-  },
-
-  //  Inserts an emoji at the caret.
-  handleEmoji (data) {
-    const { textarea: { selectionStart } } = this;
-    const { onInsertEmoji } = this.props;
-    if (onInsertEmoji) {
-      onInsertEmoji(selectionStart, data);
-    }
-  },
-
-  //  Handles the secondary submit button.
-  handleSecondarySubmit () {
-    const { handleSubmit } = this.handlers;
-    const {
-      onChangeVisibility,
-      sideArm,
-    } = this.props;
-    if (sideArm !== 'none' && onChangeVisibility) {
-      onChangeVisibility(sideArm);
-    }
-    handleSubmit();
-  },
-
-  //  Selects a suggestion from the autofill.
-  handleSelect (tokenStart, token, value) {
-    const { onSelectSuggestion } = this.props;
-    if (onSelectSuggestion) {
-      onSelectSuggestion(tokenStart, token, value);
-    }
-  },
-
-  //  Submits the status.
-  handleSubmit () {
-    const { textarea: { value }, uploadForm } = this;
-    const {
-      onChangeText,
-      onSubmit,
-      isSubmitting,
-      isChangingUpload,
-      isUploading,
-      media,
-      anyMedia,
-      text,
-      mediaDescriptionConfirmation,
-      onMediaDescriptionConfirm,
-    } = this.props;
-
-    //  If something changes inside the textarea, then we update the
-    //  state before submitting.
-    if (onChangeText && text !== value) {
-      onChangeText(value);
-    }
-
-    // Submit disabled:
-    if (isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia)) {
-      return;
-    }
-
-    // Submit unless there are media with missing descriptions
-    if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
-      const firstWithoutDescription = media.findIndex(item => !item.get('description'));
-      if (uploadForm) {
-        const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input');
-        if (inputs.length == media.size && firstWithoutDescription !== -1) {
-          inputs[firstWithoutDescription].focus();
-        }
-      }
-      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null);
-    } else if (onSubmit) {
-      onSubmit(this.context.router ? this.context.router.history : null);
-    }
-  },
-
-  //  Sets a reference to the upload form.
-  handleRefUploadForm (uploadFormComponent) {
-    this.uploadForm = uploadFormComponent;
-  },
-
-  //  Sets a reference to the textarea.
-  handleRefTextarea (textareaComponent) {
-    if (textareaComponent) {
-      this.textarea = textareaComponent.textarea;
-    }
-  },
-
-  //  Sets a reference to the CW field.
-  handleRefSpoilerText (spoilerComponent) {
-    if (spoilerComponent) {
-      this.spoilerText = spoilerComponent.spoilerText;
-    }
-  }
-};
-
-//  The component.
-class Composer extends React.Component {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-
-    //  Instance variables.
-    this.textarea = null;
-    this.spoilerText = null;
-  }
-
-  //  Tells our state the composer has been mounted.
-  componentDidMount () {
-    const { onMount } = this.props;
-    if (onMount) {
-      onMount();
-    }
-  }
-
-  //  Tells our state the composer has been unmounted.
-  componentWillUnmount () {
-    const { onUnmount } = this.props;
-    if (onUnmount) {
-      onUnmount();
-    }
-  }
-
-  //  This statement does several things:
-  //  - If we're beginning a reply, and,
-  //      - Replying to zero or one users, places the cursor at the end
-  //        of the textbox.
-  //      - Replying to more than one user, selects any usernames past
-  //        the first; this provides a convenient shortcut to drop
-  //        everyone else from the conversation.
-  componentDidUpdate (prevProps) {
-    const {
-      textarea,
-      spoilerText,
-    } = this;
-    const {
-      focusDate,
-      caretPosition,
-      isSubmitting,
-      preselectDate,
-      text,
-      preselectOnReply,
-    } = this.props;
-    let selectionEnd, selectionStart;
-
-    //  Caret/selection handling.
-    if (focusDate !== prevProps.focusDate) {
-      switch (true) {
-      case preselectDate !== prevProps.preselectDate && preselectOnReply:
-        selectionStart = text.search(/\s/) + 1;
-        selectionEnd = text.length;
-        break;
-      case !isNaN(caretPosition) && caretPosition !== null:
-        selectionStart = selectionEnd = caretPosition;
-        break;
-      default:
-        selectionStart = selectionEnd = text.length;
-      }
-      if (textarea) {
-        textarea.setSelectionRange(selectionStart, selectionEnd);
-        textarea.focus();
-        textarea.scrollIntoView();
-      }
-
-    //  Refocuses the textarea after submitting.
-    } else if (textarea && prevProps.isSubmitting && !isSubmitting) {
-      textarea.focus();
-    } else if (this.props.spoiler !== prevProps.spoiler) {
-      if (this.props.spoiler) {
-        if (spoilerText) {
-          spoilerText.focus();
-        }
-      } else {
-        if (textarea) {
-          textarea.focus();
-        }
-      }
-    }
-  }
-
-  render () {
-    const {
-      handleChangeSpoiler,
-      handleEmoji,
-      handleSecondarySubmit,
-      handleSelect,
-      handleSubmit,
-      handleRefUploadForm,
-      handleRefTextarea,
-      handleRefSpoilerText,
-    } = this.handlers;
-    const {
-      acceptContentTypes,
-      advancedOptions,
-      amUnlocked,
-      anyMedia,
-      intl,
-      isSubmitting,
-      isChangingUpload,
-      isUploading,
-      layout,
-      media,
-      poll,
-      onCancelReply,
-      onChangeAdvancedOption,
-      onChangeDescription,
-      onChangeSensitivity,
-      onChangeSpoilerness,
-      onChangeText,
-      onChangeVisibility,
-      onTogglePoll,
-      onClearSuggestions,
-      onCloseModal,
-      onFetchSuggestions,
-      onOpenActionsModal,
-      onOpenDoodleModal,
-      onOpenFocalPointModal,
-      onUndoUpload,
-      onUpload,
-      privacy,
-      progress,
-      inReplyTo,
-      resetFileKey,
-      sensitive,
-      showSearch,
-      sideArm,
-      spoiler,
-      spoilerText,
-      suggestions,
-      text,
-      spoilersAlwaysOn,
-    } = this.props;
-
-    let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
-
-    return (
-      <div className='composer'>
-        {privacy === 'direct' ? <ComposerDirectWarning /> : null}
-        {privacy === 'private' && amUnlocked ? <ComposerWarning /> : null}
-        {privacy !== 'public' && APPROX_HASHTAG_RE.test(text) ? <ComposerHashtagWarning /> : null}
-        {inReplyTo && (
-          <ComposerReply
-            status={inReplyTo}
-            intl={intl}
-            onCancel={onCancelReply}
-          />
-        )}
-        <ComposerSpoiler
-          hidden={!spoiler}
-          intl={intl}
-          onChange={handleChangeSpoiler}
-          onSubmit={handleSubmit}
-          onSecondarySubmit={handleSecondarySubmit}
-          text={spoilerText}
-          ref={handleRefSpoilerText}
-        />
-        <ComposerTextarea
-          advancedOptions={advancedOptions}
-          autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
-          disabled={isSubmitting}
-          intl={intl}
-          onChange={onChangeText}
-          onPaste={onUpload}
-          onPickEmoji={handleEmoji}
-          onSubmit={handleSubmit}
-          onSecondarySubmit={handleSecondarySubmit}
-          onSuggestionsClearRequested={onClearSuggestions}
-          onSuggestionsFetchRequested={onFetchSuggestions}
-          onSuggestionSelected={handleSelect}
-          ref={handleRefTextarea}
-          suggestions={suggestions}
-          value={text}
-        />
-        <div className='compose-form__modifiers'>
-          {isUploading || media && media.size ? (
-            <ComposerUploadForm
-              intl={intl}
-              media={media}
-              onChangeDescription={onChangeDescription}
-              onOpenFocalPointModal={onOpenFocalPointModal}
-              onRemove={onUndoUpload}
-              progress={progress}
-              uploading={isUploading}
-              handleRef={handleRefUploadForm}
-            />
-          ) : null}
-          {!!poll && (
-            <ComposerPollForm />
-          )}
-        </div>
-        <ComposerOptions
-          acceptContentTypes={acceptContentTypes}
-          advancedOptions={advancedOptions}
-          disabled={isSubmitting}
-          allowMedia={!poll && (media ? media.size < 4 && !media.some(
-              item => item.get('type') === 'video'
-            ) : true)}
-          hasMedia={media && !!media.size}
-          allowPoll={!(media && !!media.size)}
-          hasPoll={!!poll}
-          intl={intl}
-          onChangeAdvancedOption={onChangeAdvancedOption}
-          onChangeSensitivity={onChangeSensitivity}
-          onChangeVisibility={onChangeVisibility}
-          onTogglePoll={onTogglePoll}
-          onDoodleOpen={onOpenDoodleModal}
-          onModalClose={onCloseModal}
-          onModalOpen={onOpenActionsModal}
-          onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
-          onUpload={onUpload}
-          privacy={privacy}
-          resetFileKey={resetFileKey}
-          sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
-          spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
-        />
-        <ComposerPublisher
-          countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
-          disabled={disabledButton}
-          intl={intl}
-          onSecondarySubmit={handleSecondarySubmit}
-          onSubmit={handleSubmit}
-          privacy={privacy}
-          sideArm={sideArm}
-        />
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-Composer.propTypes = {
-  intl: PropTypes.object.isRequired,
-
-  //  State props.
-  acceptContentTypes: PropTypes.string,
-  advancedOptions: ImmutablePropTypes.map,
-  amUnlocked: PropTypes.bool,
-  focusDate: PropTypes.instanceOf(Date),
-  caretPosition: PropTypes.number,
-  isSubmitting: PropTypes.bool,
-  isChangingUpload: PropTypes.bool,
-  isUploading: PropTypes.bool,
-  layout: PropTypes.string,
-  media: ImmutablePropTypes.list,
-  preselectDate: PropTypes.instanceOf(Date),
-  privacy: PropTypes.string,
-  progress: PropTypes.number,
-  inReplyTo: ImmutablePropTypes.map,
-  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,
-  anyMedia: PropTypes.bool,
-  spoilersAlwaysOn: PropTypes.bool,
-  mediaDescriptionConfirmation: PropTypes.bool,
-  preselectOnReply: PropTypes.bool,
-
-  //  Dispatch props.
-  onCancelReply: PropTypes.func,
-  onChangeAdvancedOption: 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,
-  onMount: PropTypes.func,
-  onOpenActionsModal: PropTypes.func,
-  onOpenDoodleModal: PropTypes.func,
-  onSelectSuggestion: PropTypes.func,
-  onSubmit: PropTypes.func,
-  onUndoUpload: PropTypes.func,
-  onUnmount: PropTypes.func,
-  onUpload: PropTypes.func,
-  onMediaDescriptionConfirm: PropTypes.func,
-};
-
-Composer.contextTypes = {
-  router: PropTypes.object,
-};
-
-//  Connecting and export.
-export { Composer as WrappedComponent };
-export default wrap(Composer, mapStateToProps, mapDispatchToProps, true);
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
deleted file mode 100644
index b76410561..000000000
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js
+++ /dev/null
@@ -1,146 +0,0 @@
-//  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;
-
-    this.state = {
-      mounted: false,
-    };
-  }
-
-  //  On mounting, we add our listeners.
-  componentDidMount () {
-    const { handleDocumentClick } = this.handlers;
-    document.addEventListener('click', handleDocumentClick, false);
-    document.addEventListener('touchend', handleDocumentClick, withPassive);
-    this.setState({ mounted: true });
-  }
-
-  //  On unmounting, we remove our listeners.
-  componentWillUnmount () {
-    const { handleDocumentClick } = this.handlers;
-    document.removeEventListener('click', handleDocumentClick, false);
-    document.removeEventListener('touchend', handleDocumentClick, withPassive);
-  }
-
-  //  Rendering.
-  render () {
-    const { mounted } = this.state;
-    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 }) => (
-          // It should not be transformed when mounting because the resulting
-          // size will be used to determine the coordinate of the menu by
-          // react-overlays
-          <div
-            className='composer--options--dropdown--content'
-            ref={handleRef}
-            style={{
-              ...style,
-              opacity: opacity,
-              transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
-            }}
-          >
-            {items ? items.map(
-              ({
-                name,
-                ...rest
-              }) => (
-                <ComposerOptionsDropdownContentItem
-                  active={name === value}
-                  key={name}
-                  name={name}
-                  onChange={onChange}
-                  onClose={onClose}
-                  options={rest}
-                />
-              )
-            ) : null}
-          </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,
-  })),
-  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/content/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js
deleted file mode 100644
index 68a52083f..000000000
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js
+++ /dev/null
@@ -1,129 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import Toggle from 'react-toggle';
-
-//  Components.
-import Icon from 'flavours/glitch/components/icon';
-
-//  Utils.
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-
-//  Handlers.
-const handlers = {
-
-  //  This function activates the dropdown item.
-  handleActivate (e) {
-    const {
-      name,
-      onChange,
-      onClose,
-      options: { on },
-    } = this.props;
-
-    //  If the escape key was pressed, we close the dropdown.
-    if (e.key === 'Escape' && onClose) {
-      onClose();
-
-    //  Otherwise, we both close the dropdown and change the value.
-    } else if (onChange && (!e.key || e.key === 'Enter')) {
-      e.preventDefault();  //  Prevents change in focus on click
-      if ((on === null || typeof on === 'undefined') && onClose) {
-        onClose();
-      }
-      onChange(name);
-    }
-  },
-};
-
-//  The component.
-export default class ComposerOptionsDropdownContentItem extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-  }
-
-  //  Rendering.
-  render () {
-    const { handleActivate } = this.handlers;
-    const {
-      active,
-      options: {
-        icon,
-        meta,
-        on,
-        text,
-      },
-    } = this.props;
-    const computedClass = classNames('composer--options--dropdown--content--item', {
-      active,
-      lengthy: meta,
-      'toggled-off': !on && on !== null && typeof on !== 'undefined',
-      'toggled-on': on,
-      'with-icon': icon,
-    });
-
-    //  The result.
-    return (
-      <div
-        className={computedClass}
-        onClick={handleActivate}
-        onKeyDown={handleActivate}
-        role='button'
-        tabIndex='0'
-      >
-        {function () {
-
-          //  We render a `<Toggle>` if we were provided an `on`
-          //  property, and otherwise show an `<Icon>` if available.
-          switch (true) {
-          case on !== null && typeof on !== 'undefined':
-            return (
-              <Toggle
-                checked={on}
-                onChange={handleActivate}
-              />
-            );
-          case !!icon:
-            return (
-              <Icon
-                className='icon'
-                fullwidth
-                icon={icon}
-              />
-            );
-          default:
-            return null;
-          }
-        }()}
-        {meta ? (
-          <div className='content'>
-            <strong>{text}</strong>
-            {meta}
-          </div>
-        ) :
-          <div className='content'>
-            <strong>{text}</strong>
-          </div>}
-      </div>
-    );
-  }
-
-};
-
-//  Props.
-ComposerOptionsDropdownContentItem.propTypes = {
-  active: PropTypes.bool,
-  name: PropTypes.string,
-  onChange: PropTypes.func,
-  onClose: PropTypes.func,
-  options: PropTypes.shape({
-    icon: PropTypes.string,
-    meta: PropTypes.node,
-    on: PropTypes.bool,
-    text: PropTypes.node,
-  }),
-};
diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
deleted file mode 100644
index 7817cc964..000000000
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
+++ /dev/null
@@ -1,229 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import Overlay from 'react-overlays/lib/Overlay';
-
-//  Components.
-import IconButton from 'flavours/glitch/components/icon_button';
-import ComposerOptionsDropdownContent from './content';
-
-//  Utils.
-import { isUserTouching } from 'flavours/glitch/util/is_mobile';
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-
-//  Handlers.
-const handlers = {
-
-  //  Closes the dropdown.
-  handleClose () {
-    this.setState({ open: false });
-  },
-
-  //  The enter key toggles the dropdown's open state, and the escape
-  //  key closes it.
-  handleKeyDown ({ key }) {
-    const {
-      handleClose,
-      handleToggle,
-    } = this.handlers;
-    switch (key) {
-    case 'Enter':
-      handleToggle(key);
-      break;
-    case 'Escape':
-      handleClose();
-      break;
-    }
-  },
-
-  //  Creates an action modal object.
-  handleMakeModal () {
-    const component = this;
-    const {
-      items,
-      onChange,
-      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 ({ target }) {
-    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 (isUserTouching()) {
-
-      //  This gets the modal to open.
-      const modal = handleMakeModal();
-
-      //  If we can, we then open the modal.
-      if (modal && onModalOpen) {
-        onModalOpen(modal);
-        return;
-      }
-    }
-
-    const { top } = target.getBoundingClientRect();
-    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
-    //  Otherwise, we just set our state to open.
-    this.setState({ open: !open });
-  },
-
-  //  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);
-    }
-  },
-};
-
-//  The component.
-export default class ComposerOptionsDropdown extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-    this.state = {
-      needsModalUpdate: false,
-      open: false,
-      placement: 'bottom',
-    };
-  }
-
-  //  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 {
-      handleClose,
-      handleKeyDown,
-      handleToggle,
-    } = this.handlers;
-    const {
-      active,
-      disabled,
-      title,
-      icon,
-      items,
-      onChange,
-      value,
-    } = this.props;
-    const { open, placement } = this.state;
-    const computedClass = classNames('composer--options--dropdown', {
-      active,
-      open,
-      top: placement === 'top',
-    });
-
-    //  The result.
-    return (
-      <div
-        className={computedClass}
-        onKeyDown={handleKeyDown}
-      >
-        <IconButton
-          active={open || active}
-          className='value'
-          disabled={disabled}
-          icon={icon}
-          onClick={handleToggle}
-          size={18}
-          style={{
-            height: null,
-            lineHeight: '27px',
-          }}
-          title={title}
-        />
-        <Overlay
-          containerPadding={20}
-          placement={placement}
-          show={open}
-          target={this}
-        >
-          <ComposerOptionsDropdownContent
-            items={items}
-            onChange={onChange}
-            onClose={handleClose}
-            value={value}
-          />
-        </Overlay>
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerOptionsDropdown.propTypes = {
-  active: PropTypes.bool,
-  disabled: PropTypes.bool,
-  icon: PropTypes.string,
-  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,
-  onModalClose: PropTypes.func,
-  onModalOpen: PropTypes.func,
-  title: PropTypes.string,
-  value: PropTypes.string,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
deleted file mode 100644
index 7c7f01dc2..000000000
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ /dev/null
@@ -1,377 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import {
-  FormattedMessage,
-  defineMessages,
-} from 'react-intl';
-import spring from 'react-motion/lib/spring';
-
-//  Components.
-import IconButton from 'flavours/glitch/components/icon_button';
-import TextIconButton from 'flavours/glitch/components/text_icon_button';
-import Dropdown from './dropdown';
-
-//  Utils.
-import Motion from 'flavours/glitch/util/optional_motion';
-import {
-  assignHandlers,
-  hiddenComponent,
-} from 'flavours/glitch/util/react_helpers';
-import { pollLimits } from 'flavours/glitch/util/initial_state';
-
-//  Messages.
-const messages = defineMessages({
-  advanced_options_icon_title: {
-    defaultMessage: 'Advanced options',
-    id: 'advanced_options.icon_title',
-  },
-  attach: {
-    defaultMessage: 'Attach...',
-    id: 'compose.attach',
-  },
-  change_privacy: {
-    defaultMessage: 'Adjust status privacy',
-    id: 'privacy.change',
-  },
-  direct_long: {
-    defaultMessage: 'Post to mentioned users only',
-    id: 'privacy.direct.long',
-  },
-  direct_short: {
-    defaultMessage: 'Direct',
-    id: 'privacy.direct.short',
-  },
-  doodle: {
-    defaultMessage: 'Draw something',
-    id: 'compose.attach.doodle',
-  },
-  local_only_long: {
-    defaultMessage: 'Do not post to other instances',
-    id: 'advanced_options.local-only.long',
-  },
-  local_only_short: {
-    defaultMessage: 'Local-only',
-    id: 'advanced_options.local-only.short',
-  },
-  private_long: {
-    defaultMessage: 'Post to followers only',
-    id: 'privacy.private.long',
-  },
-  private_short: {
-    defaultMessage: 'Followers-only',
-    id: 'privacy.private.short',
-  },
-  public_long: {
-    defaultMessage: 'Post to public timelines',
-    id: 'privacy.public.long',
-  },
-  public_short: {
-    defaultMessage: 'Public',
-    id: 'privacy.public.short',
-  },
-  sensitive: {
-    defaultMessage: 'Mark media as sensitive',
-    id: 'compose_form.sensitive',
-  },
-  spoiler: {
-    defaultMessage: 'Hide text behind warning',
-    id: 'compose_form.spoiler',
-  },
-  threaded_mode_long: {
-    defaultMessage: 'Automatically opens a reply on posting',
-    id: 'advanced_options.threaded_mode.long',
-  },
-  threaded_mode_short: {
-    defaultMessage: 'Threaded mode',
-    id: 'advanced_options.threaded_mode.short',
-  },
-  unlisted_long: {
-    defaultMessage: 'Do not show in public timelines',
-    id: 'privacy.unlisted.long',
-  },
-  unlisted_short: {
-    defaultMessage: 'Unlisted',
-    id: 'privacy.unlisted.short',
-  },
-  upload: {
-    defaultMessage: 'Upload a file',
-    id: 'compose.attach.upload',
-  },
-  add_poll: {
-    defaultMessage: 'Add a poll',
-    id: 'poll_button.add_poll',
-  },
-  remove_poll: {
-    defaultMessage: 'Remove poll',
-    id: 'poll_button.remove_poll',
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  Handles file selection.
-  handleChangeFiles ({ target: { files } }) {
-    const { onUpload } = this.props;
-    if (files.length && onUpload) {
-      onUpload(files);
-    }
-  },
-
-  //  Handles attachment clicks.
-  handleClickAttach (name) {
-    const { fileElement } = this;
-    const { onDoodleOpen } = this.props;
-
-    //  We switch over the name of the option.
-    switch (name) {
-    case 'upload':
-      if (fileElement) {
-        fileElement.click();
-      }
-      return;
-    case 'doodle':
-      if (onDoodleOpen) {
-        onDoodleOpen();
-      }
-      return;
-    }
-  },
-
-  //  Handles a ref to the file input.
-  handleRefFileElement (fileElement) {
-    this.fileElement = fileElement;
-  },
-};
-
-//  The component.
-export default class ComposerOptions extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-
-    //  Instance variables.
-    this.fileElement = null;
-  }
-
-  //  Rendering.
-  render () {
-    const {
-      handleChangeFiles,
-      handleClickAttach,
-      handleRefFileElement,
-    } = this.handlers;
-    const {
-      acceptContentTypes,
-      advancedOptions,
-      disabled,
-      allowMedia,
-      hasMedia,
-      allowPoll,
-      hasPoll,
-      intl,
-      onChangeAdvancedOption,
-      onChangeSensitivity,
-      onChangeVisibility,
-      onTogglePoll,
-      onModalClose,
-      onModalOpen,
-      onToggleSpoiler,
-      privacy,
-      resetFileKey,
-      sensitive,
-      spoiler,
-    } = this.props;
-
-    //  We predefine our privacy items so that we can easily pick the
-    //  dropdown icon later.
-    const privacyItems = {
-      direct: {
-        icon: 'envelope',
-        meta: <FormattedMessage {...messages.direct_long} />,
-        name: 'direct',
-        text: <FormattedMessage {...messages.direct_short} />,
-      },
-      private: {
-        icon: 'lock',
-        meta: <FormattedMessage {...messages.private_long} />,
-        name: 'private',
-        text: <FormattedMessage {...messages.private_short} />,
-      },
-      public: {
-        icon: 'globe',
-        meta: <FormattedMessage {...messages.public_long} />,
-        name: 'public',
-        text: <FormattedMessage {...messages.public_short} />,
-      },
-      unlisted: {
-        icon: 'unlock',
-        meta: <FormattedMessage {...messages.unlisted_long} />,
-        name: 'unlisted',
-        text: <FormattedMessage {...messages.unlisted_short} />,
-      },
-    };
-
-    //  The result.
-    return (
-      <div className='composer--options'>
-        <input
-          accept={acceptContentTypes}
-          disabled={disabled || !allowMedia}
-          key={resetFileKey}
-          onChange={handleChangeFiles}
-          ref={handleRefFileElement}
-          type='file'
-          multiple
-          {...hiddenComponent}
-        />
-        <Dropdown
-          disabled={disabled || !allowMedia}
-          icon='paperclip'
-          items={[
-            {
-              icon: 'cloud-upload',
-              name: 'upload',
-              text: <FormattedMessage {...messages.upload} />,
-            },
-            {
-              icon: 'paint-brush',
-              name: 'doodle',
-              text: <FormattedMessage {...messages.doodle} />,
-            },
-          ]}
-          onChange={handleClickAttach}
-          onModalClose={onModalClose}
-          onModalOpen={onModalOpen}
-          title={intl.formatMessage(messages.attach)}
-        />
-        {!!pollLimits && (
-          <IconButton
-            active={hasPoll}
-            disabled={disabled || !allowPoll}
-            icon='tasks'
-            inverted
-            onClick={onTogglePoll}
-            size={18}
-            style={{
-              height: null,
-              lineHeight: null,
-            }}
-            title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
-          />
-        )}
-        <Motion
-          defaultStyle={{ scale: 0.87 }}
-          style={{
-            scale: spring(hasMedia ? 1 : 0.87, {
-              stiffness: 200,
-              damping: 3,
-            }),
-          }}
-        >
-          {({ scale }) => (
-            <div
-              style={{
-                display: hasMedia ? null : 'none',
-                transform: `scale(${scale})`,
-              }}
-            >
-              <IconButton
-                active={sensitive}
-                className='sensitive'
-                disabled={spoiler}
-                icon={sensitive ? 'eye-slash' : 'eye'}
-                inverted
-                onClick={onChangeSensitivity}
-                size={18}
-                style={{
-                  height: null,
-                  lineHeight: null,
-                }}
-                title={intl.formatMessage(messages.sensitive)}
-              />
-            </div>
-          )}
-        </Motion>
-        <hr />
-        <Dropdown
-          disabled={disabled}
-          icon={(privacyItems[privacy] || {}).icon}
-          items={[
-            privacyItems.public,
-            privacyItems.unlisted,
-            privacyItems.private,
-            privacyItems.direct,
-          ]}
-          onChange={onChangeVisibility}
-          onModalClose={onModalClose}
-          onModalOpen={onModalOpen}
-          title={intl.formatMessage(messages.change_privacy)}
-          value={privacy}
-        />
-        {onToggleSpoiler && (
-          <TextIconButton
-            active={spoiler}
-            ariaControls='glitch.composer.spoiler.input'
-            label='CW'
-            onClick={onToggleSpoiler}
-            title={intl.formatMessage(messages.spoiler)}
-          />
-        )}
-        <Dropdown
-          active={advancedOptions && advancedOptions.some(value => !!value)}
-          disabled={disabled}
-          icon='ellipsis-h'
-          items={advancedOptions ? [
-            {
-              meta: <FormattedMessage {...messages.local_only_long} />,
-              name: 'do_not_federate',
-              on: advancedOptions.get('do_not_federate'),
-              text: <FormattedMessage {...messages.local_only_short} />,
-            },
-            {
-              meta: <FormattedMessage {...messages.threaded_mode_long} />,
-              name: 'threaded_mode',
-              on: advancedOptions.get('threaded_mode'),
-              text: <FormattedMessage {...messages.threaded_mode_short} />,
-            },
-          ] : null}
-          onChange={onChangeAdvancedOption}
-          onModalClose={onModalClose}
-          onModalOpen={onModalOpen}
-          title={intl.formatMessage(messages.advanced_options_icon_title)}
-        />
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerOptions.propTypes = {
-  acceptContentTypes: PropTypes.string,
-  advancedOptions: ImmutablePropTypes.map,
-  disabled: PropTypes.bool,
-  allowMedia: PropTypes.bool,
-  hasMedia: PropTypes.bool,
-  allowPoll: PropTypes.bool,
-  hasPoll: PropTypes.bool,
-  intl: PropTypes.object.isRequired,
-  onChangeAdvancedOption: PropTypes.func,
-  onChangeSensitivity: PropTypes.func,
-  onChangeVisibility: PropTypes.func,
-  onTogglePoll: PropTypes.func,
-  onDoodleOpen: PropTypes.func,
-  onModalClose: PropTypes.func,
-  onModalOpen: PropTypes.func,
-  onToggleSpoiler: PropTypes.func,
-  onUpload: PropTypes.func,
-  privacy: PropTypes.string,
-  resetFileKey: PropTypes.number,
-  sensitive: PropTypes.bool,
-  spoiler: PropTypes.bool,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
deleted file mode 100644
index 1915b62d5..000000000
--- a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import IconButton from 'flavours/glitch/components/icon_button';
-import Icon from 'flavours/glitch/components/icon';
-import classNames from 'classnames';
-import { pollLimits } from 'flavours/glitch/util/initial_state';
-
-const messages = defineMessages({
-  option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
-  add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
-  remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
-  poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
-  single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' },
-  multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' },
-  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
-  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
-  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
-});
-
-@injectIntl
-class Option extends React.PureComponent {
-
-  static propTypes = {
-    title: PropTypes.string.isRequired,
-    index: PropTypes.number.isRequired,
-    isPollMultiple: PropTypes.bool,
-    onChange: PropTypes.func.isRequired,
-    onRemove: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  handleOptionTitleChange = e => {
-    this.props.onChange(this.props.index, e.target.value);
-  };
-
-  handleOptionRemove = () => {
-    this.props.onRemove(this.props.index);
-  };
-
-  render () {
-    const { isPollMultiple, title, index, intl } = this.props;
-
-    return (
-      <li>
-        <label className='poll__text editable'>
-          <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
-
-          <input
-            type='text'
-            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
-            maxLength={pollLimits.max_option_chars}
-            value={title}
-            onChange={this.handleOptionTitleChange}
-          />
-        </label>
-
-        <div className='poll__cancel'>
-          <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
-        </div>
-      </li>
-    );
-  }
-
-}
-
-export default
-@injectIntl
-class PollForm extends ImmutablePureComponent {
-
-  static propTypes = {
-    options: ImmutablePropTypes.list,
-    expiresIn: PropTypes.number,
-    isMultiple: PropTypes.bool,
-    onChangeOption: PropTypes.func.isRequired,
-    onAddOption: PropTypes.func.isRequired,
-    onRemoveOption: PropTypes.func.isRequired,
-    onChangeSettings: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  handleAddOption = () => {
-    this.props.onAddOption('');
-  };
-
-  handleSelectDuration = e => {
-    this.props.onChangeSettings(e.target.value, this.props.isMultiple);
-  };
-
-  handleSelectMultiple = e => {
-    this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true');
-  };
-
-  render () {
-    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
-
-    if (!options) {
-      return null;
-    }
-
-    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} />)}
-          {options.size < pollLimits.max_options && (
-            <label className='poll__text editable'>
-              <span className={classNames('poll__input')} style={{ opacity: 0 }} />
-              <button className='button button-secondary' onClick={this.handleAddOption}><Icon icon='plus' /> <FormattedMessage {...messages.add_option} /></button>
-            </label>
-          )}
-        </ul>
-
-        <div className='poll__footer'>
-          <select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}>
-            <option value='false'>{intl.formatMessage(messages.single_choice)}</option>
-            <option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
-          </select>
-
-          <select value={expiresIn} onChange={this.handleSelectDuration}>
-            <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
-            <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
-            <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
-            <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
-            <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
-            <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
-            <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
-          </select>
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/index.js b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
deleted file mode 100644
index 5232c3b31..000000000
--- a/app/javascript/flavours/glitch/features/composer/poll_form/index.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { connect } from 'react-redux';
-import PollForm from './components/poll_form';
-import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
-
-const mapStateToProps = state => ({
-  options: state.getIn(['compose', 'poll', 'options']),
-  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
-  isMultiple: state.getIn(['compose', 'poll', 'multiple']),
-});
-
-const mapDispatchToProps = dispatch => ({
-  onAddOption(title) {
-    dispatch(addPollOption(title));
-  },
-
-  onRemoveOption(index) {
-    dispatch(removePollOption(index));
-  },
-
-  onChangeOption(index, title) {
-    dispatch(changePollOption(index, title));
-  },
-
-  onChangeSettings(expiresIn, isMultiple) {
-    dispatch(changePollSettings(expiresIn, isMultiple));
-  },
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/flavours/glitch/features/composer/publisher/index.js b/app/javascript/flavours/glitch/features/composer/publisher/index.js
deleted file mode 100644
index dc9c8f8eb..000000000
--- a/app/javascript/flavours/glitch/features/composer/publisher/index.js
+++ /dev/null
@@ -1,122 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {
-  defineMessages,
-  FormattedMessage,
-} from 'react-intl';
-import { length } from 'stringz';
-
-//  Components.
-import Button from 'flavours/glitch/components/button';
-import Icon from 'flavours/glitch/components/icon';
-
-//  Utils.
-import { maxChars } from 'flavours/glitch/util/initial_state';
-
-//  Messages.
-const messages = defineMessages({
-  publish: {
-    defaultMessage: 'Toot',
-    id: 'compose_form.publish',
-  },
-  publishLoud: {
-    defaultMessage: '{publish}!',
-    id: 'compose_form.publish_loud',
-  },
-});
-
-//  The component.
-export default function ComposerPublisher ({
-  countText,
-  disabled,
-  intl,
-  onSecondarySubmit,
-  onSubmit,
-  privacy,
-  sideArm,
-}) {
-  const diff = maxChars - length(countText || '');
-  const computedClass = classNames('composer--publisher', {
-    disabled: disabled || diff < 0,
-    over: diff < 0,
-  });
-
-  //  The result.
-  return (
-    <div className={computedClass}>
-      <span className='count'>{diff}</span>
-      {sideArm && sideArm !== 'none' ? (
-        <Button
-          className='side_arm'
-          disabled={disabled || diff < 0}
-          onClick={onSecondarySubmit}
-          style={{ padding: null }}
-          text={
-            <span>
-              <Icon
-                icon={{
-                  public: 'globe',
-                  unlisted: 'unlock',
-                  private: 'lock',
-                  direct: 'envelope',
-                }[sideArm]}
-              />
-            </span>
-          }
-          title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
-        />
-      ) : null}
-      <Button
-        className='primary'
-        text={function () {
-          switch (true) {
-          case !!sideArm && sideArm !== 'none':
-          case privacy === 'direct':
-          case privacy === 'private':
-            return (
-              <span>
-                <Icon
-                  icon={{
-                    direct: 'envelope',
-                    private: 'lock',
-                    public: 'globe',
-                    unlisted: 'unlock',
-                  }[privacy]}
-                />
-                {' '}
-                <FormattedMessage {...messages.publish} />
-              </span>
-            );
-          case privacy === 'public':
-            return (
-              <span>
-                <FormattedMessage
-                  {...messages.publishLoud}
-                  values={{ publish: <FormattedMessage {...messages.publish} /> }}
-                />
-              </span>
-            );
-          default:
-            return <span><FormattedMessage {...messages.publish} /></span>;
-          }
-        }()}
-        title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
-        onClick={onSubmit}
-        disabled={disabled || diff < 0}
-      />
-    </div>
-  );
-}
-
-//  Props.
-ComposerPublisher.propTypes = {
-  countText: PropTypes.string,
-  disabled: PropTypes.bool,
-  intl: PropTypes.object.isRequired,
-  onSecondarySubmit: PropTypes.func,
-  onSubmit: PropTypes.func,
-  privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
-  sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
-};
diff --git a/app/javascript/flavours/glitch/features/composer/reply/index.js b/app/javascript/flavours/glitch/features/composer/reply/index.js
deleted file mode 100644
index 56e9e96a5..000000000
--- a/app/javascript/flavours/glitch/features/composer/reply/index.js
+++ /dev/null
@@ -1,96 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { defineMessages } from 'react-intl';
-
-//  Components.
-import AccountContainer from 'flavours/glitch/containers/account_container';
-import IconButton from 'flavours/glitch/components/icon_button';
-import AttachmentList from 'flavours/glitch/components/attachment_list';
-
-//  Utils.
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-import { isRtl } from 'flavours/glitch/util/rtl';
-
-//  Messages.
-const messages = defineMessages({
-  cancel: {
-    defaultMessage: 'Cancel',
-    id: 'reply_indicator.cancel',
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  Handles a click on the "close" button.
-  handleClick () {
-    const { onCancel } = this.props;
-    if (onCancel) {
-      onCancel();
-    }
-  },
-};
-
-//  The component.
-export default class ComposerReply extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-  }
-
-  //  Rendering.
-  render () {
-    const { handleClick } = this.handlers;
-    const {
-      status,
-      intl,
-    } = this.props;
-
-    const account     = status.get('account');
-    const content     = status.get('content');
-    const attachments = status.get('media_attachments');
-
-    //  The result.
-    return (
-      <article className='composer--reply'>
-        <header>
-          <IconButton
-            className='cancel'
-            icon='times'
-            onClick={handleClick}
-            title={intl.formatMessage(messages.cancel)}
-            inverted
-          />
-          {account && (
-            <AccountContainer
-              id={account}
-              small
-            />
-          )}
-        </header>
-        <div
-          className='content'
-          dangerouslySetInnerHTML={{ __html: content || '' }}
-          style={{ direction: isRtl(content) ? 'rtl' : 'ltr' }}
-        />
-        {attachments.size > 0 && (
-          <AttachmentList
-            compact
-            media={attachments}
-          />
-        )}
-      </article>
-    );
-  }
-
-}
-
-ComposerReply.propTypes = {
-  status: ImmutablePropTypes.map.isRequired,
-  intl: PropTypes.object.isRequired,
-  onCancel: PropTypes.func,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/spoiler/index.js b/app/javascript/flavours/glitch/features/composer/spoiler/index.js
deleted file mode 100644
index e2f9c7021..000000000
--- a/app/javascript/flavours/glitch/features/composer/spoiler/index.js
+++ /dev/null
@@ -1,107 +0,0 @@
-//  Package imports.
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, FormattedMessage } from 'react-intl';
-
-//  Utils.
-import {
-  assignHandlers,
-  hiddenComponent,
-} from 'flavours/glitch/util/react_helpers';
-
-//  Messages.
-const messages = defineMessages({
-  placeholder: {
-    defaultMessage: 'Write your warning here',
-    id: 'compose_form.spoiler_placeholder',
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  Handles a keypress.
-  handleKeyDown ({
-    ctrlKey,
-    keyCode,
-    metaKey,
-    altKey,
-  }) {
-    const { onSubmit, onSecondarySubmit } = this.props;
-
-    //  We submit the status on control/meta + enter.
-    if (onSubmit && keyCode === 13 && (ctrlKey || metaKey)) {
-      onSubmit();
-    }
-
-    // Submit the status with secondary visibility on alt + enter.
-    if (onSecondarySubmit && keyCode === 13 && altKey) {
-      onSecondarySubmit();
-    }
-  },
-
-  handleRefSpoilerText (spoilerText) {
-    this.spoilerText = spoilerText;
-  },
-
-  //  When the escape key is released, we focus the UI.
-  handleKeyUp ({ key }) {
-    if (key === 'Escape') {
-      document.querySelector('.ui').parentElement.focus();
-    }
-  },
-};
-
-//  The component.
-export default class ComposerSpoiler extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-  }
-
-  //  Rendering.
-  render () {
-    const { handleKeyDown, handleKeyUp, handleRefSpoilerText } = this.handlers;
-    const {
-      hidden,
-      intl,
-      onChange,
-      text,
-    } = this.props;
-
-    //  The result.
-    return (
-      <div className={`composer--spoiler ${hidden ? '' : 'composer--spoiler--visible'}`}>
-        <label>
-          <span {...hiddenComponent}>
-            <FormattedMessage {...messages.placeholder} />
-          </span>
-          <input
-            id='glitch.composer.spoiler.input'
-            onChange={onChange}
-            onKeyDown={handleKeyDown}
-            onKeyUp={handleKeyUp}
-            placeholder={intl.formatMessage(messages.placeholder)}
-            type='text'
-            value={text}
-            ref={handleRefSpoilerText}
-            disabled={hidden}
-          />
-        </label>
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerSpoiler.propTypes = {
-  hidden: PropTypes.bool,
-  intl: PropTypes.object.isRequired,
-  onChange: PropTypes.func,
-  onSubmit: PropTypes.func,
-  onSecondarySubmit: PropTypes.func,
-  text: PropTypes.string,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js b/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js
deleted file mode 100644
index 049cdd5cd..000000000
--- a/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js
+++ /dev/null
@@ -1,60 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { defineMessages } from 'react-intl';
-
-//  Components.
-import Icon from 'flavours/glitch/components/icon';
-
-//  Messages.
-const messages = defineMessages({
-  localOnly: {
-    defaultMessage: 'This post is local-only',
-    id: 'advanced_options.local-only.tooltip',
-  },
-  threadedMode: {
-    defaultMessage: 'Threaded mode enabled',
-    id: 'advanced_options.threaded_mode.tooltip',
-  },
-});
-
-//  We use an array of tuples here instead of an object because it
-//  preserves order.
-const iconMap = [
-  ['do_not_federate', 'home', messages.localOnly],
-  ['threaded_mode', 'comments', messages.threadedMode],
-];
-
-//  The component.
-export default function ComposerTextareaIcons ({
-  advancedOptions,
-  intl,
-}) {
-
-  //  The result. We just map every active option to its icon.
-  return (
-    <div className='composer--textarea--icons'>
-      {advancedOptions ? iconMap.map(
-        ([key, icon, message]) => advancedOptions.get(key) ? (
-          <span
-            className='textarea_icon'
-            key={key}
-            title={intl.formatMessage(message)}
-          >
-            <Icon
-              fullwidth
-              icon={icon}
-            />
-          </span>
-        ) : null
-      ) : null}
-    </div>
-  );
-}
-
-//  Props.
-ComposerTextareaIcons.propTypes = {
-  advancedOptions: ImmutablePropTypes.map,
-  intl: PropTypes.object.isRequired,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js
deleted file mode 100644
index 50e46fc78..000000000
--- a/app/javascript/flavours/glitch/features/composer/textarea/index.js
+++ /dev/null
@@ -1,312 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import {
-  defineMessages,
-  FormattedMessage,
-} from 'react-intl';
-import Textarea from 'react-textarea-autosize';
-
-//  Components.
-import EmojiPicker from 'flavours/glitch/features/emoji_picker';
-import ComposerTextareaIcons from './icons';
-import ComposerTextareaSuggestions from './suggestions';
-
-//  Utils.
-import { isRtl } from 'flavours/glitch/util/rtl';
-import {
-  assignHandlers,
-  hiddenComponent,
-} from 'flavours/glitch/util/react_helpers';
-
-//  Messages.
-const messages = defineMessages({
-  placeholder: {
-    defaultMessage: 'What is on your mind?',
-    id: 'compose_form.placeholder',
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  When blurring the textarea, suggestions are hidden.
-  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.
-  handleChange ({
-    target: {
-      selectionStart,
-      value,
-    },
-  }) {
-    const {
-      onChange,
-      onSuggestionsFetchRequested,
-      onSuggestionsClearRequested,
-    } = this.props;
-    const { lastToken } = this.state;
-
-    //  This gets the token at the caret location, if it begins with an
-    //  `@` (mentions) or `:` (shortcodes).
-    const left = value.slice(0, selectionStart).search(/[^\s\u200B]+$/);
-    const right = value.slice(selectionStart).search(/[\s\u200B]/);
-    const token = function () {
-      switch (true) {
-      case left < 0 || !/[@:#]/.test(value[left]):
-        return null;
-      case right < 0:
-        return value.slice(left);
-      default:
-        return value.slice(left, right + selectionStart).trim().toLowerCase();
-      }
-    }();
-
-    //  We only request suggestions for tokens which are at least 3
-    //  characters long.
-    if (onSuggestionsFetchRequested && token && token.length >= 3) {
-      if (lastToken !== token) {
-        this.setState({
-          lastToken: token,
-          selectedSuggestion: 0,
-          tokenStart: left,
-        });
-        onSuggestionsFetchRequested(token);
-      }
-    } else {
-      this.setState({ lastToken: null });
-      if (onSuggestionsClearRequested) {
-        onSuggestionsClearRequested();
-      }
-    }
-
-    //  Updates the value of the textarea.
-    if (onChange) {
-      onChange(value);
-    }
-  },
-
-  //  Handles a click on an autosuggestion.
-  handleClickSuggestion (index) {
-    const { textarea } = this;
-    const {
-      onSuggestionSelected,
-      suggestions,
-    } = this.props;
-    const {
-      lastToken,
-      tokenStart,
-    } = this.state;
-    onSuggestionSelected(tokenStart, lastToken, suggestions.get(index));
-    textarea.focus();
-  },
-
-  //  Handles a keypress.  If the autosuggestions are visible, we need
-  //  to allow keypresses to navigate and sleect them.
-  handleKeyDown (e) {
-    const {
-      disabled,
-      onSubmit,
-      onSecondarySubmit,
-      onSuggestionSelected,
-      suggestions,
-    } = this.props;
-    const {
-      lastToken,
-      suggestionsHidden,
-      selectedSuggestion,
-      tokenStart,
-    } = this.state;
-
-    //  Keypresses do nothing if the composer is disabled.
-    if (disabled) {
-      e.preventDefault();
-      return;
-    }
-
-    //  We submit the status on control/meta + enter.
-    if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
-      onSubmit();
-    }
-
-    // Submit the status with secondary visibility on alt + enter.
-    if (onSecondarySubmit && e.keyCode === 13 && e.altKey) {
-      onSecondarySubmit();
-    }
-
-    //  Switches over the pressed key.
-    switch(e.key) {
-
-    //  On arrow down, we pick the next suggestion.
-    case 'ArrowDown':
-      if (suggestions && suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
-      }
-      return;
-
-    //  On arrow up, we pick the previous suggestion.
-    case 'ArrowUp':
-      if (suggestions && suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
-      }
-      return;
-
-    //  On enter or tab, we select the suggestion.
-    case 'Enter':
-    case 'Tab':
-      if (onSuggestionSelected && lastToken !== null && suggestions && suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        e.stopPropagation();
-        onSuggestionSelected(tokenStart, lastToken, suggestions.get(selectedSuggestion));
-      }
-      return;
-    }
-  },
-
-  //  When the escape key is released, we either close the suggestions
-  //  window or focus the UI.
-  handleKeyUp ({ key }) {
-    const { suggestionsHidden } = this.state;
-    if (key === 'Escape') {
-      if (!suggestionsHidden) {
-        this.setState({ suggestionsHidden: true });
-      } else {
-        document.querySelector('.ui').parentElement.focus();
-      }
-    }
-  },
-
-  //  Handles the pasting of images into the composer.
-  handlePaste (e) {
-    const { onPaste } = this.props;
-    let d;
-    if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) {
-      onPaste(d);
-      e.preventDefault();
-    }
-  },
-
-  //  Saves a reference to the textarea.
-  handleRefTextarea (textarea) {
-    this.textarea = textarea;
-  },
-};
-
-//  The component.
-export default class ComposerTextarea extends React.Component {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-    this.state = {
-      suggestionsHidden: false,
-      selectedSuggestion: 0,
-      lastToken: null,
-      tokenStart: 0,
-    };
-
-    //  Instance variables.
-    this.textarea = null;
-  }
-
-  //  When we receive new suggestions, we unhide the suggestions window
-  //  if we didn't have any suggestions before.
-  componentWillReceiveProps (nextProps) {
-    const { suggestions } = this.props;
-    const { suggestionsHidden } = this.state;
-    if (nextProps.suggestions && nextProps.suggestions !== suggestions && nextProps.suggestions.size > 0 && suggestionsHidden) {
-      this.setState({ suggestionsHidden: false });
-    }
-  }
-
-  //  Rendering.
-  render () {
-    const {
-      handleBlur,
-      handleChange,
-      handleClickSuggestion,
-      handleKeyDown,
-      handleKeyUp,
-      handlePaste,
-      handleRefTextarea,
-    } = this.handlers;
-    const {
-      advancedOptions,
-      autoFocus,
-      disabled,
-      intl,
-      onPickEmoji,
-      suggestions,
-      value,
-    } = this.props;
-    const {
-      selectedSuggestion,
-      suggestionsHidden,
-    } = this.state;
-
-    //  The result.
-    return (
-      <div className='composer--textarea'>
-        <label>
-          <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
-          <ComposerTextareaIcons
-            advancedOptions={advancedOptions}
-            intl={intl}
-          />
-          <Textarea
-            aria-autocomplete='list'
-            autoFocus={autoFocus}
-            className='textarea'
-            disabled={disabled}
-            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' }}
-          />
-        </label>
-        <EmojiPicker onPickEmoji={onPickEmoji} />
-        <ComposerTextareaSuggestions
-          hidden={suggestionsHidden}
-          onSuggestionClick={handleClickSuggestion}
-          suggestions={suggestions}
-          value={selectedSuggestion}
-        />
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerTextarea.propTypes = {
-  advancedOptions: ImmutablePropTypes.map,
-  autoFocus: PropTypes.bool,
-  disabled: PropTypes.bool,
-  intl: PropTypes.object.isRequired,
-  onChange: PropTypes.func,
-  onPaste: PropTypes.func,
-  onPickEmoji: PropTypes.func,
-  onSubmit: PropTypes.func,
-  onSecondarySubmit: PropTypes.func,
-  onSuggestionsClearRequested: PropTypes.func,
-  onSuggestionsFetchRequested: PropTypes.func,
-  onSuggestionSelected: PropTypes.func,
-  suggestions: ImmutablePropTypes.list,
-  value: PropTypes.string,
-};
-
-//  Default props.
-ComposerTextarea.defaultProps = { autoFocus: true };
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js
deleted file mode 100644
index dc72585f2..000000000
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js
+++ /dev/null
@@ -1,43 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-//  Components.
-import ComposerTextareaSuggestionsItem from './item';
-
-//  The component.
-export default function ComposerTextareaSuggestions ({
-  hidden,
-  onSuggestionClick,
-  suggestions,
-  value,
-}) {
-
-  //  The result.
-  return (
-    <div
-      className='composer--textarea--suggestions'
-      hidden={hidden || !suggestions || suggestions.isEmpty()}
-    >
-      {!hidden && suggestions ? suggestions.map(
-        (suggestion, index) => (
-          <ComposerTextareaSuggestionsItem
-            index={index}
-            key={typeof suggestion === 'object' ? suggestion.id : suggestion}
-            onClick={onSuggestionClick}
-            selected={index === value}
-            suggestion={suggestion}
-          />
-        )
-      ) : null}
-    </div>
-  );
-}
-
-ComposerTextareaSuggestions.propTypes = {
-  hidden: PropTypes.bool,
-  onSuggestionClick: PropTypes.func,
-  suggestions: ImmutablePropTypes.list,
-  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
deleted file mode 100644
index 1b7ae8904..000000000
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
+++ /dev/null
@@ -1,118 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-//  Components.
-import AccountContainer from 'flavours/glitch/containers/account_container';
-
-//  Utils.
-import { unicodeMapping } from 'flavours/glitch/util/emoji';
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-
-//  Gets our asset host from the environment, if available.
-const assetHost = process.env.CDN_HOST || '';
-
-//  Handlers.
-const handlers = {
-
-  //  Handles a click on a suggestion.
-  handleClick (e) {
-    const {
-      index,
-      onClick,
-    } = this.props;
-    if (onClick) {
-      e.preventDefault();
-      e.stopPropagation();  //  Prevents following account links
-      onClick(index);
-    }
-  },
-
-  //  This prevents the focus from changing, which would mess with
-  //  our suggestion code.
-  handleMouseDown (e) {
-    e.preventDefault();
-  },
-};
-
-//  The component.
-export default class ComposerTextareaSuggestionsItem extends React.Component {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-  }
-
-  //  Rendering.
-  render () {
-    const {
-      handleMouseDown,
-      handleClick,
-    } = this.handlers;
-    const {
-      selected,
-      suggestion,
-    } = this.props;
-    const computedClass = classNames('composer--textarea--suggestions--item', { selected });
-
-    //  If the suggestion is an object, then we render an emoji.
-    //  Otherwise, we render a hashtag if it starts with #, or an account.
-    let inner;
-    if (typeof suggestion === 'object') {
-      let url;
-      if (suggestion.custom) {
-        url = suggestion.imageUrl;
-      } else {
-        const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')];
-        if (mapping) {
-          url = `${assetHost}/emoji/${mapping.filename}.svg`;
-        }
-      }
-      if (url) {
-        inner = (
-          <div className='emoji'>
-            <img
-              alt={suggestion.native || suggestion.colons}
-              className='emojione'
-              src={url}
-            />
-            {suggestion.colons}
-          </div>
-        );
-      }
-    } else if (suggestion[0] === '#') {
-      inner = suggestion;
-    } else {
-      inner = (
-        <AccountContainer
-          id={suggestion}
-          small
-        />
-      );
-    }
-
-    //  The result.
-    return (
-      <div
-        className={computedClass}
-        onMouseDown={handleMouseDown}
-        onClickCapture={handleClick}  //  Jumps in front of contents
-        role='button'
-        tabIndex='0'
-      >
-        { inner }
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerTextareaSuggestionsItem.propTypes = {
-  index: PropTypes.number,
-  onClick: PropTypes.func,
-  selected: PropTypes.bool,
-  suggestion: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
-};
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js
deleted file mode 100644
index c2ff66623..000000000
--- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js
+++ /dev/null
@@ -1,60 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-//  Components.
-import ComposerUploadFormItem from './item';
-import ComposerUploadFormProgress from './progress';
-
-//  The component.
-export default function ComposerUploadForm ({
-  intl,
-  media,
-  onChangeDescription,
-  onOpenFocalPointModal,
-  onRemove,
-  progress,
-  uploading,
-  handleRef,
-}) {
-  const computedClass = classNames('composer--upload_form', { uploading });
-
-  //  The result.
-  return (
-    <div className={computedClass} ref={handleRef}>
-      {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}
-              focusX={item.getIn(['meta', 'focus', 'x'])}
-              focusY={item.getIn(['meta', 'focus', 'y'])}
-              mediaType={item.get('type')}
-              preview={item.get('preview_url')}
-              onChangeDescription={onChangeDescription}
-              onOpenFocalPointModal={onOpenFocalPointModal}
-              onRemove={onRemove}
-            />
-          ))}
-        </div>
-      ) : null}
-    </div>
-  );
-}
-
-//  Props.
-ComposerUploadForm.propTypes = {
-  intl: PropTypes.object.isRequired,
-  media: ImmutablePropTypes.list,
-  onChangeDescription: PropTypes.func.isRequired,
-  onRemove: PropTypes.func.isRequired,
-  progress: PropTypes.number,
-  uploading: PropTypes.bool,
-  handleRef: PropTypes.func,
-};
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
deleted file mode 100644
index 4f5f66f04..000000000
--- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js
+++ /dev/null
@@ -1,202 +0,0 @@
-//  Package imports.
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {
-  FormattedMessage,
-  defineMessages,
-} from 'react-intl';
-import spring from 'react-motion/lib/spring';
-
-//  Components.
-import IconButton from 'flavours/glitch/components/icon_button';
-
-//  Utils.
-import Motion from 'flavours/glitch/util/optional_motion';
-import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-import { isUserTouching } from 'flavours/glitch/util/is_mobile';
-
-//  Messages.
-const messages = defineMessages({
-  undo: {
-    defaultMessage: 'Undo',
-    id: 'upload_form.undo',
-  },
-  description: {
-    defaultMessage: 'Describe for the visually impaired',
-    id: 'upload_form.description',
-  },
-  crop: {
-    defaultMessage: 'Crop',
-    id: 'upload_form.focus',
-  },
-});
-
-//  Handlers.
-const handlers = {
-
-  //  On blur, we save the description for the media item.
-  handleBlur () {
-    const {
-      id,
-      onChangeDescription,
-    } = this.props;
-    const { dirtyDescription } = this.state;
-
-    this.setState({ dirtyDescription: null, focused: false });
-
-    if (id && onChangeDescription && dirtyDescription !== null) {
-      onChangeDescription(id, dirtyDescription);
-    }
-  },
-
-  //  When the value of our description changes, we store it in the
-  //  temp value `dirtyDescription` in our state.
-  handleChange ({ target: { value } }) {
-    this.setState({ dirtyDescription: value });
-  },
-
-  //  Records focus on the media item.
-  handleFocus () {
-    this.setState({ focused: true });
-  },
-
-  //  Records the start of a hover over the media item.
-  handleMouseEnter () {
-    this.setState({ hovered: true });
-  },
-
-  //  Records the end of a hover over the media item.
-  handleMouseLeave () {
-    this.setState({ hovered: false });
-  },
-
-  //  Removes the media item.
-  handleRemove () {
-    const {
-      id,
-      onRemove,
-    } = this.props;
-    if (id && onRemove) {
-      onRemove(id);
-    }
-  },
-
-  //  Opens the focal point modal.
-  handleFocalPointClick () {
-    const {
-      id,
-      onOpenFocalPointModal,
-    } = this.props;
-    if (id && onOpenFocalPointModal) {
-      onOpenFocalPointModal(id);
-    }
-  },
-};
-
-//  The component.
-export default class ComposerUploadFormItem extends React.PureComponent {
-
-  //  Constructor.
-  constructor (props) {
-    super(props);
-    assignHandlers(this, handlers);
-    this.state = {
-      hovered: false,
-      focused: false,
-      dirtyDescription: null,
-    };
-  }
-
-  //  Rendering.
-  render () {
-    const {
-      handleBlur,
-      handleChange,
-      handleFocus,
-      handleMouseEnter,
-      handleMouseLeave,
-      handleRemove,
-      handleFocalPointClick,
-    } = this.handlers;
-    const {
-      intl,
-      preview,
-      focusX,
-      focusY,
-      mediaType,
-    } = this.props;
-    const {
-      focused,
-      hovered,
-      dirtyDescription,
-    } = this.state;
-    const active = hovered || focused || isUserTouching();
-    const computedClass = classNames('composer--upload_form--item', { active });
-    const x = ((focusX /  2) + .5) * 100;
-    const y = ((focusY / -2) + .5) * 100;
-    const description = dirtyDescription || (dirtyDescription !== '' && this.props.description) || '';
-
-    //  The result.
-    return (
-      <div
-        className={computedClass}
-        onMouseEnter={handleMouseEnter}
-        onMouseLeave={handleMouseLeave}
-      >
-        <Motion
-          defaultStyle={{ scale: 0.8 }}
-          style={{
-            scale: spring(1, {
-              stiffness: 180,
-              damping: 12,
-            }),
-          }}
-        >
-          {({ scale }) => (
-            <div
-              style={{
-                transform: `scale(${scale})`,
-                backgroundImage: preview ? `url(${preview})` : null,
-                backgroundPosition: `${x}% ${y}%`
-              }}
-            >
-              <div className={classNames('composer--upload_form--actions', { active })}>
-                <button className='icon-button' onClick={handleRemove}>
-                  <i className='fa fa-times' /> <FormattedMessage {...messages.undo} />
-                </button>
-                {mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>}
-              </div>
-              <label>
-                <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span>
-                <textarea
-                  maxLength={420}
-                  onBlur={handleBlur}
-                  onChange={handleChange}
-                  onFocus={handleFocus}
-                  placeholder={intl.formatMessage(messages.description)}
-                  value={description}
-                />
-              </label>
-            </div>
-          )}
-        </Motion>
-      </div>
-    );
-  }
-
-}
-
-//  Props.
-ComposerUploadFormItem.propTypes = {
-  description: PropTypes.string,
-  id: PropTypes.string,
-  intl: PropTypes.object.isRequired,
-  onChangeDescription: PropTypes.func.isRequired,
-  onOpenFocalPointModal: PropTypes.func.isRequired,
-  onRemove: PropTypes.func.isRequired,
-  focusX: PropTypes.number,
-  focusY: PropTypes.number,
-  mediaType: PropTypes.string,
-  preview: PropTypes.string,
-};
diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js
deleted file mode 100644
index 8c4b0eea6..000000000
--- a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js
+++ /dev/null
@@ -1,52 +0,0 @@
-//  Package imports.
-import PropTypes from 'prop-types';
-import React from 'react';
-import {
-  defineMessages,
-  FormattedMessage,
-} from 'react-intl';
-import spring from 'react-motion/lib/spring';
-
-//  Components.
-import Icon from 'flavours/glitch/components/icon';
-
-//  Utils.
-import Motion from 'flavours/glitch/util/optional_motion';
-
-//  Messages.
-const messages = defineMessages({
-  upload: {
-    defaultMessage: 'Uploading...',
-    id: 'upload_progress.label',
-  },
-});
-
-//  The component.
-export default function ComposerUploadFormProgress ({ progress }) {
-
-  //  The result.
-  return (
-    <div className='composer--upload_form--progress'>
-      <Icon icon='upload' />
-      <div className='message'>
-        <FormattedMessage {...messages.upload} />
-        <div className='backdrop'>
-          <Motion
-            defaultStyle={{ width: 0 }}
-            style={{ width: spring(progress) }}
-          >
-            {({ width }) =>
-              (<div
-                className='tracker'
-                style={{ width: `${width}%` }}
-              />)
-            }
-          </Motion>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-//  Props.
-ComposerUploadFormProgress.propTypes = { progress: PropTypes.number };
diff --git a/app/javascript/flavours/glitch/features/composer/warning/index.js b/app/javascript/flavours/glitch/features/composer/warning/index.js
deleted file mode 100644
index 8be8acbce..000000000
--- a/app/javascript/flavours/glitch/features/composer/warning/index.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import Motion from 'flavours/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import { defineMessages, FormattedMessage } from 'react-intl';
-import { profileLink } from 'flavours/glitch/util/backend_links';
-
-//  This is the spring used with our motion.
-const motionSpring = spring(1, { damping: 35, stiffness: 400 });
-
-//  Messages.
-const messages = defineMessages({
-  disclaimer: {
-    defaultMessage: 'Your account is not {locked}. Anyone can follow you to view your follower-only posts.',
-    id: 'compose_form.lock_disclaimer',
-  },
-  locked: {
-    defaultMessage: 'locked',
-    id: 'compose_form.lock_disclaimer.lock',
-  },
-});
-
-//  The component.
-export default function ComposerWarning () {
-  let lockedLink = <FormattedMessage {...messages.locked} />;
-  if (profileLink !== undefined) {
-    lockedLink = <a href={profileLink}>{lockedLink}</a>;
-  }
-  return (
-    <Motion
-      defaultStyle={{
-        opacity: 0,
-        scaleX: 0.85,
-        scaleY: 0.75,
-      }}
-      style={{
-        opacity: motionSpring,
-        scaleX: motionSpring,
-        scaleY: motionSpring,
-      }}
-    >
-      {({ opacity, scaleX, scaleY }) => (
-        <div
-          className='composer--warning'
-          style={{
-            opacity: opacity,
-            transform: `scale(${scaleX}, ${scaleY})`,
-          }}
-        >
-          <FormattedMessage
-            {...messages.disclaimer}
-            values={{ locked: lockedLink }}
-          />
-        </div>
-      )}
-    </Motion>
-  );
-}
-
-ComposerWarning.propTypes = {};