about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2022-02-25 12:35:11 -0600
committerStarfall <us@starfall.systems>2022-02-25 12:35:11 -0600
commitad1733ea294c6049336a9aeeb7ff96c8fea22cfa (patch)
tree306ff2d36a8bce82039890c4327f7d7bf37583dc /app/javascript/flavours
parentc5f289e8ef7ec1592d068ac797add7332343820d (diff)
parente48eaf64cc7cb0cfab388331c4823ee5fb580d59 (diff)
Merge remote-tracking branch 'glitch/main'
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js3
-rw-r--r--app/javascript/flavours/glitch/actions/reports.js115
-rw-r--r--app/javascript/flavours/glitch/actions/rules.js27
-rw-r--r--app/javascript/flavours/glitch/components/check.js9
-rw-r--r--app/javascript/flavours/glitch/features/emoji_picker/index.js15
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/admin_signup.js101
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/column_settings.js16
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notification.js14
-rw-r--r--app/javascript/flavours/glitch/features/report/category.js93
-rw-r--r--app/javascript/flavours/glitch/features/report/comment.js83
-rw-r--r--app/javascript/flavours/glitch/features/report/components/option.js60
-rw-r--r--app/javascript/flavours/glitch/features/report/components/status_check_box.js56
-rw-r--r--app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js22
-rw-r--r--app/javascript/flavours/glitch/features/report/rules.js64
-rw-r--r--app/javascript/flavours/glitch/features/report/statuses.js58
-rw-r--r--app/javascript/flavours/glitch/features/report/thanks.js84
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js239
-rw-r--r--app/javascript/flavours/glitch/packs/public.js8
-rw-r--r--app/javascript/flavours/glitch/packs/settings.js8
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js4
-rw-r--r--app/javascript/flavours/glitch/reducers/reports.js77
-rw-r--r--app/javascript/flavours/glitch/reducers/rules.js13
-rw-r--r--app/javascript/flavours/glitch/reducers/settings.js3
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss113
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss4
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss22
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss186
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss51
-rw-r--r--app/javascript/flavours/glitch/styles/footer.scss17
29 files changed, 1227 insertions, 338 deletions
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 40430102c..42ad39efa 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -57,7 +57,7 @@ defineMessages({
 });
 
 const fetchRelatedRelationships = (dispatch, notifications) => {
-  const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
+  const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
 
   if (accountIds > 0) {
     dispatch(fetchRelationships(accountIds));
@@ -144,6 +144,7 @@ const excludeTypesFromFilter = filter => {
     'poll',
     'status',
     'update',
+    'admin.sign_up',
   ]);
 
   return allTypes.filterNot(item => item === filter).toJS();
diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js
index 80c3b3280..333bc71f4 100644
--- a/app/javascript/flavours/glitch/actions/reports.js
+++ b/app/javascript/flavours/glitch/actions/reports.js
@@ -1,89 +1,38 @@
 import api from 'flavours/glitch/util/api';
-import { openModal, closeModal } from './modal';
-
-export const REPORT_INIT   = 'REPORT_INIT';
-export const REPORT_CANCEL = 'REPORT_CANCEL';
+import { openModal } from './modal';
 
 export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
 export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
 export const REPORT_SUBMIT_FAIL    = 'REPORT_SUBMIT_FAIL';
 
-export const REPORT_STATUS_TOGGLE  = 'REPORT_STATUS_TOGGLE';
-export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
-export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
-
-export function initReport(account, status) {
-  return dispatch => {
-    dispatch({
-      type: REPORT_INIT,
-      account,
-      status,
-    });
-
-    dispatch(openModal('REPORT'));
-  };
-};
-
-export function cancelReport() {
-  return {
-    type: REPORT_CANCEL,
-  };
-};
-
-export function toggleStatusReport(statusId, checked) {
-  return {
-    type: REPORT_STATUS_TOGGLE,
-    statusId,
-    checked,
-  };
-};
-
-export function submitReport() {
-  return (dispatch, getState) => {
-    dispatch(submitReportRequest());
-
-    api(getState).post('/api/v1/reports', {
-      account_id: getState().getIn(['reports', 'new', 'account_id']),
-      status_ids: getState().getIn(['reports', 'new', 'status_ids']),
-      comment: getState().getIn(['reports', 'new', 'comment']),
-      forward: getState().getIn(['reports', 'new', 'forward']),
-    }).then(response => {
-      dispatch(closeModal());
-      dispatch(submitReportSuccess(response.data));
-    }).catch(error => dispatch(submitReportFail(error)));
-  };
-};
-
-export function submitReportRequest() {
-  return {
-    type: REPORT_SUBMIT_REQUEST,
-  };
-};
-
-export function submitReportSuccess(report) {
-  return {
-    type: REPORT_SUBMIT_SUCCESS,
-    report,
-  };
-};
-
-export function submitReportFail(error) {
-  return {
-    type: REPORT_SUBMIT_FAIL,
-    error,
-  };
-};
-
-export function changeReportComment(comment) {
-  return {
-    type: REPORT_COMMENT_CHANGE,
-    comment,
-  };
-};
-
-export function changeReportForward(forward) {
-  return {
-    type: REPORT_FORWARD_CHANGE,
-    forward,
-  };
-};
+export const initReport = (account, status) => dispatch =>
+  dispatch(openModal('REPORT', {
+    accountId: account.get('id'),
+    statusId: status?.get('id'),
+  }));
+
+export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
+  dispatch(submitReportRequest());
+
+  api(getState).post('/api/v1/reports', params).then(response => {
+    dispatch(submitReportSuccess(response.data));
+    if (onSuccess) onSuccess();
+  }).catch(error => {
+    dispatch(submitReportFail(error));
+    if (onFail) onFail();
+  });
+};
+
+export const submitReportRequest = () => ({
+  type: REPORT_SUBMIT_REQUEST,
+});
+
+export const submitReportSuccess = report => ({
+  type: REPORT_SUBMIT_SUCCESS,
+  report,
+});
+
+export const submitReportFail = error => ({
+  type: REPORT_SUBMIT_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/glitch/actions/rules.js b/app/javascript/flavours/glitch/actions/rules.js
new file mode 100644
index 000000000..b95045e81
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/rules.js
@@ -0,0 +1,27 @@
+import api from 'flavours/glitch/util/api';
+
+export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
+export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
+export const RULES_FETCH_FAIL    = 'RULES_FETCH_FAIL';
+
+export const fetchRules = () => (dispatch, getState) => {
+  dispatch(fetchRulesRequest());
+
+  api(getState)
+    .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
+    .catch(err => dispatch(fetchRulesFail(err)));
+};
+
+const fetchRulesRequest = () => ({
+  type: RULES_FETCH_REQUEST,
+});
+
+const fetchRulesSuccess = rules => ({
+  type: RULES_FETCH_SUCCESS,
+  rules,
+});
+
+const fetchRulesFail = error => ({
+  type: RULES_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/flavours/glitch/components/check.js b/app/javascript/flavours/glitch/components/check.js
new file mode 100644
index 000000000..ee2ef1595
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/check.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const Check = () => (
+  <svg width='14' height='11' viewBox='0 0 14 11'>
+    <path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
+  </svg>
+);
+
+export default Check;
diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 78f691c98..5de9fe107 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -250,7 +250,7 @@ class EmojiPickerMenu extends React.PureComponent {
 
   state = {
     modifierOpen: false,
-    placement: null,
+    readyToFocus: false,
   };
 
   handleDocumentClick = e => {
@@ -262,6 +262,16 @@ class EmojiPickerMenu extends React.PureComponent {
   componentDidMount () {
     document.addEventListener('click', this.handleDocumentClick, false);
     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
+    // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
+    // to wait for a frame before focusing
+    requestAnimationFrame(() => {
+      this.setState({ readyToFocus: true });
+      if (this.node) {
+        const element = this.node.querySelector('input[type="search"]');
+        if (element) element.focus();
+      }
+    });
   }
 
   componentWillUnmount () {
@@ -361,7 +371,7 @@ class EmojiPickerMenu extends React.PureComponent {
           showSkinTones={false}
           backgroundImageFn={backgroundImageFn}
           notFound={notFoundFn}
-          autoFocus
+          autoFocus={this.state.readyToFocus}
           emojiTooltip
           native={useSystemEmojiFont}
         />
@@ -396,6 +406,7 @@ class EmojiPickerDropdown extends React.PureComponent {
   state = {
     active: false,
     loading: false,
+    placement: null,
   };
 
   setRef = (c) => {
diff --git a/app/javascript/flavours/glitch/features/notifications/components/admin_signup.js b/app/javascript/flavours/glitch/features/notifications/components/admin_signup.js
new file mode 100644
index 000000000..355ebef94
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/notifications/components/admin_signup.js
@@ -0,0 +1,101 @@
+//  Package imports.
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { HotKeys } from 'react-hotkeys';
+import classNames from 'classnames';
+
+// Our imports.
+import Permalink from 'flavours/glitch/components/permalink';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import NotificationOverlayContainer from '../containers/overlay_container';
+import Icon from 'flavours/glitch/components/icon';
+
+export default class NotificationFollow extends ImmutablePureComponent {
+
+  static propTypes = {
+    hidden: PropTypes.bool,
+    id: PropTypes.string.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    notification: ImmutablePropTypes.map.isRequired,
+    unread: PropTypes.bool,
+  };
+
+  handleMoveUp = () => {
+    const { notification, onMoveUp } = this.props;
+    onMoveUp(notification.get('id'));
+  }
+
+  handleMoveDown = () => {
+    const { notification, onMoveDown } = this.props;
+    onMoveDown(notification.get('id'));
+  }
+
+  handleOpen = () => {
+    this.handleOpenProfile();
+  }
+
+  handleOpenProfile = () => {
+    const { notification } = this.props;
+    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
+  }
+
+  handleMention = e => {
+    e.preventDefault();
+
+    const { notification, onMention } = this.props;
+    onMention(notification.get('account'), this.context.router.history);
+  }
+
+  getHandlers () {
+    return {
+      moveUp: this.handleMoveUp,
+      moveDown: this.handleMoveDown,
+      open: this.handleOpen,
+      openProfile: this.handleOpenProfile,
+      mention: this.handleMention,
+      reply: this.handleMention,
+    };
+  }
+
+  render () {
+    const { account, notification, hidden, unread } = this.props;
+
+    //  Links to the display name.
+    const displayName = account.get('display_name_html') || account.get('username');
+    const link = (
+      <bdi><Permalink
+        className='notification__display-name'
+        href={account.get('url')}
+        title={account.get('acct')}
+        to={`/@${account.get('acct')}`}
+        dangerouslySetInnerHTML={{ __html: displayName }}
+      /></bdi>
+    );
+
+    //  Renders.
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex='0'>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon fixedWidth id='user-plus' />
+            </div>
+
+            <FormattedMessage
+              id='notification.admin.sign_up'
+              defaultMessage='{name} signed up'
+              values={{ name: link }}
+            />
+          </div>
+
+          <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
+          <NotificationOverlayContainer notification={notification} />
+        </div>
+      </HotKeys>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
index 569ba4db0..0dad079ad 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
@@ -6,6 +6,7 @@ import ClearColumnButton from './clear_column_button';
 import GrantPermissionButton from './grant_permission_button';
 import SettingToggle from './setting_toggle';
 import PillBarButton from './pill_bar_button';
+import { isStaff } from 'flavours/glitch/util/initial_state';
 
 export default class ColumnSettings extends React.PureComponent {
 
@@ -156,7 +157,7 @@ export default class ColumnSettings extends React.PureComponent {
         </div>
 
         <div role='group' aria-labelledby='notifications-update'>
-          <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
+          <span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
 
           <div className='column-settings__pillbar'>
             <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
@@ -165,6 +166,19 @@ export default class ColumnSettings extends React.PureComponent {
             <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
           </div>
         </div>
+
+        {isStaff && (
+          <div role='group' aria-labelledby='notifications-admin-sign-up'>
+            <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
+
+            <div className='column-settings__pillbar'>
+              <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
+              {showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.sign_up']} onChange={this.onPushChange} label={pushStr} />}
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
+              <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
+            </div>
+          </div>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js
index 1cf205898..e0cd3c7a6 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notification.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js
@@ -8,6 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import StatusContainer from 'flavours/glitch/containers/status_container';
 import NotificationFollow from './follow';
 import NotificationFollowRequestContainer from '../containers/follow_request_container';
+import NotificationAdminSignup from './admin_signup';
 
 export default class Notification extends ImmutablePureComponent {
 
@@ -63,6 +64,19 @@ export default class Notification extends ImmutablePureComponent {
           unread={this.props.unread}
         />
       );
+    case 'admin.sign_up':
+      return (
+        <NotificationAdminSignup
+          hidden={hidden}
+          id={notification.get('id')}
+          account={notification.get('account')}
+          notification={notification}
+          onMoveDown={onMoveDown}
+          onMoveUp={onMoveUp}
+          onMention={onMention}
+          unread={this.props.unread}
+        />
+      );
     case 'mention':
       return (
         <StatusContainer
diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js
new file mode 100644
index 000000000..ddbc82563
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/category.js
@@ -0,0 +1,93 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+import Option from './components/option';
+
+const messages = defineMessages({
+  dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
+  dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
+  spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
+  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
+  violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
+  violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
+  other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
+  other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' },
+  status: { id: 'report.category.title_status', defaultMessage: 'post' },
+  account: { id: 'report.category.title_account', defaultMessage: 'profile' },
+});
+
+export default @injectIntl
+class Category extends React.PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    category: PropTypes.string,
+    onChangeCategory: PropTypes.func.isRequired,
+    startedFrom: PropTypes.oneOf(['status', 'account']),
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep, category } = this.props;
+
+    switch(category) {
+    case 'dislike':
+      onNextStep('thanks');
+      break;
+    case 'violation':
+      onNextStep('rules');
+      break;
+    default:
+      onNextStep('statuses');
+      break;
+    }
+  };
+
+  handleCategoryToggle = (value, checked) => {
+    const { onChangeCategory } = this.props;
+
+    if (checked) {
+      onChangeCategory(value);
+    }
+  };
+
+  render () {
+    const { category, startedFrom, intl } = this.props;
+
+    const options = [
+      'dislike',
+      'spam',
+      'violation',
+      'other',
+    ];
+
+    return (
+      <React.Fragment>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p>
+
+        <div>
+          {options.map(item => (
+            <Option
+              key={item}
+              name='category'
+              value={item}
+              checked={category === item}
+              onToggle={this.handleCategoryToggle}
+              label={intl.formatMessage(messages[item])}
+              description={intl.formatMessage(messages[`${item}_description`])}
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/report/comment.js b/app/javascript/flavours/glitch/features/report/comment.js
new file mode 100644
index 000000000..b2663bbf2
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/comment.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+import Toggle from 'react-toggle';
+
+const messages = defineMessages({
+  placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
+});
+
+export default @injectIntl
+class Comment extends React.PureComponent {
+
+  static propTypes = {
+    onSubmit: PropTypes.func.isRequired,
+    comment: PropTypes.string.isRequired,
+    onChangeComment: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    isSubmitting: PropTypes.bool,
+    forward: PropTypes.bool,
+    isRemote: PropTypes.bool,
+    domain: PropTypes.string,
+    onChangeForward: PropTypes.func.isRequired,
+  };
+
+  handleClick = () => {
+    const { onSubmit } = this.props;
+    onSubmit();
+  };
+
+  handleChange = e => {
+    const { onChangeComment } = this.props;
+    onChangeComment(e.target.value);
+  };
+
+  handleKeyDown = e => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      this.handleClick();
+    }
+  };
+
+  handleForwardChange = e => {
+    const { onChangeForward } = this.props;
+    onChangeForward(e.target.checked);
+  };
+
+  render () {
+    const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
+
+    return (
+      <React.Fragment>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
+
+        <textarea
+          className='report-dialog-modal__textarea'
+          placeholder={intl.formatMessage(messages.placeholder)}
+          value={comment}
+          onChange={this.handleChange}
+          onKeyDown={this.handleKeyDown}
+          disabled={isSubmitting}
+        />
+
+        {isRemote && (
+          <React.Fragment>
+            <p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
+
+            <label className='report-dialog-modal__toggle'>
+              <Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
+              <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
+            </label>
+          </React.Fragment>
+        )}
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleClick}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/report/components/option.js b/app/javascript/flavours/glitch/features/report/components/option.js
new file mode 100644
index 000000000..7e94f0654
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/components/option.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Check from 'flavours/glitch/components/check';
+
+export default class Option extends React.PureComponent {
+
+  static propTypes = {
+    name: PropTypes.string.isRequired,
+    value: PropTypes.string.isRequired,
+    checked: PropTypes.bool,
+    label: PropTypes.node,
+    description: PropTypes.node,
+    onToggle: PropTypes.func,
+    multiple: PropTypes.bool,
+    labelComponent: PropTypes.node,
+  };
+
+  handleKeyPress = e => {
+    const { value, checked, onToggle } = this.props;
+
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.stopPropagation();
+      e.preventDefault();
+      onToggle(value, !checked);
+    }
+  }
+
+  handleChange = e => {
+    const { value, onToggle } = this.props;
+    onToggle(value, e.target.checked);
+  }
+
+  render () {
+    const { name, value, checked, label, labelComponent, description, multiple } = this.props;
+
+    return (
+      <label className='dialog-option poll__option selectable'>
+        <input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} />
+
+        <span
+          className={classNames('poll__input', { active: checked, checkbox: multiple })}
+          tabIndex='0'
+          role='radio'
+          onKeyPress={this.handleKeyPress}
+          aria-checked={checked}
+          aria-label={label}
+        >{checked && <Check />}</span>
+
+        {labelComponent ? labelComponent : (
+          <span className='poll__option__text'>
+            <strong>{label}</strong>
+            {description}
+          </span>
+        )}
+      </label>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
index cc49042fc..adb5e77a7 100644
--- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
@@ -1,23 +1,32 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Toggle from 'react-toggle';
 import noop from 'lodash/noop';
 import StatusContent from 'flavours/glitch/components/status_content';
 import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import Bundle from 'flavours/glitch/features/ui/components/bundle';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import Option from './option';
 
 export default class StatusCheckBox extends React.PureComponent {
 
   static propTypes = {
+    id: PropTypes.string.isRequired,
     status: ImmutablePropTypes.map.isRequired,
     checked: PropTypes.bool,
     onToggle: PropTypes.func.isRequired,
-    disabled: PropTypes.bool,
+  };
+
+  handleStatusesToggle = (value, checked) => {
+    const { onToggle } = this.props;
+    onToggle(value, checked);
   };
 
   render () {
-    const { status, checked, onToggle, disabled } = this.props;
+    const { status, checked } = this.props;
+
     let media = null;
 
     if (status.get('reblog')) {
@@ -51,26 +60,45 @@ export default class StatusCheckBox extends React.PureComponent {
       } else {
         media = (
           <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
-            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} revealed={false} height={110} onOpenMedia={noop} />}
+            {Component => (
+              <Component
+                media={status.get('media_attachments')}
+                sensitive={status.get('sensitive')}
+                revealed={false}
+                height={110}
+                onOpenMedia={noop}
+              />
+            )}
           </Bundle>
         );
       }
     }
 
-    return (
-      <div className='status-check-box'>
-        <div className='status-check-box__status'>
-          <StatusContent
-            status={status}
-            media={media}
-          />
-        </div>
+    const labelComponent = (
+      <div className='status-check-box__status poll__option__text'>
+        <div className='detailed-status__display-name'>
+          <div className='detailed-status__display-avatar'>
+            <Avatar account={status.get('account')} size={46} />
+          </div>
 
-        <div className='status-check-box-toggle'>
-          <Toggle checked={checked} onChange={onToggle} disabled={disabled} />
+          <div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
         </div>
+
+        <StatusContent status={status} media={media} />
       </div>
     );
+
+    return (
+      <Option
+        name='status_ids'
+        value={status.get('id')}
+        checked={checked}
+        onToggle={this.handleStatusesToggle}
+        label={status.get('search_index')}
+        labelComponent={labelComponent}
+        multiple
+      />
+    );
   }
 
 }
diff --git a/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
index 9bfd41ffc..aa34b3efd 100644
--- a/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
+++ b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
@@ -1,19 +1,15 @@
 import { connect } from 'react-redux';
 import StatusCheckBox from '../components/status_check_box';
-import { toggleStatusReport } from 'flavours/glitch/actions/reports';
-import { Set as ImmutableSet } from 'immutable';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 
-const mapStateToProps = (state, { id }) => ({
-  status: state.getIn(['statuses', id]),
-  checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
-});
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
 
-const mapDispatchToProps = (dispatch, { id }) => ({
+  const mapStateToProps = (state, { id }) => ({
+    status: getStatus(state, { id }),
+  });
 
-  onToggle (e) {
-    dispatch(toggleStatusReport(id, e.target.checked));
-  },
+  return mapStateToProps;
+};
 
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
+export default connect(makeMapStateToProps)(StatusCheckBox);
diff --git a/app/javascript/flavours/glitch/features/report/rules.js b/app/javascript/flavours/glitch/features/report/rules.js
new file mode 100644
index 000000000..4772e04a2
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/rules.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+import Option from './components/option';
+
+const mapStateToProps = state => ({
+  rules: state.get('rules'),
+});
+
+export default @connect(mapStateToProps)
+class Rules extends React.PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    rules: ImmutablePropTypes.list,
+    selectedRuleIds: ImmutablePropTypes.set.isRequired,
+    onToggle: PropTypes.func.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep } = this.props;
+    onNextStep('statuses');
+  };
+
+  handleRulesToggle = (value, checked) => {
+    const { onToggle } = this.props;
+    onToggle(value, checked);
+  };
+
+  render () {
+    const { rules, selectedRuleIds } = this.props;
+
+    return (
+      <React.Fragment>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p>
+
+        <div>
+          {rules.map(item => (
+            <Option
+              key={item.get('id')}
+              name='rule_ids'
+              value={item.get('id')}
+              checked={selectedRuleIds.includes(item.get('id'))}
+              onToggle={this.handleRulesToggle}
+              label={item.get('text')}
+              multiple
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/report/statuses.js b/app/javascript/flavours/glitch/features/report/statuses.js
new file mode 100644
index 000000000..69cfbb3e5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/statuses.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
+import { OrderedSet } from 'immutable';
+import { FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+
+const mapStateToProps = (state, { accountId }) => ({
+  availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
+});
+
+export default @connect(mapStateToProps)
+class Statuses extends React.PureComponent {
+
+  static propTypes = {
+    onNextStep: PropTypes.func.isRequired,
+    accountId: PropTypes.string.isRequired,
+    availableStatusIds: ImmutablePropTypes.set.isRequired,
+    selectedStatusIds: ImmutablePropTypes.set.isRequired,
+    onToggle: PropTypes.func.isRequired,
+  };
+
+  handleNextClick = () => {
+    const { onNextStep } = this.props;
+    onNextStep('comment');
+  };
+
+  render () {
+    const { availableStatusIds, selectedStatusIds, onToggle } = this.props;
+
+    return (
+      <React.Fragment>
+        <h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p>
+
+        <div className='report-dialog-modal__statuses'>
+          {availableStatusIds.union(selectedStatusIds).map(statusId => (
+            <StatusCheckBox
+              id={statusId}
+              key={statusId}
+              checked={selectedStatusIds.includes(statusId)}
+              onToggle={onToggle}
+            />
+          ))}
+        </div>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/report/thanks.js b/app/javascript/flavours/glitch/features/report/thanks.js
new file mode 100644
index 000000000..9c41baa7f
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/report/thanks.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/components/button';
+import { connect } from 'react-redux';
+import {
+  unfollowAccount,
+  muteAccount,
+  blockAccount,
+} from 'mastodon/actions/accounts';
+
+const mapStateToProps = () => ({});
+
+export default @connect(mapStateToProps)
+class Thanks extends React.PureComponent {
+
+  static propTypes = {
+    submitted: PropTypes.bool,
+    onClose: PropTypes.func.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  handleCloseClick = () => {
+    const { onClose } = this.props;
+    onClose();
+  };
+
+  handleUnfollowClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(unfollowAccount(account.get('id')));
+    onClose();
+  };
+
+  handleMuteClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(muteAccount(account.get('id')));
+    onClose();
+  };
+
+  handleBlockClick = () => {
+    const { dispatch, account, onClose } = this.props;
+    dispatch(blockAccount(account.get('id')));
+    onClose();
+  };
+
+  render () {
+    const { account, submitted } = this.props;
+
+    return (
+      <React.Fragment>
+        <h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3>
+        <p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p>
+
+        {account.getIn(['relationship', 'following']) && (
+          <React.Fragment>
+            <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4>
+            <p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p>
+            <Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button>
+            <hr />
+          </React.Fragment>
+        )}
+
+        <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p>
+        <Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button>
+
+        <hr />
+
+        <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4>
+        <p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p>
+        <Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button>
+
+        <div className='flex-spacer' />
+
+        <div className='report-dialog-modal__actions'>
+          <Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
index 5cb7c5d07..dcbe94929 100644
--- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -1,38 +1,32 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { changeReportComment, changeReportForward, submitReport } from 'flavours/glitch/actions/reports';
+import { submitReport } from 'flavours/glitch/actions/reports';
 import { expandAccountTimeline } from 'flavours/glitch/actions/timelines';
+import { fetchRules } from 'flavours/glitch/actions/rules';
+import { fetchRelationships } from 'flavours/glitch/actions/accounts';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { makeGetAccount } from 'flavours/glitch/selectors';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
 import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Button from 'flavours/glitch/components/button';
-import Toggle from 'react-toggle';
-import IconButton from '../../../components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Category from 'flavours/glitch/features/report/category';
+import Statuses from 'flavours/glitch/features/report/statuses';
+import Rules from 'flavours/glitch/features/report/rules';
+import Comment from 'flavours/glitch/features/report/comment';
+import Thanks from 'flavours/glitch/features/report/thanks';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
-  placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
-  submit: { id: 'report.submit', defaultMessage: 'Submit' },
 });
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
 
-  const mapStateToProps = state => {
-    const accountId = state.getIn(['reports', 'new', 'account_id']);
-
-    return {
-      isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
-      account: getAccount(state, accountId),
-      comment: state.getIn(['reports', 'new', 'comment']),
-      forward: state.getIn(['reports', 'new', 'forward']),
-      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
-    };
-  };
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, accountId),
+  });
 
   return mapStateToProps;
 };
@@ -42,92 +36,183 @@ export default @connect(makeMapStateToProps)
 class ReportModal extends ImmutablePureComponent {
 
   static propTypes = {
-    isSubmitting: PropTypes.bool,
-    account: ImmutablePropTypes.map,
-    statusIds: ImmutablePropTypes.orderedSet.isRequired,
-    comment: PropTypes.string.isRequired,
-    forward: PropTypes.bool,
+    accountId: PropTypes.string.isRequired,
+    statusId: PropTypes.string,
     dispatch: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    account: ImmutablePropTypes.map.isRequired,
   };
 
-  handleCommentChange = e => {
-    this.props.dispatch(changeReportComment(e.target.value));
-  }
-
-  handleForwardChange = e => {
-    this.props.dispatch(changeReportForward(e.target.checked));
-  }
+  state = {
+    step: 'category',
+    selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
+    comment: '',
+    category: null,
+    selectedRuleIds: OrderedSet(),
+    forward: true,
+    isSubmitting: false,
+    isSubmitted: false,
+  };
 
   handleSubmit = () => {
-    this.props.dispatch(submitReport());
-  }
+    const { dispatch, accountId } = this.props;
+    const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state;
+
+    this.setState({ isSubmitting: true });
+
+    dispatch(submitReport({
+      account_id: accountId,
+      status_ids: selectedStatusIds.toArray(),
+      comment,
+      forward,
+      category,
+      rule_ids: selectedRuleIds.toArray(),
+    }, this.handleSuccess, this.handleFail));
+  };
+
+  handleSuccess = () => {
+    this.setState({ isSubmitting: false, isSubmitted: true, step: 'thanks' });
+  };
+
+  handleFail = () => {
+    this.setState({ isSubmitting: false });
+  };
+
+  handleStatusToggle = (statusId, checked) => {
+    const { selectedStatusIds } = this.state;
+
+    if (checked) {
+      this.setState({ selectedStatusIds: selectedStatusIds.add(statusId) });
+    } else {
+      this.setState({ selectedStatusIds: selectedStatusIds.remove(statusId) });
+    }
+  };
 
-  handleKeyDown = e => {
-    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
-      this.handleSubmit();
+  handleRuleToggle = (ruleId, checked) => {
+    const { selectedRuleIds } = this.state;
+
+    if (checked) {
+      this.setState({ selectedRuleIds: selectedRuleIds.add(ruleId) });
+    } else {
+      this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) });
     }
   }
 
+  handleChangeCategory = category => {
+    this.setState({ category });
+  };
+
+  handleChangeComment = comment => {
+    this.setState({ comment });
+  };
+
+  handleChangeForward = forward => {
+    this.setState({ forward });
+  };
+
+  handleNextStep = step => {
+    this.setState({ step });
+  };
+
   componentDidMount () {
-    this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true }));
-  }
+    const { dispatch, accountId } = this.props;
 
-  componentWillReceiveProps (nextProps) {
-    if (this.props.account !== nextProps.account && nextProps.account) {
-      this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true }));
-    }
+    dispatch(fetchRelationships([accountId]));
+    dispatch(expandAccountTimeline(accountId, { withReplies: true }));
+    dispatch(fetchRules());
   }
 
   render () {
-    const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props;
+    const {
+      accountId,
+      account,
+      intl,
+      onClose,
+    } = this.props;
 
     if (!account) {
       return null;
     }
 
-    const domain = account.get('acct').split('@')[1];
+    const {
+      step,
+      selectedStatusIds,
+      selectedRuleIds,
+      comment,
+      forward,
+      category,
+      isSubmitting,
+      isSubmitted,
+    } = this.state;
+
+    const domain   = account.get('acct').split('@')[1];
+    const isRemote = !!domain;
+
+    let stepComponent;
+
+    switch(step) {
+    case 'category':
+      stepComponent = (
+        <Category
+          onNextStep={this.handleNextStep}
+          startedFrom={this.props.statusId ? 'status' : 'account'}
+          category={category}
+          onChangeCategory={this.handleChangeCategory}
+        />
+      );
+      break;
+    case 'rules':
+      stepComponent = (
+        <Rules
+          onNextStep={this.handleNextStep}
+          selectedRuleIds={selectedRuleIds}
+          onToggle={this.handleRuleToggle}
+        />
+      );
+      break;
+    case 'statuses':
+      stepComponent = (
+        <Statuses
+          onNextStep={this.handleNextStep}
+          accountId={accountId}
+          selectedStatusIds={selectedStatusIds}
+          onToggle={this.handleStatusToggle}
+        />
+      );
+      break;
+    case 'comment':
+      stepComponent = (
+        <Comment
+          onSubmit={this.handleSubmit}
+          isSubmitting={isSubmitting}
+          isRemote={isRemote}
+          comment={comment}
+          forward={forward}
+          domain={domain}
+          onChangeComment={this.handleChangeComment}
+          onChangeForward={this.handleChangeForward}
+        />
+      );
+      break;
+    case 'thanks':
+      stepComponent = (
+        <Thanks
+          submitted={isSubmitted}
+          account={account}
+          onClose={onClose}
+        />
+      );
+    }
 
     return (
-      <div className='modal-root__modal report-modal'>
+      <div className='modal-root__modal report-dialog-modal'>
         <div className='report-modal__target'>
           <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
           <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
         </div>
 
-        <div className='report-modal__container'>
-          <div className='report-modal__comment'>
-            <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p>
-
-            <textarea
-              className='setting-text light'
-              placeholder={intl.formatMessage(messages.placeholder)}
-              value={comment}
-              onChange={this.handleCommentChange}
-              onKeyDown={this.handleKeyDown}
-              disabled={isSubmitting}
-              autoFocus
-            />
-
-            {domain && (
-              <div>
-                <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
-
-                <div className='setting-toggle'>
-                  <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
-                  <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label>
-                </div>
-              </div>
-            )}
-
-            <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
-          </div>
-
-          <div className='report-modal__statuses'>
-            <div>
-              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
-            </div>
-          </div>
+        <div className='report-dialog-modal__container'>
+          {stepComponent}
         </div>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index a92f3d5a8..84ec9fce7 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -147,13 +147,7 @@ function main() {
   });
 
   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';
-    }
+    document.querySelector('.sidebar ul').classList.toggle('visible');
   });
 
   // Empty the honeypot fields in JS in case something like an extension
diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js
index 9c4d119c1..0a53e1c25 100644
--- a/app/javascript/flavours/glitch/packs/settings.js
+++ b/app/javascript/flavours/glitch/packs/settings.js
@@ -7,13 +7,7 @@ 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';
-    }
+    document.querySelector('.sidebar ul').classList.toggle('visible');
   });
 }
 
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index d9123b103..92348c0c5 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -17,7 +17,7 @@ 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 rules from './rules';
 import boosts from './boosts';
 import contexts from './contexts';
 import compose from './compose';
@@ -64,7 +64,7 @@ const reducers = {
   push_notifications,
   mutes,
   blocks,
-  reports,
+  rules,
   boosts,
   contexts,
   compose,
diff --git a/app/javascript/flavours/glitch/reducers/reports.js b/app/javascript/flavours/glitch/reducers/reports.js
deleted file mode 100644
index 1f7f3f273..000000000
--- a/app/javascript/flavours/glitch/reducers/reports.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
-  REPORT_INIT,
-  REPORT_SUBMIT_REQUEST,
-  REPORT_SUBMIT_SUCCESS,
-  REPORT_SUBMIT_FAIL,
-  REPORT_CANCEL,
-  REPORT_STATUS_TOGGLE,
-  REPORT_COMMENT_CHANGE,
-  REPORT_FORWARD_CHANGE,
-} from 'flavours/glitch/actions/reports';
-import {
-  TIMELINE_DELETE,
-} from 'flavours/glitch/actions/timelines';
-import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
-
-const initialState = ImmutableMap({
-  new: ImmutableMap({
-    isSubmitting: false,
-    account_id: null,
-    status_ids: ImmutableSet(),
-    comment: '',
-    forward: false,
-  }),
-});
-
-const deleteStatus = (state, id, references) => {
-  references.forEach(ref => {
-    state = deleteStatus(state, ref[0], []);
-  });
-
-  return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.remove(id));
-};
-
-export default function reports(state = initialState, action) {
-  switch(action.type) {
-  case REPORT_INIT:
-    return state.withMutations(map => {
-      map.setIn(['new', 'isSubmitting'], false);
-      map.setIn(['new', 'account_id'], action.account.get('id'));
-
-      if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
-        map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
-        map.setIn(['new', 'comment'], '');
-      } else if (action.status) {
-        map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
-      }
-    });
-  case REPORT_STATUS_TOGGLE:
-    return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
-      if (action.checked) {
-        return set.add(action.statusId);
-      }
-
-      return set.remove(action.statusId);
-    });
-  case REPORT_COMMENT_CHANGE:
-    return state.setIn(['new', 'comment'], action.comment);
-  case REPORT_FORWARD_CHANGE:
-    return state.setIn(['new', 'forward'], action.forward);
-  case REPORT_SUBMIT_REQUEST:
-    return state.setIn(['new', 'isSubmitting'], true);
-  case REPORT_SUBMIT_FAIL:
-    return state.setIn(['new', 'isSubmitting'], false);
-  case REPORT_CANCEL:
-  case REPORT_SUBMIT_SUCCESS:
-    return state.withMutations(map => {
-      map.setIn(['new', 'account_id'], null);
-      map.setIn(['new', 'status_ids'], ImmutableSet());
-      map.setIn(['new', 'comment'], '');
-      map.setIn(['new', 'isSubmitting'], false);
-    });
-  case TIMELINE_DELETE:
-    return deleteStatus(state, action.id, action.references);
-  default:
-    return state;
-  }
-};
diff --git a/app/javascript/flavours/glitch/reducers/rules.js b/app/javascript/flavours/glitch/reducers/rules.js
new file mode 100644
index 000000000..6cc2230bc
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/rules.js
@@ -0,0 +1,13 @@
+import { RULES_FETCH_SUCCESS } from 'flavours/glitch/actions/rules';
+import { List as ImmutableList, fromJS } from 'immutable';
+
+const initialState = ImmutableList();
+
+export default function rules(state = initialState, action) {
+  switch (action.type) {
+  case RULES_FETCH_SUCCESS:
+    return fromJS(action.rules);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index 48587ce64..676a1ccc1 100644
--- a/app/javascript/flavours/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -41,6 +41,7 @@ const initialState = ImmutableMap({
       poll: false,
       status: false,
       update: false,
+      'admin.sign_up': false,
     }),
 
     quickFilter: ImmutableMap({
@@ -61,6 +62,7 @@ const initialState = ImmutableMap({
       poll: true,
       status: true,
       update: true,
+      'admin.sign_up': true,
     }),
 
     sounds: ImmutableMap({
@@ -72,6 +74,7 @@ const initialState = ImmutableMap({
       poll: true,
       status: true,
       update: true,
+      'admin.sign_up': true,
     }),
   }),
 
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 66ce92ce2..a1b99636c 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -322,6 +322,10 @@ $content-width: 840px;
 
       & > ul {
         display: none;
+
+        &.visible {
+          display: block;
+        }
       }
 
       ul a,
@@ -594,12 +598,16 @@ body,
 }
 
 .log-entry {
+  display: block;
   line-height: 20px;
   padding: 15px;
   padding-left: 15px * 2 + 40px;
   background: $ui-base-color;
   border-bottom: 1px solid darken($ui-base-color, 8%);
   position: relative;
+  text-decoration: none;
+  color: $darker-text-color;
+  font-size: 14px;
 
   &:first-child {
     border-top-left-radius: 4px;
@@ -612,15 +620,12 @@ body,
     border-bottom: 0;
   }
 
-  &:hover {
+  &:hover,
+  &:focus,
+  &:active {
     background: lighten($ui-base-color, 4%);
   }
 
-  &__header {
-    color: $darker-text-color;
-    font-size: 14px;
-  }
-
   &__avatar {
     position: absolute;
     left: 15px;
@@ -1278,6 +1283,30 @@ a.sparkline {
       background: linear-gradient(to left, $ui-base-color, transparent);
       pointer-events: none;
     }
+
+    a {
+      color: $secondary-text-color;
+      text-decoration: none;
+      unicode-bidi: isolate;
+
+      &:hover {
+        text-decoration: underline;
+
+        .fa {
+          color: lighten($dark-text-color, 7%);
+        }
+      }
+
+      &.mention {
+        &:hover {
+          text-decoration: none;
+
+          span {
+            text-decoration: underline;
+          }
+        }
+      }
+    }
   }
 
   &__actions {
@@ -1467,3 +1496,75 @@ a.sparkline {
     }
   }
 }
+
+.strike-card {
+  padding: 15px;
+  border-radius: 4px;
+  background: $ui-base-color;
+  font-size: 15px;
+  line-height: 20px;
+  word-wrap: break-word;
+  font-weight: 400;
+  color: $primary-text-color;
+
+  p {
+    margin-bottom: 20px;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    strong {
+      font-weight: 700;
+    }
+  }
+
+  &__rules {
+    list-style: disc;
+    padding-left: 15px;
+    margin-bottom: 20px;
+    color: $darker-text-color;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    &__text {
+      color: $primary-text-color;
+    }
+  }
+
+  &__statuses-list {
+    border-radius: 4px;
+    border: 1px solid darken($ui-base-color, 8%);
+    font-size: 13px;
+    line-height: 18px;
+    overflow: hidden;
+
+    &__item {
+      padding: 16px;
+      background: lighten($ui-base-color, 2%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:last-child {
+        border-bottom: 0;
+      }
+
+      &__meta {
+        color: $darker-text-color;
+      }
+
+      a {
+        color: inherit;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index fd62bb651..937751d00 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -652,14 +652,14 @@
   & > .primary {
     display: inline-block;
     margin: 0;
-    padding: 0 10px;
+    padding: 7px 10px;
     text-align: center;
   }
 
   & > .side_arm {
     display: inline-block;
     margin: 0 2px;
-    padding: 0;
+    padding: 7px 0;
     width: 36px;
     text-align: center;
   }
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 656d8f25d..55abd6e1e 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -41,16 +41,14 @@
   cursor: pointer;
   display: inline-block;
   font-family: inherit;
-  font-size: 14px;
+  font-size: 17px;
   font-weight: 500;
-  height: 36px;
   letter-spacing: 0;
-  line-height: 36px;
+  line-height: 22px;
   overflow: hidden;
-  padding: 0 16px;
+  padding: 7px 18px;
   position: relative;
   text-align: center;
-  text-transform: uppercase;
   text-decoration: none;
   text-overflow: ellipsis;
   transition: all 100ms ease-in;
@@ -82,17 +80,6 @@
     cursor: default;
   }
 
-  &.button-primary,
-  &.button-alternative,
-  &.button-secondary,
-  &.button-alternative-2 {
-    font-size: 16px;
-    line-height: 36px;
-    height: auto;
-    text-transform: none;
-    padding: 4px 16px;
-  }
-
   &.button-alternative {
     color: $inverted-text-color;
     background: $ui-primary-color;
@@ -121,8 +108,7 @@
     color: $darker-text-color;
     text-transform: none;
     background: transparent;
-    padding: 3px 15px;
-    border-radius: 4px;
+    padding: 6px 17px;
     border: 1px solid $ui-primary-color;
 
     &:active,
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index fb2445a17..ae1afc320 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -537,6 +537,192 @@
   max-width: 700px;
 }
 
+.report-dialog-modal {
+  max-width: 90vw;
+  width: 480px;
+  height: 80vh;
+  background: lighten($ui-secondary-color, 8%);
+  color: $inverted-text-color;
+  border-radius: 8px;
+  overflow: hidden;
+  position: relative;
+  flex-direction: column;
+  display: flex;
+
+  &__container {
+    box-sizing: border-box;
+    border-top: 1px solid $ui-secondary-color;
+    padding: 20px;
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+    overflow: auto;
+  }
+
+  &__title {
+    font-size: 28px;
+    line-height: 33px;
+    font-weight: 700;
+    margin-bottom: 15px;
+
+    @media screen and (max-height: 800px) {
+      font-size: 22px;
+    }
+  }
+
+  &__subtitle {
+    font-size: 17px;
+    font-weight: 600;
+    line-height: 22px;
+    margin-bottom: 4px;
+  }
+
+  &__lead {
+    font-size: 17px;
+    line-height: 22px;
+    color: lighten($inverted-text-color, 16%);
+    margin-bottom: 30px;
+  }
+
+  &__actions {
+    margin-top: 30px;
+    display: flex;
+
+    .button {
+      flex: 1 1 auto;
+    }
+  }
+
+  &__statuses {
+    flex-grow: 1;
+    min-height: 0;
+    overflow: auto;
+  }
+
+  .status__content a {
+    color: $highlight-text-color;
+  }
+
+  .status__content,
+  .status__content p {
+    color: $inverted-text-color;
+  }
+
+  .dialog-option .poll__input {
+    border-color: $inverted-text-color;
+    color: $ui-secondary-color;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 8px;
+      height: auto;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: lighten($inverted-text-color, 15%);
+      border-width: 4px;
+    }
+
+    &.active {
+      border-color: $inverted-text-color;
+      background: $inverted-text-color;
+    }
+  }
+
+  .poll__option.dialog-option {
+    padding: 15px 0;
+    flex: 0 0 auto;
+    border-bottom: 1px solid $ui-secondary-color;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    & > .poll__option__text {
+      font-size: 13px;
+      color: lighten($inverted-text-color, 16%);
+
+      strong {
+        font-size: 17px;
+        font-weight: 500;
+        line-height: 22px;
+        color: $inverted-text-color;
+        display: block;
+        margin-bottom: 4px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+
+  .flex-spacer {
+    background: transparent;
+  }
+
+  &__textarea {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $inverted-text-color;
+    background: $simple-background-color;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 17px;
+    line-height: 22px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
+    border-radius: 4px;
+    margin: 20px 0;
+
+    &::placeholder {
+      color: $dark-text-color;
+    }
+
+    &:focus {
+      outline: 0;
+    }
+  }
+
+  &__toggle {
+    display: flex;
+    align-items: center;
+
+    & > span {
+      font-size: 17px;
+      font-weight: 500;
+      margin-left: 10px;
+    }
+  }
+
+  .button.button-secondary {
+    border-color: $inverted-text-color;
+    color: $inverted-text-color;
+    flex: 0 0 auto;
+
+    &:hover,
+    &:focus,
+    &:active {
+      border-color: lighten($inverted-text-color, 15%);
+      color: lighten($inverted-text-color, 15%);
+    }
+  }
+
+  hr {
+    border: 0;
+    background: transparent;
+    margin: 15px 0;
+  }
+}
+
 .report-modal__container {
   display: flex;
   border-top: 1px solid $ui-secondary-color;
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index d9154e4c7..77541ab74 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -521,42 +521,39 @@
   justify-content: flex-start;
 }
 
-.status-check-box {
-  border-bottom: 1px solid $ui-secondary-color;
-  display: flex;
+.status-check-box__status {
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  padding: 0 10px;
 
-  .status-check-box__status {
-    margin: 10px 0 10px 10px;
-    flex: 1;
-    overflow: hidden;
+  .detailed-status__display-name {
+    color: lighten($inverted-text-color, 16%);
 
-    .media-gallery {
-      max-width: 250px;
+    span {
+      display: inline;
     }
 
-    .status__content {
-      padding: 0;
-      white-space: normal;
+    &:hover strong {
+      text-decoration: none;
     }
+  }
 
-    .video-player,
-    .audio-player {
-      margin-top: 8px;
-      max-width: 250px;
-    }
+  .media-gallery,
+  .audio-player,
+  .video-player {
+    margin-top: 8px;
+    max-width: 250px;
+  }
 
-    .media-gallery__item-thumbnail {
-      cursor: default;
-    }
+  .status__content {
+    padding: 0;
+    white-space: normal;
   }
-}
 
-.status-check-box-toggle {
-  align-items: center;
-  display: flex;
-  flex: 0 0 auto;
-  justify-content: center;
-  padding: 10px;
+  .media-gallery__item-thumbnail {
+    cursor: default;
+  }
 }
 
 .status__prepend {
diff --git a/app/javascript/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss
index 00d290883..073ebda7e 100644
--- a/app/javascript/flavours/glitch/styles/footer.scss
+++ b/app/javascript/flavours/glitch/styles/footer.scss
@@ -90,6 +90,20 @@
         .column-4 {
           display: none;
         }
+
+        .column-2 h4 {
+          display: none;
+        }
+      }
+    }
+
+    .legal-xs {
+      display: none;
+      text-align: center;
+      padding-top: 20px;
+
+      @media screen and (max-width: $no-gap-breakpoint) {
+        display: block;
       }
     }
 
@@ -105,7 +119,8 @@
       }
     }
 
-    ul a {
+    ul a,
+    .legal-xs a {
       text-decoration: none;
       color: lighten($ui-base-color, 34%);