From 14530a232383282e9d45e4832a6935592d541620 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:32:42 +0900
Subject: [PATCH 01/24] Bump bullet from 6.1.2 to 6.1.3 (#15629)

Bumps [bullet](https://github.com/flyerhzm/bullet) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/flyerhzm/bullet/releases)
- [Changelog](https://github.com/flyerhzm/bullet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/flyerhzm/bullet/compare/6.1.2...6.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 516db5a321..b400c4c80c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -109,7 +109,7 @@ GEM
     brakeman (4.10.1)
     browser (4.2.0)
     builder (3.2.4)
-    bullet (6.1.2)
+    bullet (6.1.3)
       activesupport (>= 3.0.0)
       uniform_notifier (~> 1.11)
     bundler-audit (0.7.0.1)
@@ -647,7 +647,7 @@ GEM
       unf_ext
     unf_ext (0.0.7.7)
     unicode-display_width (1.7.0)
-    uniform_notifier (1.13.0)
+    uniform_notifier (1.13.2)
     warden (1.2.9)
       rack (>= 2.0.9)
     webauthn (3.0.0.alpha1)

From 3a3990a81dc56e344cf81af14f0bb0ee1658ca58 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:33:06 +0900
Subject: [PATCH 02/24] Bump rack-attack from 6.3.1 to 6.4.0 (#15628)

Bumps [rack-attack](https://github.com/rack/rack-attack) from 6.3.1 to 6.4.0.
- [Release notes](https://github.com/rack/rack-attack/releases)
- [Changelog](https://github.com/rack/rack-attack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rack/rack-attack/compare/v6.3.1...v6.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index aeb6d73ee7..e1e3cc8f56 100644
--- a/Gemfile
+++ b/Gemfile
@@ -73,7 +73,7 @@ gem 'parallel', '~> 1.20'
 gem 'posix-spawn'
 gem 'pundit', '~> 2.1'
 gem 'premailer-rails'
-gem 'rack-attack', '~> 6.3'
+gem 'rack-attack', '~> 6.4'
 gem 'rack-cors', '~> 1.1', require: 'rack/cors'
 gem 'rails-i18n', '~> 5.1'
 gem 'rails-settings-cached', '~> 0.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index b400c4c80c..9c5706fa0e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -436,7 +436,7 @@ GEM
     raabro (1.3.3)
     racc (1.5.2)
     rack (2.2.3)
-    rack-attack (6.3.1)
+    rack-attack (6.4.0)
       rack (>= 1.0, < 3)
     rack-cors (1.1.1)
       rack (>= 2.0.0)
@@ -772,7 +772,7 @@ DEPENDENCIES
   puma (~> 5.1)
   pundit (~> 2.1)
   rack (~> 2.2.3)
-  rack-attack (~> 6.3)
+  rack-attack (~> 6.4)
   rack-cors (~> 1.1)
   rails (~> 5.2.4.4)
   rails-controller-testing (~> 1.0)

From 97e075f197e30b8e55fbe169c8073cef08d4daef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:33:46 +0900
Subject: [PATCH 03/24] Bump oj from 3.11.0 to 3.11.1 (#15626)

Bumps [oj](https://github.com/ohler55/oj) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/ohler55/oj/releases)
- [Changelog](https://github.com/ohler55/oj/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ohler55/oj/compare/v3.11.0...v3.11.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 9c5706fa0e..dbca7343ba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -369,7 +369,7 @@ GEM
       concurrent-ruby (~> 1.0, >= 1.0.2)
       sidekiq (>= 3.5)
       statsd-ruby (~> 1.4, >= 1.4.0)
-    oj (3.11.0)
+    oj (3.11.1)
     omniauth (1.9.1)
       hashie (>= 3.4.6)
       rack (>= 1.6.2, < 3)

From 4d16f25119e4934f396b858298b91f239f9aeb70 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:34:05 +0900
Subject: [PATCH 04/24] Bump webpack-bundle-analyzer from 4.3.0 to 4.4.0
 (#15625)

Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.3.0...v4.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index cc88ee2c61..755b52b378 100644
--- a/package.json
+++ b/package.json
@@ -165,7 +165,7 @@
     "uuid": "^8.3.1",
     "webpack": "^4.46.0",
     "webpack-assets-manifest": "^4.0.1",
-    "webpack-bundle-analyzer": "^4.3.0",
+    "webpack-bundle-analyzer": "^4.4.0",
     "webpack-cli": "^3.3.12",
     "webpack-merge": "^5.7.3",
     "wicg-inert": "^3.1.0"
diff --git a/yarn.lock b/yarn.lock
index 1110d9dfc7..75dc36d381 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11069,10 +11069,10 @@ webpack-assets-manifest@^4.0.1:
     tapable "^1.0"
     webpack-sources "^1.0"
 
-webpack-bundle-analyzer@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz#2f3c0ca9041d5ee47fa418693cf56b4a518b578b"
-  integrity sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA==
+webpack-bundle-analyzer@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7"
+  integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==
   dependencies:
     acorn "^8.0.4"
     acorn-walk "^8.0.0"

From 70128820dc06501cb791a88559fb4b82244a08c9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:34:48 +0900
Subject: [PATCH 05/24] Bump tzinfo-data from 1.2020.6 to 1.2021.1 (#15623)

Bumps [tzinfo-data](https://github.com/tzinfo/tzinfo-data) from 1.2020.6 to 1.2021.1.
- [Release notes](https://github.com/tzinfo/tzinfo-data/releases)
- [Commits](https://github.com/tzinfo/tzinfo-data/compare/v1.2020.6...v1.2021.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index e1e3cc8f56..bbad0a8416 100644
--- a/Gemfile
+++ b/Gemfile
@@ -94,7 +94,7 @@ gem 'stoplight', '~> 2.2.1'
 gem 'strong_migrations', '~> 0.7'
 gem 'tty-prompt', '~> 0.23', require: false
 gem 'twitter-text', '~> 1.14'
-gem 'tzinfo-data', '~> 1.2020'
+gem 'tzinfo-data', '~> 1.2021'
 gem 'webpacker', '~> 5.2'
 gem 'webpush'
 gem 'webauthn', '~> 3.0.0.alpha1'
diff --git a/Gemfile.lock b/Gemfile.lock
index dbca7343ba..50ca2feb1c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -641,7 +641,7 @@ GEM
       unf (~> 0.1.0)
     tzinfo (1.2.9)
       thread_safe (~> 0.1)
-    tzinfo-data (1.2020.6)
+    tzinfo-data (1.2021.1)
       tzinfo (>= 1.0.0)
     unf (0.1.4)
       unf_ext
@@ -807,7 +807,7 @@ DEPENDENCIES
   thor (~> 1.0)
   tty-prompt (~> 0.23)
   twitter-text (~> 1.14)
-  tzinfo-data (~> 1.2020)
+  tzinfo-data (~> 1.2021)
   webauthn (~> 3.0.0.alpha1)
   webmock (~> 3.11)
   webpacker (~> 5.2)

From e9ac2632299e9df7ee120d5936648ed96b9fd4a5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Jan 2021 23:35:03 +0900
Subject: [PATCH 06/24] Bump thor from 1.0.1 to 1.1.0 (#15622)

Bumps [thor](https://github.com/erikhuda/thor) from 1.0.1 to 1.1.0.
- [Release notes](https://github.com/erikhuda/thor/releases)
- [Changelog](https://github.com/erikhuda/thor/blob/master/CHANGELOG.md)
- [Commits](https://github.com/erikhuda/thor/compare/v1.0.1...v1.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index bbad0a8416..971c3967f7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,7 +8,7 @@ gem 'pkg-config', '~> 1.4'
 gem 'puma', '~> 5.1'
 gem 'rails', '~> 5.2.4.4'
 gem 'sprockets', '~> 3.7.2'
-gem 'thor', '~> 1.0'
+gem 'thor', '~> 1.1'
 gem 'rack', '~> 2.2.3'
 
 gem 'hamlit-rails', '~> 0.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 50ca2feb1c..003f426095 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -619,7 +619,7 @@ GEM
       unicode-display_width (~> 1.1, >= 1.1.1)
     terrapin (0.6.0)
       climate_control (>= 0.0.3, < 1.0)
-    thor (1.0.1)
+    thor (1.1.0)
     thread_safe (0.3.6)
     thwait (0.2.0)
       e2mmap
@@ -804,7 +804,7 @@ DEPENDENCIES
   stoplight (~> 2.2.1)
   streamio-ffmpeg (~> 3.0)
   strong_migrations (~> 0.7)
-  thor (~> 1.0)
+  thor (~> 1.1)
   tty-prompt (~> 0.23)
   twitter-text (~> 1.14)
   tzinfo-data (~> 1.2021)

From 38f56315883f8bb45a1fbcff371016246cd1b3f4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 27 Jan 2021 08:58:29 +0900
Subject: [PATCH 07/24] Bump sidekiq from 6.1.2 to 6.1.3 (#15621)

Bumps [sidekiq](https://github.com/mperham/sidekiq) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/mperham/sidekiq/releases)
- [Changelog](https://github.com/mperham/sidekiq/blob/master/Changes.md)
- [Commits](https://github.com/mperham/sidekiq/compare/v6.1.2...v6.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 003f426095..113d5638d4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -569,7 +569,7 @@ GEM
       railties (>= 4.0.0)
     securecompare (1.0.0)
     semantic_range (2.3.0)
-    sidekiq (6.1.2)
+    sidekiq (6.1.3)
       connection_pool (>= 2.2.2)
       rack (~> 2.0)
       redis (>= 4.2.0)

From 2319e85a8a681b135b80e12a2f32a5cdbcec1b6a Mon Sep 17 00:00:00 2001
From: abcang <abcang1015@gmail.com>
Date: Fri, 29 Jan 2021 02:24:22 +0900
Subject: [PATCH 08/24] Fix react/jsx-no-duplicate-props (#15636)

---
 app/javascript/mastodon/features/account/components/header.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 4647b98b21..2087fc3b80 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -330,7 +330,7 @@ class Header extends ImmutablePureComponent {
                     <dl key={i}>
                       <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
 
-                      <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')} className='translate'>
+                      <dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
                         {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
                       </dd>
                     </dl>

From b4281f5a51c8c54891b7a74e9573faec6871a460 Mon Sep 17 00:00:00 2001
From: Daniel Jakots <danieljakots@users.noreply.github.com>
Date: Thu, 28 Jan 2021 16:53:56 -0500
Subject: [PATCH 09/24] Update to Node.js-12.20.1 (#15558)

This is a security release. You can read the announce at
https://nodejs.org/en/blog/vulnerability/january-2021-security-releases/
---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 136b2a35ae..84e6435f3e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ FROM ubuntu:20.04 as build-dep
 SHELL ["/usr/bin/bash", "-c"]
 
 # Install Node v12 (LTS)
-ENV NODE_VER="12.20.0"
+ENV NODE_VER="12.20.1"
 RUN ARCH= && \
     dpkgArch="$(dpkg --print-architecture)" && \
   case "${dpkgArch##*-}" in \

From 13d5b8157904b2eb6b7f43e3fb834fca2f38f0dd Mon Sep 17 00:00:00 2001
From: ThibG <thib@sitedethib.com>
Date: Fri, 29 Jan 2021 18:38:56 +0100
Subject: [PATCH 10/24] =?UTF-8?q?Fix=20=E2=80=9Ctootctl=20accounts=20unfol?=
 =?UTF-8?q?low=E2=80=9D=20(#15639)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fixes #15635

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 lib/mastodon/accounts_cli.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index 653bfca301..74162256ff 100644
--- a/lib/mastodon/accounts_cli.rb
+++ b/lib/mastodon/accounts_cli.rb
@@ -402,7 +402,7 @@ module Mastodon
         exit(1)
       end
 
-      parallelize_with_progress(target_account.followers.local) do |account|
+      processed, = parallelize_with_progress(target_account.followers.local) do |account|
         UnfollowService.new.call(account, target_account)
       end
 

From eb0f9e315939a66ebad0b174beeb1edde7ba7e8d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Feb 2021 03:48:29 +0900
Subject: [PATCH 11/24] Bump rubocop from 1.7.0 to 1.8.1 (#15573)

* Bump rubocop from 1.7.0 to 1.8.1

Bumps [rubocop](https://github.com/rubocop-hq/rubocop) from 1.7.0 to 1.8.1.
- [Release notes](https://github.com/rubocop-hq/rubocop/releases)
- [Changelog](https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop-hq/rubocop/compare/v1.7.0...v1.8.1)

Signed-off-by: dependabot[bot] <support@github.com>

* Fix .codeclimate.yml

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>
---
 .codeclimate.yml |  2 +-
 Gemfile          |  2 +-
 Gemfile.lock     | 10 +++++-----
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/.codeclimate.yml b/.codeclimate.yml
index 2d1de877ce..62dc2334ca 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -30,7 +30,7 @@ plugins:
     channel: eslint-7
   rubocop:
     enabled: true
-    channel: rubocop-1-70
+    channel: rubocop-1-8-1
   sass-lint:
     enabled: true
 exclude_patterns:
diff --git a/Gemfile b/Gemfile
index 971c3967f7..8ac92cdf95 100644
--- a/Gemfile
+++ b/Gemfile
@@ -138,7 +138,7 @@ group :development do
   gem 'letter_opener', '~> 1.7'
   gem 'letter_opener_web', '~> 1.4'
   gem 'memory_profiler'
-  gem 'rubocop', '~> 1.7', require: false
+  gem 'rubocop', '~> 1.8', require: false
   gem 'rubocop-rails', '~> 2.9', require: false
   gem 'brakeman', '~> 4.10', require: false
   gem 'bundler-audit', '~> 0.7', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 113d5638d4..ba6fd64ac7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -538,16 +538,16 @@ GEM
     rspec-support (3.10.1)
     rspec_junit_formatter (0.4.1)
       rspec-core (>= 2, < 4, != 2.12.0)
-    rubocop (1.7.0)
+    rubocop (1.8.1)
       parallel (~> 1.10)
-      parser (>= 2.7.1.5)
+      parser (>= 3.0.0.0)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 1.8, < 3.0)
       rexml
       rubocop-ast (>= 1.2.0, < 2.0)
       ruby-progressbar (~> 1.7)
-      unicode-display_width (>= 1.4.0, < 2.0)
-    rubocop-ast (1.3.0)
+      unicode-display_width (>= 1.4.0, < 3.0)
+    rubocop-ast (1.4.0)
       parser (>= 2.7.1.5)
     rubocop-rails (2.9.1)
       activesupport (>= 4.2.0)
@@ -786,7 +786,7 @@ DEPENDENCIES
   rspec-rails (~> 4.0)
   rspec-sidekiq (~> 3.1)
   rspec_junit_formatter (~> 0.4)
-  rubocop (~> 1.7)
+  rubocop (~> 1.8)
   rubocop-rails (~> 2.9)
   ruby-progressbar (~> 1.11)
   sanitize (~> 5.2)

From c8c764dd8b168ec876918d16b3f25280893d20dc Mon Sep 17 00:00:00 2001
From: abcang <abcang1015@gmail.com>
Date: Mon, 1 Feb 2021 05:24:17 +0900
Subject: [PATCH 12/24] Fix N+1 query when rendering with StatusSerializer
 (#15641)

---
 app/models/status.rb | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/models/status.rb b/app/models/status.rb
index fcc7a026d5..74e81f6127 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -114,7 +114,7 @@ class Status < ApplicationRecord
                    :tags,
                    :preview_cards,
                    :preloadable_poll,
-                   account: :account_stat,
+                   account: [:account_stat, :user],
                    active_mentions: { account: :account_stat },
                    reblog: [
                      :application,
@@ -124,7 +124,7 @@ class Status < ApplicationRecord
                      :conversation,
                      :status_stat,
                      :preloadable_poll,
-                     account: :account_stat,
+                     account: [:account_stat, :user],
                      active_mentions: { account: :account_stat },
                    ],
                    thread: { account: :account_stat }
@@ -301,7 +301,7 @@ class Status < ApplicationRecord
 
       return if account_ids.empty?
 
-      accounts = Account.where(id: account_ids).includes(:account_stat).index_by(&:id)
+      accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
 
       cached_items.each do |item|
         item.account = accounts[item.account_id]

From 7ab53f221a3d66a0bbc94b36d847b7bcf62fbd16 Mon Sep 17 00:00:00 2001
From: abcang <abcang1015@gmail.com>
Date: Mon, 1 Feb 2021 05:24:57 +0900
Subject: [PATCH 13/24] Improved performance of notification preloading
 (#15640)

* Improved performance of notification preloading

* Remove Cacheable from Notification

* Fix test
---
 .../api/v1/notifications_controller.rb        |   7 +-
 app/models/notification.rb                    |  56 ++++++---
 .../application_controller_spec.rb            |   4 -
 spec/models/notification_spec.rb              | 119 ++++++++++++++----
 4 files changed, 136 insertions(+), 50 deletions(-)

diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index 522c35ba54..f9d7808398 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -31,12 +31,13 @@ class Api::V1::NotificationsController < Api::BaseController
   private
 
   def load_notifications
-    cache_collection_paginated_by_id(
-      browserable_account_notifications,
-      Notification,
+    notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id(
       limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
       params_slice(:max_id, :since_id, :min_id)
     )
+    Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
+      cache_collection(target_statuses, Status)
+    end
   end
 
   def browserable_account_notifications
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 5e9ea62a0d..98a6a618f5 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -17,7 +17,6 @@ class Notification < ApplicationRecord
   self.inheritance_column = nil
 
   include Paginable
-  include Cacheable
 
   LEGACY_TYPE_CLASS_MAP = {
     'Mention'       => :mention,
@@ -38,7 +37,13 @@ class Notification < ApplicationRecord
     poll
   ).freeze
 
-  STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
+  TARGET_STATUS_INCLUDES_BY_TYPE = {
+    status: :status,
+    reblog: [status: :reblog],
+    mention: [mention: :status],
+    favourite: [favourite: :status],
+    poll: [poll: :status],
+  }.freeze
 
   belongs_to :account, optional: true
   belongs_to :from_account, class_name: 'Account', optional: true
@@ -65,8 +70,6 @@ class Notification < ApplicationRecord
     end
   }
 
-  cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
-
   def type
     @type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
   end
@@ -87,21 +90,40 @@ class Notification < ApplicationRecord
   end
 
   class << self
-    def cache_ids
-      select(:id, :updated_at, :activity_type, :activity_id)
-    end
+    def preload_cache_collection_target_statuses(notifications, &_block)
+      notifications.group_by(&:type).each do |type, grouped_notifications|
+        associations = TARGET_STATUS_INCLUDES_BY_TYPE[type]
+        next unless associations
 
-    def reload_stale_associations!(cached_items)
-      account_ids = (cached_items.map(&:from_account_id) + cached_items.filter_map { |item| item.target_status&.account_id }).uniq
-
-      return if account_ids.empty?
-
-      accounts = Account.where(id: account_ids).includes(:account_stat).index_by(&:id)
-
-      cached_items.each do |item|
-        item.from_account = accounts[item.from_account_id]
-        item.target_status.account = accounts[item.target_status.account_id] if item.target_status
+        # Instead of using the usual `includes`, manually preload each type.
+        # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more.
+        ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations)
       end
+
+      unique_target_statuses = notifications.map(&:target_status).compact.uniq
+      # Call cache_collection in block
+      cached_statuses_by_id = yield(unique_target_statuses).index_by(&:id)
+
+      notifications.each do |notification|
+        next if notification.target_status.nil?
+
+        cached_status = cached_statuses_by_id[notification.target_status.id]
+
+        case notification.type
+        when :status
+          notification.status = cached_status
+        when :reblog
+          notification.status.reblog = cached_status
+        when :favourite
+          notification.favourite.status = cached_status
+        when :mention
+          notification.mention.status = cached_status
+        when :poll
+          notification.poll.status = cached_status
+        end
+      end
+
+      notifications
     end
   end
 
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 63ae27a924..e73a08a0e9 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -349,10 +349,6 @@ describe ApplicationController, type: :controller do
       expect(C.new.cache_collection(raw, Object)).to eq raw
     end
 
-    context 'Notification' do
-      include_examples 'cacheable', :notification, Notification
-    end
-
     context 'Status' do
       include_examples 'cacheable', :status, Status
     end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
index 0a045904bb..1e9e45d8d6 100644
--- a/spec/models/notification_spec.rb
+++ b/spec/models/notification_spec.rb
@@ -56,47 +56,114 @@ RSpec.describe Notification, type: :model do
     end
   end
 
-  describe '.reload_stale_associations!' do
-    context 'account_ids are empty' do
-      let(:cached_items) { [] }
-
-      subject { described_class.reload_stale_associations!(cached_items) }
-
-      it 'returns nil' do
-        is_expected.to be nil
+  describe '.preload_cache_collection_target_statuses' do
+    subject do
+      described_class.preload_cache_collection_target_statuses(notifications) do |target_statuses|
+        # preload account for testing instead of using cache_collection
+        Status.preload(:account).where(id: target_statuses.map(&:id))
       end
     end
 
-    context 'account_ids are present' do
+    context 'notifications are empty' do
+      let(:notifications) { [] }
+
+      it 'returns []' do
+        is_expected.to eq []
+      end
+    end
+
+    context 'notifications are present' do
       before do
-        allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1)
-        allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2)
-        allow(Account).to receive_message_chain(:where, :includes, :index_by).and_return(accounts_with_ids)
+        notifications.each(&:reload)
       end
 
-      let(:cached_items) do
+      let(:mention) { Fabricate(:mention) }
+      let(:status) { Fabricate(:status) }
+      let(:reblog) { Fabricate(:status, reblog: Fabricate(:status)) }
+      let(:follow) { Fabricate(:follow) }
+      let(:follow_request) { Fabricate(:follow_request) }
+      let(:favourite) { Fabricate(:favourite) }
+      let(:poll) { Fabricate(:poll) }
+
+      let(:notifications) do
         [
-          Fabricate(:notification, activity: Fabricate(:status)),
-          Fabricate(:notification, activity: Fabricate(:follow)),
+          Fabricate(:notification, type: :mention, activity: mention),
+          Fabricate(:notification, type: :status, activity: status),
+          Fabricate(:notification, type: :reblog, activity: reblog),
+          Fabricate(:notification, type: :follow, activity: follow),
+          Fabricate(:notification, type: :follow_request, activity: follow_request),
+          Fabricate(:notification, type: :favourite, activity: favourite),
+          Fabricate(:notification, type: :poll, activity: poll),
         ]
       end
 
-      let(:stale_account1) { cached_items[0].from_account }
-      let(:stale_account2) { cached_items[1].from_account }
+      it 'preloads target status' do
+        # mention
+        expect(subject[0].type).to eq :mention
+        expect(subject[0].association(:mention)).to be_loaded
+        expect(subject[0].mention.association(:status)).to be_loaded
 
-      let(:account1) { Fabricate(:account) }
-      let(:account2) { Fabricate(:account) }
+        # status
+        expect(subject[1].type).to eq :status
+        expect(subject[1].association(:status)).to be_loaded
 
-      let(:accounts_with_ids) { { account1.id => account1, account2.id => account2 } }
+        # reblog
+        expect(subject[2].type).to eq :reblog
+        expect(subject[2].association(:status)).to be_loaded
+        expect(subject[2].status.association(:reblog)).to be_loaded
 
-      it 'reloads associations' do
-        expect(cached_items[0].from_account).to be stale_account1
-        expect(cached_items[1].from_account).to be stale_account2
+        # follow: nothing
+        expect(subject[3].type).to eq :follow
+        expect(subject[3].target_status).to be_nil
 
-        described_class.reload_stale_associations!(cached_items)
+        # follow_request: nothing
+        expect(subject[4].type).to eq :follow_request
+        expect(subject[4].target_status).to be_nil
 
-        expect(cached_items[0].from_account).to be account1
-        expect(cached_items[1].from_account).to be account2
+        # favourite
+        expect(subject[5].type).to eq :favourite
+        expect(subject[5].association(:favourite)).to be_loaded
+        expect(subject[5].favourite.association(:status)).to be_loaded
+
+        # poll
+        expect(subject[6].type).to eq :poll
+        expect(subject[6].association(:poll)).to be_loaded
+        expect(subject[6].poll.association(:status)).to be_loaded
+      end
+
+      it 'replaces to cached status' do
+        # mention
+        expect(subject[0].type).to eq :mention
+        expect(subject[0].target_status.association(:account)).to be_loaded
+        expect(subject[0].target_status).to eq mention.status
+
+        # status
+        expect(subject[1].type).to eq :status
+        expect(subject[1].target_status.association(:account)).to be_loaded
+        expect(subject[1].target_status).to eq status
+
+        # reblog
+        expect(subject[2].type).to eq :reblog
+        expect(subject[2].target_status.association(:account)).to be_loaded
+        expect(subject[2].target_status).to eq reblog.reblog
+
+        # follow: nothing
+        expect(subject[3].type).to eq :follow
+        expect(subject[3].target_status).to be_nil
+
+        # follow_request: nothing
+        expect(subject[4].type).to eq :follow_request
+        expect(subject[4].target_status).to be_nil
+
+        # favourite
+        expect(subject[5].type).to eq :favourite
+        expect(subject[5].target_status.association(:account)).to be_loaded
+        expect(subject[5].target_status).to eq favourite.status
+
+        # poll
+        expect(subject[6].type).to eq :poll
+        expect(subject[6].target_status.association(:account)).to be_loaded
+        expect(subject[6].target_status).to eq poll.status
       end
     end
   end

From 3efa0c54b6656fba189baab0857e60c0bc4f3c7d Mon Sep 17 00:00:00 2001
From: ThibG <thib@sitedethib.com>
Date: Sun, 31 Jan 2021 21:25:31 +0100
Subject: [PATCH 14/24] Change custom emoji to be animated when hovering
 container (#15637)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 .../__snapshots__/display_name-test.js.snap   |  2 +
 .../mastodon/components/display_name.js       | 43 +++++----------
 .../mastodon/components/status_content.js     | 43 +++++++--------
 .../features/account/components/header.js     | 43 +++++----------
 .../components/conversation.js                | 43 +++++----------
 .../directory/components/account_card.js      | 47 ++++++----------
 .../components/announcements.js               | 53 +++++++++----------
 7 files changed, 103 insertions(+), 171 deletions(-)

diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
index 29fdc2412d..0f27473af4 100644
--- a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
@@ -3,6 +3,8 @@
 exports[`<DisplayName /> renders display name + account name 1`] = `
 <span
   className="display-name"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
 >
   <bdi>
     <strong
diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js
index 70ef82789e..7ccfbd0cd5 100644
--- a/app/javascript/mastodon/components/display_name.js
+++ b/app/javascript/mastodon/components/display_name.js
@@ -11,45 +11,30 @@ export default class DisplayName extends React.PureComponent {
     localDomain: PropTypes.string,
   };
 
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
-  setRef = (c) => {
-    this.node = c;
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render () {
@@ -81,7 +66,7 @@ export default class DisplayName extends React.PureComponent {
     }
 
     return (
-      <span className='display-name' ref={this.setRef}>
+      <span className='display-name' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         {displayName} {suffix}
       </span>
     );
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 190ced1a80..35bd505142 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -75,35 +75,38 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  _updateStatusEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
+      emoji.src = emoji.getAttribute('data-original');
+    }
+  }
 
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
     }
   }
 
   componentDidMount () {
     this._updateStatusLinks();
-    this._updateStatusEmojis();
   }
 
   componentDidUpdate () {
     this._updateStatusLinks();
-    this._updateStatusEmojis();
   }
 
   onMentionClick = (mention, e) => {
@@ -122,14 +125,6 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
   handleMouseDown = (e) => {
     this.startXY = [e.clientX, e.clientY];
   }
@@ -219,7 +214,7 @@ export default class StatusContent extends React.PureComponent {
       }
 
       return (
-        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
             <span dangerouslySetInnerHTML={spoilerContent} className='translate' />
             {' '}
@@ -237,7 +232,7 @@ export default class StatusContent extends React.PureComponent {
       );
     } else if (this.props.onClick) {
       const output = [
-        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
+        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
@@ -253,7 +248,7 @@ export default class StatusContent extends React.PureComponent {
       return output;
     } else {
       return (
-        <div className={classNames} ref={this.setRef} tabIndex='0'>
+        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 2087fc3b80..8e49486bda 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -96,45 +96,30 @@ class Header extends ImmutablePureComponent {
     return !location.pathname.match(/\/(followers|following)\/?$/);
   }
 
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
-  setRef = (c) => {
-    this.node = c;
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render () {
@@ -276,7 +261,7 @@ class Header extends ImmutablePureComponent {
     }
 
     return (
-      <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
+      <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         <div className='account__header__image'>
           <div className='account__header__info'>
             {!suspended && info}
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index 6ecc27facd..43e1d77b94 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -44,41 +44,30 @@ class Conversation extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
   };
 
-  _updateEmojis () {
-    const node = this.namesNode;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   handleClick = () => {
@@ -123,10 +112,6 @@ class Conversation extends ImmutablePureComponent {
     this.props.onToggleHidden(this.props.lastStatus);
   }
 
-  setNamesRef = (c) => {
-    this.namesNode = c;
-  }
-
   render () {
     const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
 
@@ -171,7 +156,7 @@ class Conversation extends ImmutablePureComponent {
                 {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
               </div>
 
-              <div className='conversation__content__names' ref={this.setNamesRef}>
+              <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
                 <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
               </div>
             </div>
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
index e377338280..8f0e8db4b4 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.js
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -102,43 +102,32 @@ class AccountCard extends ImmutablePureComponent {
     onMute: PropTypes.func.isRequired,
   };
 
-  _updateEmojis() {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount() {
-    this._updateEmojis();
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
-  componentDidUpdate() {
-    this._updateEmojis();
-  }
-
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  };
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  };
-
   handleFollow = () => {
     this.props.onFollow(this.props.account);
   };
@@ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent {
     this.props.onMute(this.props.account);
   };
 
-  setRef = (c) => {
-    this.node = c;
-  };
-
   render() {
     const { account, intl } = this.props;
 
@@ -239,7 +224,7 @@ class AccountCard extends ImmutablePureComponent {
           </div>
         </div>
 
-        <div className='directory__card__extra' ref={this.setRef}>
+        <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <div
             className='account__header__content translate'
             dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js
index 4534f7121d..ff1566e05c 100644
--- a/app/javascript/mastodon/features/getting_started/components/announcements.js
+++ b/app/javascript/mastodon/features/getting_started/components/announcements.js
@@ -39,35 +39,10 @@ class Content extends ImmutablePureComponent {
 
   componentDidMount () {
     this._updateLinks();
-    this._updateEmojis();
   }
 
   componentDidUpdate () {
     this._updateLinks();
-    this._updateEmojis();
-  }
-
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
-      return;
-    }
-
-    const emojis = node.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
-    }
   }
 
   _updateLinks () {
@@ -132,12 +107,30 @@ class Content extends ImmutablePureComponent {
     }
   }
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-original');
+    }
   }
 
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render () {
@@ -148,6 +141,8 @@ class Content extends ImmutablePureComponent {
         className='announcements__item__content translate'
         ref={this.setRef}
         dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
       />
     );
   }

From c8d11b8bdb97a1a2f8aaf5deca5f1c6c7c0d2688 Mon Sep 17 00:00:00 2001
From: Shubhendra Singh Chauhan <withshubh@gmail.com>
Date: Mon, 1 Feb 2021 01:56:09 +0530
Subject: [PATCH 15/24] Fixed code quality issues (#15541)

* Added .deepsource.toml

* Removed bad use of `alias`

* Fixed operand order in the binary expression

* Prefixed unused method arguments with an underscore

* Replaced the old OpenSSL algorithmic constants with the newer strings initializers.

* Removed unnecessary UTF-8 encoding comment
---
 .deepsource.toml                               | 18 ++++++++++++++++++
 config/initializers/fast_blank.rb              |  2 +-
 config/initializers/simple_form.rb             |  4 ++--
 config/initializers/twitter_regex.rb           |  2 +-
 lib/json_ld/identity.rb                        |  1 -
 lib/json_ld/security.rb                        |  1 -
 lib/mastodon/email_domain_blocks_cli.rb        |  2 +-
 lib/paperclip/color_extractor.rb               |  4 ++--
 .../activitypub/linked_data_signature_spec.rb  |  2 +-
 9 files changed, 26 insertions(+), 10 deletions(-)
 create mode 100644 .deepsource.toml

diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 0000000000..73d38901ca
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,18 @@
+version = 1
+
+test_patterns = ["/app/javascript/mastodon/**/__tests__/**"]
+
+[[analyzers]]
+name = "ruby"
+enabled = true
+
+[[analyzers]]
+name = "javascript"
+enabled = true
+
+  [analyzers.meta]
+  environment = [
+    "nodejs",
+    "browser",
+    "jest"
+  ]
diff --git a/config/initializers/fast_blank.rb b/config/initializers/fast_blank.rb
index 174ea76647..f0b7cac78b 100644
--- a/config/initializers/fast_blank.rb
+++ b/config/initializers/fast_blank.rb
@@ -1,5 +1,5 @@
 if String.method_defined?(:blank_as?)
   class String
-    alias_method :blank?, :blank_as?
+    alias blank? blank_as?
   end
 end
diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index 3dc48ef08f..3a2097d2fb 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -1,7 +1,7 @@
 # Use this setup block to configure all options available in SimpleForm.
 
 module AppendComponent
-  def append(wrapper_options = nil)
+  def append(_wrapper_options = nil)
     @append ||= begin
       options[:append].to_s.html_safe if options[:append].present?
     end
@@ -9,7 +9,7 @@ module AppendComponent
 end
 
 module RecommendedComponent
-  def recommended(wrapper_options = nil)
+  def recommended(_wrapper_options = nil)
     return unless options[:recommended]
     options[:label_text] = ->(raw_label_text, _required_label_text, _label_present) { safe_join([raw_label_text, ' ', content_tag(:span, I18n.t('simple_form.recommended'), class: 'recommended')]) }
     nil
diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb
index 7f99a00056..aca85dd43a 100644
--- a/config/initializers/twitter_regex.rb
+++ b/config/initializers/twitter_regex.rb
@@ -75,7 +75,7 @@ module Twitter
     # XMPP or magnet URIs an empty array will be returned.
     #
     # If a block is given then it will be called for each XMPP URI.
-    def extract_extra_uris_with_indices(text, options = {}) # :yields: uri, start, end
+    def extract_extra_uris_with_indices(text, _options = {}) # :yields: uri, start, end
       return [] unless text && text.index(":")
       urls = []
 
diff --git a/lib/json_ld/identity.rb b/lib/json_ld/identity.rb
index 4fb3f8e9d9..f41899150d 100644
--- a/lib/json_ld/identity.rb
+++ b/lib/json_ld/identity.rb
@@ -1,4 +1,3 @@
-# -*- encoding: utf-8 -*-
 # frozen_string_literal: true
 # This file generated automatically from http://w3id.org/identity/v1
 require 'json/ld'
diff --git a/lib/json_ld/security.rb b/lib/json_ld/security.rb
index a6fbce95f5..ef53913401 100644
--- a/lib/json_ld/security.rb
+++ b/lib/json_ld/security.rb
@@ -1,4 +1,3 @@
-# -*- encoding: utf-8 -*-
 # frozen_string_literal: true
 # This file generated automatically from http://w3id.org/security/v1
 require 'json/ld'
diff --git a/lib/mastodon/email_domain_blocks_cli.rb b/lib/mastodon/email_domain_blocks_cli.rb
index 55a637d682..f79df302a2 100644
--- a/lib/mastodon/email_domain_blocks_cli.rb
+++ b/lib/mastodon/email_domain_blocks_cli.rb
@@ -113,7 +113,7 @@ module Mastodon
         result = entry.destroy
 
         if result
-          processed += 1 + children_count
+          processed += children_count + 1
         else
           say("#{domain} could not be unblocked.", :red)
           failed += 1
diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb
index a70a3d21f2..d3b8e10229 100644
--- a/lib/paperclip/color_extractor.rb
+++ b/lib/paperclip/color_extractor.rb
@@ -55,7 +55,7 @@ module Paperclip
       # If we don't have enough colors for accent and foreground, generate
       # new ones by manipulating the background color
       (2 - foreground_colors.size).times do |i|
-        foreground_colors << lighten_or_darken(background_color, 35 + (15 * i))
+        foreground_colors << lighten_or_darken(background_color, 35 + (i * 15))
       end
 
       # We want the color with the highest contrast to background to be the foreground one,
@@ -147,7 +147,7 @@ module Paperclip
         g = l.to_f
         b = l.to_f # achromatic
       else
-        q = l < 0.5 ? l * (1 + s) : l + s - l * s
+        q = l < 0.5 ? l * (s + 1) : l + s - l * s
         p = 2 * l - q
         r = hue_to_rgb(p, q, h + 1 / 3.0)
         g = hue_to_rgb(p, q, h)
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index 1f413eec99..2222c46fb5 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -81,6 +81,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do
     options_hash   = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
     document_hash  = Digest::SHA256.hexdigest(canonicalize(document))
     to_be_verified = options_hash + document_hash
-    Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified))
+    Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
   end
 end

From a044ddac5b3c0e2012c0e91bfbc07aa256a0684f Mon Sep 17 00:00:00 2001
From: ThibG <thib@sitedethib.com>
Date: Tue, 2 Feb 2021 14:49:57 +0100
Subject: [PATCH 16/24] Fix race conditions on account migration creation
 (#15597)

* Atomically check for processing lock in Move handler

* Prevent race condition when creating account migrations

Fixes #15595

* Add tests

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/lib/activitypub/activity/move.rb          | 11 +--
 app/models/account_migration.rb               | 14 ++-
 .../settings/migrations_controller_spec.rb    | 37 ++++++-
 spec/lib/activitypub/activity/move_spec.rb    | 99 ++++++++++++++-----
 4 files changed, 127 insertions(+), 34 deletions(-)

diff --git a/app/lib/activitypub/activity/move.rb b/app/lib/activitypub/activity/move.rb
index 7e073f64d2..8576ceccdf 100644
--- a/app/lib/activitypub/activity/move.rb
+++ b/app/lib/activitypub/activity/move.rb
@@ -4,9 +4,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity
   PROCESSING_COOLDOWN = 7.days.seconds
 
   def perform
-    return if origin_account.uri != object_uri || processed?
-
-    mark_as_processing!
+    return if origin_account.uri != object_uri
+    return unless mark_as_processing!
 
     target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri)
 
@@ -35,12 +34,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity
     value_or_id(@json['target'])
   end
 
-  def processed?
-    redis.exists?("move_in_progress:#{@account.id}")
-  end
-
   def mark_as_processing!
-    redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true)
+    redis.set("move_in_progress:#{@account.id}", true, nx: true, ex: PROCESSING_COOLDOWN)
   end
 
   def unmark_as_processing!
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
index 4fae98ed72..ded32c9c60 100644
--- a/app/models/account_migration.rb
+++ b/app/models/account_migration.rb
@@ -14,6 +14,8 @@
 #
 
 class AccountMigration < ApplicationRecord
+  include Redisable
+
   COOLDOWN_PERIOD = 30.days.freeze
 
   belongs_to :account
@@ -39,7 +41,13 @@ class AccountMigration < ApplicationRecord
 
     return false unless errors.empty?
 
-    save
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        save
+      else
+        raise Mastodon::RaceConditionError
+      end
+    end
   end
 
   def cooldown_at
@@ -75,4 +83,8 @@ class AccountMigration < ApplicationRecord
   def validate_migration_cooldown
     errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
   end
+
+  def lock_options
+    { redis: redis, key: "account_migration:#{account.id}" }
+  end
 end
diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb
index 36e4ba86e4..048d9de8d2 100644
--- a/spec/controllers/settings/migrations_controller_spec.rb
+++ b/spec/controllers/settings/migrations_controller_spec.rb
@@ -51,7 +51,7 @@ describe Settings::MigrationsController do
       it_behaves_like 'authenticate user'
     end
 
-    context 'when user is sign in' do
+    context 'when user is signed in' do
       subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } }
 
       let(:user) { Fabricate(:user, password: '12345678') }
@@ -67,12 +67,45 @@ describe Settings::MigrationsController do
         end
       end
 
-      context 'when acct is a current account' do
+      context 'when acct is the current account' do
         let(:acct) { user.account }
 
         it 'renders show' do
           is_expected.to render_template :show
         end
+
+        it 'does not update the moved account' do
+          expect(user.account.reload.moved_to_account_id).to be_nil
+        end
+      end
+
+      context 'when target account does not reference the account being moved from' do
+        let(:acct) { Fabricate(:account, also_known_as: []) }
+
+        it 'renders show' do
+          is_expected.to render_template :show
+        end
+
+        it 'does not update the moved account' do
+          expect(user.account.reload.moved_to_account_id).to be_nil
+        end
+      end
+
+      context 'when a recent migration already exists ' do
+        let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) }
+
+        before do
+          moved_to = Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)])
+          user.account.migrations.create!(acct: moved_to.acct)
+        end
+
+        it 'renders show' do
+          is_expected.to render_template :show
+        end
+
+        it 'does not update the moved account' do
+          expect(user.account.reload.moved_to_account_id).to be_nil
+        end
       end
     end
   end
diff --git a/spec/lib/activitypub/activity/move_spec.rb b/spec/lib/activitypub/activity/move_spec.rb
index 3574f273a9..2d1d276c57 100644
--- a/spec/lib/activitypub/activity/move_spec.rb
+++ b/spec/lib/activitypub/activity/move_spec.rb
@@ -1,23 +1,11 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Move do
-  let(:follower)    { Fabricate(:account) }
-  let(:old_account) { Fabricate(:account) }
-  let(:new_account) { Fabricate(:account) }
-
-  before do
-    follower.follow!(old_account)
-
-    old_account.update!(uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox')
-    new_account.update!(uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: [old_account.uri])
-
-    stub_request(:post, 'https://example.org/inbox').to_return(status: 200)
-    stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
-
-    service_stub = double
-    allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub)
-    allow(service_stub).to receive(:call).and_return(new_account)
-  end
+  let(:follower)         { Fabricate(:account) }
+  let(:old_account)      { Fabricate(:account, uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox') }
+  let(:new_account)      { Fabricate(:account, uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: also_known_as) }
+  let(:also_known_as)    { [old_account.uri] }
+  let(:returned_account) { new_account }
 
   let(:json) do
     {
@@ -30,6 +18,17 @@ RSpec.describe ActivityPub::Activity::Move do
     }.with_indifferent_access
   end
 
+  before do
+    follower.follow!(old_account)
+
+    stub_request(:post, old_account.inbox_url).to_return(status: 200)
+    stub_request(:post, new_account.inbox_url).to_return(status: 200)
+
+    service_stub = double
+    allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub)
+    allow(service_stub).to receive(:call).and_return(returned_account)
+  end
+
   describe '#perform' do
     subject { described_class.new(json, old_account) }
 
@@ -37,16 +36,70 @@ RSpec.describe ActivityPub::Activity::Move do
       subject.perform
     end
 
-    it 'sets moved account on old account' do
-      expect(old_account.reload.moved_to_account_id).to eq new_account.id
+    context 'when all conditions are met' do
+      it 'sets moved account on old account' do
+        expect(old_account.reload.moved_to_account_id).to eq new_account.id
+      end
+
+      it 'makes followers unfollow old account' do
+        expect(follower.following?(old_account)).to be false
+      end
+
+      it 'makes followers follow-request the new account' do
+        expect(follower.requested?(new_account)).to be true
+      end
     end
 
-    it 'makes followers unfollow old account' do
-      expect(follower.following?(old_account)).to be false
+    context "when the new account can't be resolved" do
+      let(:returned_account) { nil }
+
+      it 'does not set moved account on old account' do
+        expect(old_account.reload.moved_to_account_id).to be_nil
+      end
+
+      it 'does not make followers unfollow old account' do
+        expect(follower.following?(old_account)).to be true
+      end
+
+      it 'does not make followers follow-request the new account' do
+        expect(follower.requested?(new_account)).to be false
+      end
     end
 
-    it 'makes followers follow-request the new account' do
-      expect(follower.requested?(new_account)).to be true
+    context 'when the new account does not references the old account' do
+      let(:also_known_as) { [] }
+
+      it 'does not set moved account on old account' do
+        expect(old_account.reload.moved_to_account_id).to be_nil
+      end
+
+      it 'does not make followers unfollow old account' do
+        expect(follower.following?(old_account)).to be true
+      end
+
+      it 'does not make followers follow-request the new account' do
+        expect(follower.requested?(new_account)).to be false
+      end
+    end
+
+    context 'when a Move has been recently processed' do
+      around do |example|
+        Redis.current.set("move_in_progress:#{old_account.id}", true, nx: true, ex: 7.days.seconds)
+        example.run
+        Redis.current.del("move_in_progress:#{old_account.id}")
+      end
+
+      it 'does not set moved account on old account' do
+        expect(old_account.reload.moved_to_account_id).to be_nil
+      end
+
+      it 'does not make followers unfollow old account' do
+        expect(follower.following?(old_account)).to be true
+      end
+
+      it 'does not make followers follow-request the new account' do
+        expect(follower.requested?(new_account)).to be false
+      end
     end
   end
 end

From ab39d5fda55de468c9f5689065e3c328c7a9bd71 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Feb 2021 23:47:12 +0900
Subject: [PATCH 17/24] Bump chewy from 5.1.0 to 5.2.0 (#15648)

Bumps [chewy](https://github.com/toptal/chewy) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/toptal/chewy/releases)
- [Changelog](https://github.com/toptal/chewy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/toptal/chewy/compare/v5.1.0...v5.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      |  2 +-
 Gemfile.lock | 22 +++++++++++++---------
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/Gemfile b/Gemfile
index 8ac92cdf95..8dc1c43889 100644
--- a/Gemfile
+++ b/Gemfile
@@ -31,7 +31,7 @@ gem 'bootsnap', '~> 1.5', require: false
 gem 'browser'
 gem 'charlock_holmes', '~> 0.7.7'
 gem 'iso-639'
-gem 'chewy', '~> 5.1'
+gem 'chewy', '~> 5.2'
 gem 'cld3', '~> 3.4.1'
 gem 'devise', '~> 4.7'
 gem 'devise-two-factor', '~> 3.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index ba6fd64ac7..a96d4ee9bc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -143,8 +143,8 @@ GEM
       activesupport
     cbor (0.5.9.6)
     charlock_holmes (0.7.7)
-    chewy (5.1.0)
-      activesupport (>= 4.0)
+    chewy (5.2.0)
+      activesupport (>= 5.2)
       elasticsearch (>= 2.0.0)
       elasticsearch-dsl
     chunky_png (1.3.15)
@@ -195,13 +195,13 @@ GEM
       railties (>= 3.2)
     e2mmap (0.1.0)
     ed25519 (1.2.4)
-    elasticsearch (7.9.0)
-      elasticsearch-api (= 7.9.0)
-      elasticsearch-transport (= 7.9.0)
-    elasticsearch-api (7.9.0)
+    elasticsearch (7.10.1)
+      elasticsearch-api (= 7.10.1)
+      elasticsearch-transport (= 7.10.1)
+    elasticsearch-api (7.10.1)
       multi_json
     elasticsearch-dsl (0.1.9)
-    elasticsearch-transport (7.9.0)
+    elasticsearch-transport (7.10.1)
       faraday (~> 1)
       multi_json
     encryptor (3.0.0)
@@ -212,8 +212,11 @@ GEM
     fabrication (2.21.1)
     faker (2.15.1)
       i18n (>= 1.6, < 2)
-    faraday (1.0.1)
+    faraday (1.3.0)
+      faraday-net_http (~> 1.0)
       multipart-post (>= 1.2, < 3)
+      ruby2_keywords
+    faraday-net_http (1.0.1)
     fast_blank (1.0.0)
     fastimage (2.2.1)
     ffi (1.10.0)
@@ -556,6 +559,7 @@ GEM
     ruby-progressbar (1.11.0)
     ruby-saml (1.11.0)
       nokogiri (>= 1.5.10)
+    ruby2_keywords (0.0.4)
     rufus-scheduler (3.6.0)
       fugit (~> 1.1, >= 1.1.6)
     safety_net_attestation (0.4.0)
@@ -703,7 +707,7 @@ DEPENDENCIES
   capistrano-yarn (~> 2.0)
   capybara (~> 3.34)
   charlock_holmes (~> 0.7.7)
-  chewy (~> 5.1)
+  chewy (~> 5.2)
   cld3 (~> 3.4.1)
   climate_control (~> 0.2)
   color_diff (~> 0.1)

From b109df0e2cda737bc8e69dc3c92e725ebc08d07e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Feb 2021 23:47:35 +0900
Subject: [PATCH 18/24] Bump capybara from 3.34.0 to 3.35.3 (#15649)

Bumps [capybara](https://github.com/teamcapybara/capybara) from 3.34.0 to 3.35.3.
- [Release notes](https://github.com/teamcapybara/capybara/releases)
- [Changelog](https://github.com/teamcapybara/capybara/blob/master/History.md)
- [Commits](https://github.com/teamcapybara/capybara/compare/3.34.0...3.35.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Gemfile b/Gemfile
index 8dc1c43889..9443725ee8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -117,7 +117,7 @@ group :production, :test do
 end
 
 group :test do
-  gem 'capybara', '~> 3.34'
+  gem 'capybara', '~> 3.35'
   gem 'climate_control', '~> 0.2'
   gem 'faker', '~> 2.15'
   gem 'microformats', '~> 4.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index a96d4ee9bc..2d25252f5d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -131,13 +131,13 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (3.34.0)
+    capybara (3.35.3)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (~> 1.8)
       rack (>= 1.6.0)
       rack-test (>= 0.6.3)
-      regexp_parser (~> 1.5)
+      regexp_parser (>= 1.5, < 3.0)
       xpath (~> 3.2)
     case_transform (0.2)
       activesupport
@@ -506,7 +506,7 @@ GEM
       redis-store (>= 1.2, < 2)
     redis-store (1.9.0)
       redis (>= 4, < 5)
-    regexp_parser (1.8.2)
+    regexp_parser (2.0.3)
     request_store (1.5.0)
       rack (>= 1.4)
     responders (3.0.1)
@@ -705,7 +705,7 @@ DEPENDENCIES
   capistrano-rails (~> 1.6)
   capistrano-rbenv (~> 2.2)
   capistrano-yarn (~> 2.0)
-  capybara (~> 3.34)
+  capybara (~> 3.35)
   charlock_holmes (~> 0.7.7)
   chewy (~> 5.2)
   cld3 (~> 3.4.1)

From 2620e2cfa33c60984cf5de1525e4545fd46e4cd2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Feb 2021 23:56:17 +0900
Subject: [PATCH 19/24] Bump webmock from 3.11.1 to 3.11.2 (#15650)

Bumps [webmock](https://github.com/bblimke/webmock) from 3.11.1 to 3.11.2.
- [Release notes](https://github.com/bblimke/webmock/releases)
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.11.1...v3.11.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 2d25252f5d..a0a72a9f1a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -664,7 +664,7 @@ GEM
       safety_net_attestation (~> 0.4.0)
       securecompare (~> 1.0)
       tpm-key_attestation (~> 0.9.0)
-    webmock (3.11.1)
+    webmock (3.11.2)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff (>= 0.4.0, < 2.0.0)

From fd7f4056af927868ccf623f49abba356aa2a830e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Feb 2021 23:56:54 +0900
Subject: [PATCH 20/24] Bump mini-css-extract-plugin from 1.3.4 to 1.3.5
 (#15654)

Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v1.3.4...v1.3.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 755b52b378..c7542d8f44 100644
--- a/package.json
+++ b/package.json
@@ -111,7 +111,7 @@
     "lodash": "^4.17.19",
     "mark-loader": "^0.1.6",
     "marky": "^1.2.1",
-    "mini-css-extract-plugin": "^1.3.4",
+    "mini-css-extract-plugin": "^1.3.5",
     "mkdirp": "^1.0.4",
     "npmlog": "^4.1.2",
     "object-assign": "^4.1.1",
diff --git a/yarn.lock b/yarn.lock
index 75dc36d381..f4eb6a9ab6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7105,10 +7105,10 @@ min-indent@^1.0.0:
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
-mini-css-extract-plugin@^1.3.4:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz#706e69632cdcdb8b15bf8e638442a0dba304a9c8"
-  integrity sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg==
+mini-css-extract-plugin@^1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz#252166e78879c106e0130f229d44e0cbdfcebed3"
+  integrity sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ==
   dependencies:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"

From b8aed2d25541b767d9dfb1ee2ad8d5cd2db16695 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 3 Feb 2021 04:27:19 +0900
Subject: [PATCH 21/24] Bump fastimage from 2.2.1 to 2.2.2 (#15651)

Bumps [fastimage](https://github.com/sdsykes/fastimage) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/sdsykes/fastimage/releases)
- [Commits](https://github.com/sdsykes/fastimage/compare/v2.2.1...v2.2.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index a0a72a9f1a..65512d711f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -218,7 +218,7 @@ GEM
       ruby2_keywords
     faraday-net_http (1.0.1)
     fast_blank (1.0.0)
-    fastimage (2.2.1)
+    fastimage (2.2.2)
     ffi (1.10.0)
     ffi-compiler (1.0.1)
       ffi (>= 1.0.0)

From 1d96bf9f43529745148b15a4cbb39a852ebb5280 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 3 Feb 2021 04:27:57 +0900
Subject: [PATCH 22/24] Bump oj from 3.11.1 to 3.11.2 (#15657)

Bumps [oj](https://github.com/ohler55/oj) from 3.11.1 to 3.11.2.
- [Release notes](https://github.com/ohler55/oj/releases)
- [Changelog](https://github.com/ohler55/oj/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ohler55/oj/compare/v3.11.1...v3.11.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 65512d711f..1a8c012ffb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -372,7 +372,7 @@ GEM
       concurrent-ruby (~> 1.0, >= 1.0.2)
       sidekiq (>= 3.5)
       statsd-ruby (~> 1.4, >= 1.4.0)
-    oj (3.11.1)
+    oj (3.11.2)
     omniauth (1.9.1)
       hashie (>= 3.4.6)
       rack (>= 1.6.2, < 3)

From 4e933924bdea3392ebeaeaa7c341593eb200512c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= <me@mkljczk.pl>
Date: Wed, 3 Feb 2021 00:38:48 +0100
Subject: [PATCH 23/24] Update Doorkeeper strings for
 `doorkeeper.errors.messages.invalid_request` (#15659)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Update Doorkeeper strings for doorkeeper.errors.messages.invalid_request

Signed-off-by: marcin mikołajczak <me@mkljczk.pl>

* Update config/locales/doorkeeper.en.yml

Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>

Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>
---
 config/locales/doorkeeper.en.yml | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 2be2ef0364..ec322f0718 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -83,7 +83,10 @@ en:
         invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
         invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
         invalid_redirect_uri: The redirect uri included is not valid.
-        invalid_request: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
+        invalid_request:
+          missing_param: 'Missing required parameter: %{value}.'
+          request_not_authorized: Request need to be authorized. Required parameter for authorizing request is missing or invalid.
+          unknown: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
         invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found
         invalid_scope: The requested scope is invalid, unknown, or malformed.
         invalid_token:

From e1fa06c459a28a575d0da540432c61b702e99cdd Mon Sep 17 00:00:00 2001
From: ThibG <thib@sitedethib.com>
Date: Sun, 31 Jan 2021 21:25:31 +0100
Subject: [PATCH 24/24] [Glitch] Change custom emoji to be animated when
 hovering container

Port 3efa0c54b6656fba189baab0857e60c0bc4f3c7d to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../glitch/components/display_name.js         | 43 +++++---------
 .../glitch/components/status_content.js       | 59 ++++++++++---------
 .../features/account/components/header.js     | 43 +++++---------
 .../components/conversation.js                | 43 +++++---------
 .../directory/components/account_card.js      | 47 +++++----------
 .../components/announcements.js               | 53 ++++++++---------
 6 files changed, 112 insertions(+), 176 deletions(-)

diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js
index 44662a8b87..ad978a2c61 100644
--- a/app/javascript/flavours/glitch/components/display_name.js
+++ b/app/javascript/flavours/glitch/components/display_name.js
@@ -15,45 +15,30 @@ export default class DisplayName extends React.PureComponent {
     handleClick: PropTypes.func,
   };
 
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
-  setRef = (c) => {
-    this.node = c;
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render() {
@@ -101,7 +86,7 @@ export default class DisplayName extends React.PureComponent {
     }
 
     return (
-      <span className={computedClass} ref={this.setRef}>
+      <span className={computedClass} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         {displayName}
         {inline ? ' ' : null}
         {suffix}
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index 61a28e9a76..34ff973059 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -154,35 +154,38 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  _updateStatusEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
+      emoji.src = emoji.getAttribute('data-original');
+    }
+  }
 
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
     }
   }
 
   componentDidMount () {
     this._updateStatusLinks();
-    this._updateStatusEmojis();
   }
 
   componentDidUpdate () {
     this._updateStatusLinks();
-    this._updateStatusEmojis();
     if (this.props.onUpdate) this.props.onUpdate();
   }
 
@@ -206,14 +209,6 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
   handleMouseDown = (e) => {
     this.startXY = [e.clientX, e.clientY];
   }
@@ -253,10 +248,6 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
-  setRef = (c) => {
-    this.node = c;
-  }
-
   setContentsRef = (c) => {
     this.contentsNode = c;
   }
@@ -323,7 +314,7 @@ export default class StatusContent extends React.PureComponent {
       }
 
       return (
-        <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}>
+        <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
           <p
             style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
           >
@@ -343,6 +334,8 @@ export default class StatusContent extends React.PureComponent {
               tabIndex={!hidden ? 0 : null}
               dangerouslySetInnerHTML={content}
               className='status__content__text translate'
+              onMouseEnter={this.handleMouseEnter}
+              onMouseLeave={this.handleMouseLeave}
             />
             {media}
           </div>
@@ -356,7 +349,6 @@ export default class StatusContent extends React.PureComponent {
           onMouseDown={this.handleMouseDown}
           onMouseUp={this.handleMouseUp}
           tabIndex='0'
-          ref={this.setRef}
         >
           <div
             ref={this.setContentsRef}
@@ -364,6 +356,8 @@ export default class StatusContent extends React.PureComponent {
             dangerouslySetInnerHTML={content}
             className='status__content__text translate'
             tabIndex='0'
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
           />
           {media}
         </div>
@@ -373,9 +367,16 @@ export default class StatusContent extends React.PureComponent {
         <div
           className='status__content'
           tabIndex='0'
-          ref={this.setRef}
         >
-          <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text translate' dangerouslySetInnerHTML={content} tabIndex='0' />
+          <div
+            ref={this.setContentsRef}
+            key={`contents-${tagLinks}`}
+            className='status__content__text translate'
+            dangerouslySetInnerHTML={content}
+            tabIndex='0'
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
+          />
           {media}
         </div>
       );
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 6a572862d3..c18520c00a 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -88,45 +88,30 @@ class Header extends ImmutablePureComponent {
     window.open(profileLink, '_blank');
   }
 
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  }
-
-  setRef = (c) => {
-    this.node = c;
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render () {
@@ -279,7 +264,7 @@ class Header extends ImmutablePureComponent {
     }
 
     return (
-      <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
+      <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         <div className='account__header__image'>
           <div className='account__header__info'>
             {info}
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
index 0af8b348f2..98b48cd90e 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
@@ -67,41 +67,30 @@ class Conversation extends ImmutablePureComponent {
     }
   }
 
-  _updateEmojis () {
-    const node = this.namesNode;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount () {
-    this._updateEmojis();
-  }
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
 
-  componentDidUpdate () {
-    this._updateEmojis();
-  }
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  }
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   handleClick = () => {
@@ -152,10 +141,6 @@ class Conversation extends ImmutablePureComponent {
     this.setState({ isExpanded: value });
   }
 
-  setNamesRef = (c) => {
-    this.namesNode = c;
-  }
-
   render () {
     const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
     const { isExpanded } = this.state;
@@ -206,7 +191,7 @@ class Conversation extends ImmutablePureComponent {
                 {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
               </div>
 
-              <div className='conversation__content__names' ref={this.setNamesRef}>
+              <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
                 <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
               </div>
             </div>
diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js
index 5f952b3825..9fe84c10b1 100644
--- a/app/javascript/flavours/glitch/features/directory/components/account_card.js
+++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js
@@ -102,43 +102,32 @@ class AccountCard extends ImmutablePureComponent {
     onMute: PropTypes.func.isRequired,
   };
 
-  _updateEmojis() {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
       return;
     }
 
-    const emojis = node.querySelectorAll('.custom-emoji');
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
 
     for (var i = 0; i < emojis.length; i++) {
       let emoji = emojis[i];
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+      emoji.src = emoji.getAttribute('data-original');
     }
   }
 
-  componentDidMount() {
-    this._updateEmojis();
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
-  componentDidUpdate() {
-    this._updateEmojis();
-  }
-
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
-  };
-
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
-  };
-
   handleFollow = () => {
     this.props.onFollow(this.props.account);
   };
@@ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent {
     this.props.onMute(this.props.account);
   };
 
-  setRef = (c) => {
-    this.node = c;
-  };
-
   render() {
     const { account, intl } = this.props;
 
@@ -239,7 +224,7 @@ class AccountCard extends ImmutablePureComponent {
           </div>
         </div>
 
-        <div className='directory__card__extra' ref={this.setRef}>
+        <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <div
             className='account__header__content translate'
             dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
index 1ab5d9ba69..4eebe83ef2 100644
--- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
+++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
@@ -39,35 +39,10 @@ class Content extends ImmutablePureComponent {
 
   componentDidMount () {
     this._updateLinks();
-    this._updateEmojis();
   }
 
   componentDidUpdate () {
     this._updateLinks();
-    this._updateEmojis();
-  }
-
-  _updateEmojis () {
-    const node = this.node;
-
-    if (!node || autoPlayGif) {
-      return;
-    }
-
-    const emojis = node.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-
-      if (emoji.classList.contains('status-emoji')) {
-        continue;
-      }
-
-      emoji.classList.add('status-emoji');
-
-      emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
-      emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
-    }
   }
 
   _updateLinks () {
@@ -132,12 +107,30 @@ class Content extends ImmutablePureComponent {
     }
   }
 
-  handleEmojiMouseEnter = ({ target }) => {
-    target.src = target.getAttribute('data-original');
+  handleMouseEnter = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-original');
+    }
   }
 
-  handleEmojiMouseLeave = ({ target }) => {
-    target.src = target.getAttribute('data-static');
+  handleMouseLeave = ({ currentTarget }) => {
+    if (autoPlayGif) {
+      return;
+    }
+
+    const emojis = currentTarget.querySelectorAll('.custom-emoji');
+
+    for (var i = 0; i < emojis.length; i++) {
+      let emoji = emojis[i];
+      emoji.src = emoji.getAttribute('data-static');
+    }
   }
 
   render () {
@@ -148,6 +141,8 @@ class Content extends ImmutablePureComponent {
         className='announcements__item__content translate'
         ref={this.setRef}
         dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
       />
     );
   }