From 46ca470f01802818bf2bdb5c165fffa627e439c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 6 Jan 2022 14:43:58 +0100 Subject: [PATCH 1/3] Turn some pages into modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../components/status_reply_mentions.js | 31 ++++- app/soapbox/features/favourites/index.js | 72 ---------- app/soapbox/features/mentions/index.js | 96 ------------- app/soapbox/features/reactions/index.js | 128 ------------------ app/soapbox/features/reblogs/index.js | 100 -------------- .../components/status_interaction_bar.js | 76 +++++++++-- .../ui/components/favourites_modal.js | 90 ++++++++++++ .../features/ui/components/mentions_modal.js | 94 +++++++++++++ .../features/ui/components/modal_root.js | 8 ++ .../features/ui/components/reactions_modal.js | 119 ++++++++++++++++ .../features/ui/components/reblogs_modal.js | 92 +++++++++++++ app/soapbox/features/ui/index.js | 8 -- .../features/ui/util/async-components.js | 32 ++--- app/styles/components/modal.scss | 15 +- 14 files changed, 529 insertions(+), 432 deletions(-) delete mode 100644 app/soapbox/features/favourites/index.js delete mode 100644 app/soapbox/features/mentions/index.js delete mode 100644 app/soapbox/features/reactions/index.js delete mode 100644 app/soapbox/features/reblogs/index.js create mode 100644 app/soapbox/features/ui/components/favourites_modal.js create mode 100644 app/soapbox/features/ui/components/mentions_modal.js create mode 100644 app/soapbox/features/ui/components/reactions_modal.js create mode 100644 app/soapbox/features/ui/components/reblogs_modal.js diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js index 580880fee..1bb43562d 100644 --- a/app/soapbox/components/status_reply_mentions.js +++ b/app/soapbox/components/status_reply_mentions.js @@ -1,15 +1,35 @@ import React from 'react'; +import { connect } from 'react-redux'; import { FormattedMessage, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import { openModal } from 'soapbox/actions/modal'; -export default @injectIntl +const mapDispatchToProps = (dispatch) => ({ + onOpenMentionsModal(username, statusId) { + dispatch(openModal('MENTIONS', { + username, + statusId, + })); + }, +}); + +export default @connect(null, mapDispatchToProps) +@injectIntl class StatusReplyMentions extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + onOpenMentionsModal: PropTypes.func, + } + + handleOpenMentionsModal = () => { + const { status, onOpenMentionsModal } = this.props; + + onOpenMentionsModal(status.getIn(['account', 'acct']), status.get('id')); } render() { @@ -54,6 +74,7 @@ class StatusReplyMentions extends ImmutablePureComponent { } } + // The typical case with a reply-to and a list of mentions. return (
@@ -68,9 +89,13 @@ class StatusReplyMentions extends ImmutablePureComponent { {' '} )), more: to.size > 2 && ( - + + - + ), }} /> diff --git a/app/soapbox/features/favourites/index.js b/app/soapbox/features/favourites/index.js deleted file mode 100644 index 7bf1852d9..000000000 --- a/app/soapbox/features/favourites/index.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import { fetchFavourites } from '../../actions/interactions'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ScrollableList from '../../components/scrollable_list'; - -const messages = defineMessages({ - heading: { id: 'column.favourites', defaultMessage: 'Likes' }, -}); - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), -}); - -export default @connect(mapStateToProps) -@injectIntl -class Favourites extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - }; - - componentDidMount() { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); - } - - componentDidUpdate(prevProps) { - const { statusId } = this.props.params; - const { prevStatusId } = prevProps.params; - - if (statusId !== prevStatusId && statusId) { - this.props.dispatch(fetchFavourites(statusId)); - } - } - - render() { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - - - - ); - } - - const emptyMessage = ; - - return ( - - - {accountIds.map(id => - , - )} - - - ); - } - -} diff --git a/app/soapbox/features/mentions/index.js b/app/soapbox/features/mentions/index.js deleted file mode 100644 index d77e7b8f2..000000000 --- a/app/soapbox/features/mentions/index.js +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import MissingIndicator from '../../components/missing_indicator'; -import { fetchStatus } from '../../actions/statuses'; -import { injectIntl, defineMessages } from 'react-intl'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ScrollableList from '../../components/scrollable_list'; -import { makeGetStatus } from '../../selectors'; - -const messages = defineMessages({ - heading: { id: 'column.mentions', defaultMessage: 'Mentions' }, -}); - -const mapStateToProps = (state, props) => { - const getStatus = makeGetStatus(); - const status = getStatus(state, { - id: props.params.statusId, - username: props.params.username, - }); - - return { - status, - accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class Mentions extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - status: ImmutablePropTypes.map, - }; - - fetchData = () => { - const { dispatch, params } = this.props; - const { statusId } = params; - - dispatch(fetchStatus(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - componentDidUpdate(prevProps) { - const { params } = this.props; - - if (params.statusId !== prevProps.params.statusId) { - this.fetchData(); - } - } - - render() { - const { intl, accountIds, status } = this.props; - - if (!accountIds) { - return ( - - - - ); - } - - if (!status) { - return ( - - - - ); - } - - return ( - - - {accountIds.map(id => - , - )} - - - ); - } - -} diff --git a/app/soapbox/features/reactions/index.js b/app/soapbox/features/reactions/index.js deleted file mode 100644 index 5750c3053..000000000 --- a/app/soapbox/features/reactions/index.js +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import LoadingIndicator from '../../components/loading_indicator'; -import MissingIndicator from '../../components/missing_indicator'; -import { fetchFavourites, fetchReactions } from '../../actions/interactions'; -import { fetchStatus } from '../../actions/statuses'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ScrollableList from '../../components/scrollable_list'; -import { makeGetStatus } from '../../selectors'; - -const messages = defineMessages({ - heading: { id: 'column.reactions', defaultMessage: 'Reactions' }, -}); - -const mapStateToProps = (state, props) => { - const getStatus = makeGetStatus(); - const status = getStatus(state, { - id: props.params.statusId, - username: props.params.username, - }); - - const favourites = state.getIn(['user_lists', 'favourited_by', props.params.statusId]); - const reactions = state.getIn(['user_lists', 'reactions', props.params.statusId]); - const allReactions = favourites && reactions && ImmutableOrderedSet(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).union(reactions || []); - - return { - status, - reactions: allReactions, - accounts: allReactions && (props.params.reaction - ? allReactions.find(reaction => reaction.name === props.params.reaction).accounts.map(account => ({ id: account, reaction: props.params.reaction })) - : allReactions.map(reaction => reaction.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten()), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class Reactions extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - reactions: ImmutablePropTypes.orderedSet, - accounts: ImmutablePropTypes.orderedSet, - status: ImmutablePropTypes.map, - }; - - fetchData = () => { - const { dispatch, params } = this.props; - const { statusId } = params; - - dispatch(fetchFavourites(statusId)); - dispatch(fetchReactions(statusId)); - dispatch(fetchStatus(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - componentDidUpdate(prevProps) { - const { params } = this.props; - - if (params.statusId !== prevProps.params.statusId) { - this.fetchData(); - } - } - - handleFilterChange = (reaction) => () => { - const { params } = this.props; - const { username, statusId } = params; - - this.context.router.history.replace(`/@${username}/posts/${statusId}/reactions/${reaction}`); - }; - - render() { - const { intl, params, reactions, accounts, status } = this.props; - - if (!accounts) { - return ( - - - - ); - } - - if (!status) { - return ( - - - - ); - } - - const emptyMessage = ; - - return ( - - { - reactions.size > 0 && ( -
- - {reactions?.filter(reaction => reaction.count).map(reaction => )} -
- ) - } - - {accounts.map((account) => - , - )} - -
- ); - } - -} diff --git a/app/soapbox/features/reblogs/index.js b/app/soapbox/features/reblogs/index.js deleted file mode 100644 index ea536940c..000000000 --- a/app/soapbox/features/reblogs/index.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from '../../components/loading_indicator'; -import MissingIndicator from '../../components/missing_indicator'; -import { fetchReblogs } from '../../actions/interactions'; -import { fetchStatus } from '../../actions/statuses'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import AccountContainer from '../../containers/account_container'; -import Column from '../ui/components/column'; -import ScrollableList from '../../components/scrollable_list'; -import { makeGetStatus } from '../../selectors'; - -const messages = defineMessages({ - heading: { id: 'column.reblogs', defaultMessage: 'Reposts' }, -}); - -const mapStateToProps = (state, props) => { - const getStatus = makeGetStatus(); - const status = getStatus(state, { - id: props.params.statusId, - username: props.params.username, - }); - - return { - status, - accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class Reblogs extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - status: ImmutablePropTypes.map, - }; - - fetchData = () => { - const { dispatch, params } = this.props; - const { statusId } = params; - - dispatch(fetchReblogs(statusId)); - dispatch(fetchStatus(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - componentDidUpdate(prevProps) { - const { params } = this.props; - - if (params.statusId !== prevProps.params.statusId) { - this.fetchData(); - } - } - - render() { - const { intl, accountIds, status } = this.props; - - if (!accountIds) { - return ( - - - - ); - } - - if (!status) { - return ( - - - - ); - } - - const emptyMessage = ; - - return ( - - - {accountIds.map(id => - , - )} - - - ); - } - -} diff --git a/app/soapbox/features/status/components/status_interaction_bar.js b/app/soapbox/features/status/components/status_interaction_bar.js index b75769d66..e7f2b0f05 100644 --- a/app/soapbox/features/status/components/status_interaction_bar.js +++ b/app/soapbox/features/status/components/status_interaction_bar.js @@ -8,9 +8,9 @@ import emojify from 'soapbox/features/emoji/emoji'; import { reduceEmoji } from 'soapbox/utils/emoji_reacts'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; import { getFeatures } from 'soapbox/utils/features'; -import { Link } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; +import { openModal } from 'soapbox/actions/modal'; const mapStateToProps = state => { const instance = state.get('instance'); @@ -21,7 +21,29 @@ const mapStateToProps = state => { }; }; -export default @connect(mapStateToProps) +const mapDispatchToProps = (dispatch) => ({ + onOpenReblogsModal(username, statusId) { + dispatch(openModal('REBLOGS', { + username, + statusId, + })); + }, + onOpenFavouritesModal(username, statusId) { + dispatch(openModal('FAVOURITES', { + username, + statusId, + })); + }, + onOpenReactionsModal(username, statusId, reaction) { + dispatch(openModal('REACTIONS', { + username, + statusId, + reaction, + })); + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) class StatusInteractionBar extends ImmutablePureComponent { static propTypes = { @@ -29,6 +51,8 @@ class StatusInteractionBar extends ImmutablePureComponent { me: SoapboxPropTypes.me, allowedEmoji: ImmutablePropTypes.list, features: PropTypes.object.isRequired, + onOpenReblogsModal: PropTypes.func, + onOpenReactionsModal: PropTypes.func, } getNormalizedReacts = () => { @@ -41,22 +65,39 @@ class StatusInteractionBar extends ImmutablePureComponent { ).reverse(); } + handleOpenReblogsModal = () => { + const { status, onOpenReblogsModal } = this.props; + + onOpenReblogsModal(status.getIn(['account', 'acct']), status.get('id')); + } + getReposts = () => { const { status } = this.props; + if (status.get('reblogs_count')) { return ( - + - + ); } return ''; } + handleOpenFavouritesModal = () => { + const { status, onOpenFavouritesModal } = this.props; + + onOpenFavouritesModal(status.getIn(['account', 'acct']), status.get('id')); + } + getFavourites = () => { const { features, status } = this.props; @@ -72,9 +113,13 @@ class StatusInteractionBar extends ImmutablePureComponent { if (features.exposableReactions) { return ( - + {favourites} - + ); } else { return ( @@ -88,8 +133,14 @@ class StatusInteractionBar extends ImmutablePureComponent { return ''; } + handleOpenReactionsModal = (reaction) => () => { + const { status, onOpenReactionsModal } = this.props; + + onOpenReactionsModal(status.getIn(['account', 'acct']), status.get('id'), reaction.get('name')); + } + getEmojiReacts = () => { - const { status, features } = this.props; + const { features } = this.props; const emojiReacts = this.getNormalizedReacts(); const count = emojiReacts.reduce((acc, cur) => ( @@ -112,7 +163,16 @@ class StatusInteractionBar extends ImmutablePureComponent { ); if (features.exposableReactions) { - return {emojiReact}; + return ( + + {emojiReact} + + ); } return {emojiReact}; diff --git a/app/soapbox/features/ui/components/favourites_modal.js b/app/soapbox/features/ui/components/favourites_modal.js new file mode 100644 index 000000000..f0cf51965 --- /dev/null +++ b/app/soapbox/features/ui/components/favourites_modal.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; +import IconButton from 'soapbox/components/icon_button'; +import LoadingIndicator from 'soapbox/components/loading_indicator'; +import AccountContainer from 'soapbox/containers/account_container'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { fetchFavourites } from 'soapbox/actions/interactions'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +const mapStateToProps = (state, props) => { + return { + accountIds: state.getIn(['user_lists', 'favourited_by', props.statusId]), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class FavouritesModal extends React.PureComponent { + + static propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + statusId: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.orderedSet, + }; + + fetchData = () => { + const { dispatch, statusId } = this.props; + + dispatch(fetchFavourites(statusId)); + } + + componentDidMount() { + this.fetchData(); + } + + onClickClose = () => { + this.props.onClose('FAVOURITES'); + }; + + render() { + const { intl, accountIds } = this.props; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map(id => + , + )} + + ); + } + + + return ( +
+
+

+ +

+ +
+ {body} +
+ ); + } + +} diff --git a/app/soapbox/features/ui/components/mentions_modal.js b/app/soapbox/features/ui/components/mentions_modal.js new file mode 100644 index 000000000..b1554c2d2 --- /dev/null +++ b/app/soapbox/features/ui/components/mentions_modal.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import IconButton from 'soapbox/components/icon_button'; +import LoadingIndicator from 'soapbox/components/loading_indicator'; +import AccountContainer from 'soapbox/containers/account_container'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { makeGetStatus } from 'soapbox/selectors'; +import { fetchStatus } from 'soapbox/actions/statuses'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +const mapStateToProps = (state, props) => { + const getStatus = makeGetStatus(); + const status = getStatus(state, { + id: props.statusId, + username: props.username, + }); + + return { + accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null, + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class MentionsModal extends React.PureComponent { + + static propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + statusId: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.orderedSet, + }; + + fetchData = () => { + const { dispatch, statusId } = this.props; + + dispatch(fetchStatus(statusId)); + } + + componentDidMount() { + this.fetchData(); + } + + onClickClose = () => { + this.props.onClose('MENTIONS'); + }; + + render() { + const { intl, accountIds } = this.props; + + let body; + + if (!accountIds) { + body = ; + } else { + body = ( + + {accountIds.map(id => + , + )} + + ); + } + + return ( +
+
+

+ +

+ +
+ {body} +
+ ); + } + +} diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index dd5d4970b..52f94871d 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -25,6 +25,10 @@ import { UnauthorizedModal, EditFederationModal, ComponentModal, + ReactionsModal, + FavouritesModal, + ReblogsModal, + MentionsModal, } from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { @@ -47,6 +51,10 @@ const MODAL_COMPONENTS = { 'CRYPTO_DONATE': CryptoDonateModal, 'EDIT_FEDERATION': EditFederationModal, 'COMPONENT': ComponentModal, + 'REBLOGS': ReblogsModal, + 'FAVOURITES': FavouritesModal, + 'REACTIONS': ReactionsModal, + 'MENTIONS': MentionsModal, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/reactions_modal.js b/app/soapbox/features/ui/components/reactions_modal.js new file mode 100644 index 000000000..8b905ac25 --- /dev/null +++ b/app/soapbox/features/ui/components/reactions_modal.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { List as ImmutableList } from 'immutable'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; +import IconButton from 'soapbox/components/icon_button'; +import LoadingIndicator from 'soapbox/components/loading_indicator'; +import AccountContainer from 'soapbox/containers/account_container'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +const mapStateToProps = (state, props) => { + + const favourites = state.getIn(['user_lists', 'favourited_by', props.statusId]); + const reactions = state.getIn(['user_lists', 'reactions', props.statusId]); + const allReactions = favourites && reactions && ImmutableList(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).concat(reactions || []); + + return { + reactions: allReactions, + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class ReactionsModal extends React.PureComponent { + + static propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + statusId: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + reaction: PropTypes.string, + dispatch: PropTypes.func.isRequired, + reactions: ImmutablePropTypes.list, + }; + + state = { + reaction: this.props.reaction, + } + + fetchData = () => { + const { dispatch, statusId } = this.props; + + dispatch(fetchFavourites(statusId)); + dispatch(fetchReactions(statusId)); + } + + componentDidMount() { + this.fetchData(); + } + + onClickClose = () => { + this.props.onClose('REACTIONS'); + }; + + handleFilterChange = (reaction) => () => { + this.setState({ reaction }); + }; + + render() { + const { intl, reactions } = this.props; + const { reaction } = this.state; + + const accounts = reactions && (reaction + ? reactions.find(reaction => reaction.name === this.state.reaction).accounts.map(account => ({ id: account, reaction: this.state.reaction })) + : reactions.map(reaction => reaction.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten()); + + let body; + + if (!accounts) { + body = ; + } else { + const emptyMessage = ; + + body = (<> + { + reactions.size > 0 && ( +
+ + {reactions?.filter(reaction => reaction.count).map(reaction => )} +
+ ) + } + + {accounts.map((account) => + , + )} + + ); + } + + + return ( +
+
+

+ +

+ +
+ {body} +
+ ); + } + +} diff --git a/app/soapbox/features/ui/components/reblogs_modal.js b/app/soapbox/features/ui/components/reblogs_modal.js new file mode 100644 index 000000000..c6d4811f5 --- /dev/null +++ b/app/soapbox/features/ui/components/reblogs_modal.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; +import IconButton from 'soapbox/components/icon_button'; +import LoadingIndicator from 'soapbox/components/loading_indicator'; +import AccountContainer from 'soapbox/containers/account_container'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { fetchReblogs } from 'soapbox/actions/interactions'; +import { fetchStatus } from 'soapbox/actions/statuses'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +const mapStateToProps = (state, props) => { + return { + accountIds: state.getIn(['user_lists', 'reblogged_by', props.statusId]), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class ReblogsModal extends React.PureComponent { + + static propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + statusId: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.orderedSet, + }; + + fetchData = () => { + const { dispatch, statusId } = this.props; + + dispatch(fetchReblogs(statusId)); + dispatch(fetchStatus(statusId)); + } + + componentDidMount() { + this.fetchData(); + } + + onClickClose = () => { + this.props.onClose('REBLOGS'); + }; + + render() { + const { intl, accountIds } = this.props; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map(id => + , + )} + + ); + } + + + return ( +
+
+

+ +

+ +
+ {body} +
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 2de0dcbfb..9f13aedf4 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -56,10 +56,6 @@ import { HomeTimeline, Followers, Following, - Reblogs, - Reactions, - Mentions, - Favourites, DirectTimeline, Conversations, HashtagTimeline, @@ -298,10 +294,6 @@ class SwitchingColumnsArea extends React.PureComponent { - - - - diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 69dd8aff6..c80b53ee9 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -94,22 +94,6 @@ export function Following() { return import(/* webpackChunkName: "features/following" */'../../following'); } -export function Reblogs() { - return import(/* webpackChunkName: "features/reblogs" */'../../reblogs'); -} - -export function Reactions() { - return import(/* webpackChunkName: "features/reactions" */'../../reactions'); -} - -export function Mentions() { - return import(/* webpackChunkName: "features/mentions" */'../../mentions'); -} - -export function Favourites() { - return import(/* webpackChunkName: "features/favourites" */'../../favourites'); -} - export function FollowRequests() { return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); } @@ -214,6 +198,22 @@ export function ComponentModal() { return import(/* webpackChunkName: "features/ui" */'../components/component_modal'); } +export function ReblogsModal() { + return import(/* webpackChunkName: "features/ui" */'../components/reblogs_modal'); +} + +export function FavouritesModal() { + return import(/* webpackChunkName: "features/ui" */'../components/favourites_modal'); +} + +export function ReactionsModal() { + return import(/* webpackChunkName: "features/ui" */'../components/reactions_modal'); +} + +export function MentionsModal() { + return import(/* webpackChunkName: "features/ui" */'../components/mentions_modal'); +} + export function ListEditor() { return import(/* webpackChunkName: "features/list_editor" */'../../list_editor'); } diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index b39da7545..4c1a61598 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -339,7 +339,10 @@ .confirmation-modal, .report-modal, .actions-modal, -.mute-modal { +.mute-modal, +.reactions-modal, +.reblogs-modal, +.mentions-modal { position: relative; flex-direction: column; overflow: hidden; @@ -385,6 +388,16 @@ } } +.reactions-modal, +.reblogs-modal, +.mentions-modal { + height: 80vh; + + .slist { + overflow: auto; + } +} + .boost-modal__container { overflow-x: scroll; padding: 10px; From 2d3784eb889ac3979ddde3b05400fd00b07b5d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 6 Jan 2022 17:45:10 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Minor=20changes,=20port=20'Generalize=20?= =?UTF-8?q?=E2=80=9Cpress=20back=20to=20close=20modal=E2=80=9D=20to=20any?= =?UTF-8?q?=20modal=20and=20to=20public=20pages'=20from=20Mastodon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/modal_root.js | 37 +++++++++++++++++++ .../components/status_reply_mentions.js | 8 +--- app/soapbox/containers/soapbox.js | 7 +--- .../components/status_interaction_bar.js | 21 ++++++----- .../features/ui/components/media_modal.js | 24 ------------ .../features/ui/components/video_modal.js | 28 -------------- app/styles/components/emoji-reacts.scss | 4 ++ app/styles/components/reply-mentions.scss | 4 ++ package.json | 1 + yarn.lock | 2 +- 10 files changed, 63 insertions(+), 73 deletions(-) diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index a8e8fe746..1ee69a7de 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import 'wicg-inert'; +import { createBrowserHistory } from 'history'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from '../actions/modal'; @@ -43,6 +44,7 @@ class ModalRoot extends React.PureComponent { intl: PropTypes.object.isRequired, hasComposeContent: PropTypes.bool, type: PropTypes.string, + onCancel: PropTypes.func, }; state = { @@ -100,12 +102,15 @@ class ModalRoot extends React.PureComponent { componentDidMount() { window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keydown', this.handleKeyDown, false); + this.history = this.context.router ? this.context.router.history : createBrowserHistory(); } componentDidUpdate(prevProps) { if (!!this.props.children && !prevProps.children) { this.activeElement = document.activeElement; this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); + + this._handleModalOpen(); } else if (!prevProps.children) { this.setState({ revealed: false }); } @@ -114,12 +119,16 @@ class ModalRoot extends React.PureComponent { this.activeElement.focus(); this.activeElement = null; this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + + this._handleModalClose(); } if (this.props.children) { requestAnimationFrame(() => { this.setState({ revealed: true }); }); + + this._ensureHistoryBuffer(); } } @@ -128,6 +137,34 @@ class ModalRoot extends React.PureComponent { window.removeEventListener('keydown', this.handleKeyDown); } + _handleModalOpen() { + this._modalHistoryKey = Date.now(); + this.unlistenHistory = this.history.listen((_, action) => { + if (action === 'POP') { + this.handleOnClose(); + + if (this.props.onCancel) this.props.onCancel(); + } + }); + } + + _handleModalClose() { + if (this.unlistenHistory) { + this.unlistenHistory(); + } + const { state } = this.history.location; + if (state && state.soapboxModalKey === this._modalHistoryKey) { + this.history.goBack(); + } + } + + _ensureHistoryBuffer() { + const { pathname, state } = this.history.location; + if (!state || state.soapboxModalKey !== this._modalHistoryKey) { + this.history.push(pathname, { ...state, soapboxModalKey: this._modalHistoryKey }); + } + } + getSiblings = () => { return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); } diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js index 1bb43562d..7dd68e942 100644 --- a/app/soapbox/components/status_reply_mentions.js +++ b/app/soapbox/components/status_reply_mentions.js @@ -89,13 +89,9 @@ class StatusReplyMentions extends ImmutablePureComponent { {' '} )), more: to.size > 2 && ( - - + - + ), }} /> diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index a4e48c587..9eb9640e6 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -29,9 +29,6 @@ import { createGlobals } from 'soapbox/globals'; const validLocale = locale => Object.keys(messages).includes(locale); -const previewMediaState = 'previewMediaModal'; -const previewVideoState = 'previewVideoModal'; - export const store = configureStore(); // Configure global functions for developers @@ -117,8 +114,8 @@ class SoapboxMount extends React.PureComponent { this.maybeUpdateMessages(prevProps); } - shouldUpdateScroll(_, { location }) { - return location.state !== previewMediaState && location.state !== previewVideoState; + shouldUpdateScroll(prevRouterProps, { location }) { + return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey); } render() { diff --git a/app/soapbox/features/status/components/status_interaction_bar.js b/app/soapbox/features/status/components/status_interaction_bar.js index e7f2b0f05..16648a532 100644 --- a/app/soapbox/features/status/components/status_interaction_bar.js +++ b/app/soapbox/features/status/components/status_interaction_bar.js @@ -76,16 +76,17 @@ class StatusInteractionBar extends ImmutablePureComponent { if (status.get('reblogs_count')) { return ( - - + ); } @@ -113,13 +114,14 @@ class StatusInteractionBar extends ImmutablePureComponent { if (features.exposableReactions) { return ( - {favourites} - + ); } else { return ( @@ -164,14 +166,15 @@ class StatusInteractionBar extends ImmutablePureComponent { if (features.exposableReactions) { return ( - {emojiReact} - + ); } diff --git a/app/soapbox/features/ui/components/media_modal.js b/app/soapbox/features/ui/components/media_modal.js index ce002d1c4..603681161 100644 --- a/app/soapbox/features/ui/components/media_modal.js +++ b/app/soapbox/features/ui/components/media_modal.js @@ -18,8 +18,6 @@ const messages = defineMessages({ next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); -export const previewState = 'previewMediaModal'; - export default @injectIntl class MediaModal extends ImmutablePureComponent { @@ -32,10 +30,6 @@ class MediaModal extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - static contextTypes = { - router: PropTypes.object, - }; - state = { index: null, navigationHidden: false, @@ -75,28 +69,10 @@ class MediaModal extends ImmutablePureComponent { componentDidMount() { window.addEventListener('keydown', this.handleKeyDown, false); - - if (this.context.router) { - const history = this.context.router.history; - - history.push(history.location.pathname, previewState); - - this.unlistenHistory = history.listen(() => { - this.props.onClose(); - }); - } } componentWillUnmount() { window.removeEventListener('keydown', this.handleKeyDown); - - if (this.context.router) { - this.unlistenHistory(); - - if (this.context.router.history.location.state === previewState) { - this.context.router.history.goBack(); - } - } } getIndex() { diff --git a/app/soapbox/features/ui/components/video_modal.js b/app/soapbox/features/ui/components/video_modal.js index 20bedb48a..086d843b9 100644 --- a/app/soapbox/features/ui/components/video_modal.js +++ b/app/soapbox/features/ui/components/video_modal.js @@ -5,8 +5,6 @@ import Video from 'soapbox/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -export const previewState = 'previewVideoModal'; - export default class VideoModal extends ImmutablePureComponent { static propTypes = { @@ -17,32 +15,6 @@ export default class VideoModal extends ImmutablePureComponent { onClose: PropTypes.func.isRequired, }; - static contextTypes = { - router: PropTypes.object, - }; - - componentDidMount() { - if (this.context.router) { - const history = this.context.router.history; - - history.push(history.location.pathname, previewState); - - this.unlistenHistory = history.listen(() => { - this.props.onClose(); - }); - } - } - - componentWillUnmount() { - if (this.context.router) { - this.unlistenHistory(); - - if (this.context.router.history.location.state === previewState) { - this.context.router.history.goBack(); - } - } - } - handleStatusClick = e => { const { status, account } = this.props; if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { diff --git a/app/styles/components/emoji-reacts.scss b/app/styles/components/emoji-reacts.scss index 9af8b33ca..353ab043e 100644 --- a/app/styles/components/emoji-reacts.scss +++ b/app/styles/components/emoji-reacts.scss @@ -19,6 +19,10 @@ + .emoji-react { margin-right: -8px; } + + &[type='button'] { + cursor: pointer; + } } .emoji-react--reblogs, diff --git a/app/styles/components/reply-mentions.scss b/app/styles/components/reply-mentions.scss index 444852026..9eebb19d0 100644 --- a/app/styles/components/reply-mentions.scss +++ b/app/styles/components/reply-mentions.scss @@ -20,5 +20,9 @@ .reply-mentions { display: block; margin: 4px 0 0 0; + + span { + cursor: pointer; + } } } diff --git a/package.json b/package.json index 3c3ce850d..cfc61ef3b 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "escape-html": "^1.0.3", "exif-js": "^2.3.0", "feather-icons": "^4.28.0", + "history": "^4.10.1", "html-webpack-harddisk-plugin": "^2.0.0", "html-webpack-plugin": "^5.3.2", "http-link-header": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index d867807f6..c9338896c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4588,7 +4588,7 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -history@^4.7.2: +history@^4.10.1, history@^4.7.2: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== From f28a6c5256b28d3fe18010ace0a3b74db2ce693a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 6 Jan 2022 17:52:43 +0100 Subject: [PATCH 3/3] Add max-height to the new modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/styles/components/modal.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 4c1a61598..8c086cefc 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -392,6 +392,7 @@ .reblogs-modal, .mentions-modal { height: 80vh; + max-height: 650px; .slist { overflow: auto;