From 853a67ed164b4975f8ee9370aa521849eebef47c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 19 Nov 2019 21:24:16 +0100 Subject: [PATCH] Add relationship-based options to status dropdowns (#12377) Move bookmark action in inline statuses from action bar to dropdown --- .../mastodon/components/status_action_bar.js | 91 ++++++++++++++++--- .../containers/dropdown_menu_container.js | 4 + .../mastodon/containers/status_container.js | 32 ++++++- .../features/status/components/action_bar.js | 90 +++++++++++++++--- .../mastodon/features/status/index.js | 36 +++++++- 5 files changed, 229 insertions(+), 24 deletions(-) diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 4fa2c1158..bd3bb16bb 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -1,5 +1,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import IconButton from './icon_button'; import DropdownMenuContainer from '../containers/dropdown_menu_container'; @@ -24,6 +25,7 @@ const messages = defineMessages({ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, @@ -34,6 +36,10 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); const obfuscatedCount = count => { @@ -46,7 +52,12 @@ const obfuscatedCount = count => { } }; -export default @injectIntl +const mapStateToProps = (state, { status }) => ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), +}); + +export default @connect(mapStateToProps) +@injectIntl class StatusActionBar extends ImmutablePureComponent { static contextTypes = { @@ -55,6 +66,7 @@ class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + relationship: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -62,7 +74,11 @@ class StatusActionBar extends ImmutablePureComponent { onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, + onUnmute: PropTypes.func, onBlock: PropTypes.func, + onUnblock: PropTypes.func, + onBlockDomain: PropTypes.func, + onUnblockDomain: PropTypes.func, onReport: PropTypes.func, onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, @@ -76,6 +92,7 @@ class StatusActionBar extends ImmutablePureComponent { // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ 'status', + 'relationship', 'withDismiss', ] @@ -141,11 +158,39 @@ class StatusActionBar extends ImmutablePureComponent { } handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); + const { status, relationship, onMute, onUnmute } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('muting')) { + onUnmute(account); + } else { + onMute(account); + } } handleBlockClick = () => { - this.props.onBlock(this.props.status); + const { status, relationship, onBlock, onUnblock } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('blocking')) { + onBlock(status); + } else { + onUnblock(account); + } + } + + handleBlockDomain = () => { + const { status, onBlockDomain } = this.props; + const account = status.get('account'); + + onBlockDomain(account.get('acct').split('@')[1]); + } + + handleUnblockDomain = () => { + const { status, onUnblockDomain } = this.props; + const account = status.get('account'); + + onUnblockDomain(account.get('acct').split('@')[1]); } handleOpen = () => { @@ -184,11 +229,12 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss } = this.props; + const { status, relationship, intl, withDismiss } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const account = status.get('account'); let menu = []; let reblogIcon = 'retweet'; @@ -202,6 +248,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); menu.push(null); if (status.getIn(['account', 'id']) === me || withDismiss) { @@ -221,16 +268,39 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + + if (relationship && relationship.get('muting')) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); + } + + if (relationship && relationship.get('blocking')) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); + } + + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport }); + + if (account.get('acct') !== account.get('username')) { + const domain = account.get('acct').split('@')[1]; + + menu.push(null); + + if (relationship && relationship.get('domain_blocking')) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); + } + } if (isStaff) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); } } @@ -259,7 +329,6 @@ class StatusActionBar extends ImmutablePureComponent { {shareButton} -
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index f79b19202..1427f8528 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -1,4 +1,5 @@ import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu'; +import { fetchRelationships } from 'mastodon/actions/accounts'; import { openModal, closeModal } from '../actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from '../components/dropdown_menu'; @@ -13,12 +14,15 @@ const mapStateToProps = state => ({ const mapDispatchToProps = (dispatch, { status, items }) => ({ onOpen(id, onItemClick, dropdownPlacement, keyboard) { + dispatch(fetchRelationships([status.getIn(['account', 'id'])])); + dispatch(isUserTouching() ? openModal('ACTIONS', { status, actions: items, onClick: onItemClick, }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, + onClose(id) { dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 16ba02e12..35c16a20c 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -1,3 +1,4 @@ +import React from 'react'; import { connect } from 'react-redux'; import Status from '../components/status'; import { makeGetStatus } from '../selectors'; @@ -23,11 +24,19 @@ import { hideStatus, revealStatus, } from '../actions/statuses'; +import { + unmuteAccount, + unblockAccount, +} from '../actions/accounts'; +import { + blockDomain, + unblockDomain, +} from '../actions/domain_blocks'; import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, deleteModal } from '../initial_state'; import { showAlertForError } from '../actions/alerts'; @@ -38,6 +47,7 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); const makeMapStateToProps = () => { @@ -148,6 +158,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(initBlockModal(account)); }, + onUnblock (account) { + dispatch(unblockAccount(account.get('id'))); + }, + onReport (status) { dispatch(initReport(status.get('account'), status)); }, @@ -156,6 +170,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(initMuteModal(account)); }, + onUnmute (account) { + dispatch(unmuteAccount(account.get('id'))); + }, + onMuteConversation (status) { if (status.get('muted')) { dispatch(unmuteStatus(status.get('id'))); @@ -172,6 +190,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onBlockDomain (domain) { + dispatch(openModal('CONFIRM', { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + })); + }, + + onUnblockDomain (domain) { + dispatch(unblockDomain(domain)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 1b81cd245..76334de69 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; @@ -30,9 +31,18 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); -export default @injectIntl +const mapStateToProps = (state, { status }) => ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), +}); + +export default @connect(mapStateToProps) +@injectIntl class ActionBar extends React.PureComponent { static contextTypes = { @@ -41,6 +51,7 @@ class ActionBar extends React.PureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + relationship: ImmutablePropTypes.map, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, @@ -49,8 +60,12 @@ class ActionBar extends React.PureComponent { onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onMute: PropTypes.func, - onMuteConversation: PropTypes.func, + onUnmute: PropTypes.func, onBlock: PropTypes.func, + onUnblock: PropTypes.func, + onBlockDomain: PropTypes.func, + onUnblockDomain: PropTypes.func, + onMuteConversation: PropTypes.func, onReport: PropTypes.func, onPin: PropTypes.func, onEmbed: PropTypes.func, @@ -90,17 +105,45 @@ class ActionBar extends React.PureComponent { } handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); + const { status, relationship, onMute, onUnmute } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('muting')) { + onUnmute(account); + } else { + onMute(account); + } + } + + handleBlockClick = () => { + const { status, relationship, onBlock, onUnblock } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('blocking')) { + onBlock(status); + } else { + onUnblock(account); + } + } + + handleBlockDomain = () => { + const { status, onBlockDomain } = this.props; + const account = status.get('account'); + + onBlockDomain(account.get('acct').split('@')[1]); + } + + handleUnblockDomain = () => { + const { status, onUnblockDomain } = this.props; + const account = status.get('account'); + + onUnblockDomain(account.get('acct').split('@')[1]); } handleConversationMuteClick = () => { this.props.onMuteConversation(this.props.status); } - handleBlockClick = () => { - this.props.onBlock(this.props.status); - } - handleReport = () => { this.props.onReport(this.props.status); } @@ -140,10 +183,11 @@ class ActionBar extends React.PureComponent { } render () { - const { status, intl } = this.props; + const { status, relationship, intl } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); + const account = status.get('account'); let menu = []; @@ -171,9 +215,33 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); + + if (relationship && relationship.get('muting')) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); + } + + if (relationship && relationship.get('blocking')) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); + } + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + + if (account.get('acct') !== account.get('username')) { + const domain = account.get('acct').split('@')[1]; + + menu.push(null); + + if (relationship && relationship.get('domain_blocking')) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); + } + } + if (isStaff) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); @@ -207,7 +275,7 @@ class ActionBar extends React.PureComponent {
- +
); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 9fb3fe305..55bd99886 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -32,6 +32,14 @@ import { hideStatus, revealStatus, } from '../../actions/statuses'; +import { + unblockAccount, + unmuteAccount, +} from '../../actions/accounts'; +import { + blockDomain, + unblockDomain, +} from '../../actions/domain_blocks'; import { initMuteModal } from '../../actions/mutes'; import { initBlockModal } from '../../actions/blocks'; import { initReport } from '../../actions/reports'; @@ -41,7 +49,7 @@ import ColumnBackButton from '../../components/column_back_button'; import ColumnHeader from '../../components/column_header'; import StatusContainer from '../../containers/status_container'; import { openModal } from '../../actions/modal'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import { boostModal, deleteModal } from '../../initial_state'; @@ -59,6 +67,7 @@ const messages = defineMessages({ detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); const makeMapStateToProps = () => { @@ -317,6 +326,27 @@ class Status extends ImmutablePureComponent { this.props.dispatch(openModal('EMBED', { url: status.get('url') })); } + handleUnmuteClick = account => { + this.props.dispatch(unmuteAccount(account.get('id'))); + } + + handleUnblockClick = account => { + this.props.dispatch(unblockAccount(account.get('id'))); + } + + handleBlockDomainClick = domain => { + this.props.dispatch(openModal('CONFIRM', { + message: {domain} }} />, + confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => this.props.dispatch(blockDomain(domain)), + })); + } + + handleUnblockDomainClick = domain => { + this.props.dispatch(unblockDomain(domain)); + } + + handleHotkeyMoveUp = () => { this.handleMoveUp(this.props.status.get('id')); } @@ -514,8 +544,12 @@ class Status extends ImmutablePureComponent { onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} + onUnmute={this.handleUnmuteClick} onMuteConversation={this.handleConversationMuteClick} onBlock={this.handleBlockClick} + onUnblock={this.handleUnblockClick} + onBlockDomain={this.handleBlockDomainClick} + onUnblockDomain={this.handleUnblockDomainClick} onReport={this.handleReport} onPin={this.handlePin} onEmbed={this.handleEmbed}