From 87a55e2cd67a2af754f5a80282635e789c255e72 Mon Sep 17 00:00:00 2001
From: Mashiro <moezhx@outlook.com>
Date: Sat, 5 Mar 2022 12:57:50 +0800
Subject: [PATCH 1/8] Change autocomplete type of password input box (#17702)

* Change autocomplete type of password input box

* Update _registration.html.haml
---
 app/views/about/_registration.html.haml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index b452e49368..5db620b2d1 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -10,8 +10,8 @@
         = account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: disabled
 
       = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: disabled
-      = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
-      = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: disabled
+      = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
+      = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'new-password' }, hint: false, disabled: disabled
 
       = f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: disabled
       = f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: disabled

From b5329e0035d455e72dad7249d88bd624b5cb59a0 Mon Sep 17 00:00:00 2001
From: Josh Soref <2119212+jsoref@users.noreply.github.com>
Date: Sun, 6 Mar 2022 16:51:40 -0500
Subject: [PATCH 2/8] Spelling (#17705)

* spelling: account

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: affiliated

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: appearance

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: autosuggest

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: cacheable

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: component

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: conversations

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: domain.example

Clarify what's distinct and use RFC friendly domain space.

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: environment

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: exceeds

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: functional

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: inefficiency

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: not

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: notifications

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: occurring

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: position

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: progress

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: promotable

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: reblogging

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: repetitive

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: resolve

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: saturated

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: similar

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: strategies

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: success

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: targeting

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: thumbnails

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: unauthorized

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: unsensitizes

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: validations

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

* spelling: various

Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>

Co-authored-by: Josh Soref <jsoref@users.noreply.github.com>
---
 .circleci/config.yml                          |  2 +-
 .env.production.sample                        |  2 +-
 CHANGELOG.md                                  | 20 +++++++-------
 .../mastodon/components/scrollable_list.js    |  2 +-
 .../mastodon/containers/media_container.js    |  4 +--
 .../mastodon/features/report/category.js      |  2 +-
 .../mastodon/features/video/index.js          |  4 +--
 app/javascript/mastodon/locales/en.json       |  2 +-
 .../styles/mastodon/components.scss           |  2 +-
 app/javascript/styles/mastodon/polls.scss     |  2 +-
 app/policies/user_policy.rb                   |  4 +--
 config/initializers/omniauth.rb               |  2 +-
 config/locales/en_GB.yml                      |  2 +-
 .../20170920032311_fix_reblogs_in_feeds.rb    |  2 +-
 ...08213548_reject_following_blocked_users.rb |  6 ++---
 lib/mastodon/maintenance_cli.rb               |  2 +-
 lib/mastodon/statuses_cli.rb                  |  2 +-
 spec/controllers/accounts_controller_spec.rb  |  8 +++---
 .../collections_controller_spec.rb            |  6 ++---
 .../activitypub/outboxes_controller_spec.rb   |  6 ++---
 .../activitypub/replies_controller_spec.rb    |  4 +--
 .../api/v1/accounts/notes_controller_spec.rb  |  2 +-
 .../api/v1/admin/accounts_controller_spec.rb  |  2 +-
 .../favourited_by_accounts_controller_spec.rb |  2 +-
 .../reblogged_by_accounts_controller_spec.rb  |  2 +-
 .../api/v1/statuses_controller_spec.rb        |  4 +--
 .../application_controller_spec.rb            | 20 +++++++-------
 spec/controllers/statuses_controller_spec.rb  |  6 ++---
 spec/helpers/application_helper_spec.rb       |  2 +-
 spec/lib/tag_manager_spec.rb                  | 26 +++++++++----------
 spec/models/report_spec.rb                    |  2 +-
 spec/policies/user_policy_spec.rb             |  4 +--
 .../unsuspend_account_service_spec.rb         | 16 ++++++------
 spec/support/stories/profile_stories.rb       |  2 +-
 34 files changed, 88 insertions(+), 88 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index e157f8f223..318f4d2be1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -153,7 +153,7 @@ jobs:
       - run:
           command: ./bin/rails db:migrate
           name: Run all pre-deployment migrations
-          evironment:
+          environment:
             SKIP_POST_DEPLOYMENT_MIGRATIONS: true
       - run:
           command: ./bin/rails db:migrate
diff --git a/.env.production.sample b/.env.production.sample
index 8eeff3794a..4fc58072f1 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -58,7 +58,7 @@ SMTP_SERVER=smtp.mailgun.org
 SMTP_PORT=587
 SMTP_LOGIN=
 SMTP_PASSWORD=
-SMTP_FROM_ADDRESS=notificatons@example.com
+SMTP_FROM_ADDRESS=notifications@example.com
 
 # File storage (optional)
 # -----------------------
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e9d6ea1d8..d8fde27fe3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,7 +87,7 @@ All notable changes to this project will be documented in this file.
 - Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
 - Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
 - Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
-- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
+- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
 - Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
 - Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
 
@@ -420,7 +420,7 @@ All notable changes to this project will be documented in this file.
 - Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674))
 - Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673))
 - Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675))
-- Fix inefficieny when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
+- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
 - Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534))
 - Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287))
 - Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282))
@@ -507,7 +507,7 @@ All notable changes to this project will be documented in this file.
 - Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260))
   - In web UI, toots cannot be marked as sensitive unless there is media attached
   - However, it's possible to do via API or ActivityPub
-  - Thumnails of link previews of such posts now use blurhash in web UI
+  - Thumbnails of link previews of such posts now use blurhash in web UI
   - The Card entity in REST API has a new `blurhash` attribute
 - Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763))
 - Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195))
@@ -530,7 +530,7 @@ All notable changes to this project will be documented in this file.
   - The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent`
   - The background color is chosen from the most dominant color around the edges of the thumbnail
   - The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm
-  - The most satured color of the two is designated as the accent color
+  - The most saturated color of the two is designated as the accent color
   - The one with the highest W3C contrast is designated as the foreground color
   - If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern
 - Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292))
@@ -556,7 +556,7 @@ All notable changes to this project will be documented in this file.
 - Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373))
 - Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892))
 - Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834))
-- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
+- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
 - Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954))
 - Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015))
 - Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930))
@@ -578,8 +578,8 @@ All notable changes to this project will be documented in this file.
 ### Fixed
 
 - Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394))
-- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
-- Fix RSS feeds not being cachable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
+- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
+- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
 - Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365))
 - Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339))
 - Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061))
@@ -706,7 +706,7 @@ All notable changes to this project will be documented in this file.
 - Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490))
 - Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480))
 - Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475))
-- Fix varioues issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
+- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
 - Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441))
 - Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445))
 - Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436))
@@ -1496,7 +1496,7 @@ All notable changes to this project will be documented in this file.
 - Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212))
 - Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059))
 - Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339))
-- Change web UI to not not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
+- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
 - Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378))
 - Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924))
 - Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289))
@@ -1673,7 +1673,7 @@ All notable changes to this project will be documented in this file.
 - Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583))
 - Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625))
 - Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378))
-- Do not ignore federated reports targetting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
+- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
 - Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688))
 - Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689))
 - Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438))
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 68a1785122..91d04bf4d8 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -151,7 +151,7 @@ class ScrollableList extends PureComponent {
 
     attachFullscreenListener(this.onFullScreenChange);
 
-    // Handle initial scroll posiiton
+    // Handle initial scroll position
     this.handleScroll();
   }
 
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 2f42a084fd..6ee1f0bd88 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent {
 
   handleOpenVideo = (options) => {
     const { components } = this.props;
-    const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
+    const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
     const mediaList = fromJS(media);
 
     document.body.classList.add('with-modals--active');
@@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent {
               ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
 
               ...(componentName === 'Video' ? {
-                componetIndex: i,
+                componentIndex: i,
                 onOpenVideo: this.handleOpenVideo,
               } : {
                 onOpenMedia: this.handleOpenMedia,
diff --git a/app/javascript/mastodon/features/report/category.js b/app/javascript/mastodon/features/report/category.js
index 122b51c7cd..a36dc81b12 100644
--- a/app/javascript/mastodon/features/report/category.js
+++ b/app/javascript/mastodon/features/report/category.js
@@ -8,7 +8,7 @@ const messages = defineMessages({
   dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
   dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
   spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
-  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
+  spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
   violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
   violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
   other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 70e3cd6e88..8d47e479a8 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -121,7 +121,7 @@ class Video extends React.PureComponent {
     autoPlay: PropTypes.bool,
     volume: PropTypes.number,
     muted: PropTypes.bool,
-    componetIndex: PropTypes.number,
+    componentIndex: PropTypes.number,
   };
 
   static defaultProps = {
@@ -502,7 +502,7 @@ class Video extends React.PureComponent {
       startTime: this.video.currentTime,
       autoPlay: !this.state.paused,
       defaultVolume: this.state.volume,
-      componetIndex: this.props.componetIndex,
+      componentIndex: this.props.componentIndex,
     });
   }
 
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index e87541e284..c3d23795ba 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -409,7 +409,7 @@
   "report.reasons.other": "It's something else",
   "report.reasons.other_description": "The issue does not fit into other categories",
   "report.reasons.spam": "It's spam",
-  "report.reasons.spam_description": "Malicious links, fake engagement, or repetetive replies",
+  "report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
   "report.reasons.violation": "It violates server rules",
   "report.reasons.violation_description": "You are aware that it breaks specific rules",
   "report.rules.subtitle": "Select all that apply",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 6b18ca6f21..b8c3dba615 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4315,7 +4315,7 @@ a.status-card.compact:hover {
   }
 }
 
-.upload-progess__message {
+.upload-progress__message {
   flex: 1 1 auto;
 }
 
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index e33fc79835..a719044ea2 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -69,7 +69,7 @@
       display: none;
     }
 
-    .autossugest-input {
+    .autosuggest-input {
       flex: 1 1 auto;
     }
 
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 6695a0ddf8..92e2c4f4bb 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -42,7 +42,7 @@ class UserPolicy < ApplicationPolicy
   end
 
   def promote?
-    admin? && promoteable?
+    admin? && promotable?
   end
 
   def demote?
@@ -51,7 +51,7 @@ class UserPolicy < ApplicationPolicy
 
   private
 
-  def promoteable?
+  def promotable?
     record.approved? && (!record.staff? || !record.admin?)
   end
 
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 19d59f1551..1a041ad481 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -1,5 +1,5 @@
 Rails.application.config.middleware.use OmniAuth::Builder do
-  # Vanilla omniauth stategies
+  # Vanilla omniauth strategies
 end
 
 Devise.setup do |config|
diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml
index d3461474b1..de74e0f611 100644
--- a/config/locales/en_GB.yml
+++ b/config/locales/en_GB.yml
@@ -986,7 +986,7 @@ en_GB:
     enabled: Two-factor authentication is enabled
     enabled_success: Two-factor authentication successfully enabled
     generate_recovery_codes: Generate recovery codes
-    instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
+    instructions_html: "<strong>Scan this QR code into Google Authenticator or a similar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
     lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
     manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
     recovery_codes: Backup recovery codes
diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
index bcd4b91376..4ab68e8f32 100644
--- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
+++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
@@ -16,7 +16,7 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
     # is once again set to the reblogging status' ID, and the value
     # is set to the reblogged status' ID). This is safe for Redis'
     # float conversion because in this reblog tracking zset, we only
-    # need the rebloggging status' ID to be able to stop tracking
+    # need the reblogging status' ID to be able to stop tracking
     # entries after they have gotten too far down the feed, which
     # does not require an exact value.
 
diff --git a/db/migrate/20180608213548_reject_following_blocked_users.rb b/db/migrate/20180608213548_reject_following_blocked_users.rb
index 302db6b68d..78f8df5383 100644
--- a/db/migrate/20180608213548_reject_following_blocked_users.rb
+++ b/db/migrate/20180608213548_reject_following_blocked_users.rb
@@ -22,13 +22,13 @@ class RejectFollowingBlockedUsers < ActiveRecord::Migration[5.2]
 
     follows.each do |follow|
       blocked_account = follow.account
-      followed_acccount = follow.target_account
+      followed_account = follow.target_account
 
       next follow.destroy! if blocked_account.local?
 
-      reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_acccount))
+      reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account))
 
-      ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_acccount, blocked_account.inbox_url)
+      ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_account, blocked_account.inbox_url)
 
       follow.destroy!
     end
diff --git a/lib/mastodon/maintenance_cli.rb b/lib/mastodon/maintenance_cli.rb
index 00861df774..1e047d96c6 100644
--- a/lib/mastodon/maintenance_cli.rb
+++ b/lib/mastodon/maintenance_cli.rb
@@ -510,7 +510,7 @@ module Mastodon
       accounts = accounts.sort_by(&:id).reverse
 
       @prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'."
-      @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functionnal.'
+      @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.'
 
       accounts.each_with_index do |account, idx|
         @prompt.say '%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s' % [idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A']
diff --git a/lib/mastodon/statuses_cli.rb b/lib/mastodon/statuses_cli.rb
index 91b08813b3..d4c2e6cf2a 100644
--- a/lib/mastodon/statuses_cli.rb
+++ b/lib/mastodon/statuses_cli.rb
@@ -156,7 +156,7 @@ module Mastodon
 
         ActiveRecord::Base.connection.add_index(:statuses, :conversation_id, name: :index_statuses_conversation_id, algorithm: :concurrently, if_not_exists: true)
 
-        say('Extract the deletion target from coversations... This might take a while...')
+        say('Extract the deletion target from conversations... This might take a while...')
 
         ActiveRecord::Base.connection.create_table('conversations_to_be_deleted', force: true)
 
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 73d1240291..662a89927d 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe AccountsController, type: :controller do
 
   let(:account) { Fabricate(:account) }
 
-  shared_examples 'cachable response' do
+  shared_examples 'cacheable response' do
     it 'does not set cookies' do
       expect(response.cookies).to be_empty
       expect(response.headers['Set-Cookies']).to be nil
@@ -374,7 +374,7 @@ RSpec.describe AccountsController, type: :controller do
           expect(response.media_type).to eq 'application/activity+json'
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'renders account' do
           json = body_as_json
@@ -432,7 +432,7 @@ RSpec.describe AccountsController, type: :controller do
           expect(response.media_type).to eq 'application/activity+json'
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'renders account' do
           json = body_as_json
@@ -499,7 +499,7 @@ RSpec.describe AccountsController, type: :controller do
           expect(response).to have_http_status(200)
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
       end
 
       context do
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 21a0339458..4d87f80ced 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
   let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
   let(:remote_account) { nil }
 
-  shared_examples 'cachable response' do
+  shared_examples 'cacheable response' do
     it 'does not set cookies' do
       expect(response.cookies).to be_empty
       expect(response.headers['Set-Cookies']).to be nil
@@ -48,7 +48,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
           expect(response.media_type).to eq 'application/activity+json'
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'returns orderedItems with pinned statuses' do
           expect(body[:orderedItems]).to be_an Array
@@ -101,7 +101,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
             expect(response.media_type).to eq 'application/activity+json'
           end
 
-          it_behaves_like 'cachable response'
+          it_behaves_like 'cacheable response'
 
           it 'returns orderedItems with pinned statuses' do
             json = body_as_json
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 1722690db1..04f0364472 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 RSpec.describe ActivityPub::OutboxesController, type: :controller do
   let!(:account) { Fabricate(:account) }
 
-  shared_examples 'cachable response' do
+  shared_examples 'cacheable response' do
     it 'does not set cookies' do
       expect(response.cookies).to be_empty
       expect(response.headers['Set-Cookies']).to be nil
@@ -53,7 +53,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
           expect(body[:totalItems]).to eq 4
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'does not have a Vary header' do
           expect(response.headers['Vary']).to be_nil
@@ -98,7 +98,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
           expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'returns Vary header with Signature' do
           expect(response.headers['Vary']).to include 'Signature'
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
index a2c7f336ff..a35957f24c 100644
--- a/spec/controllers/activitypub/replies_controller_spec.rb
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
   let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
   let(:remote_querier) { nil }
 
-  shared_examples 'cachable response' do
+  shared_examples 'cacheable response' do
     it 'does not set cookies' do
       expect(response.cookies).to be_empty
       expect(response.headers['Set-Cookies']).to be nil
@@ -93,7 +93,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
         expect(response.media_type).to eq 'application/activity+json'
       end
 
-      it_behaves_like 'cachable response'
+      it_behaves_like 'cacheable response'
 
       context 'without only_other_accounts' do
         it "returns items with thread author's replies" do
diff --git a/spec/controllers/api/v1/accounts/notes_controller_spec.rb b/spec/controllers/api/v1/accounts/notes_controller_spec.rb
index 47d595c705..42c2d8a86f 100644
--- a/spec/controllers/api/v1/accounts/notes_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/notes_controller_spec.rb
@@ -31,7 +31,7 @@ describe Api::V1::Accounts::NotesController do
       end
     end
 
-    context 'when account note exceends allowed length' do
+    context 'when account note exceeds allowed length' do
       let(:comment) { 'a' * 2_001 }
 
       it 'returns 422' do
diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
index bf79ee5202..3f61bbc0ba 100644
--- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
@@ -140,7 +140,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
       expect(response).to have_http_status(200)
     end
 
-    it 'unsensitives account' do
+    it 'unsensitizes account' do
       expect(account.reload.sensitized?).to be false
     end
   end
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
index 439a4738df..7cc77f4308 100644
--- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
           Fabricate(:favourite, status: status)
         end
 
-        it 'returns http unautharized' do
+        it 'returns http unauthorized' do
           get :index, params: { status_id: status.id }
           expect(response).to have_http_status(404)
         end
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
index 31320349df..8d4a6f91ca 100644
--- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
           Fabricate(:status, reblog_of_id: status.id)
         end
 
-        it 'returns http unautharized' do
+        it 'returns http unauthorized' do
           get :index, params: { status_id: status.id }
           expect(response).to have_http_status(404)
         end
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index 190dfad117..2eb30af74b 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
       let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
 
       describe 'GET #show' do
-        it 'returns http unautharized' do
+        it 'returns http unauthorized' do
           get :show, params: { id: status.id }
           expect(response).to have_http_status(404)
         end
@@ -141,7 +141,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
           Fabricate(:status, account: user.account, thread: status)
         end
 
-        it 'returns http unautharized' do
+        it 'returns http unauthorized' do
           get :context, params: { id: status.id }
           expect(response).to have_http_status(404)
         end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 0fb4ddbcf5..53e163d493 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -187,30 +187,30 @@ describe ApplicationController, type: :controller do
     controller do
       before_action :require_admin!
 
-      def sucesss
+      def success
         head 200
       end
     end
 
     before do
-      routes.draw { get 'sucesss' => 'anonymous#sucesss' }
+      routes.draw { get 'success' => 'anonymous#success' }
     end
 
     it 'returns a 403 if current user is not admin' do
       sign_in(Fabricate(:user, admin: false))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(403)
     end
 
     it 'returns a 403 if current user is only a moderator' do
       sign_in(Fabricate(:user, moderator: true))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(403)
     end
 
     it 'does nothing if current user is admin' do
       sign_in(Fabricate(:user, admin: true))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(200)
     end
   end
@@ -219,30 +219,30 @@ describe ApplicationController, type: :controller do
     controller do
       before_action :require_staff!
 
-      def sucesss
+      def success
         head 200
       end
     end
 
     before do
-      routes.draw { get 'sucesss' => 'anonymous#sucesss' }
+      routes.draw { get 'success' => 'anonymous#success' }
     end
 
     it 'returns a 403 if current user is not admin or moderator' do
       sign_in(Fabricate(:user, admin: false, moderator: false))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(403)
     end
 
     it 'does nothing if current user is moderator' do
       sign_in(Fabricate(:user, moderator: true))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(200)
     end
 
     it 'does nothing if current user is admin' do
       sign_in(Fabricate(:user, admin: true))
-      get 'sucesss'
+      get 'success'
       expect(response).to have_http_status(200)
     end
   end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 9986efa51d..05fae67fab 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -5,7 +5,7 @@ require 'rails_helper'
 describe StatusesController do
   render_views
 
-  shared_examples 'cachable response' do
+  shared_examples 'cacheable response' do
     it 'does not set cookies' do
       expect(response.cookies).to be_empty
       expect(response.headers['Set-Cookies']).to be nil
@@ -108,7 +108,7 @@ describe StatusesController do
           expect(response.headers['Vary']).to eq 'Accept'
         end
 
-        it_behaves_like 'cachable response'
+        it_behaves_like 'cacheable response'
 
         it 'returns Content-Type header' do
           expect(response.headers['Content-Type']).to include 'application/activity+json'
@@ -496,7 +496,7 @@ describe StatusesController do
             expect(response.headers['Vary']).to eq 'Accept'
           end
 
-          it_behaves_like 'cachable response'
+          it_behaves_like 'cacheable response'
 
           it 'returns Content-Type header' do
             expect(response.headers['Content-Type']).to include 'application/activity+json'
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f09e32ecc2..b9d38d8c68 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -60,7 +60,7 @@ describe ApplicationHelper do
   end
 
   describe 'favicon_path' do
-    it 'returns /favicon.ico on production enviromnent' do
+    it 'returns /favicon.ico on production environment' do
       expect(Rails.env).to receive(:production?).and_return(true)
       expect(helper.favicon_path).to eq '/favicon.ico'
     end
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 2230f97103..cd9fb936cd 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe TagManager do
 
     around do |example|
       original_local_domain = Rails.configuration.x.local_domain
-      Rails.configuration.x.local_domain = 'domain.test'
+      Rails.configuration.x.local_domain = 'domain.example.com'
 
       example.run
 
@@ -18,11 +18,11 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the slash-stripped string equals to local domain' do
-      expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true
+      expect(TagManager.instance.local_domain?('DoMaIn.Example.com/')).to eq true
     end
 
     it 'returns false for irrelevant string' do
-      expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false
+      expect(TagManager.instance.local_domain?('DoMaIn.Example.com!')).to eq false
     end
   end
 
@@ -31,7 +31,7 @@ RSpec.describe TagManager do
 
     around do |example|
       original_web_domain = Rails.configuration.x.web_domain
-      Rails.configuration.x.web_domain = 'domain.test'
+      Rails.configuration.x.web_domain = 'domain.example.com'
 
       example.run
 
@@ -43,11 +43,11 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the slash-stripped string equals to web domain' do
-      expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true
+      expect(TagManager.instance.web_domain?('DoMaIn.Example.com/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false
+      expect(TagManager.instance.web_domain?('DoMaIn.Example.com!')).to eq false
     end
   end
 
@@ -57,7 +57,7 @@ RSpec.describe TagManager do
     end
 
     it 'returns normalized domain' do
-      expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test'
+      expect(TagManager.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com'
     end
   end
 
@@ -69,18 +69,18 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the normalized string with port is local URL' do
-      Rails.configuration.x.web_domain = 'domain.test:42'
-      expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true
+      Rails.configuration.x.web_domain = 'domain.example.com:42'
+      expect(TagManager.instance.local_url?('https://DoMaIn.Example.com:42/')).to eq true
     end
 
     it 'returns true if the normalized string without port is local URL' do
-      Rails.configuration.x.web_domain = 'domain.test'
-      expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true
+      Rails.configuration.x.web_domain = 'domain.example.com'
+      expect(TagManager.instance.local_url?('https://DoMaIn.Example.com/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      Rails.configuration.x.web_domain = 'domain.test'
-      expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
+      Rails.configuration.x.web_domain = 'domain.example.com'
+      expect(TagManager.instance.local_url?('https://domain.example.net/')).to eq false
     end
   end
 end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index 3d29c02195..df32a7c9d5 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -119,7 +119,7 @@ describe Report do
     end
   end
 
-  describe 'validatiions' do
+  describe 'validations' do
     it 'has a valid fabricator' do
       report = Fabricate(:report)
       report.valid?
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 1933ee014f..731c041d11 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -114,13 +114,13 @@ RSpec.describe UserPolicy do
 
   permissions :promote? do
     context 'admin?' do
-      context 'promoteable?' do
+      context 'promotable?' do
         it 'permits' do
           expect(subject).to permit(admin, john.user)
         end
       end
 
-      context '!promoteable?' do
+      context '!promotable?' do
         it 'denies' do
           expect(subject).to_not permit(admin, admin.user)
         end
diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb
index d52cb6cc00..0593beb6f2 100644
--- a/spec/services/unsuspend_account_service_spec.rb
+++ b/spec/services/unsuspend_account_service_spec.rb
@@ -63,20 +63,20 @@ RSpec.describe UnsuspendAccountService, type: :service do
   describe 'unsuspending a remote account' do
     include_examples 'common behavior' do
       let!(:account)                 { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
-      let!(:reslove_account_service) { double }
+      let!(:resolve_account_service) { double }
 
       before do
-        allow(ResolveAccountService).to receive(:new).and_return(reslove_account_service)
+        allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service)
       end
 
       context 'when the account is not remotely suspended' do
         before do
-          allow(reslove_account_service).to receive(:call).with(account).and_return(account)
+          allow(resolve_account_service).to receive(:call).with(account).and_return(account)
         end
 
         it 're-fetches the account' do
           subject.call
-          expect(reslove_account_service).to have_received(:call).with(account)
+          expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "merges back into local followers' feeds" do
@@ -92,7 +92,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
 
       context 'when the account is remotely suspended' do
         before do
-          allow(reslove_account_service).to receive(:call).with(account) do |account|
+          allow(resolve_account_service).to receive(:call).with(account) do |account|
             account.suspend!(origin: :remote)
             account
           end
@@ -100,7 +100,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
 
         it 're-fetches the account' do
           subject.call
-          expect(reslove_account_service).to have_received(:call).with(account)
+          expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "does not merge back into local followers' feeds" do
@@ -116,12 +116,12 @@ RSpec.describe UnsuspendAccountService, type: :service do
 
       context 'when the account is remotely deleted' do
         before do
-          allow(reslove_account_service).to receive(:call).with(account).and_return(nil)
+          allow(resolve_account_service).to receive(:call).with(account).and_return(nil)
         end
 
         it 're-fetches the account' do
           subject.call
-          expect(reslove_account_service).to have_received(:call).with(account)
+          expect(resolve_account_service).to have_received(:call).with(account)
         end
 
         it "does not merge back into local followers' feeds" do
diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb
index 75b413330f..0c4a14d1c9 100644
--- a/spec/support/stories/profile_stories.rb
+++ b/spec/support/stories/profile_stories.rb
@@ -22,7 +22,7 @@ module ProfileStories
   def with_alice_as_local_user
     @alice_bio = '@alice and @bob are fictional characters commonly used as'\
                  'placeholder names in #cryptology, as well as #science and'\
-                 'engineering 📖 literature. Not affilated with @pepe.'
+                 'engineering 📖 literature. Not affiliated with @pepe.'
 
     @alice = Fabricate(
       :user,

From c439e13e1231acd763b38ea6287b60edf51cadc8 Mon Sep 17 00:00:00 2001
From: Rens Groothuijsen <l.groothuijsen@alumni.maastrichtuniversity.nl>
Date: Sun, 6 Mar 2022 23:41:44 +0100
Subject: [PATCH 3/8] Enable importing GIF emojis in CLI (#17706)

---
 lib/mastodon/emoji_cli.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/mastodon/emoji_cli.rb b/lib/mastodon/emoji_cli.rb
index 5bee70ea51..a3e9479098 100644
--- a/lib/mastodon/emoji_cli.rb
+++ b/lib/mastodon/emoji_cli.rb
@@ -41,7 +41,7 @@ module Mastodon
 
       Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar|
         tar.each do |entry|
-          next unless entry.file? && entry.full_name.end_with?('.png')
+          next unless entry.file? && entry.full_name.end_with?('.png', '.gif')
 
           filename = File.basename(entry.full_name, '.*')
 

From edf09ec747ebba5a170e27eb13663462a116ec6c Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 7 Mar 2022 09:36:47 +0100
Subject: [PATCH 4/8] Add `/api/v1/accounts/familiar_followers` to REST API
 (#17700)

* Add `/api/v1/accounts/familiar_followers` to REST API

* Change hide network preference to be stored consistently for local and remote accounts

* Add dummy classes to migration

* Apply suggestions from code review

Co-authored-by: Claire <claire.github-309c@sitedethib.com>

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 .../accounts/familiar_followers_controller.rb | 25 ++++++++
 .../follower_accounts_controller.rb           |  6 +-
 .../following_accounts_controller.rb          |  6 +-
 .../settings/preferences_controller.rb        |  1 -
 .../settings/profiles_controller.rb           |  2 +-
 app/lib/user_settings_decorator.rb            |  5 --
 app/models/account.rb                         |  4 +-
 app/models/user.rb                            |  6 +-
 .../familiar_followers_presenter.rb           | 17 ++++++
 .../rest/familiar_followers_serializer.rb     | 11 ++++
 app/views/follower_accounts/index.html.haml   |  2 +-
 app/views/following_accounts/index.html.haml  |  2 +-
 .../settings/preferences/other/show.html.haml |  3 -
 app/views/settings/profiles/show.html.haml    |  5 +-
 config/locales/simple_form.en.yml             |  3 +-
 config/routes.rb                              |  1 +
 config/settings.yml                           |  1 -
 ...4195405_migrate_hide_network_preference.rb | 37 ++++++++++++
 db/schema.rb                                  |  2 +-
 .../follower_accounts_controller_spec.rb      |  2 +-
 .../following_accounts_controller_spec.rb     |  2 +-
 .../familiar_followers_presenter_spec.rb      | 58 +++++++++++++++++++
 22 files changed, 169 insertions(+), 32 deletions(-)
 create mode 100644 app/controllers/api/v1/accounts/familiar_followers_controller.rb
 create mode 100644 app/presenters/familiar_followers_presenter.rb
 create mode 100644 app/serializers/rest/familiar_followers_serializer.rb
 create mode 100644 db/migrate/20220304195405_migrate_hide_network_preference.rb
 create mode 100644 spec/presenters/familiar_followers_presenter_spec.rb

diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb
new file mode 100644
index 0000000000..b0bd8018a2
--- /dev/null
+++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read, :'read:follows' }
+  before_action :require_user!
+  before_action :set_accounts
+
+  def index
+    render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer
+  end
+
+  private
+
+  def set_accounts
+    @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact
+  end
+
+  def familiar_followers
+    FamiliarFollowersPresenter.new(@accounts, current_user.account_id)
+  end
+
+  def account_ids
+    Array(params[:id]).map(&:to_i)
+  end
+end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index b3589a39f8..f3f8336c95 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -15,13 +15,13 @@ class FollowerAccountsController < ApplicationController
       format.html do
         expires_in 0, public: true unless user_signed_in?
 
-        next if @account.user_hides_network?
+        next if @account.hide_collections?
 
         follows
       end
 
       format.json do
-        raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
+        raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
 
         expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
 
@@ -82,7 +82,7 @@ class FollowerAccountsController < ApplicationController
   end
 
   def restrict_fields_to
-    if page_requested? || !@account.user_hides_network?
+    if page_requested? || !@account.hide_collections?
       # Return all fields
     else
       %i(id type total_items)
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 8a72dc475b..9d7f4c9bf3 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -15,13 +15,13 @@ class FollowingAccountsController < ApplicationController
       format.html do
         expires_in 0, public: true unless user_signed_in?
 
-        next if @account.user_hides_network?
+        next if @account.hide_collections?
 
         follows
       end
 
       format.json do
-        raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
+        raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
 
         expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
 
@@ -82,7 +82,7 @@ class FollowingAccountsController < ApplicationController
   end
 
   def restrict_fields_to
-    if page_requested? || !@account.user_hides_network?
+    if page_requested? || !@account.hide_collections?
       # Return all fields
     else
       %i(id type total_items)
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 32b5d79487..c7492700cd 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -47,7 +47,6 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_system_font_ui,
       :setting_noindex,
       :setting_theme,
-      :setting_hide_network,
       :setting_aggregate_reblogs,
       :setting_show_application,
       :setting_advanced_layout,
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 0c15447a6c..be5b4f3029 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
   private
 
   def account_params
-    params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
+    params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value])
   end
 
   def set_account
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index e37bc6d9f1..de054e403e 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -31,7 +31,6 @@ class UserSettingsDecorator
     user.settings['system_font_ui']      = system_font_ui_preference if change?('setting_system_font_ui')
     user.settings['noindex']             = noindex_preference if change?('setting_noindex')
     user.settings['theme']               = theme_preference if change?('setting_theme')
-    user.settings['hide_network']        = hide_network_preference if change?('setting_hide_network')
     user.settings['aggregate_reblogs']   = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
     user.settings['show_application']    = show_application_preference if change?('setting_show_application')
     user.settings['advanced_layout']     = advanced_layout_preference if change?('setting_advanced_layout')
@@ -97,10 +96,6 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_noindex'
   end
 
-  def hide_network_preference
-    boolean_cast_setting 'setting_hide_network'
-  end
-
   def show_application_preference
     boolean_cast_setting 'setting_show_application'
   end
diff --git a/app/models/account.rb b/app/models/account.rb
index dfdf9045ff..1717f16055 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -349,11 +349,11 @@ class Account < ApplicationRecord
   end
 
   def hides_followers?
-    hide_collections? || user_hides_network?
+    hide_collections?
   end
 
   def hides_following?
-    hide_collections? || user_hides_network?
+    hide_collections?
   end
 
   def object_type
diff --git a/app/models/user.rb b/app/models/user.rb
index bbf850d84d..146bdcd2a9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -126,7 +126,7 @@ class User < ApplicationRecord
   has_many :session_activations, dependent: :destroy
 
   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
-           :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
+           :reduce_motion, :system_font_ui, :noindex, :theme, :display_media,
            :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
            :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
            :disable_swiping,
@@ -273,10 +273,6 @@ class User < ApplicationRecord
     settings.notification_emails['trending_tag']
   end
 
-  def hides_network?
-    @hides_network ||= settings.hide_network
-  end
-
   def aggregates_reblogs?
     @aggregates_reblogs ||= settings.aggregate_reblogs
   end
diff --git a/app/presenters/familiar_followers_presenter.rb b/app/presenters/familiar_followers_presenter.rb
new file mode 100644
index 0000000000..c1d944b80b
--- /dev/null
+++ b/app/presenters/familiar_followers_presenter.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class FamiliarFollowersPresenter
+  class Result < ActiveModelSerializers::Model
+    attributes :id, :accounts
+  end
+
+  def initialize(accounts, current_account_id)
+    @accounts           = accounts
+    @current_account_id = current_account_id
+  end
+
+  def accounts
+    map = Follow.includes(account: :account_stat).where(target_account_id: @accounts.map(&:id)).where(account_id: Follow.where(account_id: @current_account_id).joins(:target_account).merge(Account.where(hide_collections: [nil, false])).select(:target_account_id)).group_by(&:target_account_id)
+    @accounts.map { |account| Result.new(id: account.id, accounts: (account.hide_collections? ? [] : (map[account.id] || [])).map(&:account)) }
+  end
+end
diff --git a/app/serializers/rest/familiar_followers_serializer.rb b/app/serializers/rest/familiar_followers_serializer.rb
new file mode 100644
index 0000000000..0a7e923f80
--- /dev/null
+++ b/app/serializers/rest/familiar_followers_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class REST::FamiliarFollowersSerializer < ActiveModel::Serializer
+  attribute :id
+
+  has_many :accounts, serializer: REST::AccountSerializer
+
+  def id
+    object.id.to_s
+  end
+end
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 645dd2de17..92de35a9fc 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -7,7 +7,7 @@
 
 = render 'accounts/header', account: @account
 
-- if @account.user_hides_network?
+- if @account.hide_collections?
   .nothing-here= t('accounts.network_hidden')
 - elsif user_signed_in? && @account.blocking?(current_account)
   .nothing-here= t('accounts.unavailable')
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 17fe790188..9bb1a9eddf 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -7,7 +7,7 @@
 
 = render 'accounts/header', account: @account
 
-- if @account.user_hides_network?
+- if @account.hide_collections?
   .nothing-here= t('accounts.network_hidden')
 - elsif user_signed_in? && @account.blocking?(current_account)
   .nothing-here= t('accounts.unavailable')
diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml
index b7ae3d2ef7..44f4af2eba 100644
--- a/app/views/settings/preferences/other/show.html.haml
+++ b/app/views/settings/preferences/other/show.html.haml
@@ -10,9 +10,6 @@
   .fields-group
     = f.input :setting_noindex, as: :boolean, wrapper: :with_label
 
-  .fields-group
-    = f.input :setting_hide_network, as: :boolean, wrapper: :with_label
-
   .fields-group
     = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true
 
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index d325a9ea5c..fe9666d841 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -30,7 +30,10 @@
     = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
 
   .fields-group
-    = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t(Setting.profile_directory ? 'simple_form.hints.defaults.discoverable' : 'simple_form.hints.defaults.discoverable_no_directory'), recommended: true
+    = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true
+
+  .fields-group
+    = f.input :hide_collections, as: :boolean, wrapper: :with_label, label: t('simple_form.labels.defaults.setting_hide_network'), hint: t('simple_form.hints.defaults.setting_hide_network')
 
   %hr.spacer/
 
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index c5e75b408c..b19b7891fd 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -37,8 +37,7 @@ en:
         current_password: For security purposes please enter the password of the current account
         current_username: To confirm, please enter the username of the current account
         digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
-        discoverable: Allow your account to be discovered by strangers through recommendations, profile directory and other features
-        discoverable_no_directory: Allow your account to be discovered by strangers through recommendations and other features
+        discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features
         email: You will be sent a confirmation e-mail
         fields: You can have up to 4 items displayed as a table on your profile
         header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
diff --git a/config/routes.rb b/config/routes.rb
index 25eb1558fc..9e2f7a6482 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -493,6 +493,7 @@ Rails.application.routes.draw do
         resource :search, only: :show, controller: :search
         resource :lookup, only: :show, controller: :lookup
         resources :relationships, only: :index
+        resources :familiar_followers, only: :index
       end
 
       resources :accounts, only: [:create, :show] do
diff --git a/config/settings.yml b/config/settings.yml
index e63788ba2f..06dd2b3f3e 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -17,7 +17,6 @@ defaults: &defaults
   timeline_preview: true
   show_staff_badge: true
   default_sensitive: false
-  hide_network: false
   unfollow_modal: false
   boost_modal: false
   delete_modal: true
diff --git a/db/migrate/20220304195405_migrate_hide_network_preference.rb b/db/migrate/20220304195405_migrate_hide_network_preference.rb
new file mode 100644
index 0000000000..102ee46d6f
--- /dev/null
+++ b/db/migrate/20220304195405_migrate_hide_network_preference.rb
@@ -0,0 +1,37 @@
+class MigrateHideNetworkPreference < ActiveRecord::Migration[6.1]
+  disable_ddl_transaction!
+
+  # Dummy classes, to make migration possible across version changes
+  class Account < ApplicationRecord
+    has_one :user, inverse_of: :account
+    scope :local, -> { where(domain: nil) }
+  end
+
+  class User < ApplicationRecord
+    belongs_to :account
+  end
+
+  def up
+    Account.reset_column_information
+
+    Setting.unscoped.where(thing_type: 'User', var: 'hide_network').find_each do |setting|
+      account = User.find(setting.thing_id).account
+
+      ApplicationRecord.transaction do
+        account.update(hide_collections: setting.value)
+        setting.delete
+      end
+    rescue ActiveRecord::RecordNotFound
+      next
+    end
+  end
+
+  def down
+    Account.local.where(hide_collections: true).includes(:user).find_each do |account|
+      ApplicationRecord.transaction do
+        Setting.create(thing_type: 'User', thing_id: account.user.id, var: 'hide_network', value: account.hide_collections?)
+        account.update(hide_collections: nil)
+      end
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 756e5e9aba..3666804eec 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2022_02_27_041951) do
+ActiveRecord::Schema.define(version: 2022_03_04_195405) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index eb095cf30c..4d2a6e01a9 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -103,7 +103,7 @@ describe FollowerAccountsController do
 
         context 'when account hides their network' do
           before do
-            alice.user.settings.hide_network = true
+            alice.update(hide_collections: true)
           end
 
           it 'returns followers count' do
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index af5ce07870..bb6d221cac 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -103,7 +103,7 @@ describe FollowingAccountsController do
 
         context 'when account hides their network' do
           before do
-            alice.user.settings.hide_network = true
+            alice.update(hide_collections: true)
           end
 
           it 'returns followers count' do
diff --git a/spec/presenters/familiar_followers_presenter_spec.rb b/spec/presenters/familiar_followers_presenter_spec.rb
new file mode 100644
index 0000000000..17be4b9715
--- /dev/null
+++ b/spec/presenters/familiar_followers_presenter_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe FamiliarFollowersPresenter do
+  describe '#accounts' do
+    let(:account) { Fabricate(:account) }
+    let(:familiar_follower) { Fabricate(:account) }
+    let(:requested_accounts) { Fabricate.times(2, :account) }
+
+    subject { described_class.new(requested_accounts, account.id) }
+
+    before do
+      familiar_follower.follow!(requested_accounts.first)
+      account.follow!(familiar_follower)
+    end
+
+    it 'returns a result for each requested account' do
+      expect(subject.accounts.map(&:id)).to eq requested_accounts.map(&:id)
+    end
+
+    it 'returns followers you follow' do
+      result = subject.accounts.first
+
+      expect(result).to_not be_nil
+      expect(result.id).to eq requested_accounts.first.id
+      expect(result.accounts).to match_array([familiar_follower])
+    end
+
+    context 'when requested account hides followers' do
+      before do
+        requested_accounts.first.update(hide_collections: true)
+      end
+
+      it 'does not return followers you follow' do
+        result = subject.accounts.first
+
+        expect(result).to_not be_nil
+        expect(result.id).to eq requested_accounts.first.id
+        expect(result.accounts).to be_empty
+      end
+    end
+
+    context 'when familiar follower hides follows' do
+      before do
+        familiar_follower.update(hide_collections: true)
+      end
+
+      it 'does not return followers you follow' do
+        result = subject.accounts.first
+
+        expect(result).to_not be_nil
+        expect(result.id).to eq requested_accounts.first.id
+        expect(result.accounts).to be_empty
+      end
+    end
+  end
+end

From 292c75aa319c877406356a1cb4fb41f4ab597cfd Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 7 Mar 2022 11:21:27 +0100
Subject: [PATCH 5/8] Fix nil error when submitting report without `rule_ids`
 (#17713)

---
 app/models/report.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/models/report.rb b/app/models/report.rb
index 3dd8a6fdd4..8ba2dd8fd2 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -129,6 +129,6 @@ class Report < ApplicationRecord
   def validate_rule_ids
     return unless violation?
 
-    errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids.size
+    errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids&.size
   end
 end

From dba4be1038063845a74e83aaa85d6ab08d5625dd Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 7 Mar 2022 11:38:52 +0100
Subject: [PATCH 6/8] Change appearance of account cards in web UI (#17689)

* Change appearance of account cards in web UI

* Various fixes and improvements

* Various fixes and improvements
---
 .../directory/components/account_card.js      | 204 +++++++-----------
 .../mastodon/features/directory/index.js      |  10 +-
 .../mastodon/features/explore/suggestions.js  |   8 +-
 .../styles/mastodon-light/diff.scss           |  13 +-
 app/javascript/styles/mastodon/admin.scss     |  50 +++--
 .../styles/mastodon/components.scss           | 153 ++-----------
 .../styles/mastodon/containers.scss           |  24 +--
 app/javascript/styles/mastodon/rtl.scss       |   5 -
 app/views/directories/index.html.haml         |  57 +++--
 config/locales/en.yml                         |   1 -
 10 files changed, 178 insertions(+), 347 deletions(-)

diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
index 03e13f28e4..31f59cd848 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.js
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -7,31 +7,28 @@ import { makeGetAccount } from 'mastodon/selectors';
 import Avatar from 'mastodon/components/avatar';
 import DisplayName from 'mastodon/components/display_name';
 import Permalink from 'mastodon/components/permalink';
-import RelativeTimestamp from 'mastodon/components/relative_timestamp';
-import IconButton from 'mastodon/components/icon_button';
+import Button from 'mastodon/components/button';
 import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
 import ShortNumber from 'mastodon/components/short_number';
 import {
   followAccount,
   unfollowAccount,
-  blockAccount,
   unblockAccount,
   unmuteAccount,
 } from 'mastodon/actions/accounts';
 import { openModal } from 'mastodon/actions/modal';
-import { initMuteModal } from 'mastodon/actions/mutes';
+import classNames from 'classnames';
 
 const messages = defineMessages({
-  follow: { id: 'account.follow', defaultMessage: 'Follow' },
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
-  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
-  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
-  unfollowConfirm: {
-    id: 'confirmations.unfollow.confirm',
-    defaultMessage: 'Unfollow',
-  },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
+  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
+  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
+  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
+  unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
 });
 
 const makeMapStateToProps = () => {
@@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   onBlock(account) {
     if (account.getIn(['relationship', 'blocking'])) {
       dispatch(unblockAccount(account.get('id')));
-    } else {
-      dispatch(blockAccount(account.get('id')));
     }
   },
 
   onMute(account) {
     if (account.getIn(['relationship', 'muting'])) {
       dispatch(unmuteAccount(account.get('id')));
-    } else {
-      dispatch(initMuteModal(account));
     }
   },
+
 });
 
 export default
@@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent {
 
   handleMute = () => {
     this.props.onMute(this.props.account);
-  };
+  }
+
+  handleEditProfile = () => {
+    window.open('/settings/profile', '_blank');
+  }
 
   render() {
     const { account, intl } = this.props;
 
-    let buttons;
+    let actionBtn;
 
-    if (
-      account.get('id') !== me &&
-      account.get('relationship', null) !== null
-    ) {
-      const following = account.getIn(['relationship', 'following']);
-      const requested = account.getIn(['relationship', 'requested']);
-      const blocking = account.getIn(['relationship', 'blocking']);
-      const muting = account.getIn(['relationship', 'muting']);
-
-      if (requested) {
-        buttons = (
-          <IconButton
-            disabled
-            icon='hourglass'
-            title={intl.formatMessage(messages.requested)}
-          />
-        );
-      } else if (blocking) {
-        buttons = (
-          <IconButton
-            active
-            icon='unlock'
-            title={intl.formatMessage(messages.unblock, {
-              name: account.get('username'),
-            })}
-            onClick={this.handleBlock}
-          />
-        );
-      } else if (muting) {
-        buttons = (
-          <IconButton
-            active
-            icon='volume-up'
-            title={intl.formatMessage(messages.unmute, {
-              name: account.get('username'),
-            })}
-            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}
-          />
-        );
+    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={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
+      } else if (account.getIn(['relationship', 'muting'])) {
+        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
+      } 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.handleFollow} />;
+      } else if (account.getIn(['relationship', 'blocking'])) {
+        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
       }
+    } else {
+      actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
     }
 
     return (
-      <div className='directory__card'>
-        <div className='directory__card__img'>
-          <img
-            src={
-              autoPlayGif ? account.get('header') : account.get('header_static')
-            }
-            alt=''
-          />
-        </div>
-
-        <div className='directory__card__bar'>
-          <Permalink
-            className='directory__card__bar__name'
-            href={account.get('url')}
-            to={`/@${account.get('acct')}`}
-          >
-            <Avatar account={account} size={48} />
-            <DisplayName account={account} />
-          </Permalink>
-
-          <div className='directory__card__bar__relationship account__relationship'>
-            {buttons}
+      <div className='account-card'>
+        <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
+          <div className='account-card__header'>
+            <img
+              src={
+                autoPlayGif ? account.get('header') : account.get('header_static')
+              }
+              alt=''
+            />
           </div>
-        </div>
 
-        <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+          <div className='account-card__title'>
+            <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
+            <DisplayName account={account} />
+          </div>
+        </Permalink>
+
+        {account.get('note').length > 0 && (
           <div
-            className='account__header__content translate'
+            className='account-card__bio translate'
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
             dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
           />
-        </div>
+        )}
 
-        <div className='directory__card__extra'>
-          <div className='accounts-table__count'>
-            <ShortNumber value={account.get('statuses_count')} />
-            <small>
-              <FormattedMessage id='account.posts' defaultMessage='Toots' />
-            </small>
+        <div className='account-card__actions'>
+          <div className='account-card__counters'>
+            <div className='account-card__counters__item'>
+              <ShortNumber value={account.get('statuses_count')} />
+              <small>
+                <FormattedMessage id='account.posts' defaultMessage='Toots' />
+              </small>
+            </div>
+
+            <div className='account-card__counters__item'>
+              <ShortNumber value={account.get('followers_count')} />{' '}
+              <small>
+                <FormattedMessage
+                  id='account.followers'
+                  defaultMessage='Followers'
+                />
+              </small>
+            </div>
+
+            <div className='account-card__counters__item'>
+              <ShortNumber value={account.get('following_count')} />{' '}
+              <small>
+                <FormattedMessage
+                  id='account.following'
+                  defaultMessage='Following'
+                />
+              </small>
+            </div>
           </div>
-          <div className='accounts-table__count'>
-            <ShortNumber value={account.get('followers_count')} />{' '}
-            <small>
-              <FormattedMessage
-                id='account.followers'
-                defaultMessage='Followers'
-              />
-            </small>
-          </div>
-          <div className='accounts-table__count'>
-            {account.get('last_status_at') === null ? (
-              <FormattedMessage
-                id='account.never_active'
-                defaultMessage='Never'
-              />
-            ) : (
-              <RelativeTimestamp timestamp={account.get('last_status_at')} />
-            )}{' '}
-            <small>
-              <FormattedMessage
-                id='account.last_status'
-                defaultMessage='Last active'
-              />
-            </small>
+
+          <div className='account-card__actions__button'>
+            {actionBtn}
           </div>
         </div>
       </div>
diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js
index 88f20d3304..94d7d1a9c5 100644
--- a/app/javascript/mastodon/features/directory/index.js
+++ b/app/javascript/mastodon/features/directory/index.js
@@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 import { List as ImmutableList } from 'immutable';
 import AccountCard from './components/account_card';
 import RadioButton from 'mastodon/components/radio_button';
-import classNames from 'classnames';
 import LoadMore from 'mastodon/components/load_more';
 import ScrollContainer from 'mastodon/containers/scroll_container';
+import LoadingIndicator from 'mastodon/components/loading_indicator';
 
 const messages = defineMessages({
   title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
     const pinned = !!columnId;
 
     const scrollableArea = (
-      <div className='scrollable' style={{ background: 'transparent' }}>
+      <div className='scrollable'>
         <div className='filter-form'>
           <div className='filter-form__column' role='group'>
             <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
@@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
           </div>
         </div>
 
-        <div className={classNames('directory__list', { loading: isLoading })}>
-          {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
+        <div className='directory__list'>
+          {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
+            <AccountCard id={accountId} key={accountId} />
+          ))}
         </div>
 
         <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
diff --git a/app/javascript/mastodon/features/explore/suggestions.js b/app/javascript/mastodon/features/explore/suggestions.js
index c094a8d934..0c6a7ef8a3 100644
--- a/app/javascript/mastodon/features/explore/suggestions.js
+++ b/app/javascript/mastodon/features/explore/suggestions.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Account from 'mastodon/containers/account_container';
+import AccountCard from 'mastodon/features/directory/components/account_card';
 import LoadingIndicator from 'mastodon/components/loading_indicator';
 import { connect } from 'react-redux';
 import { fetchSuggestions } from 'mastodon/actions/suggestions';
@@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent {
     const { isLoading, suggestions } = this.props;
 
     return (
-      <div className='explore__links'>
-        {isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
-          <Account key={suggestion.get('account')} id={suggestion.get('account')} />
+      <div className='explore__suggestions'>
+        {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
+          <AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
         ))}
       </div>
     );
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 8e6b0cdd5b..eb6bdea990 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -40,19 +40,11 @@ html {
   background: lighten($ui-base-color, 12%);
 }
 
-.filter-form,
-.directory__card__bar {
+.filter-form {
   background: $white;
   border-bottom: 1px solid lighten($ui-base-color, 8%);
 }
 
-.scrollable .directory__list {
-  width: calc(100% + 2px);
-  margin-left: -1px;
-  margin-right: -1px;
-}
-
-.directory__card,
 .table-of-contents {
   border: 1px solid lighten($ui-base-color, 8%);
 }
@@ -75,8 +67,7 @@ html {
 .column-header__back-button,
 .column-header__button,
 .column-header__button.active,
-.account__header__bar,
-.directory__card__extra {
+.account__header__bar {
   background: $white;
 }
 
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 06ec4d37bc..42f055618b 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -1220,6 +1220,11 @@ a.sparkline {
   background: $ui-base-color;
   border-radius: 4px;
 
+  &__permalink {
+    color: inherit;
+    text-decoration: none;
+  }
+
   &__header {
     padding: 4px;
     border-radius: 4px;
@@ -1236,20 +1241,22 @@ a.sparkline {
   }
 
   &__title {
-    margin-top: -25px;
+    margin-top: -(15px + 8px);
     display: flex;
     align-items: flex-end;
 
     &__avatar {
-      padding: 15px;
+      padding: 14px;
 
-      img {
+      img,
+      .account__avatar {
         display: block;
         margin: 0;
         width: 56px;
         height: 56px;
-        background: darken($ui-base-color, 8%);
+        background-color: darken($ui-base-color, 8%);
         border-radius: 8px;
+        border: 1px solid $ui-base-color;
       }
     }
 
@@ -1257,30 +1264,34 @@ a.sparkline {
       color: $darker-text-color;
       padding-bottom: 15px;
       font-size: 15px;
+      line-height: 20px;
 
       bdi {
         display: block;
         color: $primary-text-color;
-        font-weight: 500;
+        font-weight: 700;
       }
     }
   }
 
   &__bio {
     padding: 0 15px;
+    margin: 8px 0;
     overflow: hidden;
     text-overflow: ellipsis;
     word-wrap: break-word;
-    max-height: 18px * 2;
+    max-height: 21px * 2;
     position: relative;
+    font-size: 15px;
+    line-height: 21px;
 
     &::after {
       display: block;
       content: "";
       width: 50px;
-      height: 18px;
+      height: 21px;
       position: absolute;
-      bottom: 0;
+      bottom: 8px;
       right: 15px;
       background: linear-gradient(to left, $ui-base-color, transparent);
       pointer-events: none;
@@ -1293,10 +1304,6 @@ a.sparkline {
 
       &:hover {
         text-decoration: underline;
-
-        .fa {
-          color: lighten($dark-text-color, 7%);
-        }
       }
 
       &.mention {
@@ -1313,12 +1320,21 @@ a.sparkline {
 
   &__actions {
     display: flex;
+    justify-content: space-between;
     align-items: center;
-    padding-top: 10px;
 
     &__button {
-      flex: 0 0 auto;
+      flex-shrink: 1;
       padding: 0 15px;
+      overflow: hidden;
+
+      .button {
+        min-width: 0;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        max-width: 100%;
+      }
     }
   }
 
@@ -1327,19 +1343,23 @@ a.sparkline {
     display: grid;
     grid-auto-columns: minmax(0, 1fr);
     grid-auto-flow: column;
+    max-width: 340px;
+    min-width: 65px * 3;
 
     &__item {
-      padding: 15px;
+      padding: 15px 0;
       text-align: center;
       color: $primary-text-color;
       font-weight: 600;
       font-size: 15px;
+      line-height: 21px;
 
       small {
         display: block;
         color: $darker-text-color;
         font-weight: 400;
         font-size: 13px;
+        line-height: 18px;
       }
     }
   }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index b8c3dba615..5d0d44b73e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -50,7 +50,7 @@
   cursor: pointer;
   display: inline-block;
   font-family: inherit;
-  font-size: 17px;
+  font-size: 15px;
   font-weight: 500;
   letter-spacing: 0;
   line-height: 22px;
@@ -2333,17 +2333,7 @@ a.account__display-name {
     padding: 0;
   }
 
-  .directory__list {
-    display: grid;
-    grid-gap: 10px;
-    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
-
-    @media screen and (max-width: $no-gap-breakpoint) {
-      display: block;
-    }
-  }
-
-  .directory__card {
+  .account-card {
     margin-bottom: 0;
   }
 
@@ -6219,136 +6209,20 @@ a.status-card.compact:hover {
   }
 }
 
-.directory {
-  &__list {
-    width: 100%;
-    margin: 10px 0;
-    transition: opacity 100ms ease-in;
+.scrollable .account-card {
+  margin: 10px;
+  background: lighten($ui-base-color, 8%);
+}
 
-    &.loading {
-      opacity: 0.7;
-    }
-
-    @media screen and (max-width: $no-gap-breakpoint) {
-      margin: 0;
-    }
+.scrollable .account-card__title__avatar {
+  img,
+  .account__avatar {
+    border-color: lighten($ui-base-color, 8%);
   }
+}
 
-  &__card {
-    box-sizing: border-box;
-    margin-bottom: 10px;
-
-    &__img {
-      height: 125px;
-      position: relative;
-      background: darken($ui-base-color, 12%);
-      overflow: hidden;
-
-      img {
-        display: block;
-        width: 100%;
-        height: 100%;
-        margin: 0;
-        object-fit: cover;
-      }
-    }
-
-    &__bar {
-      display: flex;
-      align-items: center;
-      background: lighten($ui-base-color, 4%);
-      padding: 10px;
-
-      &__name {
-        flex: 1 1 auto;
-        display: flex;
-        align-items: center;
-        text-decoration: none;
-        overflow: hidden;
-      }
-
-      &__relationship {
-        width: 23px;
-        min-height: 1px;
-        flex: 0 0 auto;
-      }
-
-      .avatar {
-        flex: 0 0 auto;
-        width: 48px;
-        height: 48px;
-        padding-top: 2px;
-
-        img {
-          width: 100%;
-          height: 100%;
-          display: block;
-          margin: 0;
-          border-radius: 4px;
-          background: darken($ui-base-color, 8%);
-          object-fit: cover;
-        }
-      }
-
-      .display-name {
-        margin-left: 15px;
-        text-align: left;
-
-        strong {
-          font-size: 15px;
-          color: $primary-text-color;
-          font-weight: 500;
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
-
-        span {
-          display: block;
-          font-size: 14px;
-          color: $darker-text-color;
-          font-weight: 400;
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
-      }
-    }
-
-    &__extra {
-      background: $ui-base-color;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      .accounts-table__count {
-        width: 33.33%;
-        flex: 0 0 auto;
-        padding: 15px 0;
-      }
-
-      .account__header__content {
-        box-sizing: border-box;
-        padding: 15px 10px;
-        border-bottom: 1px solid lighten($ui-base-color, 8%);
-        width: 100%;
-        min-height: 18px + 30px;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-
-        p {
-          display: none;
-
-          &:first-child {
-            display: inline;
-          }
-        }
-
-        br {
-          display: none;
-        }
-      }
-    }
-  }
+.scrollable .account-card__bio::after {
+  background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent);
 }
 
 .account-gallery__container {
@@ -6452,6 +6326,7 @@ a.status-card.compact:hover {
 
   &__column {
     padding: 10px 15px;
+    padding-bottom: 0;
   }
 
   .radio-button {
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index a180df437a..81459f5ba5 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -409,14 +409,6 @@
     }
   }
 
-  .directory__card {
-    border-radius: 4px;
-
-    @media screen and (max-width: $no-gap-breakpoint) {
-      border-radius: 0;
-    }
-  }
-
   .page-header {
     @media screen and (max-width: $no-gap-breakpoint) {
       border-bottom: 0;
@@ -835,19 +827,21 @@
     grid-gap: 10px;
     grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 
+    .account-card {
+      display: flex;
+      flex-direction: column;
+    }
+
     @media screen and (max-width: $no-gap-breakpoint) {
       display: block;
-    }
 
-    .icon-button {
-      font-size: 18px;
+      .account-card {
+        margin-bottom: 10px;
+        display: block;
+      }
     }
   }
 
-  .directory__card {
-    margin-bottom: 0;
-  }
-
   .card-grid {
     display: flex;
     flex-wrap: wrap;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index ea7bb5113c..98eb1511cc 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -12,11 +12,6 @@ body.rtl {
     margin-left: 10px;
   }
 
-  .directory__card__bar .display-name {
-    margin-left: 0;
-    margin-right: 15px;
-  }
-
   .display-name,
   .announcements__item {
     text-align: right;
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index 04639e32c2..2b338115bd 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -19,37 +19,36 @@
 - else
   .directory__list
     - @accounts.each do |account|
-      .directory__card
-        .directory__card__img
-          = image_tag account.header.url, alt: ''
-        .directory__card__bar
-          = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
-            .avatar
-              = image_tag account.avatar.url, alt: '', class: 'u-photo'
-
+      .account-card
+        = link_to TagManager.instance.url_for(account), class: 'account-card__permalink' do
+          .account-card__header
+            = image_tag account.header.url, alt: ''
+          .account-card__title
+            .account-card__title__avatar
+              = image_tag account.avatar.url, alt: ''
             .display-name
               %bdi
                 %strong.emojify.p-name= display_name(account, custom_emojify: true)
-              %span= acct(account)
-          .directory__card__bar__relationship.account__relationship
-            = minimal_account_action_button(account)
-
-        .directory__card__extra
-          .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
-
-        .directory__card__extra
-          .accounts-table__count
-            = friendly_number_to_human account.statuses_count
-            %small= t('accounts.posts', count: account.statuses_count).downcase
-          .accounts-table__count
-            = friendly_number_to_human account.followers_count
-            %small= t('accounts.followers', count: account.followers_count).downcase
-          .accounts-table__count
-            - if account.last_status_at.present?
-              %time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at.to_date
-            - else
-              = t('accounts.never_active')
-
-            %small= t('accounts.last_active')
+              %span
+                = acct(account)
+                = fa_icon('lock') if account.locked?
+        - if account.note.present?
+          .account-card__bio.emojify
+            = Formatter.instance.simplified_format(account, custom_emojify: true)
+        - else
+          .flex-spacer
+        .account-card__actions
+          .account-card__counters
+            .account-card__counters__item
+              = friendly_number_to_human account.statuses_count
+              %small= t('accounts.posts', count: account.statuses_count).downcase
+            .account-card__counters__item
+              = friendly_number_to_human account.followers_count
+              %small= t('accounts.followers', count: account.followers_count).downcase
+            .account-card__counters__item
+              = friendly_number_to_human account.following_count
+              %small= t('accounts.following', count: account.following_count).downcase
+          .account-card__actions__button
+            = account_action_button(account)
 
   = paginate @accounts
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2367d5ded4..1964285b05 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -72,7 +72,6 @@ en:
     media: Media
     moved_html: "%{name} has moved to %{new_profile_link}:"
     network_hidden: This information is not available
-    never_active: Never
     nothing_here: There is nothing here!
     people_followed_by: People whom %{name} follows
     people_who_follow: People who follow %{name}

From 61ae6b35357c1ca71f61d81e357e3851f0e3de8d Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 7 Mar 2022 23:40:55 +0100
Subject: [PATCH 7/8] Add more migration tests (#17710)

* Add migration tests for hide_network settings migration

* Add tests about suspended/suspended_at

* Add more tests regarding the results of migrations

* Fix migration test regarding stale conflicting remote account

* Add migration tests about AccountConversation
---
 .circleci/config.yml | 20 ++++++++++
 lib/tasks/tests.rake | 92 ++++++++++++++++++++++++++++++++++++++------
 2 files changed, 101 insertions(+), 11 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 318f4d2be1..6d89e670a2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -127,9 +127,18 @@ jobs:
       - run:
           command: ./bin/rails tests:migrations:populate_v2
           name: Populate database with test data
+      - run:
+          command: ./bin/rails db:migrate VERSION=20180514140000
+          name: Run migrations up to v2.4.0
+      - run:
+          command: ./bin/rails tests:migrations:populate_v2_4
+          name: Populate database with test data
       - run:
           command: ./bin/rails db:migrate
           name: Run all remaining migrations
+      - run:
+          command: ./bin/rails tests:migrations:check_database
+          name: Check migration result
 
   test-two-step-migrations:
     executor:
@@ -150,6 +159,14 @@ jobs:
       - run:
           command: ./bin/rails tests:migrations:populate_v2
           name: Populate database with test data
+      - run:
+          command: ./bin/rails db:migrate VERSION=20180514140000
+          name: Run pre-deployment migrations up to v2.4.0
+          environment:
+            SKIP_POST_DEPLOYMENT_MIGRATIONS: true
+      - run:
+          command: ./bin/rails tests:migrations:populate_v2_4
+          name: Populate database with test data
       - run:
           command: ./bin/rails db:migrate
           name: Run all pre-deployment migrations
@@ -158,6 +175,9 @@ jobs:
       - run:
           command: ./bin/rails db:migrate
           name: Run all post-deployment remaining migrations
+      - run:
+          command: ./bin/rails tests:migrations:check_database
+          name: Check migration result
 
 workflows:
   version: 2
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
index 0f38b50e39..8082f32fb4 100644
--- a/lib/tasks/tests.rake
+++ b/lib/tasks/tests.rake
@@ -2,6 +2,50 @@
 
 namespace :tests do
   namespace :migrations do
+    desc 'Check that database state is consistent with a successful migration from populated data'
+    task check_database: :environment do
+      unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false
+        puts 'Unexpected value for Account#hide_collections? for user @admin'
+        exit(1)
+      end
+
+      unless Account.find_by(username: 'user', domain: nil)&.hide_collections? == true
+        puts 'Unexpected value for Account#hide_collections? for user @user'
+        exit(1)
+      end
+
+      unless Account.find_by(username: 'evil', domain: 'activitypub.com')&.suspended?
+        puts 'Unexpected value for Account#suspended? for user @evil@activitypub.com'
+        exit(1)
+      end
+
+      unless Status.find(6).account_id == Status.find(7).account_id
+        puts 'Users @remote@remote.com and @Remote@remote.com not properly merged'
+        exit(1)
+      end
+
+      if Account.where(domain: Rails.configuration.x.local_domain).exists?
+        puts 'Faux remote accounts not properly claned up'
+        exit(1)
+      end
+
+      unless AccountConversation.first&.last_status_id == 11
+        puts 'AccountConversation records not created as expected'
+        exit(1)
+      end
+    end
+
+    desc 'Populate the database with test data for 2.4.0'
+    task populate_v2_4: :environment do
+      ActiveRecord::Base.connection.execute(<<~SQL)
+        INSERT INTO "settings"
+          (id, thing_type, thing_id, var, value, created_at, updated_at)
+        VALUES
+          (1, 'User', 1, 'hide_network', E'--- false\n', now(), now()),
+          (2, 'User', 2, 'hide_network', E'--- true\n', now(), now());
+      SQL
+    end
+
     desc 'Populate the database with test data for 2.0.0'
     task populate_v2: :environment do
       admin_key   = OpenSSL::PKey::RSA.new(2048)
@@ -34,7 +78,7 @@ namespace :tests do
            'https://remote.com/@remote', 'https://remote.com/salmon/1'),
           (4, 'Remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
            'https://remote.com/@Remote', 'https://remote.com/salmon/1'),
-          (5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now(), now(),
+          (5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now() - interval '1 year', now() - interval '1 year',
            'https://remote.com/stale/@REMOTE', 'https://remote.com/stale/salmon/1');
 
         INSERT INTO "accounts"
@@ -49,6 +93,13 @@ namespace :tests do
           (7, 'user', #{local_domain}, #{user_private_key}, #{user_public_key}, now(), now()),
           (8, 'pt_user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
 
+        INSERT INTO "accounts"
+          (id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url, suspended)
+        VALUES
+          (9, 'evil', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(),
+           1, 'https://activitypub.com/users/evil/inbox', 'https://activitypub.com/users/evil/outbox',
+           'https://activitypub.com/users/evil/followers', true);
+
         -- users
 
         INSERT INTO "users"
@@ -62,6 +113,9 @@ namespace :tests do
         VALUES
           (3, 7, 'ptuser@localhost', now(), now(), false, 'pt');
 
+        -- conversations
+        INSERT INTO "conversations" (id, created_at, updated_at) VALUES (1, now(), now());
+
         -- statuses
 
         INSERT INTO "statuses"
@@ -97,14 +151,22 @@ namespace :tests do
         VALUES
           (9, 1, 2, now(), now());
 
+        INSERT INTO "statuses"
+          (id, account_id, text, in_reply_to_id, conversation_id, visibility, created_at, updated_at)
+        VALUES
+          (10, 2, '@admin hey!', NULL, 1, 3, now(), now()),
+          (11, 1, '@user hey!', 10, 1, 3, now(), now());
+
         -- mentions (from previous statuses)
 
         INSERT INTO "mentions"
-          (status_id, account_id, created_at, updated_at)
+          (id, status_id, account_id, created_at, updated_at)
         VALUES
-          (2, 3, now(), now()),
-          (3, 4, now(), now()),
-          (4, 5, now(), now());
+          (1, 2, 3, now(), now()),
+          (2, 3, 4, now(), now()),
+          (3, 4, 5, now(), now()),
+          (4, 10, 1, now(), now()),
+          (5, 11, 2, now(), now());
 
         -- stream entries
 
@@ -121,7 +183,6 @@ namespace :tests do
           (8, 5, 'status', now(), now()),
           (9, 1, 'status', now(), now());
 
-
         -- custom emoji
 
         INSERT INTO "custom_emojis"
@@ -161,12 +222,12 @@ namespace :tests do
         -- follows
 
         INSERT INTO "follows"
-          (account_id, target_account_id, created_at, updated_at)
+          (id, account_id, target_account_id, created_at, updated_at)
         VALUES
-          (1, 5, now(), now()),
-          (6, 2, now(), now()),
-          (5, 2, now(), now()),
-          (6, 1, now(), now());
+          (1, 1, 5, now(), now()),
+          (2, 6, 2, now(), now()),
+          (3, 5, 2, now(), now()),
+          (4, 6, 1, now(), now());
 
         -- follow requests
 
@@ -175,6 +236,15 @@ namespace :tests do
         VALUES
           (2, 5, now(), now()),
           (5, 1, now(), now());
+
+        -- notifications
+
+        INSERT INTO "notifications"
+          (id, from_account_id, account_id, activity_type, activity_id, created_at, updated_at)
+        VALUES
+          (1, 6, 2, 'Follow', 2, now(), now()),
+          (2, 2, 1, 'Mention', 4, now(), now()),
+          (3, 1, 2, 'Mention', 5, now(), now());
       SQL
     end
   end

From 8f6c67bfdeddd1c2c1085067e3dc549fb53f6ff4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 8 Mar 2022 09:14:39 +0100
Subject: [PATCH 8/8] Fix performance of account timelines (#17709)

* Fix performance of account timelines

* Various fixes and improvements

* Fix duplicate results being returned

Co-authored-by: Claire <claire.github-309c@sitedethib.com>

* Fix grouping for pinned statuses scope

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 .../activitypub/outboxes_controller.rb        |   2 +-
 .../api/v1/accounts/statuses_controller.rb    |  41 +---
 app/models/account_statuses_filter.rb         | 134 ++++++++++
 app/models/status.rb                          |  22 --
 spec/models/account_statuses_filter_spec.rb   | 229 ++++++++++++++++++
 spec/models/status_spec.rb                    |  53 ----
 6 files changed, 366 insertions(+), 115 deletions(-)
 create mode 100644 app/models/account_statuses_filter.rb
 create mode 100644 spec/models/account_statuses_filter_spec.rb

diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index b2aab56a56..cd3992502d 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -62,7 +62,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
     return unless page_requested?
 
     @statuses = cache_collection_paginated_by_id(
-      @account.statuses.permitted_for(@account, signed_request_account),
+      AccountStatusesFilter.new(@account, signed_request_account).results,
       Status,
       LIMIT,
       params_slice(:max_id, :min_id, :since_id)
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 2c027ea769..38c9f5a20d 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -22,53 +22,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   end
 
   def cached_account_statuses
-    statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
-
-    statuses.merge!(only_media_scope) if truthy_param?(:only_media)
-    statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
-    statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
-    statuses.merge!(hashtag_scope)    if params[:tagged].present?
-
     cache_collection_paginated_by_id(
-      statuses,
+      AccountStatusesFilter.new(@account, current_account, params).results,
       Status,
       limit_param(DEFAULT_STATUSES_LIMIT),
       params_slice(:max_id, :since_id, :min_id)
     )
   end
 
-  def permitted_account_statuses
-    @account.statuses.permitted_for(@account, current_account)
-  end
-
-  def only_media_scope
-    Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
-  end
-
-  def pinned_scope
-    @account.pinned_statuses.permitted_for(@account, current_account)
-  end
-
-  def no_replies_scope
-    Status.without_replies
-  end
-
-  def no_reblogs_scope
-    Status.without_reblogs
-  end
-
-  def hashtag_scope
-    tag = Tag.find_normalized(params[:tagged])
-
-    if tag
-      Status.tagged_with(tag.id)
-    else
-      Status.none
-    end
-  end
-
   def pagination_params(core_params)
-    params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
+    params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
   end
 
   def insert_pagination_headers
diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb
new file mode 100644
index 0000000000..211f414787
--- /dev/null
+++ b/app/models/account_statuses_filter.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+class AccountStatusesFilter
+  KEYS = %i(
+    pinned
+    tagged
+    only_media
+    exclude_replies
+    exclude_reblogs
+  ).freeze
+
+  attr_reader :params, :account, :current_account
+
+  def initialize(account, current_account, params = {})
+    @account         = account
+    @current_account = current_account
+    @params          = params
+  end
+
+  def results
+    scope = initial_scope
+
+    scope.merge!(pinned_scope)     if pinned?
+    scope.merge!(only_media_scope) if only_media?
+    scope.merge!(no_replies_scope) if exclude_replies?
+    scope.merge!(no_reblogs_scope) if exclude_reblogs?
+    scope.merge!(hashtag_scope)    if tagged?
+
+    scope
+  end
+
+  private
+
+  def initial_scope
+    if suspended?
+      Status.none
+    elsif anonymous?
+      account.statuses.where(visibility: %i(public unlisted))
+    elsif author?
+      account.statuses.all # NOTE: #merge! does not work without the #all
+    elsif blocked?
+      Status.none
+    else
+      filtered_scope
+    end
+  end
+
+  def filtered_scope
+    scope = account.statuses.left_outer_joins(:mentions)
+
+    scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id]))
+    scope.merge!(filtered_reblogs_scope) if reblogs_may_occur?
+
+    scope
+  end
+
+  def filtered_reblogs_scope
+    Status.left_outer_joins(:reblog).where(reblog_of_id: nil).or(Status.where.not(reblogs_statuses: { account_id: current_account.excluded_from_timeline_account_ids }))
+  end
+
+  def only_media_scope
+    Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
+  end
+
+  def no_replies_scope
+    Status.without_replies
+  end
+
+  def no_reblogs_scope
+    Status.without_reblogs
+  end
+
+  def pinned_scope
+    account.pinned_statuses.group(Status.arel_table[:id], StatusPin.arel_table[:created_at])
+  end
+
+  def hashtag_scope
+    tag = Tag.find_normalized(params[:tagged])
+
+    if tag
+      Status.tagged_with(tag.id)
+    else
+      Status.none
+    end
+  end
+
+  def suspended?
+    account.suspended?
+  end
+
+  def anonymous?
+    current_account.nil?
+  end
+
+  def author?
+    current_account.id == account.id
+  end
+
+  def blocked?
+    account.blocking?(current_account) || (current_account.domain.present? && account.domain_blocking?(current_account.domain))
+  end
+
+  def follower?
+    current_account.following?(account)
+  end
+
+  def reblogs_may_occur?
+    !exclude_reblogs? && !only_media? && !tagged?
+  end
+
+  def pinned?
+    truthy_param?(:pinned)
+  end
+
+  def only_media?
+    truthy_param?(:only_media)
+  end
+
+  def exclude_replies?
+    truthy_param?(:exclude_replies)
+  end
+
+  def exclude_reblogs?
+    truthy_param?(:exclude_reblogs)
+  end
+
+  def tagged?
+    params[:tagged].present?
+  end
+
+  def truthy_param?(key)
+    ActiveModel::Type::Boolean.new.cast(params[key])
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 60dde50459..af3e645dc3 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -345,28 +345,6 @@ class Status < ApplicationRecord
       end
     end
 
-    def permitted_for(target_account, account)
-      visibility = [:public, :unlisted]
-
-      if account.nil?
-        where(visibility: visibility)
-      elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
-        none
-      elsif account.id == target_account.id # author can see own stuff
-        all
-      else
-        # followers can see followers-only stuff, but also things they are mentioned in.
-        # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
-        visibility.push(:private) if account.following?(target_account)
-
-        scope = left_outer_joins(:reblog)
-
-        scope.where(visibility: visibility)
-             .or(scope.where(id: account.mentions.select(:status_id)))
-             .merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids })))
-      end
-    end
-
     def from_text(text)
       return [] if text.blank?
 
diff --git a/spec/models/account_statuses_filter_spec.rb b/spec/models/account_statuses_filter_spec.rb
new file mode 100644
index 0000000000..03f0ffeb06
--- /dev/null
+++ b/spec/models/account_statuses_filter_spec.rb
@@ -0,0 +1,229 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AccountStatusesFilter do
+  let(:account) { Fabricate(:account) }
+  let(:current_account) { nil }
+  let(:params) { {} }
+
+  subject { described_class.new(account, current_account, params) }
+
+  def status!(visibility)
+    Fabricate(:status, account: account, visibility: visibility)
+  end
+
+  def status_with_tag!(visibility, tag)
+    Fabricate(:status, account: account, visibility: visibility, tags: [tag])
+  end
+
+  def status_with_parent!(visibility)
+    Fabricate(:status, account: account, visibility: visibility, thread: Fabricate(:status))
+  end
+
+  def status_with_reblog!(visibility)
+    Fabricate(:status, account: account, visibility: visibility, reblog: Fabricate(:status))
+  end
+
+  def status_with_mention!(visibility, mentioned_account = nil)
+    Fabricate(:status, account: account, visibility: visibility).tap do |status|
+      Fabricate(:mention, status: status, account: mentioned_account || Fabricate(:account))
+    end
+  end
+
+  def status_with_media_attachment!(visibility)
+    Fabricate(:status, account: account, visibility: visibility).tap do |status|
+      Fabricate(:media_attachment, account: account, status: status)
+    end
+  end
+
+  describe '#results' do
+    let(:tag) { Fabricate(:tag) }
+
+    before do
+      status!(:public)
+      status!(:unlisted)
+      status!(:private)
+      status_with_parent!(:public)
+      status_with_reblog!(:public)
+      status_with_tag!(:public, tag)
+      status_with_mention!(:direct)
+      status_with_media_attachment!(:public)
+    end
+
+    shared_examples 'filter params' do
+      context 'with only_media param' do
+        let(:params) { { only_media: true } }
+
+        it 'returns only statuses with media' do
+          expect(subject.results.all?(&:with_media?)).to be true
+        end
+      end
+
+      context 'with tagged param' do
+        let(:params) { { tagged: tag.name } }
+
+        it 'returns only statuses with tag' do
+          expect(subject.results.all? { |s| s.tags.include?(tag) }).to be true
+        end
+      end
+
+      context 'with exclude_replies param' do
+        let(:params) { { exclude_replies: true } }
+
+        it 'returns only statuses that are not replies' do
+          expect(subject.results.none?(&:reply?)).to be true
+        end
+      end
+
+      context 'with exclude_reblogs param' do
+        let(:params) { { exclude_reblogs: true } }
+
+        it 'returns only statuses that are not reblogs' do
+          expect(subject.results.none?(&:reblog?)).to be true
+        end
+      end
+    end
+
+    context 'when accessed anonymously' do
+      let(:current_account) { nil }
+      let(:direct_status) { nil }
+
+      it 'returns only public statuses' do
+        expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
+      end
+
+      it 'returns public replies' do
+        expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
+      end
+
+      it 'returns public reblogs' do
+        expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
+      end
+
+      it_behaves_like 'filter params'
+    end
+
+    context 'when accessed with a blocked account' do
+      let(:current_account) { Fabricate(:account) }
+
+      before do
+        account.block!(current_account)
+      end
+
+      it 'returns nothing' do
+        expect(subject.results.to_a).to be_empty
+      end
+    end
+
+    context 'when accessed by self' do
+      let(:current_account) { account }
+
+      it 'returns everything' do
+        expect(subject.results.pluck(:visibility).uniq).to match_array %w(direct private unlisted public)
+      end
+
+      it 'returns replies' do
+        expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
+      end
+
+      it 'returns reblogs' do
+        expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
+      end
+
+      it_behaves_like 'filter params'
+    end
+
+    context 'when accessed by a follower' do
+      let(:current_account) { Fabricate(:account) }
+
+      before do
+        current_account.follow!(account)
+      end
+
+      it 'returns private statuses' do
+        expect(subject.results.pluck(:visibility).uniq).to match_array %w(private unlisted public)
+      end
+
+      it 'returns replies' do
+        expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
+      end
+
+      it 'returns reblogs' do
+        expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
+      end
+
+      context 'when there is a direct status mentioning the non-follower' do
+        let!(:direct_status) { status_with_mention!(:direct, current_account) }
+
+        it 'returns the direct status' do
+          expect(subject.results.pluck(:id)).to include(direct_status.id)
+        end
+      end
+
+      it_behaves_like 'filter params'
+    end
+
+    context 'when accessed by a non-follower' do
+      let(:current_account) { Fabricate(:account) }
+
+      it 'returns only public statuses' do
+        expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
+      end
+
+      it 'returns public replies' do
+        expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
+      end
+
+      it 'returns public reblogs' do
+        expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
+      end
+
+      context 'when there is a private status mentioning the non-follower' do
+        let!(:private_status) { status_with_mention!(:private, current_account) }
+
+        it 'returns the private status' do
+          expect(subject.results.pluck(:id)).to include(private_status.id)
+        end
+      end
+
+      context 'when blocking a reblogged account' do
+        let(:reblog) { status_with_reblog!('public') }
+
+        before do
+          current_account.block!(reblog.reblog.account)
+        end
+
+        it 'does not return reblog of blocked account' do
+          expect(subject.results.pluck(:id)).to_not include(reblog.id)
+        end
+      end
+
+      context 'when muting a reblogged account' do
+        let(:reblog) { status_with_reblog!('public') }
+
+        before do
+          current_account.mute!(reblog.reblog.account)
+        end
+
+        it 'does not return reblog of muted account' do
+          expect(subject.results.pluck(:id)).to_not include(reblog.id)
+        end
+      end
+
+      context 'when blocked by a reblogged account' do
+        let(:reblog) { status_with_reblog!('public') }
+
+        before do
+          reblog.reblog.account.block!(current_account)
+        end
+
+        it 'does not return reblog of blocked-by account' do
+          expect(subject.results.pluck(:id)).to_not include(reblog.id)
+        end
+      end
+
+      it_behaves_like 'filter params'
+    end
+  end
+end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 67af6d06d1..130f4d03fa 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -348,59 +348,6 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe '.permitted_for' do
-    subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
-
-    let(:target_account) { alice }
-    let(:account) { bob }
-    let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
-    let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
-    let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
-
-    let!(:direct_status) do
-      Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
-        Fabricate(:mention, status: status, account: account)
-      end
-    end
-
-    let!(:other_direct_status) do
-      Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
-        Fabricate(:mention, status: status)
-      end
-    end
-
-    context 'given nil' do
-      let(:account) { nil }
-      let(:direct_status) { nil }
-      it { is_expected.to eq(%w(unlisted public)) }
-    end
-
-    context 'given blocked account' do
-      before do
-        target_account.block!(account)
-      end
-
-      it { is_expected.to be_empty }
-    end
-
-    context 'given same account' do
-      let(:account) { target_account }
-      it { is_expected.to eq(%w(direct direct private unlisted public)) }
-    end
-
-    context 'given followed account' do
-      before do
-        account.follow!(target_account)
-      end
-
-      it { is_expected.to eq(%w(direct private unlisted public)) }
-    end
-
-    context 'given unfollowed account' do
-      it { is_expected.to eq(%w(direct unlisted public)) }
-    end
-  end
-
   describe 'before_validation' do
     it 'sets account being replied to correctly over intermediary nodes' do
       first_status = Fabricate(:status, account: bob)