Merge branch 'main' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						85e0b5ce91
					
				
					 33 changed files with 453 additions and 319 deletions
				
			
		|  | @ -30,7 +30,7 @@ plugins: | ||||||
|     channel: eslint-7 |     channel: eslint-7 | ||||||
|   rubocop: |   rubocop: | ||||||
|     enabled: true |     enabled: true | ||||||
|     channel: rubocop-1-70 |     channel: rubocop-1-8-1 | ||||||
|   sass-lint: |   sass-lint: | ||||||
|     enabled: true |     enabled: true | ||||||
| exclude_patterns: | exclude_patterns: | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								.deepsource.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.deepsource.toml
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||||
|  |   ] | ||||||
|  | @ -4,7 +4,7 @@ FROM ubuntu:20.04 as build-dep | ||||||
| SHELL ["/usr/bin/bash", "-c"] | SHELL ["/usr/bin/bash", "-c"] | ||||||
| 
 | 
 | ||||||
| # Install Node v12 (LTS) | # Install Node v12 (LTS) | ||||||
| ENV NODE_VER="12.20.0" | ENV NODE_VER="12.20.1" | ||||||
| RUN ARCH= && \ | RUN ARCH= && \ | ||||||
|     dpkgArch="$(dpkg --print-architecture)" && \ |     dpkgArch="$(dpkg --print-architecture)" && \ | ||||||
|   case "${dpkgArch##*-}" in \ |   case "${dpkgArch##*-}" in \ | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Gemfile
									
									
									
									
									
								
							|  | @ -8,7 +8,7 @@ gem 'pkg-config', '~> 1.4' | ||||||
| gem 'puma', '~> 5.1' | gem 'puma', '~> 5.1' | ||||||
| gem 'rails', '~> 5.2.4.4' | gem 'rails', '~> 5.2.4.4' | ||||||
| gem 'sprockets', '~> 3.7.2' | gem 'sprockets', '~> 3.7.2' | ||||||
| gem 'thor', '~> 1.0' | gem 'thor', '~> 1.1' | ||||||
| gem 'rack', '~> 2.2.3' | gem 'rack', '~> 2.2.3' | ||||||
| 
 | 
 | ||||||
| gem 'hamlit-rails', '~> 0.2' | gem 'hamlit-rails', '~> 0.2' | ||||||
|  | @ -31,7 +31,7 @@ gem 'bootsnap', '~> 1.5', require: false | ||||||
| gem 'browser' | gem 'browser' | ||||||
| gem 'charlock_holmes', '~> 0.7.7' | gem 'charlock_holmes', '~> 0.7.7' | ||||||
| gem 'iso-639' | gem 'iso-639' | ||||||
| gem 'chewy', '~> 5.1' | gem 'chewy', '~> 5.2' | ||||||
| gem 'cld3', '~> 3.4.1' | gem 'cld3', '~> 3.4.1' | ||||||
| gem 'devise', '~> 4.7' | gem 'devise', '~> 4.7' | ||||||
| gem 'devise-two-factor', '~> 3.1' | gem 'devise-two-factor', '~> 3.1' | ||||||
|  | @ -73,7 +73,7 @@ gem 'parallel', '~> 1.20' | ||||||
| gem 'posix-spawn' | gem 'posix-spawn' | ||||||
| gem 'pundit', '~> 2.1' | gem 'pundit', '~> 2.1' | ||||||
| gem 'premailer-rails' | gem 'premailer-rails' | ||||||
| gem 'rack-attack', '~> 6.3' | gem 'rack-attack', '~> 6.4' | ||||||
| gem 'rack-cors', '~> 1.1', require: 'rack/cors' | gem 'rack-cors', '~> 1.1', require: 'rack/cors' | ||||||
| gem 'rails-i18n', '~> 5.1' | gem 'rails-i18n', '~> 5.1' | ||||||
| gem 'rails-settings-cached', '~> 0.6' | gem 'rails-settings-cached', '~> 0.6' | ||||||
|  | @ -94,7 +94,7 @@ gem 'stoplight', '~> 2.2.1' | ||||||
| gem 'strong_migrations', '~> 0.7' | gem 'strong_migrations', '~> 0.7' | ||||||
| gem 'tty-prompt', '~> 0.23', require: false | gem 'tty-prompt', '~> 0.23', require: false | ||||||
| gem 'twitter-text', '~> 1.14' | gem 'twitter-text', '~> 1.14' | ||||||
| gem 'tzinfo-data', '~> 1.2020' | gem 'tzinfo-data', '~> 1.2021' | ||||||
| gem 'webpacker', '~> 5.2' | gem 'webpacker', '~> 5.2' | ||||||
| gem 'webpush' | gem 'webpush' | ||||||
| gem 'webauthn', '~> 3.0.0.alpha1' | gem 'webauthn', '~> 3.0.0.alpha1' | ||||||
|  | @ -119,7 +119,7 @@ group :production, :test do | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| group :test do | group :test do | ||||||
|   gem 'capybara', '~> 3.34' |   gem 'capybara', '~> 3.35' | ||||||
|   gem 'climate_control', '~> 0.2' |   gem 'climate_control', '~> 0.2' | ||||||
|   gem 'faker', '~> 2.15' |   gem 'faker', '~> 2.15' | ||||||
|   gem 'microformats', '~> 4.2' |   gem 'microformats', '~> 4.2' | ||||||
|  | @ -140,7 +140,7 @@ group :development do | ||||||
|   gem 'letter_opener', '~> 1.7' |   gem 'letter_opener', '~> 1.7' | ||||||
|   gem 'letter_opener_web', '~> 1.4' |   gem 'letter_opener_web', '~> 1.4' | ||||||
|   gem 'memory_profiler' |   gem 'memory_profiler' | ||||||
|   gem 'rubocop', '~> 1.7', require: false |   gem 'rubocop', '~> 1.8', require: false | ||||||
|   gem 'rubocop-rails', '~> 2.9', require: false |   gem 'rubocop-rails', '~> 2.9', require: false | ||||||
|   gem 'brakeman', '~> 4.10', require: false |   gem 'brakeman', '~> 4.10', require: false | ||||||
|   gem 'bundler-audit', '~> 0.7', require: false |   gem 'bundler-audit', '~> 0.7', require: false | ||||||
|  |  | ||||||
							
								
								
									
										64
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								Gemfile.lock
									
									
									
									
									
								
							|  | @ -109,7 +109,7 @@ GEM | ||||||
|     brakeman (4.10.1) |     brakeman (4.10.1) | ||||||
|     browser (4.2.0) |     browser (4.2.0) | ||||||
|     builder (3.2.4) |     builder (3.2.4) | ||||||
|     bullet (6.1.2) |     bullet (6.1.3) | ||||||
|       activesupport (>= 3.0.0) |       activesupport (>= 3.0.0) | ||||||
|       uniform_notifier (~> 1.11) |       uniform_notifier (~> 1.11) | ||||||
|     bundler-audit (0.7.0.1) |     bundler-audit (0.7.0.1) | ||||||
|  | @ -131,20 +131,20 @@ GEM | ||||||
|       sshkit (~> 1.3) |       sshkit (~> 1.3) | ||||||
|     capistrano-yarn (2.0.2) |     capistrano-yarn (2.0.2) | ||||||
|       capistrano (~> 3.0) |       capistrano (~> 3.0) | ||||||
|     capybara (3.34.0) |     capybara (3.35.3) | ||||||
|       addressable |       addressable | ||||||
|       mini_mime (>= 0.1.3) |       mini_mime (>= 0.1.3) | ||||||
|       nokogiri (~> 1.8) |       nokogiri (~> 1.8) | ||||||
|       rack (>= 1.6.0) |       rack (>= 1.6.0) | ||||||
|       rack-test (>= 0.6.3) |       rack-test (>= 0.6.3) | ||||||
|       regexp_parser (~> 1.5) |       regexp_parser (>= 1.5, < 3.0) | ||||||
|       xpath (~> 3.2) |       xpath (~> 3.2) | ||||||
|     case_transform (0.2) |     case_transform (0.2) | ||||||
|       activesupport |       activesupport | ||||||
|     cbor (0.5.9.6) |     cbor (0.5.9.6) | ||||||
|     charlock_holmes (0.7.7) |     charlock_holmes (0.7.7) | ||||||
|     chewy (5.1.0) |     chewy (5.2.0) | ||||||
|       activesupport (>= 4.0) |       activesupport (>= 5.2) | ||||||
|       elasticsearch (>= 2.0.0) |       elasticsearch (>= 2.0.0) | ||||||
|       elasticsearch-dsl |       elasticsearch-dsl | ||||||
|     chunky_png (1.3.15) |     chunky_png (1.3.15) | ||||||
|  | @ -195,13 +195,13 @@ GEM | ||||||
|       railties (>= 3.2) |       railties (>= 3.2) | ||||||
|     e2mmap (0.1.0) |     e2mmap (0.1.0) | ||||||
|     ed25519 (1.2.4) |     ed25519 (1.2.4) | ||||||
|     elasticsearch (7.9.0) |     elasticsearch (7.10.1) | ||||||
|       elasticsearch-api (= 7.9.0) |       elasticsearch-api (= 7.10.1) | ||||||
|       elasticsearch-transport (= 7.9.0) |       elasticsearch-transport (= 7.10.1) | ||||||
|     elasticsearch-api (7.9.0) |     elasticsearch-api (7.10.1) | ||||||
|       multi_json |       multi_json | ||||||
|     elasticsearch-dsl (0.1.9) |     elasticsearch-dsl (0.1.9) | ||||||
|     elasticsearch-transport (7.9.0) |     elasticsearch-transport (7.10.1) | ||||||
|       faraday (~> 1) |       faraday (~> 1) | ||||||
|       multi_json |       multi_json | ||||||
|     encryptor (3.0.0) |     encryptor (3.0.0) | ||||||
|  | @ -212,10 +212,13 @@ GEM | ||||||
|     fabrication (2.21.1) |     fabrication (2.21.1) | ||||||
|     faker (2.15.1) |     faker (2.15.1) | ||||||
|       i18n (>= 1.6, < 2) |       i18n (>= 1.6, < 2) | ||||||
|     faraday (1.0.1) |     faraday (1.3.0) | ||||||
|  |       faraday-net_http (~> 1.0) | ||||||
|       multipart-post (>= 1.2, < 3) |       multipart-post (>= 1.2, < 3) | ||||||
|  |       ruby2_keywords | ||||||
|  |     faraday-net_http (1.0.1) | ||||||
|     fast_blank (1.0.0) |     fast_blank (1.0.0) | ||||||
|     fastimage (2.2.1) |     fastimage (2.2.2) | ||||||
|     ffi (1.10.0) |     ffi (1.10.0) | ||||||
|     ffi-compiler (1.0.1) |     ffi-compiler (1.0.1) | ||||||
|       ffi (>= 1.0.0) |       ffi (>= 1.0.0) | ||||||
|  | @ -369,7 +372,7 @@ GEM | ||||||
|       concurrent-ruby (~> 1.0, >= 1.0.2) |       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||||
|       sidekiq (>= 3.5) |       sidekiq (>= 3.5) | ||||||
|       statsd-ruby (~> 1.4, >= 1.4.0) |       statsd-ruby (~> 1.4, >= 1.4.0) | ||||||
|     oj (3.11.0) |     oj (3.11.2) | ||||||
|     omniauth (1.9.1) |     omniauth (1.9.1) | ||||||
|       hashie (>= 3.4.6) |       hashie (>= 3.4.6) | ||||||
|       rack (>= 1.6.2, < 3) |       rack (>= 1.6.2, < 3) | ||||||
|  | @ -436,7 +439,7 @@ GEM | ||||||
|     raabro (1.3.3) |     raabro (1.3.3) | ||||||
|     racc (1.5.2) |     racc (1.5.2) | ||||||
|     rack (2.2.3) |     rack (2.2.3) | ||||||
|     rack-attack (6.3.1) |     rack-attack (6.4.0) | ||||||
|       rack (>= 1.0, < 3) |       rack (>= 1.0, < 3) | ||||||
|     rack-cors (1.1.1) |     rack-cors (1.1.1) | ||||||
|       rack (>= 2.0.0) |       rack (>= 2.0.0) | ||||||
|  | @ -504,7 +507,7 @@ GEM | ||||||
|       redis-store (>= 1.2, < 2) |       redis-store (>= 1.2, < 2) | ||||||
|     redis-store (1.9.0) |     redis-store (1.9.0) | ||||||
|       redis (>= 4, < 5) |       redis (>= 4, < 5) | ||||||
|     regexp_parser (1.8.2) |     regexp_parser (2.0.3) | ||||||
|     request_store (1.5.0) |     request_store (1.5.0) | ||||||
|       rack (>= 1.4) |       rack (>= 1.4) | ||||||
|     responders (3.0.1) |     responders (3.0.1) | ||||||
|  | @ -539,16 +542,16 @@ GEM | ||||||
|     rspec-support (3.10.1) |     rspec-support (3.10.1) | ||||||
|     rspec_junit_formatter (0.4.1) |     rspec_junit_formatter (0.4.1) | ||||||
|       rspec-core (>= 2, < 4, != 2.12.0) |       rspec-core (>= 2, < 4, != 2.12.0) | ||||||
|     rubocop (1.7.0) |     rubocop (1.8.1) | ||||||
|       parallel (~> 1.10) |       parallel (~> 1.10) | ||||||
|       parser (>= 2.7.1.5) |       parser (>= 3.0.0.0) | ||||||
|       rainbow (>= 2.2.2, < 4.0) |       rainbow (>= 2.2.2, < 4.0) | ||||||
|       regexp_parser (>= 1.8, < 3.0) |       regexp_parser (>= 1.8, < 3.0) | ||||||
|       rexml |       rexml | ||||||
|       rubocop-ast (>= 1.2.0, < 2.0) |       rubocop-ast (>= 1.2.0, < 2.0) | ||||||
|       ruby-progressbar (~> 1.7) |       ruby-progressbar (~> 1.7) | ||||||
|       unicode-display_width (>= 1.4.0, < 2.0) |       unicode-display_width (>= 1.4.0, < 3.0) | ||||||
|     rubocop-ast (1.3.0) |     rubocop-ast (1.4.0) | ||||||
|       parser (>= 2.7.1.5) |       parser (>= 2.7.1.5) | ||||||
|     rubocop-rails (2.9.1) |     rubocop-rails (2.9.1) | ||||||
|       activesupport (>= 4.2.0) |       activesupport (>= 4.2.0) | ||||||
|  | @ -557,6 +560,7 @@ GEM | ||||||
|     ruby-progressbar (1.11.0) |     ruby-progressbar (1.11.0) | ||||||
|     ruby-saml (1.11.0) |     ruby-saml (1.11.0) | ||||||
|       nokogiri (>= 1.5.10) |       nokogiri (>= 1.5.10) | ||||||
|  |     ruby2_keywords (0.0.4) | ||||||
|     rufus-scheduler (3.6.0) |     rufus-scheduler (3.6.0) | ||||||
|       fugit (~> 1.1, >= 1.1.6) |       fugit (~> 1.1, >= 1.1.6) | ||||||
|     safety_net_attestation (0.4.0) |     safety_net_attestation (0.4.0) | ||||||
|  | @ -570,7 +574,7 @@ GEM | ||||||
|       railties (>= 4.0.0) |       railties (>= 4.0.0) | ||||||
|     securecompare (1.0.0) |     securecompare (1.0.0) | ||||||
|     semantic_range (2.3.0) |     semantic_range (2.3.0) | ||||||
|     sidekiq (6.1.2) |     sidekiq (6.1.3) | ||||||
|       connection_pool (>= 2.2.2) |       connection_pool (>= 2.2.2) | ||||||
|       rack (~> 2.0) |       rack (~> 2.0) | ||||||
|       redis (>= 4.2.0) |       redis (>= 4.2.0) | ||||||
|  | @ -620,7 +624,7 @@ GEM | ||||||
|       unicode-display_width (~> 1.1, >= 1.1.1) |       unicode-display_width (~> 1.1, >= 1.1.1) | ||||||
|     terrapin (0.6.0) |     terrapin (0.6.0) | ||||||
|       climate_control (>= 0.0.3, < 1.0) |       climate_control (>= 0.0.3, < 1.0) | ||||||
|     thor (1.0.1) |     thor (1.1.0) | ||||||
|     thread_safe (0.3.6) |     thread_safe (0.3.6) | ||||||
|     thwait (0.2.0) |     thwait (0.2.0) | ||||||
|       e2mmap |       e2mmap | ||||||
|  | @ -642,13 +646,13 @@ GEM | ||||||
|       unf (~> 0.1.0) |       unf (~> 0.1.0) | ||||||
|     tzinfo (1.2.9) |     tzinfo (1.2.9) | ||||||
|       thread_safe (~> 0.1) |       thread_safe (~> 0.1) | ||||||
|     tzinfo-data (1.2020.6) |     tzinfo-data (1.2021.1) | ||||||
|       tzinfo (>= 1.0.0) |       tzinfo (>= 1.0.0) | ||||||
|     unf (0.1.4) |     unf (0.1.4) | ||||||
|       unf_ext |       unf_ext | ||||||
|     unf_ext (0.0.7.7) |     unf_ext (0.0.7.7) | ||||||
|     unicode-display_width (1.7.0) |     unicode-display_width (1.7.0) | ||||||
|     uniform_notifier (1.13.0) |     uniform_notifier (1.13.2) | ||||||
|     warden (1.2.9) |     warden (1.2.9) | ||||||
|       rack (>= 2.0.9) |       rack (>= 2.0.9) | ||||||
|     webauthn (3.0.0.alpha1) |     webauthn (3.0.0.alpha1) | ||||||
|  | @ -661,7 +665,7 @@ GEM | ||||||
|       safety_net_attestation (~> 0.4.0) |       safety_net_attestation (~> 0.4.0) | ||||||
|       securecompare (~> 1.0) |       securecompare (~> 1.0) | ||||||
|       tpm-key_attestation (~> 0.9.0) |       tpm-key_attestation (~> 0.9.0) | ||||||
|     webmock (3.11.1) |     webmock (3.11.2) | ||||||
|       addressable (>= 2.3.6) |       addressable (>= 2.3.6) | ||||||
|       crack (>= 0.3.2) |       crack (>= 0.3.2) | ||||||
|       hashdiff (>= 0.4.0, < 2.0.0) |       hashdiff (>= 0.4.0, < 2.0.0) | ||||||
|  | @ -702,9 +706,9 @@ DEPENDENCIES | ||||||
|   capistrano-rails (~> 1.6) |   capistrano-rails (~> 1.6) | ||||||
|   capistrano-rbenv (~> 2.2) |   capistrano-rbenv (~> 2.2) | ||||||
|   capistrano-yarn (~> 2.0) |   capistrano-yarn (~> 2.0) | ||||||
|   capybara (~> 3.34) |   capybara (~> 3.35) | ||||||
|   charlock_holmes (~> 0.7.7) |   charlock_holmes (~> 0.7.7) | ||||||
|   chewy (~> 5.1) |   chewy (~> 5.2) | ||||||
|   cld3 (~> 3.4.1) |   cld3 (~> 3.4.1) | ||||||
|   climate_control (~> 0.2) |   climate_control (~> 0.2) | ||||||
|   color_diff (~> 0.1) |   color_diff (~> 0.1) | ||||||
|  | @ -773,7 +777,7 @@ DEPENDENCIES | ||||||
|   puma (~> 5.1) |   puma (~> 5.1) | ||||||
|   pundit (~> 2.1) |   pundit (~> 2.1) | ||||||
|   rack (~> 2.2.3) |   rack (~> 2.2.3) | ||||||
|   rack-attack (~> 6.3) |   rack-attack (~> 6.4) | ||||||
|   rack-cors (~> 1.1) |   rack-cors (~> 1.1) | ||||||
|   rails (~> 5.2.4.4) |   rails (~> 5.2.4.4) | ||||||
|   rails-controller-testing (~> 1.0) |   rails-controller-testing (~> 1.0) | ||||||
|  | @ -788,7 +792,7 @@ DEPENDENCIES | ||||||
|   rspec-rails (~> 4.0) |   rspec-rails (~> 4.0) | ||||||
|   rspec-sidekiq (~> 3.1) |   rspec-sidekiq (~> 3.1) | ||||||
|   rspec_junit_formatter (~> 0.4) |   rspec_junit_formatter (~> 0.4) | ||||||
|   rubocop (~> 1.7) |   rubocop (~> 1.8) | ||||||
|   rubocop-rails (~> 2.9) |   rubocop-rails (~> 2.9) | ||||||
|   ruby-progressbar (~> 1.11) |   ruby-progressbar (~> 1.11) | ||||||
|   sanitize (~> 5.2) |   sanitize (~> 5.2) | ||||||
|  | @ -806,10 +810,10 @@ DEPENDENCIES | ||||||
|   stoplight (~> 2.2.1) |   stoplight (~> 2.2.1) | ||||||
|   streamio-ffmpeg (~> 3.0) |   streamio-ffmpeg (~> 3.0) | ||||||
|   strong_migrations (~> 0.7) |   strong_migrations (~> 0.7) | ||||||
|   thor (~> 1.0) |   thor (~> 1.1) | ||||||
|   tty-prompt (~> 0.23) |   tty-prompt (~> 0.23) | ||||||
|   twitter-text (~> 1.14) |   twitter-text (~> 1.14) | ||||||
|   tzinfo-data (~> 1.2020) |   tzinfo-data (~> 1.2021) | ||||||
|   webauthn (~> 3.0.0.alpha1) |   webauthn (~> 3.0.0.alpha1) | ||||||
|   webmock (~> 3.11) |   webmock (~> 3.11) | ||||||
|   webpacker (~> 5.2) |   webpacker (~> 5.2) | ||||||
|  |  | ||||||
|  | @ -40,12 +40,13 @@ class Api::V1::NotificationsController < Api::BaseController | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def load_notifications |   def load_notifications | ||||||
|     cache_collection_paginated_by_id( |     notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id( | ||||||
|       browserable_account_notifications, |  | ||||||
|       Notification, |  | ||||||
|       limit_param(DEFAULT_NOTIFICATIONS_LIMIT), |       limit_param(DEFAULT_NOTIFICATIONS_LIMIT), | ||||||
|       params_slice(:max_id, :since_id, :min_id) |       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 |   end | ||||||
| 
 | 
 | ||||||
|   def browserable_account_notifications |   def browserable_account_notifications | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ | ||||||
| exports[`<DisplayName /> renders display name + account name 1`] = ` | exports[`<DisplayName /> renders display name + account name 1`] = ` | ||||||
| <span | <span | ||||||
|   className="display-name" |   className="display-name" | ||||||
|  |   onMouseEnter={[Function]} | ||||||
|  |   onMouseLeave={[Function]} | ||||||
| > | > | ||||||
|   <bdi> |   <bdi> | ||||||
|     <strong |     <strong | ||||||
|  |  | ||||||
|  | @ -11,45 +11,30 @@ export default class DisplayName extends React.PureComponent { | ||||||
|     localDomain: PropTypes.string, |     localDomain: PropTypes.string, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   _updateEmojis () { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     const node = this.node; |     if (autoPlayGif) { | ||||||
| 
 |  | ||||||
|     if (!node || autoPlayGif) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const emojis = node.querySelectorAll('.custom-emoji'); |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
| 
 | 
 | ||||||
|     for (var i = 0; i < emojis.length; i++) { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|       let emoji = emojis[i]; |       let emoji = emojis[i]; | ||||||
|       if (emoji.classList.contains('status-emoji')) { |       emoji.src = emoji.getAttribute('data-original'); | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       emoji.classList.add('status-emoji'); |  | ||||||
| 
 |  | ||||||
|       emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); |  | ||||||
|       emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|     this._updateEmojis(); |     if (autoPlayGif) { | ||||||
|   } |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|   componentDidUpdate () { |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
|     this._updateEmojis(); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   handleEmojiMouseEnter = ({ target }) => { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|     target.src = target.getAttribute('data-original'); |       let emoji = emojis[i]; | ||||||
|   } |       emoji.src = emoji.getAttribute('data-static'); | ||||||
| 
 |     } | ||||||
|   handleEmojiMouseLeave = ({ target }) => { |  | ||||||
|     target.src = target.getAttribute('data-static'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setRef = (c) => { |  | ||||||
|     this.node = c; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|  | @ -81,7 +66,7 @@ export default class DisplayName extends React.PureComponent { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <span className='display-name' ref={this.setRef}> |       <span className='display-name' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> | ||||||
|         {displayName} {suffix} |         {displayName} {suffix} | ||||||
|       </span> |       </span> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -75,35 +75,38 @@ export default class StatusContent extends React.PureComponent { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _updateStatusEmojis () { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     const node = this.node; |     if (autoPlayGif) { | ||||||
| 
 |  | ||||||
|     if (!node || autoPlayGif) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const emojis = node.querySelectorAll('.custom-emoji'); |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
| 
 | 
 | ||||||
|     for (var i = 0; i < emojis.length; i++) { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|       let emoji = emojis[i]; |       let emoji = emojis[i]; | ||||||
|       if (emoji.classList.contains('status-emoji')) { |       emoji.src = emoji.getAttribute('data-original'); | ||||||
|         continue; |     } | ||||||
|       } |   } | ||||||
|       emoji.classList.add('status-emoji'); |  | ||||||
| 
 | 
 | ||||||
|       emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|       emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); |     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 () { |   componentDidMount () { | ||||||
|     this._updateStatusLinks(); |     this._updateStatusLinks(); | ||||||
|     this._updateStatusEmojis(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidUpdate () { |   componentDidUpdate () { | ||||||
|     this._updateStatusLinks(); |     this._updateStatusLinks(); | ||||||
|     this._updateStatusEmojis(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onMentionClick = (mention, e) => { |   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) => { |   handleMouseDown = (e) => { | ||||||
|     this.startXY = [e.clientX, e.clientY]; |     this.startXY = [e.clientX, e.clientY]; | ||||||
|   } |   } | ||||||
|  | @ -219,7 +214,7 @@ export default class StatusContent extends React.PureComponent { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return ( |       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 }}> |           <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> | ||||||
|             <span dangerouslySetInnerHTML={spoilerContent} className='translate' /> |             <span dangerouslySetInnerHTML={spoilerContent} className='translate' /> | ||||||
|             {' '} |             {' '} | ||||||
|  | @ -237,7 +232,7 @@ export default class StatusContent extends React.PureComponent { | ||||||
|       ); |       ); | ||||||
|     } else if (this.props.onClick) { |     } else if (this.props.onClick) { | ||||||
|       const output = [ |       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} /> |           <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> | ||||||
| 
 | 
 | ||||||
|           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} |           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} | ||||||
|  | @ -253,7 +248,7 @@ export default class StatusContent extends React.PureComponent { | ||||||
|       return output; |       return output; | ||||||
|     } else { |     } else { | ||||||
|       return ( |       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} /> |           <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> | ||||||
| 
 | 
 | ||||||
|           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} |           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} | ||||||
|  |  | ||||||
|  | @ -96,45 +96,30 @@ class Header extends ImmutablePureComponent { | ||||||
|     return !location.pathname.match(/\/(followers|following)\/?$/); |     return !location.pathname.match(/\/(followers|following)\/?$/); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _updateEmojis () { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     const node = this.node; |     if (autoPlayGif) { | ||||||
| 
 |  | ||||||
|     if (!node || autoPlayGif) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const emojis = node.querySelectorAll('.custom-emoji'); |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
| 
 | 
 | ||||||
|     for (var i = 0; i < emojis.length; i++) { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|       let emoji = emojis[i]; |       let emoji = emojis[i]; | ||||||
|       if (emoji.classList.contains('status-emoji')) { |       emoji.src = emoji.getAttribute('data-original'); | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       emoji.classList.add('status-emoji'); |  | ||||||
| 
 |  | ||||||
|       emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); |  | ||||||
|       emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|     this._updateEmojis(); |     if (autoPlayGif) { | ||||||
|   } |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|   componentDidUpdate () { |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
|     this._updateEmojis(); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   handleEmojiMouseEnter = ({ target }) => { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|     target.src = target.getAttribute('data-original'); |       let emoji = emojis[i]; | ||||||
|   } |       emoji.src = emoji.getAttribute('data-static'); | ||||||
| 
 |     } | ||||||
|   handleEmojiMouseLeave = ({ target }) => { |  | ||||||
|     target.src = target.getAttribute('data-static'); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setRef = (c) => { |  | ||||||
|     this.node = c; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|  | @ -276,7 +261,7 @@ class Header extends ImmutablePureComponent { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     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__image'> | ||||||
|           <div className='account__header__info'> |           <div className='account__header__info'> | ||||||
|             {!suspended && info} |             {!suspended && info} | ||||||
|  | @ -330,7 +315,7 @@ class Header extends ImmutablePureComponent { | ||||||
|                     <dl key={i}> |                     <dl key={i}> | ||||||
|                       <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' /> |                       <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') }} /> |                         {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> |                       </dd> | ||||||
|                     </dl> |                     </dl> | ||||||
|  |  | ||||||
|  | @ -44,41 +44,30 @@ class Conversation extends ImmutablePureComponent { | ||||||
|     intl: PropTypes.object.isRequired, |     intl: PropTypes.object.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   _updateEmojis () { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     const node = this.namesNode; |     if (autoPlayGif) { | ||||||
| 
 |  | ||||||
|     if (!node || autoPlayGif) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const emojis = node.querySelectorAll('.custom-emoji'); |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
| 
 | 
 | ||||||
|     for (var i = 0; i < emojis.length; i++) { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|       let emoji = emojis[i]; |       let emoji = emojis[i]; | ||||||
|       if (emoji.classList.contains('status-emoji')) { |       emoji.src = emoji.getAttribute('data-original'); | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       emoji.classList.add('status-emoji'); |  | ||||||
| 
 |  | ||||||
|       emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); |  | ||||||
|       emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|     this._updateEmojis(); |     if (autoPlayGif) { | ||||||
|   } |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|   componentDidUpdate () { |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
|     this._updateEmojis(); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   handleEmojiMouseEnter = ({ target }) => { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|     target.src = target.getAttribute('data-original'); |       let emoji = emojis[i]; | ||||||
|   } |       emoji.src = emoji.getAttribute('data-static'); | ||||||
| 
 |     } | ||||||
|   handleEmojiMouseLeave = ({ target }) => { |  | ||||||
|     target.src = target.getAttribute('data-static'); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleClick = () => { |   handleClick = () => { | ||||||
|  | @ -123,10 +112,6 @@ class Conversation extends ImmutablePureComponent { | ||||||
|     this.props.onToggleHidden(this.props.lastStatus); |     this.props.onToggleHidden(this.props.lastStatus); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setNamesRef = (c) => { |  | ||||||
|     this.namesNode = c; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   render () { |   render () { | ||||||
|     const { accounts, lastStatus, unread, scrollKey, intl } = this.props; |     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')} /> |                 {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> | ||||||
|               </div> |               </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> }} /> |                 <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  | @ -102,43 +102,32 @@ class AccountCard extends ImmutablePureComponent { | ||||||
|     onMute: PropTypes.func.isRequired, |     onMute: PropTypes.func.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   _updateEmojis() { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     const node = this.node; |     if (autoPlayGif) { | ||||||
| 
 |  | ||||||
|     if (!node || autoPlayGif) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const emojis = node.querySelectorAll('.custom-emoji'); |     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||||
| 
 | 
 | ||||||
|     for (var i = 0; i < emojis.length; i++) { |     for (var i = 0; i < emojis.length; i++) { | ||||||
|       let emoji = emojis[i]; |       let emoji = emojis[i]; | ||||||
|       if (emoji.classList.contains('status-emoji')) { |       emoji.src = emoji.getAttribute('data-original'); | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       emoji.classList.add('status-emoji'); |  | ||||||
| 
 |  | ||||||
|       emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); |  | ||||||
|       emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount() { |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|     this._updateEmojis(); |     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 = () => { |   handleFollow = () => { | ||||||
|     this.props.onFollow(this.props.account); |     this.props.onFollow(this.props.account); | ||||||
|   }; |   }; | ||||||
|  | @ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent { | ||||||
|     this.props.onMute(this.props.account); |     this.props.onMute(this.props.account); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   setRef = (c) => { |  | ||||||
|     this.node = c; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   render() { |   render() { | ||||||
|     const { account, intl } = this.props; |     const { account, intl } = this.props; | ||||||
| 
 | 
 | ||||||
|  | @ -239,7 +224,7 @@ class AccountCard extends ImmutablePureComponent { | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div className='directory__card__extra' ref={this.setRef}> |         <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> | ||||||
|           <div |           <div | ||||||
|             className='account__header__content translate' |             className='account__header__content translate' | ||||||
|             dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} |             dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} | ||||||
|  |  | ||||||
|  | @ -39,35 +39,10 @@ class Content extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   componentDidMount () { | ||||||
|     this._updateLinks(); |     this._updateLinks(); | ||||||
|     this._updateEmojis(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidUpdate () { |   componentDidUpdate () { | ||||||
|     this._updateLinks(); |     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 () { |   _updateLinks () { | ||||||
|  | @ -132,12 +107,30 @@ class Content extends ImmutablePureComponent { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleEmojiMouseEnter = ({ target }) => { |   handleMouseEnter = ({ currentTarget }) => { | ||||||
|     target.src = target.getAttribute('data-original'); |     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 }) => { |   handleMouseLeave = ({ currentTarget }) => { | ||||||
|     target.src = target.getAttribute('data-static'); |     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 () { |   render () { | ||||||
|  | @ -148,6 +141,8 @@ class Content extends ImmutablePureComponent { | ||||||
|         className='announcements__item__content translate' |         className='announcements__item__content translate' | ||||||
|         ref={this.setRef} |         ref={this.setRef} | ||||||
|         dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }} |         dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }} | ||||||
|  |         onMouseEnter={this.handleMouseEnter} | ||||||
|  |         onMouseLeave={this.handleMouseLeave} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -4,9 +4,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity | ||||||
|   PROCESSING_COOLDOWN = 7.days.seconds |   PROCESSING_COOLDOWN = 7.days.seconds | ||||||
| 
 | 
 | ||||||
|   def perform |   def perform | ||||||
|     return if origin_account.uri != object_uri || processed? |     return if origin_account.uri != object_uri | ||||||
| 
 |     return unless mark_as_processing! | ||||||
|     mark_as_processing! |  | ||||||
| 
 | 
 | ||||||
|     target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri) |     target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri) | ||||||
| 
 | 
 | ||||||
|  | @ -35,12 +34,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity | ||||||
|     value_or_id(@json['target']) |     value_or_id(@json['target']) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def processed? |  | ||||||
|     redis.exists?("move_in_progress:#{@account.id}") |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def mark_as_processing! |   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 |   end | ||||||
| 
 | 
 | ||||||
|   def unmark_as_processing! |   def unmark_as_processing! | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| class AccountMigration < ApplicationRecord | class AccountMigration < ApplicationRecord | ||||||
|  |   include Redisable | ||||||
|  | 
 | ||||||
|   COOLDOWN_PERIOD = 30.days.freeze |   COOLDOWN_PERIOD = 30.days.freeze | ||||||
| 
 | 
 | ||||||
|   belongs_to :account |   belongs_to :account | ||||||
|  | @ -39,7 +41,13 @@ class AccountMigration < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|     return false unless errors.empty? |     return false unless errors.empty? | ||||||
| 
 | 
 | ||||||
|     save |     RedisLock.acquire(lock_options) do |lock| | ||||||
|  |       if lock.acquired? | ||||||
|  |         save | ||||||
|  |       else | ||||||
|  |         raise Mastodon::RaceConditionError | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def cooldown_at |   def cooldown_at | ||||||
|  | @ -75,4 +83,8 @@ class AccountMigration < ApplicationRecord | ||||||
|   def validate_migration_cooldown |   def validate_migration_cooldown | ||||||
|     errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists? |     errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists? | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def lock_options | ||||||
|  |     { redis: redis, key: "account_migration:#{account.id}" } | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -17,7 +17,6 @@ class Notification < ApplicationRecord | ||||||
|   self.inheritance_column = nil |   self.inheritance_column = nil | ||||||
| 
 | 
 | ||||||
|   include Paginable |   include Paginable | ||||||
|   include Cacheable |  | ||||||
| 
 | 
 | ||||||
|   LEGACY_TYPE_CLASS_MAP = { |   LEGACY_TYPE_CLASS_MAP = { | ||||||
|     'Mention'       => :mention, |     'Mention'       => :mention, | ||||||
|  | @ -38,7 +37,13 @@ class Notification < ApplicationRecord | ||||||
|     poll |     poll | ||||||
|   ).freeze |   ).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 :account, optional: true | ||||||
|   belongs_to :from_account, class_name: 'Account', optional: true |   belongs_to :from_account, class_name: 'Account', optional: true | ||||||
|  | @ -65,8 +70,6 @@ class Notification < ApplicationRecord | ||||||
|     end |     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 |   def type | ||||||
|     @type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym |     @type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym | ||||||
|   end |   end | ||||||
|  | @ -87,21 +90,40 @@ class Notification < ApplicationRecord | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   class << self |   class << self | ||||||
|     def cache_ids |     def preload_cache_collection_target_statuses(notifications, &_block) | ||||||
|       select(:id, :updated_at, :activity_type, :activity_id) |       notifications.group_by(&:type).each do |type, grouped_notifications| | ||||||
|     end |         associations = TARGET_STATUS_INCLUDES_BY_TYPE[type] | ||||||
|  |         next unless associations | ||||||
| 
 | 
 | ||||||
|     def reload_stale_associations!(cached_items) |         # Instead of using the usual `includes`, manually preload each type. | ||||||
|       account_ids = (cached_items.map(&:from_account_id) + cached_items.filter_map { |item| item.target_status&.account_id }).uniq |         # 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) | ||||||
|       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 |  | ||||||
|       end |       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 | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -120,7 +120,7 @@ class Status < ApplicationRecord | ||||||
|                    :tags, |                    :tags, | ||||||
|                    :preview_cards, |                    :preview_cards, | ||||||
|                    :preloadable_poll, |                    :preloadable_poll, | ||||||
|                    account: :account_stat, |                    account: [:account_stat, :user], | ||||||
|                    active_mentions: { account: :account_stat }, |                    active_mentions: { account: :account_stat }, | ||||||
|                    reblog: [ |                    reblog: [ | ||||||
|                      :application, |                      :application, | ||||||
|  | @ -130,7 +130,7 @@ class Status < ApplicationRecord | ||||||
|                      :conversation, |                      :conversation, | ||||||
|                      :status_stat, |                      :status_stat, | ||||||
|                      :preloadable_poll, |                      :preloadable_poll, | ||||||
|                      account: :account_stat, |                      account: [:account_stat, :user], | ||||||
|                      active_mentions: { account: :account_stat }, |                      active_mentions: { account: :account_stat }, | ||||||
|                    ], |                    ], | ||||||
|                    thread: { account: :account_stat } |                    thread: { account: :account_stat } | ||||||
|  | @ -354,7 +354,7 @@ class Status < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|       return if account_ids.empty? |       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| |       cached_items.each do |item| | ||||||
|         item.account = accounts[item.account_id] |         item.account = accounts[item.account_id] | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| if String.method_defined?(:blank_as?) | if String.method_defined?(:blank_as?) | ||||||
|   class String |   class String | ||||||
|     alias_method :blank?, :blank_as? |     alias blank? blank_as? | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| # Use this setup block to configure all options available in SimpleForm. | # Use this setup block to configure all options available in SimpleForm. | ||||||
| 
 | 
 | ||||||
| module AppendComponent | module AppendComponent | ||||||
|   def append(wrapper_options = nil) |   def append(_wrapper_options = nil) | ||||||
|     @append ||= begin |     @append ||= begin | ||||||
|       options[:append].to_s.html_safe if options[:append].present? |       options[:append].to_s.html_safe if options[:append].present? | ||||||
|     end |     end | ||||||
|  | @ -9,7 +9,7 @@ module AppendComponent | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| module RecommendedComponent | module RecommendedComponent | ||||||
|   def recommended(wrapper_options = nil) |   def recommended(_wrapper_options = nil) | ||||||
|     return unless options[:recommended] |     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')]) } |     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 |     nil | ||||||
|  |  | ||||||
|  | @ -75,7 +75,7 @@ module Twitter | ||||||
|     # XMPP or magnet URIs an empty array will be returned. |     # XMPP or magnet URIs an empty array will be returned. | ||||||
|     # |     # | ||||||
|     # If a block is given then it will be called for each XMPP URI. |     # 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(":") |       return [] unless text && text.index(":") | ||||||
|       urls = [] |       urls = [] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -83,7 +83,10 @@ en: | ||||||
|         invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. |         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_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_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_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_scope: The requested scope is invalid, unknown, or malformed. | ||||||
|         invalid_token: |         invalid_token: | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| # -*- encoding: utf-8 -*- |  | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| # This file generated automatically from http://w3id.org/identity/v1 | # This file generated automatically from http://w3id.org/identity/v1 | ||||||
| require 'json/ld' | require 'json/ld' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| # -*- encoding: utf-8 -*- |  | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| # This file generated automatically from http://w3id.org/security/v1 | # This file generated automatically from http://w3id.org/security/v1 | ||||||
| require 'json/ld' | require 'json/ld' | ||||||
|  |  | ||||||
|  | @ -402,7 +402,7 @@ module Mastodon | ||||||
|         exit(1) |         exit(1) | ||||||
|       end |       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) |         UnfollowService.new.call(account, target_account) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -113,7 +113,7 @@ module Mastodon | ||||||
|         result = entry.destroy |         result = entry.destroy | ||||||
| 
 | 
 | ||||||
|         if result |         if result | ||||||
|           processed += 1 + children_count |           processed += children_count + 1 | ||||||
|         else |         else | ||||||
|           say("#{domain} could not be unblocked.", :red) |           say("#{domain} could not be unblocked.", :red) | ||||||
|           failed += 1 |           failed += 1 | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ module Paperclip | ||||||
|       # If we don't have enough colors for accent and foreground, generate |       # If we don't have enough colors for accent and foreground, generate | ||||||
|       # new ones by manipulating the background color |       # new ones by manipulating the background color | ||||||
|       (2 - foreground_colors.size).times do |i| |       (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 |       end | ||||||
| 
 | 
 | ||||||
|       # We want the color with the highest contrast to background to be the foreground one, |       # 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 |         g = l.to_f | ||||||
|         b = l.to_f # achromatic |         b = l.to_f # achromatic | ||||||
|       else |       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 |         p = 2 * l - q | ||||||
|         r = hue_to_rgb(p, q, h + 1 / 3.0) |         r = hue_to_rgb(p, q, h + 1 / 3.0) | ||||||
|         g = hue_to_rgb(p, q, h) |         g = hue_to_rgb(p, q, h) | ||||||
|  |  | ||||||
|  | @ -114,7 +114,7 @@ | ||||||
|     "lodash": "^4.17.19", |     "lodash": "^4.17.19", | ||||||
|     "mark-loader": "^0.1.6", |     "mark-loader": "^0.1.6", | ||||||
|     "marky": "^1.2.1", |     "marky": "^1.2.1", | ||||||
|     "mini-css-extract-plugin": "^1.3.4", |     "mini-css-extract-plugin": "^1.3.5", | ||||||
|     "mkdirp": "^1.0.4", |     "mkdirp": "^1.0.4", | ||||||
|     "npmlog": "^4.1.2", |     "npmlog": "^4.1.2", | ||||||
|     "object-assign": "^4.1.1", |     "object-assign": "^4.1.1", | ||||||
|  | @ -168,7 +168,7 @@ | ||||||
|     "uuid": "^8.3.1", |     "uuid": "^8.3.1", | ||||||
|     "webpack": "^4.46.0", |     "webpack": "^4.46.0", | ||||||
|     "webpack-assets-manifest": "^4.0.1", |     "webpack-assets-manifest": "^4.0.1", | ||||||
|     "webpack-bundle-analyzer": "^4.3.0", |     "webpack-bundle-analyzer": "^4.4.0", | ||||||
|     "webpack-cli": "^3.3.12", |     "webpack-cli": "^3.3.12", | ||||||
|     "webpack-merge": "^5.7.3", |     "webpack-merge": "^5.7.3", | ||||||
|     "wicg-inert": "^3.1.0" |     "wicg-inert": "^3.1.0" | ||||||
|  |  | ||||||
|  | @ -353,10 +353,6 @@ describe ApplicationController, type: :controller do | ||||||
|       expect(C.new.cache_collection(raw, Object)).to eq raw |       expect(C.new.cache_collection(raw, Object)).to eq raw | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'Notification' do |  | ||||||
|       include_examples 'cacheable', :notification, Notification |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'Status' do |     context 'Status' do | ||||||
|       include_examples 'cacheable', :status, Status |       include_examples 'cacheable', :status, Status | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ describe Settings::MigrationsController do | ||||||
|       it_behaves_like 'authenticate user' |       it_behaves_like 'authenticate user' | ||||||
|     end |     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' } } } |       subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } } | ||||||
| 
 | 
 | ||||||
|       let(:user) { Fabricate(:user, password: '12345678') } |       let(:user) { Fabricate(:user, password: '12345678') } | ||||||
|  | @ -67,12 +67,45 @@ describe Settings::MigrationsController do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when acct is a current account' do |       context 'when acct is the current account' do | ||||||
|         let(:acct) { user.account } |         let(:acct) { user.account } | ||||||
| 
 | 
 | ||||||
|         it 'renders show' do |         it 'renders show' do | ||||||
|           is_expected.to render_template :show |           is_expected.to render_template :show | ||||||
|         end |         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 |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,23 +1,11 @@ | ||||||
| require 'rails_helper' | require 'rails_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe ActivityPub::Activity::Move do | RSpec.describe ActivityPub::Activity::Move do | ||||||
|   let(:follower)    { Fabricate(:account) } |   let(:follower)         { Fabricate(:account) } | ||||||
|   let(:old_account) { 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) } |   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] } | ||||||
|   before do |   let(:returned_account) { new_account } | ||||||
|     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(:json) do |   let(:json) do | ||||||
|     { |     { | ||||||
|  | @ -30,6 +18,17 @@ RSpec.describe ActivityPub::Activity::Move do | ||||||
|     }.with_indifferent_access |     }.with_indifferent_access | ||||||
|   end |   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 |   describe '#perform' do | ||||||
|     subject { described_class.new(json, old_account) } |     subject { described_class.new(json, old_account) } | ||||||
| 
 | 
 | ||||||
|  | @ -37,16 +36,70 @@ RSpec.describe ActivityPub::Activity::Move do | ||||||
|       subject.perform |       subject.perform | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'sets moved account on old account' do |     context 'when all conditions are met' do | ||||||
|       expect(old_account.reload.moved_to_account_id).to eq new_account.id |       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 |     end | ||||||
| 
 | 
 | ||||||
|     it 'makes followers unfollow old account' do |     context "when the new account can't be resolved" do | ||||||
|       expect(follower.following?(old_account)).to be false |       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 |     end | ||||||
| 
 | 
 | ||||||
|     it 'makes followers follow-request the new account' do |     context 'when the new account does not references the old account' do | ||||||
|       expect(follower.requested?(new_account)).to be true |       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 |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -81,6 +81,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do | ||||||
|     options_hash   = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT))) |     options_hash   = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT))) | ||||||
|     document_hash  = Digest::SHA256.hexdigest(canonicalize(document)) |     document_hash  = Digest::SHA256.hexdigest(canonicalize(document)) | ||||||
|     to_be_verified = options_hash + document_hash |     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 | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -56,47 +56,114 @@ RSpec.describe Notification, type: :model do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.reload_stale_associations!' do |   describe '.preload_cache_collection_target_statuses' do | ||||||
|     context 'account_ids are empty' do |     subject do | ||||||
|       let(:cached_items) { [] } |       described_class.preload_cache_collection_target_statuses(notifications) do |target_statuses| | ||||||
| 
 |         # preload account for testing instead of using cache_collection | ||||||
|       subject { described_class.reload_stale_associations!(cached_items) } |         Status.preload(:account).where(id: target_statuses.map(&:id)) | ||||||
| 
 |  | ||||||
|       it 'returns nil' do |  | ||||||
|         is_expected.to be nil |  | ||||||
|       end |       end | ||||||
|     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 |       before do | ||||||
|         allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1) |         notifications.each(&:reload) | ||||||
|         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) |  | ||||||
|       end |       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, type: :mention, activity: mention), | ||||||
|           Fabricate(:notification, activity: Fabricate(:follow)), |           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 |       end | ||||||
| 
 | 
 | ||||||
|       let(:stale_account1) { cached_items[0].from_account } |       it 'preloads target status' do | ||||||
|       let(:stale_account2) { cached_items[1].from_account } |         # 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) } |         # status | ||||||
|       let(:account2) { Fabricate(:account) } |         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 |         # follow: nothing | ||||||
|         expect(cached_items[0].from_account).to be stale_account1 |         expect(subject[3].type).to eq :follow | ||||||
|         expect(cached_items[1].from_account).to be stale_account2 |         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 |         # favourite | ||||||
|         expect(cached_items[1].from_account).to be account2 |         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 |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -7115,10 +7115,10 @@ min-indent@^1.0.0: | ||||||
|   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" |   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" | ||||||
|   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== |   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== | ||||||
| 
 | 
 | ||||||
| mini-css-extract-plugin@^1.3.4: | mini-css-extract-plugin@^1.3.5: | ||||||
|   version "1.3.4" |   version "1.3.5" | ||||||
|   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz#706e69632cdcdb8b15bf8e638442a0dba304a9c8" |   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz#252166e78879c106e0130f229d44e0cbdfcebed3" | ||||||
|   integrity sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg== |   integrity sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ== | ||||||
|   dependencies: |   dependencies: | ||||||
|     loader-utils "^2.0.0" |     loader-utils "^2.0.0" | ||||||
|     schema-utils "^3.0.0" |     schema-utils "^3.0.0" | ||||||
|  | @ -11079,10 +11079,10 @@ webpack-assets-manifest@^4.0.1: | ||||||
|     tapable "^1.0" |     tapable "^1.0" | ||||||
|     webpack-sources "^1.0" |     webpack-sources "^1.0" | ||||||
| 
 | 
 | ||||||
| webpack-bundle-analyzer@^4.3.0: | webpack-bundle-analyzer@^4.4.0: | ||||||
|   version "4.3.0" |   version "4.4.0" | ||||||
|   resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz#2f3c0ca9041d5ee47fa418693cf56b4a518b578b" |   resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7" | ||||||
|   integrity sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA== |   integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g== | ||||||
|   dependencies: |   dependencies: | ||||||
|     acorn "^8.0.4" |     acorn "^8.0.4" | ||||||
|     acorn-walk "^8.0.0" |     acorn-walk "^8.0.0" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue