From 0a0a1f1495be69467b03a7597f995b4902698452 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 19 Jun 2023 03:51:40 -0300 Subject: [PATCH 01/39] Migrate to request specs in `/api/v1/tags` (#25439) --- .rubocop_todo.yml | 2 - .../api/v1/tags_controller_spec.rb | 88 --------- spec/requests/api/v1/tags_spec.rb | 169 ++++++++++++++++++ 3 files changed, 169 insertions(+), 90 deletions(-) delete mode 100644 spec/controllers/api/v1/tags_controller_spec.rb create mode 100644 spec/requests/api/v1/tags_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index aba4415fcb..f3b24cdbc4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -316,7 +316,6 @@ RSpec/LetSetup: - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v1/filters_controller_spec.rb' - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' @@ -756,7 +755,6 @@ Rails/WhereExists: - 'app/workers/move_worker.rb' - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - 'lib/tasks/tests.rake' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/models/account_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb' diff --git a/spec/controllers/api/v1/tags_controller_spec.rb b/spec/controllers/api/v1/tags_controller_spec.rb deleted file mode 100644 index e914f5992d..0000000000 --- a/spec/controllers/api/v1/tags_controller_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::TagsController do - render_views - - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:follows' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - - before { allow(controller).to receive(:doorkeeper_token) { token } } - - describe 'GET #show' do - before do - get :show, params: { id: name } - end - - context 'with existing tag' do - let!(:tag) { Fabricate(:tag) } - let(:name) { tag.name } - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - end - - context 'with non-existing tag' do - let(:name) { 'hoge' } - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - end - end - - describe 'POST #follow' do - let!(:unrelated_tag) { Fabricate(:tag) } - - before do - TagFollow.create!(account: user.account, tag: unrelated_tag) - - post :follow, params: { id: name } - end - - context 'with existing tag' do - let!(:tag) { Fabricate(:tag) } - let(:name) { tag.name } - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'creates follow' do - expect(TagFollow.where(tag: tag, account: user.account).exists?).to be true - end - end - - context 'with non-existing tag' do - let(:name) { 'hoge' } - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'creates follow' do - expect(TagFollow.where(tag: Tag.find_by!(name: name), account: user.account).exists?).to be true - end - end - end - - describe 'POST #unfollow' do - let!(:tag) { Fabricate(:tag, name: 'foo') } - let!(:tag_follow) { Fabricate(:tag_follow, account: user.account, tag: tag) } - - before do - post :unfollow, params: { id: tag.name } - end - - it 'returns http success' do - expect(response).to have_http_status(:success) - end - - it 'removes the follow' do - expect(TagFollow.where(tag: tag, account: user.account).exists?).to be false - end - end -end diff --git a/spec/requests/api/v1/tags_spec.rb b/spec/requests/api/v1/tags_spec.rb new file mode 100644 index 0000000000..300ddf805c --- /dev/null +++ b/spec/requests/api/v1/tags_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Tags' do + let(:user) { Fabricate(:user) } + let(:scopes) { 'write:follows' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/tags/:id' do + subject do + get "/api/v1/tags/#{name}" + end + + context 'when the tag exists' do + let!(:tag) { Fabricate(:tag) } + let(:name) { tag.name } + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns the tag' do + subject + + expect(body_as_json[:name]).to eq(name) + end + end + + context 'when the tag does not exist' do + let(:name) { 'hoge' } + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + end + + context 'when the tag name is invalid' do + let(:name) { 'tag-name' } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /api/v1/tags/:id/follow' do + subject do + post "/api/v1/tags/#{name}/follow", headers: headers + end + + let!(:tag) { Fabricate(:tag) } + let(:name) { tag.name } + + it_behaves_like 'forbidden for wrong scope', 'read read:follows' + + context 'when the tag exists' do + it 'returns http success' do + subject + + expect(response).to have_http_status(:success) + end + + it 'creates follow' do + subject + + expect(TagFollow.where(tag: tag, account: user.account)).to exist + end + end + + context 'when the tag does not exist' do + let(:name) { 'hoge' } + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'creates a new tag with the specified name' do + subject + + expect(Tag.where(name: name)).to exist + end + + it 'creates follow' do + subject + + expect(TagFollow.where(tag: Tag.find_by(name: name), account: user.account)).to exist + end + end + + context 'when the tag name is invalid' do + let(:name) { 'tag-name' } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + + context 'when the Authorization header is missing' do + let(:headers) { {} } + let(:name) { 'unauthorized' } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + end + end + end + + describe 'POST #unfollow' do + subject do + post "/api/v1/tags/#{name}/unfollow", headers: headers + end + + let(:name) { tag.name } + let!(:tag) { Fabricate(:tag, name: 'foo') } + + before do + Fabricate(:tag_follow, account: user.account, tag: tag) + end + + it_behaves_like 'forbidden for wrong scope', 'read read:follows' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'removes the follow' do + subject + + expect(TagFollow.where(tag: tag, account: user.account)).to_not exist + end + + context 'when the tag name is invalid' do + let(:name) { 'tag-name' } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + + context 'when the Authorization header is missing' do + let(:headers) { {} } + let(:name) { 'unauthorized' } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + end + end + end +end From b9bc9d0bdada72c74f52fc933c437e19a2e67f3f Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 19 Jun 2023 03:53:05 -0300 Subject: [PATCH 02/39] Fix incorrect pagination headers in `/api/v2/admin/accounts` (#25477) --- app/controllers/api/v2/admin/accounts_controller.rb | 8 ++++++++ spec/controllers/api/v2/admin/accounts_controller_spec.rb | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index 0c451f778c..65cf0c4db4 100644 --- a/app/controllers/api/v2/admin/accounts_controller.rb +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController private + def next_path + api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + def filtered_accounts AccountFilter.new(translated_filter_params).results end diff --git a/spec/controllers/api/v2/admin/accounts_controller_spec.rb b/spec/controllers/api/v2/admin/accounts_controller_spec.rb index a775be1709..635f645915 100644 --- a/spec/controllers/api/v2/admin/accounts_controller_spec.rb +++ b/spec/controllers/api/v2/admin/accounts_controller_spec.rb @@ -55,5 +55,13 @@ RSpec.describe Api::V2::Admin::AccountsController do end end end + + context 'with limit param' do + let(:params) { { limit: 1 } } + + it 'sets the correct pagination headers' do + expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v2_admin_accounts_url(limit: 1, max_id: admin_account.id) + end + end end end From a0d7ae257da66fc88732a491a315bc86ea299532 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 09:03:50 +0200 Subject: [PATCH 03/39] Update dependency aws-sdk-s3 to v1.126.0 (#25480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index bdbeb79221..5f3678fe58 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,7 +106,7 @@ GEM aws-sdk-kms (1.67.0) aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.125.0) + aws-sdk-s3 (1.126.0) aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) From 155ec185b2ed96b0c091bdd0c01e1193eda386e4 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 19 Jun 2023 03:04:15 -0400 Subject: [PATCH 04/39] Remove unused `picture_hint` helper method (#25485) --- app/helpers/settings_helper.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index ae89cec780..889ca7f402 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -24,13 +24,4 @@ module SettingsHelper safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end - - def picture_hint(hint, picture) - if picture.original_filename.nil? - hint - else - link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end - end end From e835198b26c9985daf353a6a96886ff61fd62c17 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 19 Jun 2023 03:05:42 -0400 Subject: [PATCH 05/39] Combine assertions in api/v1/notifications spec (#25486) --- .../api/v1/notifications_controller_spec.rb | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index 28b8e656ab..6615848b83 100644 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/notifications_controller_spec.rb @@ -67,24 +67,13 @@ RSpec.describe Api::V1::NotificationsController do get :index end - it 'returns http success' do + it 'returns expected notification types', :aggregate_failures do expect(response).to have_http_status(200) - end - it 'includes reblog' do - expect(body_as_json.pluck(:type)).to include 'reblog' - end - - it 'includes mention' do - expect(body_as_json.pluck(:type)).to include 'mention' - end - - it 'includes favourite' do - expect(body_as_json.pluck(:type)).to include 'favourite' - end - - it 'includes follow' do - expect(body_as_json.pluck(:type)).to include 'follow' + expect(body_json_types).to include 'reblog' + expect(body_json_types).to include 'mention' + expect(body_json_types).to include 'favourite' + expect(body_json_types).to include 'follow' end end @@ -93,12 +82,14 @@ RSpec.describe Api::V1::NotificationsController do get :index, params: { account_id: third.account.id } end - it 'returns http success' do + it 'returns only notifications from specified user', :aggregate_failures do expect(response).to have_http_status(200) + + expect(body_json_account_ids.uniq).to eq [third.account.id.to_s] end - it 'returns only notifications from specified user' do - expect(body_as_json.map { |x| x[:account][:id] }.uniq).to eq [third.account.id.to_s] + def body_json_account_ids + body_as_json.map { |x| x[:account][:id] } end end @@ -107,27 +98,23 @@ RSpec.describe Api::V1::NotificationsController do get :index, params: { account_id: 'foo' } end - it 'returns http success' do + it 'returns nothing', :aggregate_failures do expect(response).to have_http_status(200) - end - it 'returns nothing' do expect(body_as_json.size).to eq 0 end end - describe 'with excluded_types param' do + describe 'with exclude_types param' do before do get :index, params: { exclude_types: %w(mention) } end - it 'returns http success' do + it 'returns everything but excluded type', :aggregate_failures do expect(response).to have_http_status(200) - end - it 'returns everything but excluded type' do expect(body_as_json.size).to_not eq 0 - expect(body_as_json.pluck(:type).uniq).to_not include 'mention' + expect(body_json_types.uniq).to_not include 'mention' end end @@ -136,13 +123,15 @@ RSpec.describe Api::V1::NotificationsController do get :index, params: { types: %w(mention) } end - it 'returns http success' do + it 'returns only requested type', :aggregate_failures do expect(response).to have_http_status(200) - end - it 'returns only requested type' do - expect(body_as_json.pluck(:type).uniq).to eq ['mention'] + expect(body_json_types.uniq).to eq ['mention'] end end + + def body_json_types + body_as_json.pluck(:type) + end end end From 3a65fb044fae4d4d717765b8163e6fbaac4e1795 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 19 Jun 2023 03:50:35 -0400 Subject: [PATCH 06/39] Add coverage for `UserMailer` methods (#25484) --- spec/mailers/user_mailer_spec.rb | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 702aa1c354..3c42a2bb7a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -142,4 +142,59 @@ describe UserMailer do expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_rejected.title') end end + + describe 'two_factor_enabled' do + let(:mail) { described_class.two_factor_enabled(receiver) } + + it 'renders two_factor_enabled mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_enabled.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_enabled.explanation') + end + end + + describe 'two_factor_disabled' do + let(:mail) { described_class.two_factor_disabled(receiver) } + + it 'renders two_factor_disabled mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_disabled.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_disabled.explanation') + end + end + + describe 'webauthn_enabled' do + let(:mail) { described_class.webauthn_enabled(receiver) } + + it 'renders webauthn_enabled mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_enabled.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_enabled.explanation') + end + end + + describe 'webauthn_disabled' do + let(:mail) { described_class.webauthn_disabled(receiver) } + + it 'renders webauthn_disabled mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_disabled.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_disabled.explanation') + end + end + + describe 'two_factor_recovery_codes_changed' do + let(:mail) { described_class.two_factor_recovery_codes_changed(receiver) } + + it 'renders two_factor_recovery_codes_changed mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_recovery_codes_changed.explanation') + end + end + + describe 'webauthn_credential_added' do + let(:credential) { Fabricate.build(:webauthn_credential) } + let(:mail) { described_class.webauthn_credential_added(receiver, credential) } + + it 'renders webauthn_credential_added mail' do + expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_credential.added.subject') + expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.added.explanation') + end + end end From cec4f1d5062943a0fbe867bfe17f62c8c7c3069b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:28:59 +0200 Subject: [PATCH 07/39] Update dependency dotenv to v16.2.0 (#25506) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1f8b99641f..7acecd4e3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4719,9 +4719,9 @@ domutils@^3.0.1: domhandler "^5.0.3" dotenv@^16.0.3: - version "16.1.4" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c" - integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw== + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== duplexer@^0.1.2: version "0.1.2" @@ -10732,6 +10732,7 @@ string-length@^4.0.1: strip-ansi "^6.0.0" "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10830,6 +10831,7 @@ stringz@^2.1.0: char-regex "^1.0.2" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12231,6 +12233,7 @@ workbox-window@7.0.0, workbox-window@^7.0.0: workbox-core "7.0.0" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From e1c9d52e913b75a69ebb71c33c489c56b57b23af Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 19 Jun 2023 07:48:25 -0400 Subject: [PATCH 08/39] Reduce `sleep` time in request pool spec (#25470) --- app/lib/request_pool.rb | 5 +++-- spec/lib/request_pool_spec.rb | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/lib/request_pool.rb b/app/lib/request_pool.rb index 6be1722860..86c825498d 100644 --- a/app/lib/request_pool.rb +++ b/app/lib/request_pool.rb @@ -28,8 +28,9 @@ class RequestPool end MAX_IDLE_TIME = 30 - WAIT_TIMEOUT = 5 MAX_POOL_SIZE = ENV.fetch('MAX_REQUEST_POOL_SIZE', 512).to_i + REAPER_FREQUENCY = 30 + WAIT_TIMEOUT = 5 class Connection attr_reader :site, :last_used_at, :created_at, :in_use, :dead, :fresh @@ -98,7 +99,7 @@ class RequestPool def initialize @pool = ConnectionPool::SharedConnectionPool.new(size: MAX_POOL_SIZE, timeout: WAIT_TIMEOUT) { |site| Connection.new(site) } - @reaper = Reaper.new(self, 30) + @reaper = Reaper.new(self, REAPER_FREQUENCY) @reaper.run end diff --git a/spec/lib/request_pool_spec.rb b/spec/lib/request_pool_spec.rb index 395268fe43..f179e6ca94 100644 --- a/spec/lib/request_pool_spec.rb +++ b/spec/lib/request_pool_spec.rb @@ -48,16 +48,25 @@ describe RequestPool do expect(subject.size).to be > 1 end - it 'closes idle connections' do - stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') - - subject.with('http://example.com') do |http_client| - http_client.get('/').flush + context 'with an idle connection' do + before do + stub_const('RequestPool::MAX_IDLE_TIME', 1) # Lower idle time limit to 1 seconds + stub_const('RequestPool::REAPER_FREQUENCY', 0.1) # Run reaper every 0.1 seconds + stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') end - expect(subject.size).to eq 1 - sleep RequestPool::MAX_IDLE_TIME + 30 + 1 - expect(subject.size).to eq 0 + it 'closes the connections' do + subject.with('http://example.com') do |http_client| + http_client.get('/').flush + end + + expect { reaper_observes_idle_timeout }.to change(subject, :size).from(1).to(0) + end + + def reaper_observes_idle_timeout + # One full idle period and 2 reaper cycles more + sleep RequestPool::MAX_IDLE_TIME + (RequestPool::REAPER_FREQUENCY * 2) + end end end end From 804488d38e9942280f7d320af8c7fef7860a4ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=84=E3=81=A1=20=E3=81=B2?= Date: Mon, 19 Jun 2023 21:11:46 +0900 Subject: [PATCH 09/39] Rewrite `` as FC and TS (#25481) --- .../components/autosuggest_hashtag.jsx | 44 ------------------- .../components/autosuggest_hashtag.tsx | 42 ++++++++++++++++++ .../mastodon/components/autosuggest_input.jsx | 2 +- .../components/autosuggest_textarea.jsx | 2 +- 4 files changed, 44 insertions(+), 46 deletions(-) delete mode 100644 app/javascript/mastodon/components/autosuggest_hashtag.jsx create mode 100644 app/javascript/mastodon/components/autosuggest_hashtag.tsx diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.jsx b/app/javascript/mastodon/components/autosuggest_hashtag.jsx deleted file mode 100644 index b509f48df0..0000000000 --- a/app/javascript/mastodon/components/autosuggest_hashtag.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import ShortNumber from 'mastodon/components/short_number'; - -export default class AutosuggestHashtag extends PureComponent { - - static propTypes = { - tag: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string, - history: PropTypes.array, - }).isRequired, - }; - - render() { - const { tag } = this.props; - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
-
- #{tag.name} -
- {tag.history !== undefined && ( -
- -
- )} -
- ); - } - -} diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.tsx b/app/javascript/mastodon/components/autosuggest_hashtag.tsx new file mode 100644 index 0000000000..c6798054db --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest_hashtag.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; + +import ShortNumber from 'mastodon/components/short_number'; + +interface Props { + tag: { + name: string; + url?: string; + history?: Array<{ + uses: number; + accounts: string; + day: string; + }>; + following?: boolean; + type: 'hashtag'; + }; +} + +export const AutosuggestHashtag: React.FC = ({ tag }) => { + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); + + return ( +
+
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/components/autosuggest_input.jsx b/app/javascript/mastodon/components/autosuggest_input.jsx index 890f94928b..06cbb5d75b 100644 --- a/app/javascript/mastodon/components/autosuggest_input.jsx +++ b/app/javascript/mastodon/components/autosuggest_input.jsx @@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index 463d2e94c1..230e4f6572 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; From dd07393e755062d2d656ae7872c949ef7a9ddec7 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 19 Jun 2023 15:06:06 +0200 Subject: [PATCH 10/39] Fix user settings not getting validated (#25508) --- app/models/user_settings.rb | 5 ++++- spec/models/user_settings_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 2c025d6c56..0f77f45f78 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -72,7 +72,10 @@ class UserSettings raise KeyError, "Undefined setting: #{key}" unless self.class.definition_for?(key) - typecast_value = self.class.definition_for(key).type_cast(value) + setting_definition = self.class.definition_for(key) + typecast_value = setting_definition.type_cast(value) + + raise ArgumentError, "Invalid value for setting #{key}: #{typecast_value}" if setting_definition.in.present? && setting_definition.in.exclude?(typecast_value) if typecast_value.nil? @original_hash.delete(key) diff --git a/spec/models/user_settings_spec.rb b/spec/models/user_settings_spec.rb index f0e4272fd9..653597c90d 100644 --- a/spec/models/user_settings_spec.rb +++ b/spec/models/user_settings_spec.rb @@ -49,6 +49,16 @@ RSpec.describe UserSettings do expect(subject[:always_send_emails]).to be true end end + + context 'when the setting has a closed set of values' do + it 'updates the attribute when given a valid value' do + expect { subject[:'web.display_media'] = :show_all }.to change { subject[:'web.display_media'] }.from('default').to('show_all') + end + + it 'raises an error when given an invalid value' do + expect { subject[:'web.display_media'] = 'invalid value' }.to raise_error ArgumentError + end + end end describe '#update' do From 3a91603b1507d3821e8a20734e6ed92ae4d06c3b Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Tue, 20 Jun 2023 18:04:35 +0200 Subject: [PATCH 11/39] Prevent UserCleanupScheduler from overwhelming streaming (#25519) --- app/services/remove_status_service.rb | 18 +++++++++++++++++- .../scheduler/user_cleanup_scheduler.rb | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 25da2c6eb4..4eda5b355b 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -12,6 +12,7 @@ class RemoveStatusService < BaseService # @option [Boolean] :immediate # @option [Boolean] :preserve # @option [Boolean] :original_removed + # @option [Boolean] :skip_streaming def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @@ -52,6 +53,9 @@ class RemoveStatusService < BaseService private + # The following FeedManager calls all do not result in redis publishes for + # streaming, as the `:update` option is false + def remove_from_self FeedManager.instance.unpush_from_home(@account, @status) end @@ -75,6 +79,8 @@ class RemoveStatusService < BaseService # followers. Here we send a delete to actively mentioned accounts # that may not follow the account + return if skip_streaming? + @status.active_mentions.find_each do |mention| redis.publish("timeline:#{mention.account_id}", @payload) end @@ -103,7 +109,7 @@ class RemoveStatusService < BaseService # without us being able to do all the fancy stuff @status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).includes(:account).reorder(nil).find_each do |reblog| - RemoveStatusService.new.call(reblog, original_removed: true) + RemoveStatusService.new.call(reblog, original_removed: true, skip_streaming: skip_streaming?) end end @@ -114,6 +120,8 @@ class RemoveStatusService < BaseService return unless @status.public_visibility? + return if skip_streaming? + @status.tags.map(&:name).each do |hashtag| redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local? @@ -123,6 +131,8 @@ class RemoveStatusService < BaseService def remove_from_public return unless @status.public_visibility? + return if skip_streaming? + redis.publish('timeline:public', @payload) redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload) end @@ -130,6 +140,8 @@ class RemoveStatusService < BaseService def remove_from_media return unless @status.public_visibility? + return if skip_streaming? + redis.publish('timeline:public:media', @payload) redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload) end @@ -143,4 +155,8 @@ class RemoveStatusService < BaseService def permanently? @options[:immediate] || !(@options[:preserve] || @status.reported?) end + + def skip_streaming? + !!@options[:skip_streaming] + end end diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb index 45cfbc62e6..4aee7935a2 100644 --- a/app/workers/scheduler/user_cleanup_scheduler.rb +++ b/app/workers/scheduler/user_cleanup_scheduler.rb @@ -24,7 +24,7 @@ class Scheduler::UserCleanupScheduler def clean_discarded_statuses! Status.unscoped.discarded.where('deleted_at <= ?', 30.days.ago).find_in_batches do |statuses| RemovalWorker.push_bulk(statuses) do |status| - [status.id, { 'immediate' => true }] + [status.id, { 'immediate' => true, 'skip_streaming' => true }] end end end From c78280a8ce4c841dd2a454ba086e95cfa4c37438 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jun 2023 18:10:19 +0200 Subject: [PATCH 12/39] Add translate="no" to outgoing mentions and links (#25524) --- app/lib/text_formatter.rb | 4 ++-- lib/sanitize_ext/sanitize_config.rb | 10 ++++++++-- spec/lib/sanitize_config_spec.rb | 8 ++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb index 243e892891..0404cbaced 100644 --- a/app/lib/text_formatter.rb +++ b/app/lib/text_formatter.rb @@ -79,7 +79,7 @@ class TextFormatter cutoff = url[prefix.length..-1].length > 30 <<~HTML.squish - #{h(display_url)} + #{h(display_url)} HTML rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError h(entity[:url]) @@ -122,7 +122,7 @@ class TextFormatter display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username <<~HTML.squish - @#{h(display_username)} + @#{h(display_username)} HTML end diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 9cc500c36e..bcd89af67a 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -36,6 +36,11 @@ class Sanitize node['class'] = class_list.join(' ') end + TRANSLATE_TRANSFORMER = lambda do |env| + node = env[:node] + node.remove_attribute('translate') unless node['translate'] == 'no' + end + UNSUPPORTED_HREF_TRANSFORMER = lambda do |env| return unless env[:node_name] == 'a' @@ -63,8 +68,8 @@ class Sanitize elements: %w(p br span a del pre blockquote code b strong u i em ul ol li), attributes: { - 'a' => %w(href rel class), - 'span' => %w(class), + 'a' => %w(href rel class translate), + 'span' => %w(class translate), 'ol' => %w(start reversed), 'li' => %w(value), }, @@ -80,6 +85,7 @@ class Sanitize transformers: [ CLASS_WHITELIST_TRANSFORMER, + TRANSLATE_TRANSFORMER, UNSUPPORTED_ELEMENTS_TRANSFORMER, UNSUPPORTED_HREF_TRANSFORMER, ] diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index a01122bed0..550ad1c52b 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -38,6 +38,14 @@ describe Sanitize::Config do expect(Sanitize.fragment('Test', subject)).to eq 'Test' end + it 'keeps a with translate="no"' do + expect(Sanitize.fragment('Test', subject)).to eq 'Test' + end + + it 'removes "translate" attribute with invalid value' do + expect(Sanitize.fragment('Test', subject)).to eq 'Test' + end + it 'removes a with unparsable href' do expect(Sanitize.fragment('Test', subject)).to eq 'Test' end From fd23f5024377ee3d31d1a9fd7d2f094036ceb45b Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jun 2023 18:15:35 +0200 Subject: [PATCH 13/39] Fix wrong view being displayed when a webhook fails validation (#25464) --- app/controllers/admin/webhooks_controller.rb | 2 +- spec/controllers/admin/webhooks_controller_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index 01d9ba8ce2..e087476658 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -42,7 +42,7 @@ module Admin if @webhook.update(resource_params) redirect_to admin_webhook_path(@webhook) else - render :show + render :edit end end diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb index 5e45c74082..074956c555 100644 --- a/spec/controllers/admin/webhooks_controller_spec.rb +++ b/spec/controllers/admin/webhooks_controller_spec.rb @@ -82,7 +82,7 @@ describe Admin::WebhooksController do end.to_not change(webhook, :url) expect(response).to have_http_status(:success) - expect(response).to render_template(:show) + expect(response).to render_template(:edit) end end From e53eb38a8d1eabe7b1de6852e7114ace1c435d63 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Tue, 20 Jun 2023 13:16:48 -0300 Subject: [PATCH 14/39] Migrate to request specs in `/api/v1/admin/account_actions` (#25514) --- .../admin/account_actions_controller_spec.rb | 55 ------- .../api/v1/admin/account_actions_spec.rb | 154 ++++++++++++++++++ 2 files changed, 154 insertions(+), 55 deletions(-) delete mode 100644 spec/controllers/api/v1/admin/account_actions_controller_spec.rb create mode 100644 spec/requests/api/v1/admin/account_actions_spec.rb diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb deleted file mode 100644 index 523350e123..0000000000 --- a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::Admin::AccountActionsController do - render_views - - let(:role) { UserRole.find_by(name: 'Moderator') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:account) { Fabricate(:account) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'POST #create' do - context 'with type of disable' do - before do - post :create, params: { account_id: account.id, type: 'disable' } - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'performs action against account' do - expect(account.reload.user_disabled?).to be true - end - - it 'logs action' do - log_item = Admin::ActionLog.last - - expect(log_item).to_not be_nil - expect(log_item.action).to eq :disable - expect(log_item.account_id).to eq user.account_id - expect(log_item.target_id).to eq account.user.id - end - end - - context 'with no type' do - before do - post :create, params: { account_id: account.id } - end - - it 'returns http unprocessable entity' do - expect(response).to have_http_status(422) - end - end - end -end diff --git a/spec/requests/api/v1/admin/account_actions_spec.rb b/spec/requests/api/v1/admin/account_actions_spec.rb new file mode 100644 index 0000000000..9295d262d6 --- /dev/null +++ b/spec/requests/api/v1/admin/account_actions_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Account actions' do + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:scopes) { 'admin:write admin:write:accounts' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + let(:mailer) { instance_double(ActionMailer::MessageDelivery, deliver_later!: nil) } + + before do + allow(UserMailer).to receive(:warning).with(target_account.user, anything).and_return(mailer) + end + + shared_examples 'a successful notification delivery' do + it 'notifies the user about the action taken' do + subject + + expect(UserMailer).to have_received(:warning).with(target_account.user, anything).once + expect(mailer).to have_received(:deliver_later!).once + end + end + + shared_examples 'a successful logged action' do |action_type, target_type| + it 'logs action' do + subject + + log_item = Admin::ActionLog.last + + expect(log_item).to be_present + expect(log_item.action).to eq(action_type) + expect(log_item.account_id).to eq(user.account_id) + expect(log_item.target_id).to eq(target_type == :user ? target_account.user.id : target_account.id) + end + end + + describe 'POST /api/v1/admin/accounts/:id/action' do + subject do + post "/api/v1/admin/accounts/#{target_account.id}/action", headers: headers, params: params + end + + let(:target_account) { Fabricate(:account) } + + context 'with type of disable' do + let(:params) { { type: 'disable' } } + + it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'a successful notification delivery' + it_behaves_like 'a successful logged action', :disable, :user + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'disables the target account' do + expect { subject }.to change { target_account.reload.user_disabled? }.from(false).to(true) + end + end + + context 'with type of sensitive' do + let(:params) { { type: 'sensitive' } } + + it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'a successful notification delivery' + it_behaves_like 'a successful logged action', :sensitive, :account + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'marks the target account as sensitive' do + expect { subject }.to change { target_account.reload.sensitized? }.from(false).to(true) + end + end + + context 'with type of silence' do + let(:params) { { type: 'silence' } } + + it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'a successful notification delivery' + it_behaves_like 'a successful logged action', :silence, :account + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'marks the target account as silenced' do + expect { subject }.to change { target_account.reload.silenced? }.from(false).to(true) + end + end + + context 'with type of suspend' do + let(:params) { { type: 'suspend' } } + + it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' + it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'a successful notification delivery' + it_behaves_like 'a successful logged action', :suspend, :account + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'marks the target account as suspended' do + expect { subject }.to change { target_account.reload.suspended? }.from(false).to(true) + end + end + + context 'with type of none' do + let(:params) { { type: 'none' } } + + it_behaves_like 'a successful notification delivery' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + end + + context 'with no type' do + let(:params) { {} } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with invalid type' do + let(:params) { { type: 'invalid' } } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + end +end From ec91ea44575a5d9cb8ce1be5d0448ef5e2d98a6e Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jun 2023 18:32:14 +0200 Subject: [PATCH 15/39] Fix missing validation on `default_privacy` setting (#25513) --- app/models/user_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 0f77f45f78..71af7aaeb0 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -14,7 +14,7 @@ class UserSettings setting :show_application, default: true setting :default_language, default: nil setting :default_sensitive, default: false - setting :default_privacy, default: nil + setting :default_privacy, default: nil, in: %w(public unlisted private) namespace :web do setting :crop_images, default: true From ebfeaebedbc43526116fc5c0abe82f3d8745c927 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jun 2023 18:32:26 +0200 Subject: [PATCH 16/39] Fix /api/v1/conversations sometimes returning empty accounts (#25499) --- app/models/account_conversation.rb | 10 ++-------- .../api/v1/conversations_controller_spec.rb | 6 ++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb index 32fe79ccf7..25a75d8a61 100644 --- a/app/models/account_conversation.rb +++ b/app/models/account_conversation.rb @@ -32,14 +32,8 @@ class AccountConversation < ApplicationRecord end def participant_accounts - @participant_accounts ||= begin - if participant_account_ids.empty? - [account] - else - participants = Account.where(id: participant_account_ids).to_a - participants.empty? ? [account] : participants - end - end + @participant_accounts ||= Account.where(id: participant_account_ids).to_a + @participant_accounts.presence || [account] end class << self diff --git a/spec/controllers/api/v1/conversations_controller_spec.rb b/spec/controllers/api/v1/conversations_controller_spec.rb index f8a5985634..28d7c7f3ae 100644 --- a/spec/controllers/api/v1/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/conversations_controller_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Api::V1::ConversationsController do before do PostStatusService.new.call(other.account, text: 'Hey @alice', visibility: 'direct') + PostStatusService.new.call(user.account, text: 'Hey, nobody here', visibility: 'direct') end it 'returns http success' do @@ -33,7 +34,8 @@ RSpec.describe Api::V1::ConversationsController do it 'returns conversations' do get :index json = body_as_json - expect(json.size).to eq 1 + expect(json.size).to eq 2 + expect(json[0][:accounts].size).to eq 1 end context 'with since_id' do @@ -41,7 +43,7 @@ RSpec.describe Api::V1::ConversationsController do it 'returns conversations' do get :index, params: { since_id: Mastodon::Snowflake.id_at(1.hour.ago, with_random: false) } json = body_as_json - expect(json.size).to eq 1 + expect(json.size).to eq 2 end end From 37a9c2258a323eee69274dfb6d710a712201c61d Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jun 2023 18:54:05 +0200 Subject: [PATCH 17/39] Add per-test timeouts to AutoStatusesCleanupScheduler tests (#24841) --- .../scheduler/accounts_statuses_cleanup_scheduler_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 5565636d57..4d9185093a 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -75,6 +75,12 @@ describe Scheduler::AccountsStatusesCleanupScheduler do end describe '#perform' do + around do |example| + Timeout.timeout(30) do + example.run + end + end + before do # Policies for the accounts Fabricate(:account_statuses_cleanup_policy, account: account_alice) From 69db507924d6d9350cca8a7127e773d46f9b8f48 Mon Sep 17 00:00:00 2001 From: Ian Date: Wed, 21 Jun 2023 16:58:00 +0100 Subject: [PATCH 18/39] Change emoji picker icon (#25479) --- .../features/compose/components/emoji_picker_dropdown.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx index 79551b512f..494b8d8624 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx @@ -389,7 +389,7 @@ class EmojiPickerDropdown extends PureComponent { {button || 🙂} From 6ac271c2a008a1d1d865918ffd5c95daee737b63 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Thu, 22 Jun 2023 06:49:35 -0300 Subject: [PATCH 19/39] Migrate to request specs in `/api/v1/suggestions` (#25540) --- .../api/v1/suggestions_controller_spec.rb | 37 ------- spec/requests/api/v1/suggestions_spec.rb | 103 ++++++++++++++++++ 2 files changed, 103 insertions(+), 37 deletions(-) delete mode 100644 spec/controllers/api/v1/suggestions_controller_spec.rb create mode 100644 spec/requests/api/v1/suggestions_spec.rb diff --git a/spec/controllers/api/v1/suggestions_controller_spec.rb b/spec/controllers/api/v1/suggestions_controller_spec.rb deleted file mode 100644 index c61ce0ec05..0000000000 --- a/spec/controllers/api/v1/suggestions_controller_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::SuggestionsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - let(:bob) { Fabricate(:account) } - let(:jeff) { Fabricate(:account) } - - before do - PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog) - PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite) - - get :index - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns accounts' do - json = body_as_json - - expect(json.size).to be >= 1 - expect(json.pluck(:id)).to include(*[bob, jeff].map { |i| i.id.to_s }) - end - end -end diff --git a/spec/requests/api/v1/suggestions_spec.rb b/spec/requests/api/v1/suggestions_spec.rb new file mode 100644 index 0000000000..42b7f86629 --- /dev/null +++ b/spec/requests/api/v1/suggestions_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Suggestions' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/suggestions' do + subject do + get '/api/v1/suggestions', headers: headers, params: params + end + + let(:bob) { Fabricate(:account) } + let(:jeff) { Fabricate(:account) } + let(:params) { {} } + + before do + PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog) + PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite) + end + + it_behaves_like 'forbidden for wrong scope', 'write' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns accounts' do + subject + + body = body_as_json + + expect(body.size).to eq 2 + expect(body.pluck(:id)).to match_array([bob, jeff].map { |i| i.id.to_s }) + end + + context 'with limit param' do + let(:params) { { limit: 1 } } + + it 'returns only the requested number of accounts' do + subject + + expect(body_as_json.size).to eq 1 + end + end + + context 'without an authorization header' do + let(:headers) { {} } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + end + end + end + + describe 'DELETE /api/v1/suggestions/:id' do + subject do + delete "/api/v1/suggestions/#{jeff.id}", headers: headers + end + + let(:suggestions_source) { instance_double(AccountSuggestions::PastInteractionsSource, remove: nil) } + let(:bob) { Fabricate(:account) } + let(:jeff) { Fabricate(:account) } + + before do + PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog) + PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite) + allow(AccountSuggestions::PastInteractionsSource).to receive(:new).and_return(suggestions_source) + end + + it_behaves_like 'forbidden for wrong scope', 'write' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'removes the specified suggestion' do + subject + + expect(suggestions_source).to have_received(:remove).with(user.account, jeff.id.to_s).once + expect(suggestions_source).to_not have_received(:remove).with(user.account, bob.id.to_s) + end + + context 'without an authorization header' do + let(:headers) { {} } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + end + end + end +end From 0b39b9abee65894efb5797b364e7f2af9b12ba5b Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jun 2023 05:53:28 -0400 Subject: [PATCH 20/39] Speed-up on `BackupService` spec (#25527) --- spec/services/backup_service_spec.rb | 83 ++++++++++++++++------------ 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 73e0b42adb..806ba18323 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -30,7 +30,7 @@ RSpec.describe BackupService, type: :service do it 'stores them as expected' do service_call - json = Oj.load(read_zip_file(backup, 'actor.json')) + json = export_json(:actor) avatar_path = json.dig('icon', 'url') header_path = json.dig('image', 'url') @@ -42,47 +42,60 @@ RSpec.describe BackupService, type: :service do end end - it 'marks the backup as processed' do - expect { service_call }.to change(backup, :processed).from(false).to(true) + it 'marks the backup as processed and exports files' do + expect { service_call }.to process_backup + + expect_outbox_export + expect_likes_export + expect_bookmarks_export end - it 'exports outbox.json as expected' do - service_call + def process_backup + change(backup, :processed).from(false).to(true) + end - json = Oj.load(read_zip_file(backup, 'outbox.json')) - expect(json['@context']).to_not be_nil - expect(json['type']).to eq 'OrderedCollection' - expect(json['totalItems']).to eq 2 - expect(json['orderedItems'][0]['@context']).to be_nil - expect(json['orderedItems'][0]).to include({ + def expect_outbox_export + json = export_json(:outbox) + + aggregate_failures do + expect(json['@context']).to_not be_nil + expect(json['type']).to eq 'OrderedCollection' + expect(json['totalItems']).to eq 2 + expect(json['orderedItems'][0]['@context']).to be_nil + expect(json['orderedItems'][0]).to include_create_item(status) + expect(json['orderedItems'][1]).to include_create_item(private_status) + end + end + + def expect_likes_export + json = export_json(:likes) + + aggregate_failures do + expect(json['type']).to eq 'OrderedCollection' + expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)] + end + end + + def expect_bookmarks_export + json = export_json(:bookmarks) + + aggregate_failures do + expect(json['type']).to eq 'OrderedCollection' + expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)] + end + end + + def export_json(type) + Oj.load(read_zip_file(backup, "#{type}.json")) + end + + def include_create_item(status) + include({ 'type' => 'Create', 'object' => include({ 'id' => ActivityPub::TagManager.instance.uri_for(status), - 'content' => '

Hello

', + 'content' => "

#{status.text}

", }), }) - expect(json['orderedItems'][1]).to include({ - 'type' => 'Create', - 'object' => include({ - 'id' => ActivityPub::TagManager.instance.uri_for(private_status), - 'content' => '

secret

', - }), - }) - end - - it 'exports likes.json as expected' do - service_call - - json = Oj.load(read_zip_file(backup, 'likes.json')) - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)] - end - - it 'exports bookmarks.json as expected' do - service_call - - json = Oj.load(read_zip_file(backup, 'bookmarks.json')) - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)] end end From 8d2c26834f7a485e6fd9083b17b025ad5030e471 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Thu, 22 Jun 2023 19:10:49 +0900 Subject: [PATCH 21/39] Fix custom signup URL may not loaded (#25531) --- .../mastodon/features/ui/components/header.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index bb6747c00c..05abc1ca63 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -8,6 +8,7 @@ import { Link, withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; +import { fetchServer } from 'mastodon/actions/server'; import { Avatar } from 'mastodon/components/avatar'; import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo'; import { registrationsOpen, me } from 'mastodon/initial_state'; @@ -28,6 +29,9 @@ const mapDispatchToProps = (dispatch) => ({ openClosedRegistrationsModal() { dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, + dispatchServer() { + dispatch(fetchServer()); + } }); class Header extends PureComponent { @@ -40,8 +44,14 @@ class Header extends PureComponent { openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, signupUrl: PropTypes.string.isRequired, + dispatchServer: PropTypes.func }; + componentDidMount () { + const { dispatchServer } = this.props; + dispatchServer(); + } + render () { const { signedIn } = this.context.identity; const { location, openClosedRegistrationsModal, signupUrl } = this.props; From 63d15d533070a3c1b97f048fbfffa0b1a34381e4 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jun 2023 08:51:53 -0400 Subject: [PATCH 22/39] Speed-up on `StatusesController` spec (#25549) --- spec/controllers/statuses_controller_spec.rb | 237 ++----------------- 1 file changed, 21 insertions(+), 216 deletions(-) diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 1885814cda..bd98929c02 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -75,23 +75,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns public Cache-Control header' do expect(response.headers['Cache-Control']).to include 'public' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -100,25 +88,13 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns Link header' do - expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - it_behaves_like 'cacheable response' - it 'returns Content-Type header' do + it 'renders ActivityPub Note object successfully', :aggregate_failures do + expect(response).to have_http_status(200) + expect(response.headers['Link'].to_s).to include 'activity+json' + expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -199,23 +175,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -224,27 +188,12 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do + it 'renders ActivityPub Note object successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -263,23 +212,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -288,27 +225,12 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do + it 'renders ActivityPub Note object successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -350,23 +272,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -375,27 +285,12 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do + it 'renders ActivityPub Note object successfully' do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -463,23 +358,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -488,25 +371,13 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns Link header' do - expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do - expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - it_behaves_like 'cacheable response' - it 'returns Content-Type header' do + it 'renders ActivityPub Note object successfully', :aggregate_failures do + expect(response).to have_http_status(200) + expect(response.headers['Link'].to_s).to include 'activity+json' + expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -525,23 +396,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -550,27 +409,12 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do + it 'renders ActivityPub Note object successfully' do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -612,23 +456,11 @@ describe StatusesController do context 'with HTML' do let(:format) { 'html' } - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'renders status' do expect(response).to render_template(:show) expect(response.body).to include status.text end @@ -637,27 +469,12 @@ describe StatusesController do context 'with JSON' do let(:format) { 'json' } - it 'returns http success' do + it 'renders ActivityPub Note object', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns private Cache-Control header' do expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' - end - - it 'renders ActivityPub Note object' do json = body_as_json expect(json[:content]).to include status.text end @@ -933,23 +750,11 @@ describe StatusesController do get :embed, params: { account_username: status.account.username, id: status.id } end - it 'returns http success' do + it 'renders status successfully', :aggregate_failures do expect(response).to have_http_status(200) - end - - it 'returns Link header' do expect(response.headers['Link'].to_s).to include 'activity+json' - end - - it 'returns Vary header' do expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie' - end - - it 'returns public Cache-Control header' do expect(response.headers['Cache-Control']).to include 'public' - end - - it 'renders status' do expect(response).to render_template(:embed) expect(response.body).to include status.text end From 602c458ab6773e56e512c032c16fe4c7cddc1c44 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jun 2023 14:52:25 +0200 Subject: [PATCH 23/39] Add finer permission requirements for managing webhooks (#25463) --- app/controllers/admin/webhooks_controller.rb | 3 +++ app/models/webhook.rb | 22 +++++++++++++++++++ app/policies/webhook_policy.rb | 4 ++-- app/views/admin/webhooks/_form.html.haml | 2 +- config/locales/activerecord.en.yml | 4 ++++ .../admin/webhooks_controller_spec.rb | 2 +- spec/policies/webhook_policy_spec.rb | 22 ++++++++++++++++--- 7 files changed, 52 insertions(+), 7 deletions(-) diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index e087476658..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -28,6 +28,7 @@ module Admin authorize :webhook, :create? @webhook = Webhook.new(resource_params) + @webhook.current_account = current_account if @webhook.save redirect_to admin_webhook_path(@webhook) @@ -39,6 +40,8 @@ module Admin def update authorize @webhook, :update? + @webhook.current_account = current_account + if @webhook.update(resource_params) redirect_to admin_webhook_path(@webhook) else diff --git a/app/models/webhook.rb b/app/models/webhook.rb index c46fce743e..14f33c5fc4 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -24,6 +24,8 @@ class Webhook < ApplicationRecord status.updated ).freeze + attr_writer :current_account + scope :enabled, -> { where(enabled: true) } validates :url, presence: true, url: true @@ -31,6 +33,7 @@ class Webhook < ApplicationRecord validates :events, presence: true validate :validate_events + validate :validate_permissions validate :validate_template before_validation :strip_events @@ -48,12 +51,31 @@ class Webhook < ApplicationRecord update!(enabled: false) end + def required_permissions + events.map { |event| Webhook.permission_for_event(event) } + end + + def self.permission_for_event(event) + case event + when 'account.approved', 'account.created', 'account.updated' + :manage_users + when 'report.created' + :manage_reports + when 'status.created', 'status.updated' + :view_devops + end + end + private def validate_events errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) } end + def validate_permissions + errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) } + end + def validate_template return if template.blank? diff --git a/app/policies/webhook_policy.rb b/app/policies/webhook_policy.rb index a2199a333f..577e891b66 100644 --- a/app/policies/webhook_policy.rb +++ b/app/policies/webhook_policy.rb @@ -14,7 +14,7 @@ class WebhookPolicy < ApplicationPolicy end def update? - role.can?(:manage_webhooks) + role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) } end def enable? @@ -30,6 +30,6 @@ class WebhookPolicy < ApplicationPolicy end def destroy? - role.can?(:manage_webhooks) + role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) } end end diff --git a/app/views/admin/webhooks/_form.html.haml b/app/views/admin/webhooks/_form.html.haml index 8d019ff43b..c870e943f4 100644 --- a/app/views/admin/webhooks/_form.html.haml +++ b/app/views/admin/webhooks/_form.html.haml @@ -5,7 +5,7 @@ = f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' } .fields-group - = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', disabled: Webhook::EVENTS.filter { |event| !current_user.role.can?(Webhook.permission_for_event(event)) } .fields-group = f.input :template, wrapper: :with_block_label, input_html: { placeholder: '{ "content": "Hello {{object.username}}" }' } diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 8aee15659f..a53c7c6e9e 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -53,3 +53,7 @@ en: position: elevated: cannot be higher than your current role own_role: cannot be changed with your current role + webhook: + attributes: + events: + invalid_permissions: cannot include events you don't have the rights to diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb index 074956c555..0ccfbbcc6e 100644 --- a/spec/controllers/admin/webhooks_controller_spec.rb +++ b/spec/controllers/admin/webhooks_controller_spec.rb @@ -48,7 +48,7 @@ describe Admin::WebhooksController do end context 'with an existing record' do - let!(:webhook) { Fabricate :webhook } + let!(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) } describe 'GET #show' do it 'returns http success and renders view' do diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb index 1eac8932d4..909311461a 100644 --- a/spec/policies/webhook_policy_spec.rb +++ b/spec/policies/webhook_policy_spec.rb @@ -8,16 +8,32 @@ describe WebhookPolicy do let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } let(:john) { Fabricate(:account) } - permissions :index?, :create?, :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do + permissions :index?, :create? do context 'with an admin' do it 'permits' do - expect(policy).to permit(admin, Tag) + expect(policy).to permit(admin, Webhook) end end context 'with a non-admin' do it 'denies' do - expect(policy).to_not permit(john, Tag) + expect(policy).to_not permit(john, Webhook) + end + end + end + + permissions :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do + let(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) } + + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, webhook) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, webhook) end end end From 38433ccd0bb9a47c9882e64d4644f7c5b47858b3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jun 2023 08:53:13 -0400 Subject: [PATCH 24/39] Reduce `Admin::Reports::Actions` spec db activity (#25465) --- .../admin/reports/actions_controller_spec.rb | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb index 701855f92e..1f3951516d 100644 --- a/spec/controllers/admin/reports/actions_controller_spec.rb +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -62,17 +62,10 @@ describe Admin::Reports::ActionsController do end shared_examples 'common behavior' do - it 'closes the report' do - expect { subject }.to change { report.reload.action_taken? }.from(false).to(true) - end + it 'closes the report and redirects' do + expect { subject }.to mark_report_action_taken.and create_target_account_strike - it 'creates a strike with the expected text' do - expect { subject }.to change { report.target_account.strikes.count }.by(1) expect(report.target_account.strikes.last.text).to eq text - end - - it 'redirects' do - subject expect(response).to redirect_to(admin_reports_path) end @@ -81,20 +74,21 @@ describe Admin::Reports::ActionsController do { report_id: report.id } end - it 'closes the report' do - expect { subject }.to change { report.reload.action_taken? }.from(false).to(true) - end + it 'closes the report and redirects' do + expect { subject }.to mark_report_action_taken.and create_target_account_strike - it 'creates a strike with the expected text' do - expect { subject }.to change { report.target_account.strikes.count }.by(1) expect(report.target_account.strikes.last.text).to eq '' - end - - it 'redirects' do - subject expect(response).to redirect_to(admin_reports_path) end end + + def mark_report_action_taken + change { report.reload.action_taken? }.from(false).to(true) + end + + def create_target_account_strike + change { report.target_account.strikes.count }.by(1) + end end shared_examples 'all action types' do From 05f9e39b32f15d71eb9ec524d1ab871e5c0d03da Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jun 2023 08:55:22 -0400 Subject: [PATCH 25/39] Fix `RSpec/VerifiedDoubles` cop (#25469) --- .rubocop_todo.yml | 39 ------------------- .../admin/change_emails_controller_spec.rb | 3 +- .../admin/confirmations_controller_spec.rb | 2 +- .../admin/disputes/appeals_controller_spec.rb | 6 ++- .../admin/domain_allows_controller_spec.rb | 2 +- .../admin/domain_blocks_controller_spec.rb | 2 +- .../api/v1/reports_controller_spec.rb | 3 +- .../api/web/embeds_controller_spec.rb | 2 +- .../auth/sessions_controller_spec.rb | 3 +- .../authorize_interactions_controller_spec.rb | 10 ++--- .../disputes/appeals_controller_spec.rb | 3 +- spec/helpers/statuses_helper_spec.rb | 34 ++++++++-------- spec/lib/activitypub/activity/add_spec.rb | 2 +- spec/lib/activitypub/activity/move_spec.rb | 2 +- spec/lib/request_spec.rb | 4 +- spec/lib/suspicious_sign_in_detector_spec.rb | 2 +- spec/models/account/field_spec.rb | 6 +-- spec/models/account_migration_spec.rb | 4 +- spec/models/session_activation_spec.rb | 4 +- spec/models/setting_spec.rb | 2 +- spec/services/account_search_service_spec.rb | 4 +- .../bootstrap_timeline_service_spec.rb | 2 +- spec/services/bulk_import_service_spec.rb | 16 ++++---- spec/services/fetch_resource_service_spec.rb | 4 +- spec/services/import_service_spec.rb | 2 +- spec/services/post_status_service_spec.rb | 4 +- spec/services/resolve_url_service_spec.rb | 4 +- spec/services/search_service_spec.rb | 8 ++-- .../unsuspend_account_service_spec.rb | 2 +- .../blacklisted_email_validator_spec.rb | 4 +- .../disallowed_hashtags_validator_spec.rb | 4 +- spec/validators/email_mx_validator_spec.rb | 32 ++++++++------- .../validators/follow_limit_validator_spec.rb | 6 +-- spec/validators/note_length_validator_spec.rb | 12 ++++-- spec/validators/poll_validator_spec.rb | 4 +- .../status_length_validator_spec.rb | 26 ++++++++----- spec/validators/status_pin_validator_spec.rb | 10 ++--- .../unique_username_validator_spec.rb | 20 ++++++---- .../unreserved_username_validator_spec.rb | 4 +- spec/validators/url_validator_spec.rb | 4 +- spec/views/statuses/show.html.haml_spec.rb | 2 +- .../activitypub/processing_worker_spec.rb | 3 +- .../workers/admin/domain_purge_worker_spec.rb | 2 +- spec/workers/domain_block_worker_spec.rb | 2 +- .../workers/domain_clear_media_worker_spec.rb | 2 +- spec/workers/feed_insert_worker_spec.rb | 8 ++-- spec/workers/move_worker_spec.rb | 2 +- ...lish_scheduled_announcement_worker_spec.rb | 2 +- spec/workers/refollow_worker_spec.rb | 2 +- spec/workers/regeneration_worker_spec.rb | 2 +- 50 files changed, 162 insertions(+), 172 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f3b24cdbc4..975c9d28fb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -437,45 +437,6 @@ RSpec/SubjectStub: - 'spec/services/unallow_domain_service_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb' -# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. -RSpec/VerifiedDoubles: - Exclude: - - 'spec/controllers/admin/change_emails_controller_spec.rb' - - 'spec/controllers/admin/confirmations_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/web/embeds_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/disputes/appeals_controller_spec.rb' - - 'spec/helpers/statuses_helper_spec.rb' - - 'spec/lib/suspicious_sign_in_detector_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/services/account_search_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - - 'spec/services/search_service_spec.rb' - - 'spec/validators/blacklisted_email_validator_spec.rb' - - 'spec/validators/disallowed_hashtags_validator_spec.rb' - - 'spec/validators/email_mx_validator_spec.rb' - - 'spec/validators/follow_limit_validator_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - - 'spec/validators/poll_validator_spec.rb' - - 'spec/validators/status_length_validator_spec.rb' - - 'spec/validators/status_pin_validator_spec.rb' - - 'spec/validators/unique_username_validator_spec.rb' - - 'spec/validators/unreserved_username_validator_spec.rb' - - 'spec/validators/url_validator_spec.rb' - - 'spec/views/statuses/show.html.haml_spec.rb' - - 'spec/workers/activitypub/processing_worker_spec.rb' - - 'spec/workers/admin/domain_purge_worker_spec.rb' - - 'spec/workers/domain_block_worker_spec.rb' - - 'spec/workers/domain_clear_media_worker_spec.rb' - - 'spec/workers/feed_insert_worker_spec.rb' - - 'spec/workers/regeneration_worker_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: diff --git a/spec/controllers/admin/change_emails_controller_spec.rb b/spec/controllers/admin/change_emails_controller_spec.rb index 503862a7b9..dd8a764b64 100644 --- a/spec/controllers/admin/change_emails_controller_spec.rb +++ b/spec/controllers/admin/change_emails_controller_spec.rb @@ -23,7 +23,8 @@ RSpec.describe Admin::ChangeEmailsController do describe 'GET #update' do before do - allow(UserMailer).to receive(:confirmation_instructions).and_return(double('email', deliver_later: nil)) + allow(UserMailer).to receive(:confirmation_instructions) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) end it 'returns http success' do diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index 181616a66e..9559160786 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Admin::ConfirmationsController do let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) } before do - allow(UserMailer).to receive(:confirmation_instructions) { double(:email, deliver_later: nil) } + allow(UserMailer).to receive(:confirmation_instructions) { instance_double(ActionMailer::MessageDelivery, deliver_later: nil) } end context 'when email is not confirmed' do diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb index 99b19298c6..3c3f23f529 100644 --- a/spec/controllers/admin/disputes/appeals_controller_spec.rb +++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb @@ -19,7 +19,8 @@ RSpec.describe Admin::Disputes::AppealsController do let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } before do - allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil)) + allow(UserMailer).to receive(:appeal_approved) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :approve, params: { id: appeal.id } end @@ -40,7 +41,8 @@ RSpec.describe Admin::Disputes::AppealsController do let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } before do - allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil)) + allow(UserMailer).to receive(:appeal_rejected) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :reject, params: { id: appeal.id } end diff --git a/spec/controllers/admin/domain_allows_controller_spec.rb b/spec/controllers/admin/domain_allows_controller_spec.rb index 6b0453476a..6f82f322b5 100644 --- a/spec/controllers/admin/domain_allows_controller_spec.rb +++ b/spec/controllers/admin/domain_allows_controller_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Admin::DomainAllowsController do describe 'DELETE #destroy' do it 'disallows the domain' do - service = double(call: true) + service = instance_double(UnallowDomainService, call: true) allow(UnallowDomainService).to receive(:new).and_return(service) domain_allow = Fabricate(:domain_allow) delete :destroy, params: { id: domain_allow.id } diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index d499aa64ce..fb7fb2957f 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -213,7 +213,7 @@ RSpec.describe Admin::DomainBlocksController do describe 'DELETE #destroy' do it 'unblocks the domain' do - service = double(call: true) + service = instance_double(UnblockDomainService, call: true) allow(UnblockDomainService).to receive(:new).and_return(service) domain_block = Fabricate(:domain_block) delete :destroy, params: { id: domain_block.id } diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index 0eb9ce1709..01b7e4a71c 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -23,7 +23,8 @@ RSpec.describe Api::V1::ReportsController do let(:rule_ids) { nil } before do - allow(AdminMailer).to receive(:new_report).and_return(double('email', deliver_later: nil)) + allow(AdminMailer).to receive(:new_report) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :create, params: { status_ids: [status.id], account_id: target_account.id, comment: 'reasons', category: category, rule_ids: rule_ids, forward: forward } end diff --git a/spec/controllers/api/web/embeds_controller_spec.rb b/spec/controllers/api/web/embeds_controller_spec.rb index b0c48a5aed..8c4e1a8f26 100644 --- a/spec/controllers/api/web/embeds_controller_spec.rb +++ b/spec/controllers/api/web/embeds_controller_spec.rb @@ -26,7 +26,7 @@ describe Api::Web::EmbedsController do context 'when fails to find status' do let(:url) { 'https://host.test/oembed.html' } - let(:service_instance) { double('fetch_oembed_service') } + let(:service_instance) { instance_double(FetchOEmbedService) } before do allow(FetchOEmbedService).to receive(:new) { service_instance } diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 5b7d5d5cd4..c727a76333 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -127,7 +127,8 @@ RSpec.describe Auth::SessionsController do before do allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip) - allow(UserMailer).to receive(:suspicious_sign_in).and_return(double('email', deliver_later!: nil)) + allow(UserMailer).to receive(:suspicious_sign_in) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later!: nil)) user.update(current_sign_in_at: 1.month.ago) post :create, params: { user: { email: user.email, password: user.password } } end diff --git a/spec/controllers/authorize_interactions_controller_spec.rb b/spec/controllers/authorize_interactions_controller_spec.rb index e521039410..098c25ba32 100644 --- a/spec/controllers/authorize_interactions_controller_spec.rb +++ b/spec/controllers/authorize_interactions_controller_spec.rb @@ -28,7 +28,7 @@ describe AuthorizeInteractionsController do end it 'renders error when account cant be found' do - service = double + service = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service) allow(service).to receive(:call).with('missing@hostname').and_return(nil) @@ -40,7 +40,7 @@ describe AuthorizeInteractionsController do it 'sets resource from url' do account = Fabricate(:account) - service = double + service = instance_double(ResolveURLService) allow(ResolveURLService).to receive(:new).and_return(service) allow(service).to receive(:call).with('http://example.com').and_return(account) @@ -52,7 +52,7 @@ describe AuthorizeInteractionsController do it 'sets resource from acct uri' do account = Fabricate(:account) - service = double + service = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service) allow(service).to receive(:call).with('found@hostname').and_return(account) @@ -82,7 +82,7 @@ describe AuthorizeInteractionsController do end it 'shows error when account not found' do - service = double + service = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service) allow(service).to receive(:call).with('user@hostname').and_return(nil) @@ -94,7 +94,7 @@ describe AuthorizeInteractionsController do it 'follows account when found' do target_account = Fabricate(:account) - service = double + service = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service) allow(service).to receive(:call).with('user@hostname').and_return(target_account) diff --git a/spec/controllers/disputes/appeals_controller_spec.rb b/spec/controllers/disputes/appeals_controller_spec.rb index d0e1cd3908..a0f9c7b910 100644 --- a/spec/controllers/disputes/appeals_controller_spec.rb +++ b/spec/controllers/disputes/appeals_controller_spec.rb @@ -14,7 +14,8 @@ RSpec.describe Disputes::AppealsController do let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } before do - allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil)) + allow(AdminMailer).to receive(:new_appeal) + .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } } end diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb index 105da7e1b1..b7824ca604 100644 --- a/spec/helpers/statuses_helper_spec.rb +++ b/spec/helpers/statuses_helper_spec.rb @@ -117,42 +117,42 @@ describe StatusesHelper do describe '#style_classes' do it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.style_classes(status, false, false, false) expect(classes).to eq 'entry' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) classes = helper.style_classes(status, false, false, false) expect(classes).to eq 'entry entry-reblog' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.style_classes(status, true, false, false) expect(classes).to eq 'entry entry-predecessor' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.style_classes(status, false, true, false) expect(classes).to eq 'entry entry-successor' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.style_classes(status, false, false, true) expect(classes).to eq 'entry entry-center' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) classes = helper.style_classes(status, true, true, true) expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' @@ -161,35 +161,35 @@ describe StatusesHelper do describe '#microformats_classes' do it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.microformats_classes(status, false, false) expect(classes).to eq '' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.microformats_classes(status, true, false) expect(classes).to eq 'p-in-reply-to' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) classes = helper.microformats_classes(status, false, true) expect(classes).to eq 'p-comment' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) classes = helper.microformats_classes(status, true, false) expect(classes).to eq 'p-in-reply-to p-repost-of' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) classes = helper.microformats_classes(status, true, true) expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' @@ -198,42 +198,42 @@ describe StatusesHelper do describe '#microformats_h_class' do it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) css_class = helper.microformats_h_class(status, false, false, false) expect(css_class).to eq 'h-entry' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) css_class = helper.microformats_h_class(status, false, false, false) expect(css_class).to eq 'h-cite' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) css_class = helper.microformats_h_class(status, true, false, false) expect(css_class).to eq 'h-cite' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) css_class = helper.microformats_h_class(status, false, true, false) expect(css_class).to eq 'h-cite' end it do - status = double(reblog?: false) + status = instance_double(Status, reblog?: false) css_class = helper.microformats_h_class(status, false, false, true) expect(css_class).to eq '' end it do - status = double(reblog?: true) + status = instance_double(Status, reblog?: true) css_class = helper.microformats_h_class(status, true, true, true) expect(css_class).to eq 'h-cite' diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index 9c45e465e4..ec6df01716 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -26,7 +26,7 @@ RSpec.describe ActivityPub::Activity::Add do end context 'when status was not known before' do - let(:service_stub) { double } + let(:service_stub) { instance_double(ActivityPub::FetchRemoteStatusService) } let(:json) do { diff --git a/spec/lib/activitypub/activity/move_spec.rb b/spec/lib/activitypub/activity/move_spec.rb index 8bd23aa7bf..f3973c70ce 100644 --- a/spec/lib/activitypub/activity/move_spec.rb +++ b/spec/lib/activitypub/activity/move_spec.rb @@ -26,7 +26,7 @@ RSpec.describe ActivityPub::Activity::Move do 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 + service_stub = instance_double(ActivityPub::FetchRemoteAccountService) allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub) allow(service_stub).to receive(:call).and_return(returned_account) end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index e88631e475..f0861376b9 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -48,7 +48,7 @@ describe Request do end it 'executes a HTTP request when the first address is private' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844)) allow(resolver).to receive(:timeouts=).and_return(nil) @@ -83,7 +83,7 @@ describe Request do end it 'raises Mastodon::ValidationError' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face)) allow(resolver).to receive(:timeouts=).and_return(nil) diff --git a/spec/lib/suspicious_sign_in_detector_spec.rb b/spec/lib/suspicious_sign_in_detector_spec.rb index c61b1ef1e6..9e64aff08a 100644 --- a/spec/lib/suspicious_sign_in_detector_spec.rb +++ b/spec/lib/suspicious_sign_in_detector_spec.rb @@ -7,7 +7,7 @@ RSpec.describe SuspiciousSignInDetector do subject { described_class.new(user).suspicious?(request) } let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) } - let(:request) { double(remote_ip: remote_ip) } + let(:request) { instance_double(ActionDispatch::Request, remote_ip: remote_ip) } let(:remote_ip) { nil } context 'when user has 2FA enabled' do diff --git a/spec/models/account/field_spec.rb b/spec/models/account/field_spec.rb index 5715a53791..22593bb218 100644 --- a/spec/models/account/field_spec.rb +++ b/spec/models/account/field_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Account::Field do describe '#verified?' do subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) } - let(:account) { double('Account', local?: true) } + let(:account) { instance_double(Account, local?: true) } context 'when verified_at is set' do let(:verified_at) { Time.now.utc.iso8601 } @@ -28,7 +28,7 @@ RSpec.describe Account::Field do describe '#mark_verified!' do subject { described_class.new(account, original_hash) } - let(:account) { double('Account', local?: true) } + let(:account) { instance_double(Account, local?: true) } let(:original_hash) { { 'name' => 'Foo', 'value' => 'Bar' } } before do @@ -47,7 +47,7 @@ RSpec.describe Account::Field do describe '#verifiable?' do subject { described_class.new(account, 'name' => 'Foo', 'value' => value) } - let(:account) { double('Account', local?: local) } + let(:account) { instance_double(Account, local?: local) } context 'with local accounts' do let(:local) { true } diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index d76edddd51..f4544740b1 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -15,7 +15,7 @@ RSpec.describe AccountMigration do before do target_account.aliases.create!(acct: source_account.acct) - service_double = double + service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service_double) allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account) end @@ -29,7 +29,7 @@ RSpec.describe AccountMigration do let(:target_acct) { 'target@remote' } before do - service_double = double + service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(service_double) allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil) end diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb index 052a06e5ca..75842e25ba 100644 --- a/spec/models/session_activation_spec.rb +++ b/spec/models/session_activation_spec.rb @@ -16,7 +16,7 @@ RSpec.describe SessionActivation do allow(session_activation).to receive(:detection).and_return(detection) end - let(:detection) { double(id: 1) } + let(:detection) { instance_double(Browser::Chrome, id: 1) } let(:session_activation) { Fabricate(:session_activation) } it 'returns detection.id' do @@ -30,7 +30,7 @@ RSpec.describe SessionActivation do end let(:session_activation) { Fabricate(:session_activation) } - let(:detection) { double(platform: double(id: 1)) } + let(:detection) { instance_double(Browser::Chrome, platform: instance_double(Browser::Platform, id: 1)) } it 'returns detection.platform.id' do expect(session_activation.platform).to be 1 diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index bba585cec6..5ed5c5d766 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Setting do context 'when RailsSettings::Settings.object returns truthy' do let(:object) { db_val } - let(:db_val) { double(value: 'db_val') } + let(:db_val) { instance_double(described_class, value: 'db_val') } context 'when default_value is a Hash' do let(:default_value) { { default_value: 'default_value' } } diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb index 98264e6e13..1cd036f484 100644 --- a/spec/services/account_search_service_spec.rb +++ b/spec/services/account_search_service_spec.rb @@ -53,7 +53,7 @@ describe AccountSearchService, type: :service do context 'when there is a domain but no exact match' do it 'follows the remote account when resolve is true' do - service = double(call: nil) + service = instance_double(ResolveAccountService, call: nil) allow(ResolveAccountService).to receive(:new).and_return(service) results = subject.call('newuser@remote.com', nil, limit: 10, resolve: true) @@ -61,7 +61,7 @@ describe AccountSearchService, type: :service do end it 'does not follow the remote account when resolve is false' do - service = double(call: nil) + service = instance_double(ResolveAccountService, call: nil) allow(ResolveAccountService).to receive(:new).and_return(service) results = subject.call('newuser@remote.com', nil, limit: 10, resolve: false) diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb index 5a15ba7418..721a0337fd 100644 --- a/spec/services/bootstrap_timeline_service_spec.rb +++ b/spec/services/bootstrap_timeline_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe BootstrapTimelineService, type: :service do subject { described_class.new } context 'when the new user has registered from an invite' do - let(:service) { double } + let(:service) { instance_double(FollowService) } let(:autofollow) { false } let(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) } let(:invite) { Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now, autofollow: autofollow) } diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb index 09dfb0a0b6..281b642ea4 100644 --- a/spec/services/bulk_import_service_spec.rb +++ b/spec/services/bulk_import_service_spec.rb @@ -47,7 +47,7 @@ RSpec.describe BulkImportService do it 'requests to follow all the listed users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -95,7 +95,7 @@ RSpec.describe BulkImportService do it 'requests to follow all the expected users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -133,7 +133,7 @@ RSpec.describe BulkImportService do it 'blocks all the listed users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -177,7 +177,7 @@ RSpec.describe BulkImportService do it 'requests to follow all the expected users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -215,7 +215,7 @@ RSpec.describe BulkImportService do it 'mutes all the listed users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -263,7 +263,7 @@ RSpec.describe BulkImportService do it 'requests to follow all the expected users once the workers have run' do subject.call(import) - resolve_account_service_double = double + resolve_account_service_double = instance_double(ResolveAccountService) allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } @@ -360,7 +360,7 @@ RSpec.describe BulkImportService do it 'updates the bookmarks as expected once the workers have run' do subject.call(import) - service_double = double + service_double = instance_double(ActivityPub::FetchRemoteStatusService) allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } @@ -403,7 +403,7 @@ RSpec.describe BulkImportService do it 'updates the bookmarks as expected once the workers have run' do subject.call(import) - service_double = double + service_double = instance_double(ActivityPub::FetchRemoteStatusService) allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index da7e423517..0f1068471f 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -24,7 +24,7 @@ RSpec.describe FetchResourceService, type: :service do context 'when OpenSSL::SSL::SSLError is raised' do before do - request = double + request = instance_double(Request) allow(Request).to receive(:new).and_return(request) allow(request).to receive(:add_headers) allow(request).to receive(:on_behalf_of) @@ -36,7 +36,7 @@ RSpec.describe FetchResourceService, type: :service do context 'when HTTP::ConnectionError is raised' do before do - request = double + request = instance_double(Request) allow(Request).to receive(:new).and_return(request) allow(request).to receive(:add_headers) allow(request).to receive(:on_behalf_of) diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 32ba4409c3..1904ac8dc9 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -219,7 +219,7 @@ RSpec.describe ImportService, type: :service do end before do - service = double + service = instance_double(ActivityPub::FetchRemoteStatusService) allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service) allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1') diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 76ef5391f0..d201292e17 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -132,7 +132,7 @@ RSpec.describe PostStatusService, type: :service do end it 'processes mentions' do - mention_service = double(:process_mentions_service) + mention_service = instance_double(ProcessMentionsService) allow(mention_service).to receive(:call) allow(ProcessMentionsService).to receive(:new).and_return(mention_service) account = Fabricate(:account) @@ -163,7 +163,7 @@ RSpec.describe PostStatusService, type: :service do end it 'processes hashtags' do - hashtags_service = double(:process_hashtags_service) + hashtags_service = instance_double(ProcessHashtagsService) allow(hashtags_service).to receive(:call) allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service) account = Fabricate(:account) diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 8d2af74173..ad5bebb4ed 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -9,7 +9,7 @@ describe ResolveURLService, type: :service do it 'returns nil when there is no resource url' do url = 'http://example.com/missing-resource' known_account = Fabricate(:account, uri: url) - service = double + service = instance_double(FetchResourceService) allow(FetchResourceService).to receive(:new).and_return service allow(service).to receive(:response_code).and_return(404) @@ -21,7 +21,7 @@ describe ResolveURLService, type: :service do it 'returns known account on temporary error' do url = 'http://example.com/missing-resource' known_account = Fabricate(:account, uri: url) - service = double + service = instance_double(FetchResourceService) allow(FetchResourceService).to receive(:new).and_return service allow(service).to receive(:response_code).and_return(500) diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 00f693dfab..1283a23bf1 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -25,7 +25,7 @@ describe SearchService, type: :service do context 'when it does not find anything' do it 'returns the empty results' do - service = double(call: nil) + service = instance_double(ResolveURLService, call: nil) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10, resolve: true) @@ -37,7 +37,7 @@ describe SearchService, type: :service do context 'when it finds an account' do it 'includes the account in the results' do account = Account.new - service = double(call: account) + service = instance_double(ResolveURLService, call: account) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10, resolve: true) @@ -49,7 +49,7 @@ describe SearchService, type: :service do context 'when it finds a status' do it 'includes the status in the results' do status = Status.new - service = double(call: status) + service = instance_double(ResolveURLService, call: status) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10, resolve: true) @@ -64,7 +64,7 @@ describe SearchService, type: :service do it 'includes the account in the results' do query = 'username' account = Account.new - service = double(call: [account]) + service = instance_double(AccountSearchService, call: [account]) allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb index e02ae41b99..7ef2630aeb 100644 --- a/spec/services/unsuspend_account_service_spec.rb +++ b/spec/services/unsuspend_account_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe UnsuspendAccountService, type: :service do describe 'unsuspending a remote account' do include_examples 'with common context' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } - let!(:resolve_account_service) { double } + let!(:resolve_account_service) { instance_double(ResolveAccountService) } before do allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service) diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb index a642405ae6..3d3d50f659 100644 --- a/spec/validators/blacklisted_email_validator_spec.rb +++ b/spec/validators/blacklisted_email_validator_spec.rb @@ -6,8 +6,8 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do describe '#validate' do subject { described_class.new.validate(user); errors } - let(:user) { double(email: 'info@mail.com', sign_up_ip: '1.2.3.4', errors: errors) } - let(:errors) { double(add: nil) } + let(:user) { instance_double(User, email: 'info@mail.com', sign_up_ip: '1.2.3.4', errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } before do allow(user).to receive(:valid_invitation?).and_return(false) diff --git a/spec/validators/disallowed_hashtags_validator_spec.rb b/spec/validators/disallowed_hashtags_validator_spec.rb index e98db38792..7144d28918 100644 --- a/spec/validators/disallowed_hashtags_validator_spec.rb +++ b/spec/validators/disallowed_hashtags_validator_spec.rb @@ -11,8 +11,8 @@ RSpec.describe DisallowedHashtagsValidator, type: :validator do described_class.new.validate(status) end - let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| "##{x}" }.join(' ')) } - let(:errors) { double(add: nil) } + let(:status) { instance_double(Status, errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| "##{x}" }.join(' ')) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } context 'with a remote reblog' do let(:local) { false } diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb index d9703d81b1..876d73c184 100644 --- a/spec/validators/email_mx_validator_spec.rb +++ b/spec/validators/email_mx_validator_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe EmailMxValidator do describe '#validate' do - let(:user) { double(email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: double(add: nil)) } + let(:user) { instance_double(User, email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: instance_double(ActiveModel::Errors, add: nil)) } context 'with an e-mail domain that is explicitly allowed' do around do |block| @@ -15,7 +15,7 @@ describe EmailMxValidator do end it 'does not add errors if there are no DNS records' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) @@ -29,7 +29,7 @@ describe EmailMxValidator do end it 'adds no error if there are DNS records for the e-mail domain' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([Resolv::DNS::Resource::IN::A.new('192.0.2.42')]) @@ -46,19 +46,19 @@ describe EmailMxValidator do allow(TagManager).to receive(:instance).and_return(double) allow(double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError) - user = double(email: 'foo@example.com', errors: double(add: nil)) + user = instance_double(User, email: 'foo@example.com', errors: instance_double(ActiveModel::Errors, add: nil)) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the domain email portion is blank' do - user = double(email: 'foo@', errors: double(add: nil)) + user = instance_double(User, email: 'foo@', errors: instance_double(ActiveModel::Errors, add: nil)) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the email domain name contains empty labels' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getresources).with('example..com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example..com', Resolv::DNS::Resource::IN::A).and_return([Resolv::DNS::Resource::IN::A.new('192.0.2.42')]) @@ -66,13 +66,13 @@ describe EmailMxValidator do allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) - user = double(email: 'foo@example..com', sign_up_ip: '1.2.3.4', errors: double(add: nil)) + user = instance_double(User, email: 'foo@example..com', sign_up_ip: '1.2.3.4', errors: instance_double(ActiveModel::Errors, add: nil)) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if there are no DNS records for the e-mail domain' do - resolver = double + resolver = instance_double(Resolv::DNS) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) @@ -85,9 +85,11 @@ describe EmailMxValidator do end it 'adds an error if a MX record does not lead to an IP' do - resolver = double + resolver = instance_double(Resolv::DNS) - allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) + allow(resolver).to receive(:getresources) + .with('example.com', Resolv::DNS::Resource::IN::MX) + .and_return([instance_double(Resolv::DNS::Resource::MX, exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([]) @@ -101,13 +103,15 @@ describe EmailMxValidator do it 'adds an error if the MX record is blacklisted' do EmailDomainBlock.create!(domain: 'mail.example.com') - resolver = double + resolver = instance_double(Resolv::DNS) - allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) + allow(resolver).to receive(:getresources) + .with('example.com', Resolv::DNS::Resource::IN::MX) + .and_return([instance_double(Resolv::DNS::Resource::MX, exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) - allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')]) - allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([instance_double(Resolv::DNS::Resource::IN::A, address: '2.3.4.5')]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([instance_double(Resolv::DNS::Resource::IN::A, address: 'fd00::2')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) diff --git a/spec/validators/follow_limit_validator_spec.rb b/spec/validators/follow_limit_validator_spec.rb index 7b9055a27f..86b6511d65 100644 --- a/spec/validators/follow_limit_validator_spec.rb +++ b/spec/validators/follow_limit_validator_spec.rb @@ -12,9 +12,9 @@ RSpec.describe FollowLimitValidator, type: :validator do described_class.new.validate(follow) end - let(:follow) { double(account: account, errors: errors) } - let(:errors) { double(add: nil) } - let(:account) { double(nil?: _nil, local?: local, following_count: 0, followers_count: 0) } + let(:follow) { instance_double(Follow, account: account, errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } + let(:account) { instance_double(Account, nil?: _nil, local?: local, following_count: 0, followers_count: 0) } let(:_nil) { true } let(:local) { false } diff --git a/spec/validators/note_length_validator_spec.rb b/spec/validators/note_length_validator_spec.rb index e45d221d76..66fccad3ec 100644 --- a/spec/validators/note_length_validator_spec.rb +++ b/spec/validators/note_length_validator_spec.rb @@ -8,7 +8,7 @@ describe NoteLengthValidator do describe '#validate' do it 'adds an error when text is over 500 characters' do text = 'a' * 520 - account = double(note: text, errors: double(add: nil)) + account = instance_double(Account, note: text, errors: activemodel_errors) subject.validate_each(account, 'note', text) expect(account.errors).to have_received(:add) @@ -16,7 +16,7 @@ describe NoteLengthValidator do it 'counts URLs as 23 characters flat' do text = ('a' * 476) + " http://#{'b' * 30}.com/example" - account = double(note: text, errors: double(add: nil)) + account = instance_double(Account, note: text, errors: activemodel_errors) subject.validate_each(account, 'note', text) expect(account.errors).to_not have_received(:add) @@ -24,10 +24,16 @@ describe NoteLengthValidator do it 'does not count non-autolinkable URLs as 23 characters flat' do text = ('a' * 476) + "http://#{'b' * 30}.com/example" - account = double(note: text, errors: double(add: nil)) + account = instance_double(Account, note: text, errors: activemodel_errors) subject.validate_each(account, 'note', text) expect(account.errors).to have_received(:add) end + + private + + def activemodel_errors + instance_double(ActiveModel::Errors, add: nil) + end end end diff --git a/spec/validators/poll_validator_spec.rb b/spec/validators/poll_validator_spec.rb index 069a471619..95feb043db 100644 --- a/spec/validators/poll_validator_spec.rb +++ b/spec/validators/poll_validator_spec.rb @@ -9,8 +9,8 @@ RSpec.describe PollValidator, type: :validator do end let(:validator) { described_class.new } - let(:poll) { double(options: options, expires_at: expires_at, errors: errors) } - let(:errors) { double(add: nil) } + let(:poll) { instance_double(Poll, options: options, expires_at: expires_at, errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } let(:options) { %w(foo bar) } let(:expires_at) { 1.day.from_now } diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb index e132b5618a..98ea15e03b 100644 --- a/spec/validators/status_length_validator_spec.rb +++ b/spec/validators/status_length_validator_spec.rb @@ -5,38 +5,38 @@ require 'rails_helper' describe StatusLengthValidator do describe '#validate' do it 'does not add errors onto remote statuses' do - status = double(local?: false) + status = instance_double(Status, local?: false) subject.validate(status) expect(status).to_not receive(:errors) end it 'does not add errors onto local reblogs' do - status = double(local?: false, reblog?: true) + status = instance_double(Status, local?: false, reblog?: true) subject.validate(status) expect(status).to_not receive(:errors) end it 'adds an error when content warning is over 500 characters' do - status = double(spoiler_text: 'a' * 520, text: '', errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: 'a' * 520, text: '', errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'adds an error when text is over 500 characters' do - status = double(spoiler_text: '', text: 'a' * 520, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: 'a' * 520, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'adds an error when text and content warning are over 500 characters total' do - status = double(spoiler_text: 'a' * 250, text: 'b' * 251, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: 'a' * 250, text: 'b' * 251, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'counts URLs as 23 characters flat' do text = ('a' * 476) + " http://#{'b' * 30}.com/example" - status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: text, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to_not have_received(:add) @@ -44,7 +44,7 @@ describe StatusLengthValidator do it 'does not count non-autolinkable URLs as 23 characters flat' do text = ('a' * 476) + "http://#{'b' * 30}.com/example" - status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: text, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) @@ -52,14 +52,14 @@ describe StatusLengthValidator do it 'does not count overly long URLs as 23 characters flat' do text = "http://example.com/valid?#{'#foo?' * 1000}" - status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: text, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'counts only the front part of remote usernames' do text = ('a' * 475) + " @alice@#{'b' * 30}.com" - status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: text, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to_not have_received(:add) @@ -67,10 +67,16 @@ describe StatusLengthValidator do it 'does count both parts of remote usernames for overly long domains' do text = "@alice@#{'b' * 500}.com" - status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) + status = instance_double(Status, spoiler_text: '', text: text, errors: activemodel_errors, local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end end + + private + + def activemodel_errors + instance_double(ActiveModel::Errors, add: nil) + end end diff --git a/spec/validators/status_pin_validator_spec.rb b/spec/validators/status_pin_validator_spec.rb index 00b89d702f..e8f8a45434 100644 --- a/spec/validators/status_pin_validator_spec.rb +++ b/spec/validators/status_pin_validator_spec.rb @@ -8,11 +8,11 @@ RSpec.describe StatusPinValidator, type: :validator do subject.validate(pin) end - let(:pin) { double(account: account, errors: errors, status: status, account_id: pin_account_id) } - let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility, direct_visibility?: visibility == 'direct') } - let(:account) { double(status_pins: status_pins, local?: local) } - let(:status_pins) { double(count: count) } - let(:errors) { double(add: nil) } + let(:pin) { instance_double(StatusPin, account: account, errors: errors, status: status, account_id: pin_account_id) } + let(:status) { instance_double(Status, reblog?: reblog, account_id: status_account_id, visibility: visibility, direct_visibility?: visibility == 'direct') } + let(:account) { instance_double(Account, status_pins: status_pins, local?: local) } + let(:status_pins) { instance_double(Array, count: count) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } let(:pin_account_id) { 1 } let(:status_account_id) { 1 } let(:visibility) { 'public' } diff --git a/spec/validators/unique_username_validator_spec.rb b/spec/validators/unique_username_validator_spec.rb index 6867cbc6ce..0d172c8408 100644 --- a/spec/validators/unique_username_validator_spec.rb +++ b/spec/validators/unique_username_validator_spec.rb @@ -6,7 +6,7 @@ describe UniqueUsernameValidator do describe '#validate' do context 'when local account' do it 'does not add errors if username is nil' do - account = double(username: nil, domain: nil, persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: nil, domain: nil, persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to_not have_received(:add) end @@ -18,14 +18,14 @@ describe UniqueUsernameValidator do it 'adds an error when the username is already used with ignoring cases' do Fabricate(:account, username: 'ABCdef') - account = double(username: 'abcDEF', domain: nil, persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: 'abcDEF', domain: nil, persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to have_received(:add) end it 'does not add errors when same username remote account exists' do Fabricate(:account, username: 'abcdef', domain: 'example.com') - account = double(username: 'abcdef', domain: nil, persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: 'abcdef', domain: nil, persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to_not have_received(:add) end @@ -34,7 +34,7 @@ describe UniqueUsernameValidator do context 'when remote account' do it 'does not add errors if username is nil' do - account = double(username: nil, domain: 'example.com', persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: nil, domain: 'example.com', persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to_not have_received(:add) end @@ -46,23 +46,29 @@ describe UniqueUsernameValidator do it 'adds an error when the username is already used with ignoring cases' do Fabricate(:account, username: 'ABCdef', domain: 'example.com') - account = double(username: 'abcDEF', domain: 'example.com', persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: 'abcDEF', domain: 'example.com', persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to have_received(:add) end it 'adds an error when the domain is already used with ignoring cases' do Fabricate(:account, username: 'ABCdef', domain: 'example.com') - account = double(username: 'ABCdef', domain: 'EXAMPLE.COM', persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: 'ABCdef', domain: 'EXAMPLE.COM', persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to have_received(:add) end it 'does not add errors when account with the same username and another domain exists' do Fabricate(:account, username: 'abcdef', domain: 'example.com') - account = double(username: 'abcdef', domain: 'example2.com', persisted?: false, errors: double(add: nil)) + account = instance_double(Account, username: 'abcdef', domain: 'example2.com', persisted?: false, errors: activemodel_errors) subject.validate(account) expect(account.errors).to_not have_received(:add) end end + + private + + def activemodel_errors + instance_double(ActiveModel::Errors, add: nil) + end end diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index 85bd7dcb6a..6f353eeafd 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -10,8 +10,8 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do end let(:validator) { described_class.new } - let(:account) { double(username: username, errors: errors) } - let(:errors) { double(add: nil) } + let(:account) { instance_double(Account, username: username, errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } context 'when @username is blank?' do let(:username) { nil } diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index a56ccd8e08..f2220e32b0 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -10,8 +10,8 @@ RSpec.describe URLValidator, type: :validator do end let(:validator) { described_class.new(attributes: [attribute]) } - let(:record) { double(errors: errors) } - let(:errors) { double(add: nil) } + let(:record) { instance_double(Webhook, errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } let(:value) { '' } let(:attribute) { :foo } diff --git a/spec/views/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb index 370743dfec..06f5132d9f 100644 --- a/spec/views/statuses/show.html.haml_spec.rb +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe 'statuses/show.html.haml', without_verify_partial_doubles: true do before do - double(api_oembed_url: '') + allow(view).to receive(:api_oembed_url).and_return('') allow(view).to receive(:show_landing_strip?).and_return(true) allow(view).to receive(:site_title).and_return('example site') allow(view).to receive(:site_hostname).and_return('example.com') diff --git a/spec/workers/activitypub/processing_worker_spec.rb b/spec/workers/activitypub/processing_worker_spec.rb index 6b57f16a92..66d1cf4890 100644 --- a/spec/workers/activitypub/processing_worker_spec.rb +++ b/spec/workers/activitypub/processing_worker_spec.rb @@ -9,7 +9,8 @@ describe ActivityPub::ProcessingWorker do describe '#perform' do it 'delegates to ActivityPub::ProcessCollectionService' do - allow(ActivityPub::ProcessCollectionService).to receive(:new).and_return(double(:service, call: nil)) + allow(ActivityPub::ProcessCollectionService).to receive(:new) + .and_return(instance_double(ActivityPub::ProcessCollectionService, call: nil)) subject.perform(account.id, '') expect(ActivityPub::ProcessCollectionService).to have_received(:new) end diff --git a/spec/workers/admin/domain_purge_worker_spec.rb b/spec/workers/admin/domain_purge_worker_spec.rb index b67c58b234..861fd71a7f 100644 --- a/spec/workers/admin/domain_purge_worker_spec.rb +++ b/spec/workers/admin/domain_purge_worker_spec.rb @@ -7,7 +7,7 @@ describe Admin::DomainPurgeWorker do describe 'perform' do it 'calls domain purge service for relevant domain block' do - service = double(call: nil) + service = instance_double(PurgeDomainService, call: nil) allow(PurgeDomainService).to receive(:new).and_return(service) result = subject.perform('example.com') diff --git a/spec/workers/domain_block_worker_spec.rb b/spec/workers/domain_block_worker_spec.rb index 8b98443fa7..33c3ca009a 100644 --- a/spec/workers/domain_block_worker_spec.rb +++ b/spec/workers/domain_block_worker_spec.rb @@ -9,7 +9,7 @@ describe DomainBlockWorker do let(:domain_block) { Fabricate(:domain_block) } it 'calls domain block service for relevant domain block' do - service = double(call: nil) + service = instance_double(BlockDomainService, call: nil) allow(BlockDomainService).to receive(:new).and_return(service) result = subject.perform(domain_block.id) diff --git a/spec/workers/domain_clear_media_worker_spec.rb b/spec/workers/domain_clear_media_worker_spec.rb index f21d1fe189..21f8f87b2f 100644 --- a/spec/workers/domain_clear_media_worker_spec.rb +++ b/spec/workers/domain_clear_media_worker_spec.rb @@ -9,7 +9,7 @@ describe DomainClearMediaWorker do let(:domain_block) { Fabricate(:domain_block, severity: :silence, reject_media: true) } it 'calls domain clear media service for relevant domain block' do - service = double(call: nil) + service = instance_double(ClearDomainMediaService, call: nil) allow(ClearDomainMediaService).to receive(:new).and_return(service) result = subject.perform(domain_block.id) diff --git a/spec/workers/feed_insert_worker_spec.rb b/spec/workers/feed_insert_worker_spec.rb index 16f7d73e02..97c73c5999 100644 --- a/spec/workers/feed_insert_worker_spec.rb +++ b/spec/workers/feed_insert_worker_spec.rb @@ -11,7 +11,7 @@ describe FeedInsertWorker do context 'when there are no records' do it 'skips push with missing status' do - instance = double(push_to_home: nil) + instance = instance_double(FeedManager, push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(nil, follower.id) @@ -20,7 +20,7 @@ describe FeedInsertWorker do end it 'skips push with missing account' do - instance = double(push_to_home: nil) + instance = instance_double(FeedManager, push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, nil) @@ -31,7 +31,7 @@ describe FeedInsertWorker do context 'when there are real records' do it 'skips the push when there is a filter' do - instance = double(push_to_home: nil, filter?: true) + instance = instance_double(FeedManager, push_to_home: nil, filter?: true) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) @@ -40,7 +40,7 @@ describe FeedInsertWorker do end it 'pushes the status onto the home timeline without filter' do - instance = double(push_to_home: nil, filter?: false) + instance = instance_double(FeedManager, push_to_home: nil, filter?: false) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) diff --git a/spec/workers/move_worker_spec.rb b/spec/workers/move_worker_spec.rb index ac7bd506b6..7577f6e896 100644 --- a/spec/workers/move_worker_spec.rb +++ b/spec/workers/move_worker_spec.rb @@ -15,7 +15,7 @@ describe MoveWorker do let!(:account_note) { Fabricate(:account_note, account: local_user.account, target_account: source_account, comment: comment) } let(:list) { Fabricate(:list, account: local_follower) } - let(:block_service) { double } + let(:block_service) { instance_double(BlockService) } before do stub_request(:post, 'https://example.org/a/inbox').to_return(status: 200) diff --git a/spec/workers/publish_scheduled_announcement_worker_spec.rb b/spec/workers/publish_scheduled_announcement_worker_spec.rb index 0977bba1ee..2e50d4a50d 100644 --- a/spec/workers/publish_scheduled_announcement_worker_spec.rb +++ b/spec/workers/publish_scheduled_announcement_worker_spec.rb @@ -12,7 +12,7 @@ describe PublishScheduledAnnouncementWorker do describe 'perform' do before do - service = double + service = instance_double(FetchRemoteStatusService) allow(FetchRemoteStatusService).to receive(:new).and_return(service) allow(service).to receive(:call).with('https://domain.com/users/foo/12345') { remote_status.reload } diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb index 1dac15385b..5718d4db49 100644 --- a/spec/workers/refollow_worker_spec.rb +++ b/spec/workers/refollow_worker_spec.rb @@ -10,7 +10,7 @@ describe RefollowWorker do let(:bob) { Fabricate(:account, domain: nil, username: 'bob') } describe 'perform' do - let(:service) { double } + let(:service) { instance_double(FollowService) } before do allow(FollowService).to receive(:new).and_return(service) diff --git a/spec/workers/regeneration_worker_spec.rb b/spec/workers/regeneration_worker_spec.rb index 147a76be50..37b0a04c49 100644 --- a/spec/workers/regeneration_worker_spec.rb +++ b/spec/workers/regeneration_worker_spec.rb @@ -9,7 +9,7 @@ describe RegenerationWorker do let(:account) { Fabricate(:account) } it 'calls the precompute feed service for the account' do - service = double(call: nil) + service = instance_double(PrecomputeFeedService, call: nil) allow(PrecomputeFeedService).to receive(:new).and_return(service) result = subject.perform(account.id) From a5b6f6da807ee057e3c9747b3b8eebb00f4c4c67 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jun 2023 14:56:14 +0200 Subject: [PATCH 26/39] Change /api/v1/statuses/:id/history to always return at least one item (#25510) --- app/controllers/api/v1/statuses/histories_controller.rb | 6 +++++- .../api/v1/statuses/histories_controller_spec.rb | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index dff2425d06..2913472b04 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController def show cache_if_unauthenticated! - render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer + render json: status_edits, each_serializer: REST::StatusEditSerializer end private + def status_edits + @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + end + def set_status @status = Status.find(params[:status_id]) authorize @status, :show? diff --git a/spec/controllers/api/v1/statuses/histories_controller_spec.rb b/spec/controllers/api/v1/statuses/histories_controller_spec.rb index 00677f1d2c..99384c8ed5 100644 --- a/spec/controllers/api/v1/statuses/histories_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/histories_controller_spec.rb @@ -23,6 +23,7 @@ describe Api::V1::Statuses::HistoriesController do it 'returns http success' do expect(response).to have_http_status(200) + expect(body_as_json.size).to_not be 0 end end end From a8c1c8bd377263677bfb654513a4160caeac77bb Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jun 2023 17:54:43 +0200 Subject: [PATCH 27/39] Fix j/k keyboard shortcuts on some status lists (#25554) --- .../mastodon/features/bookmarked_statuses/index.jsx | 3 ++- app/javascript/mastodon/features/explore/statuses.jsx | 3 ++- .../mastodon/features/favourited_statuses/index.jsx | 3 ++- app/javascript/mastodon/features/pinned_statuses/index.jsx | 4 +++- app/javascript/mastodon/selectors/index.js | 4 ++++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx index 795b859ce4..936dee12e3 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx @@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import ColumnHeader from 'mastodon/components/column_header'; import StatusList from 'mastodon/components/status_list'; import Column from 'mastodon/features/ui/components/column'; +import { getStatusList } from 'mastodon/selectors'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + statusIds: getStatusList(state, 'bookmarks'), isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx index abacf333dd..c90273714a 100644 --- a/app/javascript/mastodon/features/explore/statuses.jsx +++ b/app/javascript/mastodon/features/explore/statuses.jsx @@ -11,9 +11,10 @@ import { debounce } from 'lodash'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends'; import DismissableBanner from 'mastodon/components/dismissable_banner'; import StatusList from 'mastodon/components/status_list'; +import { getStatusList } from 'mastodon/selectors'; const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'trending', 'items']), + statusIds: getStatusList(state, 'trending'), isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'trending', 'next']), }); diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx index 4902ddc28b..abce7ac053 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.jsx +++ b/app/javascript/mastodon/features/favourited_statuses/index.jsx @@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/acti import ColumnHeader from 'mastodon/components/column_header'; import StatusList from 'mastodon/components/status_list'; import Column from 'mastodon/features/ui/components/column'; +import { getStatusList } from 'mastodon/selectors'; const messages = defineMessages({ heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'favourites', 'items']), + statusIds: getStatusList(state, 'favourites'), isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), }); diff --git a/app/javascript/mastodon/features/pinned_statuses/index.jsx b/app/javascript/mastodon/features/pinned_statuses/index.jsx index a93e82cfae..f09d5471e3 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.jsx +++ b/app/javascript/mastodon/features/pinned_statuses/index.jsx @@ -8,6 +8,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; +import { getStatusList } from 'mastodon/selectors'; + import { fetchPinnedStatuses } from '../../actions/pin_statuses'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import StatusList from '../../components/status_list'; @@ -18,7 +20,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'pins', 'items']), + statusIds: getStatusList(state, 'pins'), hasMore: !!state.getIn(['status_lists', 'pins', 'next']), }); diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index b67734316b..f92e7fe48d 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([ ], (hidden, followingOrRequested, isSelf) => { return hidden && !(isSelf || followingOrRequested); }); + +export const getStatusList = createSelector([ + (state, type) => state.getIn(['status_lists', type, 'items']), +], (items) => items.toList()); From c9cd634184e7e983931789598ad0f2c5b9106371 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jun 2023 12:46:32 -0400 Subject: [PATCH 28/39] Use default `bootsnap/setup` in boot.rb (#25502) --- config/boot.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/config/boot.rb b/config/boot.rb index 4e379e7db5..3a1d1d6d24 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -6,12 +6,4 @@ end ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. -require 'bootsnap' # Speed up boot time by caching expensive operations. - -Bootsnap.setup( - cache_dir: File.expand_path('../tmp/cache', __dir__), - development_mode: ENV.fetch('RAILS_ENV', 'development') == 'development', - load_path_cache: true, - compile_cache_iseq: false, - compile_cache_yaml: false -) +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. From 1d622c80332916dbfe51b7c41ce12e5761364703 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jun 2023 18:46:43 +0200 Subject: [PATCH 29/39] Add POST /api/v1/conversations/:id/unread (#25509) --- app/controllers/api/v1/conversations_controller.rb | 5 +++++ config/routes/api.rb | 1 + 2 files changed, 6 insertions(+) diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 63644f85e2..b3ca2f7903 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController render json: @conversation, serializer: REST::ConversationSerializer end + def unread + @conversation.update!(unread: true) + render json: @conversation, serializer: REST::ConversationSerializer + end + def destroy @conversation.destroy! render_empty diff --git a/config/routes/api.rb b/config/routes/api.rb index 19c583b3e1..a10e8058a5 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -81,6 +81,7 @@ namespace :api, format: false do resources :conversations, only: [:index, :destroy] do member do post :read + post :unread end end From 00ec43914aeded13bb369483f795fdb24dfb4b42 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 22 Jun 2023 22:48:40 +0100 Subject: [PATCH 30/39] Add onboarding prompt when home feed too slow in web UI (#25267) --- app/javascript/images/friends-cropped.png | Bin 0 -> 193366 bytes .../features/community_timeline/index.jsx | 5 +- .../mastodon/features/explore/links.jsx | 2 +- .../mastodon/features/explore/statuses.jsx | 2 +- .../mastodon/features/explore/tags.jsx | 2 +- .../components/explore_prompt.jsx | 23 ++++++ .../mastodon/features/home_timeline/index.jsx | 41 +++++++++- .../features/public_timeline/index.jsx | 5 +- app/javascript/mastodon/locales/en.json | 13 ++-- .../styles/mastodon-light/diff.scss | 5 -- .../styles/mastodon/components.scss | 72 ++++++++++++++---- 11 files changed, 131 insertions(+), 39 deletions(-) create mode 100755 app/javascript/images/friends-cropped.png create mode 100644 app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx diff --git a/app/javascript/images/friends-cropped.png b/app/javascript/images/friends-cropped.png new file mode 100755 index 0000000000000000000000000000000000000000..b13e16a5809abad8f2ee906025ed99e4ec79d129 GIT binary patch literal 193366 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�LPz1Hnz8r}E#{LsV*LQyaXPy~tX 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
    r`OwBy;Fnh_QNWR? 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�D&5p+=!u4iqz`q8xp_-w@G5!+u0pG!It9*C=4| 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 literal 0 HcmV?d00001 diff --git a/app/javascript/mastodon/features/community_timeline/index.jsx b/app/javascript/mastodon/features/community_timeline/index.jsx index a18da2f642..7e3b9babe9 100644 --- a/app/javascript/mastodon/features/community_timeline/index.jsx +++ b/app/javascript/mastodon/features/community_timeline/index.jsx @@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent { - - - - } trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx index df91337fdd..49c667f027 100644 --- a/app/javascript/mastodon/features/explore/links.jsx +++ b/app/javascript/mastodon/features/explore/links.jsx @@ -35,7 +35,7 @@ class Links extends PureComponent { const banner = ( - + ); diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx index c90273714a..eb2fe777a6 100644 --- a/app/javascript/mastodon/features/explore/statuses.jsx +++ b/app/javascript/mastodon/features/explore/statuses.jsx @@ -47,7 +47,7 @@ class Statuses extends PureComponent { return ( <> - + - + ); diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx new file mode 100644 index 0000000000..172f1a96c8 --- /dev/null +++ b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import background from 'mastodon/../images/friends-cropped.png'; +import DismissableBanner from 'mastodon/components/dismissable_banner'; + + +export const ExplorePrompt = () => ( + + + +

    +

    + +
    + + +
    +
    +); \ No newline at end of file diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index c9fe078755..f936e8327e 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -5,9 +5,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; -import { Link } from 'react-router-dom'; +import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements'; import { IconWithBadge } from 'mastodon/components/icon_with_badge'; @@ -20,6 +21,7 @@ import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import StatusListContainer from '../ui/containers/status_list_container'; +import { ExplorePrompt } from './components/explore_prompt'; import ColumnSettingsContainer from './containers/column_settings_container'; const messages = defineMessages({ @@ -28,12 +30,36 @@ const messages = defineMessages({ hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, }); +const getHomeFeedSpeed = createSelector([ + state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), + state => state.get('statuses'), +], (statusIds, statusMap) => { + const statuses = statusIds.take(20).map(id => statusMap.get(id)); + const uniqueAccountIds = (new Set(statuses.map(status => status.get('account')).toArray())).size; + const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); + const newest = new Date(statuses.getIn([0, 'created_at'], 0)); + const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds + + return { + unique: uniqueAccountIds, + gap: averageGap, + newest, + }; +}); + +const homeTooSlow = createSelector(getHomeFeedSpeed, speed => + speed.unique < 5 // If there are fewer than 5 different accounts visible + || speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes + || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago +); + const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), showAnnouncements: state.getIn(['announcements', 'show']), + tooSlow: homeTooSlow(state), }); class HomeTimeline extends PureComponent { @@ -52,6 +78,7 @@ class HomeTimeline extends PureComponent { hasAnnouncements: PropTypes.bool, unreadAnnouncements: PropTypes.number, showAnnouncements: PropTypes.bool, + tooSlow: PropTypes.bool, }; handlePin = () => { @@ -121,11 +148,11 @@ class HomeTimeline extends PureComponent { }; render () { - const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; + const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; const { signedIn } = this.context.identity; - let announcementsButton = null; + let announcementsButton, banner; if (hasAnnouncements) { announcementsButton = ( @@ -141,6 +168,10 @@ class HomeTimeline extends PureComponent { ); } + if (tooSlow) { + banner = ; + } + return ( }} />} + emptyMessage={} bindToDocument={!multiColumn} /> ) : } diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index 01b02d4024..d77b76a63e 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -142,11 +142,8 @@ class PublicTimeline extends PureComponent { - - - - } timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9f7ffad66c..fc46f9c5e6 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -197,9 +197,9 @@ "disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.", "dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.", "dismissable_banner.dismiss": "Dismiss", - "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", - "dismissable_banner.explore_statuses": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.", - "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", + "dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.", + "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.", + "dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.", "dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.", "embed.instructions": "Embed this post on your website by copying the code below.", "embed.preview": "Here is what it will look like:", @@ -232,8 +232,7 @@ "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", "empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.", "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", - "empty_column.home.suggestions": "See some suggestions", + "empty_column.home": "Your home timeline is empty! Follow more people to fill it up.", "empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.", "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", @@ -292,9 +291,13 @@ "hashtag.column_settings.tag_toggle": "Include additional tags for this column", "hashtag.follow": "Follow hashtag", "hashtag.unfollow": "Unfollow hashtag", + "home.actions.go_to_explore": "See what's trending", + "home.actions.go_to_suggestions": "Find people to follow", "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:", + "home.explore_prompt.title": "This is your home base within Mastodon.", "home.hide_announcements": "Hide announcements", "home.show_announcements": "Show announcements", "interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.", diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 7498477caa..91828d408a 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -653,11 +653,6 @@ html { border: 1px solid lighten($ui-base-color, 8%); } -.dismissable-banner { - border-left: 1px solid lighten($ui-base-color, 8%); - border-right: 1px solid lighten($ui-base-color, 8%); -} - .status__content, .reply-indicator__content { a { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a9c19a231f..c966eb5ee3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8695,27 +8695,71 @@ noscript { } .dismissable-banner { - background: $ui-base-color; - border-bottom: 1px solid lighten($ui-base-color, 8%); - display: flex; - align-items: center; - gap: 30px; + position: relative; + margin: 10px; + margin-bottom: 5px; + border-radius: 8px; + border: 1px solid $highlight-text-color; + background: rgba($highlight-text-color, 0.15); + padding-inline-end: 45px; + overflow: hidden; + + &__background-image { + width: 125%; + position: absolute; + bottom: -25%; + inset-inline-end: -25%; + z-index: -1; + opacity: 0.15; + mix-blend-mode: luminosity; + } &__message { flex: 1 1 auto; - padding: 20px 15px; - cursor: default; - font-size: 14px; - line-height: 18px; + padding: 15px; + font-size: 15px; + line-height: 22px; + font-weight: 500; color: $primary-text-color; + + p { + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + } + + h1 { + color: $highlight-text-color; + font-size: 22px; + line-height: 33px; + font-weight: 700; + margin-bottom: 15px; + } + + &__actions { + display: flex; + align-items: center; + gap: 4px; + margin-top: 30px; + } + + .button-tertiary { + background: rgba($ui-base-color, 0.15); + backdrop-filter: blur(8px); + } } &__action { - padding: 15px; - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: center; + position: absolute; + inset-inline-end: 0; + top: 0; + padding: 10px; + + .icon-button { + color: $highlight-text-color; + } } } From 0842a68532b1d1f5732e0ba6f2c62b5522114167 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 23 Jun 2023 14:44:54 +0200 Subject: [PATCH 31/39] Remove unique accounts condition from Home onboarding prompt (#25556) --- app/javascript/mastodon/features/home_timeline/index.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index f936e8327e..389efcc875 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -14,6 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container'; +import { me } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { expandHomeTimeline } from '../../actions/timelines'; @@ -34,22 +35,19 @@ const getHomeFeedSpeed = createSelector([ state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), state => state.get('statuses'), ], (statusIds, statusMap) => { - const statuses = statusIds.take(20).map(id => statusMap.get(id)); - const uniqueAccountIds = (new Set(statuses.map(status => status.get('account')).toArray())).size; + const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20); const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); const newest = new Date(statuses.getIn([0, 'created_at'], 0)); const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds return { - unique: uniqueAccountIds, gap: averageGap, newest, }; }); const homeTooSlow = createSelector(getHomeFeedSpeed, speed => - speed.unique < 5 // If there are fewer than 5 different accounts visible - || speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes + speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago ); From a985d587e13494b78ef2879e4d97f78a2df693db Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 23 Jun 2023 16:34:27 +0200 Subject: [PATCH 32/39] Change labels and styles on the onboarding screen in web UI (#25559) --- .../mastodon/components/account.jsx | 14 ++++- .../features/onboarding/components/step.jsx | 10 +-- .../mastodon/features/onboarding/follows.jsx | 24 ++----- .../mastodon/features/onboarding/index.jsx | 14 +++-- .../mastodon/features/onboarding/share.jsx | 4 +- app/javascript/mastodon/locales/en.json | 28 ++++----- .../styles/mastodon/components.scss | 63 ++++++++++++++++--- 7 files changed, 101 insertions(+), 56 deletions(-) diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index 0f3b85388c..dd5aff1d8e 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -49,6 +49,7 @@ class Account extends ImmutablePureComponent { actionTitle: PropTypes.string, defaultAction: PropTypes.string, onActionClick: PropTypes.func, + withBio: PropTypes.bool, }; static defaultProps = { @@ -80,7 +81,7 @@ class Account extends ImmutablePureComponent { }; render () { - const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props; + const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props; if (!account) { return ; @@ -171,6 +172,15 @@ class Account extends ImmutablePureComponent { )} + + {withBio && (account.get('note').length > 0 ? ( +
    + ) : ( +
    + ))}
    ); } diff --git a/app/javascript/mastodon/features/onboarding/components/step.jsx b/app/javascript/mastodon/features/onboarding/components/step.jsx index 0f478f26a3..379f433040 100644 --- a/app/javascript/mastodon/features/onboarding/components/step.jsx +++ b/app/javascript/mastodon/features/onboarding/components/step.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { Check } from 'mastodon/components/check'; import { Icon } from 'mastodon/components/icon'; +import ArrowSmallRight from './arrow_small_right'; + const Step = ({ label, description, icon, completed, onClick, href }) => { const content = ( <> @@ -15,11 +17,9 @@ const Step = ({ label, description, icon, completed, onClick, href }) => {

    {description}

    - {completed && ( -
    - -
    - )} +
    + {completed ? : } +
    ); diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx index 8b4ad0b087..472a87f5ec 100644 --- a/app/javascript/mastodon/features/onboarding/follows.jsx +++ b/app/javascript/mastodon/features/onboarding/follows.jsx @@ -12,20 +12,11 @@ import Column from 'mastodon/components/column'; import ColumnBackButton from 'mastodon/components/column_back_button'; import { EmptyAccount } from 'mastodon/components/empty_account'; import Account from 'mastodon/containers/account_container'; -import { me } from 'mastodon/initial_state'; -import { makeGetAccount } from 'mastodon/selectors'; -import ProgressIndicator from './components/progress_indicator'; - -const mapStateToProps = () => { - const getAccount = makeGetAccount(); - - return state => ({ - account: getAccount(state, me), - suggestions: state.getIn(['suggestions', 'items']), - isLoading: state.getIn(['suggestions', 'isLoading']), - }); -}; +const mapStateToProps = state => ({ + suggestions: state.getIn(['suggestions', 'items']), + isLoading: state.getIn(['suggestions', 'isLoading']), +}); class Follows extends PureComponent { @@ -33,7 +24,6 @@ class Follows extends PureComponent { onBack: PropTypes.func, dispatch: PropTypes.func.isRequired, suggestions: ImmutablePropTypes.list, - account: ImmutablePropTypes.map, isLoading: PropTypes.bool, multiColumn: PropTypes.bool, }; @@ -49,7 +39,7 @@ class Follows extends PureComponent { } render () { - const { onBack, isLoading, suggestions, account, multiColumn } = this.props; + const { onBack, isLoading, suggestions, multiColumn } = this.props; let loadedContent; @@ -58,7 +48,7 @@ class Follows extends PureComponent { } else if (suggestions.isEmpty()) { loadedContent =
    ; } else { - loadedContent = suggestions.map(suggestion => ); + loadedContent = suggestions.map(suggestion => ); } return ( @@ -71,8 +61,6 @@ class Follows extends PureComponent {

    - -
    {loadedContent}
    diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index 79291b3d08..41d499f684 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -18,6 +18,7 @@ import { closeOnboarding } from 'mastodon/actions/onboarding'; import Column from 'mastodon/features/ui/components/column'; import { me } from 'mastodon/initial_state'; import { makeGetAccount } from 'mastodon/selectors'; +import { assetHost } from 'mastodon/utils/config'; import ArrowSmallRight from './components/arrow_small_right'; import Step from './components/step'; @@ -121,21 +122,22 @@ class Onboarding extends ImmutablePureComponent {
    0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> = 7} icon='user-plus' label={} description={} /> - = 1} icon='pencil-square-o' label={} description={} /> + = 1} icon='pencil-square-o' label={} description={ }} />} /> } description={} />
    -

    +

    + - -
    -
    - + + + +
    diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index 6871793026..c5b185a244 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -177,13 +177,13 @@ class Share extends PureComponent {
    + - + -
    diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index fc46f9c5e6..63ab26bc56 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -52,6 +52,7 @@ "account.mute_notifications_short": "Mute notifications", "account.mute_short": "Mute", "account.muted": "Muted", + "account.no_bio": "No description provided.", "account.open_original_page": "Open original page", "account.posts": "Posts", "account.posts_with_replies": "Posts and replies", @@ -452,28 +453,27 @@ "notifications_permission_banner.title": "Never miss a thing", "onboarding.action.back": "Take me back", "onboarding.actions.back": "Take me back", - "onboarding.actions.close": "Don't show this screen again", - "onboarding.actions.go_to_explore": "See what's trending", - "onboarding.actions.go_to_home": "Go to your home feed", + "onboarding.actions.go_to_explore": "Take me to trending", + "onboarding.actions.go_to_home": "Take me to my home feed", "onboarding.compose.template": "Hello #Mastodon!", "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.", - "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", - "onboarding.follows.title": "Popular on Mastodon", + "onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:", + "onboarding.follows.title": "Personalize your home feed", "onboarding.share.lead": "Let people know how they can find you on Mastodon!", "onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}", "onboarding.share.next_steps": "Possible next steps:", "onboarding.share.title": "Share your profile", - "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", - "onboarding.start.skip": "Want to skip right ahead?", + "onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:", + "onboarding.start.skip": "Don't need help getting started?", "onboarding.start.title": "You've made it!", - "onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.", - "onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow", - "onboarding.steps.publish_status.body": "Say hello to the world.", + "onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.", + "onboarding.steps.follow_people.title": "Personalize your home feed", + "onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}", "onboarding.steps.publish_status.title": "Make your first post", - "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", - "onboarding.steps.setup_profile.title": "Customize your profile", - "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!", - "onboarding.steps.share_profile.title": "Share your profile", + "onboarding.steps.setup_profile.body": "Boost your interactions by having a comprehensive profile.", + "onboarding.steps.setup_profile.title": "Personalize your profile", + "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon", + "onboarding.steps.share_profile.title": "Share your Mastodon profile", "onboarding.tips.2fa": "Did you know? You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!", "onboarding.tips.accounts_from_other_servers": "Did you know? Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!", "onboarding.tips.migration": "Did you know? If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c966eb5ee3..81dee20d33 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1514,12 +1514,37 @@ body > [data-popper-placement] { } &__note { + font-size: 14px; + font-weight: 400; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 1; -webkit-box-orient: vertical; - color: $ui-secondary-color; + margin-top: 10px; + color: $darker-text-color; + + &--missing { + color: $dark-text-color; + } + + p { + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: inherit; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } } } @@ -2617,13 +2642,15 @@ $ui-header-height: 55px; .onboarding__link { display: flex; align-items: center; + justify-content: space-between; gap: 10px; color: $highlight-text-color; background: lighten($ui-base-color, 4%); border-radius: 8px; - padding: 10px; + padding: 10px 15px; box-sizing: border-box; - font-size: 17px; + font-size: 14px; + font-weight: 500; height: 56px; text-decoration: none; @@ -2685,6 +2712,7 @@ $ui-header-height: 55px; align-items: center; gap: 10px; padding: 10px; + padding-inline-end: 15px; margin-bottom: 2px; text-decoration: none; text-align: start; @@ -2697,14 +2725,14 @@ $ui-header-height: 55px; &__icon { flex: 0 0 auto; - background: $ui-base-color; border-radius: 50%; display: none; align-items: center; justify-content: center; width: 36px; height: 36px; - color: $dark-text-color; + color: $highlight-text-color; + font-size: 1.2rem; @media screen and (width >= 600px) { display: flex; @@ -2728,16 +2756,33 @@ $ui-header-height: 55px; } } + &__go { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + width: 21px; + height: 21px; + color: $highlight-text-color; + font-size: 17px; + + svg { + height: 1.5em; + width: auto; + } + } + &__description { flex: 1 1 auto; - line-height: 18px; + line-height: 20px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; h6 { - color: $primary-text-color; - font-weight: 700; + color: $highlight-text-color; + font-weight: 500; + font-size: 14px; overflow: hidden; text-overflow: ellipsis; } From 55e7c08a83547424024bac311d5459cb82cf6dae Mon Sep 17 00:00:00 2001 From: Claire Date: Sat, 24 Jun 2023 17:24:31 +0200 Subject: [PATCH 33/39] Fix verified badge in account lists potentially including rel="me" links (#25561) --- .../mastodon/components/verified_badge.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/verified_badge.tsx b/app/javascript/mastodon/components/verified_badge.tsx index 6b421ba42c..9a6adcfa86 100644 --- a/app/javascript/mastodon/components/verified_badge.tsx +++ b/app/javascript/mastodon/components/verified_badge.tsx @@ -1,11 +1,27 @@ import { Icon } from './icon'; +const domParser = new DOMParser(); + +const stripRelMe = (html: string) => { + const document = domParser.parseFromString(html, 'text/html').documentElement; + + document.querySelectorAll('a[rel]').forEach((link) => { + link.rel = link.rel + .split(' ') + .filter((x: string) => x !== 'me') + .join(' '); + }); + + const body = document.querySelector('body'); + return body ? { __html: body.innerHTML } : undefined; +}; + interface Props { link: string; } export const VerifiedBadge: React.FC = ({ link }) => ( - + ); From c71fc42f4ecc41358b7ffccc1c7dadaf7decf518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=84=E3=81=A1=20=E3=81=B2?= Date: Mon, 19 Jun 2023 21:11:46 +0900 Subject: [PATCH 34/39] [Glitch] Rewrite `` as FC and TS Port 804488d38e9942280f7d320af8c7fef7860a4ee5 to glitch-soc Signed-off-by: Claire --- .../glitch/components/autosuggest_hashtag.jsx | 44 ------------------- .../glitch/components/autosuggest_hashtag.tsx | 42 ++++++++++++++++++ .../glitch/components/autosuggest_input.jsx | 4 +- .../components/autosuggest_textarea.jsx | 2 +- 4 files changed, 44 insertions(+), 48 deletions(-) delete mode 100644 app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx create mode 100644 app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx deleted file mode 100644 index 37f7e20f0a..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import ShortNumber from 'flavours/glitch/components/short_number'; - -export default class AutosuggestHashtag extends PureComponent { - - static propTypes = { - tag: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string, - history: PropTypes.array, - }).isRequired, - }; - - render() { - const { tag } = this.props; - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
    -
    - #{tag.name} -
    - {tag.history !== undefined && ( -
    - -
    - )} -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx new file mode 100644 index 0000000000..932370884a --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; + +import ShortNumber from 'flavours/glitch/components/short_number'; + +interface Props { + tag: { + name: string; + url?: string; + history?: Array<{ + uses: number; + accounts: string; + day: string; + }>; + following?: boolean; + type: 'hashtag'; + }; +} + +export const AutosuggestHashtag: React.FC = ({ tag }) => { + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); + + return ( +
    +
    + #{tag.name} +
    + {tag.history !== undefined && ( +
    + +
    + )} +
    + ); +}; diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index d3b7c48ab0..f0833c8c6b 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -8,9 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; - - +import { AutosuggestHashtag } from './autosuggest_hashtag'; const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 86f10651d6..25ca3fefa5 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; From 6fe345c38396a2a1d74df8c3c28d2d24a78b058b Mon Sep 17 00:00:00 2001 From: Ian Date: Wed, 21 Jun 2023 16:58:00 +0100 Subject: [PATCH 35/39] [Glitch] Change emoji picker icon Port 69db507924d6d9350cca8a7127e773d46f9b8f48 to glitch-soc Signed-off-by: Claire --- .../features/compose/components/emoji_picker_dropdown.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index e01be88a2f..c2c8030615 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -391,7 +391,7 @@ class EmojiPickerDropdown extends PureComponent { {button || 🙂} From 222713a768a9aa9894cfceee8c9155d8f7cd5c37 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Thu, 22 Jun 2023 19:10:49 +0900 Subject: [PATCH 36/39] [Glitch] Fix custom signup URL may not loaded Port 8d2c26834f7a485e6fd9083b17b025ad5030e471 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/ui/components/header.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index bbef3a5fb8..873ff20e79 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -8,6 +8,7 @@ import { Link, withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; import { openModal } from 'flavours/glitch/actions/modal'; +import { fetchServer } from 'flavours/glitch/actions/server'; import { Avatar } from 'flavours/glitch/components/avatar'; import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo'; import Permalink from 'flavours/glitch/components/permalink'; @@ -29,6 +30,9 @@ const mapDispatchToProps = (dispatch) => ({ openClosedRegistrationsModal() { dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, + dispatchServer() { + dispatch(fetchServer()); + } }); class Header extends PureComponent { @@ -41,8 +45,14 @@ class Header extends PureComponent { openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, signupUrl: PropTypes.string.isRequired, + dispatchServer: PropTypes.func }; + componentDidMount () { + const { dispatchServer } = this.props; + dispatchServer(); + } + render () { const { signedIn } = this.context.identity; const { location, openClosedRegistrationsModal, signupUrl } = this.props; From 7d160d2272d15833c2f49013b077d7609cf4658b Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jun 2023 17:54:43 +0200 Subject: [PATCH 37/39] [Glitch] Fix j/k keyboard shortcuts on some status lists Port a8c1c8bd377263677bfb654513a4160caeac77bb to glitch-soc Signed-off-by: Claire --- .../glitch/features/pinned_statuses/index.jsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx index add05bdff0..bbb95e5522 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx @@ -8,17 +8,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import StatusList from 'flavours/glitch/components/status_list'; -import Column from 'flavours/glitch/features/ui/components/column'; +import { getStatusList } from 'flavours/glitch/selectors'; + +import { fetchPinnedStatuses } from '../../actions/pin_statuses'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import StatusList from '../../components/status_list'; +import Column from '../ui/components/column'; const messages = defineMessages({ heading: { id: 'column.pins', defaultMessage: 'Pinned post' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'pins', 'items']), + statusIds: getStatusList(state, 'pins'), hasMore: !!state.getIn(['status_lists', 'pins', 'next']), }); From 6fb34258a4ae7e1b8ac252325224c25dd6739dd5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 22 Jun 2023 22:48:40 +0100 Subject: [PATCH 38/39] [Glitch] Add onboarding prompt when home feed too slow in web UI Port 00ec43914aeded13bb369483f795fdb24dfb4b42 to glitch-soc Signed-off-by: Claire --- .../features/bookmarked_statuses/index.jsx | 3 +- .../features/community_timeline/index.jsx | 5 +- .../glitch/features/explore/links.jsx | 2 +- .../glitch/features/explore/statuses.jsx | 5 +- .../flavours/glitch/features/explore/tags.jsx | 2 +- .../features/favourited_statuses/index.jsx | 3 +- .../components/explore_prompt.jsx | 23 ++++++ .../glitch/features/home_timeline/index.jsx | 41 +++++++++-- .../glitch/features/public_timeline/index.jsx | 5 +- .../flavours/glitch/selectors/index.js | 4 ++ .../glitch/styles/components/columns.scss | 72 +++++++++++++++---- .../glitch/styles/mastodon-light/diff.scss | 5 -- 12 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index fe8b883de5..c674c82547 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; import Column from 'flavours/glitch/features/ui/components/column'; +import { getStatusList } from 'flavours/glitch/selectors'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + statusIds: getStatusList(state, 'bookmarks'), isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx index 127e7cf182..ca11adb464 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -142,11 +142,8 @@ class CommunityTimeline extends PureComponent { - - - - } trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} diff --git a/app/javascript/flavours/glitch/features/explore/links.jsx b/app/javascript/flavours/glitch/features/explore/links.jsx index ca7d948625..88976c4ea6 100644 --- a/app/javascript/flavours/glitch/features/explore/links.jsx +++ b/app/javascript/flavours/glitch/features/explore/links.jsx @@ -35,7 +35,7 @@ class Links extends PureComponent { const banner = ( - + ); diff --git a/app/javascript/flavours/glitch/features/explore/statuses.jsx b/app/javascript/flavours/glitch/features/explore/statuses.jsx index 212980c28f..ce484ef77c 100644 --- a/app/javascript/flavours/glitch/features/explore/statuses.jsx +++ b/app/javascript/flavours/glitch/features/explore/statuses.jsx @@ -11,9 +11,10 @@ import { debounce } from 'lodash'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import StatusList from 'flavours/glitch/components/status_list'; +import { getStatusList } from 'flavours/glitch/selectors'; const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'trending', 'items']), + statusIds: getStatusList(state, 'trending'), isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'trending', 'next']), }); @@ -46,7 +47,7 @@ class Statuses extends PureComponent { return ( <> - + - + ); diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx index 08152063c7..ed11f2b8ce 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx @@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glit import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; import Column from 'flavours/glitch/features/ui/components/column'; +import { getStatusList } from 'flavours/glitch/selectors'; const messages = defineMessages({ heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'favourites', 'items']), + statusIds: getStatusList(state, 'favourites'), isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), }); diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx b/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx new file mode 100644 index 0000000000..972dedd3be --- /dev/null +++ b/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import background from 'mastodon/../images/friends-cropped.png'; + + +export const ExplorePrompt = () => ( + + + +

    +

    + +
    + + +
    +
    +); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx index 791c310557..7036857020 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx @@ -5,9 +5,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; -import { Link } from 'react-router-dom'; +import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; @@ -19,6 +20,7 @@ import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_i import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { ExplorePrompt } from './components/explore_prompt'; import ColumnSettingsContainer from './containers/column_settings_container'; @@ -28,12 +30,36 @@ const messages = defineMessages({ hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, }); +const getHomeFeedSpeed = createSelector([ + state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), + state => state.get('statuses'), +], (statusIds, statusMap) => { + const statuses = statusIds.take(20).map(id => statusMap.get(id)); + const uniqueAccountIds = (new Set(statuses.map(status => status.get('account')).toArray())).size; + const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); + const newest = new Date(statuses.getIn([0, 'created_at'], 0)); + const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds + + return { + unique: uniqueAccountIds, + gap: averageGap, + newest, + }; +}); + +const homeTooSlow = createSelector(getHomeFeedSpeed, speed => + speed.unique < 5 // If there are fewer than 5 different accounts visible + || speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes + || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago +); + const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), showAnnouncements: state.getIn(['announcements', 'show']), + tooSlow: homeTooSlow(state), regex: state.getIn(['settings', 'home', 'regex', 'body']), }); @@ -53,6 +79,7 @@ class HomeTimeline extends PureComponent { hasAnnouncements: PropTypes.bool, unreadAnnouncements: PropTypes.number, showAnnouncements: PropTypes.bool, + tooSlow: PropTypes.bool, regex: PropTypes.string, }; @@ -123,11 +150,11 @@ class HomeTimeline extends PureComponent { }; render () { - const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; + const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; const { signedIn } = this.context.identity; - let announcementsButton = null; + let announcementsButton, banner; if (hasAnnouncements) { announcementsButton = ( @@ -142,6 +169,10 @@ class HomeTimeline extends PureComponent { ); } + if (tooSlow) { + banner = ; + } + return ( }} />} + emptyMessage={} bindToDocument={!multiColumn} regex={this.props.regex} /> diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx index 5bbbea0662..4e4b350f8b 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx @@ -146,11 +146,8 @@ class PublicTimeline extends PureComponent { - - - - } timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index e02bfab5db..a296ef8ede 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([ ], (hidden, followingOrRequested, isSelf) => { return hidden && !(isSelf || followingOrRequested); }); + +export const getStatusList = createSelector([ + (state, type) => state.getIn(['status_lists', type, 'items']), +], (items) => items.toList()); diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 533c5eda0f..97c8a84d69 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -960,26 +960,70 @@ $ui-header-height: 55px; } .dismissable-banner { - background: $ui-base-color; - border-bottom: 1px solid lighten($ui-base-color, 8%); - display: flex; - align-items: center; - gap: 30px; + position: relative; + margin: 10px; + margin-bottom: 5px; + border-radius: 8px; + border: 1px solid $highlight-text-color; + background: rgba($highlight-text-color, 0.15); + padding-inline-end: 45px; + overflow: hidden; + + &__background-image { + width: 125%; + position: absolute; + bottom: -25%; + inset-inline-end: -25%; + z-index: -1; + opacity: 0.15; + mix-blend-mode: luminosity; + } &__message { flex: 1 1 auto; - padding: 20px 15px; - cursor: default; - font-size: 14px; - line-height: 18px; + padding: 15px; + font-size: 15px; + line-height: 22px; + font-weight: 500; color: $primary-text-color; + + p { + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + } + + h1 { + color: $highlight-text-color; + font-size: 22px; + line-height: 33px; + font-weight: 700; + margin-bottom: 15px; + } + + &__actions { + display: flex; + align-items: center; + gap: 4px; + margin-top: 30px; + } + + .button-tertiary { + background: rgba($ui-base-color, 0.15); + backdrop-filter: blur(8px); + } } &__action { - padding: 15px; - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: center; + position: absolute; + inset-inline-end: 0; + top: 0; + padding: 10px; + + .icon-button { + color: $highlight-text-color; + } } } diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 2294f2d7c2..cfcdd742e1 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -653,11 +653,6 @@ html { border: 1px solid lighten($ui-base-color, 8%); } -.dismissable-banner { - border-left: 1px solid lighten($ui-base-color, 8%); - border-right: 1px solid lighten($ui-base-color, 8%); -} - .status__content, .reply-indicator__content { a { From 5def74a4366891944ed4493d99d31207a8eff3da Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 23 Jun 2023 14:44:54 +0200 Subject: [PATCH 39/39] [Glitch] Remove unique accounts condition from Home onboarding prompt Port 0842a68532b1d1f5732e0ba6f2c62b5522114167 to glitch-soc Signed-off-by: Claire --- .../glitch/features/home_timeline/index.jsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx index 7036857020..b22f2d886b 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx @@ -11,19 +11,20 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; -import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge'; import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { me } from 'flavours/glitch/initial_state'; + +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { expandHomeTimeline } from '../../actions/timelines'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import StatusListContainer from '../ui/containers/status_list_container'; import { ExplorePrompt } from './components/explore_prompt'; import ColumnSettingsContainer from './containers/column_settings_container'; - const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' }, @@ -34,22 +35,19 @@ const getHomeFeedSpeed = createSelector([ state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), state => state.get('statuses'), ], (statusIds, statusMap) => { - const statuses = statusIds.take(20).map(id => statusMap.get(id)); - const uniqueAccountIds = (new Set(statuses.map(status => status.get('account')).toArray())).size; + const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20); const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); const newest = new Date(statuses.getIn([0, 'created_at'], 0)); const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds return { - unique: uniqueAccountIds, gap: averageGap, newest, }; }); const homeTooSlow = createSelector(getHomeFeedSpeed, speed => - speed.unique < 5 // If there are fewer than 5 different accounts visible - || speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes + speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago );