about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-09-30 16:38:09 -0700
committerReverite <github@reverite.sh>2019-09-30 16:38:09 -0700
commit01001ba118d7adcd4966c893cf95db32fe76c273 (patch)
treefc8b580c8f0a3c1e14a8ca20e3bea5bd7acb4ff1 /app/javascript/flavours/glitch/features
parent1e75b3d874973dd71ea74b158ebeb501d9ac68a3 (diff)
parent65db9df011a3e13efa79f4e56343aa69bdad7716 (diff)
Merge branch 'glitch' into production
Diffstat (limited to 'app/javascript/flavours/glitch/features')
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js15
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js157
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js75
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/filter_bar.js2
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js18
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js20
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/block_modal.js103
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js12
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/mute_modal.js15
15 files changed, 359 insertions, 88 deletions
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index 787a36658..fff5e097f 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -5,7 +5,6 @@ import Header from '../components/header';
 import {
   followAccount,
   unfollowAccount,
-  blockAccount,
   unblockAccount,
   unmuteAccount,
   pinAccount,
@@ -16,6 +15,7 @@ import {
   directCompose
 } from 'flavours/glitch/actions/compose';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
@@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     if (account.getIn(['relationship', 'blocking'])) {
       dispatch(unblockAccount(account.get('id')));
     } else {
-      dispatch(openModal('CONFIRM', {
-        message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-        confirm: intl.formatMessage(messages.blockConfirm),
-        onConfirm: () => dispatch(blockAccount(account.get('id'))),
-        secondary: intl.formatMessage(messages.blockAndReport),
-        onSecondary: () => {
-          dispatch(blockAccount(account.get('id')));
-          dispatch(initReport(account));
-        },
-      }));
+      dispatch(initBlockModal(account));
     }
   },
 
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
index 9ddeabe75..17487b202 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
@@ -2,9 +2,28 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusContainer from 'flavours/glitch/containers/status_container';
+import StatusContent from 'flavours/glitch/components/status_content';
+import AttachmentList from 'flavours/glitch/components/attachment_list';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
+import AvatarComposite from 'flavours/glitch/components/avatar_composite';
+import Permalink from 'flavours/glitch/components/permalink';
+import IconButton from 'flavours/glitch/components/icon_button';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import { HotKeys } from 'react-hotkeys';
 
-export default class Conversation extends ImmutablePureComponent {
+const messages = defineMessages({
+  more: { id: 'status.more', defaultMessage: 'More' },
+  open: { id: 'conversation.open', defaultMessage: 'View conversation' },
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
+  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
+  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+});
+
+export default @injectIntl
+class Conversation extends ImmutablePureComponent {
 
   static contextTypes = {
     router: PropTypes.object,
@@ -13,25 +32,61 @@ export default class Conversation extends ImmutablePureComponent {
   static propTypes = {
     conversationId: PropTypes.string.isRequired,
     accounts: ImmutablePropTypes.list.isRequired,
-    lastStatusId: PropTypes.string,
+    lastStatus: ImmutablePropTypes.map,
     unread:PropTypes.bool.isRequired,
     onMoveUp: PropTypes.func,
     onMoveDown: PropTypes.func,
     markRead: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    isExpanded: undefined,
   };
 
+  parseClick = (e, destination) => {
+    const { router } = this.context;
+    const { lastStatus, unread, markRead } = this.props;
+    if (!router) return;
+
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
+      if (destination === undefined) {
+        if (unread) {
+          markRead();
+        }
+        destination = `/statuses/${lastStatus.get('id')}`;
+      }
+      let state = {...router.history.location.state};
+      state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
+      router.history.push(destination, state);
+      e.preventDefault();
+    }
+  }
+
   handleClick = () => {
     if (!this.context.router) {
       return;
     }
 
-    const { lastStatusId, unread, markRead } = this.props;
+    const { lastStatus, unread, markRead } = this.props;
 
     if (unread) {
       markRead();
     }
 
-    this.context.router.history.push(`/statuses/${lastStatusId}`);
+    this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
+  }
+
+  handleMarkAsRead = () => {
+    this.props.markRead();
+  }
+
+  handleReply = () => {
+    this.props.reply(this.props.lastStatus, this.context.router.history);
+  }
+
+  handleDelete = () => {
+    this.props.delete();
   }
 
   handleHotkeyMoveUp = () => {
@@ -42,22 +97,94 @@ export default class Conversation extends ImmutablePureComponent {
     this.props.onMoveDown(this.props.conversationId);
   }
 
+  handleConversationMute = () => {
+    this.props.onMute(this.props.lastStatus);
+  }
+
+  handleShowMore = () => {
+    if (this.props.lastStatus.get('spoiler_text')) {
+      this.setExpansion(!this.state.isExpanded);
+    }
+  };
+
+  setExpansion = value => {
+    this.setState({ isExpanded: value });
+  }
+
   render () {
-    const { accounts, lastStatusId, unread } = this.props;
+    const { accounts, lastStatus, unread, intl } = this.props;
+    const { isExpanded } = this.state;
 
-    if (lastStatusId === null) {
+    if (lastStatus === null) {
       return null;
     }
 
+    const menu = [
+      { text: intl.formatMessage(messages.open), action: this.handleClick },
+      null,
+    ];
+
+    menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
+
+    if (unread) {
+      menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
+      menu.push(null);
+    }
+
+    menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
+
+    const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
+
+    const handlers = {
+      reply: this.handleReply,
+      open: this.handleClick,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleHidden: this.handleShowMore,
+    };
+
+    let media = null;
+    if (lastStatus.get('media_attachments').size > 0) {
+      media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
+    }
+
     return (
-      <StatusContainer
-        id={lastStatusId}
-        unread={unread}
-        otherAccounts={accounts}
-        onMoveUp={this.handleHotkeyMoveUp}
-        onMoveDown={this.handleHotkeyMoveDown}
-        onClick={this.handleClick}
-      />
+      <HotKeys handlers={handlers}>
+        <div className='conversation focusable muted' tabIndex='0'>
+          <div className='conversation__avatar'>
+            <AvatarComposite accounts={accounts} size={48} />
+          </div>
+
+          <div className='conversation__content'>
+            <div className='conversation__content__info'>
+              <div className='conversation__content__relative-time'>
+                <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
+              </div>
+
+              <div className='conversation__content__names'>
+                <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
+              </div>
+            </div>
+
+            <StatusContent
+              status={lastStatus}
+              parseClick={this.parseClick}
+              expanded={isExpanded}
+              onExpandedToggle={this.handleShowMore}
+              collapsable
+              media={media}
+            />
+
+            <div className='status__action-bar'>
+              <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
+
+              <div className='status__action-bar-dropdown'>
+                <DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
+              </div>
+            </div>
+          </div>
+        </div>
+      </HotKeys>
     );
   }
 
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
index bd6f6bfb0..b15ce9f0f 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
@@ -1,19 +1,74 @@
 import { connect } from 'react-redux';
 import Conversation from '../components/conversation';
-import { markConversationRead } from '../../../actions/conversations';
+import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
+import { makeGetStatus } from 'flavours/glitch/selectors';
+import { replyCompose } from 'flavours/glitch/actions/compose';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses';
+import { defineMessages, injectIntl } from 'react-intl';
 
-const mapStateToProps = (state, { conversationId }) => {
-  const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+const messages = defineMessages({
+  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?' },
+});
+
+const mapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  return (state, { conversationId }) => {
+    const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+    const lastStatusId = conversation.get('last_status', null);
 
-  return {
-    accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
-    unread: conversation.get('unread'),
-    lastStatusId: conversation.get('last_status', null),
+    return {
+      accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
+      unread: conversation.get('unread'),
+      lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
+    };
   };
 };
 
-const mapDispatchToProps = (dispatch, { conversationId }) => ({
-  markRead: () => dispatch(markConversationRead(conversationId)),
+const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
+
+  markRead () {
+    dispatch(markConversationRead(conversationId));
+  },
+
+  reply (status, router) {
+    dispatch((_, getState) => {
+      let state = getState();
+
+      if (state.getIn(['compose', 'text']).trim().length !== 0) {
+        dispatch(openModal('CONFIRM', {
+          message: intl.formatMessage(messages.replyMessage),
+          confirm: intl.formatMessage(messages.replyConfirm),
+          onConfirm: () => dispatch(replyCompose(status, router)),
+        }));
+      } else {
+        dispatch(replyCompose(status, router));
+      }
+    });
+  },
+
+  delete () {
+    dispatch(deleteConversation(conversationId));
+  },
+
+  onMute (status) {
+    if (status.get('muted')) {
+      dispatch(unmuteStatus(status.get('id')));
+    } else {
+      dispatch(muteStatus(status.get('id')));
+    }
+  },
+
+  onToggleHidden (status) {
+    if (status.get('hidden')) {
+      dispatch(revealStatus(status.get('id')));
+    } else {
+      dispatch(hideStatus(status.get('id')));
+    }
+  },
+
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index 5c33c8677..7afadf12e 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -31,7 +31,9 @@ class Favourites extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchFavourites(this.props.params.statusId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchFavourites(this.props.params.statusId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index b12efa774..2bd0e6e2f 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -36,8 +36,10 @@ class Followers extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(fetchFollowers(this.props.params.accountId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchAccount(this.props.params.accountId));
+      this.props.dispatch(fetchFollowers(this.props.params.accountId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index 9ea008e61..f03da0c94 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -36,8 +36,10 @@ class Following extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(fetchFollowing(this.props.params.accountId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchAccount(this.props.params.accountId));
+      this.props.dispatch(fetchFollowing(this.props.params.accountId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index 68b5209dc..a5095fbd9 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -104,16 +104,14 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
   }
 
   componentDidMount () {
-    const { myAccount, fetchFollowRequests, multiColumn } = this.props;
+    const { fetchFollowRequests, multiColumn } = this.props;
 
     if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
       this.context.router.history.replace('/timelines/home');
       return;
     }
 
-    if (myAccount.get('locked')) {
-      fetchFollowRequests();
-    }
+    fetchFollowRequests();
   }
 
   render () {
@@ -148,7 +146,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
       navItems.push(<ColumnLink key='5' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
     }
 
-    if (myAccount.get('locked')) {
+    if (myAccount.get('locked') || unreadFollowRequests > 0) {
       navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
     }
 
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js
index 356ca4721..6118305d6 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js
@@ -64,7 +64,7 @@ class FilterBar extends React.PureComponent {
           onClick={this.onClick('mention')}
           title={intl.formatMessage(tooltips.mentions)}
         >
-          <Icon id='at' fixedWidth />
+          <Icon id='reply-all' fixedWidth />
         </button>
         <button
           className={selectedFilter === 'favourite' ? 'active' : ''}
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index 1fc26b0d7..a8e9db7f5 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -31,7 +31,9 @@ class Reblogs extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchReblogs(this.props.params.statusId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchReblogs(this.props.params.statusId));
+    }
   }
 
   componentWillReceiveProps(nextProps) {
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index e6c390537..e71803328 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -1,4 +1,3 @@
-import React from 'react';
 import { connect } from 'react-redux';
 import DetailedStatus from '../components/detailed_status';
 import { makeGetStatus } from 'flavours/glitch/selectors';
@@ -15,7 +14,6 @@ import {
   pin,
   unpin,
 } from 'flavours/glitch/actions/interactions';
-import { blockAccount } from 'flavours/glitch/actions/accounts';
 import {
   muteStatus,
   unmuteStatus,
@@ -24,9 +22,10 @@ import {
   revealStatus,
 } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
 import { openModal } from 'flavours/glitch/actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
 import { showAlertForError } from 'flavours/glitch/actions/alerts';
 
@@ -35,10 +34,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   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.' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   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?' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -139,16 +136,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onBlock (status) {
     const account = status.get('account');
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.blockConfirm),
-      onConfirm: () => dispatch(blockAccount(account.get('id'))),
-      secondary: intl.formatMessage(messages.blockAndReport),
-      onSecondary: () => {
-        dispatch(blockAccount(account.get('id')));
-        dispatch(initReport(account, status));
-      },
-    }));
+    dispatch(initBlockModal(account));
   },
 
   onReport (status) {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index e91ab5f3a..dd17823ad 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -26,9 +26,9 @@ import {
   directCompose,
 } from 'flavours/glitch/actions/compose';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { blockAccount } from 'flavours/glitch/actions/accounts';
 import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
 import { makeGetStatus } from 'flavours/glitch/selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
@@ -36,7 +36,7 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ColumnHeader from '../../components/column_header';
 import StatusContainer from 'flavours/glitch/containers/status_container';
 import { openModal } from 'flavours/glitch/actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
 import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
@@ -50,13 +50,11 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
   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?' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
   tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' },
 });
 
@@ -339,19 +337,9 @@ class Status extends ImmutablePureComponent {
   }
 
   handleBlockClick = (status) => {
-    const { dispatch, intl } = this.props;
+    const { dispatch } = this.props;
     const account = status.get('account');
-
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.blockConfirm),
-      onConfirm: () => dispatch(blockAccount(account.get('id'))),
-      secondary: intl.formatMessage(messages.blockAndReport),
-      onSecondary: () => {
-        dispatch(blockAccount(account.get('id')));
-        dispatch(initReport(account, status));
-      },
-    }));
+    dispatch(initBlockModal(account));
   }
 
   handleReport = (status) => {
diff --git a/app/javascript/flavours/glitch/features/ui/components/block_modal.js b/app/javascript/flavours/glitch/features/ui/components/block_modal.js
new file mode 100644
index 000000000..a07baeaa6
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/block_modal.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { makeGetAccount } from '../../../selectors';
+import Button from '../../../components/button';
+import { closeModal } from '../../../actions/modal';
+import { blockAccount } from '../../../actions/accounts';
+import { initReport } from '../../../actions/reports';
+
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = state => ({
+    account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onConfirm(account) {
+      dispatch(blockAccount(account.get('id')));
+    },
+
+    onBlockAndReport(account) {
+      dispatch(blockAccount(account.get('id')));
+      dispatch(initReport(account));
+    },
+
+    onClose() {
+      dispatch(closeModal());
+    },
+  };
+};
+
+export default @connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+class BlockModal extends React.PureComponent {
+
+  static propTypes = {
+    account: PropTypes.object.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onBlockAndReport: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount() {
+    this.button.focus();
+  }
+
+  handleClick = () => {
+    this.props.onClose();
+    this.props.onConfirm(this.props.account);
+  }
+
+  handleSecondary = () => {
+    this.props.onClose();
+    this.props.onBlockAndReport(this.props.account);
+  }
+
+  handleCancel = () => {
+    this.props.onClose();
+  }
+
+  setRef = (c) => {
+    this.button = c;
+  }
+
+  render () {
+    const { account } = this.props;
+
+    return (
+      <div className='modal-root__modal block-modal'>
+        <div className='block-modal__container'>
+          <p>
+            <FormattedMessage
+              id='confirmations.block.message'
+              defaultMessage='Are you sure you want to block {name}?'
+              values={{ name: <strong>@{account.get('acct')}</strong> }}
+            />
+          </p>
+        </div>
+
+        <div className='block-modal__action-bar'>
+          <Button onClick={this.handleCancel} className='block-modal__cancel-button'>
+            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
+          </Button>
+          <Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
+            <FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
+          </Button>
+          <Button onClick={this.handleClick} ref={this.setRef}>
+            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
index 8bded391a..d5c9e66ae 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
@@ -173,7 +173,17 @@ class FocalPointModal extends ImmutablePureComponent {
         langPath: `${assetHost}/ocr/lang-data`,
       });
 
-      worker.recognize(media.get('url'))
+      let media_url = media.get('file');
+
+      if (window.URL && URL.createObjectURL) {
+        try {
+          media_url = URL.createObjectURL(media.get('file'));
+        } catch (error) {
+          console.error(error);
+        }
+      }
+
+      worker.recognize(media_url)
         .progress(({ progress }) => this.setState({ progress }))
         .finally(() => worker.terminate())
         .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 303e05db6..0941ce9c8 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -15,6 +15,7 @@ import FocalPointModal from './focal_point_modal';
 import {
   OnboardingModal,
   MuteModal,
+  BlockModal,
   ReportModal,
   SettingsModal,
   EmbedModal,
@@ -32,6 +33,7 @@ const MODAL_COMPONENTS = {
   'DOODLE': () => Promise.resolve({ default: DoodleModal }),
   'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
   'MUTE': MuteModal,
+  'BLOCK': BlockModal,
   'REPORT': ReportModal,
   'SETTINGS': SettingsModal,
   'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
index 3492eca69..dec6413c3 100644
--- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -11,7 +11,6 @@ import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
 
 const mapStateToProps = state => {
   return {
-    isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
     account: state.getIn(['mutes', 'new', 'account']),
     notifications: state.getIn(['mutes', 'new', 'notifications']),
   };
@@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps)
 class MuteModal extends React.PureComponent {
 
   static propTypes = {
-    isSubmitting: PropTypes.bool.isRequired,
     account: PropTypes.object.isRequired,
     notifications: PropTypes.bool.isRequired,
     onClose: PropTypes.func.isRequired,
@@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent {
               values={{ name: <strong>@{account.get('acct')}</strong> }}
             />
           </p>
-          <div>
-            <label htmlFor='mute-modal__hide-notifications-checkbox'>
+          <p className='mute-modal__explanation'>
+            <FormattedMessage
+              id='confirmations.mute.explanation'
+              defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts follow you.'
+            />
+          </p>
+          <div className='setting-toggle'>
+            <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
+            <label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
               <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
-              {' '}
-              <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
             </label>
           </div>
         </div>