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 | ||||
|   rubocop: | ||||
|     enabled: true | ||||
|     channel: rubocop-1-70 | ||||
|     channel: rubocop-1-8-1 | ||||
|   sass-lint: | ||||
|     enabled: true | ||||
| 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"] | ||||
| 
 | ||||
| # 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 \ | ||||
|  |  | |||
							
								
								
									
										12
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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' | ||||
|  | @ -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' | ||||
|  | @ -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' | ||||
|  | @ -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' | ||||
|  | @ -119,7 +119,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' | ||||
|  | @ -140,7 +140,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 | ||||
|  |  | |||
							
								
								
									
										64
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								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) | ||||
|  | @ -131,20 +131,20 @@ 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 | ||||
|     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,10 +212,13 @@ 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) | ||||
|     fastimage (2.2.2) | ||||
|     ffi (1.10.0) | ||||
|     ffi-compiler (1.0.1) | ||||
|       ffi (>= 1.0.0) | ||||
|  | @ -369,7 +372,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.2) | ||||
|     omniauth (1.9.1) | ||||
|       hashie (>= 3.4.6) | ||||
|       rack (>= 1.6.2, < 3) | ||||
|  | @ -436,7 +439,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) | ||||
|  | @ -504,7 +507,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) | ||||
|  | @ -539,16 +542,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) | ||||
|  | @ -557,6 +560,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) | ||||
|  | @ -570,7 +574,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) | ||||
|  | @ -620,7 +624,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 | ||||
|  | @ -642,13 +646,13 @@ 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 | ||||
|     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) | ||||
|  | @ -661,7 +665,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) | ||||
|  | @ -702,9 +706,9 @@ 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.1) | ||||
|   chewy (~> 5.2) | ||||
|   cld3 (~> 3.4.1) | ||||
|   climate_control (~> 0.2) | ||||
|   color_diff (~> 0.1) | ||||
|  | @ -773,7 +777,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) | ||||
|  | @ -788,7 +792,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) | ||||
|  | @ -806,10 +810,10 @@ 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.2020) | ||||
|   tzinfo-data (~> 1.2021) | ||||
|   webauthn (~> 3.0.0.alpha1) | ||||
|   webmock (~> 3.11) | ||||
|   webpacker (~> 5.2) | ||||
|  |  | |||
|  | @ -40,12 +40,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 | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ | |||
| exports[`<DisplayName /> renders display name + account name 1`] = ` | ||||
| <span | ||||
|   className="display-name" | ||||
|   onMouseEnter={[Function]} | ||||
|   onMouseLeave={[Function]} | ||||
| > | ||||
|   <bdi> | ||||
|     <strong | ||||
|  |  | |||
|  | @ -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'); | ||||
|     for (var i = 0; i < emojis.length; i++) { | ||||
|       let emoji = emojis[i]; | ||||
|       emoji.src = emoji.getAttribute('data-static'); | ||||
|     } | ||||
| 
 | ||||
|   handleEmojiMouseLeave = ({ target }) => { | ||||
|     target.src = target.getAttribute('data-static'); | ||||
|   } | ||||
| 
 | ||||
|   setRef = (c) => { | ||||
|     this.node = c; | ||||
|   } | ||||
| 
 | ||||
|   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> | ||||
|     ); | ||||
|  |  | |||
|  | @ -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.src = emoji.getAttribute('data-original'); | ||||
|     } | ||||
|   } | ||||
|       emoji.classList.add('status-emoji'); | ||||
| 
 | ||||
|       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')} />} | ||||
|  |  | |||
|  | @ -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'); | ||||
|     for (var i = 0; i < emojis.length; i++) { | ||||
|       let emoji = emojis[i]; | ||||
|       emoji.src = emoji.getAttribute('data-static'); | ||||
|     } | ||||
| 
 | ||||
|   handleEmojiMouseLeave = ({ target }) => { | ||||
|     target.src = target.getAttribute('data-static'); | ||||
|   } | ||||
| 
 | ||||
|   setRef = (c) => { | ||||
|     this.node = c; | ||||
|   } | ||||
| 
 | ||||
|   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} | ||||
|  | @ -330,7 +315,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> | ||||
|  |  | |||
|  | @ -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'); | ||||
|     for (var i = 0; i < emojis.length; i++) { | ||||
|       let emoji = emojis[i]; | ||||
|       emoji.src = emoji.getAttribute('data-static'); | ||||
|     } | ||||
| 
 | ||||
|   handleEmojiMouseLeave = ({ target }) => { | ||||
|     target.src = target.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> | ||||
|  |  | |||
|  | @ -102,42 +102,31 @@ 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; | ||||
|     } | ||||
| 
 | ||||
|   componentDidUpdate() { | ||||
|     this._updateEmojis(); | ||||
|     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||
| 
 | ||||
|     for (var i = 0; i < emojis.length; i++) { | ||||
|       let emoji = emojis[i]; | ||||
|       emoji.src = emoji.getAttribute('data-static'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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') }} | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
| 
 | ||||
|   handleEmojiMouseLeave = ({ target }) => { | ||||
|     target.src = target.getAttribute('data-static'); | ||||
|     const emojis = currentTarget.querySelectorAll('.custom-emoji'); | ||||
| 
 | ||||
|     for (var i = 0; i < emojis.length; i++) { | ||||
|       let emoji = emojis[i]; | ||||
|       emoji.src = emoji.getAttribute('data-original'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -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! | ||||
|  |  | |||
|  | @ -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? | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -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,22 +90,41 @@ class Notification < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   class << self | ||||
|     def cache_ids | ||||
|       select(:id, :updated_at, :activity_type, :activity_id) | ||||
|     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 | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|     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 | ||||
|       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) | ||||
| 
 | ||||
|       return if account_ids.empty? | ||||
|       notifications.each do |notification| | ||||
|         next if notification.target_status.nil? | ||||
| 
 | ||||
|       accounts = Account.where(id: account_ids).includes(:account_stat).index_by(&:id) | ||||
|         cached_status = cached_statuses_by_id[notification.target_status.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 | ||||
|         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 | ||||
| 
 | ||||
|   after_initialize :set_from_account | ||||
|  |  | |||
|  | @ -120,7 +120,7 @@ class Status < ApplicationRecord | |||
|                    :tags, | ||||
|                    :preview_cards, | ||||
|                    :preloadable_poll, | ||||
|                    account: :account_stat, | ||||
|                    account: [:account_stat, :user], | ||||
|                    active_mentions: { account: :account_stat }, | ||||
|                    reblog: [ | ||||
|                      :application, | ||||
|  | @ -130,7 +130,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 } | ||||
|  | @ -354,7 +354,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] | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| if String.method_defined?(:blank_as?) | ||||
|   class String | ||||
|     alias_method :blank?, :blank_as? | ||||
|     alias blank? blank_as? | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 = [] | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| # -*- encoding: utf-8 -*- | ||||
| # frozen_string_literal: true | ||||
| # This file generated automatically from http://w3id.org/identity/v1 | ||||
| require 'json/ld' | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| # -*- encoding: utf-8 -*- | ||||
| # frozen_string_literal: true | ||||
| # This file generated automatically from http://w3id.org/security/v1 | ||||
| require 'json/ld' | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -114,7 +114,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", | ||||
|  | @ -168,7 +168,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" | ||||
|  |  | |||
|  | @ -353,10 +353,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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -2,22 +2,10 @@ 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(: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,6 +36,7 @@ RSpec.describe ActivityPub::Activity::Move do | |||
|       subject.perform | ||||
|     end | ||||
| 
 | ||||
|     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 | ||||
|  | @ -49,4 +49,57 @@ RSpec.describe ActivityPub::Activity::Move do | |||
|         expect(follower.requested?(new_account)).to be true | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										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" | ||||
|   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" | ||||
|  | @ -11079,10 +11079,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" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue