From 6ac271c2a008a1d1d865918ffd5c95daee737b63 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Thu, 22 Jun 2023 06:49:35 -0300 Subject: [PATCH 01/58] 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 02/58] 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 03/58] 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 04/58] 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 05/58] 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 06/58] 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 07/58] 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 08/58] 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 09/58] 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 10/58] 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 11/58] 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 12/58] 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 zcmeFYg;!K>)F^yr7?`1l9$@H{P`YzKKt(`FX=$V-L>dWcM8rVSLTRLXKw6MNq`SMj z@8S2p_gm}xA1*9ran5tj-uv0l?!6~kM@yZQh=B+I0Ma`est*AG9|i!>5d;BvXXb;z zJODr<992|w?x?7+x<7TZb9A-^0N$j)BzcWrO6XP#-Eww8GFl3c4jr>HGQL@(Tq79q z*8*H_Sm##@+!wYx9v;-D#AHDN227@D-rnvsuqJY(Asx?cx*I#ecd~Ei!`m->pKW;@ zks()YzonW{C=Cd3(8tDnSkGPdzfDd+ftBLsvRmExXT`wOJ|r&}P*1#<|7YnE4dJpE z!E#WlzjrliVQeY=`1a59OsS5T*O&k;xyLyCXqM`mwdZZs)5TcPFf|!zZ8Rq>v5P>Z zFh2v|4vB}x1Mx08k(>N!l~Q>Nd`^?ejkwHygNLR$(l1bNk?K%(8@&e@dfMi)Ctu!c z{0%A#3m)F#>EKGvgQ+Fp>t$h{o=*pF*P4hb$}e!Ad{#P&bqS7xHkf5SDBaky9&=3t zChe#1I-T%)TW%)r8{Z>lf?SMYTfOfkABFs8vRuCNj-)fGPAnpT*;nziTNf0sQfVr6 z%*2x=hg>PO#<&4OMiyEEwJmz)`^?SX?JC3Z@Cff}))*U zoMiA0p}WQ-PXHjMy8eRzsc9$xUD4{ z>b)rhz%C(mC?FKzm1Yb0U<1G#>x%%dxSat&K^DkYk^*fvkPHIOBn7nCP|#}xEI^30 zVhC4MvSfKOUBBYlR`ma^y&wdpt4M$sIv@gU3l$)B z2?ey+X+e-eZ1_MzIRIeTu94{h)9;A@R_g18=iovnFzu&`xhAR%z?M(~O1#&YUK0ao zMhJEzD*T$HTYx(n283LLeviNd+U00Jnh1b|2LSMngA$~Idkm1`h5*x6FpvuDJR{J~ z3IL{YO4rEZ@TTA60U^Kt2bm{;wg&)rVdyooPh=r;pclNU#(Irx9O5nj^80^~-H`(l zSOB036$&Eb-UqP4cmP1hfrNtKnE@#^;D3?5U&ouS0(bsD$f$w=G%-l(JqkDkV1)wH z7v%rD6!HQA1-71pPku%sAb<|w0Za#iCvQnG3zGVpof@dC1euYq1d^gesfrhJBnHA6 zhzbLM;V55VTY&%oiW#qoA*P@NCTl@JB5Y((z*6KB6njPk07`b(dnZw|1bj0=j(mS1 zg#bbz2vT6{KKQIY1&GuE=z~mOfIOLowI~C?6V(vFmj!%Qiqr!MgyuttN@=>ybR zfHw#rlXQSJ1At%wwm@Y`Rt-7A!=lt|D@zGieHcF~Jy1o49N_~YogCORz;c`~;eC!o z^CtlWJFo?AmFjND4!Mwli_*AC0V3LfEdjg)foO&Zleh7%s?LW^M7>nwcpc&a-lftG zDIsh7>O^ri%^Cj7jzSY_1P9^rVo~t92hU-OmNz~r`LYG+I2MiFLjDea0d&YTwR2)q8xs)uW$RDf`>{`dI$>S&-DXzM$4c0lT6g_!#bD=<$496#yCi3qVqD`-ihNYlvSkR_$F9UN#WN2a-b~lwYSXB{g98aKP z_4p!wAZRY-u&5U=&Zc@OUTwp(9_90ik%b~H(?F2Vg?z9LlF=r3tl%-F ztdd5kHmCd}4`0_6`U7%Z9wlj+Io9Dq@#+|^q?XU^eEnJ+_*#IFIrg%!`94K)AE0rL z$BJh_C84&V)S&>IBL?1Yhj~B`E0xEQkmz7wJY*pJKvxn;>4rUq0^FCDaUmC`JUQxE zHRv@?Dhk3i6_7n9z;p)!Xd@V}%}i&2Ji34b2W*sskx~euLI6u448##uxv|I!Lw}x< z_b{+{4++;4#NI*Y&ZUMA{GdlXzY6of2MWpYEak9FXG4w@uYhG&*Vt#dtRgjdN$%dO z_?XE`OH)Y58We(pyB{`qKZJ1tGISoj6H!sCb9!@KM)C-u44{gV3N1u1Tr}wE39fgA zb-+uJ$N?;0^Deml%vuiOPRgya?q6(CFc*&O;8QxGIL#tz&JI|O4tGU_wmQ812&ZR@w(Lo#8`6ZZ|U$LcQXKMLI9Q&6-aVu zT{=z)Aqxu}sT_W9F*lk(BV6 zn{gaKmnCI;5dK-Qk5*y`47(Z?3`9lrczCjMk~N#5)PXN;EeU}{hZf#^5lZY?!iZpC z^)|0FNvMuboRd%_AVo=~aL6?ryg&h&2s?&jHN%@v|1lt%RSasm1kMUL%|0OG)YE+EgJ=O*kF{9HI;J}aZl03_#}Ee}G=12-4@K8LyalV4Q$LiNUJ*e)E?b_1mdd};j>ae*(naqEK$D@C!$Y-T+o zz$hVvmxq1kVLtO4(mAR)7U1FI*S{KGFdnAlv%NwXF@x5mMd5wCufr|&8An%Rd62)Q z!a)G)MKtn^P-{|KBvOfe`Yue#6M2?J^eMYMDtz<5hdhOS0I`Tp$}_mun6{P?>GD28 z0}vxK&kO_Uf2jFwSkk>q!Ad-<4iOU1aS9orAc~19I?KO+o5ugbA&Kl?E=Q)9K=lozj5OKLC&)jo#}Lyq4YHBLXc9J@OCDqb4O)^xM~PK>inX-n zsAUbEGV-KiIq1y8-2Y@Cx&%)Ol}ubAd8SGmuH;lL45hq^)72j5nC~o@tkTdBBhPlf zAqm6GW-<9@hL+%g(QB5KNvYy`jxmB0ni~cfH5y}BLdnb3RA(RWMTO@`U+YOhn?G?J z3wGiCzvA=}7M~JXSdFcCmI*uSPEpO1YY*{xfRZda@_>W#ru$QncH{UXYLsFKvGB=b z#?o8)Y6!7RMsUmSUk6^Wl#u^qXJGiXk@QMR11(180S5tdy=N_P8KE;^gQfmn0WDTl z*28^{aPZqn9>Q`g*}q=YZ<*rPdDLp)%-QPB)zuL3P$m@JPpYG^t$5FH$H zh9n{k&wXl)wi#eei}!#Tg>*8GC2mZSV6udxLU#G%gTob1y1%72YJY-o!$mwjdA+X1 zpet4iVQ-Cwa6f+7UST~Z`2smCm!qo0L!dO9DL#(eu~9WDOSl$%D4U0lIjA9wa|j`1 zGDQXBSOar7c2SxQCUXP&bhO9=5%A}>s@UMJ&hoy72CE+214qPPF=4NG_Gl|r-04NO z&UbypgMj@$^~6b!b|!qXx8I)$K)#bF(^b%q)f@P~Y<yh=WL7UID3}@5`bpAlDo8D7 zYK!iN-d#;i&8VH7o%QkY@k=k6iwzr*lM_a_x*aLF^1tZb2bMGn63Y6Tyc1PD1qq?y zN@w5EkauuwwDgROs$$jifLdYH%_|n!3c{t^&3}8jcv_nlrl|Bt!@o{7@wGPn-SQS` z2R~~iT9wU)yc65|SN2Yf0~Ly*RN2`nqoW1~4f+NKRus3p_|spKHJ%nKv2F{p!IY6b z-TcvESP@v$C^@V9F9{x)#1+0pdFR`^;|xG_kkZ8#sbnjLN4Em+g0zNu!qE8O zXhZLZUt?Mk%aMS_v8cNVnarHGD*4Y_J-&+7Y_vBM!`~qhUl+efRgK0c_M2({$S`T5QhC>va@SDs$iUGRnF0UiuQR1kN~ zIv(wx{G=hoS1WbTNzW+ z>y|S7Z-N=evv?98F110oXurB3VPRo5^XCgRNj-LZ7Z(>-67upF4I5*X+5I6 zFuuAKWwVd_P(=kF0VT8``!3AO81Q;}F3NsLah%rqJ!(19Vfihn>~HWqbjV2Cn*2Ab zmjcX&t{xpdDNXkX&XYuv-{%tTGrhUAvbeQ@ova&M^}<@n@iAkS zwMi*ou?!}wBn@K9Be=|8rPw(OC|^R@9fobJK>6rvs`cCS$VV_iOJU zC=^c;$xkmXi#U@zS=Ps^JqNSwZ?}ZfqYQOY+tyxwt`ub}meB_-I4(ddI`}|M(TZjL zuY;7=Lm@9?PEKT(%7T>9Jrp(OQp(60b!}a!H`mwEhrV(Uw(z&V7q~NgXQRWOjeDs7 z`C_lu0+}O1ZjxDP0_hFu((=CEeBnwXV9beYTQXM%U5T=OJWPeB1;cY2GXPpFT+q$f zFj|i3kP>{ue%}fAaS3jiLE97T;tw=LpBQu}mfi;hDk;|4y6o9qp|7MI6kXMIl;B=j%Wcj#8MQBBZ$A-Qd2s>mY8(oud%%tBTh`Uww zqHQgLqvBQ`^fw1gLaP<=9RuWZT~b^Eo&!|G`os}@>KFs^(R%|@9XDF-I3`1O@Z10T z7U%?>{p54!8UX_|9O@?=`(!6^+df}&lxa+te0yO;v%C$Xk6{bN`GJa#g!i=AlcTKN z(eXosww1To+_^B87kV)vB5FV?0(~eN>+Cs#5OVHC+J@+ykQFV?Cgc};W`?uWn>;W<`!kqoob+s{>|5& z(X4-mT(FzB5&7g4#i2_o=d!mCJ1T|OGqke;@0?_Q9-DhyuA{*zM(cs|<7-pax*@^X zM-V5krer0mw*Sb|TNlsrT2pb#=USrSzmJDG@!l(PhjY zI87L%&|nrwB5b8nFb}HD0UK01T!VH|f?oP6pDd_F-u@>Cr;I!*mw`~92sC?6(&E=! zyIl;KBmMi=SN@gX7|6&_Fo+!pDQ5$Wf5j}TFRK*f$Gp@ciR4qmK(nRw62kHs%E&#C zmUY?O3`O`4T}?34P>iLwDpo%uOa(9N(F~a6=6zryO{XR{j17l1w^6_gl+pkD3VZwz zWTk>%_!ZKKm|i6-pF(Hg*k0|}Q)U0-vnHm5u$wiT8ISsKge5v(HNIn5P2<8a9V}oR zIyn2rN|fRWwkpg;%pC8_NMSe@U!+ax5vUZ!-BU%yL^@v)4qnH)(x=sIi=CdGUIubkQKfp8(Ltj_7CLDmw&WpNWWnJI z|LzX@I(P}_2~u}8#ONG?YA!}M#Q5Ky*Q?{&_99)z{&k}?5cw>iyw$R2nc!uS>|FDy zlbST1B!cfYNT;XXbu#!LHiVevdXo@CFZ~OY!-f+N>}}8uwT%ga2$&B%kOF9-Dh86NOHMen<~YDQ}LYuju(o zoavZQ)?UP44WCvF2PV5a>6Ma^;%wIQG!(p&A4&Hg1PJ2A`ZK5ZG^`Qcnu$T zDyfWycwGT}?w=vJnB%2y+v?-GVT>xDQnHIjKntCp7tF!VQCCp`n~VFt4*I-aR-A8yLC46bf)--|BRN%iAu_h`SBe$F)epU8 zYlK06Kv#7G)`f!Dl7whQ1-B@mS{-L_F*?JVoO)c~i|<~`lRd3hR;gP#FRgPkQbg~8 z@xj)HzvHfKqFjo_?k)FvL~k4)Al34Zf5z zO1?EwBic7c8CW|tcM^hR=ML|%BaV zRA@d?*;_Ly{w(J;B&Z&{$`GNWu|a)G&Ke8+dlJ>^@n2%mdn#lJm5+V&;PQI6%%Nq4-0 z54XXF`?H2;Oas-A4{;?9aU~B{yDcNhs+uuQM%0Qa1^EMNUDN4zTKIjUu*$}TgKW`S z*`ggk`O23n*`#Zcrw4D}M)~ffaE$cz%@uv|tR~-3@Rwb(>Bv=D8)1U-2wZ-0QHN1CXCQfh@hQ>cW%d7Jd8|F@vR?M8YSjNug^|&w zk&P;+z_adrF3PvE3S%^c_Y^Z}=PXz3-|Kt0pDdM5gu3qp=Z|yG$Ax{Jn$n8`KcADv zge|#DO^@I*P#}qJn9r{wvLuN zHS8w4JGJC}87Tl{=QW3Phq{;v)Vkqq|rL6k!~ zNK!sQ89(UJ0R5O?9|FbZjEnWC=-)qTMs?Z-!tL_eoo(>5|_ZJmY+Ny$k_5RFs8es9eMVmdqTfUN0XSsAIO16dP%)wg%Xa$3ux z_(ps|+Ou9c4{3=VeuGwibz#V8fN%T~=`cN|!wHqS0pudD^5cSh)Ej1^JQX2F_k@xv zIaqUPNoF zuPm(}G}YFp8ZE%TVD9wJ^93W3gT%&@lwjE>t9#yEH(D`BIHb*9MeNj2%6e;;6F$NJT`c~PRprv-W0t;xC-ZAEG z+r)0Q3N;M{2Qm{|XZ?eNgH1gjCC_Gej$=@~Ei3vjYJKG86-8x_jAY)OZKrFmj~t|{ zXLOnD21JJ$u#>$I<|FK}4SpABaO<-*YaZ0_=bB9DSa>NW%PbF4kN_40ZF@Ugw(Zrn zoy4}KSKjBl{oh%D3)kO14q|sd&`|ayME-2YJYvtD0F8+TI$teG%6Gx3_po6r=H4F? z_`V|9ON^oGXxR76zU+CY`qNs>u62|=i%Ww{IAK2eKn(6(%Y^c4=vH2q}!@<@|N{k=X zg(k9%3Rfe*3|v>hwC}0Lq&+m14R)*aSrU{ijE{JcNwj?WCp7eOxT9l~OOo;iPiAjy zbTna!|50Iewau#HQfakf)lRMA2tOH1`Ia~68~7>UO;Y9W7q=VR8=izSYEf{5<(X)( z28D#!66&k9H`;x+C;+QJFK-*XR1vkxj>`tm#e%i$tR}3bX_Yf3>}s!m>u&zq>hb-? z-lJ$$_P(0udv@CQ?%f;FjW7(b;Ypr73G7`+K+8*{{#j5s***SO&(g4FeztYoa--FN zgA7A+H&MSQJNUQbzj)A;o5moA=jZO#F%*v=mIna`z4KD`Q;KuTGGnM=X%lYQh}VR- zOqOz#9x+klr}I{-qer9bbojm-k$F|nd{@>~uz%fG+qIg5wZQcB^t=o{Q$COdeSHr~ zjy~8gKHS@z`k1C2d^#8$?A}ma(J|`n-|`;x8|lc-RRTT3zy>V0v8oL_BMN z0=JoEt9tIleXbuH1XJwzX1xCT25lEL(MDXj8a^h)5Ilv2#~vs->oxA)oWfc=6#>-o zFCAX5i_?R(Ux9&v%}aX&N9Arl269L1rPY0N?d0FCt-icM&{fXnrSij$DA0z11f*yv zng`OJlX4sVbd%Mhkf38LZfdVMF0*9MY6>3ucC5ZHYA9jfeYkgajB#r@dnWbQR49L+ z*@=_-t$#0Q?%%md^5f0yS_ga%13C+8JppQ)OdyOxL>ES$! zWyw=b^D&?UP!@~|+iIMhP0&zkY~4Dt=pPuNUteDSE=xefrpilAIUz6KuoE5q2a~-$ z*yQV6iJYamk=Xxr@BLkmVlQ(}PqaWfRTm@tUSdhWyO8y_!IFGkmoWVcMpu;FBFk|+ zyDo{BE~1X}=_&D#k#Q7MKRP6r0P8o$GSgcnwM6S0`zr5tv6>+*L&+zd$dvHhB38YH ze^v%$HBT0%GaJ?xmk-4Q8^4_eWV%M*_OEmvS#@)9pqRQoh|!ViX&6f6(Je-#!-0DI_Xm2Scnid-d}t z+yDdpRT|}Gl>gJGR;pq$)G}<8qcn&Onfl>`>pXYQj*}pw>Bu~|AgJfCXDPz#qPIC4 z_nc9}8Rzfk$6(ClyP9Xr%|XVZAaJ-bCYjl@(BKnbcj&owxhj8_BD+Wl-%vt_$2ikh zrhZD?u=$e+)?=Gk{ zoL(wylwc+#DB6Br@8;^_LV9p?MBF$vb^b-8|5|M1>+AhQQjAS_sc#1M;IPbWEz2=m z=eaFS#)XnbGL^68NuKs7V{=QKMvn)mpKDfbI6IMztFaOoGveu!hZ>l(;0XUqPPGz( zVT!|dB2psN3b>u^xU$pp5C!JS%aH?Nt`^eYrw<4eefmGXpetl~Zid~u`lTV|wfOD+ zM?5n#v*xt}hTR-GL&8nJ(;xr-b+6BPuI#HfoXu~_Tb#{YjKnm~|4RuQqltNxkA}UW zZ&WKn+CaJ1Bs_NyppR%A&;!@~l7vQUD?krS;V&45?H+|~imWfi`iffXMp_!JT$rx> zX-~mC7Ud`0FuiSbda7?K&pT%pJJIme{UklcmM|=K>e;&pXw6mHYd}A0B4pE97znXbSIQw!`Y@ZB{44W8=pVsxB)hGt z+3@-k&___n1Rk*R!wa}p4hXJhC$=0$#)tM8A!b*q{{ANIAhYC?G*lf@OIYrm1ejl! zLnmsVaV@?6ab~}?r1*T#;(VjjTOKjQ_~w^Omz*w?P0w%-C9xaT^F_67W8ohdYKbM# z3+sGC^3FX`0t>L+c~^Sm!7cLA(l^+hr|SKsK^KNB+-)80jjvb01& zH)&(jD_zDal-QGy%!%!98sBMhx9*)~{AQB&a~4bK3^@OwqTYD)p=eTk3>QWLUr<8B zzM$IG?kl`+>8PrY3T?B}wiy#~HNJm%P~j?LZCd8=b|GmQfA5lopf ze$QEcuf2CRTkFQO><}HuM@=*#B`1ewL8F~d3!C#gm-c3dORTN7ChhH(mimvhxOFQh zP9ttL{N2X`<0ok_b{T8?NkK)!Ei2-Bi^Do=tcVUwglB}g2t!>>NsZiK`3qu}AG9EE zr7tPYaJ>4>*(BMx1PupUFt=scA2nr$3zCEdANX&rp*J_{xU$w*9NpZu#_OJ!N;1SV z1m$i&6%`eo{UBf4&(p?Z{-M_{_=xTni|=tl?dBc&-D$o^WwxlWLw2%eiq!fah-5bU z%6DeMuk=IXIba5M1_(WIsBt+F4(xuKoP&*&KP6l$V)^?SWg^r!6>0JJXJf1l3M2&_ z_wBgs2%4W3^$PS%zPCBC-*^K02>Z&BP+TG7>yD?QuU&YqYCAo)@gz4$4xg2!&+Yk5 z(Ia4^XJ=>iGw#FtDBf^nsFISGxA$7IZvRE7)sEqix3}D_a4DN(IhEFrNr(s1WVv3V zuNU@=s^?fGxk5P{Bra}_jmCsIl#ZkE(#ycixau^#BmYi%cB0ERn884^1a^9R`m`6E zZg%<3k=(Or4Cg1=80U#EWVgYeanv%CUmi>e<4${sUfukAd09DCE4rrM^rZ(aLR;Q{k>8s5@MEP%;_F=BZ@5A#dp|P`38Q8l>u`ypQb6{_Nl0KWej< zD@)$xppQRs8**#~1HpmYOPkxS>su46;0e;=4l2NO&W)G)rx@ta!wc6w<<*+V`t5&L zwBR+Jq=*l)Ks>&WG)?;`weeS1#P^;JHyAKce`NdJqXKd*U`K3S z_`WHPgBrr?DJ=gPO;$2q*7{6QhRf_?RZgm!0Z;DFQ{11F2p?yTJS|bXgK0;;{Oi^W zjaCL<-A#)4aasf{$lJl;zKw%}5itx}A$MRSa^rAwBAJm%-osbn;M0TJ z@ zQ%LiMz~3iHO4GeoCBq4VCKbnHqod(NYvl)yTH4yNpy+Ge*SfEmqG=zrHOcdA<8qXI ztM{&FoXtX(sjIH5HWKle{zX>(SebAOcJUDfp1uiECZ&Kbtc(vBWdcK? z3xm1@1BK{!?@dpGbE(<%i6aeOVsgM{b5@!Mu0WFGktpZpxW}BkiNNWO4)1Zr%uLcU z*iQ_f+N!+xAvkV2A8{TU_OflUbfmY=YyDWB<)ppn{@rIJhTkAs&-`lU-9n|2CrvrqXieOfE$9K%0Ge|LcOF zKMRv{c4`=YZSg{%7?1M)*og1Uhytnl;TYZ6bRT1nub>HfY>s|2ZO558;`Q7~fFUTX zcdJpMFBRea>=|)pFOSXi-ifM-|I4F}zRyXfH;-yIeb+Bj;4XyU{qFAUR%a$_@tZ4T zF8;HsO0&Z?;#paGvb;9X3DnQ}sFFpLgHHv?dkN{c4sV&+`sc~9E1evkhs-OuS+*x_ zT>ToCJ5()KSmNi-#cx0cW_4_?&o>yuWQR7YxaWtigeb{eM(dv6%PKAHKh%wQ7f_J- zk;>m^D^*cJ;xd3Q`DBY+;i&m@uxIa4|J?wa;-@-VX~raUxrd5-NIWMMl6-g`bj@Ck z#LF51Ru70CNW+Y2!2S%tU(YPt47S}pydeIXSUc{~h@Y@p~-loHPyM0J1#o)XZg8$~B`WTilg$JdHw z&TTyzn%q95LFRrhReX$j6PNv@57#YZxN>1_&s)YvY3z~eQJOxjQoo9_aq5<_36-(Z(b|af;m-`!bJ=VM2RUzN zugCtXR!;&Nknjanbof07(#tE$0SOU4f~PUX(%ms&pzXY4I1%m4AOG`nodB-(|YZ z+rF`a3wgRX`1b9P^F+n{#P+%7xuiXAlTMq5U@AA+-Dmyz^NQx;ud9Rgy`#X(;H%$V zp;>p)0ibV*1@_}B*6b0QdB6V2uhUk-p__Qdd zd}8s?&T!$ouaRHTjQcVWL1Rd5T$hR~)DO2SVnrw9Gb7d>$vYhWXw66G&z1A~h`!D! z&sdB6gNj*cx}qh4RU951t@SzRjiRcy507I16uMrt3>!HqY}NbpD^1N|qHJ2LL+?Iz z_t^Ymh4T8{rOXCSb}cxHUy2&l$47<6Ki0SGoFk=#G6o5BY7-iO5d{-a`rt&NH!9!{ z&jOz+u_5hfeJU;nq^E&a)R3Hv^!aAh{;0T$3zJC%?x8l5^!EPJ=GozU4X)ToMYqq= z-V0f(c?4HJOK^Yqr|2VsfXk_s)zui6@v1(mc(tzz|m{QT^Erq<8T zx6Tiq1s@Lw%U`NCtllbpg7ecdm*$0mk;LmVzAMQmW3Fva?~-_p-e`6;2g^rtJ$EQ@ z-GGDbd9v*Qr~~D%`527ddoZ#b*(0Gk-}++>qr+ILHU*<4x1_EN+)9p}FjRQVJ;{5g z0k^0FGm3(Pr!w5GTFtJ+Tt+=57(9wSXlAF~%#Xj}7+>}trxX`nc?d9=)q2cvTiDy@ z{L91B4xG4~(;9oLK}XJg+LAB5Ghng$S5eRfmBsn4su?-vx`m~q2E)*?BP1Go;%<}V zvw%G;g}0!C|lB*ft(G2&9gFvs=J5O=KK#m9RSY+qVUK zTEe$IZ+$1ouQ-x!Q~qF>ir3?Ftd5h`Q5uHy5Px^Q`Hle;!#FQp}k zH>TgBwyR^-mZBw_$~wDXtgWf3!^6X4r!_ftW@Kcff|H?_2~Cf;0JvzE-TA33M5&flo0K5-1M$PxM##OvnhL$%s?BdwQbfYoFOB zWTIr>_bey`P3hKdoy|4|IW_!%>kE?*DWmluIkYO@ir(;CCEro?ZowKO@$x+TSDMr! zl<2jIeN13tvVbwum0na3OraX=tg(5N?>IdXkUL>E^H8A*!#u8@yIalp4#hO`mPIt4 zB;0N?>&w39_xU!?vpr8wd%D<82iENeY%zN^z5Z7H-{Lb03teXp{IoCbs;j?5y!U-Nm)IYJXC*Mv-?lb+?e92xiKPp@ctc!dbVgjNe*E)67$lzr-6Ycy(bEZ zO9k0FRbPWGm52$#x|jyN&FIV~fFe5-qxwA6g$#L`Fx?MDOwUHCF^GeF6ta!dOS<2{AHd)_8hlBz2uMzg9 z%Z&%!_x=#AoxX(nSq26NH(DIl5*aF#oq%H&$ZSh&a*y3D4ONb*s^xK}&*BrNUnZY} zlS?u!$~2=04;>Hlrd6yTL0vUG)NYs_uU%a{DYeuN-P7a1{!w_#&4ns;S>$nM?>F(J zPDVPsCEe7=NgT<8stS##-q10px`gfkV=8Gpb9$r(pyA;)8oG`GD=N>Pz?7Fo_u}fv6D5LY8QK6NaN0+?RVvp|vh@0nc8n%c# zBN~Ugoo>l<3to0F7&dToRBpwzAExeC5y&rn_%N%TfKJ^=tdQ$xie3!gHkFYVm85lZ zbR>tp!1R~V2y@ZK5`u9IHxG|WtEo9j>ujPz)mi6Px*MkFay@k7GorrT0zqwyd#T&c z;{(@C&1Dbf@~j0_Yp?cujVdaZHpM*(Zr{vFKfO^#oD_ZQe3^8d{2Nzx;Oy+0f34My z1vx&nadcvWi5ZQi4pp*jJ=U@M>9Z@%v$WW%DJ2=*sIgE^I%mxSv-&bQ);HvVb9vntx@dYyTe@`UTFq7Df^ERWoM&xi@{ z5lxkh0shUeyD?ZbZDI*0@vLrs$i2*u5BPxssf(dP{b0Mo?c~Sh8GJ+9V%A?x>c<*K zQotzibxIxy|HiXlm8Ez12@A|`8~LH%dLK;h>&@H>KCk~gl^W|^cJ56>_6 z3gf-hO>Pk#pG%gt81H9ivw_o7Z{k98recEn`bHLHCH(3JGJl&15BFVqvbB``2=?-D zZO&D8s&-k(Lgq7vKezZX0uv{-+nbt?2aCwtt)Avo_8;BrsXw?e%nl+Q795*?sws(T z;Yr8>qZB{lNP_KNDo9SA40jBTJM8D$H%d^F+ae+tDoXW}bW)TY23J}Mrl<%K;wSEDU1XXBD8JZG6ynm?8M_M;-US#NP*m1^Am2=8|t$Jn?dvqS^&8QM|RS+67DJmqj zoT$ovw_S;i_LGko1I>)P4mjOqPvBjD*5*_w<#!Z#WG|SW1CCt|XrQTI=*DiW+<;*S zDPT^Fb&K`cA7>&_4xPD5(bidgdC>S8tt=``)_NYKCnn@4T#0|~?SsDF<>{q1tbLAJ z#k2Yc`EP|Ex`~;(oTt(g3qHC({A8W4+1Sbo$($^>Pkj?kw#<{LFhRnN#G7?N-^Hmh1V3uR#Y5fySvBTHI6b% zt!z;=LMeAw6A)~1JJ zH01h~H_o~V!zI`dh2zYNYy^XUx>4Vo%fYUQK6AS=o6aD@p$+Wm&RLX#-}!i{hN9?6 zjgK&h#PqXgD?bxAx4F$)#zxk#^fPblCOx`=lNMpV)?en0|3EII%O-d~ed#D@=}O_; z@?@z({*8`}t0M}6zfXE;ksOaH5qwYRq& z_1N{ZoF`hyUBm|0E!8^y$l&zADS(aw2^UmFnpVqqE0g4>KM{V_AC@P|-azm18Oc*f zASMQ7s3XFyD-AkaH*m!c=O%f60XgYi@jSL zo6&8Qd)3aX7Kiel^D9dkABt9l->FyL1^a(hZzdiWbA7w$k)4#08}2*oGm_e`5O<3F zXi($aN$k86eKC$Y zSpRfa(^>3WCo|Fg=x`!@45PMS;fJxgEDRWXFAr97|1lQzy{9pxI5{vdQUOM?MO3bw z54Ey&kW^GuRzv;TT=vO7GBY)Lg7zi1F0T$VPU$F~h^bk^0ddP-=fr1iO7mg>5w>wbe1!NI+ z>X;O0c2^#p@izL61leT3fNgeV+F9yaGvp#U$=&xCXr2%L{LyMp=c}_Z7MA46n5gCP z^xAL9`{BH(a>V4j7#i%moRPvPOL61p6P%@MY#6544`&W*wreymA&d&8kzS;J9Bs{- z1AliCh_M`5>eEELk%ta40&DTXJ}(tbtTHDkv=@g&T&5H%M9_s2n})GYDU%ihU_5tJ zl9U7{td262Z>v9+P&^Znm;=9$mU%<1rHOmf*z}-OF^le9z|D7U$~lt-Mt@~#&irfT z4P7RxZhkp)Bm18j39xJ~B||_|;$AtE@W(aKIp7n&;kKEl!4g&%6P3c~Yo$dsMmzP3 zvQ=4sORp~A^7~V?*KelScap>hv(vj<1h8hAYI#Igqs(j6u+$@M3ZKM3 zPxMajziE5j;otWHM$0NkeEXIq(ktxpjVMU@>$SlzEd9KVc?1PJeU*LJRowffp*mls z9OiSCAI({o>w|5FvnPnKO0&HhReymRQ53@T@5IE{qk{wK-I`E-a$5z8#P-`tL-Hf0 zm!D|m|BcDkT?{8()b7y)I|`F^`P>x-rzTzF!!TfhB|1#pRj8-6Xre|^mjlccMP&$b z!5NtQVEms4iH{0VGbhJ&e7Ca$%0w>dnJ+XEfzLOejw6fcb3UbdrIwoIa1Sw#uF7u= z=l92xD)tO#>BjZQ)em>*#@Z?ty#JV#X24C>a29WgS^Fa+KRUeeQEki-PS6rR&lQjjJ@8nk0*II^=t-FN5r zoOgwL>ZGGbhM1hA{e!mZVrA5fspLm6Y^7_%eH<2NN{T;g{_iY6>#u%xd5ep!8fh|P z_xdJ&3;&AC&3m_`6q(5`5;b_stS=%-vnMCbXZEU>=8H{l@+7b*-aK5NOi$~zY4Ei1 z*-f1E@3q))JBCDuExKESUQ~$}4$WpoFeIXk(#ODA)8cOHe9HK7O6UX%;zSy9x22K$ z6H9ild@9jP0s@shtH<(9@k&M+kMiL6qrzBxC)Q*O(T|RrvWA}NXB(w;QSUaUY2^f; z;+ifJ15a|Ugdgl#Q^40MjnU50j#q1Dl0{6L>&Jl;TmE1Up=4>ZVN5~xGGeT|yBl;1 z8Csx2m6uJ|3f$j1Jl~&zKT~|;avEw|59Syx%8!t6{kc)85UbpY;=i;Sf^5hTi`edH7`N#&T#;kYeMBQSQBY$?H=*`) zbArzC-!%#65$~Pp?fT%O`Ok`h`yb4H)HIl&JbIKRC*ri;cs3f^(<@hXmR<6V#c1$j(wje(dL&_5)r-@+ z)iwP^vPOkr?w*ui&8t2d@PX+fQ$Ww9yGU4^Fs_5~rPRf~rDV9$07n)dq(AQUgkc`WW0YO*ol}yzj^d`WNOBHoGmFeBQlAPdCRJrTmURGzQQ15em!ee_tc7vRRleFRB(Q z^XxyI2`DkDdN!4Tr|7r4^h2#kQc6f<{vw%is_e5U?b+Vjx698>Dx68n%FCs}Z!4xy z6X9Q1S~C1rJLlI;cX>W{eo#Cv2tI38(7ic|pP7f07AS&^I5i8VEWvA%Rw*50r6XxS6|HC9{_CXpFF0zR7Hc53DW(l0AJ1>9c zYBDysLl;&rZx2?>7JG9+0J?BQ30#p5L1?re>GT{C&B$H}`q!sf4rJE>jbRnXwFI*fWJr7*ROOx;xBf#$lob7&0&l55wL^Tz?=SuR`Hv?T zb=*o+SruY4qOqA`zWayF-WBra>e2FuP+uzwLe|94*d;6C+! zM5fxp(;oR?Hd=Pe?5b_uJEm0ZM!UKj)zlg@x4mmAjL+h%D=zWrS-0UoRjj|?)M_g75o5;KY;R0$Gy&iF z7pF6m-%~FmK5@$tkoNqwZkyGvuFWPWPf=W9$SfMNUiWl9b}dl$SbcAS6MvvFcr{L| zXpc!ndY*jk&HM0wczWxosNS!Am}YQup@))|RJudDTN(lB?rxFp zZYhzH?)QAY&-%T8vKEVV*k|AS-usGu=9_;zmQ=FnElDOn>YDme(#)7MCxzo!JRDq+1yp>oZT;U`pGa0JnEk_mu>&tfeBCEa`UZpDM#P$ znm2F(hr7YfU)euN>(#}@P>);m?jF4A%LnfNd!;F5I@jcz5*yM~F(emmbxrGgtvURV ztL)X)>wht?2QB)3yQ%*EhqHcAvI6-X!{KSVx7$$R8uZZNT3=t^9}@Ca6tMqTrnicV z@hQCsmYRGk#C<$Y8g9z%yOAR9cFgUpxay_#;#^0`1xv{`98@hwx+S3%jT=8|S>$u! z=kb=0ADDTuWM`;z4vva^Q9QMtUFCzn{3%l0ocA&x8h?acGs?47I7>qqFp{OgY*_9X z71(?xj2Z>dCT=#F*3J-4T7e2|eReeLYA4N{N!qGh;LTGbD@r2_I;rd5pd@2-!Qzxc zkl>mLXXD^FVH4_%f;flN`u*lq0_{T8Z-eiJ|^gcUPL4`3^ygWGguQFnlXto0|U;WQygzm9ft&T+QTt{Js? zWWTz4ySZ@dTb$!WC|A?Nt!8LoO6Kj-#^qt70U3UF8sCzI5j|O=td`ID)Ep zw*N@>62E3Pt8DwQz`rE>2};{>4$pDCMdY})w3O|v~lfy?xbeW zfpAulDzehRY~aVfZkLuOI|ruamw4A6K?@6OYsdI#AE;B>4Nr=>pfQ7wfM}>acK8TL z=pE7vJQu@uE4`0Rtrky{)!XNEnHAy_Gvt`%`uuQaypX)5L5>b+!4Lbn+w%VuV|ExFt7_-ZJuM8X8d!;L z?Yj{GOcqDbUQ{H1sddxV)6Tzq{izm=7+rlA8Za5G7Y{QoTRC zyXaszo&%rfb=$Yt9^8{TB# zMGT*6$Ehgkzha_p^^m3vf;ZsmZ8_S}6^8F=jszjwNiI$3a&JL$>>UtAehyPxk$Fc231i_0KRNJ!YaIomez@)Aja6?_;OD>}|r zDtg{Ny_h-1#5)#0Jpm{ITaOp-z7%bhe$j{Cl{<6zc+-*HPrE4&ZzQwpwVky0k8E8j zz`^yM?!&DDm0^v(!X!~ozkryc04mTPz1_P=KrCWbs{{NQtg~ON_vh;B(~Gto*D5UE z9>qD8GjA%D#U8T1a$25Tcq0b;Nnc5Aii6QnEz0#8#TM4rIUjsFC2p&~THF@^btU_E%r(Hv#Gw;k16q8v zB<9EQ&`!($Dr_Ok{bAB9=U|6YZvv{!ohHLdvHvjN1o$dW4wQm4y1%z!VtLjjvx@dl_E;Z9OftUu_@-i?o>J;PSV*oBUiQt!C zs+kIliu$LO#go#q(%g-$#co@vTc3~RqubivbpGKa#I(8TV05MM+B>W~HL7-(wilsT zBEppy7|4qo_7YpS5v5X=XCk^7G-1zj5G=d(65Bk#1#}3UyH3q#2WgArzGqL_kF!Tc zk8&S|Bu{n$?SX&QT%}cyj$|yfDPxh_;nT;>J3_gHGY@~c6nnulJgTkC=o@4mAKb1TUP{!;N9|)>-;es+_VqT`I_%l=GS%?s zn70-Iy1n9GGCV9UKjX3*RB~@ z&hFGTh@M`o(!xMbuho$K3wv6uTRR;T9PTEmfbwkJ9nak@2+8+~C= z?XLIbd+poD{rx!iI9OO=Lv@@hOT~^yU>M%GeYW%2%ktdQdtJS4dU|>rK(^&JRNS)z zR8#^mhOD06H^6ml7&G{Sl_bKZE&WTy_;x!N5%|;o;o-EroSY07wCv99Q=wJ*1fSAx zC-;+?-x?aAqy0je`L+8jcbCgw$wM|pK7ZTR5C@sz^ZWYFRd}Sfi~GxB3$B_IC(kN0 zIh_r)`P%`}(a~4O$DfQmJ--&!*J}d(B=A3B16RZR;cWbnnS+my*vr}QYjHmQgbcwi z`t6b@1Fa+}P6L;iq~s}9^EAJ&)B%}_NzTj2Xn!T~ft;mMlyYIwg-~Mu2;+)L$im2a ziG^%VBmgTLV!@~XG_tTFMHxP7-?_O5#z=oDzjEIGBf2FdtmFQ?MWWNQ22avFV~wofjX2=;Lc&2m67zvWsg ztc{=SBk&w?k*J=)STP&+7=a*m16+UGwF~SPb#b+pjrRJehUWx z>Gp3ay{i&7n3n};MnSKe*q=MjDRt=i*P+RMj@+WE%TV16?S*D*2a^wPg^c2zcMt~; zD)C8y#kLKpqkGrrUQeiYXZ!t+sHmI$->5Y`)-BVMUy_q6exMRxn@k?3@KqH=pc`0O z{Alo~sxuHYzTEhim4Ef8dnd4=_;LF?%}>f;MFP|vaas7T4SI=-M-?{hM1^`lCL!{$ z!TIRCi}Ucq1Nm0& z1nS-u?%MUwTy3g{)-xoznA3i13V%P2j7G}$FTm~ebdu>QQgoW z02B+drB^K#+2MYcw)*ISl9OaH_q)?+m+bx%h1XfmvroQF&&PM+x(5{b?4j6e~;&@W0y^uHJdDTo!#+-@?h_#$PD6b{T0QXccpu=<@rh&j*Em_}$+dP^V zp#cyUGqGuott(;j<^XL?Ikt-$sO}F?lQ4t;>tz|TO2N%*&(qQ!zB4JAa(kpK;a3t= z?~YOQ?J`mcz@vDwW`z+<6$~G2=+x~-pE;U6e*>iu9@6+_-)_!SG z<)yg>1$e)oaozP#SfHI<7QVj2ZQ&f^=CNBMr?s_JZ(G>+YU;Cr%jfGG<;bY|1F>~R zfVQV1km56(O78ETw-uSc&aJIjSfYw-lb^0T0wON%r4f4Kv$?OxTi8(*CZuGm=jBU? zscoCJs9{}I?tt+4$dRNxPAbFHX2zq!Mk0ImTPO`SI$Ih1@${Oc9vfJlswbtrwS>ZL>gyw2_GGRp9 zTnW9K3{D{#6mRqB$#6BAt~qF{4h!&Er&(Yj)mm8Doh%iJ~xl6tc*c#ga0m) zPQFd_VdFLTRD=0;J6^%MyPHX>m!<;Go0T8O#o;dD4vcS48P z)6`KnD=|vxB%!_%TA3^onEc5a{gI@%-AT1F>pfR(WE3%ueoBf|KTLYdWDq_9hrS}h zhzgP{1oa~ISQld2pkeyD-E{rDn!6Ik{c=1#ZPy#|$}t&BsHCK)!43d>(xj!OcPAz$ zUYEPvtp!zdo?c)3KE!SJO51zgCW*E~Kb#GBSR@7p)^_fV40H$UhWo#ycnDk;kF{0i zTQS|g!v!Ss9@V9s^2CPB;Q?+Uc*`|Ewp-rx@zpjv_i8>Wa&XlA)-GVUy*}}+veMqW zi?NJ+B%y^%(`;j#H`>}-dLv7;m~is+bhG}!v_H;XT3hvjIZ`;co$S-9ueK@H2#Tx+ z?lo5=jDD|8FeQ~!^R~y0vAC~UnGyVb>mu;x3Q%63G7he?5@Y($rcj}25kushK_s3K zw#NVBs5R~7&&o~=mE7nFtTq?b4zud=kn8e5?y~M$m-^XAu~Ay3E{c#`oVAuXk!HMBD2c zgpHY!YG393NM>#!KQ5;}OBFh3A}248nOytNNnPjG{f5q3XCO_Mqg&0jk?LTP2<)b0i`LolAQi>}|hd!uTLjBV1NU_DLV-vuYO4~}1Cx!kWg4Zz2TjOS}a zILN;zN?{AJeTSmf8K<1n_Z|#28VUPYyLAleIZ;p*U~13(>aVr4G%qt0ITvGQ-3bo5 z3eo=4tNBS53V%LFpHpMuioDv;{n`ab&i$g#Imk{rd%SoBXWh zQN3bQuh(`-vzs6_MabnHi+`{r#|F0nMknEA0E}P`2Qp`Cb7o6{7LMGrHWW5F`p)7R zfFSS&9TRDmLiWU!RDgO8l%V|ba;0|#YD2lXxiS5=!&XFaaskB7=e8PMhHe0uwA z@ALi5zisRCQhnX)Z3lN|wUnLf4P}&>1_rv^rP%VCX=<9pN~7#$jg6dzTsvd9VHKiq zCzGk$(*J@Tb8>T&JO|%$#ntru*-##WYx9;^dj9Nca@&fNx8H01HGeJ5yK)XW9@jRj z!JYp_zR}s!InbPesepvB?_=T@mc$D52_-Bm6k1%QEu@FTxyy%BR(!A1$>+m8W#y5t zZjak9yvgf!_jqQWe#Wg7Vj-=n-F+1u^X&^3qz@Xo2~dt!GUxT)J{VygR2CZVIMcFP z*Ui5`BvO?nlvqH@PTW!H!DZ~=vU!rtq!lXtVF*-?j4LvE!lJ%AlCm&$i2_;x#F zt)+U~`?2Ul#+a{ZSSIHmM?jH0K5%U4JA2QrbRTc7>k7Q=R~{Z73hL@FY8Kf*{%AQW zQj2Zd$<~{QsB(=qyL*zmx7TZ{3-O_YE}C{Zi3=@xF6yvGRVe`im?shgjs*OD>A4n!YBC+cm#4f=8|~YUGwmP#rW~*A-L6x6np_-a-!81z^_iZiz_znasL8W` zWr?~ynf$Sdd0N3bLa)%E$gm+(_24r4j1hE+9YBCF<{E&NPY2d-(UNB23g}|RRFf4~ z4xaX__w%bq@iX39*RRqAKmNrQ@zqFL{8~VlY0$?>D%D%{BE(qIctHWKM5E7q&>ib= zdSr^}9H_nOP|_cr{qjRW17pHF#`lo63qm5`zyGgX*VXLEc`aaF8>OrJ`TP&LaJtfcZK2ht{r!E7{9qaDNR}Z{B(~)&>SiWMvh;Us`+P8%XutbtY)}&SxlM}%zUv+(yQh4J> zg2W9gR$?v)?@2A;%45dnXVaU-O&|+FHkj|GaXi~l=BAmCR6x8}h*MPhxN9)Ect{@m zyd^Jw^sMc=_OB_|>Owi&n_ExgtwU_Ru5N_?Y#LR@c%n50=#T)79<09~MCc+5ZF12> zMLnbiiaf%6+HY2(D@V;w5*Y!#(Pq0_q!qK!sMj2J_f3xYaz)qq1A`<4Iard;xZl4j z;HaM!3G@Ed>kt9{1}Z2e`Raa4g$Y!yU#zgCBri7?IZOtnyu2K+9WV)SP59~wi#982 z&)#=9Kflg89{+CSW|4F9*QoRM&(9H&z;o}f%*dE;1#G9j)YYvctd`^xqN1wJc*0AG;h!d1@lYxMBjH$z|zWqW6+|7VBE*W z8?+PlT?gNM?WhiWlF#y8G&((WU-x^c{YpJNJK_&5z}$091c6vxIU+eQku`(-6}8I| zz*UHzJWZ!RT-*ghA*G@reiUJpKrmT##S&DdBS8h8g3*01!@b)E8oy00Jxswz?hV-1 zW(i5gu5WP&u>w(~a_ahFBxpU@4WyjORAn+Yjz|&lZC(}je_Q6VwEt$D7Z(>Z_nB0O zS5_zjtCL9r&2=n4eJV6HHN65dzx=@Ej!X{_ClD*PRkhk2Wi{T7Q9n_0ie?8SBt-aB zGGkzV4t$_){j|1%ATyfz+@9N7^i}4di>%#+3uC##cwcx(=r}saiVu0e{ei+s2yd;* z`P;vH*R{3+#!t_McB!4lI$Fa(&RcqV-nZclsKBt0q^cr^=nsi7i_R*G3Yy=krk1PB z{V4gR8$4AW{M+cYpB56rV_f*FcWdQeJA-@Xmxd z7n3)aCg^P8r`hg-eaFR}+bUIJDLT-&EE|GHsAWp&2Z=r2kq-wbJouX(QtE(Z$S6k0;H{n4P0WLlqXM zz<^j5NKa2s3=0bj0f58apZhM$*_` z$IxqiaA5%&qTU;wC@7pZ?0vsy|M&21+qDvwA5p7`VvX#EvX-sd@jA)uT z%S=0uxXr0bj;qr0t6I6m)E30MDg??3immVC48 zy)jv-5nGy=KvWuaQsEfWs31uWfRN-Qex1CmWWalsK4gE=*4tF=ZIJ(gSUPvaEXXn> zuYpVI4Hs;lS8AR}%~vUwN0O-?T0mx+XjLNh`BLp4CrO0l{AZ?cl!1kjPQ`vFW=4uZ zNuv|=^~*NL>~reOmr&u${GzP$UCQYS1`4#i3j|tFcNXTMS<1|Dn|}et?|NMV3=YEr z6GVYt5No}q0wXPii3&9Z$G?7sL?5Q;zzh;}73-2{td$XDB2^CP>V)+e1$kfU^mo`B zcW+7!T0&HsCSX5zB#c!Qpc>&-`ZR2=Pu)(ryg%o*AP%SX5$*YJTwG2@cbu{_($fRX zb%^cB4MEl}2J(uEIAmmG6KiXqz<79H@NjV-OEO2-NT)AfcieUrQLV1~o^{QydC+ST zfl+RzKDUxDl`n{LPp`~QP8RDW6BnS`v=PYyh3R!k9ute{8wM~vGQ#1?j;iI_dlz;7 zxb5}F{pTgV$iufw%cGwU=S_>RQy@IkJQOz}nErw27p=K=c7fd*2?Qm~iDZ!~dfGD| zf~P)5PJBjc0nv^kksj2ip8XY7c_(VG%I`)+)U?OU%E2u#CrCGi5n6@IUeD^ z=9I(5#!&V#oQsd9EbsyW61=&KlUC2}_*0T^7uI(>85bvGYD!;R&_@BR=Pb-EF2;(A ziUMSk`M^%Pq0PgK;w89DO+`_DtPBh9_spsTB! z!!)UJRBt#v=x0U7kT6=V43#^yn>TX4ntl8#%CPlY+~=WVAaiw3?*Y3Ua<9|<7f@rLW3ypI&}e2Nr{wd;sk*{EDYF_qA%zh?V0uV zkMq-ZL~rk-a@$RK=U?sBrR;_IJ$E&_L3GK}-Pwf%Z5us+vslu z0}&47u%zlcTNP~Ou*NZTww#kZ9^<}U;wxEdDY3>j=LRAT>u7hS^uv=wSUL~P=P(5D2Xd3MmV#vnTE%(aH&hYzl?IK`fL?*!^MgXVHgB+V_U}PP8xHRfSXcLQ zMYGc5$knP+!^b|&Lb$NHOH$3Nr0UDWDW%9*Dx{;JScZ=iF_uA)BsD2$fs85?h_L#6 zvGJhhbR9xx&b;(q4Q4@d_k(ePL<(VTjSZ|6sW>^8OgZ-zd6U|5`U@b#Xp|xKB=bhH z=lC4@vV|aRqh!&Zu3KdfNsS_~`-8*QwcQrW+}sAvT=B}6j6 zfH{=-+y^5n#MorhX*wuLsz(<IwKjk_@!ng9dvZ6h= zfSsh(NPQMpRMduOxCWy6NJPt!9WbrA)w*cFezx#RasEVr@2JtVVYSZc{Nvozlp3Mg zI2#L3xphjE^6udOMRK7SoV1qHn5m1zZi$gf;#IQw9q6{76+;p;!_{ zi7+`VRU$c};O?2vAD6KD>;CN*S&l079w!UnmXFzk_UEqP^RHo3kd?WSsds;73w`KWn*cQ+#n(6)E`4D?WR? zDM(UTb@>Gknid@{Tp0xCK*9)9HtsR1?;2+_?F=JpB}x8gNZOiuU;<4lpW-H9fO5Fl zeg%dXT4RU={Fe_dj6{n6YidYqFzyoB8;7?pU>05fIC-49upFg91Adjq5WcycaL#&wwlQ_NUPIzAsdNu(6Ox9*RBrIup2VN9lAmi!66&t#9G<~l%=_E9y)L#8uW z`J5u{JqF`E6cX_VfmFF6k&zan-7`c3R z-AU=Q4UR6L>EZ%2fw8e{7-ivjb;0peWhOR`X@WQV2MI44D|Mdb685~mxVZ2E(waZk z-MCU9+vO0ebW2;??*Pb+3mhU>3!XG!_Fm=mtAm*-Yg-fnL!*mfd9O)icbzE`hMqTx zjh&)^>2~3N1)5Qsj25k=kTsGg0+RQ1X7AnLuB2|-SYSi0G4O>s@e^ZE-Kg~mKPV>< zl0+CFa;Mq@WlYagTde^Oai~>#O8M`%qLtZ&R{YiBuJ9c}JPv)qamvBWEvW|=gv&#F zr^n4;bGZFaShgz!E-C@7C*9+`u6J849}*=t-gu~A;^u;(SBQYh<{tbC3X$DamNn=; z5TT;YqIBh)alXIcjc_~S5A&L}jQUpcG@NxJnvM_IlhP9SyY-49PYg!Fo#Z6}_0E8gZTtD#f@ec!0QK{2Y zq1T#np?NAb@sb}INln2a#!}NReDw{cz=k8>SIrClFcnmlrJAI9G#&}h8`TybPGc9e zzAPXIpfl6RAJ_$vswE{X)3}PWp98JaF?zXj4)5N3o?NW>uu?vfP!u+E0AoWetiWsh zJlf1@%#^du+2#8y0S*}coIP+3cR9~Wx7gI|&oSl{(S!XjTjCwqO%V&lT^?Dlx!!}y zh)|M+Lvc|k!YLod0ZvlYHTvFjry?#Aa zaa@0ijD;lNzPAlF2gl{NCbfPkSYD48`oW!ozoBs_{@CiRob{Jn z{0J4v@Yl>!1e{q=uu+Q#Z|v>&`|f|Aj9yE?6)p1b^ko=&x~59gp-u6-IBfjHaCHcD zD}eSA+doMG6(h{gxCki_sUqjpp7sWZfz5)r89(nG3agqdr7QXjoUj)b)Vnyiuo7ep zfkXQOC?bw3)ncn=h&ERQw$=QrbdUNa3oui7PY@~*@oY&HF8c0XmQ}v$>ci)~y}jQ6 z+DZwKMNo}{XaPX8q(p9SSlE}kSCHhTsimDAdqw~qo8i+=n5l~kQD9(TG-scu(E9y- zibqWL$=h>*?CQWwr1&pSGU}y*O@w=t8RHyU!jOsyG~;qV#N!w_bPkhcBtb<;lNH)T zrWX{bFUas7T$bdHVuOxi!;TBb4o_Ob)>lFTqdZIbi`#_F>sd^^E~yLWF|WTaupTmP zfC^@?Jl4KsAi-GlpdcXx@bqNVPqmep=Gz-9hw5GB{jwVyjd+yM=4#KQ2;7FpX27?` z3(R!Wbqb<+y4C!v*c7w>6k%bu0ud3UoHrGRQSKSOC;JK~&a0O?1o;m)onQ}k>DmxfBwcOL&^U~Beg8g)hl65M z&Db;rg4-eS?Rl;ih^5y5JS^q8(Zo5me*COK2qhJri`wXNp#87@lm-V$BGIZWNij+y z8y5>UWz20^kGySy;I;HVwQdIvz9l16E>J60{_NrcB>>40YigY1wL1xAi2G8Rnr?TD z-#XJb550cw?G`naU3KS^rlTlxDsQ-;QE8Nbuh|N% z6c1&6iRa=S-|qB?K#3V${U}|#1Ie6EepO4_8?&PJ2vOsW4>C}=ZSX9eegsgqZ1F1u z(3Z^HE#bImW&K2m;{Ua|D33s!`^a)7icN+``aqEs7OW^~Tmbk~h>T;|2QC4c;4_7Y z^UB-$qor-TkG9HygZ&ZML;!3hn7m~u`H{zx(Skm8z&Dotb`ZE&PSKxzo@Z`4-1UM} z^lJ4YY$!4JqW1v0;c?z8qz+0rMyiH`jFtb)pgj_8^e3upCqW)kPsm-RMS>ru=6{Y4 z4POgkkw!3ENf;gr5DDFIe958sNLV$61Q&Q>pZPgir8p2ZH5Q2Guk#R|AwWTBYzrydxh3)PK+ z!b<^q%Je^wey1K?n*|ph0fM3-B?tN%cNy_dR8lNVYx`pf*IalLy<*__T$YFvG91uD zI2|NOV=Rhn%zV^e(6Oeb7ZI51%7sQDa(di7G8DCZ%?Ng0pV-(~1%e8wapQtyh-XJw>$;_tQvLGx<@jHMFP$aiQL64>l?~l zDX{?SkFxh%CSym^RPk7K=}LEnqac3%*LdrV-RH@#4)~vZ+mSMvaBEuN*1{thyI(Z; zap_iaV_K)d!1hKK{*DIR(k}S-0#SU97ZMaWp7GYv*v5G|3s}YX4cN%aH_exyh|o|i z-y)Pk*xG_EK4f1}4pO=_Kljqpdk<2HpJwPy;-=a6n*gYoR;iY=2jqvsx3gcCFQa#H zBe@*D%<^E!6?X}thR`*>0fLCEy8wb06CrYtpgg}4ui2ZL2Fpn@tSJ;e15N{=hjQ2*C=HY$U~uqXM_q3rbLU?X>qMP%pa89!4d5+aeGfA!a|!&rswEJd00~U{ zNQ&{mUf2`@ARQL z!D3#3!Phz}+A#A&5wXF%B(p4#lJzaW&P6V+uT23d+c3H>@1gMe{pO~&=j8IY#=ulV z@_luCK~UM>iw%7v9bji;VkTV~A85{~^we7KzdMW$(+L@|F(Xp?2bdHt6S2YhqD>?; zu_@;(vS7D}m)}VeFv|GRz+fb4q=a~C`?w=4>_;u$f0`}Ht(7ULp^B9FivGqza3}*& zh(FM-(W;2#prZ%%H~bgrvfpiiwA&l%nClr$o}&Zp?A3kEz|3WL=9VGD-2Ed@*Pvf$ zaDSf(I8YZ9^P*Xfnc|+u7jdY{N9)^TZyO>VxCD7arvh0UljG>hPLek*3F1JgMWt#` z=#>}Hf>(*rI0&&~d`0#*Za~u}LSk;k4dqGU-rl%FMAc!*3?!i7fpv4LunZ9?5Tjjl39DK zb*aBr{%6ba=8w};pn87beaG2K43BISzYPJVKa(h&l^A)mO!1_Lxd#?NqeJkPBQ%gZ z65p}8eu=AUO)p5>1Yo2AoFJg9Cd)Id+kiW8%(@7fH4UXq0EwfHdf30yI@f4Fv`BkD zSa7qWqRO8e#K%UAqv&oYS%4KN*W-cQqpOUs^%9eM@%q9YBI8=LQ6!XRB3L5dxd)2zg3<{J0`75Fh8NYQi2Fo@#eb<(jtPfkL z3efVyt%K(;+WdQa>7&OVdtc7qt|_lCyB3~Ys2Wb8j!{O)mG^h+^uqeOZ!??4Dq!IPY0ln5(c(}+mNJx9j7WEju!vV!)C>>St zp}CD1oUYWu!=9s(cD}{%Cd(2o8YVVUtl@VA80i1OEg`eI`rCpxKf6)$8!szoLm*lM&8U6?$eE3T;YP_a_dN*x&(|$%D%HP&h2~l-TkC{zPD-c>?&j=0POh zpen5eG8QeNTKuq&fB_*I8exsY@pQ2tgh&b{@jZUSx%hDJaE4+U15#XU@Vc`2{9p0$ zr$zH|b}XiC-s{Y##S8B}ybI39_ej_hX@2G{RbX)lyre(~$;SL!O1n`6dY?H1{;;8@ z8!{Ukqh^yzUe%HU-1h%c-SezwD$u-+WL_BWL*dTTS_Du=US%s4sAV1<_%Send#a;-v6R_jgqP zup3)l4Dy2SA^KbIwVIp=ex`N4Kyn)MR&^UoGkz#rkJAx<5j>bWT;Mckhc3Z@ge4_{ zL=ohGBnA=$ok@h^NO?ixQFA$n(oqZWpu$fjZ- zWRTg@7Y)C$6|DenykQq(478X_ei&Any zwtA+EuKBd&<_fXVDTxE5TWzy;giHpdjGbIxB4(blTU39 zcW3CkJtj{L|E8$@Uox;g&=(}~_^uz2gYSMN3Ig+?a*ph}MUn|(;T)Z#QMO4--GA0k;v11`YZYx(1|MR~82a)} z+^7{Kpyzy6YM3n$f~K}>9D}2!u=(=zae-}OI6o_qTfs{DmmtFk%HN%5TuUUPpD6pt z;8-FWY+8WV+I`OIEF9?(dIu_8?kL=mj6e9YAkzF!NoeNYvl?f!HBei`kfuZg?zE8<1ns2;28xouA$xuryY_Zd*SpWQU1_Q= z05}_9UJh5YU<~R4zL#`5Tj@hAet)1`-CT;3Jc&NW3su6Gun*m?#rLK%y6GW>#0=(B zHo$&edjlQTcHi0d+V7c}?l9Y{b#cd=ee!khf6c7Qze3z^Z@&`)IDs`5$U<9mbAx!h z;^xmEk>VE&U{+eV-ce0b`WvpEF4^N+7#9GGjk&iWsrwsC&~RP~?P;h$Qk|i4uU+1M zd=DDia89=GCB{Z8g79P)ZU=OqZ#lGmbXY^&RQFmHCLw_~6p_h*mO~Rn2xFp+RE6O*BGVHk5~@gbGoi;3fzcv) zRDRn_Gov#g#}Pr$AVksho1TP;5Xi&>R`}kDBu<5(w=dC?sHRMHTQygYFQa@Cw!jCj@@qC~m`aCK5++yj8dV(L;B$4ijnQVU< zm=I|6S;tsRgFjnthTl+Ets->1r3IYGjE=APiPVtIU>~im=2RrCCpeaCpTW4;Zb0%C zsP_Xt#+t6a(NH%|(T5#8yS19mp&BDHDD)-?P*Zvr1m-|c0B{)k@}>XDJ%Fqm{%4v*KzNeNVwxEH5ah&CfCw6u$bh|snyWAcUcAg;qwz+;AjXDT zW?@($Me93y(58-}nSDt4d$GHjn9#c(Ef$K^x_m z(L{qA)fGK9(b@1<+0AxYXnrEPOAr#~*I2R5jrY@7>MmGD4j_<%g97MAO#+1X0WAm; z`!hd3mghMJa?3~0@I7%Qg||wwU5|#VGfdNGNE24sq zkAOYmdm@yIYM`{TBRu=@uZ7^<6>?KEb02v;ywj2QQ|C6v-?esfz}_y&Ue=T`yaQmy z2q*@ibs@<>s7piK1MtwpwN9ih@7Y{jl7(G>9m3rd$&}N5jCx>=wc_ zWh1O5)|Uc(pSH>n^^^mA1s{Yvo-I%I7hV{7TaWkE48{=PaYP3GaHi46J?JJw@8PPb zQvDAJb3Sf-)zf8-K(H752jD6Cbf9C%1Bt_3Y|K*B?HGk}%6+3Bn209!Chq*(hI{5l z8>L#E)X!H6iy-C~NNftl^y~4Z#1hIyX0Q6?kyNBYY0%C;nJM&Us7f*|?Joie?5Ah*-*g2ATW@82K~ z2?}~C&}}$U>ysd<5yh&8Tge0^syQi)xl@sy2V$q$`zBA5vzsB~1dntN7LZur-!hnR zWJ~lhfYOk1O%X28NL)#6OyfV^6Ls%YSh{|s7-FtD3>ixebU+>xhlgEkIFT|I_l5Tl_jT76MJr`4)NDJ{+I23julqVX zJJ&W4N>+7tQm;Q^|^L+92dO? zv`R+FB#CNtNibtGGc;i90FX;^EJ;0pb_fvXY_xFb&j&+LGMrUl#>)cnLkx43MlnF+ z0?09QN`VW@oE;=2A5GDHSlJmJh>36FVg?;5{P=gN9>;a{GpWF*6}OkWj$>!L-|V;S zpLfakZLNDk>Kn%q8zVbk0gpf+k(g7neqHD0eJ0)`v zrPNn(sPM9Bd$eV$3g*A!HtVd&pB0>M1?;QC45t55k4#7y>E}>K?gP$4p3%>Oq zLqxxR^f0M2p-PKo7O7yfoQLkcf7exnP|1-(M@QW`FSz&#UQv4PL`IQ2zwL}yWg8iu zfuVH?8_2|cQ09D}e1<}@ki^@xAFA^`boklVV==ks*=uOCmp^gG%Jf;nm%MMEUBp+GjL*I*FFv3 zB^|h(w2&KnKwqUtve>Ujgee@~z!~)&83%^PA37Q>FbM`qa0HvB8Xln*{r_+dhoI!~*B5~vyLkE`pOMdDAdS87&!+Ou!C z=Iqu%t18_5I|wHyZ12Ib&4br?8=a50*|aIcn=i6&(7?T2H#dSGGKGUtG4VEl{l{7b6bn5k~8E*}iBBg{Dbe0DeNd76;@fQB!60TFxH#Lq7-w%YytxJBCOmpy7c zntTtbeQ{2gzx_SVHk1}vhf32##&z0*et{1bj6UoT5l*|cU-vq=bj7E3ALo4T-izj( z|5>tyC`aCe!SLlI;Mz1$TkmGP^`D@Ef$U_AR(l3VZwk&lgWYZh+APpW~PHG7dL89uAganp3w)u<- z@cAs?3Kl~WFb8IZSNQRDrt^|s{f&|Us4fOw`>8hgIYP%**@Zj$54#6R4afd?i(xSz! zI0W~W0>z=lf|ug%F2!Ao6)*08-fyk{ChOef>eGvY`y4SeA)ovOSvifaIY8hKY5?V8RWyxJn zVuXQyOSLTe5-Ln-;h4axiGz&|p-vh+1hY4MDu;)`1nf&}bjt*swcc%NRNmc@N{3Z> zQ48|DUqN$PkyTSerZAznn4#^6mYS4_qn7Z_hrf#^WnLxC{q7Zgm)tyA*{jS}qLWrV z7)@GDEc=}8GErwfh8Jc48DuVz_HjYC$ypSN9|gh-))BKm-C`$F%Y<6I{NbZ{>6WoP zY0+pp>!lq)#*+-mk?`4my1%@8Sm;bVHs%d_Rr9fatX!a@Y2H0tD@d1|j3%;E+*K^<`Ce#wQlzsE9P!biC^R`Se!Z*5B=KfDD`jpc*WTNjoSF z{(BIRjHJj$JDS>NdNJ^mA8bW3{!_UjoP$aaL)vAuy)3)sM_wEj1H9>u_9 zJ{2G&r?RI$F?9Doh90u6+EWgfJWQ8$?SkQzC2u1>z~XcViP$d#eh&2@u3Ct$SV7)6 ziv;EXe-i78Alrqc-rqmad4;h3s2Sm*r%MIOk>O*fYJ*FA$OMTZ_D?pR1=nt6{~tjUia@m)u$NBKOnHsVa5xnVX^n*Q3YG%=51~WD}xtq-OAj z9`r_`*|EdRpCnOXIp$vTL=L*u`gcu}mZEUgZ-$@mCJau~1?;JU3J zKQivo)3azQ%I3HKZfzd5?@zkeI|MFMP_HEqzuPXhpAXC&yOeBu`Zwx(%$l@)dX#m( zDH9AlInlUWPveiw9hYjdxckU?tV?b zILdpKgm<*_y`$<)i`TkV*U{=#T*cwMxCLjk@LUZk<4zmop!4y;ix017!Ts>|*Kya+ z`(Imh&BrrxUPLt$-0u`p3lTupobMkd9OU=f&%p;&b!}(c!22c~roXOhUhU65dOqnE z+&6#Po6M_is8rrVk{>7eDYOs4&+`v?W2S5uB2~JyEP6d|hpxA`a*#Xl{g@Y5!bO0&osek~Pdezizz!5_9R27}S& zXnUiiuMw7)KgX@-0+CKVt}H+aaUZm{Y-n33ZY6zr=Rdwh9@vvv5L*ZF;I;$4F6Aqr zGIGdXUOqLiqx>L>XbdZC|4x2FJ-+zq*?ZM<@?+b@n7QP|anMtV%qN_Nvi!OEnmrhf zAerx>Gt*+gnxW*C1krLM&ESrVA4=vB+;5&bnX1KsH@Xp1&yu`SJEN-r*@ZsmWlVk4 zO`r2S95Tz!?pvz+@Z=;SvF*F=gO}HSejfaCil@ZWO5sQh{s}-&ulkw_U&j*#B8@8b z7g&#fk5m{bM%IMooaQG3^CMaw3K8vo; z1h9lLv8di(D{w${B6MM8u);98B1|EetS_cQ8km_v-vasCI0!%K zTZD+V@oe~RtqcER`Rs1#JY5Yz2FahJm~$5cVpK$c>D5ZSCex1^dMXSMfyf`*H}Z(M z%6prFNjfv(S`hfjGfHk_(CVrVL})h={Z;P){zQhTy7Dkd(k^8u{l+K>i6G1uLRCf2 zufgK`HB%Fh#TSpkm#IYpi$(8s1vX~?tgRtq!^XiN&G^DML}bN{U{!nAtqRwLQQ{^h zJ)Gg3mG8+XqG$i8ZA~LCo~*MG$bFS2SZ73ceY-yxt6n>0`?Zx zqa>tnf(}|*PzC^h+!=?gejJ?W8lX~EugBluNG*D=kx2ea(%M>T->-RudDozlcoq;X z2Ar@+UOnP%)4o2+Y;BzdQ_{8uGLq$)1Z91xc&QsAtoY{J1Tan#Z#;lbZ3zp@af%ew+)B>|5WmkI_2bmeGeFO2p<3l34{-z zR`@BOHC^E7z;Ki1T%6(mvl|?+bKkG=xgUqWLER$o!CIld!(C(*sC)? zgv~{U;3F93tK^H8zYP7jqSX3MZ~CX6vTvk1{&?sc$?jol0Uj=%*tU+5RU{rQ59F|C zOFCfUTZpz8X||fxKS|g6@JG`wjP%^WlyN^)F1U%Z3X8m@CjzDfy1Wd|=y6n;RtP_B zs(WcH`sf%vzDGV^EnO-%I#l%yAxQ)ju8cgQ2EbqHaL%&H(9JdFJKjuCB7#rw4NHFgHnKQ zIrgkuX*Y?On+9j!vtxhXf5WUbR=sGRK0fJfYq|SYuC741jxKk9{N9+)E)xx|`deP6 z=(fp7hp?9z5h-y9AA_nM|JDdz=X6RozqZZDu*K4#8zQ44DqQ07XkX{TFYEqOxZ2Vj zTVk(mvql^$=R~CzyLVGn)A4KHDE3MDlLB&7$eXA0(R}crADYDO? zsYmp-SO{3U6m;4cfPz$&2wlFB(jr*w2oaw}_!W`jE(JE3B?QrpJ(`KGuIucdqC{*c ziY_IYt{RYxr2_Su!ch1n<#SLyg?W&UsQDG=cNA2_9A0~4Up9W6$mSjbcGMCA-L5Y* z0nCG?r@c|g%D%2p^rT016~sJ`1T`y6_A;WMSY6(L)86X_7oD)yVEe zRJIW@;{p4{Jvx11ELA3`ld8j_yLoU zPKXUg>76w!U8JL!h)a9sgpfou;@B33sln~k;sYP=5L0H%xWi;Ri-&gxYlZ+kn+ zk{S%tR7rO}MmT8%dpaM%N`TW4hx*_&ydsgS{r`uX>*=_{GQ&bna)^7K6XyvKXEpEA9A%g zQZF={Q2m_&Htt}`1x2z4Marmi_`U@ey|?TL4FbPP8L~EjK2?W-C{yd{Tdb0BnOqBn zP?bC>8c`Kda#9$9iE%|Z%G8J{E0BzSlLJZir+8>Jn)hX4XQ~IDB9iLC*G{?+&G-N{ z_nQ&V0@lDLrcuZqU*SE_b<35sgD76D>OT9Z1E{OA#1#R-LPVTs_JrVlV@H>Fl~X}W zjSrbNXFEpnJi+GLVNmx#!Mv1ZkE#-@QAc-MVW)&!ul4ZhK}S)+$~>=r9PpYbulH@{ zBrzgGfBiQUie;K*lBYf(a2ubLbd+W*r<2+ukpoA9wy(+v?S3Mx36bz)kVaW7Cw*r{ zJ1@^`Aztx5Yr{s4NIsB}iB^8y8(eIvNB5EP<+bJ~1OwoI+?qY~IA{kUEL`UwQDmBC zPR;#}-AiNgGz&k&%9aTtX;vqptXfAJovyYsn-?NJF&n=$heRX~y(^yb*+zPpytcZm za<)6O@27~F;L&?5CD!@R3XtxAxMkkJfn*1MKihfI3A zc4apmdb=FBs04C#`1uIrS+ZnB7UC;>a3ozwf2`uNOJH$6Wb~#Vwn=#F_l+a69&vU-dl@(w&VEam9!^QBhJSKlMiPXYGnF7w}h(_=JMR%d!SKtfgXHU-t^Gmm=9CKLl@g8qn*ru z?elB9oo@v-y`p^jWKy&BcN~Guq}9~z483~r_36e#yR`41&re9y3#^yGsTEEzrH_2{ z$vgbfo=WN(4US;E#y4&RQQ}G8(KR?Ng~5R_z5zO@ta#1puCL{Io%cP{0zy8k zjoDY0(D3@WaJVkEKE%?s_siTWM;ifOO{%A5yR~IK30_$Q$<{7-Q^8QqVXW`&9gx!Q2G7`e+@84 zO*a3wfiP3!*Q|EQ+IZ%Z6s*DHw#d*bpzxqKsgmV!E5;fSQ z4O_%AgBw&#*xZW7>~s~DpZE^uw@rHFh|rQ0+mv~8#lE#PR9?RIcX~>MC-!C(zcsSk zY2m@87KiQ(Chc_au7$oEE%Li(VG#f|oS8vDf%q{4WKEN9$78^b^Qjy=C+F-z76_F@ z08-g+u1HFcp?EFp0BiUtBTq)H7Ct({0N(;~0S18KorXyi| z!G#kOr3mFl>K-%bX~)Qtoa1AsGy2DlS8vaX;ocm30;l#14$s$PoOliKj0|7&rPT>F zI?%qSGC*$ZW8Z)mEkY4mVqPG|#`BHn#dPimx6u=ytw_p`Ay#B6MYe*G|z>;Z_k?J+2; z>@W9Q|8KX8&yg6xs*H}T9y7`<d6K)K< z#lR|g*JBhoA#8z}gOvgVdxu?CD*P$X*0iOLkC#ATksan^JBom1huLO_*xvL`u@taL z3(Z9o`>7asaZ|$@jXOYV@q@mDXTJ*6OsW5$B?ASH$qnx+4 zI~Tsf%^vtJ^Tb1QzvUw*yXk?e6jBV0&~SE~nhhWnUl!>V*kKN(w!x$z;I>owh2VU& z<+ot{&IuvBSNZ&=L)3M<{X5*~=ka1Q9B&(*q?EWv201RzNgaA$2lCj%hP#~#CZ0*T zyK`MP24D5?PT0Itf$#^J+JitoRPaSM((12M=8}0E6$o?{L8Gp-9FROR3=w-Lp~ysQkf%S4lf=nZ26V`b?d9NsTq^AgJHo@QhOePT6W`>PS4mX zD3KL&NX`41@au^1yw1PNi4qGHujvWi$9rvzDh>4p@#Y&dfmaJvt$Te#v$H9oo-M(_ z7O!2rcpZ{R8|famYcs4#W~=MdFPD9NeG|jO!x>g&SQElVYL@>dB+fR=EuJ~18Qbqj zJAHV?=Nqg%J*lGbXb0=G+Cq(Pa=RX!sb6LM�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 13/58] 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 14/58] 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 15/58] 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 2b78c07ef16c8cb89f95393aa12f0d79efdc41c2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 26 Jun 2023 05:26:41 +0200 Subject: [PATCH 16/58] Fix search not being easily findable on smaller screens in web UI (#25576) --- .../mastodon/features/ui/components/header.jsx | 18 +++++++++++++----- app/javascript/mastodon/locales/en.json | 2 +- app/javascript/styles/mastodon/components.scss | 5 +++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 05abc1ca63..bdd1c73052 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link, withRouter } from 'react-router-dom'; @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; import { fetchServer } from 'mastodon/actions/server'; import { Avatar } from 'mastodon/components/avatar'; +import { Icon } from 'mastodon/components/icon'; import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo'; import { registrationsOpen, me } from 'mastodon/initial_state'; @@ -21,6 +22,10 @@ const Account = connect(state => ({ )); +const messages = defineMessages({ + search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, +}); + const mapStateToProps = (state) => ({ signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up', }); @@ -44,7 +49,8 @@ class Header extends PureComponent { openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, signupUrl: PropTypes.string.isRequired, - dispatchServer: PropTypes.func + dispatchServer: PropTypes.func, + intl: PropTypes.object.isRequired, }; componentDidMount () { @@ -54,14 +60,15 @@ class Header extends PureComponent { render () { const { signedIn } = this.context.identity; - const { location, openClosedRegistrationsModal, signupUrl } = this.props; + const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props; let content; if (signedIn) { content = ( <> - {location.pathname !== '/publish' && } + {location.pathname !== '/search' && } + {location.pathname !== '/publish' && } ); @@ -84,6 +91,7 @@ class Header extends PureComponent { content = ( <> + {location.pathname !== '/search' && } {signupButton} @@ -106,4 +114,4 @@ class Header extends PureComponent { } -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)); +export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header))); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 63ab26bc56..da3b6e19eb 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -147,7 +147,7 @@ "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices", "compose_form.poll.switch_to_single": "Change poll to allow for a single choice", "compose_form.publish": "Publish", - "compose_form.publish_form": "Publish", + "compose_form.publish_form": "New post", "compose_form.publish_loud": "{publish}!", "compose_form.save_changes": "Save changes", "compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 81dee20d33..15a14ce57f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -133,12 +133,13 @@ color: $darker-text-color; background: transparent; padding: 6px 17px; - border: 1px solid $ui-primary-color; + border: 1px solid lighten($ui-base-color, 12%); &:active, &:focus, &:hover { - border-color: lighten($ui-primary-color, 4%); + background: lighten($ui-base-color, 4%); + border-color: lighten($ui-base-color, 16%); color: lighten($darker-text-color, 4%); text-decoration: none; } From 65aa04647a6a5cabdea11acf960bc4912529c738 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 26 Jun 2023 05:26:54 +0200 Subject: [PATCH 17/58] Fix onboarding prompt flashing while home feed is loading in web UI (#25579) --- .../mastodon/features/home_timeline/index.jsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index 389efcc875..41e5aa3447 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -33,9 +33,11 @@ const messages = defineMessages({ const getHomeFeedSpeed = createSelector([ state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), + state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()), state => state.get('statuses'), -], (statusIds, statusMap) => { - const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20); +], (statusIds, pendingStatusIds, statusMap) => { + const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds; + const statuses = recentStatusIds.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 @@ -46,9 +48,14 @@ const getHomeFeedSpeed = createSelector([ }; }); -const homeTooSlow = createSelector(getHomeFeedSpeed, speed => - 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 homeTooSlow = createSelector([ + state => state.getIn(['timelines', 'home', 'isLoading']), + state => state.getIn(['timelines', 'home', 'isPartial']), + getHomeFeedSpeed, +], (isLoading, isPartial, speed) => + !isLoading && !isPartial // Only if the home feed has finished loading + && (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 => ({ From 7b024baf50f58ed8062b2dbf34e3e40d9af31e62 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 26 Jun 2023 05:27:07 +0200 Subject: [PATCH 18/58] Change header backgrounds to use fewer different colors in web UI (#25577) --- .../styles/mastodon/components.scss | 62 +++++++------------ 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 15a14ce57f..53b68a8434 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3147,7 +3147,7 @@ $ui-header-height: 55px; .column-back-button { box-sizing: border-box; width: 100%; - background: lighten($ui-base-color, 4%); + background: $ui-base-color; border-radius: 4px 4px 0 0; color: $highlight-text-color; cursor: pointer; @@ -3155,6 +3155,7 @@ $ui-header-height: 55px; font-size: 16px; line-height: inherit; border: 0; + border-bottom: 1px solid lighten($ui-base-color, 8%); text-align: unset; padding: 15px; margin: 0; @@ -3167,7 +3168,7 @@ $ui-header-height: 55px; } .column-header__back-button { - background: lighten($ui-base-color, 4%); + background: $ui-base-color; border: 0; font-family: inherit; color: $highlight-text-color; @@ -3202,7 +3203,7 @@ $ui-header-height: 55px; padding: 15px; position: absolute; inset-inline-end: 0; - top: -48px; + top: -50px; } .react-toggle { @@ -3883,7 +3884,8 @@ a.status-card.compact:hover { .column-header { display: flex; font-size: 16px; - background: lighten($ui-base-color, 4%); + background: $ui-base-color; + border-bottom: 1px solid lighten($ui-base-color, 8%); border-radius: 4px 4px 0 0; flex: 0 0 auto; cursor: pointer; @@ -3938,7 +3940,7 @@ a.status-card.compact:hover { } .column-header__button { - background: lighten($ui-base-color, 4%); + background: $ui-base-color; border: 0; color: $darker-text-color; cursor: pointer; @@ -3946,16 +3948,15 @@ a.status-card.compact:hover { padding: 0 15px; &:hover { - color: lighten($darker-text-color, 7%); + color: lighten($darker-text-color, 4%); } &.active { color: $primary-text-color; - background: lighten($ui-base-color, 8%); + background: lighten($ui-base-color, 4%); &:hover { color: $primary-text-color; - background: lighten($ui-base-color, 8%); } } @@ -3969,6 +3970,7 @@ a.status-card.compact:hover { max-height: 70vh; overflow: hidden; overflow-y: auto; + border-bottom: 1px solid lighten($ui-base-color, 8%); color: $darker-text-color; transition: max-height 150ms ease-in-out, opacity 300ms linear; opacity: 1; @@ -3988,13 +3990,13 @@ a.status-card.compact:hover { height: 0; background: transparent; border: 0; - border-top: 1px solid lighten($ui-base-color, 12%); + border-top: 1px solid lighten($ui-base-color, 8%); margin: 10px 0; } } .column-header__collapsible-inner { - background: lighten($ui-base-color, 8%); + background: $ui-base-color; padding: 15px; } @@ -4407,17 +4409,13 @@ a.status-card.compact:hover { color: $primary-text-color; margin-bottom: 4px; display: block; - background-color: $base-overlay-background; - text-transform: uppercase; + background-color: rgba($black, 0.45); + backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); font-size: 11px; - font-weight: 500; - padding: 4px; + text-transform: uppercase; + font-weight: 700; + padding: 2px 6px; border-radius: 4px; - opacity: 0.7; - - &:hover { - opacity: 1; - } } .setting-toggle { @@ -4477,6 +4475,7 @@ a.status-card.compact:hover { .follow_requests-unlocked_explanation { background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); contain: initial; flex-grow: 0; } @@ -6161,6 +6160,7 @@ a.status-card.compact:hover { display: block; color: $white; background: rgba($black, 0.65); + backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); padding: 2px 6px; border-radius: 4px; font-size: 11px; @@ -6838,24 +6838,6 @@ a.status-card.compact:hover { } } } - - &.directory__section-headline { - background: darken($ui-base-color, 2%); - border-bottom-color: transparent; - - a, - button { - &.active { - &::before { - display: none; - } - - &::after { - border-color: transparent transparent darken($ui-base-color, 7%); - } - } - } - } } .filter-form { @@ -7370,7 +7352,6 @@ noscript { .account__header { overflow: hidden; - background: lighten($ui-base-color, 4%); &.inactive { opacity: 0.5; @@ -7392,6 +7373,7 @@ noscript { height: 145px; position: relative; background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); img { object-fit: cover; @@ -7405,7 +7387,7 @@ noscript { &__bar { position: relative; padding: 0 20px; - border-bottom: 1px solid lighten($ui-base-color, 12%); + border-bottom: 1px solid lighten($ui-base-color, 8%); .avatar { display: block; @@ -7414,7 +7396,7 @@ noscript { .account__avatar { background: darken($ui-base-color, 8%); - border: 2px solid lighten($ui-base-color, 4%); + border: 2px solid $ui-base-color; } } } From ed96e28c9e47a0b7e3cdf538772d37d9b16ddc89 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 26 Jun 2023 12:30:35 +0200 Subject: [PATCH 19/58] =?UTF-8?q?Fix=20compose=20form=20not=20being=20show?= =?UTF-8?q?n=20when=20clicking=20=E2=80=9CMake=20your=20first=20post?= =?UTF-8?q?=E2=80=9D=20on=20mobile=20(#25581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mastodon/actions/compose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 2ad7678caa..99610ac31f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -129,13 +129,13 @@ export function resetCompose() { }; } -export const focusCompose = (routerHistory, defaultText) => dispatch => { +export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => { dispatch({ type: COMPOSE_FOCUS, defaultText, }); - ensureComposeIsVisible(routerHistory); + ensureComposeIsVisible(getState, routerHistory); }; export function mentionCompose(account, routerHistory) { From ae30a60b1f6b7f51be38fe541e42a80ee2242d79 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Mon, 26 Jun 2023 12:31:48 +0200 Subject: [PATCH 20/58] Improve dismissable banner buttons when they dont fit on 1 line (#25580) Co-authored-by: Claire --- .../home_timeline/components/explore_prompt.jsx | 10 ++++++---- app/javascript/styles/mastodon/components.scss | 13 +++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx index 172f1a96c8..a6993c6418 100644 --- a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx +++ b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx @@ -15,9 +15,11 @@ export const ExplorePrompt = () => (

    -
    - - +
    +
    + + +
    -); \ No newline at end of file +); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 53b68a8434..3bf0b10edb 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8768,9 +8768,18 @@ noscript { &__actions { display: flex; - align-items: center; + flex-wrap: wrap; gap: 4px; - margin-top: 30px; + + &__wrapper { + display: flex; + margin-top: 30px; + } + + .button { + display: block; + flex-grow: 1; + } } .button-tertiary { From bb4756c823eef72ab6f9c52808726846a85a39f3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 26 Jun 2023 14:17:41 +0200 Subject: [PATCH 21/58] Change files to be deleted in batches instead of one-by-one (#23302) --- app/lib/attachment_batch.rb | 103 +++++++++++++++++++++ app/lib/vacuum/media_attachments_vacuum.rb | 10 +- app/services/clear_domain_media_service.rb | 30 ++---- 3 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 app/lib/attachment_batch.rb diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb new file mode 100644 index 0000000000..41dc36b64c --- /dev/null +++ b/app/lib/attachment_batch.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +class AttachmentBatch + # Maximum amount of objects you can delete in an S3 API call. It's + # important to remember that this does not correspond to the number + # of records in the batch, since records can have multiple attachments + LIMIT = 1_000 + + # Attributes generated and maintained by Paperclip (not all of them + # are always used on every class, however) + NULLABLE_ATTRIBUTES = %w( + file_name + content_type + file_size + fingerprint + created_at + updated_at + ).freeze + + # Styles that are always present even when not explicitly defined + BASE_STYLES = %i(original).freeze + + attr_reader :klass, :records, :storage_mode + + def initialize(klass, records) + @klass = klass + @records = records + @storage_mode = Paperclip::Attachment.default_options[:storage] + @attachment_names = klass.attachment_definitions.keys + end + + def delete + remove_files + batch.delete_all + end + + def clear + remove_files + batch.update_all(nullified_attributes) # rubocop:disable Rails/SkipsModelValidations + end + + private + + def batch + klass.where(id: records.map(&:id)) + end + + def remove_files + keys = [] + + logger.debug { "Preparing to delete attachments for #{records.size} records" } + + records.each do |record| + @attachment_names.each do |attachment_name| + attachment = record.public_send(attachment_name) + styles = BASE_STYLES | attachment.styles.keys + + next if attachment.blank? + + styles.each do |style| + case @storage_mode + when :s3 + logger.debug { "Adding #{attachment.path(style)} to batch for deletion" } + keys << attachment.style_name_as_path(style) + when :filesystem + logger.debug { "Deleting #{attachment.path(style)}" } + FileUtils.remove_file(attachment.path(style)) + when :fog + logger.debug { "Deleting #{attachment.path(style)}" } + attachment.directory.files.new(key: attachment.path(style)).destroy + end + end + end + end + + return unless storage_mode == :s3 + + # We can batch deletes over S3, but there is a limit of how many + # objects can be processed at once, so we have to potentially + # separate them into multiple calls. + + keys.each_slice(LIMIT) do |keys_slice| + logger.debug { "Deleting #{keys_slice.size} objects" } + + bucket.delete_objects(delete: { + objects: keys_slice.map { |key| { key: key } }, + quiet: true, + }) + end + end + + def bucket + @bucket ||= records.first.public_send(@attachment_names.first).s3_bucket + end + + def nullified_attributes + @attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil) + end + + def logger + Rails.logger + end +end diff --git a/app/lib/vacuum/media_attachments_vacuum.rb b/app/lib/vacuum/media_attachments_vacuum.rb index 7c0a85a9d9..7b21c84bbc 100644 --- a/app/lib/vacuum/media_attachments_vacuum.rb +++ b/app/lib/vacuum/media_attachments_vacuum.rb @@ -15,15 +15,15 @@ class Vacuum::MediaAttachmentsVacuum private def vacuum_cached_files! - media_attachments_past_retention_period.find_each do |media_attachment| - media_attachment.file.destroy - media_attachment.thumbnail.destroy - media_attachment.save + media_attachments_past_retention_period.find_in_batches do |media_attachments| + AttachmentBatch.new(MediaAttachment, media_attachments).clear end end def vacuum_orphaned_records! - orphaned_media_attachments.in_batches.destroy_all + orphaned_media_attachments.find_in_batches do |media_attachments| + AttachmentBatch.new(MediaAttachment, media_attachments).delete + end end def media_attachments_past_retention_period diff --git a/app/services/clear_domain_media_service.rb b/app/services/clear_domain_media_service.rb index 9e70ebe51c..7bf2d62fb0 100644 --- a/app/services/clear_domain_media_service.rb +++ b/app/services/clear_domain_media_service.rb @@ -10,14 +10,6 @@ class ClearDomainMediaService < BaseService private - def invalidate_association_caches!(status_ids) - # Normally, associated models of a status are immutable (except for accounts) - # so they are aggressively cached. After updating the media attachments to no - # longer point to a local file, we need to clear the cache to make those - # changes appear in the API and UI - Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" }) - end - def clear_media! clear_account_images! clear_account_attachments! @@ -25,31 +17,21 @@ class ClearDomainMediaService < BaseService end def clear_account_images! - blocked_domain_accounts.reorder(nil).find_each do |account| - account.avatar.destroy if account.avatar&.exists? - account.header.destroy if account.header&.exists? - account.save + blocked_domain_accounts.reorder(nil).find_in_batches do |accounts| + AttachmentBatch.new(Account, accounts).clear end end def clear_account_attachments! media_from_blocked_domain.reorder(nil).find_in_batches do |attachments| - affected_status_ids = [] - - attachments.each do |attachment| - affected_status_ids << attachment.status_id if attachment.status_id.present? - - attachment.file.destroy if attachment.file&.exists? - attachment.type = :unknown - attachment.save - end - - invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty? + AttachmentBatch.new(MediaAttachment, attachments).clear end end def clear_emojos! - emojis_from_blocked_domains.destroy_all + emojis_from_blocked_domains.find_in_batches do |custom_emojis| + AttachmentBatch.new(CustomEmoji, custom_emojis).delete + end end def blocked_domain From 7a25af64ddcfac1f4ad3fda2b6f72b03152b202e Mon Sep 17 00:00:00 2001 From: "S.H" Date: Mon, 26 Jun 2023 23:38:19 +0900 Subject: [PATCH 22/58] Remove media attachment only when file was exist (#25586) --- app/lib/attachment_batch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index 41dc36b64c..6372b01319 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -64,7 +64,7 @@ class AttachmentBatch keys << attachment.style_name_as_path(style) when :filesystem logger.debug { "Deleting #{attachment.path(style)}" } - FileUtils.remove_file(attachment.path(style)) + FileUtils.remove_file(attachment.path(style), true) when :fog logger.debug { "Deleting #{attachment.path(style)}" } attachment.directory.files.new(key: attachment.path(style)).destroy From 9caa0475f891ded573739cd00e7bc915b31abb12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:59:58 +0200 Subject: [PATCH 23/58] Update dependency react-redux to v8.1.1 (#25432) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7acecd4e3a..1bd14820a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9567,9 +9567,9 @@ react-redux-loading-bar@^5.0.4: react-lifecycles-compat "^3.0.4" react-redux@^8.0.4: - version "8.1.0" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.0.tgz#4e147339f00bbaac7196bc42bc99e6fc412846e7" - integrity sha512-CtHZzAOxi7GQvTph4dVLWwZHAWUjV2kMEQtk50OrN8z3gKxpWg3Tz7JfDw32N3Rpd7fh02z73cF6yZkK467gbQ== + version "8.1.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.1.tgz#8e740f3fd864a4cd0de5ba9cdc8ad39cc9e7c81a" + integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA== dependencies: "@babel/runtime" "^7.12.1" "@types/hoist-non-react-statics" "^3.3.1" From 0ccf6c0eb73f612f1e727bac64dddea62b296e30 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Jun 2023 09:36:11 +0200 Subject: [PATCH 24/58] Fix batch attachment deletion leaving empty directories (#25587) --- app/lib/attachment_batch.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index 6372b01319..1f87b94336 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -64,7 +64,15 @@ class AttachmentBatch keys << attachment.style_name_as_path(style) when :filesystem logger.debug { "Deleting #{attachment.path(style)}" } - FileUtils.remove_file(attachment.path(style), true) + path = attachment.path(style) + FileUtils.remove_file(path, true) + + begin + FileUtils.rmdir(File.dirname(path), parents: true) + rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES + # Ignore failure to delete a directory, with the same ignored errors + # as Paperclip + end when :fog logger.debug { "Deleting #{attachment.path(style)}" } attachment.directory.files.new(key: attachment.path(style)).destroy From d9b07b6a11b2e050e7d423f8c1fdfef4b22e810d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:19:51 +0200 Subject: [PATCH 25/58] Update dependency rails to v6.1.7.4 (#25606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 108 +++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5f3678fe58..c3eb9d4d71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,40 +18,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.3) - actionpack (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.3) - actionview (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.3) - actionpack (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.3) - activesupport (= 6.1.7.3) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -61,22 +61,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.7.3) - activesupport (= 6.1.7.3) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.3) - activesupport (= 6.1.7.3) - activerecord (6.1.7.3) - activemodel (= 6.1.7.3) - activesupport (= 6.1.7.3) - activestorage (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activesupport (= 6.1.7.3) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.3) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -412,7 +412,7 @@ GEM mime-types-data (3.2023.0218.1) mini_mime (1.1.2) mini_portile2 (2.8.2) - minitest (5.18.0) + minitest (5.18.1) msgpack (1.7.1) multi_json (1.15.0) multipart-post (2.3.0) @@ -511,20 +511,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.3) - actioncable (= 6.1.7.3) - actionmailbox (= 6.1.7.3) - actionmailer (= 6.1.7.3) - actionpack (= 6.1.7.3) - actiontext (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activemodel (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.3) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -539,9 +539,9 @@ GEM rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) From ccaa676452ac592efa07214dba5fba7137550bf8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:20:09 +0200 Subject: [PATCH 26/58] Update dependency sass to v1.63.6 (#25607) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1bd14820a8..696df8fa8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10156,9 +10156,9 @@ sass-loader@^10.2.0: semver "^7.3.2" sass@^1.62.1: - version "1.63.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.4.tgz#caf60643321044c61f6a0fe638a07abbd31cfb5d" - integrity sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ== + version "1.63.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.6.tgz#481610e612902e0c31c46b46cf2dad66943283ea" + integrity sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" From a90190f8137f441304dc1d2e17acd75058038054 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:20:46 +0200 Subject: [PATCH 27/58] Update dependency react-textarea-autosize to v8.5.0 (#25610) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 696df8fa8e..697e827764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9687,9 +9687,9 @@ react-test-renderer@^18.2.0: scheduler "^0.23.0" react-textarea-autosize@*, react-textarea-autosize@^8.4.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz#bcfc5462727014b808b14ee916c01e275e8a8335" - integrity sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q== + version "8.5.0" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.0.tgz#bb0f7faf9849850f1c20b6e7fac0309d4b92f87b" + integrity sha512-cp488su3U9RygmHmGpJp0KEt0i/+57KCK33XVPH+50swVRBhIZYh0fGduz2YLKXwl9vSKBZ9HUXcg9PQXUXqIw== dependencies: "@babel/runtime" "^7.20.13" use-composed-ref "^1.3.0" From b7f6280ef46e9e9f408b34d1e90619a029c06132 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:21:54 +0200 Subject: [PATCH 28/58] Update dependency pg-connection-string to v2.6.1 (#25605) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 697e827764..024a680e00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8760,9 +8760,9 @@ pg-cloudflare@^1.1.0: integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA== pg-connection-string@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8" - integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg== + version "2.6.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" + integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== pg-int8@1.0.1: version "1.0.1" From dbd37f129d152b49e13b7c9773ef07a259686be5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:36:21 +0200 Subject: [PATCH 29/58] Update dependency pg to v8.11.1 (#25604) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 024a680e00..770f33b842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8754,16 +8754,21 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -pg-cloudflare@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0" - integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA== +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== pg-connection-string@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== +pg-connection-string@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" + integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" @@ -8774,10 +8779,10 @@ pg-numeric@1.0.2: resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== -pg-pool@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e" - integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ== +pg-pool@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7" + integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og== pg-protocol@*, pg-protocol@^1.6.0: version "1.6.0" @@ -8809,19 +8814,19 @@ pg-types@^4.0.1: postgres-range "^1.1.1" pg@^8.5.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.0.tgz#a37e534e94b57a7ed811e926f23a7c56385f55d9" - integrity sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA== + version "8.11.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.1.tgz#297e0eb240306b1e9e4f55af8a3bae76ae4810b1" + integrity sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" - pg-connection-string "^2.6.0" - pg-pool "^3.6.0" + pg-connection-string "^2.6.1" + pg-pool "^3.6.1" pg-protocol "^1.6.0" pg-types "^2.1.0" pgpass "1.x" optionalDependencies: - pg-cloudflare "^1.1.0" + pg-cloudflare "^1.1.1" pgpass@1.x: version "1.0.5" From a9ba8263a072cdf16a18dbfc0ec8e359f308d9aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:28:07 +0200 Subject: [PATCH 30/58] Update mcr.microsoft.com/devcontainers/ruby Docker tag to v1 (#25613) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 04ac9560ca..f991036add 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # For details, see https://github.com/devcontainers/images/tree/main/src/ruby -FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye +FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye # Install Rails # RUN gem install rails webdrivers From ceca93d0d1146dd37da3cb947cf9cfdaeca0f80f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 12:16:17 +0200 Subject: [PATCH 31/58] Update dependency glob to v10.3.0 (#25608) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 770f33b842..12a992ec38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5826,9 +5826,9 @@ glob-parent@^6.0.2: is-glob "^4.0.3" glob@^10.2.5, glob@^10.2.6: - version "10.2.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.7.tgz#9dd2828cd5bc7bd861e7738d91e7113dda41d7d8" - integrity sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA== + version "10.3.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.0.tgz#763d02a894f3cdfc521b10bbbbc8e0309e750cce" + integrity sha512-AQ1/SB9HH0yCx1jXAT4vmCbTOPe5RQ+kCurjbel5xSCGhebumUv+GJZfa1rEqor3XIViqwSEmlkZCQD43RWrBg== dependencies: foreground-child "^3.1.0" jackspeak "^2.0.3" @@ -8027,9 +8027,9 @@ minimatch@^5.0.1: brace-expansion "^2.0.1" minimatch@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" - integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + version "9.0.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.2.tgz#397e387fff22f6795844d00badc903a3d5de7057" + integrity sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg== dependencies: brace-expansion "^2.0.1" @@ -10836,7 +10836,6 @@ 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== From c7c6f02ae61cdbaa8248f6264b22e1ce11aa1f35 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 27 Jun 2023 12:32:51 +0200 Subject: [PATCH 32/58] Fix suspending an already-limited domain (#25603) --- app/views/admin/domain_blocks/confirm_suspension.html.haml | 2 +- spec/features/admin/domain_blocks_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/admin/domain_blocks/confirm_suspension.html.haml b/app/views/admin/domain_blocks/confirm_suspension.html.haml index fa9272c77b..1d28ba1579 100644 --- a/app/views/admin/domain_blocks/confirm_suspension.html.haml +++ b/app/views/admin/domain_blocks/confirm_suspension.html.haml @@ -4,7 +4,7 @@ - content_for :page_title do = t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain)) -= simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f| += simple_form_for @domain_block, url: admin_domain_blocks_path, method: :post do |f| %p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain)) %ul.hint diff --git a/spec/features/admin/domain_blocks_spec.rb b/spec/features/admin/domain_blocks_spec.rb index 3cf60a48ae..c77d604ebd 100644 --- a/spec/features/admin/domain_blocks_spec.rb +++ b/spec/features/admin/domain_blocks_spec.rb @@ -53,7 +53,7 @@ describe 'blocking domains through the moderation interface' do # Confirming updates the block click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - expect(domain_block.reload.severity).to eq 'silence' + expect(domain_block.reload.severity).to eq 'suspend' end end @@ -72,7 +72,7 @@ describe 'blocking domains through the moderation interface' do # Confirming updates the block click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - expect(domain_block.reload.severity).to eq 'silence' + expect(domain_block.reload.severity).to eq 'suspend' end end end From 285a6919369e2bb1fc968e6e5b557bc62f9e53ca Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 28 Jun 2023 14:57:51 +0200 Subject: [PATCH 33/58] Remove the search button from UI header when logged out (#25631) --- app/javascript/mastodon/features/ui/components/header.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index bdd1c73052..3d249e8d4f 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -91,7 +91,6 @@ class Header extends PureComponent { content = ( <> - {location.pathname !== '/search' && } {signupButton} From 4581a528f77b06b417ea06404d7bc2eae5f04e22 Mon Sep 17 00:00:00 2001 From: jsgoldstein Date: Thu, 29 Jun 2023 07:05:21 -0400 Subject: [PATCH 34/58] Change account search to match by text when opted-in (#25599) Co-authored-by: Eugen Rochko --- app/chewy/accounts_index.rb | 52 +++++++++++++++++++------- app/models/concerns/account_search.rb | 11 ++++++ app/services/account_search_service.rb | 51 ++++++++++++++++++------- app/services/search_service.rb | 3 +- spec/services/search_service_spec.rb | 2 +- 5 files changed, 90 insertions(+), 29 deletions(-) diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a106..abde8e92f1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -2,8 +2,37 @@ class AccountsIndex < Chewy::Index settings index: { refresh_interval: '30s' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { - content: { + natural: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + verbatim: { tokenizer: 'whitespace', filter: %w(lowercase asciifolding cjk_width), }, @@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do - field :id, type: 'long' - - field :display_name, type: 'text', analyzer: 'content' do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :following_count, type: 'long', value: ->(account) { account.following_count } - field :followers_count, type: 'long', value: ->(account) { account.followers_count } - field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + field(:id, type: 'long') + field(:following_count, type: 'long') + field(:followers_count, type: 'long') + field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) + field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 67d77793fe..46cf68e1a3 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -106,6 +106,17 @@ module AccountSearch LIMIT :limit OFFSET :offset SQL + def searchable_text + PlainTextFormatter.new(note, local?).to_s if discoverable? + end + + def searchable_properties + [].tap do |properties| + properties << 'bot' if bot? + properties << 'verified' if fields.any?(&:verified?) + end + end + class_methods do def search_for(terms, limit: 10, offset: 0) tsquery = generate_query_for_search(terms) diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index dfc3a45f8f..3c9e73c124 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -9,12 +9,11 @@ class AccountSearchService < BaseService MIN_QUERY_LENGTH = 5 def call(query, account = nil, options = {}) - @acct_hint = query&.start_with?('@') - @query = query&.strip&.gsub(/\A@/, '') - @limit = options[:limit].to_i - @offset = options[:offset].to_i - @options = options - @account = account + @query = query&.strip&.gsub(/\A@/, '') + @limit = options[:limit].to_i + @offset = options[:offset].to_i + @options = options + @account = account search_service_results.compact.uniq end @@ -72,8 +71,8 @@ class AccountSearchService < BaseService end def from_elasticsearch - must_clauses = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct.edge_ngram acct) : %w(acct.edge_ngram acct display_name.edge_ngram display_name), type: 'most_fields', operator: 'and' } }] - should_clauses = [] + must_clauses = must_clause + should_clauses = should_clause if account return [] if options[:following] && following_ids.empty? @@ -88,7 +87,7 @@ class AccountSearchService < BaseService query = { bool: { must: must_clauses, should: should_clauses } } functions = [reputation_score_function, followers_score_function, time_distance_function] - records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' }) + records = AccountsIndex.query(function_score: { query: query, functions: functions }) .limit(limit_for_non_exact_results) .offset(offset) .objects @@ -133,6 +132,36 @@ class AccountSearchService < BaseService } end + def must_clause + fields = %w(username username.* display_name display_name.*) + fields << 'text' << 'text.*' if options[:use_searchable_text] + + [ + { + multi_match: { + query: terms_for_query, + fields: fields, + type: 'best_fields', + operator: 'or', + }, + }, + ] + end + + def should_clause + [ + { + multi_match: { + query: terms_for_query, + fields: %w(username username.* display_name display_name.*), + type: 'best_fields', + operator: 'and', + boost: 10, + }, + }, + ] + end + def following_ids @following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id] end @@ -182,8 +211,4 @@ class AccountSearchService < BaseService def username_complete? query.include?('@') && "@#{query}".match?(MENTION_ONLY_RE) end - - def likely_acct? - @acct_hint || username_complete? - end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index f475f81536..dad8c0b28f 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -30,7 +30,8 @@ class SearchService < BaseService @account, limit: @limit, resolve: @resolve, - offset: @offset + offset: @offset, + use_searchable_text: true ) end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 1283a23bf1..3bf7f8ce9f 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -68,7 +68,7 @@ describe SearchService, type: :service do allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) - expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false) + expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true) expect(results).to eq empty_results.merge(accounts: [account]) end end From a209d1e683c34ca945977c4b63ea29a0e3936587 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 29 Jun 2023 14:48:54 +0200 Subject: [PATCH 35/58] Fix ResolveURLService not resolving local URLs for remote content (#25637) --- app/services/resolve_url_service.rb | 21 +++++++++++++--- spec/services/resolve_url_service_spec.rb | 30 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index d8e795f3b0..d6e528654f 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -89,13 +89,28 @@ class ResolveURLService < BaseService def process_local_url recognized_params = Rails.application.routes.recognize_path(@url) - return unless recognized_params[:action] == 'show' + case recognized_params[:controller] + when 'statuses' + return unless recognized_params[:action] == 'show' - if recognized_params[:controller] == 'statuses' status = Status.find_by(id: recognized_params[:id]) check_local_status(status) - elsif recognized_params[:controller] == 'accounts' + when 'accounts' + return unless recognized_params[:action] == 'show' + Account.find_local(recognized_params[:username]) + when 'home' + return unless recognized_params[:action] == 'index' && recognized_params[:username_with_domain].present? + + if recognized_params[:any]&.match?(/\A[0-9]+\Z/) + status = Status.find_by(id: recognized_params[:any]) + check_local_status(status) + elsif recognized_params[:any].blank? + username, domain = recognized_params[:username_with_domain].gsub(/\A@/, '').split('@') + return unless username.present? && domain.present? + + Account.find_remote(username, domain) + end end end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index ad5bebb4ed..99761b6c73 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -145,5 +145,35 @@ describe ResolveURLService, type: :service do expect(subject.call(url, on_behalf_of: account)).to eq(status) end end + + context 'when searching for a local link of a remote private status' do + let(:account) { Fabricate(:account) } + let(:poster) { Fabricate(:account, username: 'foo', domain: 'example.com') } + let(:url) { 'https://example.com/@foo/42' } + let(:uri) { 'https://example.com/users/foo/statuses/42' } + let!(:status) { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) } + let(:search_url) { "https://#{Rails.configuration.x.local_domain}/@foo@example.com/#{status.id}" } + + before do + stub_request(:get, url).to_return(status: 404) if url.present? + stub_request(:get, uri).to_return(status: 404) + end + + context 'when the account follows the poster' do + before do + account.follow!(poster) + end + + it 'returns the status' do + expect(subject.call(search_url, on_behalf_of: account)).to eq(status) + end + end + + context 'when the account does not follow the poster' do + it 'does not return the status' do + expect(subject.call(search_url, on_behalf_of: account)).to be_nil + end + end + end end end From c4a8c332b20dc1a3af8e53eb86bdf5e3c1a24bba Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 30 Jun 2023 14:59:07 +0200 Subject: [PATCH 36/58] Remove `pkg-config` gem dependency (#25615) --- Gemfile | 2 -- Gemfile.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/Gemfile b/Gemfile index ad164af1e4..3feb3f9548 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,6 @@ source 'https://rubygems.org' ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' - gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' diff --git a/Gemfile.lock b/Gemfile.lock index c3eb9d4d71..f347ee19ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -478,7 +478,6 @@ GEM pg (1.5.3) pghero (3.3.3) activerecord (>= 6) - pkg-config (1.5.1) posix-spawn (0.3.15) premailer (1.21.0) addressable @@ -833,7 +832,6 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) From 8bfbd19d2b8a5929288a89fb5ea3e7a34e3a64e3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 30 Jun 2023 16:22:40 +0200 Subject: [PATCH 37/58] Update Crowdin configuration file --- crowdin.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 7cb74c4010..5cd4a744aa 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,5 @@ +skip_untranslated_strings: 1 commit_message: '[ci skip]' -skip_untranslated_strings: true - files: - source: /app/javascript/mastodon/locales/en.json translation: /app/javascript/mastodon/locales/%two_letters_code%.json From 9934949fc4c0a019fc7c7d0e7bdd6e68d496a861 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 30 Jun 2023 16:32:12 +0200 Subject: [PATCH 38/58] Fix onboarding prompt being displayed because of disconnection gaps (#25617) --- app/javascript/mastodon/features/home_timeline/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index 41e5aa3447..ae98aec0a6 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -37,7 +37,7 @@ const getHomeFeedSpeed = createSelector([ state => state.get('statuses'), ], (statusIds, pendingStatusIds, statusMap) => { const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds; - const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20); + const statuses = recentStatusIds.filter(id => id !== null).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 From 78ba12f0bf97aee817eb0ab0d2c582b187033c50 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 30 Jun 2023 17:03:25 +0200 Subject: [PATCH 39/58] Use an Immutable Record as the root state (#25584) --- app/javascript/mastodon/reducers/index.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 16047b26d8..67aa5f6c5e 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -1,3 +1,5 @@ +import { Record as ImmutableRecord } from 'immutable'; + import { loadingBarReducer } from 'react-redux-loading-bar'; import { combineReducers } from 'redux-immutable'; @@ -88,6 +90,22 @@ const reducers = { followed_tags, }; -const rootReducer = combineReducers(reducers); +// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys, +// so it is properly typed and keys can be accessed using `state.` syntax. +// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state + +// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record +const initialRootState = Object.fromEntries( + Object.entries(reducers).map(([name, reducer]) => [ + name, + reducer(undefined, { + // empty action + }), + ]) +); + +const RootStateRecord = ImmutableRecord(initialRootState, 'RootState'); + +const rootReducer = combineReducers(reducers, RootStateRecord); export { rootReducer }; From c47cdf6e17a43840844f758c919356acc5ed51ea Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 30 Jun 2023 13:09:03 -0400 Subject: [PATCH 40/58] Add index to backups on `user_id` column (#25647) --- .../20230630145300_add_index_backups_on_user_id.rb | 9 +++++++++ db/schema.rb | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20230630145300_add_index_backups_on_user_id.rb diff --git a/db/migrate/20230630145300_add_index_backups_on_user_id.rb b/db/migrate/20230630145300_add_index_backups_on_user_id.rb new file mode 100644 index 0000000000..c3d2f17707 --- /dev/null +++ b/db/migrate/20230630145300_add_index_backups_on_user_id.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIndexBackupsOnUserId < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :backups, :user_id, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 9866b10149..dbd792a617 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_05_085711) do +ActiveRecord::Schema.define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -273,6 +273,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085711) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "dump_file_size" + t.index ["user_id"], name: "index_backups_on_user_id" end create_table "blocks", force: :cascade do |t| From 683ba5ecb1beb2f5654df88dbcae3205feff820d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 15:48:16 -0400 Subject: [PATCH 41/58] Fix rails `rewhere` deprecation warning in directories api controller (#25625) --- .../api/v1/directories_controller.rb | 34 +++++- app/models/account.rb | 2 +- .../api/v1/directories_controller_spec.rb | 115 +++++++++++++++++- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c0585e8599..1109435507 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController def accounts_scope Account.discoverable.tap do |scope| - scope.merge!(Account.local) if truthy_param?(:local) - scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' - scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' - scope.merge!(Account.not_excluded_by_account(current_account)) if current_account - scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local) + scope.merge!(account_order_scope) + scope.merge!(local_account_scope) if local_accounts? + scope.merge!(account_exclusion_scope) if current_account + scope.merge!(account_domain_block_scope) if current_account && !local_accounts? end end + + def local_accounts? + truthy_param?(:local) + end + + def account_order_scope + case params[:order] + when 'new' + Account.order(id: :desc) + when 'active', nil + Account.by_recent_status + end + end + + def local_account_scope + Account.local + end + + def account_exclusion_scope + Account.not_excluded_by_account(current_account) + end + + def account_domain_block_scope + Account.not_domain_blocked_by_account(current_account) + end end diff --git a/app/models/account.rb b/app/models/account.rb index 8a606fd2a2..aa2cb395db 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -112,7 +112,7 @@ class Account < ApplicationRecord scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } - scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) } + scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) } diff --git a/spec/controllers/api/v1/directories_controller_spec.rb b/spec/controllers/api/v1/directories_controller_spec.rb index b18aedc4d1..5e21802e7a 100644 --- a/spec/controllers/api/v1/directories_controller_spec.rb +++ b/spec/controllers/api/v1/directories_controller_spec.rb @@ -5,19 +5,124 @@ require 'rails_helper' describe Api::V1::DirectoriesController do render_views - let(:user) { Fabricate(:user) } + let(:user) { Fabricate(:user, confirmed_at: nil) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } - let(:account) { Fabricate(:account) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #show' do - it 'returns http success' do - get :show + context 'with no params' do + before do + _local_unconfirmed_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: nil, approved: true), + username: 'local_unconfirmed' + ) - expect(response).to have_http_status(200) + local_unapproved_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago), + username: 'local_unapproved' + ) + local_unapproved_account.user.update(approved: false) + + _local_undiscoverable_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), + discoverable: false, + username: 'local_undiscoverable' + ) + + excluded_from_timeline_account = Fabricate( + :account, + domain: 'host.example', + discoverable: true, + username: 'remote_excluded_from_timeline' + ) + Fabricate(:block, account: user.account, target_account: excluded_from_timeline_account) + + _domain_blocked_account = Fabricate( + :account, + domain: 'test.example', + discoverable: true, + username: 'remote_domain_blocked' + ) + Fabricate(:account_domain_block, account: user.account, domain: 'test.example') + end + + it 'returns only the local discoverable account' do + local_discoverable_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), + discoverable: true, + username: 'local_discoverable' + ) + + eligible_remote_account = Fabricate( + :account, + domain: 'host.example', + discoverable: true, + username: 'eligible_remote' + ) + + get :show + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(eligible_remote_account.id.to_s) + expect(body_as_json.second[:id]).to include(local_discoverable_account.id.to_s) + end + end + + context 'when asking for local accounts only' do + it 'returns only the local accounts' do + user = Fabricate(:user, confirmed_at: 10.days.ago, approved: true) + local_account = Fabricate(:account, domain: nil, user: user) + remote_account = Fabricate(:account, domain: 'host.example') + + get :show, params: { local: '1' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(1) + expect(body_as_json.first[:id]).to include(local_account.id.to_s) + expect(response.body).to_not include(remote_account.id.to_s) + end + end + + context 'when ordered by active' do + it 'returns accounts in order of most recent status activity' do + status_old = Fabricate(:status) + travel_to 10.seconds.from_now + status_new = Fabricate(:status) + + get :show, params: { order: 'active' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(status_new.account.id.to_s) + expect(body_as_json.second[:id]).to include(status_old.account.id.to_s) + end + end + + context 'when ordered by new' do + it 'returns accounts in order of creation' do + account_old = Fabricate(:account) + travel_to 10.seconds.from_now + account_new = Fabricate(:account) + + get :show, params: { order: 'new' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(account_new.id.to_s) + expect(body_as_json.second[:id]).to include(account_old.id.to_s) + end end end end From f8bd5811263aace0ea979c5be66d14c72d1bb9ad Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 15:48:53 -0400 Subject: [PATCH 42/58] Remove unused routes (#25578) --- config/routes.rb | 4 +--- config/routes/admin.rb | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index f11fcdc237..feb24bdd2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,8 +103,6 @@ Rails.application.routes.draw do resources :followers, only: [:index], controller: :follower_accounts resources :following, only: [:index], controller: :following_accounts - resource :follow, only: [:create], controller: :account_follow - resource :unfollow, only: [:create], controller: :account_unfollow resource :outbox, only: [:show], module: :activitypub resource :inbox, only: [:create], module: :activitypub @@ -164,7 +162,7 @@ Rails.application.routes.draw do get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false resource :authorize_interaction, only: [:show, :create] - resource :share, only: [:show, :create] + resource :share, only: [:show] draw(:admin) diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 90e892e4b9..b6e945c4c3 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -3,7 +3,7 @@ namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :domain_allows, only: [:new, :create, :show, :destroy] + resources :domain_allows, only: [:new, :create, :destroy] resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do collection do post :batch @@ -31,7 +31,7 @@ namespace :admin do end resources :action_logs, only: [:index] - resources :warning_presets, except: [:new] + resources :warning_presets, except: [:new, :show] resources :announcements, except: [:show] do member do @@ -75,7 +75,7 @@ namespace :admin do end end - resources :rules + resources :rules, only: [:index, :create, :edit, :update, :destroy] resources :webhooks do member do From 0139b1c8e1aa6cde4fcabd56ededaa0e5ab40a68 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 18:04:21 -0400 Subject: [PATCH 43/58] Update uri to version 0.12.2 (CVE fix) (#25657) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f347ee19ca..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -716,7 +716,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uri (0.12.1) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) From cea9db5a0bfa0201b082e9f829fb4b3089ac0838 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 00:05:10 +0200 Subject: [PATCH 44/58] Change local and federated timelines to be in a single firehose column (#25641) --- .../mastodon/features/firehose/index.jsx | 210 ++++++++++++++++++ .../ui/components/navigation_panel.jsx | 12 +- app/javascript/mastodon/features/ui/index.jsx | 10 +- .../features/ui/util/async-components.js | 4 + app/javascript/mastodon/locales/en.json | 6 +- app/javascript/mastodon/reducers/settings.js | 4 + 6 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 app/javascript/mastodon/features/firehose/index.jsx diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx new file mode 100644 index 0000000000..e8e399f787 --- /dev/null +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -0,0 +1,210 @@ +import PropTypes from 'prop-types'; +import { useRef, useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; +import { NavLink } from 'react-router-dom'; + +import { addColumn } from 'mastodon/actions/columns'; +import { changeSetting } from 'mastodon/actions/settings'; +import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; +import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; +import DismissableBanner from 'mastodon/components/dismissable_banner'; +import initialState, { domain } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import SettingToggle from '../notifications/components/setting_toggle'; +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, +}); + +// TODO: use a proper React context later on +const useIdentity = () => ({ + signedIn: !!initialState.meta.me, + accountId: initialState.meta.me, + disabledAccountId: initialState.meta.disabled_account_id, + accessToken: initialState.meta.access_token, + permissions: initialState.role ? initialState.role.permissions : 0, +}); + +const ColumnSettings = () => { + const dispatch = useAppDispatch(); + const settings = useAppSelector((state) => state.getIn(['settings', 'firehose'])); + const onChange = useCallback( + (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)), + [dispatch], + ); + + return ( +
    +
    + } + /> +
    +
    + ); +}; + +const Firehose = ({ feedType, multiColumn }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const { signedIn } = useIdentity(); + const columnRef = useRef(null); + + const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false)); + const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0); + + const handlePin = useCallback( + () => { + switch(feedType) { + case 'community': + dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); + break; + case 'public': + dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); + break; + case 'public:remote': + dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } })); + break; + } + }, + [dispatch, onlyMedia, feedType], + ); + + const handleLoadMore = useCallback( + (maxId) => { + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + break; + case 'public': + dispatch(expandPublicTimeline({ maxId, onlyMedia })); + break; + case 'public:remote': + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true })); + break; + } + }, + [dispatch, onlyMedia, feedType], + ); + + const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []); + + useEffect(() => { + let disconnect; + + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + break; + case 'public': + dispatch(expandPublicTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia })); + } + break; + case 'public:remote': + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true })); + } + break; + } + + return () => disconnect?.(); + }, [dispatch, signedIn, feedType, onlyMedia]); + + const prependBanner = feedType === 'community' ? ( + + + + ) : ( + + + + ); + + const emptyMessage = feedType === 'community' ? ( + + ) : ( + + ); + + return ( + + + + + +
    +
    + + + + + + + + + + + +
    + + +
    + + + {intl.formatMessage(messages.title)} + + +
    + ); +} + +Firehose.propTypes = { + multiColumn: PropTypes.bool, + feedType: PropTypes.string, +}; + +export default Firehose; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 4de6c2ae63..d5e98461aa 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -20,8 +20,7 @@ const messages = defineMessages({ home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, explore: { id: 'explore.title', defaultMessage: 'Explore' }, - local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' }, - federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' }, + firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, @@ -43,6 +42,10 @@ class NavigationPanel extends Component { intl: PropTypes.object.isRequired, }; + isFirehoseActive = (match, location) => { + return match || location.pathname.startsWith('/public'); + }; + render () { const { intl } = this.props; const { signedIn, disabledAccountId } = this.context.identity; @@ -69,10 +72,7 @@ class NavigationPanel extends Component { )} {(signedIn || timelinePreview) && ( - <> - - - + )} {!signedIn && ( diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index d40fefb39f..59327f0496 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -36,8 +36,7 @@ import { Status, GettingStarted, KeyboardShortcuts, - PublicTimeline, - CommunityTimeline, + Firehose, AccountTimeline, AccountGallery, HomeTimeline, @@ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent { - - + + + + + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index c1774512a0..7b968204be 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -22,6 +22,10 @@ export function CommunityTimeline () { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); } +export function Firehose () { + return import(/* webpackChunkName: "features/firehose" */'../../firehose'); +} + export function HashtagTimeline () { return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index da3b6e19eb..f1617a2040 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -114,6 +114,7 @@ "column.directory": "Browse profiles", "column.domain_blocks": "Blocked domains", "column.favourites": "Favourites", + "column.firehose": "Live feeds", "column.follow_requests": "Follow requests", "column.home": "Home", "column.lists": "Lists", @@ -267,6 +268,9 @@ "filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.title": "Filter this post", "filter_modal.title.status": "Filter a post", + "firehose.all": "All", + "firehose.local": "Local", + "firehose.remote": "Remote", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", @@ -649,9 +653,7 @@ "subscribed_languages.target": "Change subscribed languages for {target}", "suggestions.dismiss": "Dismiss suggestion", "suggestions.header": "You might be interested in…", - "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 3641c00a45..07d1bda0f4 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -79,6 +79,10 @@ const initialState = ImmutableMap({ }), }), + firehose: ImmutableMap({ + onlyMedia: false, + }), + community: ImmutableMap({ regex: ImmutableMap({ body: '', From 4fe2d7cb59f4622ff8af2f048b883f413e87c68e Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Sat, 1 Jul 2023 19:05:44 -0300 Subject: [PATCH 45/58] Fix HTTP 500 in `/api/v1/emails/check_confirmation` (#25595) --- app/controllers/api/v1/emails/confirmations_controller.rb | 1 + .../api/v1/emails/confirmations_controller_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 29ff897b91..16e91b4497 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check before_action :require_user_owned_by_application!, except: :check before_action :require_user_not_confirmed!, except: :check + before_action :require_authenticated_user!, only: :check def create current_user.update!(email: params[:email]) if params.key?(:email) diff --git a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb index 219b5075df..80d6c8799d 100644 --- a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb +++ b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb @@ -130,5 +130,13 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do end end end + + context 'without an oauth token and an authentication cookie' do + it 'returns http unauthorized' do + get :check + + expect(response).to have_http_status(401) + end + end end end From 50c2a036952ffb9d036230388931a6aca3f00c45 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sun, 2 Jul 2023 04:38:53 -0400 Subject: [PATCH 46/58] Rails 7 update (#24241) --- Gemfile | 4 +- Gemfile.lock | 128 +++++++++--------- app/lib/inline_renderer.rb | 2 +- app/lib/rss/channel.rb | 2 +- app/lib/rss/item.rb | 2 +- app/models/announcement.rb | 2 +- app/models/concerns/account_search.rb | 4 +- .../concerns/status_safe_reblog_insert.rb | 48 +++---- app/models/notification.rb | 2 +- app/serializers/initial_state_serializer.rb | 5 +- app/services/account_search_service.rb | 2 +- app/services/batched_remove_status_service.rb | 10 +- config/application.rb | 10 +- config/environments/development.rb | 38 ++++-- config/environments/production.rb | 35 ++++- config/environments/test.rb | 45 ++++-- config/initializers/assets.rb | 9 +- .../initializers/filter_parameter_logging.rb | 8 +- .../new_framework_defaults_7_0.rb | 10 ++ db/schema.rb | 2 +- package.json | 2 +- yarn.lock | 16 +-- 22 files changed, 242 insertions(+), 144 deletions(-) create mode 100644 config/initializers/new_framework_defaults_7_0.rb diff --git a/Gemfile b/Gemfile index 3feb3f9548..aa5e39f316 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.0.0' gem 'puma', '~> 6.3' -gem 'rails', '~> 6.1.7' +gem 'rails', '~> 7.0' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' @@ -67,7 +67,7 @@ gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 6.0' +gem 'rails-i18n', '~> 7.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] diff --git a/Gemfile.lock b/Gemfile.lock index b2d75e9d4a..8048e0c953 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,40 +18,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.4) - actionpack (= 6.1.7.4) - activesupport (= 6.1.7.4) + actioncable (7.0.6) + actionpack (= 7.0.6) + activesupport (= 7.0.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.4) - actionpack (= 6.1.7.4) - activejob (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + actionmailbox (7.0.6) + actionpack (= 7.0.6) + activejob (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) mail (>= 2.7.1) - actionmailer (6.1.7.4) - actionpack (= 6.1.7.4) - actionview (= 6.1.7.4) - activejob (= 6.1.7.4) - activesupport (= 6.1.7.4) + net-imap + net-pop + net-smtp + actionmailer (7.0.6) + actionpack (= 7.0.6) + actionview (= 7.0.6) + activejob (= 7.0.6) + activesupport (= 7.0.6) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.7.4) - actionview (= 6.1.7.4) - activesupport (= 6.1.7.4) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.6) + actionview (= 7.0.6) + activesupport (= 7.0.6) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.4) - actionpack (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + actiontext (7.0.6) + actionpack (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.7.4) - activesupport (= 6.1.7.4) + actionview (7.0.6) + activesupport (= 7.0.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -61,27 +68,26 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.7.4) - activesupport (= 6.1.7.4) + activejob (7.0.6) + activesupport (= 7.0.6) globalid (>= 0.3.6) - activemodel (6.1.7.4) - activesupport (= 6.1.7.4) - activerecord (6.1.7.4) - activemodel (= 6.1.7.4) - activesupport (= 6.1.7.4) - activestorage (6.1.7.4) - actionpack (= 6.1.7.4) - activejob (= 6.1.7.4) - activerecord (= 6.1.7.4) - activesupport (= 6.1.7.4) + activemodel (7.0.6) + activesupport (= 7.0.6) + activerecord (7.0.6) + activemodel (= 7.0.6) + activesupport (= 7.0.6) + activestorage (7.0.6) + actionpack (= 7.0.6) + activejob (= 7.0.6) + activerecord (= 7.0.6) + activesupport (= 7.0.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.4) + activesupport (7.0.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) @@ -510,21 +516,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.4) - actioncable (= 6.1.7.4) - actionmailbox (= 6.1.7.4) - actionmailer (= 6.1.7.4) - actionpack (= 6.1.7.4) - actiontext (= 6.1.7.4) - actionview (= 6.1.7.4) - activejob (= 6.1.7.4) - activemodel (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + rails (7.0.6) + actioncable (= 7.0.6) + actionmailbox (= 7.0.6) + actionmailer (= 7.0.6) + actionpack (= 7.0.6) + actiontext (= 7.0.6) + actionview (= 7.0.6) + activejob (= 7.0.6) + activemodel (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) bundler (>= 1.15.0) - railties (= 6.1.7.4) - sprockets-rails (>= 2.0.0) + railties (= 7.0.6) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -535,15 +540,16 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (6.0.0) + rails-i18n (7.0.7) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) - railties (6.1.7.4) - actionpack (= 6.1.7.4) - activesupport (= 6.1.7.4) + railties (>= 6.0.0, < 8) + railties (7.0.6) + actionpack (= 7.0.6) + activesupport (= 7.0.6) method_source rake (>= 12.2) thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rdf (3.2.11) @@ -690,7 +696,7 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (1.2.2) tilt (2.2.0) - timeout (0.3.2) + timeout (0.4.0) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -842,9 +848,9 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 6.1.7) + rails (~> 7.0) rails-controller-testing (~> 1.0) - rails-i18n (~> 6.0) + rails-i18n (~> 7.0) rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 4bb240b48b..eda3da2c29 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ class InlineRenderer private def preload_associations_for_status - ActiveRecord::Associations::Preloader.new.preload(@object, { + ActiveRecord::Associations::Preloader.new(records: @object, associations: { active_mentions: :account, reblog: { diff --git a/app/lib/rss/channel.rb b/app/lib/rss/channel.rb index 1dba94e47e..9013ed066a 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ class RSS::Channel < RSS::Element end def last_build_date(date) - append_element('lastBuildDate', date.to_formatted_s(:rfc822)) + append_element('lastBuildDate', date.to_fs(:rfc822)) end def image(url, title, link) diff --git a/app/lib/rss/item.rb b/app/lib/rss/item.rb index c02991ace2..6739a2c184 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ class RSS::Item < RSS::Element end def pub_date(date) - append_element('pubDate', date.to_formatted_s(:rfc822)) + append_element('pubDate', date.to_fs(:rfc822)) end def description(str) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 339f5ae70c..c5d6dd62e1 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ class Announcement < ApplicationRecord end end - ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) + ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji) records end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 46cf68e1a3..9f7720f11b 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ module AccountSearch tsquery = generate_query_for_search(terms) find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) end end @@ -131,7 +131,7 @@ module AccountSearch sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) end end diff --git a/app/models/concerns/status_safe_reblog_insert.rb b/app/models/concerns/status_safe_reblog_insert.rb index a7ccb52e9a..5d464697c5 100644 --- a/app/models/concerns/status_safe_reblog_insert.rb +++ b/app/models/concerns/status_safe_reblog_insert.rb @@ -4,41 +4,41 @@ module StatusSafeReblogInsert extend ActiveSupport::Concern class_methods do - # This is a hack to ensure that no reblogs of discarded statuses are created, - # as this cannot be enforced through database constraints the same way we do - # for reblogs of deleted statuses. + # This patch overwrites the built-in ActiveRecord `_insert_record` method to + # ensure that no reblogs of discarded statuses are created, as this cannot be + # enforced through DB constraints the same way as reblogs of deleted statuses # - # To achieve this, we redefine the internal method responsible for issuing - # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query - # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" - # clause on the reblogged status to ensure consistency at the database level. + # We redefine the internal method responsible for issuing the `INSERT` + # statement and replace the `INSERT INTO ... VALUES ...` query with an `INSERT + # INTO ... SELECT ...` query with a `WHERE deleted_at IS NULL` clause on the + # reblogged status to ensure consistency at the database level. # - # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence - # code, and actually calls it if we are not handling a reblog. + # The code is kept similar to ActiveRecord::Persistence code and calls it + # directly when we are not handling a reblog. def _insert_record(values) - return super unless values.is_a?(Hash) && values['reblog_of_id'].present? + return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present? primary_key = self.primary_key primary_key_value = nil - if primary_key - primary_key_value = values[primary_key] - - if !primary_key_value && prefetch_primary_key? + if prefetch_primary_key? && primary_key + values[primary_key] ||= begin primary_key_value = next_sequence_value - values[primary_key] = primary_key_value + _default_attributes[primary_key].with_cast_value(primary_key_value) end end - # The following line is where we differ from stock ActiveRecord implementation + # The following line departs from stock ActiveRecord + # Original code was: + # im.insert(values.transform_keys { |name| arel_table[name] }) + # Instead, we use a custom builder when a reblog is happening: im = _compile_reblog_insert(values) - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? - - result + connection.insert(im, "#{self} Create", primary_key || false, primary_key_value).tap do |result| + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? + end end def _compile_reblog_insert(values) @@ -54,9 +54,9 @@ module StatusSafeReblogInsert binds = [] reblog_bind = nil - values.each do |name, value| + values.each do |name, attribute| attr = arel_table[name] - bind = predicate_builder.build_bind_attribute(attr.name, value) + bind = predicate_builder.build_bind_attribute(attr.name, attribute.value) im.columns << attr binds << bind diff --git a/app/models/notification.rb b/app/models/notification.rb index 5527953afc..60f834a633 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ class Notification < ApplicationRecord # Instead of using the usual `includes`, manually preload each type. # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more. - ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) + ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations) end unique_target_statuses = notifications.filter_map(&:target_status).uniq diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 769ba653ed..16a9ac7c50 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,7 +83,10 @@ class InitialStateSerializer < ActiveModel::Serializer def accounts store = {} - ActiveRecord::Associations::Preloader.new.preload([object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, [:account_stat, :user, { moved_to_account: [:account_stat, :user] }]) + ActiveRecord::Associations::Preloader.new( + records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, + associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }] + ) store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index 3c9e73c124..c4216e2fc7 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ class AccountSearchService < BaseService .objects .compact - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) records rescue Faraday::ConnectionFailed, Parslet::ParseFailed diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 7e9b671266..f5cb339cdf 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -8,7 +8,10 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API def call(statuses, **options) - ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]) + ActiveRecord::Associations::Preloader.new( + records: statuses, + associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account] + ) statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -17,7 +20,10 @@ class BatchedRemoveStatusService < BaseService # rely on direct visibility statuses being relatively rare. statuses_with_account_conversations = statuses.select(&:direct_visibility?) - ActiveRecord::Associations::Preloader.new.preload(statuses_with_account_conversations, [mentions: :account]) + ActiveRecord::Associations::Preloader.new( + records: statuses_with_account_conversations, + associations: [mentions: :account] + ) statuses_with_account_conversations.each(&:unlink_from_conversations!) diff --git a/config/application.rb b/config/application.rb index d3c99baa12..7f8da1a95f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,7 +57,15 @@ require_relative '../lib/mastodon/redis_config' module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + config.load_defaults 7.0 + + # TODO: Release a version which uses the 7.0 defaults as specified above, + # but preserves the 6.1 cache format as set below. In a subsequent change, + # remove this line setting to 6.1 cache format, and then release another version. + # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format + # https://github.com/mastodon/mastodon/pull/24241#discussion_r1162890242 + config.active_support.cache_format_version = 6.1 + config.add_autoload_paths_to_load_path = false # Settings in config/environments/* take precedence over those specified here. diff --git a/config/environments/development.rb b/config/environments/development.rb index 306324c046..9a36d3ec4d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,8 +1,10 @@ +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -12,13 +14,22 @@ Rails.application.configure do # Show full error reports. config.consider_all_requests_local = true + # Enable server timing + config.server_timing = true + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}", + } else config.action_controller.perform_caching = false + config.cache_store = :null_store end @@ -41,12 +52,19 @@ Rails.application.configure do # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. @@ -57,12 +75,14 @@ Rails.application.configure do # Raises helpful error messages. config.assets.raise_runtime_errors = true - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true - # Use an evented file watcher to asynchronously detect changes in source code, - # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true config.action_mailer.default_options = { from: 'notifications@localhost' } diff --git a/config/environments/production.rb b/config/environments/production.rb index 018d3c1c22..a3fa1a4d27 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -19,20 +21,28 @@ Rails.application.configure do # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - ActiveSupport::Logger.new(STDOUT).tap do |logger| - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + # Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present? + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Allow to specify public IP of reverse proxy if it's needed config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true config.ssl_options = { redirect: { @@ -40,6 +50,8 @@ Rails.application.configure do } } + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym @@ -50,6 +62,12 @@ Rails.application.configure do # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "mastodon_production" + + config.action_mailer.perform_caching = false + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false @@ -73,6 +91,15 @@ Rails.application.configure do end end + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + ActiveSupport::Logger.new(STDOUT).tap do |logger| + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index 08cc4c4d3c..bb4caad526 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,25 +1,32 @@ +require 'active_support/core_ext/integer/time' + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! + # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV['CI'].present? - config.assets.digest = false + config.assets_digest = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false - - # The default store, file_store is shared by processes parallelly executed - # and should not be used. config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. @@ -27,6 +34,7 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false config.action_mailer.default_options = { from: 'notifications@localhost' } @@ -46,8 +54,8 @@ Rails.application.configure do config.x.vapid_private_key = vapid_key.private_key config.x.vapid_public_key = vapid_key.public_key - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise config.i18n.default_locale = :en config.i18n.fallbacks = true @@ -57,6 +65,15 @@ Rails.application.configure do # Ref: https://github.com/mastodon/mastodon/issues/23644 10.times { |i| Status.allocate.instance_variable_set(:"@ivar_#{i}", nil) } end + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension') diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 53b39718da..ea5315c62d 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,11 +3,12 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path -# Rails.application.config.assets.paths << 'node_modules' +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w() +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) Rails.application.config.assets.initialize_on_precompile = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 06cb15bbb1..adc6568ce8 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password, :private_key, :public_key, :otp_attempt] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb new file mode 100644 index 0000000000..edaf819447 --- /dev/null +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# TODO +# The Rails 7.0 framework default here is to set this true. However, we have a +# location in devise that redirects where we don't have an easy ability to +# override a method or set a config option, but where the redirect does not +# provide this option. +# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28 +# Once a solution is found, this line can be removed. +Rails.application.config.action_controller.raise_on_open_redirects = false diff --git a/db/schema.rb b/db/schema.rb index dbd792a617..05db75215a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_30_145300) do +ActiveRecord::Schema[6.1].define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/package.json b/package.json index 49e9c7f743..7f8767404a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", - "@rails/ujs": "^6.1.7", + "@rails/ujs": "^7.0.6", "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "arrow-key-navigation": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 12a992ec38..06066033c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1719,10 +1719,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rails/ujs@^6.1.7": - version "6.1.7" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.7.tgz#b09dc5b2105dd267e8374c47e4490240451dc7f6" - integrity sha512-0e7WQ4LE/+LEfW2zfAw9ppsB6A8RmxbdAUPAF++UT80epY+7emuQDkKXmaK0a9lp6An50RvzezI0cIQjp1A58w== +"@rails/ujs@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.6.tgz#fd8937c92335f3da9495e07292511ad5f7547a6a" + integrity sha512-s5v3AC6AywOIFMz0RIMW83Xc8FPIvKMkP3ZHFlM4ISNkhdUwP9HdhVtxxo6z3dIhe9vI0Our2A8kN/QpUV02Qg== "@redis/bloom@1.2.0": version "1.2.0" @@ -8759,12 +8759,7 @@ pg-cloudflare@^1.1.1: resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== -pg-connection-string@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" - integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== - -pg-connection-string@^2.6.1: +pg-connection-string@^2.6.0, pg-connection-string@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== @@ -10836,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== From 0512537eb6722f1d52a690a6e1b22c8d0a99103b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Jul 2023 10:39:55 +0200 Subject: [PATCH 47/58] Change dropdown icon above compose form from ellipsis to bars in web UI (#25661) --- .../mastodon/features/compose/components/action_bar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index 726b5aa30d..ac84014e48 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -60,7 +60,7 @@ class ActionBar extends PureComponent { return (
    - +
    ); From 5b463454593e1aa3ab3d23446cdf57d606e933d5 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Sun, 2 Jul 2023 18:12:16 +0900 Subject: [PATCH 48/58] Prevent duplicate concurrent calls of `/api/*/instance` in web UI (#25663) --- app/javascript/mastodon/actions/server.js | 12 ++++++++++++ app/javascript/mastodon/features/about/index.jsx | 2 +- app/javascript/mastodon/reducers/server.js | 6 +++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js index bd784906d4..65f3efc3a7 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const fetchServer = () => (dispatch, getState) => { + if (getState().getIn(['server', 'server', 'isLoading'])) { + return; + } + dispatch(fetchServerRequest()); api(getState) @@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({ }); export const fetchExtendedDescription = () => (dispatch, getState) => { + if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { + return; + } + dispatch(fetchExtendedDescriptionRequest()); api(getState) @@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({ }); export const fetchDomainBlocks = () => (dispatch, getState) => { + if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { + return; + } + dispatch(fetchDomainBlocksRequest()); api(getState) diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 73d42479b8..aff38124b6 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -161,7 +161,7 @@ class About extends PureComponent {
    - {!isLoading && (server.get('rules').isEmpty() ? ( + {!isLoading && (server.get('rules', []).isEmpty() ? (

    ) : (
      diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js index 486314c338..2bbf0f9a30 100644 --- a/app/javascript/mastodon/reducers/server.js +++ b/app/javascript/mastodon/reducers/server.js @@ -17,15 +17,15 @@ import { const initialState = ImmutableMap({ server: ImmutableMap({ - isLoading: true, + isLoading: false, }), extendedDescription: ImmutableMap({ - isLoading: true, + isLoading: false, }), domainBlocks: ImmutableMap({ - isLoading: true, + isLoading: false, isAvailable: true, items: ImmutableList(), }), From ba06a2f1044aa81a99eb5fc509611dbb1150d43d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Jul 2023 11:14:22 +0200 Subject: [PATCH 49/58] Revert "Rails 7 update" (#25667) --- Gemfile | 4 +- Gemfile.lock | 128 +++++++++--------- app/lib/inline_renderer.rb | 2 +- app/lib/rss/channel.rb | 2 +- app/lib/rss/item.rb | 2 +- app/models/announcement.rb | 2 +- app/models/concerns/account_search.rb | 4 +- .../concerns/status_safe_reblog_insert.rb | 48 +++---- app/models/notification.rb | 2 +- app/serializers/initial_state_serializer.rb | 5 +- app/services/account_search_service.rb | 2 +- app/services/batched_remove_status_service.rb | 10 +- config/application.rb | 10 +- config/environments/development.rb | 38 ++---- config/environments/production.rb | 35 +---- config/environments/test.rb | 45 ++---- config/initializers/assets.rb | 9 +- .../initializers/filter_parameter_logging.rb | 8 +- .../new_framework_defaults_7_0.rb | 10 -- db/schema.rb | 2 +- package.json | 2 +- yarn.lock | 16 ++- 22 files changed, 144 insertions(+), 242 deletions(-) delete mode 100644 config/initializers/new_framework_defaults_7_0.rb diff --git a/Gemfile b/Gemfile index aa5e39f316..3feb3f9548 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.0.0' gem 'puma', '~> 6.3' -gem 'rails', '~> 7.0' +gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' @@ -67,7 +67,7 @@ gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 7.0' +gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] diff --git a/Gemfile.lock b/Gemfile.lock index 8048e0c953..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,47 +18,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.0.6) - actionpack (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activesupport (= 7.0.6) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.6) - actionview (= 7.0.6) - activesupport (= 7.0.6) - rack (~> 2.0, >= 2.2.4) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.6) - actionpack (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) - globalid (>= 0.6.0) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (7.0.6) - activesupport (= 7.0.6) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -68,26 +61,27 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.6) - activesupport (= 7.0.6) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (7.0.6) - activesupport (= 7.0.6) - activerecord (7.0.6) - activemodel (= 7.0.6) - activesupport (= 7.0.6) - activestorage (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activesupport (= 7.0.6) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.6) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) @@ -516,20 +510,21 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.6) - actioncable (= 7.0.6) - actionmailbox (= 7.0.6) - actionmailer (= 7.0.6) - actionpack (= 7.0.6) - actiontext (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activemodel (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 7.0.6) + railties (= 6.1.7.4) + sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -540,16 +535,15 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.7) + rails-i18n (6.0.0) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + railties (>= 6.0.0, < 7) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) - zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rdf (3.2.11) @@ -696,7 +690,7 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (1.2.2) tilt (2.2.0) - timeout (0.4.0) + timeout (0.3.2) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -848,9 +842,9 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 7.0) + rails (~> 6.1.7) rails-controller-testing (~> 1.0) - rails-i18n (~> 7.0) + rails-i18n (~> 6.0) rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index eda3da2c29..4bb240b48b 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ class InlineRenderer private def preload_associations_for_status - ActiveRecord::Associations::Preloader.new(records: @object, associations: { + ActiveRecord::Associations::Preloader.new.preload(@object, { active_mentions: :account, reblog: { diff --git a/app/lib/rss/channel.rb b/app/lib/rss/channel.rb index 9013ed066a..1dba94e47e 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ class RSS::Channel < RSS::Element end def last_build_date(date) - append_element('lastBuildDate', date.to_fs(:rfc822)) + append_element('lastBuildDate', date.to_formatted_s(:rfc822)) end def image(url, title, link) diff --git a/app/lib/rss/item.rb b/app/lib/rss/item.rb index 6739a2c184..c02991ace2 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ class RSS::Item < RSS::Element end def pub_date(date) - append_element('pubDate', date.to_fs(:rfc822)) + append_element('pubDate', date.to_formatted_s(:rfc822)) end def description(str) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index c5d6dd62e1..339f5ae70c 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ class Announcement < ApplicationRecord end end - ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji) + ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) records end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 9f7720f11b..46cf68e1a3 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ module AccountSearch tsquery = generate_query_for_search(terms) find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) end end @@ -131,7 +131,7 @@ module AccountSearch sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) end end diff --git a/app/models/concerns/status_safe_reblog_insert.rb b/app/models/concerns/status_safe_reblog_insert.rb index 5d464697c5..a7ccb52e9a 100644 --- a/app/models/concerns/status_safe_reblog_insert.rb +++ b/app/models/concerns/status_safe_reblog_insert.rb @@ -4,41 +4,41 @@ module StatusSafeReblogInsert extend ActiveSupport::Concern class_methods do - # This patch overwrites the built-in ActiveRecord `_insert_record` method to - # ensure that no reblogs of discarded statuses are created, as this cannot be - # enforced through DB constraints the same way as reblogs of deleted statuses + # This is a hack to ensure that no reblogs of discarded statuses are created, + # as this cannot be enforced through database constraints the same way we do + # for reblogs of deleted statuses. # - # We redefine the internal method responsible for issuing the `INSERT` - # statement and replace the `INSERT INTO ... VALUES ...` query with an `INSERT - # INTO ... SELECT ...` query with a `WHERE deleted_at IS NULL` clause on the - # reblogged status to ensure consistency at the database level. + # To achieve this, we redefine the internal method responsible for issuing + # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query + # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" + # clause on the reblogged status to ensure consistency at the database level. # - # The code is kept similar to ActiveRecord::Persistence code and calls it - # directly when we are not handling a reblog. + # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence + # code, and actually calls it if we are not handling a reblog. def _insert_record(values) - return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present? + return super unless values.is_a?(Hash) && values['reblog_of_id'].present? primary_key = self.primary_key primary_key_value = nil - if prefetch_primary_key? && primary_key - values[primary_key] ||= begin + if primary_key + primary_key_value = values[primary_key] + + if !primary_key_value && prefetch_primary_key? primary_key_value = next_sequence_value - _default_attributes[primary_key].with_cast_value(primary_key_value) + values[primary_key] = primary_key_value end end - # The following line departs from stock ActiveRecord - # Original code was: - # im.insert(values.transform_keys { |name| arel_table[name] }) - # Instead, we use a custom builder when a reblog is happening: + # The following line is where we differ from stock ActiveRecord implementation im = _compile_reblog_insert(values) - connection.insert(im, "#{self} Create", primary_key || false, primary_key_value).tap do |result| - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? - end + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? + + result end def _compile_reblog_insert(values) @@ -54,9 +54,9 @@ module StatusSafeReblogInsert binds = [] reblog_bind = nil - values.each do |name, attribute| + values.each do |name, value| attr = arel_table[name] - bind = predicate_builder.build_bind_attribute(attr.name, attribute.value) + bind = predicate_builder.build_bind_attribute(attr.name, value) im.columns << attr binds << bind diff --git a/app/models/notification.rb b/app/models/notification.rb index 60f834a633..5527953afc 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ class Notification < ApplicationRecord # Instead of using the usual `includes`, manually preload each type. # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more. - ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations) + ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) end unique_target_statuses = notifications.filter_map(&:target_status).uniq diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 16a9ac7c50..769ba653ed 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,10 +83,7 @@ class InitialStateSerializer < ActiveModel::Serializer def accounts store = {} - ActiveRecord::Associations::Preloader.new( - records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, - associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }] - ) + ActiveRecord::Associations::Preloader.new.preload([object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, [:account_stat, :user, { moved_to_account: [:account_stat, :user] }]) store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index c4216e2fc7..3c9e73c124 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ class AccountSearchService < BaseService .objects .compact - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) records rescue Faraday::ConnectionFailed, Parslet::ParseFailed diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index f5cb339cdf..7e9b671266 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -8,10 +8,7 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API def call(statuses, **options) - ActiveRecord::Associations::Preloader.new( - records: statuses, - associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account] - ) + ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]) statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -20,10 +17,7 @@ class BatchedRemoveStatusService < BaseService # rely on direct visibility statuses being relatively rare. statuses_with_account_conversations = statuses.select(&:direct_visibility?) - ActiveRecord::Associations::Preloader.new( - records: statuses_with_account_conversations, - associations: [mentions: :account] - ) + ActiveRecord::Associations::Preloader.new.preload(statuses_with_account_conversations, [mentions: :account]) statuses_with_account_conversations.each(&:unlink_from_conversations!) diff --git a/config/application.rb b/config/application.rb index 7f8da1a95f..d3c99baa12 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,15 +57,7 @@ require_relative '../lib/mastodon/redis_config' module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 - - # TODO: Release a version which uses the 7.0 defaults as specified above, - # but preserves the 6.1 cache format as set below. In a subsequent change, - # remove this line setting to 6.1 cache format, and then release another version. - # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format - # https://github.com/mastodon/mastodon/pull/24241#discussion_r1162890242 - config.active_support.cache_format_version = 6.1 - + config.load_defaults 6.1 config.add_autoload_paths_to_load_path = false # Settings in config/environments/* take precedence over those specified here. diff --git a/config/environments/development.rb b/config/environments/development.rb index 9a36d3ec4d..306324c046 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,10 +1,8 @@ -require 'active_support/core_ext/integer/time' - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -14,22 +12,13 @@ Rails.application.configure do # Show full error reports. config.consider_all_requests_local = true - # Enable server timing - config.server_timing = true - # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true - config.action_controller.enable_fragment_cache_logging = true - config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}", - } else config.action_controller.perform_caching = false - config.cache_store = :null_store end @@ -52,19 +41,12 @@ Rails.application.configure do # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = true - # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. @@ -75,14 +57,12 @@ Rails.application.configure do # Raises helpful error messages. config.assets.raise_runtime_errors = true - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Uncomment if you wish to allow Action Cable access from any origin. - # config.action_cable.disable_request_forgery_protection = true + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.action_mailer.default_options = { from: 'notifications@localhost' } diff --git a/config/environments/production.rb b/config/environments/production.rb index a3fa1a4d27..018d3c1c22 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/integer/time" - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -21,28 +19,20 @@ Rails.application.configure do # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - # Disable serving static files from the `/public` folder by default since - # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? - - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass + ActiveSupport::Logger.new(STDOUT).tap do |logger| + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - # Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present? - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Allow to specify public IP of reverse proxy if it's needed config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true config.ssl_options = { redirect: { @@ -50,8 +40,6 @@ Rails.application.configure do } } - # Include generic and useful information about system operation, but avoid logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym @@ -62,12 +50,6 @@ Rails.application.configure do # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS - # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "mastodon_production" - - config.action_mailer.perform_caching = false - # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false @@ -91,15 +73,6 @@ Rails.application.configure do end end - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") - - ActiveSupport::Logger.new(STDOUT).tap do |logger| - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end - # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index bb4caad526..08cc4c4d3c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,32 +1,25 @@ -require 'active_support/core_ext/integer/time' - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Turn false under Spring and add config.action_view.cache_template_loading = true. + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! config.cache_classes = true - # Eager loading loads your whole application. When running a single test locally, - # this probably isn't necessary. It's a good idea to do in a continuous integration - # system, or in some way before deploying your code. - config.eager_load = ENV['CI'].present? + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false - config.assets_digest = false - - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" - } + config.assets.digest = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + + # The default store, file_store is shared by processes parallelly executed + # and should not be used. config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. @@ -34,7 +27,6 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - config.action_mailer.perform_caching = false config.action_mailer.default_options = { from: 'notifications@localhost' } @@ -54,8 +46,8 @@ Rails.application.configure do config.x.vapid_private_key = vapid_key.private_key config.x.vapid_public_key = vapid_key.public_key - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true config.i18n.default_locale = :en config.i18n.fallbacks = true @@ -65,15 +57,6 @@ Rails.application.configure do # Ref: https://github.com/mastodon/mastodon/issues/23644 10.times { |i| Status.allocate.instance_variable_set(:"@ivar_#{i}", nil) } end - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true end Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension') diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index ea5315c62d..53b39718da 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,12 +3,11 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << 'node_modules' # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w() Rails.application.config.assets.initialize_on_precompile = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568ce8..06cb15bbb1 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,4 @@ # Be sure to restart your server when you modify this file. -# Configure parameters to be filtered from the log file. Use this to limit dissemination of -# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported -# notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password, :private_key, :public_key, :otp_attempt] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb deleted file mode 100644 index edaf819447..0000000000 --- a/config/initializers/new_framework_defaults_7_0.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -# TODO -# The Rails 7.0 framework default here is to set this true. However, we have a -# location in devise that redirects where we don't have an easy ability to -# override a method or set a config option, but where the redirect does not -# provide this option. -# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28 -# Once a solution is found, this line can be removed. -Rails.application.config.action_controller.raise_on_open_redirects = false diff --git a/db/schema.rb b/db/schema.rb index 05db75215a..dbd792a617 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[6.1].define(version: 2023_06_30_145300) do +ActiveRecord::Schema.define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/package.json b/package.json index 7f8767404a..49e9c7f743 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", - "@rails/ujs": "^7.0.6", + "@rails/ujs": "^6.1.7", "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "arrow-key-navigation": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 06066033c8..12a992ec38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1719,10 +1719,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rails/ujs@^7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.6.tgz#fd8937c92335f3da9495e07292511ad5f7547a6a" - integrity sha512-s5v3AC6AywOIFMz0RIMW83Xc8FPIvKMkP3ZHFlM4ISNkhdUwP9HdhVtxxo6z3dIhe9vI0Our2A8kN/QpUV02Qg== +"@rails/ujs@^6.1.7": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.7.tgz#b09dc5b2105dd267e8374c47e4490240451dc7f6" + integrity sha512-0e7WQ4LE/+LEfW2zfAw9ppsB6A8RmxbdAUPAF++UT80epY+7emuQDkKXmaK0a9lp6An50RvzezI0cIQjp1A58w== "@redis/bloom@1.2.0": version "1.2.0" @@ -8759,7 +8759,12 @@ pg-cloudflare@^1.1.1: resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== -pg-connection-string@^2.6.0, pg-connection-string@^2.6.1: +pg-connection-string@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" + integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== + +pg-connection-string@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== @@ -10831,7 +10836,6 @@ 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== From 180f0e6715ded984d8f14dab49f4f3f44f03ec53 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 16:08:58 +0200 Subject: [PATCH 50/58] Fix inefficient query when requesting a new confirmation email from a logged-in account (#25669) --- .../auth/confirmations_controller.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 632b624a37..3c8d09993b 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -28,6 +28,24 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? end + def create + # Since we don't allow users to request confirmation emails for other accounts when they + # are already logged in, so we can cut on the expensive queries by simply reusing the + # current user. + if user_signed_in? + self.resource = current_user + resource.send_confirmation_instructions + else + self.resource = current_user || User.send_confirmation_instructions(resource_params) + end + + if successfully_sent?(resource) + respond_with({}, location: after_resending_confirmation_instructions_path_for(resource_name)) + else + respond_with(resource) + end + end + def confirm_captcha check_captcha! do |message| flash.now[:alert] = message From 933ba1a3ebb3cd0ef9ac385ae277f6643480d7b2 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 16:56:16 +0200 Subject: [PATCH 51/58] Add superapp index on `oauth_applications` (#25670) --- .../20230702131023_add_superapp_index_to_applications.rb | 9 +++++++++ db/schema.rb | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20230702131023_add_superapp_index_to_applications.rb diff --git a/db/migrate/20230702131023_add_superapp_index_to_applications.rb b/db/migrate/20230702131023_add_superapp_index_to_applications.rb new file mode 100644 index 0000000000..f301127a3e --- /dev/null +++ b/db/migrate/20230702131023_add_superapp_index_to_applications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddSuperappIndexToApplications < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :oauth_applications, :superapp, where: 'superapp = true', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index dbd792a617..de88cb9d23 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_30_145300) do +ActiveRecord::Schema.define(version: 2023_07_02_131023) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -700,6 +700,7 @@ ActiveRecord::Schema.define(version: 2023_06_30_145300) do t.bigint "owner_id" t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" + t.index ["superapp"], name: "index_oauth_applications_on_superapp", where: "(superapp = true)" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end From e6a8faae81e8dff3d1ef74d159d280c5ea762739 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 19:41:35 +0200 Subject: [PATCH 52/58] Add users index on unconfirmed_email (#25672) --- .../auth/confirmations_controller.rb | 18 ------------------ db/schema.rb | 3 ++- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 3c8d09993b..632b624a37 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -28,24 +28,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? end - def create - # Since we don't allow users to request confirmation emails for other accounts when they - # are already logged in, so we can cut on the expensive queries by simply reusing the - # current user. - if user_signed_in? - self.resource = current_user - resource.send_confirmation_instructions - else - self.resource = current_user || User.send_confirmation_instructions(resource_params) - end - - if successfully_sent?(resource) - respond_with({}, location: after_resending_confirmation_instructions_path_for(resource_name)) - else - respond_with(resource) - end - end - def confirm_captcha check_captcha! do |message| flash.now[:alert] = message diff --git a/db/schema.rb b/db/schema.rb index de88cb9d23..64f1f93aaa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_07_02_131023) do +ActiveRecord::Schema.define(version: 2023_07_02_151753) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1097,6 +1097,7 @@ ActiveRecord::Schema.define(version: 2023_07_02_131023) do t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)" t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)" + t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)" end create_table "web_push_subscriptions", force: :cascade do |t| From 54cb679c19728c27ff2ccb94a7fa70f44e07936c Mon Sep 17 00:00:00 2001 From: Trevor Wolf Date: Mon, 3 Jul 2023 19:32:31 +1000 Subject: [PATCH 53/58] Change button colors to increase hover/focus contrast and consistency (#25677) --- .../styles/mastodon-light/diff.scss | 33 ----------- .../styles/mastodon-light/variables.scss | 13 +++++ app/javascript/styles/mastodon/admin.scss | 13 +---- .../styles/mastodon/components.scss | 55 ++++++------------- app/javascript/styles/mastodon/dashboard.scss | 4 +- app/javascript/styles/mastodon/forms.scss | 15 ++--- app/javascript/styles/mastodon/variables.scss | 29 +++++++++- 7 files changed, 66 insertions(+), 96 deletions(-) diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 91828d408a..9f33a5c9cc 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -5,19 +5,6 @@ html { scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25); } -// Change the colors of button texts -.button { - color: $white; - - &.button-alternative-2 { - color: $white; - } - - &.button-tertiary { - color: $highlight-text-color; - } -} - .simple_form .button.button-tertiary { color: $highlight-text-color; } @@ -436,26 +423,6 @@ html { color: $white; } -.button.button-tertiary { - &:hover, - &:focus, - &:active { - color: $white; - } -} - -.button.button-secondary { - border-color: $darker-text-color; - color: $darker-text-color; - - &:hover, - &:focus, - &:active { - border-color: darken($darker-text-color, 8%); - color: darken($darker-text-color, 8%); - } -} - .flash-message.warning { color: lighten($gold-star, 16%); } diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss index cae065878c..250e200fc6 100644 --- a/app/javascript/styles/mastodon-light/variables.scss +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -7,6 +7,12 @@ $classic-primary-color: #9baec8; $classic-secondary-color: #d9e1e8; $classic-highlight-color: #6364ff; +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + // Differences $success-green: lighten(#3c754d, 8%); @@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed; $ui-secondary-color: $classic-base-color !default; $ui-highlight-color: $classic-highlight-color !default; +$ui-button-secondary-color: $grey-600 !default; +$ui-button-secondary-border-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-500 !default; +$ui-button-tertiary-border-color: $blurple-500 !default; + $primary-text-color: $black !default; $darker-text-color: $classic-base-color !default; $highlight-text-color: darken($ui-highlight-color, 8%) !default; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 376cffe48a..bbb6ffdff7 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -128,7 +128,6 @@ $content-width: 840px; } &.selected { - background: darken($ui-base-color, 2%); border-radius: 4px 0 0; } } @@ -146,13 +145,9 @@ $content-width: 840px; .simple-navigation-active-leaf a { color: $primary-text-color; - background-color: darken($ui-highlight-color, 2%); + background-color: $ui-highlight-color; border-bottom: 0; border-radius: 0; - - &:hover { - background-color: $ui-highlight-color; - } } } @@ -246,12 +241,6 @@ $content-width: 840px; font-weight: 700; color: $primary-text-color; background: $ui-highlight-color; - - &:hover, - &:focus, - &:active { - background: lighten($ui-highlight-color, 4%); - } } } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 3bf0b10edb..43c9255d9e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -47,11 +47,11 @@ } .button { - background-color: darken($ui-highlight-color, 2%); + background-color: $ui-button-background-color; border: 10px none; border-radius: 4px; box-sizing: border-box; - color: $primary-text-color; + color: $ui-button-color; cursor: pointer; display: inline-block; font-family: inherit; @@ -71,14 +71,14 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &--destructive { &:active, &:focus, &:hover { - background-color: $error-red; + background-color: $ui-button-destructive-focus-background-color; transition: none; } } @@ -108,39 +108,18 @@ outline: 0 !important; } - &.button-alternative { - color: $inverted-text-color; - background: $ui-primary-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-primary-color, 4%); - } - } - - &.button-alternative-2 { - background: $ui-base-lighter-color; - - &:active, - &:focus, - &:hover { - background-color: lighten($ui-base-lighter-color, 4%); - } - } - &.button-secondary { - color: $darker-text-color; + color: $ui-button-secondary-color; background: transparent; padding: 6px 17px; - border: 1px solid lighten($ui-base-color, 12%); + border: 1px solid $ui-button-secondary-border-color; &:active, &:focus, &:hover { - background: lighten($ui-base-color, 4%); - border-color: lighten($ui-base-color, 16%); - color: lighten($darker-text-color, 4%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; + background-color: $ui-button-secondary-focus-background-color; text-decoration: none; } @@ -152,14 +131,14 @@ &.button-tertiary { background: transparent; padding: 6px 17px; - color: $highlight-text-color; - border: 1px solid $highlight-text-color; + color: $ui-button-tertiary-color; + border: 1px solid $ui-button-tertiary-border-color; &:active, &:focus, &:hover { - background: $ui-highlight-color; - color: $primary-text-color; + background-color: $ui-button-tertiary-focus-background-color; + color: $ui-button-tertiary-focus-color; border: 0; padding: 7px 18px; } @@ -5810,15 +5789,15 @@ a.status-card.compact:hover { } .button.button-secondary { - border-color: $inverted-text-color; - color: $inverted-text-color; + border-color: $ui-button-secondary-border-color; + color: $ui-button-secondary-color; flex: 0 0 auto; &:hover, &:focus, &:active { - border-color: lighten($inverted-text-color, 15%); - color: lighten($inverted-text-color, 15%); + border-color: $ui-button-secondary-focus-background-color; + color: $ui-button-secondary-focus-color; } } diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index bc34c6ec0a..36a7f44253 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -81,7 +81,7 @@ display: flex; align-items: baseline; border-radius: 4px; - background: darken($ui-highlight-color, 2%); + background: $ui-button-background-color; color: $primary-text-color; transition: all 100ms ease-in; font-size: 14px; @@ -94,7 +94,7 @@ &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; transition: all 200ms ease-out; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 81a656a602..f69b699a0a 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -511,8 +511,8 @@ code { width: 100%; border: 0; border-radius: 4px; - background: darken($ui-highlight-color, 2%); - color: $primary-text-color; + background: $ui-button-background-color; + color: $ui-button-color; font-size: 18px; line-height: inherit; height: auto; @@ -534,7 +534,7 @@ code { &:active, &:focus, &:hover { - background-color: $ui-highlight-color; + background-color: $ui-button-focus-background-color; } &:disabled:hover { @@ -542,15 +542,12 @@ code { } &.negative { - background: $error-value-color; - - &:hover { - background-color: lighten($error-value-color, 5%); - } + background: $ui-button-destructive-background-color; + &:hover, &:active, &:focus { - background-color: darken($error-value-color, 5%); + background-color: $ui-button-destructive-focus-background-color; } } } diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index d6dda1b3c7..3e1f24c490 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -1,8 +1,16 @@ // Commonly used web colors $black: #000000; // Black $white: #ffffff; // White +$red-600: #b7253d !default; // Deep Carmine +$red-500: #df405a !default; // Cerise +$blurple-600: #563acc; // Iris +$blurple-500: #6364ff; // Brand purple +$blurple-300: #858afa; // Faded Blue +$grey-600: #4e4c5a; // Trout +$grey-100: #dadaf3; // Topaz + $success-green: #79bd9a !default; // Padua -$error-red: #df405a !default; // Cerise +$error-red: $red-500 !default; // Cerise $warning-red: #ff5050 !default; // Sunset Orange $gold-star: #ca8f04 !default; // Dark Goldenrod @@ -31,6 +39,22 @@ $ui-base-lighter-color: lighten( $ui-primary-color: $classic-primary-color !default; // Lighter $ui-secondary-color: $classic-secondary-color !default; // Lightest $ui-highlight-color: $classic-highlight-color !default; +$ui-button-color: $white !default; +$ui-button-background-color: $blurple-500 !default; +$ui-button-focus-background-color: $blurple-600 !default; + +$ui-button-secondary-color: $grey-100 !default; +$ui-button-secondary-border-color: $grey-100 !default; +$ui-button-secondary-focus-background-color: $grey-600 !default; +$ui-button-secondary-focus-color: $white !default; + +$ui-button-tertiary-color: $blurple-300 !default; +$ui-button-tertiary-border-color: $blurple-300 !default; +$ui-button-tertiary-focus-background-color: $blurple-600 !default; +$ui-button-tertiary-focus-color: $white !default; + +$ui-button-destructive-background-color: $red-500 !default; +$ui-button-destructive-focus-background-color: $red-600 !default; // Variables for texts $primary-text-color: $white !default; @@ -38,7 +62,8 @@ $darker-text-color: $ui-primary-color !default; $dark-text-color: $ui-base-lighter-color !default; $secondary-text-color: $ui-secondary-color !default; $highlight-text-color: lighten($ui-highlight-color, 8%) !default; -$action-button-color: $ui-base-lighter-color !default; +$action-button-color: $ui-base-color !default; +$action-button-focus-color: $ui-base-color !default; $passive-text-color: $gold-star !default; $active-passive-text-color: $success-green !default; From 69e124e2edfc6c3b73054266257b669aa2d2446b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Jul 2023 16:51:04 +0200 Subject: [PATCH 54/58] Fix regression of icon button colors in web UI (#25679) --- app/javascript/styles/mastodon/variables.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index 3e1f24c490..68db9d5fc0 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -62,8 +62,8 @@ $darker-text-color: $ui-primary-color !default; $dark-text-color: $ui-base-lighter-color !default; $secondary-text-color: $ui-secondary-color !default; $highlight-text-color: lighten($ui-highlight-color, 8%) !default; -$action-button-color: $ui-base-color !default; -$action-button-focus-color: $ui-base-color !default; +$action-button-color: $ui-base-lighter-color !default; +$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default; $passive-text-color: $gold-star !default; $active-passive-text-color: $success-green !default; From 383c00819cce101673f6c6ede08098336024ee70 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 3 Jul 2023 13:06:57 -0300 Subject: [PATCH 55/58] Fix `/api/v2/search` not working with following query param (#25681) --- app/controllers/api/v2/search_controller.rb | 4 +-- app/services/search_service.rb | 16 +++++---- .../api/v2/search_controller_spec.rb | 33 +++++++++++++++++-- spec/services/search_service_spec.rb | 2 +- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 4d20aeb10f..35be549305 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index dad8c0b28f..05d2d0e7ce 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,12 +2,13 @@ class SearchService < BaseService def call(query, account, limit, options = {}) - @query = query&.strip - @account = account - @options = options - @limit = limit.to_i - @offset = options[:type].blank? ? 0 : options[:offset].to_i - @resolve = options[:resolve] || false + @query = query&.strip + @account = account + @options = options + @limit = limit.to_i + @offset = options[:type].blank? ? 0 : options[:offset].to_i + @resolve = options[:resolve] || false + @following = options[:following] || false default_results.tap do |results| next if @query.blank? || @limit.zero? @@ -31,7 +32,8 @@ class SearchService < BaseService limit: @limit, resolve: @resolve, offset: @offset, - use_searchable_text: true + use_searchable_text: true, + following: @following ) end diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb index bfabe8cc17..a3b92fc37a 100644 --- a/spec/controllers/api/v2/search_controller_spec.rb +++ b/spec/controllers/api/v2/search_controller_spec.rb @@ -14,13 +14,40 @@ RSpec.describe Api::V2::SearchController do end describe 'GET #index' do - before do - get :index, params: { q: 'test' } - end + let!(:bob) { Fabricate(:account, username: 'bob_test') } + let!(:ana) { Fabricate(:account, username: 'ana_test') } + let!(:tom) { Fabricate(:account, username: 'tom_test') } + let(:params) { { q: 'test' } } it 'returns http success' do + get :index, params: params + expect(response).to have_http_status(200) end + + context 'when searching accounts' do + let(:params) { { q: 'test', type: 'accounts' } } + + it 'returns all matching accounts' do + get :index, params: params + + expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s) + end + + context 'with following=true' do + let(:params) { { q: 'test', type: 'accounts', following: 'true' } } + + before do + user.account.follow!(ana) + end + + it 'returns only the followed accounts' do + get :index, params: params + + expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s) + end + end + end end end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 3bf7f8ce9f..497ec74474 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -68,7 +68,7 @@ describe SearchService, type: :service do allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) - expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true) + expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true, following: false) expect(results).to eq empty_results.merge(accounts: [account]) end end From 54a10523e2c452a0d356d70507729c67b3436e0d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Jul 2023 22:57:18 +0200 Subject: [PATCH 56/58] Change labels of live feeds tabs in web UI (#25683) --- .../mastodon/features/firehose/index.jsx | 25 ++++++++++--------- .../features/public_timeline/index.jsx | 3 ++- app/javascript/mastodon/locales/en.json | 6 ++--- config/routes.rb | 1 + 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index e8e399f787..4602213a61 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -135,12 +135,13 @@ const Firehose = ({ feedType, multiColumn }) => { /> ) : ( - - - + + + ); const emptyMessage = feedType === 'community' ? ( @@ -149,10 +150,10 @@ const Firehose = ({ feedType, multiColumn }) => { defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' /> ) : ( - + ); return ( @@ -171,11 +172,11 @@ const Firehose = ({ feedType, multiColumn }) => {
      - + - + diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index d77b76a63e..352baa8336 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { connectPublicStream } from '../../actions/streaming'; @@ -143,7 +144,7 @@ class PublicTimeline extends PureComponent { } + prepend={} 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 f1617a2040..b33c8e677e 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -202,7 +202,7 @@ "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.", + "dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.", "embed.instructions": "Embed this post on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -269,8 +269,8 @@ "filter_modal.select_filter.title": "Filter this post", "filter_modal.title.status": "Filter a post", "firehose.all": "All", - "firehose.local": "Local", - "firehose.remote": "Remote", + "firehose.local": "This server", + "firehose.remote": "Other servers", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", diff --git a/config/routes.rb b/config/routes.rb index feb24bdd2a..fa72d8b065 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,7 @@ Rails.application.routes.draw do /home /public /public/local + /public/remote /conversations /lists/(*any) /notifications From d9a5c1acfaea82570f2990ad2fc234eba6f57c60 Mon Sep 17 00:00:00 2001 From: forsamori Date: Mon, 3 Jul 2023 21:58:10 +0100 Subject: [PATCH 57/58] Add at-symbol prepended to mention span title (#25684) Co-authored-by: Sam BC --- app/javascript/mastodon/components/status_content.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 3b3a191d6c..688a456319 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -104,7 +104,7 @@ class StatusContent extends PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); + link.setAttribute('title', `@${mention.get('acct')}`); link.setAttribute('href', `/@${mention.get('acct')}`); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); From 6268188543ff685e943d65e3d1f335d9d3260658 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Tue, 4 Jul 2023 07:37:57 +0900 Subject: [PATCH 58/58] Fix local live feeds does not expand (#25694) --- app/javascript/mastodon/features/firehose/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index 4602213a61..9ba4fd5b2b 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -84,7 +84,7 @@ const Firehose = ({ feedType, multiColumn }) => { (maxId) => { switch(feedType) { case 'community': - dispatch(expandCommunityTimeline({ onlyMedia })); + dispatch(expandCommunityTimeline({ maxId, onlyMedia })); break; case 'public': dispatch(expandPublicTimeline({ maxId, onlyMedia }));