From a3106b80945100752c7462ce5eae4c41854a7362 Mon Sep 17 00:00:00 2001
From: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Date: Wed, 28 Oct 2020 15:06:53 -0400
Subject: [PATCH] Initial changes to make group server.

---
 Gemfile.lock                                  |   6 +
 .../accounts/following_accounts_controller.rb |   2 +-
 app/controllers/api/v1/accounts_controller.rb |   9 +-
 .../api/v1/timelines/home_controller.rb       |   2 +-
 .../api/v1/timelines/list_controller.rb       |   2 +-
 .../api/v1/timelines/public_controller.rb     |   2 +-
 app/javascript/mastodon/actions/compose.js    |   6 +-
 app/javascript/mastodon/components/account.js |   2 -
 .../features/account/components/header.js     |   4 -
 .../features/community_timeline/index.js      |   2 +-
 .../mastodon/features/compose/index.js        |   8 +-
 .../directory/components/account_card.js      |  11 --
 .../features/getting_started/index.js         |   7 +-
 .../mastodon/features/home_timeline/index.js  |   3 +-
 .../components/column_settings.js             |  30 ----
 .../containers/column_settings_container.js   |  28 ----
 .../features/public_timeline/index.js         | 140 ------------------
 .../features/ui/components/columns_area.js    |   5 -
 .../ui/components/navigation_panel.js         |   4 +-
 .../features/ui/components/tabs_bar.js        |   4 +-
 app/javascript/mastodon/features/ui/index.js  |   8 +-
 .../features/ui/util/async-components.js      |   4 -
 .../mastodon/locales/defaultMessages.json     |  14 +-
 app/javascript/mastodon/locales/en.json       |   5 +-
 app/javascript/mastodon/locales/ja.json       |   5 +-
 app/javascript/mastodon/reducers/settings.js  |   1 -
 app/lib/activitypub/activity.rb               |   1 +
 app/lib/activitypub/activity/announce.rb      |   2 +-
 app/lib/activitypub/activity/create.rb        |  22 ++-
 app/models/account.rb                         |  17 +--
 app/models/account_stat.rb                    |  19 +--
 app/models/follow.rb                          |   1 +
 app/models/follow_request.rb                  |   2 +-
 app/models/list.rb                            |  19 ---
 app/models/mention.rb                         |   1 +
 app/models/mute.rb                            |   4 +-
 app/models/status.rb                          |  12 +-
 app/models/user.rb                            |  11 --
 app/services/after_block_service.rb           |   5 -
 app/services/batched_remove_status_service.rb |  52 +------
 app/services/fan_out_on_write_service.rb      |  17 +--
 app/services/follow_service.rb                |   1 -
 app/services/mute_service.rb                  |   1 -
 app/services/precompute_feed_service.rb       |   9 --
 app/services/process_mentions_service.rb      |  15 +-
 app/services/remove_status_service.rb         |  29 +---
 app/services/unfollow_service.rb              |   1 -
 app/services/unmute_service.rb                |   2 -
 app/workers/feed_insert_worker.rb             |  43 ------
 app/workers/merge_worker.rb                   |  11 --
 app/workers/mute_worker.rb                    |  12 --
 app/workers/regeneration_worker.rb            |  14 --
 .../scheduler/feed_cleanup_scheduler.rb       |  61 --------
 app/workers/unmerge_worker.rb                 |  11 --
 config/sidekiq.yml                            |   3 -
 db/schema.rb                                  | 106 ++++++++++++-
 lib/cli.rb                                    |   4 -
 lib/mastodon/feeds_cli.rb                     |  63 --------
 .../concerns/user_tracking_concern_spec.rb    |  41 -----
 spec/models/follow_request_spec.rb            |  30 ----
 spec/models/home_feed_spec.rb                 |  44 ------
 spec/services/after_block_service_spec.rb     |  29 ----
 spec/services/mute_service_spec.rb            |  19 ---
 spec/services/precompute_feed_service_spec.rb |  36 -----
 spec/workers/feed_insert_worker_spec.rb       |  52 -------
 spec/workers/regeneration_worker_spec.rb      |  26 ----
 .../scheduler/feed_cleanup_scheduler_spec.rb  |  26 ----
 67 files changed, 215 insertions(+), 973 deletions(-)
 delete mode 100644 app/javascript/mastodon/features/public_timeline/components/column_settings.js
 delete mode 100644 app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
 delete mode 100644 app/javascript/mastodon/features/public_timeline/index.js
 delete mode 100644 app/services/precompute_feed_service.rb
 delete mode 100644 app/workers/feed_insert_worker.rb
 delete mode 100644 app/workers/merge_worker.rb
 delete mode 100644 app/workers/mute_worker.rb
 delete mode 100644 app/workers/regeneration_worker.rb
 delete mode 100644 app/workers/scheduler/feed_cleanup_scheduler.rb
 delete mode 100644 app/workers/unmerge_worker.rb
 delete mode 100644 lib/mastodon/feeds_cli.rb
 delete mode 100644 spec/models/follow_request_spec.rb
 delete mode 100644 spec/models/home_feed_spec.rb
 delete mode 100644 spec/services/after_block_service_spec.rb
 delete mode 100644 spec/services/precompute_feed_service_spec.rb
 delete mode 100644 spec/workers/feed_insert_worker_spec.rb
 delete mode 100644 spec/workers/regeneration_worker_spec.rb
 delete mode 100644 spec/workers/scheduler/feed_cleanup_scheduler_spec.rb

diff --git a/Gemfile.lock b/Gemfile.lock
index 62c20bf070..a7c5b907f8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -790,3 +790,9 @@ DEPENDENCIES
   webmock (~> 3.8)
   webpacker (~> 5.1)
   webpush
+
+RUBY VERSION
+   ruby 2.6.6p146
+
+BUNDLED WITH
+   2.0.2
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 93d4bd3a4a..4b4c10e0fa 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -6,7 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   after_action :insert_pagination_headers
 
   def index
-    @accounts = load_accounts
+    @accounts = Account.none
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 0080faf330..353b371277 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -31,11 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def follow
-    FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
-
-    options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
-
-    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
+    raise Mastodon::NotPermittedError
   end
 
   def block
@@ -49,8 +45,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def unfollow
-    UnfollowService.new.call(current_user.account, @account)
-    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
+    raise Mastodon::NotPermittedError
   end
 
   def unblock
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index ae6dbcb8b3..433495e12a 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -6,7 +6,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
   def show
-    @statuses = load_statuses
+    @statuses = Status.none
 
     render json: @statuses,
            each_serializer: REST::StatusSerializer,
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
index a15eae468d..ab12520c7d 100644
--- a/app/controllers/api/v1/timelines/list_controller.rb
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::Timelines::ListController < Api::BaseController
   end
 
   def set_statuses
-    @statuses = cached_list_statuses
+    @statuses = Status.none
   end
 
   def cached_list_statuses
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index c6e7854d93..d5c0ac47c8 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   end
 
   def public_timeline_statuses
-    Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
+    Status.as_public_timeline(current_account, truthy_param?(:local))
   end
 
   def insert_pagination_headers
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 0309225202..ce2a4b7add 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -171,11 +171,7 @@ export function submitCompose(routerHistory) {
         }
       };
 
-      if (response.data.visibility !== 'direct') {
-        insertIfOnline('home');
-      }
-
-      if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+      if (response.data.visibility === 'public' || response.data.visibility === 'unlisted') {
         insertIfOnline('community');
         insertIfOnline('public');
         insertIfOnline(`account:${response.data.account.id}`);
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 2705a60013..5cb887534b 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -102,8 +102,6 @@ class Account extends ImmutablePureComponent {
             {hidingNotificationsButton}
           </Fragment>
         );
-      } else if (!account.get('moved') || following) {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
     }
 
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 9613b0b9ed..1884674889 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -150,10 +150,6 @@ class Header extends ImmutablePureComponent {
     if (me !== account.get('id')) {
       if (!account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = '';
-      } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
-      } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
       }
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index b3cd39685c..52faa22ee0 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -11,7 +11,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
 import { connectCommunityStream } from '../../actions/streaming';
 
 const messages = defineMessages({
-  title: { id: 'column.community', defaultMessage: 'Local timeline' },
+  title: { id: 'column.community', defaultMessage: 'Group timeline' },
 });
 
 const mapStateToProps = (state, { columnId }) => {
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index e2de8b0e6a..8b8491ffc2 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -23,7 +23,7 @@ const messages = defineMessages({
   home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
-  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
+  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Group timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
   compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
@@ -98,18 +98,12 @@ class Compose extends React.PureComponent {
       header = (
         <nav className='drawer__header'>
           <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
-          {!columns.some(column => column.get('id') === 'HOME') && (
-            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
-          )}
           {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
             <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
           )}
           {!columns.some(column => column.get('id') === 'COMMUNITY') && (
             <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
           )}
-          {!columns.some(column => column.get('id') === 'PUBLIC') && (
-            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
-          )}
           <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
           <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
         </nav>
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
index 419ab9e114..c1a938f647 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.js
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -199,17 +199,6 @@ class AccountCard extends ImmutablePureComponent {
             onClick={this.handleMute}
           />
         );
-      } else if (!account.get('moved') || following) {
-        buttons = (
-          <IconButton
-            icon={following ? 'user-times' : 'user-plus'}
-            title={intl.formatMessage(
-              following ? messages.unfollow : messages.follow,
-            )}
-            onClick={this.handleFollow}
-            active={following}
-          />
-        );
       }
     }
 
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index d9838e1c73..959ef19097 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -20,7 +20,7 @@ const messages = defineMessages({
   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
-  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
+  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Group timeline' },
   direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
@@ -81,7 +81,7 @@ class GettingStarted extends ImmutablePureComponent {
     const { fetchFollowRequests, multiColumn } = this.props;
 
     if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
-      this.context.router.history.replace('/timelines/home');
+      this.context.router.history.replace('/timelines/public/local');
       return;
     }
 
@@ -99,10 +99,9 @@ class GettingStarted extends ImmutablePureComponent {
       navItems.push(
         <ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
         <ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
-        <ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
       );
 
-      height += 34 + 48*2;
+      height += 34 + 48*1;
 
       if (profile_directory) {
         navItems.push(
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 577ff33bb0..25bb29c802 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -51,7 +51,8 @@ class HomeTimeline extends React.PureComponent {
     if (columnId) {
       dispatch(removeColumn(columnId));
     } else {
-      dispatch(addColumn('HOME', {}));
+      raise("Trying to pin home timeline, should be impossible");
+//      dispatch(addColumn('HOME', {}));
     }
   }
 
diff --git a/app/javascript/mastodon/features/public_timeline/components/column_settings.js b/app/javascript/mastodon/features/public_timeline/components/column_settings.js
deleted file mode 100644
index 756b6fe06d..0000000000
--- a/app/javascript/mastodon/features/public_timeline/components/column_settings.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import SettingToggle from '../../notifications/components/setting_toggle';
-
-export default @injectIntl
-class ColumnSettings extends React.PureComponent {
-
-  static propTypes = {
-    settings: ImmutablePropTypes.map.isRequired,
-    onChange: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-  };
-
-  render () {
-    const { settings, onChange } = this.props;
-
-    return (
-      <div>
-        <div className='column-settings__row'>
-          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
-          <SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
deleted file mode 100644
index 8c9e8aef41..0000000000
--- a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { connect } from 'react-redux';
-import ColumnSettings from '../components/column_settings';
-import { changeSetting } from '../../../actions/settings';
-import { changeColumnParams } from '../../../actions/columns';
-
-const mapStateToProps = (state, { columnId }) => {
-  const uuid = columnId;
-  const columns = state.getIn(['settings', 'columns']);
-  const index = columns.findIndex(c => c.get('uuid') === uuid);
-
-  return {
-    settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
-  };
-};
-
-const mapDispatchToProps = (dispatch, { columnId }) => {
-  return {
-    onChange (key, checked) {
-      if (columnId) {
-        dispatch(changeColumnParams(columnId, key, checked));
-      } else {
-        dispatch(changeSetting(['public', ...key], checked));
-      }
-    },
-  };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
deleted file mode 100644
index 988b1b070b..0000000000
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
-import { expandPublicTimeline } from '../../actions/timelines';
-import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
-import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectPublicStream } from '../../actions/streaming';
-
-const messages = defineMessages({
-  title: { id: 'column.public', defaultMessage: 'Federated timeline' },
-});
-
-const mapStateToProps = (state, { columnId }) => {
-  const uuid = columnId;
-  const columns = state.getIn(['settings', 'columns']);
-  const index = columns.findIndex(c => c.get('uuid') === uuid);
-  const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
-  const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
-  const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
-
-  return {
-    hasUnread: !!timelineState && timelineState.get('unread') > 0,
-    onlyMedia,
-    onlyRemote,
-  };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class PublicTimeline extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static defaultProps = {
-    onlyMedia: false,
-  };
-
-  static propTypes = {
-    dispatch: PropTypes.func.isRequired,
-    shouldUpdateScroll: PropTypes.func,
-    intl: PropTypes.object.isRequired,
-    columnId: PropTypes.string,
-    multiColumn: PropTypes.bool,
-    hasUnread: PropTypes.bool,
-    onlyMedia: PropTypes.bool,
-    onlyRemote: PropTypes.bool,
-  };
-
-  handlePin = () => {
-    const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
-    }
-  }
-
-  handleMove = (dir) => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  }
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  }
-
-  componentDidMount () {
-    const { dispatch, onlyMedia, onlyRemote } = this.props;
-
-    dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
-    this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
-  }
-
-  componentDidUpdate (prevProps) {
-    if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
-      const { dispatch, onlyMedia, onlyRemote } = this.props;
-
-      this.disconnect();
-      dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
-      this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
-    }
-  }
-
-  componentWillUnmount () {
-    if (this.disconnect) {
-      this.disconnect();
-      this.disconnect = null;
-    }
-  }
-
-  setRef = c => {
-    this.column = c;
-  }
-
-  handleLoadMore = maxId => {
-    const { dispatch, onlyMedia, onlyRemote } = this.props;
-
-    dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
-  }
-
-  render () {
-    const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
-    const pinned = !!columnId;
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
-        <ColumnHeader
-          icon='globe'
-          active={hasUnread}
-          title={intl.formatMessage(messages.title)}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-        >
-          <ColumnSettingsContainer columnId={columnId} />
-        </ColumnHeader>
-
-        <StatusListContainer
-          timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
-          onLoadMore={this.handleLoadMore}
-          trackScroll={!pinned}
-          scrollKey={`public_timeline-${columnId}`}
-          emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
-          shouldUpdateScroll={shouldUpdateScroll}
-          bindToDocument={!multiColumn}
-        />
-      </Column>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 9b03cf26d2..15fc92f15c 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -15,9 +15,7 @@ import BundleColumnError from './bundle_column_error';
 import {
   Compose,
   Notifications,
-  HomeTimeline,
   CommunityTimeline,
-  PublicTimeline,
   HashtagTimeline,
   DirectTimeline,
   FavouritedStatuses,
@@ -34,10 +32,7 @@ import { scrollRight } from '../../../scroll';
 
 const componentMap = {
   'COMPOSE': Compose,
-  'HOME': HomeTimeline,
   'NOTIFICATIONS': Notifications,
-  'PUBLIC': PublicTimeline,
-  'REMOTE': PublicTimeline,
   'COMMUNITY': CommunityTimeline,
   'HASHTAG': HashtagTimeline,
   'DIRECT': DirectTimeline,
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 0c12852f5b..2db5ec4b36 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -10,11 +10,9 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
 
 const NavigationPanel = () => (
   <div className='navigation-panel'>
-    <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
     <FollowRequestsNavLink />
-    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
-    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.group_timeline' defaultMessage='Group' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
     <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index 1911da8ba3..03b3ff22c9 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -8,10 +8,8 @@ import Icon from 'mastodon/components/icon';
 import NotificationsCounterIcon from './notifications_counter_icon';
 
 export const links = [
-  <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
-  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
-  <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.group_timeline' defaultMessage='Group' /></NavLink>,
   <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
   <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
 ];
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 553cb33659..1870508d5c 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -26,11 +26,9 @@ import {
   Status,
   GettingStarted,
   KeyboardShortcuts,
-  PublicTimeline,
   CommunityTimeline,
   AccountTimeline,
   AccountGallery,
-  HomeTimeline,
   Followers,
   Following,
   Reblogs,
@@ -87,10 +85,8 @@ const keyMap = {
   moveDown: ['down', 'j'],
   moveUp: ['up', 'k'],
   back: 'backspace',
-  goToHome: 'g h',
   goToNotifications: 'g n',
   goToLocal: 'g l',
-  goToFederated: 'g t',
   goToDirect: 'g d',
   goToStart: 'g s',
   goToFavourites: 'g f',
@@ -176,7 +172,7 @@ class SwitchingColumnsArea extends React.PureComponent {
     const { children } = this.props;
     const { mobile } = this.state;
     const singleColumn = forceSingleColumn || mobile;
-    const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
+    const redirect = singleColumn ? <Redirect from='/' to='/timelines/public/local' exact /> : <Redirect from='/' to='/getting-started' exact />;
 
     return (
       <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
@@ -184,8 +180,6 @@ class SwitchingColumnsArea extends React.PureComponent {
           {redirect}
           <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
-          <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
-          <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
           <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 986efda1ec..8a45231afd 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -14,10 +14,6 @@ export function HomeTimeline () {
   return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
 }
 
-export function PublicTimeline () {
-  return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
-}
-
 export function CommunityTimeline () {
   return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
 }
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index bd9ebbc0de..155300271a 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -934,11 +934,11 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Local timeline",
+        "defaultMessage": "Group timeline",
         "id": "column.community"
       },
       {
-        "defaultMessage": "The local timeline is empty. Write something publicly to get the ball rolling!",
+        "defaultMessage": "The Group timeline is empty. Write something publicly to get the ball rolling!",
         "id": "empty_column.community"
       }
     ],
@@ -2930,6 +2930,10 @@
         "defaultMessage": "Notifications",
         "id": "tabs_bar.notifications"
       },
+      {
+        "defaultMessage": "Group",
+        "id": "tabs_bar.group_timeline"
+      },
       {
         "defaultMessage": "Local",
         "id": "tabs_bar.local_timeline"
@@ -3012,6 +3016,10 @@
         "defaultMessage": "Notifications",
         "id": "tabs_bar.notifications"
       },
+      {
+        "defaultMessage": "Group",
+        "id": "tabs_bar.group_timeline"
+      },
       {
         "defaultMessage": "Local",
         "id": "tabs_bar.local_timeline"
@@ -3103,4 +3111,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 381ff028bb..92d02e2ff8 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -59,7 +59,7 @@
   "bundle_modal_error.retry": "Try again",
   "column.blocks": "Blocked users",
   "column.bookmarks": "Bookmarks",
-  "column.community": "Local timeline",
+  "column.community": "Group timeline",
   "column.direct": "Direct messages",
   "column.directory": "Browse profiles",
   "column.domain_blocks": "Blocked domains",
@@ -271,7 +271,7 @@
   "navigation_bar.apps": "Mobile apps",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.bookmarks": "Bookmarks",
-  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.community_timeline": "Group timeline",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.direct": "Direct messages",
   "navigation_bar.discover": "Discover",
@@ -410,6 +410,7 @@
   "suggestions.dismiss": "Dismiss suggestion",
   "suggestions.header": "You might be interested in…",
   "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.group_timeline": "Group",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index ec3d0ee59a..d7900a2a35 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -59,7 +59,7 @@
   "bundle_modal_error.retry": "再試行",
   "column.blocks": "ブロックしたユーザー",
   "column.bookmarks": "ブックマーク",
-  "column.community": "ローカルタイムライン",
+  "column.community": "グループタイムライン",
   "column.direct": "ダイレクトメッセージ",
   "column.directory": "ディレクトリ",
   "column.domain_blocks": "ブロックしたドメイン",
@@ -271,7 +271,7 @@
   "navigation_bar.apps": "アプリ",
   "navigation_bar.blocks": "ブロックしたユーザー",
   "navigation_bar.bookmarks": "ブックマーク",
-  "navigation_bar.community_timeline": "ローカルタイムライン",
+  "navigation_bar.community_timeline": "グループタイムライン",
   "navigation_bar.compose": "トゥートの新規作成",
   "navigation_bar.direct": "ダイレクトメッセージ",
   "navigation_bar.discover": "見つける",
@@ -410,6 +410,7 @@
   "suggestions.dismiss": "隠す",
   "suggestions.header": "興味あるかもしれません…",
   "tabs_bar.federated_timeline": "連合",
+  "tabs_bar.group_timeline": "グループ",
   "tabs_bar.home": "ホーム",
   "tabs_bar.local_timeline": "ローカル",
   "tabs_bar.notifications": "通知",
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index efef2ad9a5..1a97705f1d 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -83,7 +83,6 @@ const initialState = ImmutableMap({
 
 const defaultColumns = fromJS([
   { id: 'COMPOSE', uuid: uuid(), params: {} },
-  { id: 'HOME', uuid: uuid(), params: {} },
   { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
 ]);
 
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index a379a7ef43..b05f073655 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -3,6 +3,7 @@
 class ActivityPub::Activity
   include JsonLdHelper
   include Redisable
+  include RoutingHelper
 
   SUPPORTED_TYPES = %w(Note Question).freeze
   CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 349e8f77e7..51963e6571 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -55,7 +55,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   end
 
   def announceable?(status)
-    status.account_id == @account.id || status.distributable?
+    status.account_id == @account.id || status.distributable? || (@account.group? && status.mentions.where(account_id: @account.id).exists?)
   end
 
   def related_to_local_activity?
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index a60b79d159..172cb873b3 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -91,6 +91,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     check_for_spam
     distribute(@status)
     forward_for_reply if @status.distributable?
+    forward_for_group
   end
 
   def find_existing_status
@@ -160,10 +161,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
     @status.mentions.create(account: delivered_to_account, silent: true)
     @status.update(visibility: :limited) if @status.direct_visibility?
-
-    return unless delivered_to_account.following?(@account)
-
-    FeedInsertWorker.perform_async(@status.id, delivered_to_account.id, :home)
   end
 
   def attach_tags(status)
@@ -494,6 +491,23 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
   end
 
+  def forward_for_group
+    groups = Account.where(id: @status.mentions.pluck(:account_id)).where(actor_type: 'Group')
+
+    groups.each do |group|
+      if @json['signature'].present? && audience_includes_followers?(group)
+        ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), group.id)
+      end
+
+      ReblogService.new.call(group, @status, {visibility: @status.visibility})
+    end
+  end
+
+  def audience_includes_followers?(account)
+    url = account_followers_url(account)
+    equals_or_includes?(audience_to, url) || equals_or_includes?(audience_cc, url)
+  end
+
   def increment_voters_count!
     poll = replied_to_status.preloadable_poll
 
diff --git a/app/models/account.rb b/app/models/account.rb
index 6b7ebda9e6..50a219824c 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -50,6 +50,7 @@
 #  avatar_storage_schema_version :integer
 #  header_storage_schema_version :integer
 #  devices_url                   :string
+#  sensitized_at                 :datetime
 #
 
 class Account < ApplicationRecord
@@ -521,7 +522,6 @@ class Account < ApplicationRecord
   before_create :generate_keys
   before_validation :prepare_contents, if: :local?
   before_validation :prepare_username, on: :create
-  before_destroy :clean_feed_manager
 
   private
 
@@ -551,19 +551,4 @@ class Account < ApplicationRecord
   def emojifiable_text
     [note, display_name, fields.map(&:name), fields.map(&:value)].join(' ')
   end
-
-  def clean_feed_manager
-    reblog_key       = FeedManager.instance.key(:home, id, 'reblogs')
-    reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
-
-    Redis.current.pipelined do
-      Redis.current.del(FeedManager.instance.key(:home, id))
-      Redis.current.del(reblog_key)
-
-      reblogged_id_set.each do |reblogged_id|
-        reblog_set_key = FeedManager.instance.key(:home, id, "reblogs:#{reblogged_id}")
-        Redis.current.del(reblog_set_key)
-      end
-    end
-  end
 end
diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb
index c84e4217c8..78c61be95c 100644
--- a/app/models/account_stat.rb
+++ b/app/models/account_stat.rb
@@ -3,15 +3,16 @@
 #
 # Table name: account_stats
 #
-#  id              :bigint(8)        not null, primary key
-#  account_id      :bigint(8)        not null
-#  statuses_count  :bigint(8)        default(0), not null
-#  following_count :bigint(8)        default(0), not null
-#  followers_count :bigint(8)        default(0), not null
-#  created_at      :datetime         not null
-#  updated_at      :datetime         not null
-#  last_status_at  :datetime
-#  lock_version    :integer          default(0), not null
+#  id                :bigint(8)        not null, primary key
+#  account_id        :bigint(8)        not null
+#  statuses_count    :bigint(8)        default(0), not null
+#  following_count   :bigint(8)        default(0), not null
+#  followers_count   :bigint(8)        default(0), not null
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#  last_status_at    :datetime
+#  lock_version      :integer          default(0), not null
+#  subscribing_count :bigint(8)        default(0), not null
 #
 
 class AccountStat < ApplicationRecord
diff --git a/app/models/follow.rb b/app/models/follow.rb
index f3e48a2ed7..272535ee7e 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -10,6 +10,7 @@
 #  target_account_id :bigint(8)        not null
 #  show_reblogs      :boolean          default(TRUE), not null
 #  uri               :string
+#  delivery          :boolean          default(TRUE), not null
 #
 
 class Follow < ApplicationRecord
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 3325e264cc..59a68e7a02 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -10,6 +10,7 @@
 #  target_account_id :bigint(8)        not null
 #  show_reblogs      :boolean          default(TRUE), not null
 #  uri               :string
+#  delivery          :boolean          default(TRUE), not null
 #
 
 class FollowRequest < ApplicationRecord
@@ -29,7 +30,6 @@ class FollowRequest < ApplicationRecord
 
   def authorize!
     account.follow!(target_account, reblogs: show_reblogs, uri: uri)
-    MergeWorker.perform_async(target_account.id, account.id) if account.local?
     destroy!
   end
 
diff --git a/app/models/list.rb b/app/models/list.rb
index c9c94fca1d..019a9523ff 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -25,23 +25,4 @@ class List < ApplicationRecord
   validates_each :account_id, on: :create do |record, _attr, value|
     record.errors.add(:base, I18n.t('lists.errors.limit')) if List.where(account_id: value).count >= PER_ACCOUNT_LIMIT
   end
-
-  before_destroy :clean_feed_manager
-
-  private
-
-  def clean_feed_manager
-    reblog_key       = FeedManager.instance.key(:list, id, 'reblogs')
-    reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
-
-    Redis.current.pipelined do
-      Redis.current.del(FeedManager.instance.key(:list, id))
-      Redis.current.del(reblog_key)
-
-      reblogged_id_set.each do |reblogged_id|
-        reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}")
-        Redis.current.del(reblog_set_key)
-      end
-    end
-  end
 end
diff --git a/app/models/mention.rb b/app/models/mention.rb
index d01a88e32e..657a6342b3 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -21,6 +21,7 @@ class Mention < ApplicationRecord
 
   scope :active, -> { where(silent: false) }
   scope :silent, -> { where(silent: true) }
+  scope :groups, -> { joins(:account).where(accounts: { actor_type: :Group }) }
 
   delegate(
     :username,
diff --git a/app/models/mute.rb b/app/models/mute.rb
index 0e00c2278f..1b034fee33 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -6,9 +6,11 @@
 #  id                 :bigint(8)        not null, primary key
 #  created_at         :datetime         not null
 #  updated_at         :datetime         not null
+#  hide_notifications :boolean          default(TRUE), not null
 #  account_id         :bigint(8)        not null
 #  target_account_id  :bigint(8)        not null
-#  hide_notifications :boolean          default(TRUE), not null
+#  expires_at         :datetime
+#  unmute_jid         :string
 #
 
 class Mute < ApplicationRecord
diff --git a/app/models/status.rb b/app/models/status.rb
index 71596ec2f8..27ffcd7987 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -22,7 +22,10 @@
 #  application_id         :bigint(8)
 #  in_reply_to_account_id :bigint(8)
 #  poll_id                :bigint(8)
+#  quote_id               :bigint(8)
 #  deleted_at             :datetime
+#  expires_at             :datetime
+#  expires_action         :integer          default(0), not null
 #
 
 class Status < ApplicationRecord
@@ -90,6 +93,7 @@ class Status < ApplicationRecord
   scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
   scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
   scope :with_public_visibility, -> { where(visibility: :public) }
+  scope :with_distributable_visibility, -> { where(visibility: [:public, :unlisted]) }
   scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
   scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
   scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
@@ -282,7 +286,7 @@ class Status < ApplicationRecord
     end
 
     def as_public_timeline(account = nil, local_only = false)
-      query = timeline_scope(local_only).without_replies
+      query = timeline_scope([:local, true].include?(local_only) ? :local : :none)
 
       apply_timeline_filters(query, account, [:local, true].include?(local_only))
     end
@@ -382,13 +386,15 @@ class Status < ApplicationRecord
                          Status.local
                        when :remote
                          Status.remote
+                       when :none
+                         Status.none
                        else
                          Status
                        end
 
       starting_scope
-        .with_public_visibility
-        .without_reblogs
+        .with_distributable_visibility
+
     end
 
     def apply_timeline_filters(query, account, local_only)
diff --git a/app/models/user.rb b/app/models/user.rb
index 306e2d4355..16cbdac440 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -391,7 +391,6 @@ class User < ApplicationRecord
 
   def prepare_returning_user!
     ActivityTracker.record('activity:logins', id)
-    regenerate_feed! if needs_feed_update?
   end
 
   def notify_staff_about_pending_account!
@@ -401,16 +400,6 @@ class User < ApplicationRecord
     end
   end
 
-  def regenerate_feed!
-    return unless Redis.current.setnx("account:#{account_id}:regeneration", true)
-    Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
-    RegenerationWorker.perform_async(account_id)
-  end
-
-  def needs_feed_update?
-    last_sign_in_at < ACTIVE_DURATION.ago
-  end
-
   def validate_email_dns?
     email_changed? && !(Rails.env.test? || Rails.env.development?)
   end
diff --git a/app/services/after_block_service.rb b/app/services/after_block_service.rb
index 2a0e10a79a..7e90f7d718 100644
--- a/app/services/after_block_service.rb
+++ b/app/services/after_block_service.rb
@@ -5,17 +5,12 @@ class AfterBlockService < BaseService
     @account        = account
     @target_account = target_account
 
-    clear_home_feed!
     clear_notifications!
     clear_conversations!
   end
 
   private
 
-  def clear_home_feed!
-    FeedManager.instance.clear_from_timeline(@account, @target_account)
-  end
-
   def clear_conversations!
     AccountConversation.where(account: @account).where('? = ANY(participant_account_ids)', @target_account.id).in_batches.destroy_all
   end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 2295a01dc3..b3cfdd84d0 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -27,16 +27,6 @@ class BatchedRemoveStatusService < BaseService
 
     return if options[:skip_side_effects]
 
-    # Batch by source account
-    statuses.group_by(&:account_id).each_value do |account_statuses|
-      account = account_statuses.first.account
-
-      next unless account
-
-      unpush_from_home_timelines(account, account_statuses)
-      unpush_from_list_timelines(account, account_statuses)
-    end
-
     # Cannot be batched
     statuses.each do |status|
       unpush_from_public_timelines(status)
@@ -45,50 +35,22 @@ class BatchedRemoveStatusService < BaseService
 
   private
 
-  def unpush_from_home_timelines(account, statuses)
-    recipients = account.followers_for_local_distribution.to_a
-
-    recipients << account if account.local?
-
-    recipients.each do |follower|
-      statuses.each do |status|
-        FeedManager.instance.unpush_from_home(follower, status)
-      end
-    end
-  end
-
-  def unpush_from_list_timelines(account, statuses)
-    account.lists_for_local_distribution.select(:id, :account_id).each do |list|
-      statuses.each do |status|
-        FeedManager.instance.unpush_from_list(list, status)
-      end
-    end
-  end
-
   def unpush_from_public_timelines(status)
-    return unless status.public_visibility?
+    return unless status.distributable?
 
     payload = @json_payloads[status.id]
 
     redis.pipelined do
-      redis.publish('timeline:public', payload)
       if status.local?
         redis.publish('timeline:public:local', payload)
-      else
-        redis.publish('timeline:public:remote', payload)
-      end
-      if status.media_attachments.any?
-        redis.publish('timeline:public:media', payload)
-        if status.local?
-          redis.publish('timeline:public:local:media', payload)
-        else
-          redis.publish('timeline:public:remote:media', payload)
-        end
+        redis.publish('timeline:public:local:media', payload) if status.media_attachments.any?
       end
 
-      @tags[status.id].each do |hashtag|
-        redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
-        redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
+      if status.public_visibility?
+        @tags[status.id].each do |hashtag|
+          redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
+          redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
+        end
       end
     end
   end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index e05d02cef4..5714238763 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -8,22 +8,13 @@ class FanOutOnWriteService < BaseService
 
     if status.direct_visibility?
       deliver_to_own_conversation(status)
-    elsif status.limited_visibility?
-      deliver_to_mentioned_followers(status)
-    else
-      deliver_to_self(status) if status.account.local?
-      deliver_to_followers(status)
-      deliver_to_lists(status)
     end
 
-    return if status.account.silenced? || !status.public_visibility? || status.reblog?
+    return if status.account.silenced? || !status.distributable?
 
     render_anonymous_payload(status)
 
     deliver_to_hashtags(status)
-
-    return if status.reply? && status.in_reply_to_account_id != status.account_id
-
     deliver_to_public(status)
     deliver_to_media(status) if status.media_attachments.any?
   end
@@ -82,22 +73,16 @@ class FanOutOnWriteService < BaseService
   def deliver_to_public(status)
     Rails.logger.debug "Delivering status #{status.id} to public timeline"
 
-    Redis.current.publish('timeline:public', @payload)
     if status.local?
       Redis.current.publish('timeline:public:local', @payload)
-    else
-      Redis.current.publish('timeline:public:remote', @payload)
     end
   end
 
   def deliver_to_media(status)
     Rails.logger.debug "Delivering status #{status.id} to media timeline"
 
-    Redis.current.publish('timeline:public:media', @payload)
     if status.local?
       Redis.current.publish('timeline:public:local:media', @payload)
-    else
-      Redis.current.publish('timeline:public:remote:media', @payload)
     end
   end
 
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 311ae7fa68..6bb89849d5 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -68,7 +68,6 @@ class FollowService < BaseService
     follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
 
     LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
-    MergeWorker.perform_async(@target_account.id, @source_account.id)
 
     follow
   end
diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb
index 676804cb99..f146f1a730 100644
--- a/app/services/mute_service.rb
+++ b/app/services/mute_service.rb
@@ -9,7 +9,6 @@ class MuteService < BaseService
     if mute.hide_notifications?
       BlockWorker.perform_async(account.id, target_account.id)
     else
-      MuteWorker.perform_async(account.id, target_account.id)
     end
 
     mute
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
deleted file mode 100644
index 076dedacab..0000000000
--- a/app/services/precompute_feed_service.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class PrecomputeFeedService < BaseService
-  def call(account)
-    FeedManager.instance.populate_feed(account)
-  ensure
-    Redis.current.del("account:#{account.id}:regeneration")
-  end
-end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 8e260811d3..1776f12458 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -13,6 +13,13 @@ class ProcessMentionsService < BaseService
     @status  = status
     mentions = []
 
+    if status.distributable?
+      mentioned_account = Account.find_local(ENV.fetch('DEFAULT_GROUP'))
+      if !mentioned_account.nil? && mentioned_account.group?
+        mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)
+      end
+    end
+
     status.text = status.text.gsub(Account::MENTION_RE) do |match|
       username, domain = Regexp.last_match(1).split('@')
 
@@ -58,7 +65,13 @@ class ProcessMentionsService < BaseService
     mentioned_account = mention.account
 
     if mentioned_account.local?
-      LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
+      if mentioned_account.group?
+        ActivityPub::DeliveryWorker.push_bulk(mentioned_account.followers.inboxes) do |inbox_url|
+          [activitypub_json, mentioned_account.id, inbox_url]
+        end
+      else
+        LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
+      end
     elsif mentioned_account.activitypub?
       ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
     end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 4f0edc3cfb..54add3d790 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -21,9 +21,6 @@ class RemoveStatusService < BaseService
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
-        remove_from_self if status.account.local?
-        remove_from_followers
-        remove_from_lists
         remove_from_affected
         remove_reblogs
         remove_from_hashtags
@@ -51,22 +48,6 @@ class RemoveStatusService < BaseService
 
   private
 
-  def remove_from_self
-    FeedManager.instance.unpush_from_home(@account, @status)
-  end
-
-  def remove_from_followers
-    @account.followers_for_local_distribution.reorder(nil).find_each do |follower|
-      FeedManager.instance.unpush_from_home(follower, @status)
-    end
-  end
-
-  def remove_from_lists
-    @account.lists_for_local_distribution.select(:id, :account_id).reorder(nil).find_each do |list|
-      FeedManager.instance.unpush_from_list(list, @status)
-    end
-  end
-
   def remove_from_affected
     @mentions.map(&:account).select(&:local?).each do |account|
       redis.publish("timeline:#{account.id}", @payload)
@@ -137,24 +118,18 @@ class RemoveStatusService < BaseService
   end
 
   def remove_from_public
-    return unless @status.public_visibility?
+    return unless @status.distributable?
 
-    redis.publish('timeline:public', @payload)
     if @status.local?
       redis.publish('timeline:public:local', @payload)
-    else
-      redis.publish('timeline:public:remote', @payload)
     end
   end
 
   def remove_from_media
-    return unless @status.public_visibility?
+    return unless @status.distributable?
 
-    redis.publish('timeline:public:media', @payload)
     if @status.local?
       redis.publish('timeline:public:local:media', @payload)
-    else
-      redis.publish('timeline:public:remote:media', @payload)
     end
   end
 
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 151f3674fd..7af3a7a093 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -27,7 +27,6 @@ class UnfollowService < BaseService
 
     create_notification(follow) if !@target_account.local? && @target_account.activitypub?
     create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub?
-    UnmergeWorker.perform_async(@target_account.id, @source_account.id) unless @options[:skip_unmerge]
 
     follow
   end
diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb
index 6aeea358f7..ed268b7c58 100644
--- a/app/services/unmute_service.rb
+++ b/app/services/unmute_service.rb
@@ -5,7 +5,5 @@ class UnmuteService < BaseService
     return unless account.muting?(target_account)
 
     account.unmute!(target_account)
-
-    MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account)
   end
 end
diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb
deleted file mode 100644
index 1ae3c877b0..0000000000
--- a/app/workers/feed_insert_worker.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-class FeedInsertWorker
-  include Sidekiq::Worker
-
-  def perform(status_id, id, type = :home)
-    @type     = type.to_sym
-    @status   = Status.find(status_id)
-
-    case @type
-    when :home
-      @follower = Account.find(id)
-    when :list
-      @list     = List.find(id)
-      @follower = @list.account
-    end
-
-    check_and_insert
-  rescue ActiveRecord::RecordNotFound
-    true
-  end
-
-  private
-
-  def check_and_insert
-    perform_push unless feed_filtered?
-  end
-
-  def feed_filtered?
-    # Note: Lists are a variation of home, so the filtering rules
-    # of home apply to both
-    FeedManager.instance.filter?(:home, @status, @follower.id)
-  end
-
-  def perform_push
-    case @type
-    when :home
-      FeedManager.instance.push_to_home(@follower, @status)
-    when :list
-      FeedManager.instance.push_to_list(@list, @status)
-    end
-  end
-end
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
deleted file mode 100644
index d745cb99c7..0000000000
--- a/app/workers/merge_worker.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class MergeWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull'
-
-  def perform(from_account_id, into_account_id)
-    FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
-  end
-end
diff --git a/app/workers/mute_worker.rb b/app/workers/mute_worker.rb
deleted file mode 100644
index 7bf0923a5d..0000000000
--- a/app/workers/mute_worker.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class MuteWorker
-  include Sidekiq::Worker
-
-  def perform(account_id, target_account_id)
-    FeedManager.instance.clear_from_timeline(
-      Account.find(account_id),
-      Account.find(target_account_id)
-    )
-  end
-end
diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb
deleted file mode 100644
index 5c13c894fe..0000000000
--- a/app/workers/regeneration_worker.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class RegenerationWorker
-  include Sidekiq::Worker
-
-  sidekiq_options lock: :until_executed
-
-  def perform(account_id, _ = :home)
-    account = Account.find(account_id)
-    PrecomputeFeedService.new.call(account)
-  rescue ActiveRecord::RecordNotFound
-    true
-  end
-end
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
deleted file mode 100644
index 458fe6193e..0000000000
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-class Scheduler::FeedCleanupScheduler
-  include Sidekiq::Worker
-  include Redisable
-
-  sidekiq_options lock: :until_executed, retry: 0
-
-  def perform
-    clean_home_feeds!
-    clean_list_feeds!
-  end
-
-  private
-
-  def clean_home_feeds!
-    clean_feeds!(inactive_account_ids, :home)
-  end
-
-  def clean_list_feeds!
-    clean_feeds!(inactive_list_ids, :list)
-  end
-
-  def clean_feeds!(ids, type)
-    reblogged_id_sets = {}
-
-    redis.pipelined do
-      ids.each do |feed_id|
-        redis.del(feed_manager.key(type, feed_id))
-        reblog_key = feed_manager.key(type, feed_id, 'reblogs')
-        # We collect a future for this: we don't block while getting
-        # it, but we can iterate over it later.
-        reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1)
-        redis.del(reblog_key)
-      end
-    end
-
-    # Remove all of the reblog tracking keys we just removed the
-    # references to.
-    redis.pipelined do
-      reblogged_id_sets.each do |feed_id, future|
-        future.value.each do |reblogged_id|
-          reblog_set_key = feed_manager.key(type, feed_id, "reblogs:#{reblogged_id}")
-          redis.del(reblog_set_key)
-        end
-      end
-    end
-  end
-
-  def inactive_account_ids
-    @inactive_account_ids ||= User.confirmed.inactive.pluck(:account_id)
-  end
-
-  def inactive_list_ids
-    List.where(account_id: inactive_account_ids).pluck(:id)
-  end
-
-  def feed_manager
-    FeedManager.instance
-  end
-end
diff --git a/app/workers/unmerge_worker.rb b/app/workers/unmerge_worker.rb
deleted file mode 100644
index ea6aacebf6..0000000000
--- a/app/workers/unmerge_worker.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class UnmergeWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull'
-
-  def perform(from_account_id, into_account_id)
-    FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
-  end
-end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 5de25de234..0f6382cd8d 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -15,9 +15,6 @@
   media_cleanup_scheduler:
     cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
     class: Scheduler::MediaCleanupScheduler
-  feed_cleanup_scheduler:
-    cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
-    class: Scheduler::FeedCleanupScheduler
   doorkeeper_cleanup_scheduler:
     cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
     class: Scheduler::DoorkeeperCleanupScheduler
diff --git a/db/schema.rb b/db/schema.rb
index cf31b6d23d..0dd192ce3d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -105,9 +105,22 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.datetime "updated_at", null: false
     t.datetime "last_status_at"
     t.integer "lock_version", default: 0, null: false
+    t.bigint "subscribing_count", default: 0, null: false
     t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true
   end
 
+  create_table "account_subscribes", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "target_account_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.bigint "list_id"
+    t.boolean "show_reblogs", default: true, null: false
+    t.index ["account_id"], name: "index_account_subscribes_on_account_id"
+    t.index ["list_id"], name: "index_account_subscribes_on_list_id"
+    t.index ["target_account_id"], name: "index_account_subscribes_on_target_account_id"
+  end
+
   create_table "account_tag_stats", force: :cascade do |t|
     t.bigint "tag_id", null: false
     t.bigint "accounts_count", default: 0, null: false
@@ -182,6 +195,7 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.integer "avatar_storage_schema_version"
     t.integer "header_storage_schema_version"
     t.string "devices_url"
+    t.datetime "sensitized_at"
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
     t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@@ -246,11 +260,11 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.bigint "user_id"
     t.string "dump_file_name"
     t.string "dump_content_type"
+    t.bigint "dump_file_size"
     t.datetime "dump_updated_at"
     t.boolean "processed", default: false, null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
-    t.bigint "dump_file_size"
   end
 
   create_table "blocks", force: :cascade do |t|
@@ -354,6 +368,38 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
   end
 
+  create_table "domain_subscribes", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "list_id"
+    t.string "domain", default: "", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.boolean "exclude_reblog", default: true
+    t.index ["account_id"], name: "index_domain_subscribes_on_account_id"
+    t.index ["list_id"], name: "index_domain_subscribes_on_list_id"
+  end
+
+  create_table "domains", force: :cascade do |t|
+    t.string "domain", default: "", null: false
+    t.string "title", default: "", null: false
+    t.string "short_description", default: "", null: false
+    t.string "email", default: "", null: false
+    t.string "version", default: "", null: false
+    t.string "thumbnail_remote_url", default: "", null: false
+    t.string "languages", array: true
+    t.boolean "registrations"
+    t.boolean "approval_required"
+    t.bigint "contact_account_id"
+    t.string "software", default: "", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "thumbnail_file_name"
+    t.string "thumbnail_content_type"
+    t.integer "thumbnail_file_size"
+    t.datetime "thumbnail_updated_at"
+    t.index ["contact_account_id"], name: "index_domains_on_contact_account_id"
+  end
+
   create_table "email_domain_blocks", force: :cascade do |t|
     t.string "domain", default: "", null: false
     t.datetime "created_at", null: false
@@ -376,6 +422,15 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.index ["from_account_id"], name: "index_encrypted_messages_on_from_account_id"
   end
 
+  create_table "favourite_tags", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "tag_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id"], name: "index_favourite_tags_on_account_id"
+    t.index ["tag_id"], name: "index_favourite_tags_on_tag_id"
+  end
+
   create_table "favourites", force: :cascade do |t|
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
@@ -404,9 +459,21 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.bigint "target_account_id", null: false
     t.boolean "show_reblogs", default: true, null: false
     t.string "uri"
+    t.boolean "delivery", default: true, null: false
     t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
   end
 
+  create_table "follow_tags", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "tag_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.bigint "list_id"
+    t.index ["account_id"], name: "index_follow_tags_on_account_id"
+    t.index ["list_id"], name: "index_follow_tags_on_list_id"
+    t.index ["tag_id"], name: "index_follow_tags_on_tag_id"
+  end
+
   create_table "follows", force: :cascade do |t|
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
@@ -414,6 +481,7 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.bigint "target_account_id", null: false
     t.boolean "show_reblogs", default: true, null: false
     t.string "uri"
+    t.boolean "delivery", default: true, null: false
     t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
     t.index ["target_account_id"], name: "index_follows_on_target_account_id"
   end
@@ -454,6 +522,22 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.index ["user_id"], name: "index_invites_on_user_id"
   end
 
+  create_table "keyword_subscribes", force: :cascade do |t|
+    t.bigint "account_id"
+    t.string "keyword", null: false
+    t.boolean "ignorecase", default: true
+    t.boolean "regexp", default: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.string "name", default: "", null: false
+    t.boolean "ignore_block", default: false
+    t.boolean "disabled", default: false
+    t.string "exclude_keyword", default: "", null: false
+    t.bigint "list_id"
+    t.index ["account_id"], name: "index_keyword_subscribes_on_account_id"
+    t.index ["list_id"], name: "index_keyword_subscribes_on_list_id"
+  end
+
   create_table "list_accounts", force: :cascade do |t|
     t.bigint "list_id", null: false
     t.bigint "account_id", null: false
@@ -526,6 +610,8 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.boolean "hide_notifications", default: true, null: false
     t.bigint "account_id", null: false
     t.bigint "target_account_id", null: false
+    t.datetime "expires_at"
+    t.string "unmute_jid"
     t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true
     t.index ["target_account_id"], name: "index_mutes_on_target_account_id"
   end
@@ -780,14 +866,19 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
     t.bigint "application_id"
     t.bigint "in_reply_to_account_id"
     t.bigint "poll_id"
+    t.bigint "quote_id"
     t.datetime "deleted_at"
+    t.datetime "expires_at"
+    t.integer "expires_action", default: 0, null: false
     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
     t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
     t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
+    t.index ["quote_id"], name: "index_statuses_on_quote_id"
     t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
     t.index ["uri"], name: "index_statuses_on_uri", unique: true
+    t.index ["url"], name: "index_statuses_on_url"
   end
 
   create_table "statuses_tags", id: false, force: :cascade do |t|
@@ -923,6 +1014,9 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
   add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
   add_foreign_key "account_pins", "accounts", on_delete: :cascade
   add_foreign_key "account_stats", "accounts", on_delete: :cascade
+  add_foreign_key "account_subscribes", "accounts", column: "target_account_id", on_delete: :cascade
+  add_foreign_key "account_subscribes", "accounts", on_delete: :cascade
+  add_foreign_key "account_subscribes", "lists", on_delete: :cascade
   add_foreign_key "account_tag_stats", "tags", on_delete: :cascade
   add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade
   add_foreign_key "account_warnings", "accounts", on_delete: :nullify
@@ -943,20 +1037,30 @@ ActiveRecord::Schema.define(version: 2020_06_28_133322) do
   add_foreign_key "custom_filters", "accounts", on_delete: :cascade
   add_foreign_key "devices", "accounts", on_delete: :cascade
   add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
+  add_foreign_key "domain_subscribes", "accounts", on_delete: :cascade
+  add_foreign_key "domain_subscribes", "lists", on_delete: :cascade
+  add_foreign_key "domains", "accounts", column: "contact_account_id"
   add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade
   add_foreign_key "encrypted_messages", "accounts", column: "from_account_id", on_delete: :cascade
   add_foreign_key "encrypted_messages", "devices", on_delete: :cascade
+  add_foreign_key "favourite_tags", "accounts", on_delete: :cascade
+  add_foreign_key "favourite_tags", "tags", on_delete: :cascade
   add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
   add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
   add_foreign_key "featured_tags", "accounts", on_delete: :cascade
   add_foreign_key "featured_tags", "tags", on_delete: :cascade
   add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade
   add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
+  add_foreign_key "follow_tags", "accounts", on_delete: :cascade
+  add_foreign_key "follow_tags", "lists", on_delete: :cascade
+  add_foreign_key "follow_tags", "tags", on_delete: :cascade
   add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
   add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
   add_foreign_key "invites", "users", on_delete: :cascade
+  add_foreign_key "keyword_subscribes", "accounts", on_delete: :cascade
+  add_foreign_key "keyword_subscribes", "lists", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
   add_foreign_key "list_accounts", "follows", on_delete: :cascade
   add_foreign_key "list_accounts", "lists", on_delete: :cascade
diff --git a/lib/cli.rb b/lib/cli.rb
index 9162144cc4..70ed0ad88f 100644
--- a/lib/cli.rb
+++ b/lib/cli.rb
@@ -4,7 +4,6 @@ require 'thor'
 require_relative 'mastodon/media_cli'
 require_relative 'mastodon/emoji_cli'
 require_relative 'mastodon/accounts_cli'
-require_relative 'mastodon/feeds_cli'
 require_relative 'mastodon/search_cli'
 require_relative 'mastodon/settings_cli'
 require_relative 'mastodon/statuses_cli'
@@ -30,9 +29,6 @@ module Mastodon
     desc 'accounts SUBCOMMAND ...ARGS', 'Manage accounts'
     subcommand 'accounts', Mastodon::AccountsCLI
 
-    desc 'feeds SUBCOMMAND ...ARGS', 'Manage feeds'
-    subcommand 'feeds', Mastodon::FeedsCLI
-
     desc 'search SUBCOMMAND ...ARGS', 'Manage the search engine'
     subcommand 'search', Mastodon::SearchCLI
 
diff --git a/lib/mastodon/feeds_cli.rb b/lib/mastodon/feeds_cli.rb
deleted file mode 100644
index 578ea15c58..0000000000
--- a/lib/mastodon/feeds_cli.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../config/boot'
-require_relative '../../config/environment'
-require_relative 'cli_helper'
-
-module Mastodon
-  class FeedsCLI < Thor
-    include CLIHelper
-
-    def self.exit_on_failure?
-      true
-    end
-
-    option :all, type: :boolean, default: false
-    option :concurrency, type: :numeric, default: 5, aliases: [:c]
-    option :verbose, type: :boolean, aliases: [:v]
-    option :dry_run, type: :boolean, default: false
-    desc 'build [USERNAME]', 'Build home and list feeds for one or all users'
-    long_desc <<-LONG_DESC
-      Build home and list feeds that are stored in Redis from the database.
-
-      With the --all option, all active users will be processed.
-      Otherwise, a single user specified by USERNAME.
-    LONG_DESC
-    def build(username = nil)
-      dry_run = options[:dry_run] ? '(DRY RUN)' : ''
-
-      if options[:all] || username.nil?
-        processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account|
-          PrecomputeFeedService.new.call(account) unless options[:dry_run]
-        end
-
-        say("Regenerated feeds for #{processed} accounts #{dry_run}", :green, true)
-      elsif username.present?
-        account = Account.find_local(username)
-
-        if account.nil?
-          say('No such account', :red)
-          exit(1)
-        end
-
-        PrecomputeFeedService.new.call(account) unless options[:dry_run]
-
-        say("OK #{dry_run}", :green, true)
-      else
-        say('No account(s) given', :red)
-        exit(1)
-      end
-    end
-
-    desc 'clear', 'Remove all home and list feeds from Redis'
-    def clear
-      keys = Redis.current.keys('feed:*')
-
-      Redis.current.pipelined do
-        keys.each { |key| Redis.current.del(key) }
-      end
-
-      say('OK', :green)
-    end
-  end
-end
diff --git a/spec/controllers/concerns/user_tracking_concern_spec.rb b/spec/controllers/concerns/user_tracking_concern_spec.rb
index 1e56202211..dd9ba1c782 100644
--- a/spec/controllers/concerns/user_tracking_concern_spec.rb
+++ b/spec/controllers/concerns/user_tracking_concern_spec.rb
@@ -43,47 +43,6 @@ describe ApplicationController, type: :controller do
       expect_updated_sign_in_at(user)
     end
 
-    describe 'feed regeneration' do
-      before do
-        alice = Fabricate(:account)
-        bob   = Fabricate(:account)
-
-        user.account.follow!(alice)
-        user.account.follow!(bob)
-
-        Fabricate(:status, account: alice, text: 'hello world')
-        Fabricate(:status, account: bob, text: 'yes hello')
-        Fabricate(:status, account: user.account, text: 'test')
-
-        user.update(last_sign_in_at: 'Tue, 04 Jul 2017 14:45:56 UTC +00:00', current_sign_in_at: 'Wed, 05 Jul 2017 22:10:52 UTC +00:00')
-
-        sign_in user, scope: :user
-      end
-
-      it 'sets a regeneration marker while regenerating' do
-        allow(RegenerationWorker).to receive(:perform_async)
-        get :show
-
-        expect_updated_sign_in_at(user)
-        expect(Redis.current.get("account:#{user.account_id}:regeneration")).to eq 'true'
-        expect(RegenerationWorker).to have_received(:perform_async)
-      end
-
-      it 'sets the regeneration marker to expire' do
-        allow(RegenerationWorker).to receive(:perform_async)
-        get :show
-        expect(Redis.current.ttl("account:#{user.account_id}:regeneration")).to be >= 0
-      end
-
-      it 'regenerates feed when sign in is older than two weeks' do
-        get :show
-
-        expect_updated_sign_in_at(user)
-        expect(Redis.current.zcard(FeedManager.instance.key(:home, user.account_id))).to eq 3
-        expect(Redis.current.get("account:#{user.account_id}:regeneration")).to be_nil
-      end
-    end
-
     def expect_updated_sign_in_at(user)
       expect(user.reload.current_sign_in_at).to be_within(1.0).of(Time.now.utc)
     end
diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb
deleted file mode 100644
index 2cf28b263c..0000000000
--- a/spec/models/follow_request_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FollowRequest, type: :model do
-  describe '#authorize!' do
-    let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) }
-    let(:account)        { Fabricate(:account) }
-    let(:target_account) { Fabricate(:account) }
-
-    it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
-      expect(account).to        receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri)
-      expect(MergeWorker).to    receive(:perform_async).with(target_account.id, account.id)
-      expect(follow_request).to receive(:destroy!)
-      follow_request.authorize!
-    end
-
-    it 'correctly passes show_reblogs when true' do
-      follow_request = Fabricate.create(:follow_request, show_reblogs: true)
-      follow_request.authorize!
-      target = follow_request.target_account
-      expect(follow_request.account.muting_reblogs?(target)).to be false
-    end
-
-    it 'correctly passes show_reblogs when false' do
-      follow_request = Fabricate.create(:follow_request, show_reblogs: false)
-      follow_request.authorize!
-      target = follow_request.target_account
-      expect(follow_request.account.muting_reblogs?(target)).to be true
-    end
-  end
-end
diff --git a/spec/models/home_feed_spec.rb b/spec/models/home_feed_spec.rb
deleted file mode 100644
index ee7a83960a..0000000000
--- a/spec/models/home_feed_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe HomeFeed, type: :model do
-  let(:account) { Fabricate(:account) }
-
-  subject { described_class.new(account) }
-
-  describe '#get' do
-    before do
-      Fabricate(:status, account: account, id: 1)
-      Fabricate(:status, account: account, id: 2)
-      Fabricate(:status, account: account, id: 3)
-      Fabricate(:status, account: account, id: 10)
-    end
-
-    context 'when feed is generated' do
-      before do
-        Redis.current.zadd(
-          FeedManager.instance.key(:home, account.id),
-          [[4, 4], [3, 3], [2, 2], [1, 1]]
-        )
-      end
-
-      it 'gets statuses with ids in the range from redis' do
-        results = subject.get(3)
-
-        expect(results.map(&:id)).to eq [3, 2]
-        expect(results.first.attributes.keys).to eq %w(id updated_at)
-      end
-    end
-
-    context 'when feed is being generated' do
-      before do
-        Redis.current.set("account:#{account.id}:regeneration", true)
-      end
-
-      it 'returns nothing' do
-        results = subject.get(3)
-
-        expect(results.map(&:id)).to eq []
-      end
-    end
-  end
-end
diff --git a/spec/services/after_block_service_spec.rb b/spec/services/after_block_service_spec.rb
deleted file mode 100644
index f63b2045ad..0000000000
--- a/spec/services/after_block_service_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe AfterBlockService, type: :service do
-  subject do
-    -> { described_class.new.call(account, target_account) }
-  end
-
-  let(:account) { Fabricate(:account) }
-  let(:target_account) { Fabricate(:account) }
-
-  describe 'home timeline' do
-    let(:status) { Fabricate(:status, account: target_account) }
-    let(:other_account_status) { Fabricate(:status) }
-    let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
-
-    before do
-      Redis.current.del(home_timeline_key)
-    end
-
-    it "clears account's statuses" do
-      FeedManager.instance.push_to_home(account, status)
-      FeedManager.instance.push_to_home(account, other_account_status)
-
-      is_expected.to change {
-        Redis.current.zrange(home_timeline_key, 0, -1)
-      }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
-    end
-  end
-end
diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb
index 4bb839b8d4..f1f13bb449 100644
--- a/spec/services/mute_service_spec.rb
+++ b/spec/services/mute_service_spec.rb
@@ -8,25 +8,6 @@ RSpec.describe MuteService, type: :service do
   let(:account) { Fabricate(:account) }
   let(:target_account) { Fabricate(:account) }
 
-  describe 'home timeline' do
-    let(:status) { Fabricate(:status, account: target_account) }
-    let(:other_account_status) { Fabricate(:status) }
-    let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
-
-    before do
-      Redis.current.del(home_timeline_key)
-    end
-
-    it "clears account's statuses" do
-      FeedManager.instance.push_to_home(account, status)
-      FeedManager.instance.push_to_home(account, other_account_status)
-
-      is_expected.to change {
-        Redis.current.zrange(home_timeline_key, 0, -1)
-      }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
-    end
-  end
-
   it 'mutes account' do
     is_expected.to change {
       account.muting?(target_account)
diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb
deleted file mode 100644
index 1f6b6ed883..0000000000
--- a/spec/services/precompute_feed_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe PrecomputeFeedService, type: :service do
-  subject { PrecomputeFeedService.new }
-
-  describe 'call' do
-    let(:account) { Fabricate(:account) }
-    it 'fills a user timeline with statuses' do
-      account = Fabricate(:account)
-      status = Fabricate(:status, account: account)
-
-      subject.call(account)
-
-      expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f)
-    end
-
-    it 'does not raise an error even if it could not find any status' do
-      account = Fabricate(:account)
-      subject.call(account)
-    end
-
-    it 'filters statuses' do
-      account = Fabricate(:account)
-      muted_account = Fabricate(:account)
-      Fabricate(:mute, account: account, target_account: muted_account)
-      reblog = Fabricate(:status, account: muted_account)
-      status = Fabricate(:status, account: account, reblog: reblog)
-
-      subject.call(account)
-
-      expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil
-    end
-  end
-end
diff --git a/spec/workers/feed_insert_worker_spec.rb b/spec/workers/feed_insert_worker_spec.rb
deleted file mode 100644
index 3509f1f50e..0000000000
--- a/spec/workers/feed_insert_worker_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe FeedInsertWorker do
-  subject { described_class.new }
-
-  describe 'perform' do
-    let(:follower) { Fabricate(:account) }
-    let(:status) { Fabricate(:status) }
-
-    context 'when there are no records' do
-      it 'skips push with missing status' do
-        instance = double(push_to_home: nil)
-        allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(nil, follower.id)
-
-        expect(result).to eq true
-        expect(instance).not_to have_received(:push_to_home)
-      end
-
-      it 'skips push with missing account' do
-        instance = double(push_to_home: nil)
-        allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(status.id, nil)
-
-        expect(result).to eq true
-        expect(instance).not_to have_received(:push_to_home)
-      end
-    end
-
-    context 'when there are real records' do
-      it 'skips the push when there is a filter' do
-        instance = double(push_to_home: nil, filter?: true)
-        allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(status.id, follower.id)
-
-        expect(result).to be_nil
-        expect(instance).not_to have_received(:push_to_home)
-      end
-
-      it 'pushes the status onto the home timeline without filter' do
-        instance = double(push_to_home: nil, filter?: false)
-        allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(status.id, follower.id)
-
-        expect(result).to be_nil
-        expect(instance).to have_received(:push_to_home).with(follower, status)
-      end
-    end
-  end
-end
diff --git a/spec/workers/regeneration_worker_spec.rb b/spec/workers/regeneration_worker_spec.rb
deleted file mode 100644
index c6bdfa0e5e..0000000000
--- a/spec/workers/regeneration_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe RegenerationWorker do
-  subject { described_class.new }
-
-  describe 'perform' do
-    let(:account) { Fabricate(:account) }
-
-    it 'calls the precompute feed service for the account' do
-      service = double(call: nil)
-      allow(PrecomputeFeedService).to receive(:new).and_return(service)
-      result = subject.perform(account.id)
-
-      expect(result).to be_nil
-      expect(service).to have_received(:call).with(account)
-    end
-
-    it 'fails when account does not exist' do
-      result = subject.perform('aaa')
-
-      expect(result).to eq(true)
-    end
-  end
-end
diff --git a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb
deleted file mode 100644
index 7fae680ba6..0000000000
--- a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'rails_helper'
-
-describe Scheduler::FeedCleanupScheduler do
-  subject { described_class.new }
-
-  let!(:active_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) }
-  let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) }
-
-  it 'clears feeds of inactives' do
-    Redis.current.zadd(feed_key_for(inactive_user), 1, 1)
-    Redis.current.zadd(feed_key_for(active_user), 1, 1)
-    Redis.current.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
-    Redis.current.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
-
-    subject.perform
-
-    expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0
-    expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1
-    expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs'))).to be false
-    expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs:2'))).to be false
-  end
-
-  def feed_key_for(user, subtype = nil)
-    FeedManager.instance.key(:home, user.account_id, subtype)
-  end
-end
-- 
GitLab