From 285a6919369e2bb1fc968e6e5b557bc62f9e53ca Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 28 Jun 2023 14:57:51 +0200 Subject: [PATCH 01/24] 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 bdd1c73052cad1..3d249e8d4f1171 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 02/24] 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 e38e14a10699ad..abde8e92f126bc 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 67d77793fef92f..46cf68e1a3e1ca 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 dfc3a45f8f7e9f..3c9e73c1244fea 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 @@ def simple_search_results 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 @@ def from_elasticsearch 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 @@ def time_distance_function } 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 @@ def exact_match? 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 f475f815366fda..dad8c0b28f791a 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -30,7 +30,8 @@ def perform_accounts_search! @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 1283a23bf14261..3bf7f8ce9fdbf8 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -68,7 +68,7 @@ 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 03/24] 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 d8e795f3b042ab..d6e528654fda59 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -89,13 +89,28 @@ def local_url? 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 ad5bebb4ed77eb..99761b6c73aa98 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -145,5 +145,35 @@ 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 04/24] Remove `pkg-config` gem dependency (#25615) --- Gemfile | 2 -- Gemfile.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/Gemfile b/Gemfile index ad164af1e49875..3feb3f95484c53 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 c3eb9d4d711c8c..f347ee19caeb7a 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 05/24] Update Crowdin configuration file --- crowdin.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 7cb74c4010210d..5cd4a744aa6f4e 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 06/24] 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 41e5aa34474341..ae98aec0a69cc0 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 07/24] 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 16047b26d84fb7..67aa5f6c5e2f37 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 08/24] 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 00000000000000..c3d2f1770748fc --- /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 9866b1014957c1..dbd792a617c73a 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 @@ 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 09/24] 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 c0585e8599a714..1109435507dfa9 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -21,11 +21,35 @@ def set_accounts 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 8a606fd2a24f9d..aa2cb395db4c4f 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 b18aedc4d1aecc..5e21802e7a3f4e 100644 --- a/spec/controllers/api/v1/directories_controller_spec.rb +++ b/spec/controllers/api/v1/directories_controller_spec.rb @@ -5,19 +5,124 @@ 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 10/24] 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 f11fcdc237ef35..feb24bdd2a937e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,8 +103,6 @@ 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 @@ 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 90e892e4b9f76b..b6e945c4c36d12 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 @@ 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 @@ 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 11/24] 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 f347ee19caeb7a..b2d75e9d4acdfc 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 12/24] 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 00000000000000..e8e399f78763eb --- /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 4de6c2ae63d0fc..d5e98461aa9092 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 d40fefb39fd49e..59327f04968dd4 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 c1774512a0328c..7b968204be3797 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 da3b6e19eb3f01..f1617a20407156 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 3641c00a4598b0..07d1bda0f4d08b 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 13/24] 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 29ff897b91b068..16e91b44977d33 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 219b5075df9f1e..80d6c8799de4b0 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 @@ 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 14/24] 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 3feb3f95484c53..aa5e39f3165dd7 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 b2d75e9d4acdfc..8048e0c953169a 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 4bb240b48b9fa0..eda3da2c29b2db 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ def self.render(object, current_account, template) 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 1dba94e47ee57c..9013ed066a1395 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ def link(str) 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 c02991ace2bd1c..6739a2c184d555 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ def link(str) 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 339f5ae70c6a22..c5d6dd62e19a72 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ def reactions(account = nil) 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 46cf68e1a3e1ca..9f7720f11bf3c5 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ def search_for(terms, limit: 10, offset: 0) 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 @@ def advanced_search_for(terms, account, limit: 10, following: false, offset: 0) 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 a7ccb52e9a91b3..5d464697c5cdbd 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 @@ def _compile_reblog_insert(values) 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 5527953afca606..60f834a633358b 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ def preload_cache_collection_target_statuses(notifications, &_block) # 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 769ba653edcc16..16a9ac7c50b15a 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,7 +83,10 @@ def compose 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 3c9e73c1244fea..c4216e2fc7aaec 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ def from_elasticsearch .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 7e9b6712665c76..f5cb339cdf747f 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 @@ def call(statuses, **options) # 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 d3c99baa1273db..7f8da1a95f9f80 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,7 +57,15 @@ 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 306324c0462612..9a36d3ec4d4a99 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 @@ # 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 @@ # 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 @@ # 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 + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = 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 + # 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 018d3c1c225a75..a3fa1a4d27f932 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 @@ # 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 @@ } } + # 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 @@ # 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 @@ 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 08cc4c4d3c202f..bb4caad526b6ae 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 @@ # 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 @@ 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 @@ # 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 53b39718da1b24..ea5315c62d1887 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 06cb15bbb11156..adc6568ce83724 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 00000000000000..edaf81944713ab --- /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 dbd792a617c73a..05db75215a76c9 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 49e9c7f743d1a7..7f8767404a0bbd 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 12a992ec3826eb..06066033c8a3a7 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 15/24] 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 726b5aa30d48bd..ac84014e48dbec 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 16/24] 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 bd784906d4edc8..65f3efc3a72a3d 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 73d42479b88d63..aff38124b6fc18 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 486314c3384532..2bbf0f9a308def 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 17/24] 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 aa5e39f3165dd7..3feb3f95484c53 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 8048e0c953169a..b2d75e9d4acdfc 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 eda3da2c29b2db..4bb240b48b9fa0 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ def self.render(object, current_account, template) 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 9013ed066a1395..1dba94e47ee57c 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ def link(str) 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 6739a2c184d555..c02991ace2bd1c 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ def link(str) 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 c5d6dd62e19a72..339f5ae70c6a22 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ def reactions(account = nil) 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 9f7720f11bf3c5..46cf68e1a3e1ca 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ def search_for(terms, limit: 10, offset: 0) 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 @@ def advanced_search_for(terms, account, limit: 10, following: false, offset: 0) 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 5d464697c5cdbd..a7ccb52e9a91b3 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 @@ def _compile_reblog_insert(values) 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 60f834a633358b..5527953afca606 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ def preload_cache_collection_target_statuses(notifications, &_block) # 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 16a9ac7c50b15a..769ba653edcc16 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,10 +83,7 @@ def compose 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 c4216e2fc7aaec..3c9e73c1244fea 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ def from_elasticsearch .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 f5cb339cdf747f..7e9b6712665c76 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 @@ def call(statuses, **options) # 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 7f8da1a95f9f80..d3c99baa1273db 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,15 +57,7 @@ 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 9a36d3ec4d4a99..306324c0462612 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 @@ # 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 @@ # 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 @@ # Raises helpful error messages. config.assets.raise_runtime_errors = true - # 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 + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = 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 a3fa1a4d27f932..018d3c1c225a75 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 @@ # 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 @@ } } - # 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 @@ # 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 @@ 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 bb4caad526b6ae..08cc4c4d3c202f 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 @@ # 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 @@ 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 @@ # 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 ea5315c62d1887..53b39718da1b24 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 adc6568ce83724..06cb15bbb11156 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 edaf81944713ab..00000000000000 --- 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 05db75215a76c9..dbd792a617c73a 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 7f8767404a0bbd..49e9c7f743d1a7 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 06066033c8a3a7..12a992ec3826eb 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 b75aa6b819214a0b15ee3c6a85583ecba9f1ae88 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 28 Jun 2023 14:57:51 +0200 Subject: [PATCH 18/24] [Glitch] Remove the search button from UI header when logged out Port 285a6919369e2bb1fc968e6e5b557bc62f9e53ca to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/features/ui/components/header.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index f2b89f3bdc6309..1ebecbd29d24fc 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -92,7 +92,6 @@ class Header extends PureComponent { content = ( <> - {location.pathname !== '/search' && } {signupButton} From 2ba4773ebe6d7c10d420b31adfadf7d2348261d3 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 30 Jun 2023 16:32:12 +0200 Subject: [PATCH 19/24] [Glitch] Fix onboarding prompt being displayed because of disconnection gaps Port 9934949fc4c0a019fc7c7d0e7bdd6e68d496a861 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/features/home_timeline/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx index 9a110f06e795bd..df25e86489afa8 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx +++ b/app/javascript/flavours/glitch/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 eb1cb8224a898e2a3e37be7c4b4b205b0dcf724e Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 30 Jun 2023 17:03:25 +0200 Subject: [PATCH 20/24] [Glitch] Use an Immutable Record as the root state Port 78ba12f0bf97aee817eb0ab0d2c582b187033c50 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/reducers/index.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/reducers/index.ts b/app/javascript/flavours/glitch/reducers/index.ts index fdfa4104f6b1a0..0bc2660b064fea 100644 --- a/app/javascript/flavours/glitch/reducers/index.ts +++ b/app/javascript/flavours/glitch/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'; @@ -92,6 +94,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 7cc2c1be296be500939cf7fcb22a78fe1051dd95 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 00:05:10 +0200 Subject: [PATCH 21/24] [Glitch] Change local and federated timelines to be in a single firehose column Port cea9db5a0bfa0201b082e9f829fb4b3089ac0838 to glitch-soc Signed-off-by: Claire --- .../glitch/features/firehose/index.jsx | 210 ++++++++++++++++++ .../ui/components/navigation_panel.jsx | 12 +- .../flavours/glitch/features/ui/index.jsx | 10 +- .../features/ui/util/async-components.js | 4 + .../flavours/glitch/reducers/settings.js | 4 + 5 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/firehose/index.jsx diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx new file mode 100644 index 00000000000000..19bb6d791ef32a --- /dev/null +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/columns'; +import { changeSetting } from 'flavours/glitch/actions/settings'; +import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming'; +import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import initialState, { domain } from 'flavours/glitch/initial_state'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/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/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index 56b6477016f579..683a2d79d92c7e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -18,8 +18,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 { onOpenSettings: PropTypes.func, }; + isFirehoseActive = (match, location) => { + return match || location.pathname.startsWith('/public'); + }; + render() { const { intl, onOpenSettings } = this.props; const { signedIn, disabledAccountId } = this.context.identity; @@ -64,10 +67,7 @@ class NavigationPanel extends Component { )} {(signedIn || timelinePreview) && ( - <> - - - + )} {!signedIn && ( diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index a6a7489e45ce98..5a14e396cc64aa 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -37,8 +37,7 @@ import { Status, GettingStarted, KeyboardShortcuts, - PublicTimeline, - CommunityTimeline, + Firehose, AccountTimeline, AccountGallery, HomeTimeline, @@ -196,8 +195,11 @@ class SwitchingColumnsArea extends PureComponent { - - + + + + + diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index 0e632bc81698d1..24e8a42a68eafe 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -22,6 +22,10 @@ export function CommunityTimeline () { return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline'); } +export function Firehose () { + return import(/* webpackChunkName: "flavours/glitch/async/firehose" */'../../firehose'); +} + export function HashtagTimeline () { return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline'); } diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index 103702d8d7dde3..0f038670eda3e2 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -84,6 +84,10 @@ const initialState = ImmutableMap({ }), }), + firehose: ImmutableMap({ + onlyMedia: false, + }), + community: ImmutableMap({ regex: ImmutableMap({ body: '', From c49e339c893372bfd3904ddbb85117549d5beed5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Jul 2023 10:39:55 +0200 Subject: [PATCH 22/24] [Glitch] Change dropdown icon above compose form from ellipsis to bars in web UI Port 0512537eb6722f1d52a690a6e1b22c8d0a99103b to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/compose/components/action_bar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index 2a6202f8462c8c..f155979ef92ca1 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -62,7 +62,7 @@ class ActionBar extends PureComponent { return (
    - +
    ); From 587ddc2c7ff4bc02a61ac258afc1d764eb81988f Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Sun, 2 Jul 2023 18:12:16 +0900 Subject: [PATCH 23/24] [Glitch] Prevent duplicate concurrent calls of `/api/*/instance` in web UI Port 5b463454593e1aa3ab3d23446cdf57d606e933d5 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/actions/server.js | 12 ++++++++++++ .../flavours/glitch/features/about/index.jsx | 2 +- app/javascript/flavours/glitch/reducers/server.js | 6 +++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index bd784906d4edc8..65f3efc3a72a3d 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index 42a3077de6a6d3..fe07a870b23d6b 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/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/flavours/glitch/reducers/server.js b/app/javascript/flavours/glitch/reducers/server.js index 0b774b5e20127e..e39e2ba48b8054 100644 --- a/app/javascript/flavours/glitch/reducers/server.js +++ b/app/javascript/flavours/glitch/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 9f3c3f5209a1e58b187a685ef0da08078d9c6bb2 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 20:24:27 +0200 Subject: [PATCH 24/24] =?UTF-8?q?Show=20local-only=20posts=20in=20?= =?UTF-8?q?=E2=80=9CAll=E2=80=9D=20by=20default,=20and=20add=20back=20opti?= =?UTF-8?q?on=20to=20toggle=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flavours/glitch/features/firehose/index.jsx | 16 ++++++++++++---- app/javascript/flavours/glitch/locales/en.json | 1 + .../flavours/glitch/reducers/settings.js | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx index 19bb6d791ef32a..9db45a0e47fb8e 100644 --- a/app/javascript/flavours/glitch/features/firehose/index.jsx +++ b/app/javascript/flavours/glitch/features/firehose/index.jsx @@ -49,6 +49,12 @@ const ColumnSettings = () => { onChange={onChange} label={} /> + } + /> ); @@ -63,6 +69,8 @@ const Firehose = ({ feedType, multiColumn }) => { const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false)); const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0); + const allowLocalOnly = useAppSelector((state) => state.getIn(['settings', 'firehose', 'allowLocalOnly'])); + const handlePin = useCallback( () => { switch(feedType) { @@ -70,14 +78,14 @@ const Firehose = ({ feedType, multiColumn }) => { dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); break; case 'public': - dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); + dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly } })); break; case 'public:remote': dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } })); break; } }, - [dispatch, onlyMedia, feedType], + [dispatch, onlyMedia, feedType, allowLocalOnly], ); const handleLoadMore = useCallback( @@ -87,7 +95,7 @@ const Firehose = ({ feedType, multiColumn }) => { dispatch(expandCommunityTimeline({ onlyMedia })); break; case 'public': - dispatch(expandPublicTimeline({ maxId, onlyMedia })); + dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly })); break; case 'public:remote': dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true })); @@ -112,7 +120,7 @@ const Firehose = ({ feedType, multiColumn }) => { case 'public': dispatch(expandPublicTimeline({ onlyMedia })); if (signedIn) { - disconnect = dispatch(connectPublicStream({ onlyMedia })); + disconnect = dispatch(connectPublicStream({ onlyMedia, allowLocalOnly })); } break; case 'public:remote': diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 07ca42339d7fb6..5553750531ee23 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -52,6 +52,7 @@ "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", + "firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"", "follow_recommendations.done": "Done", "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index 0f038670eda3e2..fcf72a0b15ec1b 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -86,6 +86,7 @@ const initialState = ImmutableMap({ firehose: ImmutableMap({ onlyMedia: false, + allowLocalOnly: true, }), community: ImmutableMap({