diff --git a/app/soapbox/actions/modal.js b/app/soapbox/actions/modal.js index 8c0eed922..72604ecc6 100644 --- a/app/soapbox/actions/modal.js +++ b/app/soapbox/actions/modal.js @@ -9,10 +9,9 @@ export function openModal(type, props) { }; } -export function closeModal(type, noPop) { +export function closeModal(type) { return { type: MODAL_CLOSE, modalType: type, - noPop, }; } diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index 3b622e3e0..6cbe185e3 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -6,7 +6,7 @@ import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; import { cancelReplyCompose } from '../actions/compose'; -import { openModal } from '../actions/modal'; +import { openModal, closeModal } from '../actions/modal'; const messages = defineMessages({ confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -31,7 +31,11 @@ const mapDispatchToProps = (dispatch) => ({ onOpenModal(type, opts) { dispatch(openModal(type, opts)); }, + onCloseModal(type) { + dispatch(closeModal(type)); + }, onCancelReplyCompose() { + dispatch(closeModal('COMPOSE')); dispatch(cancelReplyCompose()); }, }); @@ -42,12 +46,12 @@ class ModalRoot extends React.PureComponent { children: PropTypes.node, onClose: PropTypes.func.isRequired, onOpenModal: PropTypes.func.isRequired, + onCloseModal: PropTypes.func.isRequired, onCancelReplyCompose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, hasComposeContent: PropTypes.bool, type: PropTypes.string, onCancel: PropTypes.func, - noPop: PropTypes.bool, }; state = { @@ -64,7 +68,7 @@ class ModalRoot extends React.PureComponent { } handleOnClose = () => { - const { onOpenModal, hasComposeContent, intl, type, onCancelReplyCompose } = this.props; + const { onOpenModal, onCloseModal, hasComposeContent, intl, type, onCancelReplyCompose } = this.props; if (hasComposeContent && type === 'COMPOSE') { onOpenModal('CONFIRM', { @@ -73,10 +77,10 @@ class ModalRoot extends React.PureComponent { message: , confirm: intl.formatMessage(messages.confirm), onConfirm: () => onCancelReplyCompose(), - onCancel: () => onOpenModal('COMPOSE'), + onCancel: () => onCloseModal('CONFIRM'), }); } else if (hasComposeContent && type === 'CONFIRM') { - onOpenModal('COMPOSE'); + onCloseModal('CONFIRM'); } else { this.props.onClose(); } @@ -125,9 +129,7 @@ class ModalRoot extends React.PureComponent { this.activeElement = null; this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); - if (!this.props.noPop) { - this._handleModalClose(); - } + this._handleModalClose(prevProps.type); } if (this.props.children) { @@ -155,13 +157,15 @@ class ModalRoot extends React.PureComponent { }); } - _handleModalClose() { + _handleModalClose(type) { if (this.unlistenHistory) { this.unlistenHistory(); } - const { state } = this.history.location; - if (state && state.soapboxModalKey === this._modalHistoryKey) { - this.history.goBack(); + if (!['FAVOURITES', 'MENTIONS', 'REACTIONS', 'REBLOGS'].includes(type)) { + const { state } = this.history.location; + if (state && state.soapboxModalKey === this._modalHistoryKey) { + this.history.goBack(); + } } } diff --git a/app/soapbox/containers/dropdown_menu_container.js b/app/soapbox/containers/dropdown_menu_container.js index 79d8445bf..ba1932052 100644 --- a/app/soapbox/containers/dropdown_menu_container.js +++ b/app/soapbox/containers/dropdown_menu_container.js @@ -6,7 +6,7 @@ import DropdownMenu from '../components/dropdown_menu'; import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', + isModalOpen: state.get('modal').size && state.get('modal').last().modalType === 'ACTIONS', dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), diff --git a/app/soapbox/features/compose/containers/compose_form_container.js b/app/soapbox/features/compose/containers/compose_form_container.js index 0fe6fc1eb..3478b852c 100644 --- a/app/soapbox/features/compose/containers/compose_form_container.js +++ b/app/soapbox/features/compose/containers/compose_form_container.js @@ -26,7 +26,7 @@ const mapStateToProps = state => ({ isUploading: state.getIn(['compose', 'is_uploading']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, - isModalOpen: state.get('modal').modalType === 'COMPOSE', + isModalOpen: state.get('modal').size && state.get('modal').last().modalType === 'COMPOSE', maxTootChars: state.getIn(['instance', 'max_toot_chars']), scheduledAt: state.getIn(['compose', 'schedule']), scheduledStatusCount: state.get('scheduled_statuses').size, diff --git a/app/soapbox/features/compose/containers/privacy_dropdown_container.js b/app/soapbox/features/compose/containers/privacy_dropdown_container.js index 23da32fbd..811b96772 100644 --- a/app/soapbox/features/compose/containers/privacy_dropdown_container.js +++ b/app/soapbox/features/compose/containers/privacy_dropdown_container.js @@ -6,7 +6,7 @@ import { isUserTouching } from '../../../is_mobile'; import PrivacyDropdown from '../components/privacy_dropdown'; const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', + isModalOpen: state.get('modal').size && state.get('modal').last().modalType === 'ACTIONS', value: state.getIn(['compose', 'privacy']), }); diff --git a/app/soapbox/features/compose/containers/reply_mentions_container.js b/app/soapbox/features/compose/containers/reply_mentions_container.js index 9d2d209e0..14bc51c2c 100644 --- a/app/soapbox/features/compose/containers/reply_mentions_container.js +++ b/app/soapbox/features/compose/containers/reply_mentions_container.js @@ -38,9 +38,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = dispatch => ({ onOpenMentionsModal() { - dispatch(openModal('REPLY_MENTIONS', { - onCancel: () => dispatch(openModal('COMPOSE')), - })); + dispatch(openModal('REPLY_MENTIONS')); }, }); diff --git a/app/soapbox/features/compose/containers/upload_container.js b/app/soapbox/features/compose/containers/upload_container.js index a9829b13d..382711e8f 100644 --- a/app/soapbox/features/compose/containers/upload_container.js +++ b/app/soapbox/features/compose/containers/upload_container.js @@ -26,7 +26,7 @@ const mapDispatchToProps = dispatch => ({ }, onOpenModal: media => { - dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0, onClose: console.log })); }, onSubmit(router) { diff --git a/app/soapbox/features/ui/components/compose_modal.js b/app/soapbox/features/ui/components/compose_modal.js index 6c76ee57f..cde791dd2 100644 --- a/app/soapbox/features/ui/components/compose_modal.js +++ b/app/soapbox/features/ui/components/compose_modal.js @@ -5,10 +5,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { cancelReplyCompose } from 'soapbox/actions/compose'; +import { openModal, closeModal } from 'soapbox/actions/modal'; import IconButton from 'soapbox/components/icon_button'; -import { cancelReplyCompose } from '../../../actions/compose'; -import { openModal } from '../../../actions/modal'; import ComposeFormContainer from '../../compose/containers/compose_form_container'; const messages = defineMessages({ @@ -49,8 +49,10 @@ class ComposeModal extends ImmutablePureComponent { heading: , message: , confirm: intl.formatMessage(messages.confirm), - onConfirm: () => dispatch(cancelReplyCompose()), - onCancel: () => dispatch(openModal('COMPOSE')), + onConfirm: () => { + dispatch(closeModal('COMPOSE')); + dispatch(cancelReplyCompose()); + }, })); } else { onClose('COMPOSE'); diff --git a/app/soapbox/features/ui/components/confirmation_modal.js b/app/soapbox/features/ui/components/confirmation_modal.js index 41b22bfe0..14b22eb41 100644 --- a/app/soapbox/features/ui/components/confirmation_modal.js +++ b/app/soapbox/features/ui/components/confirmation_modal.js @@ -33,18 +33,18 @@ class ConfirmationModal extends React.PureComponent { } handleClick = () => { - this.props.onClose(); + this.props.onClose('CONFIRM'); this.props.onConfirm(); } handleSecondary = () => { - this.props.onClose(); + this.props.onClose('CONFIRM'); this.props.onSecondary(); } handleCancel = () => { const { onClose, onCancel } = this.props; - onClose(); + onClose('CONFIRM'); if (onCancel) onCancel(); } diff --git a/app/soapbox/features/ui/components/favourites_modal.js b/app/soapbox/features/ui/components/favourites_modal.js index 29c6c9e54..4df4d4da8 100644 --- a/app/soapbox/features/ui/components/favourites_modal.js +++ b/app/soapbox/features/ui/components/favourites_modal.js @@ -24,10 +24,6 @@ export default @connect(mapStateToProps) @injectIntl class FavouritesModal extends React.PureComponent { - static contextTypes = { - router: PropTypes.object, - }; - static propTypes = { onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -45,21 +41,10 @@ class FavouritesModal extends React.PureComponent { componentDidMount() { this.fetchData(); - this.unlistenHistory = this.context.router.history.listen((_, action) => { - if (action === 'PUSH') { - this.onClickClose(null, true); - } - }); } - componentWillUnmount() { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - } - - onClickClose = (_, noPop) => { - this.props.onClose('FAVOURITES', noPop); + onClickClose = () => { + this.props.onClose('FAVOURITES'); }; render() { diff --git a/app/soapbox/features/ui/components/mentions_modal.js b/app/soapbox/features/ui/components/mentions_modal.js index 178a9ca80..ebd814a86 100644 --- a/app/soapbox/features/ui/components/mentions_modal.js +++ b/app/soapbox/features/ui/components/mentions_modal.js @@ -32,10 +32,6 @@ export default @connect(mapStateToProps) @injectIntl class MentionsModal extends React.PureComponent { - static contextTypes = { - router: PropTypes.object, - }; - static propTypes = { onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -53,21 +49,10 @@ class MentionsModal extends React.PureComponent { componentDidMount() { this.fetchData(); - this.unlistenHistory = this.context.router.history.listen((_, action) => { - if (action === 'PUSH') { - this.onClickClose(null, true); - } - }); } - componentWillUnmount() { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - } - - onClickClose = (_, noPop) => { - this.props.onClose('MENTIONS', noPop); + onClickClose = () => { + this.props.onClose('MENTIONS'); }; render() { diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 5425cf5eb..4ea71ff15 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -64,7 +64,6 @@ export default class ModalRoot extends React.PureComponent { static propTypes = { type: PropTypes.string, props: PropTypes.object, - noPop: PropTypes.bool, onClose: PropTypes.func.isRequired, }; @@ -88,17 +87,17 @@ export default class ModalRoot extends React.PureComponent { return ; } - onClickClose = (_, noPop) => { + onClickClose = (_) => { const { onClose, type } = this.props; - onClose(type, noPop); + onClose(type); } render() { - const { type, props, noPop } = this.props; + const { type, props } = this.props; const visible = !!type; return ( - + {visible && ( {(SpecificComponent) => } diff --git a/app/soapbox/features/ui/components/reactions_modal.js b/app/soapbox/features/ui/components/reactions_modal.js index 2eb65644b..eabe50042 100644 --- a/app/soapbox/features/ui/components/reactions_modal.js +++ b/app/soapbox/features/ui/components/reactions_modal.js @@ -32,10 +32,6 @@ export default @connect(mapStateToProps) @injectIntl class ReactionsModal extends React.PureComponent { - static contextTypes = { - router: PropTypes.object, - }; - static propTypes = { onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -59,21 +55,10 @@ class ReactionsModal extends React.PureComponent { componentDidMount() { this.fetchData(); - this.unlistenHistory = this.context.router.history.listen((_, action) => { - if (action === 'PUSH') { - this.onClickClose(null, true); - } - }); } - componentWillUnmount() { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - } - - onClickClose = (_, noPop) => { - this.props.onClose('REACTIONS', noPop); + onClickClose = () => { + this.props.onClose('REACTIONS'); }; handleFilterChange = (reaction) => () => { diff --git a/app/soapbox/features/ui/components/reblogs_modal.js b/app/soapbox/features/ui/components/reblogs_modal.js index b50ccb794..662ec48c7 100644 --- a/app/soapbox/features/ui/components/reblogs_modal.js +++ b/app/soapbox/features/ui/components/reblogs_modal.js @@ -60,8 +60,8 @@ class ReblogsModal extends React.PureComponent { } } - onClickClose = (_, noPop) => { - this.props.onClose('REBLOGS', noPop); + onClickClose = () => { + this.props.onClose('REBLOGS'); }; render() { diff --git a/app/soapbox/features/ui/components/reply_mentions_modal.js b/app/soapbox/features/ui/components/reply_mentions_modal.js index c42062219..97db2b916 100644 --- a/app/soapbox/features/ui/components/reply_mentions_modal.js +++ b/app/soapbox/features/ui/components/reply_mentions_modal.js @@ -36,27 +36,25 @@ const makeMapStateToProps = () => { return { mentions, author: status.getIn(['account', 'id']), - to: state.getIn(['compose', 'to']), + // to: state.getIn(['compose', 'to']), isReply: true, }; }; }; -class ComposeModal extends ImmutablePureComponent { +class ReplyMentionsModal extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + mentions: ImmutablePropTypes.OrderedSet, author: PropTypes.string, intl: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, - inReplyTo: PropTypes.string, dispatch: PropTypes.func.isRequired, }; onClickClose = () => { - const { onClose, onCancel } = this.props; - onClose('COMPOSE'); - if (onCancel) onCancel(); + const { onClose } = this.props; + onClose('REPLY_MENTIONS'); }; render() { @@ -85,4 +83,4 @@ class ComposeModal extends ImmutablePureComponent { } -export default injectIntl(connect(makeMapStateToProps)(ComposeModal)); +export default injectIntl(connect(makeMapStateToProps)(ReplyMentionsModal)); diff --git a/app/soapbox/features/ui/containers/modal_container.js b/app/soapbox/features/ui/containers/modal_container.js index f37650827..4ea414696 100644 --- a/app/soapbox/features/ui/containers/modal_container.js +++ b/app/soapbox/features/ui/containers/modal_container.js @@ -4,19 +4,25 @@ import { cancelReplyCompose } from '../../../actions/compose'; import { closeModal } from '../../../actions/modal'; import ModalRoot from '../components/modal_root'; -const mapStateToProps = state => ({ - type: state.get('modal').modalType, - props: state.get('modal').modalProps, - noPop: state.get('modal').noPop, -}); +const mapStateToProps = state => { + const modal = state.get('modal').last({ + modalType: null, + modalProps: {}, + }); + + return { + type: modal.modalType, + props: modal.modalProps, + }; +}; const mapDispatchToProps = (dispatch) => ({ - onClose(optionalType, noPop) { - if (optionalType === 'COMPOSE') { + onClose(type) { + if (type === 'COMPOSE') { dispatch(cancelReplyCompose()); } - dispatch(closeModal(undefined, noPop)); + dispatch(closeModal(type)); }, }); diff --git a/app/soapbox/reducers/__tests__/modal-test.js b/app/soapbox/reducers/__tests__/modal-test.js index 4aba72b00..059d1c4d3 100644 --- a/app/soapbox/reducers/__tests__/modal-test.js +++ b/app/soapbox/reducers/__tests__/modal-test.js @@ -1,46 +1,57 @@ +import { List as ImmutableList } from 'immutable'; + import { MODAL_OPEN, MODAL_CLOSE } from 'soapbox/actions/modal'; import reducer from '../modal'; describe('modal reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual({ - modalType: null, - modalProps: {}, - noPop: false, - }); + expect(reducer(undefined, {})).toEqual(ImmutableList()); }); it('should handle MODAL_OPEN', () => { - const state = { - modalType: null, - modalProps: {}, - noPop: false, - }; + const state = ImmutableList(); const action = { type: MODAL_OPEN, modalType: 'type1', modalProps: { props1: '1' }, }; - expect(reducer(state, action)).toMatchObject({ + expect(reducer(state, action)).toMatchObject(ImmutableList([{ modalType: 'type1', modalProps: { props1: '1' }, - }); + }])); }); it('should handle MODAL_CLOSE', () => { - const state = { + const state = ImmutableList([{ modalType: 'type1', modalProps: { props1: '1' }, - }; + }]); const action = { type: MODAL_CLOSE, }; - expect(reducer(state, action)).toMatchObject({ - modalType: null, - modalProps: {}, - noPop: false, - }); + expect(reducer(state, action)).toMatchObject(ImmutableList()); + }); + + it('should handle MODAL_CLOSE with specified modalType', () => { + const state = ImmutableList([ + { + modalType: 'type1', + }, + { + modalType: 'type2', + }, + { + modalType: 'type1', + }, + ]); + const action = { + type: MODAL_CLOSE, + modalType: 'type2', + }; + expect(reducer(state, action)).toMatchObject(ImmutableList([{ + modalType: 'type1', + }])); }); }); diff --git a/app/soapbox/reducers/modal.js b/app/soapbox/reducers/modal.js index 5c0cba93d..fa6fae7f2 100644 --- a/app/soapbox/reducers/modal.js +++ b/app/soapbox/reducers/modal.js @@ -1,22 +1,24 @@ +import { List as ImmutableList } from 'immutable'; + import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; -const initialState = { - modalType: null, - modalProps: {}, - noPop: false, -}; +const initialState = ImmutableList(); export default function modal(state = initialState, action) { switch(action.type) { case MODAL_OPEN: - return { modalType: action.modalType, modalProps: action.modalProps }; + return state.push({ modalType: action.modalType, modalProps: action.modalProps }); case MODAL_CLOSE: - return { - ...(action.modalType === undefined || action.modalType === state.modalType) - ? initialState - : state, - noPop: !!action.noPop, - }; + if (state.size === 0) { + return state; + } + if (action.modalType === undefined) { + return state.pop(); + } + if (state.some(({ modalType }) => action.modalType === modalType)) { + return state.slice(0, state.findLastIndex(({ modalType }) => action.modalType === modalType)); + } + return state; default: return state; }