diff options
39 files changed, 565 insertions, 495 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 2d1de877c..62dc2334c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,7 +30,7 @@ plugins: channel: eslint-7 rubocop: enabled: true - channel: rubocop-1-70 + channel: rubocop-1-8-1 sass-lint: enabled: true exclude_patterns: diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..73d38901c --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,18 @@ +version = 1 + +test_patterns = ["/app/javascript/mastodon/**/__tests__/**"] + +[[analyzers]] +name = "ruby" +enabled = true + +[[analyzers]] +name = "javascript" +enabled = true + + [analyzers.meta] + environment = [ + "nodejs", + "browser", + "jest" + ] diff --git a/Dockerfile b/Dockerfile index 136b2a35a..84e6435f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM ubuntu:20.04 as build-dep SHELL ["/usr/bin/bash", "-c"] # Install Node v12 (LTS) -ENV NODE_VER="12.20.0" +ENV NODE_VER="12.20.1" RUN ARCH= && \ dpkgArch="$(dpkg --print-architecture)" && \ case "${dpkgArch##*-}" in \ diff --git a/Gemfile b/Gemfile index cfc392c3c..639a47729 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'pkg-config', '~> 1.4' gem 'puma', '~> 5.1' gem 'rails', '~> 5.2.4.4' gem 'sprockets', '~> 3.7.2' -gem 'thor', '~> 1.0' +gem 'thor', '~> 1.1' gem 'rack', '~> 2.2.3' gem 'hamlit-rails', '~> 0.2' @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index bea9f7878..7d1a467da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,7 +109,7 @@ GEM brakeman (4.10.1) browser (4.2.0) builder (3.2.4) - bullet (6.1.2) + bullet (6.1.3) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundler-audit (0.7.0.1) @@ -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) diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index fda348265..eefd28d45 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -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 diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index 44662a8b8..ad978a2c6 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -15,45 +15,30 @@ export default class DisplayName extends React.PureComponent { handleClick: PropTypes.func, }; - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } - - componentDidUpdate () { - this._updateEmojis(); - } - - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render() { @@ -101,7 +86,7 @@ export default class DisplayName extends React.PureComponent { } return ( - <span className={computedClass} ref={this.setRef}> + <span className={computedClass} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> {displayName} {inline ? ' ' : null} {suffix} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 61a28e9a7..34ff97305 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -154,35 +154,38 @@ export default class StatusContent extends React.PureComponent { } } - _updateStatusEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); + emoji.src = emoji.getAttribute('data-original'); + } + } + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); } } componentDidMount () { this._updateStatusLinks(); - this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); - this._updateStatusEmojis(); if (this.props.onUpdate) this.props.onUpdate(); } @@ -206,14 +209,6 @@ export default class StatusContent extends React.PureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } - handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -253,10 +248,6 @@ export default class StatusContent extends React.PureComponent { } } - setRef = (c) => { - this.node = c; - } - setContentsRef = (c) => { this.contentsNode = c; } @@ -323,7 +314,7 @@ export default class StatusContent extends React.PureComponent { } return ( - <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}> + <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} > @@ -343,6 +334,8 @@ export default class StatusContent extends React.PureComponent { tabIndex={!hidden ? 0 : null} dangerouslySetInnerHTML={content} className='status__content__text translate' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} /> {media} </div> @@ -356,7 +349,6 @@ export default class StatusContent extends React.PureComponent { onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} tabIndex='0' - ref={this.setRef} > <div ref={this.setContentsRef} @@ -364,6 +356,8 @@ export default class StatusContent extends React.PureComponent { dangerouslySetInnerHTML={content} className='status__content__text translate' tabIndex='0' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} /> {media} </div> @@ -373,9 +367,16 @@ export default class StatusContent extends React.PureComponent { <div className='status__content' tabIndex='0' - ref={this.setRef} > - <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text translate' dangerouslySetInnerHTML={content} tabIndex='0' /> + <div + ref={this.setContentsRef} + key={`contents-${tagLinks}`} + className='status__content__text translate' + dangerouslySetInnerHTML={content} + tabIndex='0' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + /> {media} </div> ); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 6a572862d..c18520c00 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -88,45 +88,30 @@ class Header extends ImmutablePureComponent { window.open(profileLink, '_blank'); } - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } - - componentDidUpdate () { - this._updateEmojis(); - } - - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -279,7 +264,7 @@ class Header extends ImmutablePureComponent { } return ( - <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}> + <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='account__header__image'> <div className='account__header__info'> {info} diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js index 0af8b348f..98b48cd90 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js @@ -67,41 +67,30 @@ class Conversation extends ImmutablePureComponent { } } - _updateEmojis () { - const node = this.namesNode; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } - - componentDidUpdate () { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } handleClick = () => { @@ -152,10 +141,6 @@ class Conversation extends ImmutablePureComponent { this.setState({ isExpanded: value }); } - setNamesRef = (c) => { - this.namesNode = c; - } - render () { const { accounts, lastStatus, unread, scrollKey, intl } = this.props; const { isExpanded } = this.state; @@ -206,7 +191,7 @@ class Conversation extends ImmutablePureComponent { {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> </div> - <div className='conversation__content__names' ref={this.setNamesRef}> + <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> </div> </div> diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 5f952b382..9fe84c10b 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -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(); - } - - componentDidUpdate() { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - }; + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - }; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + } handleFollow = () => { this.props.onFollow(this.props.account); @@ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent { this.props.onMute(this.props.account); }; - setRef = (c) => { - this.node = c; - }; - render() { const { account, intl } = this.props; @@ -239,7 +224,7 @@ class AccountCard extends ImmutablePureComponent { </div> </div> - <div className='directory__card__extra' ref={this.setRef}> + <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='account__header__content translate' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 1ab5d9ba6..4eebe83ef 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -39,35 +39,10 @@ class Content extends ImmutablePureComponent { componentDidMount () { this._updateLinks(); - this._updateEmojis(); } componentDidUpdate () { this._updateLinks(); - this._updateEmojis(); - } - - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { - return; - } - - const emojis = node.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - - if (emoji.classList.contains('status-emoji')) { - continue; - } - - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); - } } _updateLinks () { @@ -132,12 +107,30 @@ class Content extends ImmutablePureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -148,6 +141,8 @@ class Content extends ImmutablePureComponent { className='announcements__item__content translate' ref={this.setRef} dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} /> ); } diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap index 29fdc2412..0f27473af 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap @@ -3,6 +3,8 @@ exports[`<DisplayName /> renders display name + account name 1`] = ` <span className="display-name" + onMouseEnter={[Function]} + onMouseLeave={[Function]} > <bdi> <strong diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index 70ef82789..7ccfbd0cd 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -11,45 +11,30 @@ export default class DisplayName extends React.PureComponent { localDomain: PropTypes.string, }; - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } - - componentDidUpdate () { - this._updateEmojis(); - } - - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -81,7 +66,7 @@ export default class DisplayName extends React.PureComponent { } return ( - <span className='display-name' ref={this.setRef}> + <span className='display-name' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> {displayName} {suffix} </span> ); diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 190ced1a8..35bd50514 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -75,35 +75,38 @@ export default class StatusContent extends React.PureComponent { } } - _updateStatusEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); + emoji.src = emoji.getAttribute('data-original'); + } + } + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); } } componentDidMount () { this._updateStatusLinks(); - this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); - this._updateStatusEmojis(); } onMentionClick = (mention, e) => { @@ -122,14 +125,6 @@ export default class StatusContent extends React.PureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } - handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -219,7 +214,7 @@ export default class StatusContent extends React.PureComponent { } return ( - <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> <span dangerouslySetInnerHTML={spoilerContent} className='translate' /> {' '} @@ -237,7 +232,7 @@ export default class StatusContent extends React.PureComponent { ); } else if (this.props.onClick) { const output = [ - <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> + <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} @@ -253,7 +248,7 @@ export default class StatusContent extends React.PureComponent { return output; } else { return ( - <div className={classNames} ref={this.setRef} tabIndex='0'> + <div className={classNames} ref={this.setRef} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 4647b98b2..8e49486bd 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -96,45 +96,30 @@ class Header extends ImmutablePureComponent { return !location.pathname.match(/\/(followers|following)\/?$/); } - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } - - componentDidUpdate () { - this._updateEmojis(); - } - - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -276,7 +261,7 @@ class Header extends ImmutablePureComponent { } return ( - <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}> + <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='account__header__image'> <div className='account__header__info'> {!suspended && info} @@ -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> diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index 6ecc27fac..43e1d77b9 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -44,41 +44,30 @@ class Conversation extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - _updateEmojis () { - const node = this.namesNode; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - componentDidUpdate () { - this._updateEmojis(); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } handleClick = () => { @@ -123,10 +112,6 @@ class Conversation extends ImmutablePureComponent { this.props.onToggleHidden(this.props.lastStatus); } - setNamesRef = (c) => { - this.namesNode = c; - } - render () { const { accounts, lastStatus, unread, scrollKey, intl } = this.props; @@ -171,7 +156,7 @@ class Conversation extends ImmutablePureComponent { {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> </div> - <div className='conversation__content__names' ref={this.setNamesRef}> + <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> </div> </div> diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index e37733828..8f0e8db4b 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -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(); - } - - componentDidUpdate() { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - }; + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - }; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + } handleFollow = () => { this.props.onFollow(this.props.account); @@ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent { this.props.onMute(this.props.account); }; - setRef = (c) => { - this.node = c; - }; - render() { const { account, intl } = this.props; @@ -239,7 +224,7 @@ class AccountCard extends ImmutablePureComponent { </div> </div> - <div className='directory__card__extra' ref={this.setRef}> + <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='account__header__content translate' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 4534f7121..ff1566e05 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -39,35 +39,10 @@ class Content extends ImmutablePureComponent { componentDidMount () { this._updateLinks(); - this._updateEmojis(); } componentDidUpdate () { this._updateLinks(); - this._updateEmojis(); - } - - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { - return; - } - - const emojis = node.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - - if (emoji.classList.contains('status-emoji')) { - continue; - } - - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); - } } _updateLinks () { @@ -132,12 +107,30 @@ class Content extends ImmutablePureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -148,6 +141,8 @@ class Content extends ImmutablePureComponent { className='announcements__item__content translate' ref={this.setRef} dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} /> ); } diff --git a/app/lib/activitypub/activity/move.rb b/app/lib/activitypub/activity/move.rb index 7e073f64d..8576ceccd 100644 --- a/app/lib/activitypub/activity/move.rb +++ b/app/lib/activitypub/activity/move.rb @@ -4,9 +4,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity PROCESSING_COOLDOWN = 7.days.seconds def perform - return if origin_account.uri != object_uri || processed? - - mark_as_processing! + return if origin_account.uri != object_uri + return unless mark_as_processing! target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri) @@ -35,12 +34,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity value_or_id(@json['target']) end - def processed? - redis.exists?("move_in_progress:#{@account.id}") - end - def mark_as_processing! - redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true) + redis.set("move_in_progress:#{@account.id}", true, nx: true, ex: PROCESSING_COOLDOWN) end def unmark_as_processing! diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index 4fae98ed7..ded32c9c6 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -14,6 +14,8 @@ # class AccountMigration < ApplicationRecord + include Redisable + COOLDOWN_PERIOD = 30.days.freeze belongs_to :account @@ -39,7 +41,13 @@ class AccountMigration < ApplicationRecord return false unless errors.empty? - save + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + save + else + raise Mastodon::RaceConditionError + end + end end def cooldown_at @@ -75,4 +83,8 @@ class AccountMigration < ApplicationRecord def validate_migration_cooldown errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists? end + + def lock_options + { redis: redis, key: "account_migration:#{account.id}" } + end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 5e9ea62a0..98a6a618f 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -17,7 +17,6 @@ class Notification < ApplicationRecord self.inheritance_column = nil include Paginable - include Cacheable LEGACY_TYPE_CLASS_MAP = { 'Mention' => :mention, @@ -38,7 +37,13 @@ class Notification < ApplicationRecord poll ).freeze - STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze + TARGET_STATUS_INCLUDES_BY_TYPE = { + status: :status, + reblog: [status: :reblog], + mention: [mention: :status], + favourite: [favourite: :status], + poll: [poll: :status], + }.freeze belongs_to :account, optional: true belongs_to :from_account, class_name: 'Account', optional: true @@ -65,8 +70,6 @@ class Notification < ApplicationRecord end } - cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES] - def type @type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym end @@ -87,21 +90,40 @@ class Notification < ApplicationRecord end class << self - def cache_ids - select(:id, :updated_at, :activity_type, :activity_id) - end - - def reload_stale_associations!(cached_items) - account_ids = (cached_items.map(&:from_account_id) + cached_items.filter_map { |item| item.target_status&.account_id }).uniq - - return if account_ids.empty? - - accounts = Account.where(id: account_ids).includes(:account_stat).index_by(&:id) + 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 - 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 + unique_target_statuses = notifications.map(&:target_status).compact.uniq + # Call cache_collection in block + cached_statuses_by_id = yield(unique_target_statuses).index_by(&:id) + + notifications.each do |notification| + next if notification.target_status.nil? + + cached_status = cached_statuses_by_id[notification.target_status.id] + + case notification.type + when :status + notification.status = cached_status + when :reblog + notification.status.reblog = cached_status + when :favourite + notification.favourite.status = cached_status + when :mention + notification.mention.status = cached_status + when :poll + notification.poll.status = cached_status + end end + + notifications end end diff --git a/app/models/status.rb b/app/models/status.rb index 836d363ef..7bedbef07 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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] diff --git a/config/initializers/fast_blank.rb b/config/initializers/fast_blank.rb index 174ea7664..f0b7cac78 100644 --- a/config/initializers/fast_blank.rb +++ b/config/initializers/fast_blank.rb @@ -1,5 +1,5 @@ if String.method_defined?(:blank_as?) class String - alias_method :blank?, :blank_as? + alias blank? blank_as? end end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 3dc48ef08..3a2097d2f 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -1,7 +1,7 @@ # Use this setup block to configure all options available in SimpleForm. module AppendComponent - def append(wrapper_options = nil) + def append(_wrapper_options = nil) @append ||= begin options[:append].to_s.html_safe if options[:append].present? end @@ -9,7 +9,7 @@ module AppendComponent end module RecommendedComponent - def recommended(wrapper_options = nil) + def recommended(_wrapper_options = nil) return unless options[:recommended] options[:label_text] = ->(raw_label_text, _required_label_text, _label_present) { safe_join([raw_label_text, ' ', content_tag(:span, I18n.t('simple_form.recommended'), class: 'recommended')]) } nil diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index 7f99a0005..aca85dd43 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -75,7 +75,7 @@ module Twitter # XMPP or magnet URIs an empty array will be returned. # # If a block is given then it will be called for each XMPP URI. - def extract_extra_uris_with_indices(text, options = {}) # :yields: uri, start, end + def extract_extra_uris_with_indices(text, _options = {}) # :yields: uri, start, end return [] unless text && text.index(":") urls = [] diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 2be2ef036..ec322f071 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -83,7 +83,10 @@ en: invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. invalid_redirect_uri: The redirect uri included is not valid. - invalid_request: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. + invalid_request: + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: Request need to be authorized. Required parameter for authorizing request is missing or invalid. + unknown: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found invalid_scope: The requested scope is invalid, unknown, or malformed. invalid_token: diff --git a/lib/json_ld/identity.rb b/lib/json_ld/identity.rb index 4fb3f8e9d..f41899150 100644 --- a/lib/json_ld/identity.rb +++ b/lib/json_ld/identity.rb @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # frozen_string_literal: true # This file generated automatically from http://w3id.org/identity/v1 require 'json/ld' diff --git a/lib/json_ld/security.rb b/lib/json_ld/security.rb index a6fbce95f..ef5391340 100644 --- a/lib/json_ld/security.rb +++ b/lib/json_ld/security.rb @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # frozen_string_literal: true # This file generated automatically from http://w3id.org/security/v1 require 'json/ld' diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 653bfca30..74162256f 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -402,7 +402,7 @@ module Mastodon exit(1) end - parallelize_with_progress(target_account.followers.local) do |account| + processed, = parallelize_with_progress(target_account.followers.local) do |account| UnfollowService.new.call(account, target_account) end diff --git a/lib/mastodon/email_domain_blocks_cli.rb b/lib/mastodon/email_domain_blocks_cli.rb index 55a637d68..f79df302a 100644 --- a/lib/mastodon/email_domain_blocks_cli.rb +++ b/lib/mastodon/email_domain_blocks_cli.rb @@ -113,7 +113,7 @@ module Mastodon result = entry.destroy if result - processed += 1 + children_count + processed += children_count + 1 else say("#{domain} could not be unblocked.", :red) failed += 1 diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index a70a3d21f..d3b8e1022 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -55,7 +55,7 @@ module Paperclip # If we don't have enough colors for accent and foreground, generate # new ones by manipulating the background color (2 - foreground_colors.size).times do |i| - foreground_colors << lighten_or_darken(background_color, 35 + (15 * i)) + foreground_colors << lighten_or_darken(background_color, 35 + (i * 15)) end # We want the color with the highest contrast to background to be the foreground one, @@ -147,7 +147,7 @@ module Paperclip g = l.to_f b = l.to_f # achromatic else - q = l < 0.5 ? l * (1 + s) : l + s - l * s + q = l < 0.5 ? l * (s + 1) : l + s - l * s p = 2 * l - q r = hue_to_rgb(p, q, h + 1 / 3.0) g = hue_to_rgb(p, q, h) diff --git a/package.json b/package.json index b41004ad7..4cebdda02 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 686ae70fb..7d0b47928 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -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 diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb index 36e4ba86e..048d9de8d 100644 --- a/spec/controllers/settings/migrations_controller_spec.rb +++ b/spec/controllers/settings/migrations_controller_spec.rb @@ -51,7 +51,7 @@ describe Settings::MigrationsController do it_behaves_like 'authenticate user' end - context 'when user is sign in' do + context 'when user is signed in' do subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } } let(:user) { Fabricate(:user, password: '12345678') } @@ -67,12 +67,45 @@ describe Settings::MigrationsController do end end - context 'when acct is a current account' do + context 'when acct is the current account' do let(:acct) { user.account } it 'renders show' do is_expected.to render_template :show end + + it 'does not update the moved account' do + expect(user.account.reload.moved_to_account_id).to be_nil + end + end + + context 'when target account does not reference the account being moved from' do + let(:acct) { Fabricate(:account, also_known_as: []) } + + it 'renders show' do + is_expected.to render_template :show + end + + it 'does not update the moved account' do + expect(user.account.reload.moved_to_account_id).to be_nil + end + end + + context 'when a recent migration already exists ' do + let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } + + before do + moved_to = Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) + user.account.migrations.create!(acct: moved_to.acct) + end + + it 'renders show' do + is_expected.to render_template :show + end + + it 'does not update the moved account' do + expect(user.account.reload.moved_to_account_id).to be_nil + end end end end diff --git a/spec/lib/activitypub/activity/move_spec.rb b/spec/lib/activitypub/activity/move_spec.rb index 3574f273a..2d1d276c5 100644 --- a/spec/lib/activitypub/activity/move_spec.rb +++ b/spec/lib/activitypub/activity/move_spec.rb @@ -1,23 +1,11 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Move do - let(:follower) { Fabricate(:account) } - let(:old_account) { Fabricate(:account) } - let(:new_account) { Fabricate(:account) } - - before do - follower.follow!(old_account) - - old_account.update!(uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox') - new_account.update!(uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: [old_account.uri]) - - stub_request(:post, 'https://example.org/inbox').to_return(status: 200) - stub_request(:post, 'https://example.com/inbox').to_return(status: 200) - - service_stub = double - allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub) - allow(service_stub).to receive(:call).and_return(new_account) - end + let(:follower) { Fabricate(:account) } + let(:old_account) { Fabricate(:account, uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox') } + let(:new_account) { Fabricate(:account, uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: also_known_as) } + let(:also_known_as) { [old_account.uri] } + let(:returned_account) { new_account } let(:json) do { @@ -30,6 +18,17 @@ RSpec.describe ActivityPub::Activity::Move do }.with_indifferent_access end + before do + follower.follow!(old_account) + + stub_request(:post, old_account.inbox_url).to_return(status: 200) + stub_request(:post, new_account.inbox_url).to_return(status: 200) + + service_stub = double + allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub) + allow(service_stub).to receive(:call).and_return(returned_account) + end + describe '#perform' do subject { described_class.new(json, old_account) } @@ -37,16 +36,70 @@ RSpec.describe ActivityPub::Activity::Move do subject.perform end - it 'sets moved account on old account' do - expect(old_account.reload.moved_to_account_id).to eq new_account.id + context 'when all conditions are met' do + it 'sets moved account on old account' do + expect(old_account.reload.moved_to_account_id).to eq new_account.id + end + + it 'makes followers unfollow old account' do + expect(follower.following?(old_account)).to be false + end + + it 'makes followers follow-request the new account' do + expect(follower.requested?(new_account)).to be true + end end - it 'makes followers unfollow old account' do - expect(follower.following?(old_account)).to be false + context "when the new account can't be resolved" do + let(:returned_account) { nil } + + it 'does not set moved account on old account' do + expect(old_account.reload.moved_to_account_id).to be_nil + end + + it 'does not make followers unfollow old account' do + expect(follower.following?(old_account)).to be true + end + + it 'does not make followers follow-request the new account' do + expect(follower.requested?(new_account)).to be false + end end - it 'makes followers follow-request the new account' do - expect(follower.requested?(new_account)).to be true + context 'when the new account does not references the old account' do + let(:also_known_as) { [] } + + it 'does not set moved account on old account' do + expect(old_account.reload.moved_to_account_id).to be_nil + end + + it 'does not make followers unfollow old account' do + expect(follower.following?(old_account)).to be true + end + + it 'does not make followers follow-request the new account' do + expect(follower.requested?(new_account)).to be false + end + end + + context 'when a Move has been recently processed' do + around do |example| + Redis.current.set("move_in_progress:#{old_account.id}", true, nx: true, ex: 7.days.seconds) + example.run + Redis.current.del("move_in_progress:#{old_account.id}") + end + + it 'does not set moved account on old account' do + expect(old_account.reload.moved_to_account_id).to be_nil + end + + it 'does not make followers unfollow old account' do + expect(follower.following?(old_account)).to be true + end + + it 'does not make followers follow-request the new account' do + expect(follower.requested?(new_account)).to be false + end end end end diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 1f413eec9..2222c46fb 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -81,6 +81,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT))) document_hash = Digest::SHA256.hexdigest(canonicalize(document)) to_be_verified = options_hash + document_hash - Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified)) + Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified)) end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 0a045904b..1e9e45d8d 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -56,47 +56,114 @@ RSpec.describe Notification, type: :model do end end - describe '.reload_stale_associations!' do - context 'account_ids are empty' do - let(:cached_items) { [] } + 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 - subject { described_class.reload_stale_associations!(cached_items) } + context 'notifications are empty' do + let(:notifications) { [] } - it 'returns nil' do - is_expected.to be nil + it 'returns []' do + is_expected.to eq [] end end - context 'account_ids are present' do + 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 } - - let(:account1) { Fabricate(:account) } - let(:account2) { Fabricate(:account) } - - let(:accounts_with_ids) { { account1.id => account1, account2.id => account2 } } - - it 'reloads associations' do - expect(cached_items[0].from_account).to be stale_account1 - expect(cached_items[1].from_account).to be stale_account2 - - described_class.reload_stale_associations!(cached_items) + 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 + + # status + expect(subject[1].type).to eq :status + expect(subject[1].association(:status)).to be_loaded + + # 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 + + # 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].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 - expect(cached_items[0].from_account).to be account1 - expect(cached_items[1].from_account).to be account2 + 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 diff --git a/yarn.lock b/yarn.lock index 6b5c18b10..053be0c14 100644 --- a/yarn.lock +++ b/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" |