about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/compose/containers
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/compose/containers')
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js15
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js144
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/dropdown_container.js12
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js83
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/header_container.js36
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js34
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/navigation_container.js30
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/options_container.js53
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js52
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js23
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js32
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_container.js35
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_results_container.js18
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx75
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_container.js25
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js8
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js10
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx68
18 files changed, 753 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
new file mode 100644
index 000000000..0e1c328fe
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import AutosuggestAccount from '../components/autosuggest_account';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { id }) => ({
+    account: getAccount(state, id),
+  });
+
+  return mapStateToProps;
+};
+
+export default connect(makeMapStateToProps)(AutosuggestAccount);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
new file mode 100644
index 000000000..ddcdb367a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -0,0 +1,144 @@
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import ComposeForm from '../components/compose_form';
+import {
+  changeCompose,
+  changeComposeSpoilerText,
+  changeComposeSpoilerness,
+  changeComposeVisibility,
+  clearComposeSuggestions,
+  fetchComposeSuggestions,
+  insertEmojiCompose,
+  selectComposeSuggestion,
+  submitCompose,
+  uploadCompose,
+} from 'flavours/glitch/actions/compose';
+import {
+  openModal,
+} from 'flavours/glitch/actions/modal';
+import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
+
+import { privacyPreference } from 'flavours/glitch/utils/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',
+  },
+  missingDescriptionEdit: {
+    id: 'confirmations.missing_media_description.edit',
+    defaultMessage: 'Edit media',
+  },
+});
+
+//  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 {
+    advancedOptions: state.getIn(['compose', 'advanced_options']),
+    focusDate: state.getIn(['compose', 'focusDate']),
+    caretPosition: state.getIn(['compose', 'caretPosition']),
+    isSubmitting: state.getIn(['compose', 'is_submitting']),
+    isEditing: state.getIn(['compose', 'id']) !== null,
+    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']),
+    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']),
+    suggestions: state.getIn(['compose', 'suggestions']),
+    text: state.getIn(['compose', 'text']),
+    anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+    spoilersAlwaysOn: spoilersAlwaysOn,
+    mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
+    preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
+    isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
+    lang: state.getIn(['compose', 'language']),
+  };
+}
+
+//  Dispatch mapping.
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+  onChange(text) {
+    dispatch(changeCompose(text));
+  },
+
+  onSubmit(routerHistory) {
+    dispatch(submitCompose(routerHistory));
+  },
+
+  onClearSuggestions() {
+    dispatch(clearComposeSuggestions());
+  },
+
+  onFetchSuggestions(token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
+
+  onSuggestionSelected(position, token, suggestion, path) {
+    dispatch(selectComposeSuggestion(position, token, suggestion, path));
+  },
+
+  onChangeSpoilerText(text) {
+    dispatch(changeComposeSpoilerText(text));
+  },
+
+  onPaste(files) {
+    dispatch(uploadCompose(files));
+  },
+
+  onPickEmoji(position, emoji) {
+    dispatch(insertEmojiCompose(position, emoji));
+  },
+
+  onChangeSpoilerness() {
+    dispatch(changeComposeSpoilerness());
+  },
+
+  onChangeVisibility(value) {
+    dispatch(changeComposeVisibility(value));
+  },
+
+  onMediaDescriptionConfirm(routerHistory, mediaId, overriddenVisibility = null) {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.missingDescriptionMessage),
+      confirm: intl.formatMessage(messages.missingDescriptionConfirm),
+      onConfirm: () => {
+        if (overriddenVisibility) {
+          dispatch(changeComposeVisibility(overriddenVisibility));
+        }
+        dispatch(submitCompose(routerHistory));
+      },
+      secondary: intl.formatMessage(messages.missingDescriptionEdit),
+      onSecondary: () => dispatch(openModal('FOCAL_POINT', { id: mediaId })),
+      onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
+    }));
+  },
+
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm));
diff --git a/app/javascript/flavours/glitch/features/compose/containers/dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/dropdown_container.js
new file mode 100644
index 000000000..3ac16505f
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/dropdown_container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import { isUserTouching } from 'flavours/glitch/is_mobile';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
+import Dropdown from '../components/dropdown';
+
+const mapDispatchToProps = dispatch => ({
+  isUserTouching,
+  onModalOpen: props => dispatch(openModal('ACTIONS', props)),
+  onModalClose: () => dispatch(closeModal()),
+});
+
+export default connect(null, mapDispatchToProps)(Dropdown);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
new file mode 100644
index 000000000..66d51947a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
@@ -0,0 +1,83 @@
+import { connect } from 'react-redux';
+import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
+import { changeSetting } from 'flavours/glitch/actions/settings';
+import { createSelector } from 'reselect';
+import { Map as ImmutableMap } from 'immutable';
+import { useEmoji } from 'flavours/glitch/actions/emojis';
+
+const perLine = 8;
+const lines   = 2;
+
+const DEFAULTS = [
+  '+1',
+  'grinning',
+  'kissing_heart',
+  'heart_eyes',
+  'laughing',
+  'stuck_out_tongue_winking_eye',
+  'sweat_smile',
+  'joy',
+  'yum',
+  'disappointed',
+  'thinking_face',
+  'weary',
+  'sob',
+  'sunglasses',
+  'heart',
+  'ok_hand',
+];
+
+const getFrequentlyUsedEmojis = createSelector([
+  state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
+], emojiCounters => {
+  let emojis = emojiCounters
+    .keySeq()
+    .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
+    .reverse()
+    .slice(0, perLine * lines)
+    .toArray();
+
+  if (emojis.length < DEFAULTS.length) {
+    let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
+    emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
+  }
+
+  return emojis;
+});
+
+const getCustomEmojis = createSelector([
+  state => state.get('custom_emojis'),
+], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
+  const aShort = a.get('shortcode').toLowerCase();
+  const bShort = b.get('shortcode').toLowerCase();
+
+  if (aShort < bShort) {
+    return -1;
+  } else if (aShort > bShort ) {
+    return 1;
+  } else {
+    return 0;
+  }
+}));
+
+const mapStateToProps = state => ({
+  custom_emojis: getCustomEmojis(state),
+  skinTone: state.getIn(['settings', 'skinTone']),
+  frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
+});
+
+const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
+  onSkinTone: skinTone => {
+    dispatch(changeSetting(['skinTone'], skinTone));
+  },
+
+  onPickEmoji: emoji => {
+    dispatch(useEmoji(emoji));
+
+    if (onPickEmoji) {
+      onPickEmoji(emoji);
+    }
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/header_container.js b/app/javascript/flavours/glitch/features/compose/containers/header_container.js
new file mode 100644
index 000000000..e1ce19fb0
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/header_container.js
@@ -0,0 +1,36 @@
+import { openModal } from 'flavours/glitch/actions/modal';
+import { connect }   from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import Header from '../components/header';
+import { logOut } from 'flavours/glitch/utils/log_out';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapStateToProps = state => {
+  return {
+    columns: state.getIn(['settings', 'columns']),
+    unreadNotifications: state.getIn(['notifications', 'unread']),
+    showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onSettingsClick (e) {
+    e.preventDefault();
+    e.stopPropagation();
+    dispatch(openModal('SETTINGS', {}));
+  },
+  onLogout () {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.logoutMessage),
+      confirm: intl.formatMessage(messages.logoutConfirm),
+      closeWhenConfirm: false,
+      onConfirm: () => logOut(),
+    }));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
new file mode 100644
index 000000000..828d08cf5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux';
+import LanguageDropdown from '../components/language_dropdown';
+import { changeComposeLanguage } from 'flavours/glitch/actions/compose';
+import { useLanguage } from 'flavours/glitch/actions/languages';
+import { createSelector } from 'reselect';
+import { Map as ImmutableMap } from 'immutable';
+
+const getFrequentlyUsedLanguages = createSelector([
+  state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()),
+], languageCounters => (
+  languageCounters.keySeq()
+    .sort((a, b) => languageCounters.get(a) - languageCounters.get(b))
+    .reverse()
+    .toArray()
+));
+
+const mapStateToProps = state => ({
+  frequentlyUsedLanguages: getFrequentlyUsedLanguages(state),
+  value: state.getIn(['compose', 'language']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeComposeLanguage(value));
+  },
+
+  onClose (value) {
+    dispatch(useLanguage(value));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
new file mode 100644
index 000000000..89036adcd
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
@@ -0,0 +1,30 @@
+import { connect }   from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import NavigationBar from '../components/navigation_bar';
+import { logOut } from 'flavours/glitch/utils/log_out';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { me } from 'flavours/glitch/initial_state';
+
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
+const mapStateToProps = state => {
+  return {
+    account: state.getIn(['accounts', me]),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onLogout () {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.logoutMessage),
+      confirm: intl.formatMessage(messages.logoutConfirm),
+      closeWhenConfirm: false,
+      onConfirm: () => logOut(),
+    }));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
new file mode 100644
index 000000000..19a90ac8b
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
@@ -0,0 +1,53 @@
+import { connect } from 'react-redux';
+import Options from '../components/options';
+import {
+  changeComposeAdvancedOption,
+  changeComposeContentType,
+  addPoll,
+  removePoll,
+} from 'flavours/glitch/actions/compose';
+import { openModal } from 'flavours/glitch/actions/modal';
+
+function mapStateToProps (state) {
+  const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
+  const poll = state.getIn(['compose', 'poll']);
+  const media = state.getIn(['compose', 'media_attachments']);
+  const pending_media = state.getIn(['compose', 'pending_media_attachments']);
+  return {
+    acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
+    resetFileKey: state.getIn(['compose', 'resetFileKey']),
+    hasPoll: !!poll,
+    allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4),
+    hasMedia: media && !!media.size,
+    allowPoll: !(media && !!media.size),
+    showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
+    contentType: state.getIn(['compose', 'content_type']),
+  };
+}
+
+const mapDispatchToProps = (dispatch) => ({
+
+  onChangeAdvancedOption(option, value) {
+    dispatch(changeComposeAdvancedOption(option, value));
+  },
+
+  onChangeContentType(value) {
+    dispatch(changeComposeContentType(value));
+  },
+
+  onTogglePoll() {
+    dispatch((_, getState) => {
+      if (getState().getIn(['compose', 'poll'])) {
+        dispatch(removePoll());
+      } else {
+        dispatch(addPoll());
+      }
+    });
+  },
+
+  onDoodleOpen() {
+    dispatch(openModal('DOODLE', { noEsc: true, noClose: true }));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Options);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js
new file mode 100644
index 000000000..14038b3e8
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js
@@ -0,0 +1,52 @@
+import { connect } from 'react-redux';
+import PollForm from '../components/poll_form';
+import {
+  addPollOption,
+  removePollOption,
+  changePollOption,
+  changePollSettings,
+  clearComposeSuggestions,
+  fetchComposeSuggestions,
+  selectComposeSuggestion,
+} from 'flavours/glitch/actions/compose';
+
+const mapStateToProps = state => ({
+  suggestions: state.getIn(['compose', 'suggestions']),
+  options: state.getIn(['compose', 'poll', 'options']),
+  lang: state.getIn(['compose', 'language']),
+  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));
+  },
+
+  onClearSuggestions () {
+    dispatch(clearComposeSuggestions());
+  },
+
+  onFetchSuggestions (token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
+
+  onSuggestionSelected (position, token, accountId, path) {
+    dispatch(selectComposeSuggestion(position, token, accountId, path));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
new file mode 100644
index 000000000..5591d89c4
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux';
+import PrivacyDropdown from '../components/privacy_dropdown';
+import { changeComposeVisibility } from 'flavours/glitch/actions/compose';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
+import { isUserTouching } from 'flavours/glitch/is_mobile';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['compose', 'privacy']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeComposeVisibility(value));
+  },
+
+  isUserTouching,
+  onModalOpen: props => dispatch(openModal('ACTIONS', props)),
+  onModalClose: () => dispatch(closeModal()),
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
new file mode 100644
index 000000000..dd6899be4
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux';
+import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
+import ReplyIndicator from '../components/reply_indicator';
+
+const makeMapStateToProps = () => {
+  const mapStateToProps = state => {
+    let statusId = state.getIn(['compose', 'id'], null);
+    let editing  = true;
+
+    if (statusId === null) {
+      statusId = state.getIn(['compose', 'in_reply_to']);
+      editing  = false;
+    }
+
+    return {
+      status: state.getIn(['statuses', statusId]),
+      editing,
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onCancel () {
+    dispatch(cancelReplyCompose());
+  },
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/search_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
new file mode 100644
index 000000000..8f4bfcf08
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
@@ -0,0 +1,35 @@
+import { connect } from 'react-redux';
+import {
+  changeSearch,
+  clearSearch,
+  submitSearch,
+  showSearch,
+} from 'flavours/glitch/actions/search';
+import Search from '../components/search';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['search', 'value']),
+  submitted: state.getIn(['search', 'submitted']),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeSearch(value));
+  },
+
+  onClear () {
+    dispatch(clearSearch());
+  },
+
+  onSubmit () {
+    dispatch(submitSearch());
+  },
+
+  onShow () {
+    dispatch(showSearch());
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
new file mode 100644
index 000000000..5c2c1be23
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import SearchResults from '../components/search_results';
+import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
+import { expandSearch } from 'flavours/glitch/actions/search';
+
+const mapStateToProps = state => ({
+  results: state.getIn(['search', 'results']),
+  suggestions: state.getIn(['suggestions', 'items']),
+  searchTerm: state.getIn(['search', 'searchTerm']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  fetchSuggestions: () => dispatch(fetchSuggestions()),
+  expandSearch: type => dispatch(expandSearch(type)),
+  dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx
new file mode 100644
index 000000000..9c23d3f47
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { changeComposeSensitivity } from 'flavours/glitch/actions/compose';
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+const messages = defineMessages({
+  marked: {
+    id: 'compose_form.sensitive.marked',
+    defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
+  },
+  unmarked: {
+    id: 'compose_form.sensitive.unmarked',
+    defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
+  },
+});
+
+const mapStateToProps = state => {
+  const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
+  const spoilerText = state.getIn(['compose', 'spoiler_text']);
+  return {
+    active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0),
+    disabled: state.getIn(['compose', 'spoiler']),
+    mediaCount: state.getIn(['compose', 'media_attachments']).size,
+  };
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onClick () {
+    dispatch(changeComposeSensitivity());
+  },
+
+});
+
+class SensitiveButton extends React.PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    disabled: PropTypes.bool,
+    mediaCount: PropTypes.number,
+    onClick: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { active, disabled, mediaCount, onClick, intl } = this.props;
+
+    return (
+      <div className='compose-form__sensitive-button'>
+        <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
+          <input
+            name='mark-sensitive'
+            type='checkbox'
+            checked={active}
+            onChange={onClick}
+            disabled={disabled}
+          />
+
+          <span className={classNames('checkbox', { active })} />
+
+          <FormattedMessage
+            id='compose_form.sensitive.hide'
+            defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
+            values={{ count: mediaCount }}
+          />
+        </label>
+      </div>
+    );
+  }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
new file mode 100644
index 000000000..2189c870b
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import Upload from '../components/upload';
+import { undoUploadCompose, initMediaEditModal, submitCompose } from 'flavours/glitch/actions/compose';
+
+const mapStateToProps = (state, { id }) => ({
+  media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onUndo: id => {
+    dispatch(undoUploadCompose(id));
+  },
+
+  onOpenFocalPoint: id => {
+    dispatch(initMediaEditModal(id));
+  },
+
+  onSubmit (router) {
+    dispatch(submitCompose(router));
+  },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Upload);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
new file mode 100644
index 000000000..a6798bf51
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import UploadForm from '../components/upload_form';
+
+const mapStateToProps = state => ({
+  mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
+});
+
+export default connect(mapStateToProps)(UploadForm);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
new file mode 100644
index 000000000..b18c76a43
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
@@ -0,0 +1,10 @@
+import { connect } from 'react-redux';
+import UploadProgress from '../components/upload_progress';
+
+const mapStateToProps = state => ({
+  active: state.getIn(['compose', 'is_uploading']),
+  progress: state.getIn(['compose', 'progress']),
+  isProcessing: state.getIn(['compose', 'is_processing']),
+});
+
+export default connect(mapStateToProps)(UploadProgress);
diff --git a/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx
new file mode 100644
index 000000000..5b48c45e4
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import Warning from '../components/warning';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { me } from 'flavours/glitch/initial_state';
+import { profileLink, termsLink } from 'flavours/glitch/utils/backend_links';
+
+const buildHashtagRE = () => {
+  try {
+    const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
+    const ALPHA = '\\p{L}\\p{M}';
+    const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
+    return new RegExp(
+      '(?:^|[^\\/\\)\\w])#((' +
+      '[' + WORD + '_]' +
+      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
+      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
+      '[' + WORD + HASHTAG_SEPARATORS +']*' +
+      '[' + WORD + '_]' +
+      ')|(' +
+      '[' + WORD + '_]*' +
+      '[' + ALPHA + ']' +
+      '[' + WORD + '_]*' +
+      '))', 'iu',
+    );
+  } catch {
+    return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
+  }
+};
+
+const APPROX_HASHTAG_RE = buildHashtagRE();
+
+const mapStateToProps = state => ({
+  needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
+  hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
+  directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
+});
+
+const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
+  if (needsLockWarning) {
+    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
+  }
+
+  if (hashtagWarning) {
+    return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
+  }
+
+  if (directMessageWarning) {
+    const message = (
+      <span>
+        <FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> {!!termsLink && <a href={termsLink} target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
+      </span>
+    );
+
+    return <Warning message={message} />;
+  }
+
+  return null;
+};
+
+WarningWrapper.propTypes = {
+  needsLockWarning: PropTypes.bool,
+  hashtagWarning: PropTypes.bool,
+  directMessageWarning: PropTypes.bool,
+};
+
+export default connect(mapStateToProps)(WarningWrapper);