about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/boosts.js29
-rw-r--r--app/javascript/flavours/glitch/actions/interactions.js4
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js1
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js16
-rw-r--r--app/javascript/flavours/glitch/containers/dropdown_menu_container.js1
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js9
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown.js8
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js76
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js115
-rw-r--r--app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js7
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js7
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js7
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js9
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/boost_modal.js37
-rw-r--r--app/javascript/flavours/glitch/reducers/boosts.js25
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/modal.js3
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss13
-rw-r--r--app/javascript/flavours/glitch/styles/modal.scss13
-rw-r--r--app/javascript/flavours/glitch/util/resize_image.js2
20 files changed, 274 insertions, 110 deletions
diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js
new file mode 100644
index 000000000..6e14065d6
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/boosts.js
@@ -0,0 +1,29 @@
+import { openModal } from './modal';
+
+export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
+export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
+
+export function initBoostModal(props) {
+  return (dispatch, getState) => {
+    const default_privacy = getState().getIn(['compose', 'default_privacy']);
+
+    const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
+
+    dispatch({
+      type: BOOSTS_INIT_MODAL,
+      privacy
+    });
+
+    dispatch(openModal('BOOST', props));
+  };
+}
+
+
+export function changeBoostPrivacy(privacy) {
+  return dispatch => {
+    dispatch({
+      type: BOOSTS_CHANGE_PRIVACY,
+      privacy,
+    });
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index 4407f8b6e..336c8fa90 100644
--- a/app/javascript/flavours/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
 export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
 export const UNBOOKMARK_FAIL    = 'UNBOOKMARKED_FAIL';
 
-export function reblog(status) {
+export function reblog(status, visibility) {
   return function (dispatch, getState) {
     dispatch(reblogRequest(status));
 
-    api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
       // The reblog API method returns a new status wrapped around the original. In this case we are only
       // interested in how the original is modified, hence passing it skipping the wrapper
       dispatch(importFetchedStatus(response.data.reblog));
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index d1aba691c..023fecb9a 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent {
     disabled: PropTypes.bool,
     status: ImmutablePropTypes.map,
     isUserTouching: PropTypes.func,
-    isModalOpen: PropTypes.bool.isRequired,
     onOpen: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     dropdownPlacement: PropTypes.string,
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 2ccb02c62..74bfd948e 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -193,9 +193,10 @@ class StatusActionBar extends ImmutablePureComponent {
   render () {
     const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props;
 
-    const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
+    const mutingConversation = status.get('muted');
     const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
 
     let menu = [];
     let reblogIcon = 'retweet';
@@ -211,16 +212,17 @@ class StatusActionBar extends ImmutablePureComponent {
 
     menu.push(null);
 
-    if (status.getIn(['account', 'id']) === me || withDismiss) {
-      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+    if (writtenByMe && publicStatus) {
+      menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
       menu.push(null);
     }
 
-    if (status.getIn(['account', 'id']) === me) {
-      if (publicStatus) {
-        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
-      }
+    if (writtenByMe || withDismiss) {
+      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+      menu.push(null);
+    }
 
+    if (writtenByMe) {
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
     } else {
diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
index e60ee814d..1c0385b74 100644
--- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
+++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
@@ -5,7 +5,6 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
 import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 const mapStateToProps = state => ({
-  isModalOpen: state.get('modal').modalType === 'ACTIONS',
   dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
   openDropdownId: state.getIn(['dropdown_menu', 'openId']),
   openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 7782246a6..6461bf805 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -21,6 +21,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
+import { initBoostModal } from 'flavours/glitch/actions/boosts';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
@@ -96,11 +97,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     });
   },
 
-  onModalReblog (status) {
+  onModalReblog (status, privacy) {
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else {
-      dispatch(reblog(status));
+      dispatch(reblog(status, privacy));
     }
   },
 
@@ -108,11 +109,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     dispatch((_, getState) => {
       let state = getState();
       if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog, missingMediaDescription: true }));
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog, missingMediaDescription: true }));
       } else if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
       }
     });
   },
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
index 04ef3964b..abf7cbba1 100644
--- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
@@ -31,6 +31,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
     title: PropTypes.string,
     value: PropTypes.string,
     onChange: PropTypes.func,
+    noModal: PropTypes.bool,
+    container: PropTypes.func,
   };
 
   state = {
@@ -42,10 +44,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
 
   //  Toggles opening and closing the dropdown.
   handleToggle = ({ target, type }) => {
-    const { onModalOpen } = this.props;
+    const { onModalOpen, noModal } = this.props;
     const { open } = this.state;
 
-    if (isUserTouching()) {
+    if (!noModal && isUserTouching()) {
       if (this.state.open) {
         this.props.onModalClose();
       } else {
@@ -183,6 +185,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
       items,
       onChange,
       value,
+      container,
     } = this.props;
     const { open, placement } = this.state;
     const computedClass = classNames('composer--options--dropdown', {
@@ -219,6 +222,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
           placement={placement}
           show={open}
           target={this}
+          container={container}
         >
           <DropdownMenu
             items={items}
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index 9e332aabd..f9212bbae 100644
--- a/app/javascript/flavours/glitch/features/compose/components/options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/options.js
@@ -9,6 +9,7 @@ import spring from 'react-motion/lib/spring';
 import IconButton from 'flavours/glitch/components/icon_button';
 import TextIconButton from './text_icon_button';
 import Dropdown from './dropdown';
+import PrivacyDropdown from './privacy_dropdown';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 //  Utils.
@@ -25,22 +26,10 @@ const messages = defineMessages({
     defaultMessage: 'Attach...',
     id: 'compose.attach',
   },
-  change_privacy: {
-    defaultMessage: 'Adjust status privacy',
-    id: 'privacy.change',
-  },
   content_type: {
     defaultMessage: 'Content type',
     id: 'content-type.change',
   },
-  direct_long: {
-    defaultMessage: 'Visible for mentioned users only',
-    id: 'privacy.direct.long',
-  },
-  direct_short: {
-    defaultMessage: 'Direct',
-    id: 'privacy.direct.short',
-  },
   doodle: {
     defaultMessage: 'Draw something',
     id: 'compose.attach.doodle',
@@ -65,22 +54,6 @@ const messages = defineMessages({
     defaultMessage: 'Plain text',
     id: 'compose.content-type.plain',
   },
-  private_long: {
-    defaultMessage: 'Visible for followers only',
-    id: 'privacy.private.long',
-  },
-  private_short: {
-    defaultMessage: 'Followers-only',
-    id: 'privacy.private.short',
-  },
-  public_long: {
-    defaultMessage: 'Visible for all, shown in public timelines',
-    id: 'privacy.public.long',
-  },
-  public_short: {
-    defaultMessage: 'Public',
-    id: 'privacy.public.short',
-  },
   spoiler: {
     defaultMessage: 'Hide text behind warning',
     id: 'compose_form.spoiler',
@@ -93,14 +66,6 @@ const messages = defineMessages({
     defaultMessage: 'Threaded mode',
     id: 'advanced_options.threaded_mode.short',
   },
-  unlisted_long: {
-    defaultMessage: 'Visible for all, but not in public timelines',
-    id: 'privacy.unlisted.long',
-  },
-  unlisted_short: {
-    defaultMessage: 'Unlisted',
-    id: 'privacy.unlisted.short',
-  },
   upload: {
     defaultMessage: 'Upload a file',
     id: 'compose.attach.upload',
@@ -201,35 +166,6 @@ class ComposerOptions extends ImmutablePureComponent {
       showContentTypeChoice,
     } = 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} />,
-      },
-    };
-
     const contentTypeItems = {
       plain: {
         icon: 'file-text',
@@ -297,19 +233,11 @@ class ComposerOptions extends ImmutablePureComponent {
           />
         )}
         <hr />
-        <Dropdown
+        <PrivacyDropdown
           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}
         />
         {showContentTypeChoice && (
diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
new file mode 100644
index 000000000..39f7c7bd1
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
@@ -0,0 +1,115 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import Dropdown from './dropdown';
+
+const messages = defineMessages({
+  change_privacy: {
+    defaultMessage: 'Adjust status privacy',
+    id: 'privacy.change',
+  },
+  direct_long: {
+    defaultMessage: 'Visible for mentioned users only',
+    id: 'privacy.direct.long',
+  },
+  direct_short: {
+    defaultMessage: 'Direct',
+    id: 'privacy.direct.short',
+  },
+  private_long: {
+    defaultMessage: 'Visible for followers only',
+    id: 'privacy.private.long',
+  },
+  private_short: {
+    defaultMessage: 'Followers-only',
+    id: 'privacy.private.short',
+  },
+  public_long: {
+    defaultMessage: 'Visible for all, shown in public timelines',
+    id: 'privacy.public.long',
+  },
+  public_short: {
+    defaultMessage: 'Public',
+    id: 'privacy.public.short',
+  },
+  unlisted_long: {
+    defaultMessage: 'Visible for all, but not in public timelines',
+    id: 'privacy.unlisted.long',
+  },
+  unlisted_short: {
+    defaultMessage: 'Unlisted',
+    id: 'privacy.unlisted.short',
+  },
+});
+
+export default @injectIntl
+class PrivacyDropdown extends React.PureComponent {
+
+  static propTypes = {
+    isUserTouching: PropTypes.func,
+    onModalOpen: PropTypes.func,
+    onModalClose: PropTypes.func,
+    value: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired,
+    noDirect: PropTypes.bool,
+    noModal: PropTypes.bool,
+    container: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = 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} />,
+      },
+    };
+
+    const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private];
+
+    if (!noDirect) {
+      items.push(privacyItems.direct);
+    }
+
+    return (
+      <Dropdown
+        disabled={disabled}
+        icon={(privacyItems[value] || {}).icon}
+        items={items}
+        onChange={onChange}
+        onModalClose={onModalClose}
+        onModalOpen={onModalOpen}
+        title={intl.formatMessage(messages.change_privacy)}
+        container={container}
+        noModal={noModal}
+        value={value}
+      />
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
index 2ddba140e..d8989ec61 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
@@ -11,6 +11,7 @@ import { replyCompose } from 'flavours/glitch/actions/compose';
 import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions';
 import { makeGetStatus } from 'flavours/glitch/selectors';
 import { openModal } from 'flavours/glitch/actions/modal';
+import { initBoostModal } from 'flavours/glitch/actions/boosts';
 
 const messages = defineMessages({
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
@@ -82,9 +83,9 @@ class Footer extends ImmutablePureComponent {
     }
   };
 
-  _performReblog = () => {
+  _performReblog = (privacy) => {
     const { dispatch, status } = this.props;
-    dispatch(reblog(status));
+    dispatch(reblog(status, privacy));
   }
 
   handleReblogClick = e => {
@@ -95,7 +96,7 @@ class Footer extends ImmutablePureComponent {
     } else if ((e && e.shiftKey) || !boostModal) {
       this._performReblog();
     } else {
-      dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
+      dispatch(initBoostModal({ status, onReblog: this._performReblog }));
     }
   };
 
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 0f16d93fe..6ed5f3865 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -145,8 +145,9 @@ class ActionBar extends React.PureComponent {
   render () {
     const { status, intl } = this.props;
 
-    const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
     const mutingConversation = status.get('muted');
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
 
     let menu = [];
 
@@ -156,12 +157,12 @@ class ActionBar extends React.PureComponent {
       menu.push(null);
     }
 
-    if (me === status.getIn(['account', 'id'])) {
+    if (writtenByMe) {
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+        menu.push(null);
       }
 
-      menu.push(null);
       menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
       menu.push(null);
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index 9d11f37e0..40e186569 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -24,6 +24,7 @@ import {
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
+import { initBoostModal } from 'flavours/glitch/actions/boosts';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
 import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
@@ -67,8 +68,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     });
   },
 
-  onModalReblog (status) {
-    dispatch(reblog(status));
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
   },
 
   onReblog (status, e) {
@@ -78,7 +79,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
       if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
       }
     }
   },
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index b330adf3f..21e441407 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -30,6 +30,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
+import { initBoostModal } from 'flavours/glitch/actions/boosts';
 import { makeGetStatus } from 'flavours/glitch/selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
 import ColumnBackButton from 'flavours/glitch/components/column_back_button';
@@ -262,13 +263,13 @@ class Status extends ImmutablePureComponent {
     }
   }
 
-  handleModalReblog = (status) => {
+  handleModalReblog = (status, privacy) => {
     const { dispatch } = this.props;
 
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else {
-      dispatch(reblog(status));
+      dispatch(reblog(status, privacy));
     }
   }
 
@@ -276,11 +277,11 @@ class Status extends ImmutablePureComponent {
     const { settings, dispatch } = this.props;
 
     if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
-      dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
+      dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
     } else if ((e && e.shiftKey) || !boostModal) {
       this.handleModalReblog(status);
     } else {
-      dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
+      dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
index 12ad426c8..c4af25599 100644
--- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
@@ -1,4 +1,5 @@
 import React from 'react';
+import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -10,7 +11,9 @@ import DisplayName from 'flavours/glitch/components/display_name';
 import AttachmentList from 'flavours/glitch/components/attachment_list';
 import Icon from 'flavours/glitch/components/icon';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
 import classNames from 'classnames';
+import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
 
 const messages = defineMessages({
   cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@@ -21,7 +24,22 @@ const messages = defineMessages({
   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 });
 
-export default @injectIntl
+const mapStateToProps = state => {
+  return {
+    privacy: state.getIn(['boosts', 'new', 'privacy']),
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onChangeBoostPrivacy(value) {
+      dispatch(changeBoostPrivacy(value));
+    },
+  };
+};
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
 class BoostModal extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -41,7 +59,7 @@ class BoostModal extends ImmutablePureComponent {
   }
 
   handleReblog = () => {
-    this.props.onReblog(this.props.status);
+    this.props.onReblog(this.props.status, this.props.privacy);
     this.props.onClose();
   }
 
@@ -55,12 +73,16 @@ class BoostModal extends ImmutablePureComponent {
     }
   }
 
+  _findContainer = () => {
+    return document.getElementsByClassName('modal-root__container')[0];
+  };
+
   setRef = (c) => {
     this.button = c;
   }
 
   render () {
-    const { status, missingMediaDescription, intl } = this.props;
+    const { status, missingMediaDescription, privacy, intl } = this.props;
     const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
 
     const visibilityIconInfo = {
@@ -111,6 +133,15 @@ class BoostModal extends ImmutablePureComponent {
                 <FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
             }
           </div>
+
+          {status.get('visibility') !== 'private' && !status.get('reblogged') && (
+            <PrivacyDropdown
+              noDirect
+              value={privacy}
+              container={this._findContainer}
+              onChange={this.props.onChangeBoostPrivacy}
+            />
+          )}
           <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/reducers/boosts.js b/app/javascript/flavours/glitch/reducers/boosts.js
new file mode 100644
index 000000000..3541ca0c2
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/boosts.js
@@ -0,0 +1,25 @@
+import Immutable from 'immutable';
+
+import {
+  BOOSTS_INIT_MODAL,
+  BOOSTS_CHANGE_PRIVACY,
+} from 'flavours/glitch/actions/boosts';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    privacy: 'public',
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BOOSTS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'privacy'], action.privacy);
+    });
+  case BOOSTS_CHANGE_PRIVACY:
+    return state.setIn(['new', 'privacy'], action.privacy);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index b1ddb769e..c452e834c 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -18,6 +18,7 @@ import status_lists from './status_lists';
 import mutes from './mutes';
 import blocks from './blocks';
 import reports from './reports';
+import boosts from './boosts';
 import contexts from './contexts';
 import compose from './compose';
 import search from './search';
@@ -61,6 +62,7 @@ const reducers = {
   mutes,
   blocks,
   reports,
+  boosts,
   contexts,
   compose,
   search,
diff --git a/app/javascript/flavours/glitch/reducers/modal.js b/app/javascript/flavours/glitch/reducers/modal.js
index 7bd9d4b32..52b05d69b 100644
--- a/app/javascript/flavours/glitch/reducers/modal.js
+++ b/app/javascript/flavours/glitch/reducers/modal.js
@@ -1,4 +1,5 @@
 import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
+import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
 
 const initialState = {
   modalType: null,
@@ -11,6 +12,8 @@ export default function modal(state = initialState, action) {
     return { modalType: action.modalType, modalProps: action.modalProps };
   case MODAL_CLOSE:
     return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
+  case TIMELINE_DELETE:
+    return (state.modalProps.statusId === action.id) ? initialState : state;
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index 421cbec00..0bda305a8 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -458,6 +458,10 @@
   }
 }
 
+.favourite-modal .status-direct {
+  background-color: inherit;
+}
+
 .actions-modal {
   .status {
     background: $white;
@@ -1027,3 +1031,12 @@
     }
   }
 }
+
+.modal-root__container .composer--options--dropdown {
+  flex-grow: 0;
+}
+
+.modal-root__container .composer--options--dropdown--content {
+  pointer-events: auto;
+  z-index: 9999;
+}
diff --git a/app/javascript/flavours/glitch/styles/modal.scss b/app/javascript/flavours/glitch/styles/modal.scss
index 10de454c6..6c6de4206 100644
--- a/app/javascript/flavours/glitch/styles/modal.scss
+++ b/app/javascript/flavours/glitch/styles/modal.scss
@@ -12,10 +12,19 @@
   flex-direction: column;
   justify-content: flex-end;
 
-  > * {
+  > div {
     flex: 1;
     max-height: 235px;
-    background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain;
+    position: relative;
+
+    img {
+      max-height: 100%;
+      max-width: 100%;
+      height: 100%;
+      position: absolute;
+      bottom: 0;
+      left: 0;
+    }
   }
 }
 
diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js
index 57fc6a6ec..fb8c3c11e 100644
--- a/app/javascript/flavours/glitch/util/resize_image.js
+++ b/app/javascript/flavours/glitch/util/resize_image.js
@@ -1,6 +1,6 @@
 import EXIF from 'exif-js';
 
-const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
+const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
 
 const _browser_quirks = {};