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 bdd1c73052..3d249e8d4f 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -91,7 +91,6 @@ class Header extends PureComponent { content = ( <> - {location.pathname !== '/search' && } {signupButton} From 4581a528f77b06b417ea06404d7bc2eae5f04e22 Mon Sep 17 00:00:00 2001 From: jsgoldstein Date: Thu, 29 Jun 2023 07:05:21 -0400 Subject: [PATCH 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 e38e14a106..abde8e92f1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -2,8 +2,37 @@ class AccountsIndex < Chewy::Index settings index: { refresh_interval: '30s' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { - content: { + natural: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + verbatim: { tokenizer: 'whitespace', filter: %w(lowercase asciifolding cjk_width), }, @@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do - field :id, type: 'long' - - field :display_name, type: 'text', analyzer: 'content' do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :following_count, type: 'long', value: ->(account) { account.following_count } - field :followers_count, type: 'long', value: ->(account) { account.followers_count } - field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + field(:id, type: 'long') + field(:following_count, type: 'long') + field(:followers_count, type: 'long') + field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) + field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 67d77793fe..46cf68e1a3 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -106,6 +106,17 @@ module AccountSearch LIMIT :limit OFFSET :offset SQL + def searchable_text + PlainTextFormatter.new(note, local?).to_s if discoverable? + end + + def searchable_properties + [].tap do |properties| + properties << 'bot' if bot? + properties << 'verified' if fields.any?(&:verified?) + end + end + class_methods do def search_for(terms, limit: 10, offset: 0) tsquery = generate_query_for_search(terms) diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index dfc3a45f8f..3c9e73c124 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -9,12 +9,11 @@ class AccountSearchService < BaseService MIN_QUERY_LENGTH = 5 def call(query, account = nil, options = {}) - @acct_hint = query&.start_with?('@') - @query = query&.strip&.gsub(/\A@/, '') - @limit = options[:limit].to_i - @offset = options[:offset].to_i - @options = options - @account = account + @query = query&.strip&.gsub(/\A@/, '') + @limit = options[:limit].to_i + @offset = options[:offset].to_i + @options = options + @account = account search_service_results.compact.uniq end @@ -72,8 +71,8 @@ class AccountSearchService < BaseService end def from_elasticsearch - must_clauses = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct.edge_ngram acct) : %w(acct.edge_ngram acct display_name.edge_ngram display_name), type: 'most_fields', operator: 'and' } }] - should_clauses = [] + must_clauses = must_clause + should_clauses = should_clause if account return [] if options[:following] && following_ids.empty? @@ -88,7 +87,7 @@ class AccountSearchService < BaseService query = { bool: { must: must_clauses, should: should_clauses } } functions = [reputation_score_function, followers_score_function, time_distance_function] - records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' }) + records = AccountsIndex.query(function_score: { query: query, functions: functions }) .limit(limit_for_non_exact_results) .offset(offset) .objects @@ -133,6 +132,36 @@ class AccountSearchService < BaseService } end + def must_clause + fields = %w(username username.* display_name display_name.*) + fields << 'text' << 'text.*' if options[:use_searchable_text] + + [ + { + multi_match: { + query: terms_for_query, + fields: fields, + type: 'best_fields', + operator: 'or', + }, + }, + ] + end + + def should_clause + [ + { + multi_match: { + query: terms_for_query, + fields: %w(username username.* display_name display_name.*), + type: 'best_fields', + operator: 'and', + boost: 10, + }, + }, + ] + end + def following_ids @following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id] end @@ -182,8 +211,4 @@ class AccountSearchService < BaseService def username_complete? query.include?('@') && "@#{query}".match?(MENTION_ONLY_RE) end - - def likely_acct? - @acct_hint || username_complete? - end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index f475f81536..dad8c0b28f 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -30,7 +30,8 @@ class SearchService < BaseService @account, limit: @limit, resolve: @resolve, - offset: @offset + offset: @offset, + use_searchable_text: true ) end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 1283a23bf1..3bf7f8ce9f 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -68,7 +68,7 @@ describe SearchService, type: :service do allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) - expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false) + expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true) expect(results).to eq empty_results.merge(accounts: [account]) end end From a209d1e683c34ca945977c4b63ea29a0e3936587 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 29 Jun 2023 14:48:54 +0200 Subject: [PATCH 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 d8e795f3b0..d6e528654f 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -89,13 +89,28 @@ class ResolveURLService < BaseService def process_local_url recognized_params = Rails.application.routes.recognize_path(@url) - return unless recognized_params[:action] == 'show' + case recognized_params[:controller] + when 'statuses' + return unless recognized_params[:action] == 'show' - if recognized_params[:controller] == 'statuses' status = Status.find_by(id: recognized_params[:id]) check_local_status(status) - elsif recognized_params[:controller] == 'accounts' + when 'accounts' + return unless recognized_params[:action] == 'show' + Account.find_local(recognized_params[:username]) + when 'home' + return unless recognized_params[:action] == 'index' && recognized_params[:username_with_domain].present? + + if recognized_params[:any]&.match?(/\A[0-9]+\Z/) + status = Status.find_by(id: recognized_params[:any]) + check_local_status(status) + elsif recognized_params[:any].blank? + username, domain = recognized_params[:username_with_domain].gsub(/\A@/, '').split('@') + return unless username.present? && domain.present? + + Account.find_remote(username, domain) + end end end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index ad5bebb4ed..99761b6c73 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -145,5 +145,35 @@ describe ResolveURLService, type: :service do expect(subject.call(url, on_behalf_of: account)).to eq(status) end end + + context 'when searching for a local link of a remote private status' do + let(:account) { Fabricate(:account) } + let(:poster) { Fabricate(:account, username: 'foo', domain: 'example.com') } + let(:url) { 'https://example.com/@foo/42' } + let(:uri) { 'https://example.com/users/foo/statuses/42' } + let!(:status) { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) } + let(:search_url) { "https://#{Rails.configuration.x.local_domain}/@foo@example.com/#{status.id}" } + + before do + stub_request(:get, url).to_return(status: 404) if url.present? + stub_request(:get, uri).to_return(status: 404) + end + + context 'when the account follows the poster' do + before do + account.follow!(poster) + end + + it 'returns the status' do + expect(subject.call(search_url, on_behalf_of: account)).to eq(status) + end + end + + context 'when the account does not follow the poster' do + it 'does not return the status' do + expect(subject.call(search_url, on_behalf_of: account)).to be_nil + end + end + end end end From c4a8c332b20dc1a3af8e53eb86bdf5e3c1a24bba Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 30 Jun 2023 14:59:07 +0200 Subject: [PATCH 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 ad164af1e4..3feb3f9548 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,6 @@ source 'https://rubygems.org' ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' - gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' diff --git a/Gemfile.lock b/Gemfile.lock index c3eb9d4d71..f347ee19ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -478,7 +478,6 @@ GEM pg (1.5.3) pghero (3.3.3) activerecord (>= 6) - pkg-config (1.5.1) posix-spawn (0.3.15) premailer (1.21.0) addressable @@ -833,7 +832,6 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) From 8bfbd19d2b8a5929288a89fb5ea3e7a34e3a64e3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 30 Jun 2023 16:22:40 +0200 Subject: [PATCH 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 7cb74c4010..5cd4a744aa 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,5 @@ +skip_untranslated_strings: 1 commit_message: '[ci skip]' -skip_untranslated_strings: true - files: - source: /app/javascript/mastodon/locales/en.json translation: /app/javascript/mastodon/locales/%two_letters_code%.json From 9934949fc4c0a019fc7c7d0e7bdd6e68d496a861 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 30 Jun 2023 16:32:12 +0200 Subject: [PATCH 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 41e5aa3447..ae98aec0a6 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -37,7 +37,7 @@ const getHomeFeedSpeed = createSelector([ state => state.get('statuses'), ], (statusIds, pendingStatusIds, statusMap) => { const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds; - const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20); + const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20); const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); const newest = new Date(statuses.getIn([0, 'created_at'], 0)); const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds From 78ba12f0bf97aee817eb0ab0d2c582b187033c50 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 30 Jun 2023 17:03:25 +0200 Subject: [PATCH 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 16047b26d8..67aa5f6c5e 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -1,3 +1,5 @@ +import { Record as ImmutableRecord } from 'immutable'; + import { loadingBarReducer } from 'react-redux-loading-bar'; import { combineReducers } from 'redux-immutable'; @@ -88,6 +90,22 @@ const reducers = { followed_tags, }; -const rootReducer = combineReducers(reducers); +// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys, +// so it is properly typed and keys can be accessed using `state.` syntax. +// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state + +// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record +const initialRootState = Object.fromEntries( + Object.entries(reducers).map(([name, reducer]) => [ + name, + reducer(undefined, { + // empty action + }), + ]) +); + +const RootStateRecord = ImmutableRecord(initialRootState, 'RootState'); + +const rootReducer = combineReducers(reducers, RootStateRecord); export { rootReducer }; From c47cdf6e17a43840844f758c919356acc5ed51ea Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 30 Jun 2023 13:09:03 -0400 Subject: [PATCH 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 0000000000..c3d2f17707 --- /dev/null +++ b/db/migrate/20230630145300_add_index_backups_on_user_id.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIndexBackupsOnUserId < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :backups, :user_id, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 9866b10149..dbd792a617 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_05_085711) do +ActiveRecord::Schema.define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -273,6 +273,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085711) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "dump_file_size" + t.index ["user_id"], name: "index_backups_on_user_id" end create_table "blocks", force: :cascade do |t| From 683ba5ecb1beb2f5654df88dbcae3205feff820d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 15:48:16 -0400 Subject: [PATCH 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 c0585e8599..1109435507 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController def accounts_scope Account.discoverable.tap do |scope| - scope.merge!(Account.local) if truthy_param?(:local) - scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' - scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' - scope.merge!(Account.not_excluded_by_account(current_account)) if current_account - scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local) + scope.merge!(account_order_scope) + scope.merge!(local_account_scope) if local_accounts? + scope.merge!(account_exclusion_scope) if current_account + scope.merge!(account_domain_block_scope) if current_account && !local_accounts? end end + + def local_accounts? + truthy_param?(:local) + end + + def account_order_scope + case params[:order] + when 'new' + Account.order(id: :desc) + when 'active', nil + Account.by_recent_status + end + end + + def local_account_scope + Account.local + end + + def account_exclusion_scope + Account.not_excluded_by_account(current_account) + end + + def account_domain_block_scope + Account.not_domain_blocked_by_account(current_account) + end end diff --git a/app/models/account.rb b/app/models/account.rb index 8a606fd2a2..aa2cb395db 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -112,7 +112,7 @@ class Account < ApplicationRecord scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } - scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) } + scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) } diff --git a/spec/controllers/api/v1/directories_controller_spec.rb b/spec/controllers/api/v1/directories_controller_spec.rb index b18aedc4d1..5e21802e7a 100644 --- a/spec/controllers/api/v1/directories_controller_spec.rb +++ b/spec/controllers/api/v1/directories_controller_spec.rb @@ -5,19 +5,124 @@ require 'rails_helper' describe Api::V1::DirectoriesController do render_views - let(:user) { Fabricate(:user) } + let(:user) { Fabricate(:user, confirmed_at: nil) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } - let(:account) { Fabricate(:account) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #show' do - it 'returns http success' do - get :show + context 'with no params' do + before do + _local_unconfirmed_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: nil, approved: true), + username: 'local_unconfirmed' + ) - expect(response).to have_http_status(200) + local_unapproved_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago), + username: 'local_unapproved' + ) + local_unapproved_account.user.update(approved: false) + + _local_undiscoverable_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), + discoverable: false, + username: 'local_undiscoverable' + ) + + excluded_from_timeline_account = Fabricate( + :account, + domain: 'host.example', + discoverable: true, + username: 'remote_excluded_from_timeline' + ) + Fabricate(:block, account: user.account, target_account: excluded_from_timeline_account) + + _domain_blocked_account = Fabricate( + :account, + domain: 'test.example', + discoverable: true, + username: 'remote_domain_blocked' + ) + Fabricate(:account_domain_block, account: user.account, domain: 'test.example') + end + + it 'returns only the local discoverable account' do + local_discoverable_account = Fabricate( + :account, + domain: nil, + user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), + discoverable: true, + username: 'local_discoverable' + ) + + eligible_remote_account = Fabricate( + :account, + domain: 'host.example', + discoverable: true, + username: 'eligible_remote' + ) + + get :show + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(eligible_remote_account.id.to_s) + expect(body_as_json.second[:id]).to include(local_discoverable_account.id.to_s) + end + end + + context 'when asking for local accounts only' do + it 'returns only the local accounts' do + user = Fabricate(:user, confirmed_at: 10.days.ago, approved: true) + local_account = Fabricate(:account, domain: nil, user: user) + remote_account = Fabricate(:account, domain: 'host.example') + + get :show, params: { local: '1' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(1) + expect(body_as_json.first[:id]).to include(local_account.id.to_s) + expect(response.body).to_not include(remote_account.id.to_s) + end + end + + context 'when ordered by active' do + it 'returns accounts in order of most recent status activity' do + status_old = Fabricate(:status) + travel_to 10.seconds.from_now + status_new = Fabricate(:status) + + get :show, params: { order: 'active' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(status_new.account.id.to_s) + expect(body_as_json.second[:id]).to include(status_old.account.id.to_s) + end + end + + context 'when ordered by new' do + it 'returns accounts in order of creation' do + account_old = Fabricate(:account) + travel_to 10.seconds.from_now + account_new = Fabricate(:account) + + get :show, params: { order: 'new' } + + expect(response).to have_http_status(200) + expect(body_as_json.size).to eq(2) + expect(body_as_json.first[:id]).to include(account_new.id.to_s) + expect(body_as_json.second[:id]).to include(account_old.id.to_s) + end end end end From f8bd5811263aace0ea979c5be66d14c72d1bb9ad Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 15:48:53 -0400 Subject: [PATCH 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 f11fcdc237..feb24bdd2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,8 +103,6 @@ Rails.application.routes.draw do resources :followers, only: [:index], controller: :follower_accounts resources :following, only: [:index], controller: :following_accounts - resource :follow, only: [:create], controller: :account_follow - resource :unfollow, only: [:create], controller: :account_unfollow resource :outbox, only: [:show], module: :activitypub resource :inbox, only: [:create], module: :activitypub @@ -164,7 +162,7 @@ Rails.application.routes.draw do get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false resource :authorize_interaction, only: [:show, :create] - resource :share, only: [:show, :create] + resource :share, only: [:show] draw(:admin) diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 90e892e4b9..b6e945c4c3 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -3,7 +3,7 @@ namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :domain_allows, only: [:new, :create, :show, :destroy] + resources :domain_allows, only: [:new, :create, :destroy] resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do collection do post :batch @@ -31,7 +31,7 @@ namespace :admin do end resources :action_logs, only: [:index] - resources :warning_presets, except: [:new] + resources :warning_presets, except: [:new, :show] resources :announcements, except: [:show] do member do @@ -75,7 +75,7 @@ namespace :admin do end end - resources :rules + resources :rules, only: [:index, :create, :edit, :update, :destroy] resources :webhooks do member do From 0139b1c8e1aa6cde4fcabd56ededaa0e5ab40a68 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 1 Jul 2023 18:04:21 -0400 Subject: [PATCH 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 f347ee19ca..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -716,7 +716,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uri (0.12.1) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) From cea9db5a0bfa0201b082e9f829fb4b3089ac0838 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 2 Jul 2023 00:05:10 +0200 Subject: [PATCH 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 0000000000..e8e399f787 --- /dev/null +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -0,0 +1,210 @@ +import PropTypes from 'prop-types'; +import { useRef, useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; +import { NavLink } from 'react-router-dom'; + +import { addColumn } from 'mastodon/actions/columns'; +import { changeSetting } from 'mastodon/actions/settings'; +import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; +import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; +import DismissableBanner from 'mastodon/components/dismissable_banner'; +import initialState, { domain } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import SettingToggle from '../notifications/components/setting_toggle'; +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, +}); + +// TODO: use a proper React context later on +const useIdentity = () => ({ + signedIn: !!initialState.meta.me, + accountId: initialState.meta.me, + disabledAccountId: initialState.meta.disabled_account_id, + accessToken: initialState.meta.access_token, + permissions: initialState.role ? initialState.role.permissions : 0, +}); + +const ColumnSettings = () => { + const dispatch = useAppDispatch(); + const settings = useAppSelector((state) => state.getIn(['settings', 'firehose'])); + const onChange = useCallback( + (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)), + [dispatch], + ); + + return ( +
+
+ } + /> +
+
+ ); +}; + +const Firehose = ({ feedType, multiColumn }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const { signedIn } = useIdentity(); + const columnRef = useRef(null); + + const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false)); + const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0); + + const handlePin = useCallback( + () => { + switch(feedType) { + case 'community': + dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); + break; + case 'public': + dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); + break; + case 'public:remote': + dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } })); + break; + } + }, + [dispatch, onlyMedia, feedType], + ); + + const handleLoadMore = useCallback( + (maxId) => { + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + break; + case 'public': + dispatch(expandPublicTimeline({ maxId, onlyMedia })); + break; + case 'public:remote': + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true })); + break; + } + }, + [dispatch, onlyMedia, feedType], + ); + + const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []); + + useEffect(() => { + let disconnect; + + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + break; + case 'public': + dispatch(expandPublicTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia })); + } + break; + case 'public:remote': + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true })); + } + break; + } + + return () => disconnect?.(); + }, [dispatch, signedIn, feedType, onlyMedia]); + + const prependBanner = feedType === 'community' ? ( + + + + ) : ( + + + + ); + + const emptyMessage = feedType === 'community' ? ( + + ) : ( + + ); + + return ( + + + + + +
+
+ + + + + + + + + + + +
+ + +
+ + + {intl.formatMessage(messages.title)} + + +
+ ); +} + +Firehose.propTypes = { + multiColumn: PropTypes.bool, + feedType: PropTypes.string, +}; + +export default Firehose; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 4de6c2ae63..d5e98461aa 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -20,8 +20,7 @@ const messages = defineMessages({ home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, explore: { id: 'explore.title', defaultMessage: 'Explore' }, - local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' }, - federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' }, + firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, @@ -43,6 +42,10 @@ class NavigationPanel extends Component { intl: PropTypes.object.isRequired, }; + isFirehoseActive = (match, location) => { + return match || location.pathname.startsWith('/public'); + }; + render () { const { intl } = this.props; const { signedIn, disabledAccountId } = this.context.identity; @@ -69,10 +72,7 @@ class NavigationPanel extends Component { )} {(signedIn || timelinePreview) && ( - <> - - - + )} {!signedIn && ( diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index d40fefb39f..59327f0496 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -36,8 +36,7 @@ import { Status, GettingStarted, KeyboardShortcuts, - PublicTimeline, - CommunityTimeline, + Firehose, AccountTimeline, AccountGallery, HomeTimeline, @@ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent { - - + + + + + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index c1774512a0..7b968204be 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -22,6 +22,10 @@ export function CommunityTimeline () { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); } +export function Firehose () { + return import(/* webpackChunkName: "features/firehose" */'../../firehose'); +} + export function HashtagTimeline () { return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index da3b6e19eb..f1617a2040 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -114,6 +114,7 @@ "column.directory": "Browse profiles", "column.domain_blocks": "Blocked domains", "column.favourites": "Favourites", + "column.firehose": "Live feeds", "column.follow_requests": "Follow requests", "column.home": "Home", "column.lists": "Lists", @@ -267,6 +268,9 @@ "filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.title": "Filter this post", "filter_modal.title.status": "Filter a post", + "firehose.all": "All", + "firehose.local": "Local", + "firehose.remote": "Remote", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", @@ -649,9 +653,7 @@ "subscribed_languages.target": "Change subscribed languages for {target}", "suggestions.dismiss": "Dismiss suggestion", "suggestions.header": "You might be interested in…", - "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 3641c00a45..07d1bda0f4 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -79,6 +79,10 @@ const initialState = ImmutableMap({ }), }), + firehose: ImmutableMap({ + onlyMedia: false, + }), + community: ImmutableMap({ regex: ImmutableMap({ body: '', From 4fe2d7cb59f4622ff8af2f048b883f413e87c68e Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Sat, 1 Jul 2023 19:05:44 -0300 Subject: [PATCH 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 29ff897b91..16e91b4497 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check before_action :require_user_owned_by_application!, except: :check before_action :require_user_not_confirmed!, except: :check + before_action :require_authenticated_user!, only: :check def create current_user.update!(email: params[:email]) if params.key?(:email) diff --git a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb index 219b5075df..80d6c8799d 100644 --- a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb +++ b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb @@ -130,5 +130,13 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do end end end + + context 'without an oauth token and an authentication cookie' do + it 'returns http unauthorized' do + get :check + + expect(response).to have_http_status(401) + end + end end end From 50c2a036952ffb9d036230388931a6aca3f00c45 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sun, 2 Jul 2023 04:38:53 -0400 Subject: [PATCH 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 3feb3f9548..aa5e39f316 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.0.0' gem 'puma', '~> 6.3' -gem 'rails', '~> 6.1.7' +gem 'rails', '~> 7.0' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' @@ -67,7 +67,7 @@ gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 6.0' +gem 'rails-i18n', '~> 7.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] diff --git a/Gemfile.lock b/Gemfile.lock index b2d75e9d4a..8048e0c953 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,40 +18,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.4) - actionpack (= 6.1.7.4) - activesupport (= 6.1.7.4) + actioncable (7.0.6) + actionpack (= 7.0.6) + activesupport (= 7.0.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.4) - actionpack (= 6.1.7.4) - activejob (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + actionmailbox (7.0.6) + actionpack (= 7.0.6) + activejob (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) mail (>= 2.7.1) - actionmailer (6.1.7.4) - actionpack (= 6.1.7.4) - actionview (= 6.1.7.4) - activejob (= 6.1.7.4) - activesupport (= 6.1.7.4) + net-imap + net-pop + net-smtp + actionmailer (7.0.6) + actionpack (= 7.0.6) + actionview (= 7.0.6) + activejob (= 7.0.6) + activesupport (= 7.0.6) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.7.4) - actionview (= 6.1.7.4) - activesupport (= 6.1.7.4) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.6) + actionview (= 7.0.6) + activesupport (= 7.0.6) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.4) - actionpack (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + actiontext (7.0.6) + actionpack (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.7.4) - activesupport (= 6.1.7.4) + actionview (7.0.6) + activesupport (= 7.0.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -61,27 +68,26 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.7.4) - activesupport (= 6.1.7.4) + activejob (7.0.6) + activesupport (= 7.0.6) globalid (>= 0.3.6) - activemodel (6.1.7.4) - activesupport (= 6.1.7.4) - activerecord (6.1.7.4) - activemodel (= 6.1.7.4) - activesupport (= 6.1.7.4) - activestorage (6.1.7.4) - actionpack (= 6.1.7.4) - activejob (= 6.1.7.4) - activerecord (= 6.1.7.4) - activesupport (= 6.1.7.4) + activemodel (7.0.6) + activesupport (= 7.0.6) + activerecord (7.0.6) + activemodel (= 7.0.6) + activesupport (= 7.0.6) + activestorage (7.0.6) + actionpack (= 7.0.6) + activejob (= 7.0.6) + activerecord (= 7.0.6) + activesupport (= 7.0.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.4) + activesupport (7.0.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) @@ -510,21 +516,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.4) - actioncable (= 6.1.7.4) - actionmailbox (= 6.1.7.4) - actionmailer (= 6.1.7.4) - actionpack (= 6.1.7.4) - actiontext (= 6.1.7.4) - actionview (= 6.1.7.4) - activejob (= 6.1.7.4) - activemodel (= 6.1.7.4) - activerecord (= 6.1.7.4) - activestorage (= 6.1.7.4) - activesupport (= 6.1.7.4) + rails (7.0.6) + actioncable (= 7.0.6) + actionmailbox (= 7.0.6) + actionmailer (= 7.0.6) + actionpack (= 7.0.6) + actiontext (= 7.0.6) + actionview (= 7.0.6) + activejob (= 7.0.6) + activemodel (= 7.0.6) + activerecord (= 7.0.6) + activestorage (= 7.0.6) + activesupport (= 7.0.6) bundler (>= 1.15.0) - railties (= 6.1.7.4) - sprockets-rails (>= 2.0.0) + railties (= 7.0.6) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -535,15 +540,16 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (6.0.0) + rails-i18n (7.0.7) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) - railties (6.1.7.4) - actionpack (= 6.1.7.4) - activesupport (= 6.1.7.4) + railties (>= 6.0.0, < 8) + railties (7.0.6) + actionpack (= 7.0.6) + activesupport (= 7.0.6) method_source rake (>= 12.2) thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rdf (3.2.11) @@ -690,7 +696,7 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (1.2.2) tilt (2.2.0) - timeout (0.3.2) + timeout (0.4.0) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -842,9 +848,9 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 6.1.7) + rails (~> 7.0) rails-controller-testing (~> 1.0) - rails-i18n (~> 6.0) + rails-i18n (~> 7.0) rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 4bb240b48b..eda3da2c29 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ class InlineRenderer private def preload_associations_for_status - ActiveRecord::Associations::Preloader.new.preload(@object, { + ActiveRecord::Associations::Preloader.new(records: @object, associations: { active_mentions: :account, reblog: { diff --git a/app/lib/rss/channel.rb b/app/lib/rss/channel.rb index 1dba94e47e..9013ed066a 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ class RSS::Channel < RSS::Element end def last_build_date(date) - append_element('lastBuildDate', date.to_formatted_s(:rfc822)) + append_element('lastBuildDate', date.to_fs(:rfc822)) end def image(url, title, link) diff --git a/app/lib/rss/item.rb b/app/lib/rss/item.rb index c02991ace2..6739a2c184 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ class RSS::Item < RSS::Element end def pub_date(date) - append_element('pubDate', date.to_formatted_s(:rfc822)) + append_element('pubDate', date.to_fs(:rfc822)) end def description(str) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 339f5ae70c..c5d6dd62e1 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ class Announcement < ApplicationRecord end end - ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) + ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji) records end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 46cf68e1a3..9f7720f11b 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ module AccountSearch tsquery = generate_query_for_search(terms) find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) end end @@ -131,7 +131,7 @@ module AccountSearch sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) end end diff --git a/app/models/concerns/status_safe_reblog_insert.rb b/app/models/concerns/status_safe_reblog_insert.rb index a7ccb52e9a..5d464697c5 100644 --- a/app/models/concerns/status_safe_reblog_insert.rb +++ b/app/models/concerns/status_safe_reblog_insert.rb @@ -4,41 +4,41 @@ module StatusSafeReblogInsert extend ActiveSupport::Concern class_methods do - # This is a hack to ensure that no reblogs of discarded statuses are created, - # as this cannot be enforced through database constraints the same way we do - # for reblogs of deleted statuses. + # This patch overwrites the built-in ActiveRecord `_insert_record` method to + # ensure that no reblogs of discarded statuses are created, as this cannot be + # enforced through DB constraints the same way as reblogs of deleted statuses # - # To achieve this, we redefine the internal method responsible for issuing - # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query - # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" - # clause on the reblogged status to ensure consistency at the database level. + # We redefine the internal method responsible for issuing the `INSERT` + # statement and replace the `INSERT INTO ... VALUES ...` query with an `INSERT + # INTO ... SELECT ...` query with a `WHERE deleted_at IS NULL` clause on the + # reblogged status to ensure consistency at the database level. # - # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence - # code, and actually calls it if we are not handling a reblog. + # The code is kept similar to ActiveRecord::Persistence code and calls it + # directly when we are not handling a reblog. def _insert_record(values) - return super unless values.is_a?(Hash) && values['reblog_of_id'].present? + return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present? primary_key = self.primary_key primary_key_value = nil - if primary_key - primary_key_value = values[primary_key] - - if !primary_key_value && prefetch_primary_key? + if prefetch_primary_key? && primary_key + values[primary_key] ||= begin primary_key_value = next_sequence_value - values[primary_key] = primary_key_value + _default_attributes[primary_key].with_cast_value(primary_key_value) end end - # The following line is where we differ from stock ActiveRecord implementation + # The following line departs from stock ActiveRecord + # Original code was: + # im.insert(values.transform_keys { |name| arel_table[name] }) + # Instead, we use a custom builder when a reblog is happening: im = _compile_reblog_insert(values) - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? - - result + connection.insert(im, "#{self} Create", primary_key || false, primary_key_value).tap do |result| + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? + end end def _compile_reblog_insert(values) @@ -54,9 +54,9 @@ module StatusSafeReblogInsert binds = [] reblog_bind = nil - values.each do |name, value| + values.each do |name, attribute| attr = arel_table[name] - bind = predicate_builder.build_bind_attribute(attr.name, value) + bind = predicate_builder.build_bind_attribute(attr.name, attribute.value) im.columns << attr binds << bind diff --git a/app/models/notification.rb b/app/models/notification.rb index 5527953afc..60f834a633 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ class Notification < ApplicationRecord # Instead of using the usual `includes`, manually preload each type. # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more. - ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) + ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations) end unique_target_statuses = notifications.filter_map(&:target_status).uniq diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 769ba653ed..16a9ac7c50 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,7 +83,10 @@ class InitialStateSerializer < ActiveModel::Serializer def accounts store = {} - ActiveRecord::Associations::Preloader.new.preload([object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, [:account_stat, :user, { moved_to_account: [:account_stat, :user] }]) + ActiveRecord::Associations::Preloader.new( + records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, + associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }] + ) store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index 3c9e73c124..c4216e2fc7 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ class AccountSearchService < BaseService .objects .compact - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) records rescue Faraday::ConnectionFailed, Parslet::ParseFailed diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 7e9b671266..f5cb339cdf 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -8,7 +8,10 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API def call(statuses, **options) - ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]) + ActiveRecord::Associations::Preloader.new( + records: statuses, + associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account] + ) statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -17,7 +20,10 @@ class BatchedRemoveStatusService < BaseService # rely on direct visibility statuses being relatively rare. statuses_with_account_conversations = statuses.select(&:direct_visibility?) - ActiveRecord::Associations::Preloader.new.preload(statuses_with_account_conversations, [mentions: :account]) + ActiveRecord::Associations::Preloader.new( + records: statuses_with_account_conversations, + associations: [mentions: :account] + ) statuses_with_account_conversations.each(&:unlink_from_conversations!) diff --git a/config/application.rb b/config/application.rb index d3c99baa12..7f8da1a95f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,7 +57,15 @@ require_relative '../lib/mastodon/redis_config' module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + config.load_defaults 7.0 + + # TODO: Release a version which uses the 7.0 defaults as specified above, + # but preserves the 6.1 cache format as set below. In a subsequent change, + # remove this line setting to 6.1 cache format, and then release another version. + # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format + # https://github.com/mastodon/mastodon/pull/24241#discussion_r1162890242 + config.active_support.cache_format_version = 6.1 + config.add_autoload_paths_to_load_path = false # Settings in config/environments/* take precedence over those specified here. diff --git a/config/environments/development.rb b/config/environments/development.rb index 306324c046..9a36d3ec4d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,8 +1,10 @@ +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -12,13 +14,22 @@ Rails.application.configure do # Show full error reports. config.consider_all_requests_local = true + # Enable server timing + config.server_timing = true + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}", + } else config.action_controller.perform_caching = false + config.cache_store = :null_store end @@ -41,12 +52,19 @@ Rails.application.configure do # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. @@ -57,12 +75,14 @@ Rails.application.configure do # Raises helpful error messages. config.assets.raise_runtime_errors = true - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true - # Use an evented file watcher to asynchronously detect changes in source code, - # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true config.action_mailer.default_options = { from: 'notifications@localhost' } diff --git a/config/environments/production.rb b/config/environments/production.rb index 018d3c1c22..a3fa1a4d27 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -19,20 +21,28 @@ Rails.application.configure do # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - ActiveSupport::Logger.new(STDOUT).tap do |logger| - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + # Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present? + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Allow to specify public IP of reverse proxy if it's needed config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true config.ssl_options = { redirect: { @@ -40,6 +50,8 @@ Rails.application.configure do } } + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym @@ -50,6 +62,12 @@ Rails.application.configure do # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "mastodon_production" + + config.action_mailer.perform_caching = false + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false @@ -73,6 +91,15 @@ Rails.application.configure do end end + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + + ActiveSupport::Logger.new(STDOUT).tap do |logger| + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index 08cc4c4d3c..bb4caad526 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,25 +1,32 @@ +require 'active_support/core_ext/integer/time' + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! + # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV['CI'].present? - config.assets.digest = false + config.assets_digest = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false - - # The default store, file_store is shared by processes parallelly executed - # and should not be used. config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. @@ -27,6 +34,7 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false config.action_mailer.default_options = { from: 'notifications@localhost' } @@ -46,8 +54,8 @@ Rails.application.configure do config.x.vapid_private_key = vapid_key.private_key config.x.vapid_public_key = vapid_key.public_key - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise config.i18n.default_locale = :en config.i18n.fallbacks = true @@ -57,6 +65,15 @@ Rails.application.configure do # Ref: https://github.com/mastodon/mastodon/issues/23644 10.times { |i| Status.allocate.instance_variable_set(:"@ivar_#{i}", nil) } end + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension') diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 53b39718da..ea5315c62d 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,11 +3,12 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path -# Rails.application.config.assets.paths << 'node_modules' +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w() +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) Rails.application.config.assets.initialize_on_precompile = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 06cb15bbb1..adc6568ce8 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password, :private_key, :public_key, :otp_attempt] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb new file mode 100644 index 0000000000..edaf819447 --- /dev/null +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# TODO +# The Rails 7.0 framework default here is to set this true. However, we have a +# location in devise that redirects where we don't have an easy ability to +# override a method or set a config option, but where the redirect does not +# provide this option. +# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28 +# Once a solution is found, this line can be removed. +Rails.application.config.action_controller.raise_on_open_redirects = false diff --git a/db/schema.rb b/db/schema.rb index dbd792a617..05db75215a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_06_30_145300) do +ActiveRecord::Schema[6.1].define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/package.json b/package.json index 49e9c7f743..7f8767404a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", - "@rails/ujs": "^6.1.7", + "@rails/ujs": "^7.0.6", "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "arrow-key-navigation": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 12a992ec38..06066033c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1719,10 +1719,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rails/ujs@^6.1.7": - version "6.1.7" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.7.tgz#b09dc5b2105dd267e8374c47e4490240451dc7f6" - integrity sha512-0e7WQ4LE/+LEfW2zfAw9ppsB6A8RmxbdAUPAF++UT80epY+7emuQDkKXmaK0a9lp6An50RvzezI0cIQjp1A58w== +"@rails/ujs@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.6.tgz#fd8937c92335f3da9495e07292511ad5f7547a6a" + integrity sha512-s5v3AC6AywOIFMz0RIMW83Xc8FPIvKMkP3ZHFlM4ISNkhdUwP9HdhVtxxo6z3dIhe9vI0Our2A8kN/QpUV02Qg== "@redis/bloom@1.2.0": version "1.2.0" @@ -8759,12 +8759,7 @@ pg-cloudflare@^1.1.1: resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== -pg-connection-string@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" - integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== - -pg-connection-string@^2.6.1: +pg-connection-string@^2.6.0, pg-connection-string@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== @@ -10836,6 +10831,7 @@ stringz@^2.1.0: char-regex "^1.0.2" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From 0512537eb6722f1d52a690a6e1b22c8d0a99103b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Jul 2023 10:39:55 +0200 Subject: [PATCH 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 726b5aa30d..ac84014e48 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -60,7 +60,7 @@ class ActionBar extends PureComponent { return (
- +
); From 5b463454593e1aa3ab3d23446cdf57d606e933d5 Mon Sep 17 00:00:00 2001 From: mogaminsk Date: Sun, 2 Jul 2023 18:12:16 +0900 Subject: [PATCH 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 bd784906d4..65f3efc3a7 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const fetchServer = () => (dispatch, getState) => { + if (getState().getIn(['server', 'server', 'isLoading'])) { + return; + } + dispatch(fetchServerRequest()); api(getState) @@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({ }); export const fetchExtendedDescription = () => (dispatch, getState) => { + if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { + return; + } + dispatch(fetchExtendedDescriptionRequest()); api(getState) @@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({ }); export const fetchDomainBlocks = () => (dispatch, getState) => { + if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { + return; + } + dispatch(fetchDomainBlocksRequest()); api(getState) diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 73d42479b8..aff38124b6 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -161,7 +161,7 @@ class About extends PureComponent {
- {!isLoading && (server.get('rules').isEmpty() ? ( + {!isLoading && (server.get('rules', []).isEmpty() ? (

) : (
    diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js index 486314c338..2bbf0f9a30 100644 --- a/app/javascript/mastodon/reducers/server.js +++ b/app/javascript/mastodon/reducers/server.js @@ -17,15 +17,15 @@ import { const initialState = ImmutableMap({ server: ImmutableMap({ - isLoading: true, + isLoading: false, }), extendedDescription: ImmutableMap({ - isLoading: true, + isLoading: false, }), domainBlocks: ImmutableMap({ - isLoading: true, + isLoading: false, isAvailable: true, items: ImmutableList(), }), From ba06a2f1044aa81a99eb5fc509611dbb1150d43d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Jul 2023 11:14:22 +0200 Subject: [PATCH 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 aa5e39f316..3feb3f9548 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.0.0' gem 'puma', '~> 6.3' -gem 'rails', '~> 7.0' +gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' @@ -67,7 +67,7 @@ gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 7.0' +gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] diff --git a/Gemfile.lock b/Gemfile.lock index 8048e0c953..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,47 +18,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.0.6) - actionpack (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activesupport (= 7.0.6) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.6) - actionview (= 7.0.6) - activesupport (= 7.0.6) - rack (~> 2.0, >= 2.2.4) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.6) - actionpack (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) - globalid (>= 0.6.0) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (7.0.6) - activesupport (= 7.0.6) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -68,26 +61,27 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.6) - activesupport (= 7.0.6) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (7.0.6) - activesupport (= 7.0.6) - activerecord (7.0.6) - activemodel (= 7.0.6) - activesupport (= 7.0.6) - activestorage (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activesupport (= 7.0.6) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.6) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) @@ -516,20 +510,21 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.6) - actioncable (= 7.0.6) - actionmailbox (= 7.0.6) - actionmailer (= 7.0.6) - actionpack (= 7.0.6) - actiontext (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activemodel (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 7.0.6) + railties (= 6.1.7.4) + sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -540,16 +535,15 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.7) + rails-i18n (6.0.0) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + railties (>= 6.0.0, < 7) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) - zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rdf (3.2.11) @@ -696,7 +690,7 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (1.2.2) tilt (2.2.0) - timeout (0.4.0) + timeout (0.3.2) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -848,9 +842,9 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 7.0) + rails (~> 6.1.7) rails-controller-testing (~> 1.0) - rails-i18n (~> 7.0) + rails-i18n (~> 6.0) rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index eda3da2c29..4bb240b48b 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,7 +37,7 @@ class InlineRenderer private def preload_associations_for_status - ActiveRecord::Associations::Preloader.new(records: @object, associations: { + ActiveRecord::Associations::Preloader.new.preload(@object, { active_mentions: :account, reblog: { diff --git a/app/lib/rss/channel.rb b/app/lib/rss/channel.rb index 9013ed066a..1dba94e47e 100644 --- a/app/lib/rss/channel.rb +++ b/app/lib/rss/channel.rb @@ -16,7 +16,7 @@ class RSS::Channel < RSS::Element end def last_build_date(date) - append_element('lastBuildDate', date.to_fs(:rfc822)) + append_element('lastBuildDate', date.to_formatted_s(:rfc822)) end def image(url, title, link) diff --git a/app/lib/rss/item.rb b/app/lib/rss/item.rb index 6739a2c184..c02991ace2 100644 --- a/app/lib/rss/item.rb +++ b/app/lib/rss/item.rb @@ -20,7 +20,7 @@ class RSS::Item < RSS::Element end def pub_date(date) - append_element('pubDate', date.to_fs(:rfc822)) + append_element('pubDate', date.to_formatted_s(:rfc822)) end def description(str) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index c5d6dd62e1..339f5ae70c 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -80,7 +80,7 @@ class Announcement < ApplicationRecord end end - ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji) + ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) records end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 9f7720f11b..46cf68e1a3 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ module AccountSearch tsquery = generate_query_for_search(terms) find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) end end @@ -131,7 +131,7 @@ module AccountSearch sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) end end diff --git a/app/models/concerns/status_safe_reblog_insert.rb b/app/models/concerns/status_safe_reblog_insert.rb index 5d464697c5..a7ccb52e9a 100644 --- a/app/models/concerns/status_safe_reblog_insert.rb +++ b/app/models/concerns/status_safe_reblog_insert.rb @@ -4,41 +4,41 @@ module StatusSafeReblogInsert extend ActiveSupport::Concern class_methods do - # This patch overwrites the built-in ActiveRecord `_insert_record` method to - # ensure that no reblogs of discarded statuses are created, as this cannot be - # enforced through DB constraints the same way as reblogs of deleted statuses + # This is a hack to ensure that no reblogs of discarded statuses are created, + # as this cannot be enforced through database constraints the same way we do + # for reblogs of deleted statuses. # - # We redefine the internal method responsible for issuing the `INSERT` - # statement and replace the `INSERT INTO ... VALUES ...` query with an `INSERT - # INTO ... SELECT ...` query with a `WHERE deleted_at IS NULL` clause on the - # reblogged status to ensure consistency at the database level. + # To achieve this, we redefine the internal method responsible for issuing + # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query + # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" + # clause on the reblogged status to ensure consistency at the database level. # - # The code is kept similar to ActiveRecord::Persistence code and calls it - # directly when we are not handling a reblog. + # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence + # code, and actually calls it if we are not handling a reblog. def _insert_record(values) - return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present? + return super unless values.is_a?(Hash) && values['reblog_of_id'].present? primary_key = self.primary_key primary_key_value = nil - if prefetch_primary_key? && primary_key - values[primary_key] ||= begin + if primary_key + primary_key_value = values[primary_key] + + if !primary_key_value && prefetch_primary_key? primary_key_value = next_sequence_value - _default_attributes[primary_key].with_cast_value(primary_key_value) + values[primary_key] = primary_key_value end end - # The following line departs from stock ActiveRecord - # Original code was: - # im.insert(values.transform_keys { |name| arel_table[name] }) - # Instead, we use a custom builder when a reblog is happening: + # The following line is where we differ from stock ActiveRecord implementation im = _compile_reblog_insert(values) - connection.insert(im, "#{self} Create", primary_key || false, primary_key_value).tap do |result| - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? - end + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? + + result end def _compile_reblog_insert(values) @@ -54,9 +54,9 @@ module StatusSafeReblogInsert binds = [] reblog_bind = nil - values.each do |name, attribute| + values.each do |name, value| attr = arel_table[name] - bind = predicate_builder.build_bind_attribute(attr.name, attribute.value) + bind = predicate_builder.build_bind_attribute(attr.name, value) im.columns << attr binds << bind diff --git a/app/models/notification.rb b/app/models/notification.rb index 60f834a633..5527953afc 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ class Notification < ApplicationRecord # Instead of using the usual `includes`, manually preload each type. # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more. - ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations) + ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) end unique_target_statuses = notifications.filter_map(&:target_status).uniq diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 16a9ac7c50..769ba653ed 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -83,10 +83,7 @@ class InitialStateSerializer < ActiveModel::Serializer def accounts store = {} - ActiveRecord::Associations::Preloader.new( - records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, - associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }] - ) + ActiveRecord::Associations::Preloader.new.preload([object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, [:account_stat, :user, { moved_to_account: [:account_stat, :user] }]) store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index c4216e2fc7..3c9e73c124 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -93,7 +93,7 @@ class AccountSearchService < BaseService .objects .compact - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) records rescue Faraday::ConnectionFailed, Parslet::ParseFailed diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index f5cb339cdf..7e9b671266 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -8,10 +8,7 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API def call(statuses, **options) - ActiveRecord::Associations::Preloader.new( - records: statuses, - associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account] - ) + ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]) statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -20,10 +17,7 @@ class BatchedRemoveStatusService < BaseService # rely on direct visibility statuses being relatively rare. statuses_with_account_conversations = statuses.select(&:direct_visibility?) - ActiveRecord::Associations::Preloader.new( - records: statuses_with_account_conversations, - associations: [mentions: :account] - ) + ActiveRecord::Associations::Preloader.new.preload(statuses_with_account_conversations, [mentions: :account]) statuses_with_account_conversations.each(&:unlink_from_conversations!) diff --git a/config/application.rb b/config/application.rb index 7f8da1a95f..d3c99baa12 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,15 +57,7 @@ require_relative '../lib/mastodon/redis_config' module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 - - # TODO: Release a version which uses the 7.0 defaults as specified above, - # but preserves the 6.1 cache format as set below. In a subsequent change, - # remove this line setting to 6.1 cache format, and then release another version. - # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format - # https://github.com/mastodon/mastodon/pull/24241#discussion_r1162890242 - config.active_support.cache_format_version = 6.1 - + config.load_defaults 6.1 config.add_autoload_paths_to_load_path = false # Settings in config/environments/* take precedence over those specified here. diff --git a/config/environments/development.rb b/config/environments/development.rb index 9a36d3ec4d..306324c046 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,10 +1,8 @@ -require 'active_support/core_ext/integer/time' - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -14,22 +12,13 @@ Rails.application.configure do # Show full error reports. config.consider_all_requests_local = true - # Enable server timing - config.server_timing = true - # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true - config.action_controller.enable_fragment_cache_logging = true - config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}", - } else config.action_controller.perform_caching = false - config.cache_store = :null_store end @@ -52,19 +41,12 @@ Rails.application.configure do # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = true - # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. @@ -75,14 +57,12 @@ Rails.application.configure do # Raises helpful error messages. config.assets.raise_runtime_errors = true - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Uncomment if you wish to allow Action Cable access from any origin. - # config.action_cable.disable_request_forgery_protection = true + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.action_mailer.default_options = { from: 'notifications@localhost' } diff --git a/config/environments/production.rb b/config/environments/production.rb index a3fa1a4d27..018d3c1c22 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/integer/time" - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -21,28 +19,20 @@ Rails.application.configure do # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - # Disable serving static files from the `/public` folder by default since - # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? - - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass + ActiveSupport::Logger.new(STDOUT).tap do |logger| + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - # Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present? - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Allow to specify public IP of reverse proxy if it's needed config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true config.ssl_options = { redirect: { @@ -50,8 +40,6 @@ Rails.application.configure do } } - # Include generic and useful information about system operation, but avoid logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym @@ -62,12 +50,6 @@ Rails.application.configure do # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS - # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "mastodon_production" - - config.action_mailer.perform_caching = false - # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false @@ -91,15 +73,6 @@ Rails.application.configure do end end - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") - - ActiveSupport::Logger.new(STDOUT).tap do |logger| - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end - # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index bb4caad526..08cc4c4d3c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,32 +1,25 @@ -require 'active_support/core_ext/integer/time' - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Turn false under Spring and add config.action_view.cache_template_loading = true. + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! config.cache_classes = true - # Eager loading loads your whole application. When running a single test locally, - # this probably isn't necessary. It's a good idea to do in a continuous integration - # system, or in some way before deploying your code. - config.eager_load = ENV['CI'].present? + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false - config.assets_digest = false - - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" - } + config.assets.digest = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + + # The default store, file_store is shared by processes parallelly executed + # and should not be used. config.cache_store = :memory_store # Raise exceptions instead of rendering exception templates. @@ -34,7 +27,6 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - config.action_mailer.perform_caching = false config.action_mailer.default_options = { from: 'notifications@localhost' } @@ -54,8 +46,8 @@ Rails.application.configure do config.x.vapid_private_key = vapid_key.private_key config.x.vapid_public_key = vapid_key.public_key - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true config.i18n.default_locale = :en config.i18n.fallbacks = true @@ -65,15 +57,6 @@ Rails.application.configure do # Ref: https://github.com/mastodon/mastodon/issues/23644 10.times { |i| Status.allocate.instance_variable_set(:"@ivar_#{i}", nil) } end - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true end Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension') diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index ea5315c62d..53b39718da 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,12 +3,11 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << 'node_modules' # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w() Rails.application.config.assets.initialize_on_precompile = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568ce8..06cb15bbb1 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,4 @@ # Be sure to restart your server when you modify this file. -# Configure parameters to be filtered from the log file. Use this to limit dissemination of -# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported -# notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password, :private_key, :public_key, :otp_attempt] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb deleted file mode 100644 index edaf819447..0000000000 --- a/config/initializers/new_framework_defaults_7_0.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -# TODO -# The Rails 7.0 framework default here is to set this true. However, we have a -# location in devise that redirects where we don't have an easy ability to -# override a method or set a config option, but where the redirect does not -# provide this option. -# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28 -# Once a solution is found, this line can be removed. -Rails.application.config.action_controller.raise_on_open_redirects = false diff --git a/db/schema.rb b/db/schema.rb index 05db75215a..dbd792a617 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[6.1].define(version: 2023_06_30_145300) do +ActiveRecord::Schema.define(version: 2023_06_30_145300) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/package.json b/package.json index 7f8767404a..49e9c7f743 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", - "@rails/ujs": "^7.0.6", + "@rails/ujs": "^6.1.7", "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "arrow-key-navigation": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 06066033c8..12a992ec38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1719,10 +1719,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rails/ujs@^7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.6.tgz#fd8937c92335f3da9495e07292511ad5f7547a6a" - integrity sha512-s5v3AC6AywOIFMz0RIMW83Xc8FPIvKMkP3ZHFlM4ISNkhdUwP9HdhVtxxo6z3dIhe9vI0Our2A8kN/QpUV02Qg== +"@rails/ujs@^6.1.7": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.7.tgz#b09dc5b2105dd267e8374c47e4490240451dc7f6" + integrity sha512-0e7WQ4LE/+LEfW2zfAw9ppsB6A8RmxbdAUPAF++UT80epY+7emuQDkKXmaK0a9lp6An50RvzezI0cIQjp1A58w== "@redis/bloom@1.2.0": version "1.2.0" @@ -8759,7 +8759,12 @@ pg-cloudflare@^1.1.1: resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== -pg-connection-string@^2.6.0, pg-connection-string@^2.6.1: +pg-connection-string@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" + integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== + +pg-connection-string@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== @@ -10831,7 +10836,6 @@ stringz@^2.1.0: char-regex "^1.0.2" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: - name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== From 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 f2b89f3bdc..1ebecbd29d 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 9a110f06e7..df25e86489 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 fdfa4104f6..0bc2660b06 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 0000000000..19bb6d791e --- /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 56b6477016..683a2d79d9 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 a6a7489e45..5a14e396cc 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 0e632bc816..24e8a42a68 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 103702d8d7..0f038670ed 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 2a6202f846..f155979ef9 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 bd784906d4..65f3efc3a7 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 42a3077de6..fe07a870b2 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 0b774b5e20..e39e2ba48b 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 19bb6d791e..9db45a0e47 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 07ca42339d..5553750531 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 0f038670ed..fcf72a0b15 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({