about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/actions/boosts.js29
-rw-r--r--app/javascript/mastodon/actions/interactions.js4
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js1
-rw-r--r--app/javascript/mastodon/containers/dropdown_menu_container.js1
-rw-r--r--app/javascript/mastodon/containers/status_container.js7
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js18
-rw-r--r--app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js1
-rw-r--r--app/javascript/mastodon/features/notifications/containers/notification_container.js7
-rw-r--r--app/javascript/mastodon/features/picture_in_picture/components/footer.js11
-rw-r--r--app/javascript/mastodon/features/status/containers/detailed_status_container.js7
-rw-r--r--app/javascript/mastodon/features/status/index.js7
-rw-r--r--app/javascript/mastodon/features/ui/components/boost_modal.js38
-rw-r--r--app/javascript/mastodon/reducers/boosts.js25
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/styles/mastodon/components.scss10
15 files changed, 137 insertions, 31 deletions
diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js
new file mode 100644
index 000000000..6e14065d6
--- /dev/null
+++ b/app/javascript/mastodon/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/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 28c6b1a62..d60ccc1fb 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/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/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index c6b4b1187..7d0588901 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/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/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js
index 6ec9bbffd..c45bab40b 100644
--- a/app/javascript/mastodon/containers/dropdown_menu_container.js
+++ b/app/javascript/mastodon/containers/dropdown_menu_container.js
@@ -6,7 +6,6 @@ import DropdownMenu from '../components/dropdown_menu';
 import { isUserTouching } from '../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/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index d6bcb8973..9abdec138 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -35,6 +35,7 @@ import {
 } from '../actions/domain_blocks';
 import { initMuteModal } from '../actions/mutes';
 import { initBlockModal } from '../actions/blocks';
+import { initBoostModal } from '../actions/boosts';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { deployPictureInPicture } from '../actions/picture_in_picture';
@@ -82,11 +83,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     });
   },
 
-  onModalReblog (status) {
+  onModalReblog (status, privacy) {
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else {
-      dispatch(reblog(status));
+      dispatch(reblog(status, privacy));
     }
   },
 
@@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     if ((e && 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/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 309f46290..936b14e9e 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -127,7 +127,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
           // 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={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, zIndex: 2 }} role='listbox' ref={this.setRef}>
+          <div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
             {items.map(item => (
               <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
                 <div className='privacy-dropdown__option__icon'>
@@ -153,11 +153,12 @@ class PrivacyDropdown extends React.PureComponent {
 
   static propTypes = {
     isUserTouching: PropTypes.func,
-    isModalOpen: PropTypes.bool.isRequired,
     onModalOpen: PropTypes.func,
     onModalClose: PropTypes.func,
     value: PropTypes.string.isRequired,
     onChange: PropTypes.func.isRequired,
+    noDirect: PropTpes.bool,
+    container: PropTypes.func,
     intl: PropTypes.object.isRequired,
   };
 
@@ -167,7 +168,7 @@ class PrivacyDropdown extends React.PureComponent {
   };
 
   handleToggle = ({ target }) => {
-    if (this.props.isUserTouching()) {
+    if (this.props.isUserTouching && this.props.isUserTouching()) {
       if (this.state.open) {
         this.props.onModalClose();
       } else {
@@ -236,12 +237,17 @@ class PrivacyDropdown extends React.PureComponent {
       { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
       { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
       { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
-      { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
     ];
+
+    if (!this.props.noDirect) {
+      this.options.push(
+        { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
+      );
+    }
   }
 
   render () {
-    const { value, intl } = this.props;
+    const { value, container, intl } = this.props;
     const { open, placement } = this.state;
 
     const valueOption = this.options.find(item => item.value === value);
@@ -264,7 +270,7 @@ class PrivacyDropdown extends React.PureComponent {
           />
         </div>
 
-        <Overlay show={open} placement={placement} target={this}>
+        <Overlay show={open} placement={placement} target={this} container={container}>
           <PrivacyDropdownMenu
             items={this.options}
             value={value}
diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
index 0ddf531d3..e5bbb94da 100644
--- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
@@ -5,7 +5,6 @@ import { openModal, closeModal } from '../../../actions/modal';
 import { isUserTouching } from '../../../is_mobile';
 
 const mapStateToProps = state => ({
-  isModalOpen: state.get('modal').modalType === 'ACTIONS',
   value: state.getIn(['compose', 'privacy']),
 });
 
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 78576c760..555d5e1b5 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -1,6 +1,7 @@
 import { connect } from 'react-redux';
 import { makeGetNotification, makeGetStatus } from '../../../selectors';
 import Notification from '../components/notification';
+import { initBoostModal } from '../../../actions/boosts';
 import { openModal } from '../../../actions/modal';
 import { mentionCompose } from '../../../actions/compose';
 import {
@@ -35,8 +36,8 @@ const mapDispatchToProps = dispatch => ({
     dispatch(mentionCompose(account, router));
   },
 
-  onModalReblog (status) {
-    dispatch(reblog(status));
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
   },
 
   onReblog (status, e) {
@@ -46,7 +47,7 @@ const mapDispatchToProps = dispatch => ({
       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/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 1b1ec6d54..1ecb18bf8 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -10,6 +10,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import { replyCompose } from 'mastodon/actions/compose';
 import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
 import { makeGetStatus } from 'mastodon/selectors';
+import { initBoostModal } from 'mastodon/actions/boosts';
 import { openModal } from 'mastodon/actions/modal';
 
 const messages = defineMessages({
@@ -89,9 +90,9 @@ class Footer extends ImmutablePureComponent {
     }
   };
 
-  _performReblog = () => {
-    const { dispatch, status } = this.props;
-    dispatch(reblog(status));
+  _performReblog = (status, privacy) => {
+    const { dispatch } = this.props;
+    dispatch(reblog(status, privacy));
   }
 
   handleReblogClick = e => {
@@ -100,9 +101,9 @@ class Footer extends ImmutablePureComponent {
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else if ((e && e.shiftKey) || !boostModal) {
-      this._performReblog();
+      this._performReblog(status);
     } else {
-      dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
+      dispatch(initBoostModal({ status, onReblog: this._performReblog }));
     }
   };
 
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 0ac4519c8..bfed16620 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -23,6 +23,7 @@ import {
 } from '../../../actions/statuses';
 import { initMuteModal } from '../../../actions/mutes';
 import { initBlockModal } from '../../../actions/blocks';
+import { initBoostModal } from '../../../actions/boosts';
 import { initReport } from '../../../actions/reports';
 import { openModal } from '../../../actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -68,8 +69,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     });
   },
 
-  onModalReblog (status) {
-    dispatch(reblog(status));
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
   },
 
   onReblog (status, e) {
@@ -79,7 +80,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/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 09822f372..df8362a1b 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -42,6 +42,7 @@ import {
 } from '../../actions/domain_blocks';
 import { initMuteModal } from '../../actions/mutes';
 import { initBlockModal } from '../../actions/blocks';
+import { initBoostModal } from '../../actions/boosts';
 import { initReport } from '../../actions/reports';
 import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
@@ -234,8 +235,8 @@ class Status extends ImmutablePureComponent {
     }
   }
 
-  handleModalReblog = (status) => {
-    this.props.dispatch(reblog(status));
+  handleModalReblog = (status, privacy) => {
+    this.props.dispatch(reblog(status, privacy));
   }
 
   handleReblogClick = (status, e) => {
@@ -245,7 +246,7 @@ class Status extends ImmutablePureComponent {
       if ((e && e.shiftKey) || !boostModal) {
         this.handleModalReblog(status);
       } else {
-        this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
+        this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
       }
     }
   }
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js
index 963bb5dc4..83229833b 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.js
+++ b/app/javascript/mastodon/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 '../../../components/display_name';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Icon from 'mastodon/components/icon';
 import AttachmentList from 'mastodon/components/attachment_list';
+import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
 import classNames from 'classnames';
+import { changeBoostPrivacy } from 'mastodon/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 = {
@@ -32,6 +50,8 @@ class BoostModal extends ImmutablePureComponent {
     status: ImmutablePropTypes.map.isRequired,
     onReblog: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
+    onChangeBoostPrivacy: PropTypes.func.isRequired,
+    privacy: PropTypes.string.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -40,7 +60,7 @@ class BoostModal extends ImmutablePureComponent {
   }
 
   handleReblog = () => {
-    this.props.onReblog(this.props.status);
+    this.props.onReblog(this.props.status, this.props.privacy);
     this.props.onClose();
   }
 
@@ -52,12 +72,16 @@ class BoostModal extends ImmutablePureComponent {
     }
   }
 
+  _findContainer = () => {
+    return document.getElementsByClassName('modal-root__container')[0];
+  };
+
   setRef = (c) => {
     this.button = c;
   }
 
   render () {
-    const { status, intl } = this.props;
+    const { status, privacy, intl } = this.props;
     const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
 
     const visibilityIconInfo = {
@@ -102,6 +126,14 @@ class BoostModal extends ImmutablePureComponent {
 
         <div className='boost-modal__action-bar'>
           <div><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/mastodon/reducers/boosts.js b/app/javascript/mastodon/reducers/boosts.js
new file mode 100644
index 000000000..d0d825057
--- /dev/null
+++ b/app/javascript/mastodon/reducers/boosts.js
@@ -0,0 +1,25 @@
+import Immutable from 'immutable';
+
+import {
+  BOOSTS_INIT_MODAL,
+  BOOSTS_CHANGE_PRIVACY,
+} from 'mastodon/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/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a8fb69c27..3b3c5ae29 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -16,6 +16,7 @@ import push_notifications from './push_notifications';
 import status_lists from './status_lists';
 import mutes from './mutes';
 import blocks from './blocks';
+import boosts from './boosts';
 import reports from './reports';
 import contexts from './contexts';
 import compose from './compose';
@@ -57,6 +58,7 @@ const reducers = {
   push_notifications,
   mutes,
   blocks,
+  boosts,
   reports,
   contexts,
   compose,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 43b340723..bdb7ce768 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4209,6 +4209,7 @@ a.status-card.compact:hover {
   border-radius: 4px;
   margin-left: 40px;
   overflow: hidden;
+  z-index: 2;
 
   &.top {
     transform-origin: 50% 100%;
@@ -4219,6 +4220,15 @@ a.status-card.compact:hover {
   }
 }
 
+.modal-root__container .privacy-dropdown {
+  flex-grow: 0;
+}
+
+.modal-root__container .privacy-dropdown__dropdown {
+  pointer-events: auto;
+  z-index: 9999;
+}
+
 .privacy-dropdown__option {
   color: $inverted-text-color;
   padding: 10px;