about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/blocks.js14
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js5
-rw-r--r--app/javascript/flavours/glitch/actions/conversations.js28
-rw-r--r--app/javascript/flavours/glitch/actions/importer/normalizer.js3
-rw-r--r--app/javascript/flavours/glitch/components/avatar_composite.js28
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button_slim.js2
-rw-r--r--app/javascript/flavours/glitch/components/poll.js27
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js17
-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
-rw-r--r--app/javascript/flavours/glitch/packs/public.js10
-rw-r--r--app/javascript/flavours/glitch/packs/settings.js20
-rw-r--r--app/javascript/flavours/glitch/reducers/blocks.js22
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js7
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/mutes.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/notifications.js4
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js5
-rw-r--r--app/javascript/flavours/glitch/styles/_mixins.scss18
-rw-r--r--app/javascript/flavours/glitch/styles/about.scss179
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss199
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss36
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss8
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss83
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss73
-rw-r--r--app/javascript/flavours/glitch/styles/components/search.scss22
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss62
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss15
-rw-r--r--app/javascript/flavours/glitch/styles/mastodon-light/diff.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/polls.scss10
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss67
-rw-r--r--app/javascript/flavours/glitch/styles/widgets.scss83
-rw-r--r--app/javascript/flavours/glitch/theme.yml2
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js4
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js2
50 files changed, 1091 insertions, 421 deletions
diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js
index 498ce519f..adae9d83c 100644
--- a/app/javascript/flavours/glitch/actions/blocks.js
+++ b/app/javascript/flavours/glitch/actions/blocks.js
@@ -1,6 +1,7 @@
 import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
 import { importFetchedAccounts } from './importer';
+import { openModal } from './modal';
 
 export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
 export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
@@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
 export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
 export const BLOCKS_EXPAND_FAIL    = 'BLOCKS_EXPAND_FAIL';
 
+export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
+
 export function fetchBlocks() {
   return (dispatch, getState) => {
     dispatch(fetchBlocksRequest());
@@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
     error,
   };
 };
+
+export function initBlockModal(account) {
+  return dispatch => {
+    dispatch({
+      type: BLOCKS_INIT_MODAL,
+      account,
+    });
+
+    dispatch(openModal('BLOCK'));
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index e1da03745..69cf65b5a 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -261,7 +261,7 @@ export function uploadCompose(files) {
             progress[i] = loaded;
             dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
           },
-        }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+        }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
       }).catch(error => dispatch(uploadComposeFail(error)));
     };
   };
@@ -316,10 +316,11 @@ export function uploadComposeProgress(loaded, total) {
   };
 };
 
-export function uploadComposeSuccess(media) {
+export function uploadComposeSuccess(media, file) {
   return {
     type: COMPOSE_UPLOAD_SUCCESS,
     media: media,
+    file: file,
     skipLoading: true,
   };
 };
diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js
index 856f8f10f..e5c85c65d 100644
--- a/app/javascript/flavours/glitch/actions/conversations.js
+++ b/app/javascript/flavours/glitch/actions/conversations.js
@@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE        = 'CONVERSATIONS_UPDATE';
 
 export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
 
+export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
+export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
+export const CONVERSATIONS_DELETE_FAIL    = 'CONVERSATIONS_DELETE_FAIL';
+
 export const mountConversations = () => ({
   type: CONVERSATIONS_MOUNT,
 });
@@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
     conversation,
   });
 };
+
+export const deleteConversation = conversationId => (dispatch, getState) => {
+  dispatch(deleteConversationRequest(conversationId));
+
+  api(getState).delete(`/api/v1/conversations/${conversationId}`)
+    .then(() => dispatch(deleteConversationSuccess(conversationId)))
+    .catch(error => dispatch(deleteConversationFail(conversationId, error)));
+};
+
+export const deleteConversationRequest = id => ({
+  type: CONVERSATIONS_DELETE_REQUEST,
+  id,
+});
+
+export const deleteConversationSuccess = id => ({
+  type: CONVERSATIONS_DELETE_SUCCESS,
+  id,
+});
+
+export const deleteConversationFail = (id, error) => ({
+  type: CONVERSATIONS_DELETE_FAIL,
+  id,
+  error,
+});
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js
index 52d85c059..b35c4d7bd 100644
--- a/app/javascript/flavours/glitch/actions/importer/normalizer.js
+++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js
@@ -71,8 +71,9 @@ export function normalizePoll(poll) {
 
   const emojiMap = makeEmojiMap(normalPoll);
 
-  normalPoll.options = poll.options.map(option => ({
+  normalPoll.options = poll.options.map((option, index) => ({
     ...option,
+    voted: poll.own_votes && poll.own_votes.includes(index),
     title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
   }));
 
diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js
index c52df043a..125b51c44 100644
--- a/app/javascript/flavours/glitch/components/avatar_composite.js
+++ b/app/javascript/flavours/glitch/components/avatar_composite.js
@@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
 
     if (size === 2) {
       if (index === 0) {
-        right = '2px';
+        right = '1px';
       } else {
-        left = '2px';
+        left = '1px';
       }
     } else if (size === 3) {
       if (index === 0) {
-        right = '2px';
+        right = '1px';
       } else if (index > 0) {
-        left = '2px';
+        left = '1px';
       }
 
       if (index === 1) {
-        bottom = '2px';
+        bottom = '1px';
       } else if (index > 1) {
-        top = '2px';
+        top = '1px';
       }
     } else if (size === 4) {
       if (index === 0 || index === 2) {
-        right = '2px';
+        right = '1px';
       }
 
       if (index === 1 || index === 3) {
-        left = '2px';
+        left = '1px';
       }
 
       if (index < 2) {
-        bottom = '2px';
+        bottom = '1px';
       } else {
-        top = '2px';
+        top = '1px';
       }
     }
 
@@ -96,7 +96,13 @@ export default class AvatarComposite extends React.PureComponent {
 
     return (
       <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
-        {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
+        {accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
+
+        {accounts.size > 4 && (
+          <span className='account__avatar-composite__label'>
+            +{accounts.size - 4}
+          </span>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.js b/app/javascript/flavours/glitch/components/column_back_button_slim.js
index b57e3a057..faa0c23a8 100644
--- a/app/javascript/flavours/glitch/components/column_back_button_slim.js
+++ b/app/javascript/flavours/glitch/components/column_back_button_slim.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { FormattedMessage } from 'react-intl';
 import PropTypes from 'prop-types';
-import Icon from 'mastodon/components/icon';
+import Icon from 'flavours/glitch/components/icon';
 
 export default class ColumnBackButtonSlim extends React.PureComponent {
 
diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js
index 905aa54c1..a7f9a97da 100644
--- a/app/javascript/flavours/glitch/components/poll.js
+++ b/app/javascript/flavours/glitch/components/poll.js
@@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
 import escapeTextContentForBrowser from 'escape-html';
 import emojify from 'flavours/glitch/util/emoji';
 import RelativeTimestamp from './relative_timestamp';
+import Icon from 'flavours/glitch/components/icon';
 
 const messages = defineMessages({
   closed: { id: 'poll.closed', defaultMessage: 'Closed' },
+  voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
 });
 
 const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
@@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
   };
 
   renderOption (option, optionIndex, showResults) {
-    const { poll, disabled } = this.props;
-    const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
-    const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
-    const active             = !!this.state.selected[`${optionIndex}`];
+    const { poll, disabled, intl } = this.props;
+    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');
+    const percent         = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
+    const leading         = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
+    const active          = !!this.state.selected[`${optionIndex}`];
+    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
 
     let titleEmojified = option.get('title_emojified');
     if (!titleEmojified) {
@@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
           />
 
           {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
-          {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
+          {showResults && <span className='poll__number'>
+            {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
+            {Math.round(percent)}%
+          </span>}
 
           <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
         </label>
@@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
     const showResults   = poll.get('voted') || expired;
     const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
 
+    let votesCount = null;
+
+    if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
+      votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
+    } else {
+      votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
+    }
+
     return (
       <div className='poll'>
         <ul>
@@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
         <div className='poll__footer'>
           {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
           {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
-          <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
+          {votesCount}
           {poll.get('expires_at') && <span> · {timeRemaining}</span>}
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 15eb4f85f..4c3555dea 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -1,4 +1,3 @@
-import React from 'react';
 import { connect } from 'react-redux';
 import Status from 'flavours/glitch/components/status';
 import { List as ImmutableList } from 'immutable';
@@ -18,9 +17,9 @@ import {
   pin,
   unpin,
 } from 'flavours/glitch/actions/interactions';
-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 { openModal } from 'flavours/glitch/actions/modal';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
@@ -37,10 +36,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? You will lose all replies, boosts and favourites to it.' },
-  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' },
   unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
   author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
   matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
@@ -83,6 +80,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
   onReply (status, router) {
     dispatch((_, getState) => {
       let state = getState();
+
       if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
         dispatch(openModal('CONFIRM', {
           message: intl.formatMessage(messages.replyMessage),
@@ -186,16 +184,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
 
   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));
   },
 
   onUnfilter (status, onConfirm) {
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>
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index 019de2167..5a15830df 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -114,6 +114,16 @@ function main() {
       this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
     });
   });
+
+  delegate(document, '.sidebar__toggle__icon', 'click', () => {
+    const target = document.querySelector('.sidebar ul');
+
+    if (target.style.display === 'block') {
+      target.style.display = 'none';
+    } else {
+      target.style.display = 'block';
+    }
+  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js
new file mode 100644
index 000000000..b32f38226
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/settings.js
@@ -0,0 +1,20 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+import ready from 'flavours/glitch/util/ready';
+
+function main() {
+  const { delegate } = require('rails-ujs');
+
+  delegate(document, '.sidebar__toggle__icon', 'click', () => {
+    const target = document.querySelector('.sidebar ul');
+
+    if (target.style.display === 'block') {
+      target.style.display = 'none';
+    } else {
+      target.style.display = 'block';
+    }
+  });
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/glitch/reducers/blocks.js b/app/javascript/flavours/glitch/reducers/blocks.js
new file mode 100644
index 000000000..1b6507163
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/blocks.js
@@ -0,0 +1,22 @@
+import Immutable from 'immutable';
+
+import {
+  BLOCKS_INIT_MODAL,
+} from '../actions/blocks';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    account_id: null,
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BLOCKS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'account_id'], action.account.get('id'));
+    });
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index adad205c0..17ce5de3c 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -190,10 +190,13 @@ function continueThread (state, status) {
   });
 }
 
-function appendMedia(state, media) {
+function appendMedia(state, media, file) {
   const prevSize = state.get('media_attachments').size;
 
   return state.withMutations(map => {
+    if (media.get('type') === 'image') {
+      media = media.set('file', file);
+    }
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
     map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
@@ -422,7 +425,7 @@ export default function compose(state = initialState, action) {
   case COMPOSE_UPLOAD_REQUEST:
     return state.set('is_uploading', true);
   case COMPOSE_UPLOAD_SUCCESS:
-    return appendMedia(state, fromJS(action.media));
+    return appendMedia(state, fromJS(action.media), action.file);
   case COMPOSE_UPLOAD_FAIL:
     return state.set('is_uploading', false);
   case COMPOSE_UPLOAD_UNDO:
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index b03590194..7dbca3a29 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -16,6 +16,7 @@ import local_settings from './local_settings';
 import push_notifications from './push_notifications';
 import status_lists from './status_lists';
 import mutes from './mutes';
+import blocks from './blocks';
 import reports from './reports';
 import contexts from './contexts';
 import compose from './compose';
@@ -53,6 +54,7 @@ const reducers = {
   local_settings,
   push_notifications,
   mutes,
+  blocks,
   reports,
   contexts,
   compose,
diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js
index 8f52a7704..7111bb710 100644
--- a/app/javascript/flavours/glitch/reducers/mutes.js
+++ b/app/javascript/flavours/glitch/reducers/mutes.js
@@ -7,7 +7,6 @@ import {
 
 const initialState = Immutable.Map({
   new: Immutable.Map({
-    isSubmitting: false,
     account: null,
     notifications: true,
   }),
@@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) {
   switch (action.type) {
   case MUTES_INIT_MODAL:
     return state.withMutations((state) => {
-      state.setIn(['new', 'isSubmitting'], false);
       state.setIn(['new', 'account'], action.account);
       state.setIn(['new', 'notifications'], true);
     });
diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index 8dc7a4aba..8d5c6785c 100644
--- a/app/javascript/flavours/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -52,7 +52,7 @@ const notificationToMap = (state, notification) => ImmutableMap({
 const normalizeNotification = (state, notification, usePendingItems) => {
   const top = !shouldCountUnreadNotifications(state);
 
-  if (usePendingItems || !top || !state.get('pendingItems').isEmpty()) {
+  if (usePendingItems || !state.get('pendingItems').isEmpty()) {
     return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1);
   }
 
@@ -82,7 +82,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
 
   return state.withMutations(mutable => {
     if (!items.isEmpty()) {
-      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('top') || !mutable.get('pendingItems').isEmpty());
+      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
 
       mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
         const lastIndex = 1 + list.findLastIndex(
diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index e6bef18e9..df88a6c23 100644
--- a/app/javascript/flavours/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -40,7 +40,8 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
     if (timeline.endsWith(':pinned')) {
       mMap.set('items', statuses.map(status => status.get('id')));
     } else if (!statuses.isEmpty()) {
-      usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty());
+      usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
+
       mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
         const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
@@ -62,7 +63,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 const updateTimeline = (state, timeline, status, usePendingItems) => {
   const top = state.getIn([timeline, 'top']);
 
-  if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
+  if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
     if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
       return state;
     }
diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss
index d542b1083..088b41e76 100644
--- a/app/javascript/flavours/glitch/styles/_mixins.scss
+++ b/app/javascript/flavours/glitch/styles/_mixins.scss
@@ -62,24 +62,6 @@
   color: $darker-text-color;
   font-size: 14px;
   margin: 0;
-
-  &::-moz-focus-inner {
-    border: 0;
-  }
-
-  &::-moz-focus-inner,
-  &:focus,
-  &:active {
-    outline: 0 !important;
-  }
-
-  &:focus {
-    background: lighten($ui-base-color, 4%);
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
 }
 
 @mixin search-popout() {
diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss
index 0e910693d..7c129674d 100644
--- a/app/javascript/flavours/glitch/styles/about.scss
+++ b/app/javascript/flavours/glitch/styles/about.scss
@@ -17,109 +17,102 @@ $small-breakpoint: 960px;
 
 .rich-formatting {
   font-family: $font-sans-serif, sans-serif;
-  font-size: 16px;
+  font-size: 14px;
   font-weight: 400;
-  font-size: 16px;
-  line-height: 30px;
+  line-height: 1.7;
+  word-wrap: break-word;
   color: $darker-text-color;
-  padding-right: 10px;
 
   a {
     color: $highlight-text-color;
     text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
   }
 
   p,
   li {
-    font-family: $font-sans-serif, sans-serif;
-    font-size: 16px;
-    font-weight: 400;
-    font-size: 16px;
-    line-height: 30px;
-    margin-bottom: 12px;
     color: $darker-text-color;
+  }
 
-    a {
-      color: $highlight-text-color;
-      text-decoration: underline;
-    }
+  p {
+    margin-top: 0;
+    margin-bottom: .85em;
 
     &:last-child {
       margin-bottom: 0;
     }
   }
 
-  strong,
-  em {
+  strong {
     font-weight: 700;
-    color: lighten($darker-text-color, 10%);
+    color: $secondary-text-color;
   }
 
-  h1 {
-    font-family: $font-display, sans-serif;
-    font-size: 26px;
-    line-height: 30px;
-    font-weight: 500;
-    margin-bottom: 20px;
+  em {
+    font-style: italic;
     color: $secondary-text-color;
+  }
 
-    small {
-      font-family: $font-sans-serif, sans-serif;
-      display: block;
-      font-size: 18px;
-      font-weight: 400;
-      color: lighten($darker-text-color, 10%);
-    }
+  code {
+    font-size: 0.85em;
+    background: darken($ui-base-color, 8%);
+    border-radius: 4px;
+    padding: 0.2em 0.3em;
   }
 
-  h2 {
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
     font-family: $font-display, sans-serif;
-    font-size: 22px;
-    line-height: 26px;
+    margin-top: 1.275em;
+    margin-bottom: .85em;
     font-weight: 500;
-    margin-bottom: 20px;
     color: $secondary-text-color;
   }
 
+  h1 {
+    font-size: 2em;
+  }
+
+  h2 {
+    font-size: 1.75em;
+  }
+
   h3 {
-    font-family: $font-display, sans-serif;
-    font-size: 18px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.5em;
   }
 
   h4 {
-    font-family: $font-display, sans-serif;
-    font-size: 16px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.25em;
   }
 
-  h5 {
-    font-family: $font-display, sans-serif;
-    font-size: 14px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  h5,
+  h6 {
+    font-size: 1em;
   }
 
-  h6 {
-    font-family: $font-display, sans-serif;
-    font-size: 12px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  ul {
+    list-style: disc;
+  }
+
+  ol {
+    list-style: decimal;
   }
 
   ul,
   ol {
-    margin-left: 20px;
+    margin: 0;
+    padding: 0;
+    padding-left: 2em;
+    margin-bottom: 0.85em;
 
     &[type='a'] {
       list-style-type: lower-alpha;
@@ -130,31 +123,63 @@ $small-breakpoint: 960px;
     }
   }
 
-  ul {
-    list-style: disc;
-  }
-
-  ol {
-    list-style: decimal;
-  }
-
-  li > ol,
-  li > ul {
-    margin-top: 6px;
-  }
-
   hr {
     width: 100%;
     height: 0;
     border: 0;
-    border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
-    margin: 20px 0;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+    margin: 1.7em 0;
 
     &.spacer {
       height: 1px;
       border: 0;
     }
   }
+
+  table {
+    width: 100%;
+    border-collapse: collapse;
+    break-inside: auto;
+    margin-top: 24px;
+    margin-bottom: 32px;
+
+    thead tr,
+    tbody tr {
+      break-after: auto;
+      break-inside: avoid;
+      border-bottom: 1px solid lighten($ui-base-color, 4%);
+      font-size: 1em;
+      line-height: 1.625;
+      font-weight: 400;
+      text-align: left;
+      color: $darker-text-color;
+    }
+
+    thead tr {
+      border-bottom-width: 2px;
+      line-height: 1.5;
+      font-weight: 500;
+      color: $dark-text-color;
+    }
+
+    th,
+    td {
+      padding: 8px;
+      align-self: start;
+      align-items: start;
+
+      &.nowrap {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 25%;
+      }
+    }
+  }
+
+  & > :first-child {
+    margin-top: 0;
+  }
 }
 
 .information-board {
@@ -418,7 +443,7 @@ $small-breakpoint: 960px;
   }
 
   &__call-to-action {
-    background: darken($ui-base-color, 4%);
+    background: $ui-base-color;
     border-radius: 4px;
     padding: 25px 40px;
     overflow: hidden;
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 089ae68c0..1d25d0129 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -5,21 +5,66 @@ $content-width: 840px;
 .admin-wrapper {
   display: flex;
   justify-content: center;
-  height: 100%;
+  width: 100%;
+  min-height: 100vh;
 
   .sidebar-wrapper {
-    flex: 1 1 $sidebar-width;
-    height: 100%;
-    background: $ui-base-color;
-    display: flex;
-    justify-content: flex-end;
+    min-height: 100vh;
+    overflow: hidden;
+    pointer-events: none;
+    flex: 1 1 auto;
+
+    &__inner {
+      display: flex;
+      justify-content: flex-end;
+      background: $ui-base-color;
+      height: 100%;
+    }
   }
 
   .sidebar {
     width: $sidebar-width;
-    height: 100%;
     padding: 0;
-    overflow-y: auto;
+    pointer-events: auto;
+
+    &__toggle {
+      display: none;
+      background: lighten($ui-base-color, 8%);
+      height: 48px;
+
+      &__logo {
+        flex: 1 1 auto;
+
+        a {
+          display: inline-block;
+          padding: 15px;
+        }
+
+        svg {
+          fill: $primary-text-color;
+          height: 20px;
+          position: relative;
+          bottom: -2px;
+        }
+      }
+
+      &__icon {
+        display: block;
+        color: $darker-text-color;
+        text-decoration: none;
+        flex: 0 0 auto;
+        font-size: 20px;
+        padding: 15px;
+      }
+
+      a {
+        &:hover,
+        &:focus,
+        &:active {
+          background: lighten($ui-base-color, 12%);
+        }
+      }
+    }
 
     .logo {
       display: block;
@@ -52,6 +97,9 @@ $content-width: 840px;
         transition: all 200ms linear;
         transition-property: color, background-color;
         border-radius: 4px 0 0 4px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
 
         i.fa {
           margin-right: 5px;
@@ -99,12 +147,30 @@ $content-width: 840px;
   }
 
   .content-wrapper {
-    flex: 2 1 $content-width;
-    overflow: auto;
+    box-sizing: border-box;
+    width: 100%;
+    max-width: $content-width;
+    flex: 1 1 auto;
+  }
+
+  @media screen and (max-width: $content-width + $sidebar-width) {
+    .sidebar-wrapper--empty {
+      display: none;
+    }
+
+    .sidebar-wrapper {
+      width: $sidebar-width;
+      flex: 0 0 auto;
+    }
+  }
+
+  @media screen and (max-width: $no-columns-breakpoint) {
+    .sidebar-wrapper {
+      width: 100%;
+    }
   }
 
   .content {
-    max-width: $content-width;
     padding: 20px 15px;
     padding-top: 60px;
     padding-left: 25px;
@@ -123,6 +189,12 @@ $content-width: 840px;
       padding-bottom: 40px;
       border-bottom: 1px solid lighten($ui-base-color, 8%);
       margin-bottom: 40px;
+
+      @media screen and (max-width: $no-columns-breakpoint) {
+        border-bottom: 0;
+        padding-bottom: 0;
+        font-weight: 700;
+      }
     }
 
     h3 {
@@ -147,7 +219,7 @@ $content-width: 840px;
       font-size: 16px;
       color: $secondary-text-color;
       line-height: 28px;
-      font-weight: 400;
+      font-weight: 500;
     }
 
     .fields-group h6 {
@@ -176,7 +248,7 @@ $content-width: 840px;
 
     & > p {
       font-size: 14px;
-      line-height: 18px;
+      line-height: 21px;
       color: $secondary-text-color;
       margin-bottom: 20px;
 
@@ -204,49 +276,86 @@ $content-width: 840px;
         border: 0;
       }
     }
-
-    .muted-hint {
-      color: $darker-text-color;
-
-      a {
-        color: $highlight-text-color;
-      }
-    }
-
-    .positive-hint {
-      color: $valid-value-color;
-      font-weight: 500;
-    }
-
-    .negative-hint {
-      color: $error-value-color;
-      font-weight: 500;
-    }
-
-    .neutral-hint {
-      color: $dark-text-color;
-      font-weight: 500;
-    }
   }
 
   @media screen and (max-width: $no-columns-breakpoint) {
     display: block;
-    overflow-y: auto;
-    -webkit-overflow-scrolling: touch;
 
-    .sidebar-wrapper,
-    .content-wrapper {
-      flex: 0 0 auto;
-      height: auto;
-      overflow: initial;
+    .sidebar-wrapper {
+      min-height: 0;
     }
 
     .sidebar {
       width: 100%;
       padding: 0;
       height: auto;
+
+      &__toggle {
+        display: flex;
+      }
+
+      & > ul {
+        display: none;
+      }
+
+      ul a,
+      ul ul a {
+        border-radius: 0;
+        border-bottom: 1px solid lighten($ui-base-color, 4%);
+        transition: none;
+
+        &:hover {
+          transition: none;
+        }
+      }
+
+      ul ul {
+        border-radius: 0;
+      }
+
+      ul .simple-navigation-active-leaf a {
+        border-bottom-color: $ui-highlight-color;
+      }
+    }
+  }
+}
+
+hr.spacer {
+  width: 100%;
+  border: 0;
+  margin: 20px 0;
+  height: 1px;
+}
+
+body,
+.admin-wrapper .content {
+  .muted-hint {
+    color: $darker-text-color;
+
+    a {
+      color: $highlight-text-color;
     }
   }
+
+  .positive-hint {
+    color: $valid-value-color;
+    font-weight: 500;
+  }
+
+  .negative-hint {
+    color: $error-value-color;
+    font-weight: 500;
+  }
+
+  .neutral-hint {
+    color: $dark-text-color;
+    font-weight: 500;
+  }
+
+  .warning-hint {
+    color: $gold-star;
+    font-weight: 500;
+  }
 }
 
 .filters {
@@ -255,10 +364,10 @@ $content-width: 840px;
 
   .filter-subset {
     flex: 0 0 auto;
-    margin: 0 40px 10px 0;
+    margin: 0 40px 20px 0;
 
     &:last-child {
-      margin-bottom: 20px;
+      margin-bottom: 30px;
     }
 
     ul {
diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index 4de3955a6..64e543b78 100644
--- a/app/javascript/flavours/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
@@ -74,9 +74,6 @@ body {
 
   &.admin {
     background: darken($ui-base-color, 4%);
-    position: fixed;
-    width: 100%;
-    height: 100%;
     padding: 0;
   }
 
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index dc49e083c..5be4da48a 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -50,6 +50,8 @@
   &-composite {
     @include avatar-radius;
     overflow: hidden;
+    position: relative;
+    cursor: default;
 
     & div {
       @include avatar-radius;
@@ -57,6 +59,18 @@
       position: relative;
       box-sizing: border-box;
     }
+
+    &__label {
+      display: block;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: $primary-text-color;
+      text-shadow: 1px 1px 2px $base-shadow-color;
+      font-weight: 700;
+      font-size: 15px;
+    }
   }
 }
 
@@ -245,6 +259,28 @@
   .column-select {
     &__control {
       @include search-input();
+
+      &::placeholder {
+        color: lighten($darker-text-color, 4%);
+      }
+
+      &::-moz-focus-inner {
+        border: 0;
+      }
+
+      &::-moz-focus-inner,
+      &:focus,
+      &:active {
+        outline: 0 !important;
+      }
+
+      &:focus {
+        background: lighten($ui-base-color, 4%);
+      }
+
+      @media screen and (max-width: 600px) {
+        font-size: 16px;
+      }
     }
 
     &__placeholder {
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index 656615f4f..436974919 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -44,6 +44,10 @@
     font-family: inherit;
     resize: vertical;
 
+    &::placeholder {
+      color: $dark-text-color;
+    }
+
     &:focus { outline: 0 }
     @include single-column('screen and (max-width: 630px)') { font-size: 16px }
   }
@@ -263,6 +267,10 @@
       resize: none;
       scrollbar-color: initial;
 
+      &::placeholder {
+        color: $dark-text-color;
+      }
+
       &::-webkit-scrollbar {
         all: unset;
       }
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 97c525565..848ef78df 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1433,49 +1433,68 @@
   height: 1em;
 }
 
-.layout-toggle {
+.conversation {
   display: flex;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
   padding: 5px;
+  padding-bottom: 0;
 
-  button {
-    box-sizing: border-box;
-    flex: 0 0 50%;
-    background: transparent;
-    padding: 5px;
-    border: 0;
-    position: relative;
+  &:focus {
+    background: lighten($ui-base-color, 2%);
+    outline: 0;
+  }
 
-    &:hover,
-    &:focus,
-    &:active {
-      svg path:first-child {
-        fill: lighten($ui-base-color, 16%);
-      }
-    }
+  &__avatar {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-top: 12px;
   }
 
-  svg {
-    width: 100%;
-    height: auto;
+  &__content {
+    flex: 1 1 auto;
+    padding: 10px 5px;
+    padding-right: 15px;
+    word-break: break-all;
+    overflow: hidden;
 
-    path:first-child {
-      fill: lighten($ui-base-color, 12%);
+    &__info {
+      overflow: hidden;
+      display: flex;
+      flex-direction: row-reverse;
+      justify-content: space-between;
     }
 
-    path:last-child {
-      fill: darken($ui-base-color, 14%);
+    &__relative-time {
+      font-size: 15px;
+      color: $darker-text-color;
+      padding-left: 15px;
     }
-  }
 
-  &__active {
-    color: $ui-highlight-color;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    background: lighten($ui-base-color, 12%);
-    border-radius: 50%;
-    padding: 0.35rem;
+    &__names {
+      color: $darker-text-color;
+      font-size: 15px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin-bottom: 4px;
+      flex-basis: 170px;
+      flex-shrink: 1000;
+
+      a {
+        color: $primary-text-color;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+
+    .status__content {
+      margin: 0;
+    }
   }
 }
 
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index ec32c9114..4f3e5babf 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -405,7 +405,8 @@
 .confirmation-modal,
 .report-modal,
 .actions-modal,
-.mute-modal {
+.mute-modal,
+.block-modal {
   background: lighten($ui-secondary-color, 8%);
   color: $inverted-text-color;
   border-radius: 8px;
@@ -465,7 +466,8 @@
 .boost-modal__action-bar,
 .favourite-modal__action-bar,
 .confirmation-modal__action-bar,
-.mute-modal__action-bar {
+.mute-modal__action-bar,
+.block-modal__action-bar {
   display: flex;
   justify-content: space-between;
   background: $ui-secondary-color;
@@ -495,11 +497,13 @@
   font-size: 14px;
 }
 
-.mute-modal {
+.mute-modal,
+.block-modal {
   line-height: 24px;
 }
 
-.mute-modal .react-toggle {
+.mute-modal .react-toggle,
+.block-modal .react-toggle {
   vertical-align: middle;
 }
 
@@ -712,27 +716,29 @@
 }
 
 .confirmation-modal__action-bar,
-.mute-modal__action-bar {
-  .confirmation-modal__secondary-button,
-  .confirmation-modal__cancel-button,
-  .mute-modal__cancel-button {
-    background-color: transparent;
-    color: $lighter-text-color;
-    font-size: 14px;
-    font-weight: 500;
-
-    &:hover,
-    &:focus,
-    &:active {
-      color: darken($lighter-text-color, 4%);
-    }
-  }
-
+.mute-modal__action-bar,
+.block-modal__action-bar {
   .confirmation-modal__secondary-button {
     flex-shrink: 1;
   }
 }
 
+.confirmation-modal__secondary-button,
+.confirmation-modal__cancel-button,
+.mute-modal__cancel-button,
+.block-modal__cancel-button {
+  background-color: transparent;
+  color: $lighter-text-color;
+  font-size: 14px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus,
+  &:active {
+    color: darken($lighter-text-color, 4%);
+  }
+}
+
 .confirmation-modal__do_not_ask_again {
   padding-left: 20px;
   padding-right: 20px;
@@ -747,10 +753,10 @@
 
 .confirmation-modal__container,
 .mute-modal__container,
+.block-modal__container,
 .report-modal__target {
   padding: 30px;
   font-size: 16px;
-  text-align: center;
 
   strong {
     font-weight: 500;
@@ -763,6 +769,31 @@
   }
 }
 
+.confirmation-modal__container,
+.report-modal__target {
+  text-align: center;
+}
+
+.block-modal,
+.mute-modal {
+  &__explanation {
+    margin-top: 20px;
+  }
+
+  .setting-toggle {
+    margin-top: 20px;
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+
+    &__label {
+      color: $inverted-text-color;
+      margin: 0;
+      margin-left: 8px;
+    }
+  }
+}
+
 .report-modal__target {
   padding: 15px;
 
diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss
index c3ea47eb0..30d69d05c 100644
--- a/app/javascript/flavours/glitch/styles/components/search.scss
+++ b/app/javascript/flavours/glitch/styles/components/search.scss
@@ -10,6 +10,28 @@
   padding-right: 30px;
   line-height: 18px;
   font-size: 16px;
+
+  &::placeholder {
+    color: lighten($darker-text-color, 4%);
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+
+  &:focus {
+    background: lighten($ui-base-color, 4%);
+  }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
 }
 
 .search__icon {
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 24ab71969..ae89ac0a8 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -673,6 +673,7 @@ a.status__display-name,
 }
 
 .muted {
+  .status__content,
   .status__content p,
   .status__content a,
   .status__content__text {
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index 17455ca58..6a48ff354 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -143,6 +143,63 @@
     grid-row: 3;
   }
 
+  @media screen and (max-width: $no-gap-breakpoint) {
+    grid-gap: 0;
+    grid-template-columns: minmax(0, 100%);
+
+    .column-0 {
+      grid-column: 1;
+    }
+
+    .column-1 {
+      grid-column: 1;
+      grid-row: 3;
+    }
+
+    .column-2 {
+      grid-column: 1;
+      grid-row: 2;
+    }
+
+    .column-3 {
+      grid-column: 1;
+      grid-row: 4;
+    }
+  }
+}
+
+.grid-4 {
+  display: grid;
+  grid-gap: 10px;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  grid-auto-columns: 25%;
+  grid-auto-rows: max-content;
+
+  .column-0 {
+    grid-column: 1 / 5;
+    grid-row: 1;
+  }
+
+  .column-1 {
+    grid-column: 1 / 4;
+    grid-row: 2;
+  }
+
+  .column-2 {
+    grid-column: 4;
+    grid-row: 2;
+  }
+
+  .column-3 {
+    grid-column: 2 / 5;
+    grid-row: 3;
+  }
+
+  .column-4 {
+    grid-column: 1;
+    grid-row: 3;
+  }
+
   .landing-page__call-to-action {
     min-height: 100%;
   }
@@ -192,6 +249,11 @@
 
     .column-3 {
       grid-column: 1;
+      grid-row: 5;
+    }
+
+    .column-4 {
+      grid-column: 1;
       grid-row: 4;
     }
   }
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index dae29a003..747c5309d 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -245,6 +245,10 @@ code {
       &-6 {
         max-width: 50%;
       }
+
+      .actions {
+        margin-top: 27px;
+      }
     }
 
     .fields-group:last-child,
@@ -300,6 +304,13 @@ code {
     }
   }
 
+  .input.static .label_input__wrapper {
+    font-size: 16px;
+    padding: 10px;
+    border: 1px solid $dark-text-color;
+    border-radius: 4px;
+  }
+
   input[type=text],
   input[type=number],
   input[type=email],
@@ -318,6 +329,10 @@ code {
     border-radius: 4px;
     padding: 10px;
 
+    &::placeholder {
+      color: lighten($darker-text-color, 4%);
+    }
+
     &:invalid {
       box-shadow: none;
     }
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
index 4c2b76a21..5c7fa87da 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
@@ -226,6 +226,7 @@
 .boost-modal,
 .confirmation-modal,
 .mute-modal,
+.block-modal,
 .report-modal,
 .embed-modal,
 .error-modal,
@@ -236,6 +237,7 @@
 .boost-modal__action-bar,
 .confirmation-modal__action-bar,
 .mute-modal__action-bar,
+.block-modal__action-bar,
 .onboarding-modal__paginator,
 .error-modal__footer {
   background: darken($ui-base-color, 6%);
diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss
index 06f60408d..95d8e510c 100644
--- a/app/javascript/flavours/glitch/styles/polls.scss
+++ b/app/javascript/flavours/glitch/styles/polls.scss
@@ -102,13 +102,19 @@
 
   &__number {
     display: inline-block;
-    width: 36px;
+    width: 52px;
     font-weight: 700;
     padding: 0 10px;
+    padding-left: 8px;
     text-align: right;
     margin-top: auto;
     margin-bottom: auto;
-    flex: 0 0 36px;
+    flex: 0 0 52px;
+  }
+
+  &__vote__mark {
+    float: left;
+    line-height: 18px;
   }
 
   &__footer {
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index 669f72787..b84f6a708 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -288,70 +288,3 @@ a.table-action-link {
     }
   }
 }
-
-.blocks-table {
-  width: 100%;
-  max-width: 100%;
-  border-spacing: 0;
-  border-collapse: collapse;
-  table-layout: fixed;
-  border: 1px solid darken($ui-base-color, 8%);
-
-  thead {
-    border: 1px solid darken($ui-base-color, 8%);
-    background: darken($ui-base-color, 4%);
-    font-weight: 500;
-
-    th.severity-column {
-      width: 120px;
-    }
-
-    th.button-column {
-      width: 23px;
-    }
-  }
-
-  tbody > tr {
-    border: 1px solid darken($ui-base-color, 8%);
-    border-bottom: 0;
-    background: darken($ui-base-color, 4%);
-
-    &:hover {
-      background: darken($ui-base-color, 2%);
-    }
-
-    &.even {
-      background: $ui-base-color;
-
-      &:hover {
-        background: lighten($ui-base-color, 2%);
-      }
-    }
-
-    &.rationale {
-      background: lighten($ui-base-color, 4%);
-      border-top: 0;
-
-      &:hover {
-        background: lighten($ui-base-color, 6%);
-      }
-
-      &.hidden {
-        display: none;
-      }
-    }
-
-    td:first-child {
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-  }
-
-  th,
-  td {
-    padding: 8px;
-    line-height: 18px;
-    vertical-align: top;
-    text-align: left;
-  }
-}
diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss
index 9e9c6eb58..a6f7fc0be 100644
--- a/app/javascript/flavours/glitch/styles/widgets.scss
+++ b/app/javascript/flavours/glitch/styles/widgets.scss
@@ -128,41 +128,43 @@
   margin-bottom: 10px;
 }
 
-.contact-widget,
-.landing-page__information.contact-widget {
-  box-sizing: border-box;
-  padding: 20px;
-  min-height: 100%;
-  border-radius: 4px;
-  background: $ui-base-color;
-  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-}
-
 .contact-widget {
+  min-height: 100%;
   font-size: 15px;
   color: $darker-text-color;
   line-height: 20px;
   word-wrap: break-word;
   font-weight: 400;
+  padding: 0;
 
-  strong {
-    font-weight: 500;
+  h4 {
+    padding: 10px;
+    text-transform: uppercase;
+    font-weight: 700;
+    font-size: 13px;
+    color: $darker-text-color;
   }
 
-  p {
-    margin-bottom: 10px;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
+  .account {
+    border-bottom: 0;
+    padding: 10px 0;
+    padding-top: 5px;
   }
 
-  &__mail {
-    margin-top: 10px;
+  & > a {
+    display: inline-block;
+    padding: 10px;
+    padding-top: 0;
+    color: $darker-text-color;
+    text-decoration: none;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
-    a {
-      color: $primary-text-color;
-      text-decoration: none;
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
     }
   }
 }
@@ -557,3 +559,38 @@ $fluid-breakpoint: $maximum-width + 20px;
     }
   }
 }
+
+.table-of-contents {
+  background: darken($ui-base-color, 4%);
+  min-height: 100%;
+  font-size: 14px;
+  border-radius: 4px;
+
+  li a {
+    display: block;
+    font-weight: 500;
+    padding: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    text-decoration: none;
+    color: $primary-text-color;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+
+  li:last-child a {
+    border-bottom: 0;
+  }
+
+  li ul {
+    padding-left: 20px;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+  }
+}
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
index 06e26ade2..0fd627f19 100644
--- a/app/javascript/flavours/glitch/theme.yml
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -18,7 +18,7 @@ pack:
   mailer:
   modal:
   public: packs/public.js
-  settings:
+  settings: packs/settings.js
   share: packs/share.js
 
 #  (OPTIONAL) The directory which contains localization files for
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 6c0acdb27..26255bbb7 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -122,6 +122,10 @@ export function MuteModal () {
   return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal');
 }
 
+export function BlockModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'flavours/glitch/features/ui/components/block_modal');
+}
+
 export function ReportModal () {
   return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
 }
diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index b2d13cc95..e1a244127 100644
--- a/app/javascript/flavours/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -100,4 +100,4 @@ export const buildCustomEmojis = (customEmojis) => {
   return emojis;
 };
 
-export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set());
+export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));