From 853a67ed164b4975f8ee9370aa521849eebef47c Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
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 4fa2c1158c..bd3bb16bbc 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 {
         <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
         <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
         {shareButton}
-        <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
 
         <div className='status__action-bar-dropdown'>
           <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js
index f79b192029..1427f85289 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 16ba02e120..35c16a20ca 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: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
+      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 1b81cd245d..76334de69b 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,15 +105,43 @@ 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');
 
-  handleConversationMuteClick = () => {
-    this.props.onMuteConversation(this.props.status);
+    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]);
+  }
+
+  handleConversationMuteClick = () => {
+    this.props.onMuteConversation(this.props.status);
   }
 
   handleReport = () => {
@@ -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 {
         <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
 
         <div className='detailed-status__action-bar-dropdown'>
-          <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' />
+          <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 9fb3fe305c..55bd998865 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: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
+      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}
-- 
GitLab