diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 93bc87d7605..afb70669565 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,7 +35,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -48,7 +48,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -61,6 +61,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 90c9dc89370..ac95c708ddf 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -167,7 +167,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6 with: files: coverage/lcov/*.lcov env: diff --git a/.nvmrc b/.nvmrc index a2e33f6e2c0..78582455673 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.15 +24.16 diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml index bbd172e6560..d98c6fc4865 100644 --- a/.rubocop/rails.yml +++ b/.rubocop/rails.yml @@ -24,3 +24,6 @@ Rails/RakeEnvironment: Rails/SkipsModelValidations: Enabled: false + +Rails/StrongParametersExpect: + Enabled: false diff --git a/.rubocop/style.yml b/.rubocop/style.yml index f59340d452e..1d1c9f98797 100644 --- a/.rubocop/style.yml +++ b/.rubocop/style.yml @@ -33,6 +33,9 @@ Style/NumericLiterals: AllowedPatterns: - \d{4}_\d{2}_\d{2}_\d{6} +Style/OneClassPerFile: + Enabled: false + Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': () diff --git a/Gemfile b/Gemfile index d4c1008a4fb..8576937d2df 100644 --- a/Gemfile +++ b/Gemfile @@ -135,7 +135,7 @@ group :test do # Browser integration testing gem 'capybara', '~> 3.39' gem 'capybara-playwright-driver' - gem 'playwright-ruby-client', '1.59.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package + gem 'playwright-ruby-client', '1.60.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package # Used to reset the database between system tests gem 'database_cleaner-active_record' @@ -223,7 +223,7 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'net-http', '~> 0.6.0' +gem 'net-http', '~> 0.9.0' gem 'rubyzip', '~> 3.0' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 62d049d0333..2a659eb3c21 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,8 +99,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1249.0) - aws-sdk-core (3.247.0) + aws-partitions (1.1254.0) + aws-sdk-core (3.250.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -235,7 +235,7 @@ GEM fabrication (3.0.0) faker (3.8.0) i18n (>= 1.8.11, < 2) - faraday (2.14.1) + faraday (2.14.2) faraday-net_http (>= 2.0, < 3.5) json logger @@ -243,7 +243,7 @@ GEM faraday (>= 1, < 3) faraday-httpclient (2.0.2) httpclient (>= 2.2) - faraday-net_http (3.4.2) + faraday-net_http (3.4.3) net-http (~> 0.5) fast_blank (1.0.1) fastimage (2.4.1) @@ -378,7 +378,7 @@ GEM addressable (~> 2.8) bigdecimal (>= 3.1, < 5) jsonapi-renderer (0.2.2) - jwt (2.10.2) + jwt (2.10.3) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -455,8 +455,8 @@ GEM msgpack (1.8.0) multi_json (1.20.1) mutex_m (0.3.0) - net-http (0.6.0) - uri + net-http (0.9.1) + uri (>= 0.11.1) net-imap (0.6.4) date net-protocol @@ -539,7 +539,7 @@ GEM opentelemetry-instrumentation-active_support (~> 0.10) opentelemetry-instrumentation-active_support (0.12.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-base (0.26.0) + opentelemetry-instrumentation-base (0.26.1) opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) @@ -559,7 +559,7 @@ GEM opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rack (0.31.0) + opentelemetry-instrumentation-rack (0.31.1) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-rails (0.42.0) opentelemetry-instrumentation-action_mailer (~> 0.7) @@ -588,7 +588,7 @@ GEM ostruct (0.6.3) ox (2.14.26) bigdecimal (>= 3.0) - parallel (1.28.0) + parallel (2.1.0) parser (3.3.11.1) ast (~> 2.4.1) racc @@ -598,7 +598,7 @@ GEM pg (1.6.3) pghero (3.8.0) activerecord (>= 7.2) - playwright-ruby-client (1.59.1) + playwright-ruby-client (1.60.0) base64 concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) @@ -755,11 +755,11 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.7) - rubocop (1.84.2) + rubocop (1.86.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) @@ -779,7 +779,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) - rubocop-rails (2.34.3) + rubocop-rails (2.35.2) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -816,7 +816,7 @@ GEM securerandom (0.4.1) shoulda-matchers (7.0.1) activesupport (>= 7.1) - sidekiq (8.1.5) + sidekiq (8.1.6) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) @@ -827,7 +827,7 @@ GEM sidekiq-scheduler (6.0.2) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) - sidekiq-unique-jobs (8.0.13) + sidekiq-unique-jobs (8.1.0) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) @@ -1012,7 +1012,7 @@ DEPENDENCIES memory_profiler mime-types (~> 3.7.0) mutex_m - net-http (~> 0.6.0) + net-http (~> 0.9.0) net-ldap (~> 0.18) nokogiri (~> 1.15) omniauth (~> 2.0) @@ -1040,7 +1040,7 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - playwright-ruby-client (= 1.59.1) + playwright-ruby-client (= 1.60.0) premailer-rails prometheus_exporter (~> 2.2) propshaft diff --git a/app/controllers/activitypub/featured_collections_controller.rb b/app/controllers/activitypub/featured_collections_controller.rb index 09de5583cc9..12c0648fae4 100644 --- a/app/controllers/activitypub/featured_collections_controller.rb +++ b/app/controllers/activitypub/featured_collections_controller.rb @@ -9,7 +9,6 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, if: -> { authorized_fetch_mode? } before_action :set_collections @@ -72,8 +71,4 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController ) end end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 10dbe846e4c..03234b0bde4 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -25,6 +25,8 @@ module Admin @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes + @collections = @report.collections + @collection_form = Admin::CollectionBatchAction.new render 'admin/reports/show' end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 9b5beeab67e..765996eb3cf 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -16,6 +16,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController FILTER_PARAMS = %i( resolved + unresolved account_id target_account_id ).freeze diff --git a/app/controllers/api/v1_alpha/collection_items_controller.rb b/app/controllers/api/v1/collection_items_controller.rb similarity index 84% rename from app/controllers/api/v1_alpha/collection_items_controller.rb rename to app/controllers/api/v1/collection_items_controller.rb index 2c46cc4f9fc..3ec5e18ed95 100644 --- a/app/controllers/api/v1_alpha/collection_items_controller.rb +++ b/app/controllers/api/v1/collection_items_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -class Api::V1Alpha::CollectionItemsController < Api::BaseController +class Api::V1::CollectionItemsController < Api::BaseController include Authorization - before_action :check_feature_enabled - before_action -> { doorkeeper_authorize! :write, :'write:collections' } before_action :require_user! @@ -55,8 +53,4 @@ class Api::V1Alpha::CollectionItemsController < Api::BaseController def set_collection_item @collection_item = @collection.collection_items.find(params[:id]) end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1/collections_controller.rb similarity index 78% rename from app/controllers/api/v1_alpha/collections_controller.rb rename to app/controllers/api/v1/collections_controller.rb index 4e15b65ff58..9acd535f465 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1/collections_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1Alpha::CollectionsController < Api::BaseController +class Api::V1::CollectionsController < Api::BaseController include Authorization DEFAULT_COLLECTIONS_LIMIT = 40 @@ -9,8 +9,6 @@ class Api::V1Alpha::CollectionsController < Api::BaseController render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422 end - before_action :check_feature_enabled - before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy] @@ -28,10 +26,9 @@ class Api::V1Alpha::CollectionsController < Api::BaseController cache_if_unauthenticated! authorize @account, :index_collections? - presenter = CollectionsPresenter.new(collections: @collections) - render json: presenter, serializer: REST::CollectionsWithAccountPreviewsSerializer + render json: @collections, each_serializer: REST::CollectionSerializer, adapter: :json rescue Mastodon::NotPermittedError - render json: { collections: [], partial_accounts: [] } + render json: { collections: [] } end def show @@ -74,7 +71,6 @@ class Api::V1Alpha::CollectionsController < Api::BaseController def set_collections @collections = @account.collections .with_tag - .preload(top_items: :account) .order(created_at: :desc) .offset(offset_param) .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) @@ -93,20 +89,16 @@ class Api::V1Alpha::CollectionsController < Api::BaseController params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name) end - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end - def next_path return unless records_continue? - api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def prev_path return if offset_param.zero? - api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def records_continue? diff --git a/app/controllers/api/v1_alpha/in_collections_controller.rb b/app/controllers/api/v1/in_collections_controller.rb similarity index 70% rename from app/controllers/api/v1_alpha/in_collections_controller.rb rename to app/controllers/api/v1/in_collections_controller.rb index 087464989ef..c34845e463e 100644 --- a/app/controllers/api/v1_alpha/in_collections_controller.rb +++ b/app/controllers/api/v1/in_collections_controller.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -class Api::V1Alpha::InCollectionsController < Api::BaseController +class Api::V1::InCollectionsController < Api::BaseController include Authorization DEFAULT_COLLECTIONS_LIMIT = 40 - before_action :check_feature_enabled - before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index] before_action :require_user! @@ -37,20 +35,16 @@ class Api::V1Alpha::InCollectionsController < Api::BaseController .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) end - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end - def next_path return unless records_continue? - api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def prev_path return if offset_param.zero? - api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def records_continue? diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8e341aa48e6..a8653631c27 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -23,10 +23,6 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - if Mastodon::Feature.collections_enabled? - params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: []) - else - params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: []) - end + params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: []) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 78b237357c9..d3f742d62a1 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -170,6 +170,6 @@ class Api::V1::StatusesController < Api::BaseController end def serialized_accounts(accounts) - ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer) + ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer, scope_name: :current_user, scope: current_user) end end diff --git a/app/controllers/collection_items_controller.rb b/app/controllers/collection_items_controller.rb index 09c1e0e192a..51044b59654 100644 --- a/app/controllers/collection_items_controller.rb +++ b/app/controllers/collection_items_controller.rb @@ -7,7 +7,6 @@ class CollectionItemsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, if: -> { authorized_fetch_mode? } before_action :set_collection_item @@ -35,8 +34,4 @@ class CollectionItemsController < ApplicationController rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 70541433f00..628418557c7 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -8,7 +8,6 @@ class CollectionsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_collection @@ -51,8 +50,4 @@ class CollectionsController < ApplicationController recently_updated = @collection.updated_at > 15.minutes.ago recently_updated ? 30.seconds : 5.minutes end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 40806a45158..a3dd83ecca6 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -20,8 +20,9 @@ module Admin::FilterHelper def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) new_class = filtered_url_for(link_class_params) + is_selected = selected?(link_class_params) - link_to text, new_url, class: filter_link_class(new_class) + link_to text, new_url, class: filter_link_class(new_class), 'aria-current': (is_selected ? 'true' : nil) end def table_link_to(icon, text, path, **options) diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index fe7c7327ce3..3d8396c81a9 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -3,6 +3,7 @@ import { createAction } from '@reduxjs/toolkit'; import { apiRemoveAccountFromFollowers, apiGetEndorsedAccounts, + apiGetAccounts, } from 'mastodon/api/accounts'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; @@ -113,3 +114,12 @@ export const fetchEndorsedAccounts = createDataLoadingThunk( return data; }, ); + +export const fetchAccounts = createDataLoadingThunk( + 'accounts/multi_accounts', + ({ accountIds }: { accountIds: string[] }) => apiGetAccounts(accountIds), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + return data; + }, +); diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 06335f9fbff..b4be55cbfea 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -327,7 +327,7 @@ export function uploadCompose(files) { dispatch(showAlert({ message: messages.uploadQuote })); return; } - const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']); + const uploadLimit = getState().getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']); const media = getState().getIn(['compose', 'media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']); const progress = new Array(files.length).fill(0); diff --git a/app/javascript/mastodon/actions/importer/emoji.ts b/app/javascript/mastodon/actions/importer/emoji.ts index 9e06c88f66e..e9356ab6215 100644 --- a/app/javascript/mastodon/actions/importer/emoji.ts +++ b/app/javascript/mastodon/actions/importer/emoji.ts @@ -1,5 +1,8 @@ import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { loadCustomEmoji } from '@/mastodon/features/emoji'; +import { emojiLogger } from '@/mastodon/features/emoji/utils'; + +const log = emojiLogger('actions'); export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { if (emojis.length === 0) { @@ -18,5 +21,11 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { if (existingEmojis.length < emojis.length) { await clearCache('custom'); await loadCustomEmoji(); + + const { reloadCustomEmojis } = + await import('@/mastodon/features/emoji/picker'); + await reloadCustomEmojis(); + + log('Custom emojis updated, reloaded cache and picker data.'); } } diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 2d03ef080f5..eddd6a93008 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -36,9 +36,18 @@ function notificationTypeForFilter(type: NotificationType) { } function notificationTypeForQuickFilter(type: NotificationType) { - if (type === 'quoted_update') return 'update'; - else if (type === 'quote') return 'mention'; - else return type; + switch (type) { + case 'quoted_update': + return 'update'; + case 'quote': + return 'mention'; + case 'collection_update': + return 'collection'; + case 'added_to_collection': + return 'collection'; + default: + return type; + } } function excludeAllTypesExcept(filter: string) { diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index da0c5f11025..cb4cd1251c4 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -86,6 +86,9 @@ export function setupBrowserNotifications() { }; } +/** + * @param {(NotificationPermission) => void} callback + */ export function requestBrowserPermission(callback = noOp) { return dispatch => { requestNotificationPermission((permission) => { diff --git a/app/javascript/mastodon/actions/server.ts b/app/javascript/mastodon/actions/server.ts index 4e4795c1233..b301919b237 100644 --- a/app/javascript/mastodon/actions/server.ts +++ b/app/javascript/mastodon/actions/server.ts @@ -16,19 +16,33 @@ export const fetchServer = createDataLoadingThunk( dispatch(importFetchedAccount(instance.contact.account)); } }, + { + condition: (_, { getState }) => !getState().server.server.isLoading, + }, ); export const fetchExtendedDescription = createDataLoadingThunk( 'server/extended_description', () => apiGetExtendedDescription(), + { + condition: (_, { getState }) => + !getState().server.extendedDescription.isLoading, + }, ); export const fetchServerTranslationLanguages = createDataLoadingThunk( 'server/translation_languages', () => apiGetTranslationLanguages(), + { + condition: (_, { getState }) => + !getState().server.translationLanguages.isLoading, + }, ); export const fetchDomainBlocks = createDataLoadingThunk( 'server/domain_blocks', () => apiGetDomainBlocks(), + { + condition: (_, { getState }) => !getState().server.domainBlocks.isLoading, + }, ); diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 2229d17c560..52c5b017d96 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -19,6 +19,11 @@ import type { ApiProfileUpdateParams, } from '../api_types/profile'; +export const apiGetAccounts = (ids: string[]) => + apiRequestGet('v1/accounts', { + id: ids, + }); + export const apiSubmitAccountNote = (id: string, value: string) => apiRequestPost(`v1/accounts/${id}/note`, { comment: value, diff --git a/app/javascript/mastodon/api/collections.ts b/app/javascript/mastodon/api/collections.ts index 7f39651f02a..badd3b43976 100644 --- a/app/javascript/mastodon/api/collections.ts +++ b/app/javascript/mastodon/api/collections.ts @@ -15,48 +15,40 @@ import type { } from '../api_types/collections'; export const apiCreateCollection = (collection: ApiCreateCollectionPayload) => - apiRequestPost('v1_alpha/collections', collection); + apiRequestPost('v1/collections', collection); export const apiUpdateCollection = ({ id, ...collection }: ApiUpdateCollectionPayload) => - apiRequestPut( - `v1_alpha/collections/${id}`, - collection, - ); + apiRequestPut(`v1/collections/${id}`, collection); export const apiDeleteCollection = (collectionId: string) => - apiRequestDelete(`v1_alpha/collections/${collectionId}`); + apiRequestDelete(`v1/collections/${collectionId}`); export const apiGetCollection = (collectionId: string) => apiRequestGet( - `v1_alpha/collections/${collectionId}`, + `v1/collections/${collectionId}`, ); export const apiGetCollectionsCreatedByAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/collections`); export const apiGetCollectionsFeaturingAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/in_collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/in_collections`); export const apiAddCollectionItem = (collectionId: string, accountId: string) => apiRequestPost( - `v1_alpha/collections/${collectionId}/items`, + `v1/collections/${collectionId}/items`, { account_id: accountId }, ); export const apiRemoveCollectionItem = (collectionId: string, itemId: string) => apiRequestDelete( - `v1_alpha/collections/${collectionId}/items/${itemId}`, + `v1/collections/${collectionId}/items/${itemId}`, ); export const apiRevokeCollectionInclusion = ( collectionId: string, itemId: string, -) => - apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`); +) => apiRequestPost(`v1/collections/${collectionId}/items/${itemId}/revoke`); diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts index fa7e6e4554b..a3f4b5be95f 100644 --- a/app/javascript/mastodon/api/lists.ts +++ b/app/javascript/mastodon/api/lists.ts @@ -15,7 +15,7 @@ export const apiUpdate = (list: Partial) => export const apiGetLists = () => apiRequestGet('v1/lists'); -export const apiGetAccounts = (listId: string) => +export const apiGetListAccounts = (listId: string) => apiRequestGet(`v1/lists/${listId}/accounts`, { limit: 0, }); diff --git a/app/javascript/mastodon/api_types/search.ts b/app/javascript/mastodon/api_types/search.ts index 795cbb2b41f..961dd65699a 100644 --- a/app/javascript/mastodon/api_types/search.ts +++ b/app/javascript/mastodon/api_types/search.ts @@ -1,4 +1,5 @@ import type { ApiAccountJSON } from './accounts'; +import type { ApiCollectionJSON } from './collections'; import type { ApiStatusJSON } from './statuses'; import type { ApiHashtagJSON } from './tags'; @@ -8,4 +9,5 @@ export interface ApiSearchResultsJSON { accounts: ApiAccountJSON[]; statuses: ApiStatusJSON[]; hashtags: ApiHashtagJSON[]; + collections: ApiCollectionJSON[]; } diff --git a/app/javascript/mastodon/components/account_header/buttons.tsx b/app/javascript/mastodon/components/account_header/buttons.tsx index 7a5ca4332cd..7d7f6e7d8b6 100644 --- a/app/javascript/mastodon/components/account_header/buttons.tsx +++ b/app/javascript/mastodon/components/account_header/buttons.tsx @@ -11,7 +11,7 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react'; -import { CopyIconButton } from '../copy_icon_button'; +import { CopyIconButton } from '../copy_button'; import { FollowButton } from '../follow_button'; import { IconButton } from '../icon_button'; diff --git a/app/javascript/mastodon/components/account_header/name.tsx b/app/javascript/mastodon/components/account_header/name.tsx index 2b3f1907b4d..b46e849765f 100644 --- a/app/javascript/mastodon/components/account_header/name.tsx +++ b/app/javascript/mastodon/components/account_header/name.tsx @@ -7,17 +7,16 @@ import classNames from 'classnames'; import Overlay from 'react-overlays/esm/Overlay'; -import { showAlert } from '@/mastodon/actions/alerts'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useRelationship } from '@/mastodon/hooks/useRelationship'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { useAppSelector } from '@/mastodon/store'; import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; import HelpIcon from '@/material-icons/400-24px/help.svg?react'; import DomainIcon from '@/material-icons/400-24px/language.svg?react'; import { FollowsYouBadge } from '../badge'; -import { Button } from '../button'; +import { CopyButton } from '../copy_button'; import { DisplayName } from '../display_name'; import { Icon } from '../icon'; @@ -90,17 +89,6 @@ const AccountNameHelp: FC<{ const handle = `@${username}@${domain}`; - const dispatch = useAppDispatch(); - const [copied, setCopied] = useState(false); - const handleCopy = useCallback(() => { - void navigator.clipboard.writeText(handle); - setCopied(true); - dispatch(showAlert({ message: messages.copied })); - setTimeout(() => { - setCopied(false); - }, 700); - }, [handle, dispatch]); - return ( <> + )} diff --git a/app/javascript/mastodon/components/account_header/styles.module.scss b/app/javascript/mastodon/components/account_header/styles.module.scss index 2daf3867346..4de49f79aea 100644 --- a/app/javascript/mastodon/components/account_header/styles.module.scss +++ b/app/javascript/mastodon/components/account_header/styles.module.scss @@ -110,6 +110,9 @@ word-break: break-all; text-align: left; + /* Allow the handle text to be selected */ + user-select: text; + > svg { width: 16px; height: 16px; @@ -480,6 +483,7 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; justify-content: center; align-items: flex-start; margin: 16px 0; + padding: 16px; } .bannerBaseCentered { diff --git a/app/javascript/mastodon/components/copy_button.tsx b/app/javascript/mastodon/components/copy_button.tsx new file mode 100644 index 00000000000..1812054d427 --- /dev/null +++ b/app/javascript/mastodon/components/copy_button.tsx @@ -0,0 +1,75 @@ +import { useState, useCallback } from 'react'; + +import { defineMessages } from 'react-intl'; + +import classNames from 'classnames'; + +import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; +import { showAlert } from 'mastodon/actions/alerts'; +import { IconButton } from 'mastodon/components/icon_button'; +import { useAppDispatch } from 'mastodon/store'; + +import { Button } from './button'; + +const messages = defineMessages({ + copied: { + id: 'copy_icon_button.copied', + defaultMessage: 'Copied to clipboard', + }, +}); + +export function useCopyToClipboard({ text }: { text: string }) { + const [wasCopied, setWasCopied] = useState(false); + const dispatch = useAppDispatch(); + + const copyText = useCallback(() => { + void navigator.clipboard.writeText(text); + setWasCopied(true); + dispatch(showAlert({ message: messages.copied })); + setTimeout(() => { + setWasCopied(false); + }, 700); + }, [setWasCopied, text, dispatch]); + + return { copyText, wasCopied }; +} + +export const CopyButton: React.FC< + Omit< + React.ComponentPropsWithoutRef, + 'onClick' | 'text' | 'children' + > & { + value: string; + children: React.ReactNode | ((wasCopied: boolean) => React.ReactNode); + } +> = ({ value, children, ...otherProps }) => { + const { copyText, wasCopied } = useCopyToClipboard({ text: value }); + + const label = typeof children === 'function' ? children(wasCopied) : children; + + return ( + + ); +}; + +export const CopyIconButton: React.FC<{ + title: string; + value: string; + className?: string; + 'aria-describedby'?: string; +}> = ({ title, value, className, 'aria-describedby': ariaDescribedBy }) => { + const { copyText, wasCopied } = useCopyToClipboard({ text: value }); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/copy_icon_button.tsx b/app/javascript/mastodon/components/copy_icon_button.tsx deleted file mode 100644 index 51cffe6292e..00000000000 --- a/app/javascript/mastodon/components/copy_icon_button.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useState, useCallback } from 'react'; - -import { defineMessages } from 'react-intl'; - -import classNames from 'classnames'; - -import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; -import { showAlert } from 'mastodon/actions/alerts'; -import { IconButton } from 'mastodon/components/icon_button'; -import { useAppDispatch } from 'mastodon/store'; - -const messages = defineMessages({ - copied: { - id: 'copy_icon_button.copied', - defaultMessage: 'Copied to clipboard', - }, -}); - -export const CopyIconButton: React.FC<{ - title: string; - value: string; - className?: string; - 'aria-describedby'?: string; -}> = ({ title, value, className, 'aria-describedby': ariaDescribedBy }) => { - const [copied, setCopied] = useState(false); - const dispatch = useAppDispatch(); - - const handleClick = useCallback(() => { - void navigator.clipboard.writeText(value); - setCopied(true); - dispatch(showAlert({ message: messages.copied })); - setTimeout(() => { - setCopied(false); - }, 700); - }, [setCopied, value, dispatch]); - - return ( - - ); -}; diff --git a/app/javascript/mastodon/components/empty_state/empty_state.module.scss b/app/javascript/mastodon/components/empty_state/empty_state.module.scss index 64d9a4e584e..cb2332aec6f 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.module.scss +++ b/app/javascript/mastodon/components/empty_state/empty_state.module.scss @@ -13,8 +13,7 @@ .content { max-width: 370px; - svg, - img { + :where(svg, img) { width: 200px; aspect-ratio: 1; object-fit: contain; @@ -22,16 +21,10 @@ margin-bottom: 16px; } - h3 { - font-size: 17px; - font-weight: 500; - text-wrap: balance; - line-height: 1.2; - } - p { margin-top: 8px; font-size: 15px; + line-height: 1.4; color: var(--color-text-secondary); text-wrap: pretty; } @@ -41,6 +34,18 @@ } } +.heading { + font-size: 17px; + font-weight: 500; + text-wrap: balance; + line-height: 1.2; +} + +.errorImage { + width: 280px; + margin: -10% 0; +} + [data-color-scheme='dark'] .defaultImage { --color-skin-1: #3a3a50; --color-skin-2: #67678e; diff --git a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx index 83fce034686..c1faaf6f399 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx +++ b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx @@ -29,6 +29,14 @@ export const Default: Story = { }, }; +export const Error: Story = { + args: { + image: 'error', + title: 'Error', + message: 'Something went wrong loading the page.', + }, +}; + export const WithAction: Story = { args: { ...Default.args, diff --git a/app/javascript/mastodon/components/empty_state/index.tsx b/app/javascript/mastodon/components/empty_state/index.tsx index e332aaedb5c..3f0738ce5a5 100644 --- a/app/javascript/mastodon/components/empty_state/index.tsx +++ b/app/javascript/mastodon/components/empty_state/index.tsx @@ -4,10 +4,15 @@ import classNames from 'classnames'; import ElephantImage from '@/images/elephant_ui.svg?react'; +import { GIF } from '../gif'; + import classes from './empty_state.module.scss'; const images = { default: , + error: ( + + ), }; /** @@ -21,6 +26,7 @@ export const EmptyState: React.FC<{ title?: React.ReactNode; message?: React.ReactNode; children?: React.ReactNode; + headingLevel?: 'h2' | 'h3' | 'h4'; className?: string; }> = ({ image = 'default', @@ -29,6 +35,7 @@ export const EmptyState: React.FC<{ ), message, children, + headingLevel: Heading = 'h2', className, }) => { const imageToRender = typeof image === 'string' ? images[image] : image; @@ -38,7 +45,7 @@ export const EmptyState: React.FC<{ {(title || message || imageToRender) && (
{imageToRender} - {!!title &&

{title}

} + {!!title && {title}} {!!message &&

{message}

}
)} diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index f3e7b454765..a0e0a36f790 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -100,6 +100,10 @@ interface ComboboxProps< * Icon to be displayed in the text input */ icon?: TextInputProps['icon'] | null; + /** + * Set to true to open as soon as there is focus + */ + openOnFocus?: boolean; /** * Set to false to keep the menu open when an item is selected */ @@ -217,8 +221,10 @@ const ComboboxWithRef = ( renderGroupTitle, renderItem, onSelectItem, + onFocus, onChange, onKeyDown, + openOnFocus = false, closeOnSelect = true, suppressMenu = false, icon = SearchIcon, @@ -288,6 +294,16 @@ const ComboboxWithRef = ( } }, []); + const handleFocus: React.FocusEventHandler = useCallback( + (e) => { + if (openOnFocus) { + setShouldMenuOpen(true); + } + onFocus?.(e); + }, + [onFocus, openOnFocus], + ); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { onChange(e); @@ -487,6 +503,7 @@ const ComboboxWithRef = ( autoComplete='off' spellCheck='false' value={value} + onFocus={handleFocus} onChange={handleInputChange} onKeyDown={handleInputKeyDown} icon={icon ?? undefined} diff --git a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx index d772315adeb..ab8849d45aa 100644 --- a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx +++ b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx @@ -4,7 +4,7 @@ import { useIntl } from 'react-intl'; import classNames from 'classnames'; -import { CopyIconButton } from 'mastodon/components/copy_icon_button'; +import { CopyIconButton } from '@/mastodon/components/copy_button'; import classes from './copy_link_field.module.scss'; import { FormFieldWrapper } from './form_field_wrapper'; diff --git a/app/javascript/mastodon/components/gif.tsx b/app/javascript/mastodon/components/gif.tsx index 1cc0881a5a3..7bce9857845 100644 --- a/app/javascript/mastodon/components/gif.tsx +++ b/app/javascript/mastodon/components/gif.tsx @@ -4,7 +4,7 @@ import { autoPlayGif } from 'mastodon/initial_state'; export const GIF: React.FC<{ src: string; staticSrc: string; - className: string; + className?: string; animate?: boolean; }> = ({ src, staticSrc, className, animate = autoPlayGif }) => { const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate); diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index a0c704a4e7c..dd7ff3b3e7e 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -144,11 +144,16 @@ export const HoverCardController: React.FC = () => { setScrollTimeout(handleScrollEnd, 100); }; - const handleMouseMove = () => { + const handleMouseMove = (e: MouseEvent) => { if (isUsingTouch) { isUsingTouch = false; } + const hasMoved = Math.max(e.movementX, e.movementY) > 0; + if (!hasMoved) { + return; + } + delayEnterTimeout(enterDelay); cancelMoveTimeout(); diff --git a/app/javascript/mastodon/components/scrollable_list/index.jsx b/app/javascript/mastodon/components/scrollable_list/index.jsx index a80fe5581aa..e44a827a2ff 100644 --- a/app/javascript/mastodon/components/scrollable_list/index.jsx +++ b/app/javascript/mastodon/components/scrollable_list/index.jsx @@ -285,7 +285,7 @@ class ScrollableList extends PureComponent { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); document.removeEventListener('wheel', this.handleWheel, listenerOptions); - } else { + } else if (this.node) { this.node.removeEventListener('scroll', this.handleScroll); this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index c909f9f0b1f..808f4d73bfc 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -556,7 +556,7 @@ class Status extends ImmutablePureComponent { ).find((item) => compareUrls(item.get('url'), cardUrl)); if (taggedCollection) { - media = ; + media = ; } else { media = ( + ); } } diff --git a/app/javascript/mastodon/components/status/header.tsx b/app/javascript/mastodon/components/status/header.tsx index 65790bb4932..1ce5c4a36c6 100644 --- a/app/javascript/mastodon/components/status/header.tsx +++ b/app/javascript/mastodon/components/status/header.tsx @@ -20,7 +20,8 @@ export interface StatusHeaderProps { status: Status; account?: Account; avatarSize?: number; - children?: ReactNode; + contentBeforeDate?: ReactNode; + contentAfterDate?: ReactNode; wrapperProps?: HTMLAttributes; displayNameProps?: DisplayNameProps; onHeaderClick?: MouseEventHandler; @@ -33,10 +34,11 @@ export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode; export const StatusHeader: FC = ({ status, account, - children, className, avatarSize = 48, wrapperProps, + contentBeforeDate, + contentAfterDate, onHeaderClick, }) => { const statusAccount = status.get('account') as Account | undefined; @@ -51,6 +53,14 @@ export const StatusHeader: FC = ({ className={classNames('status__info', className)} /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ > + + + {contentBeforeDate} + = ({ {editedAt && } - - - {children} + {contentAfterDate} ); }; diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 4d9ae4fa3f6..7e5cccf1986 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -69,7 +69,7 @@ class TranslateButton extends PureComponent { } const mapStateToProps = state => ({ - languages: state.server.translationLanguages.items, + languages: state.server.translationLanguages.item, }); class StatusContent extends PureComponent { @@ -187,7 +187,7 @@ class StatusContent extends PureComponent { const renderReadMore = this.props.onClick && status.get('collapsed'); const contentLocale = intl.locale.replace(/[_-].*/, ''); - const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); + const targetLanguages = this.props.languages?.[status.get('language') || 'und']; const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); const content = statusContent ?? getStatusContent(status); diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index 5c9804fb40e..b269792a5e3 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -225,17 +225,20 @@ export const QuotedStatus: React.FC = ({ const intl = useIntl(); const headerRenderFn: StatusHeaderRenderFn = useCallback( (props) => ( - - {onQuoteCancel && ( - - )} - + + ) + } + /> ), [intl, onQuoteCancel], ); diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 4c1ee5254ae..85b76aee32e 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -18,6 +18,7 @@ import { LinkFooter} from 'mastodon/features/ui/components/link_footer'; import { Section } from './components/section'; import { RulesSection } from './components/rules'; +import { getColumnSkipLinkId } from '../ui/components/skip_links'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -80,7 +81,7 @@ class About extends PureComponent { return ( -
+
- +

diff --git a/app/javascript/mastodon/features/account_edit/components/column.tsx b/app/javascript/mastodon/features/account_edit/components/column.tsx index a9b0f8cbd5d..2718aab7f21 100644 --- a/app/javascript/mastodon/features/account_edit/components/column.tsx +++ b/app/javascript/mastodon/features/account_edit/components/column.tsx @@ -9,7 +9,7 @@ import { Helmet } from '@unhead/react/helmet'; import { Column } from '@/mastodon/components/column'; import { ColumnHeader } from '@/mastodon/components/column_header'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import { useColumnsContext } from '../../ui/util/columns_context'; import classes from '../styles.module.scss'; diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx index 19ced4ce23c..ad903d5a5d1 100644 --- a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx +++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx @@ -2,14 +2,14 @@ import { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; import { openModal } from '@/mastodon/actions/modal'; import { Button } from '@/mastodon/components/button'; +import { DisplayName } from '@/mastodon/components/display_name'; import { EmptyState } from '@/mastodon/components/empty_state'; import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint'; -import { areCollectionsEnabled } from '@/mastodon/features/collections/utils'; +import { useAccount } from '@/mastodon/hooks/useAccount'; import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; import { useAppDispatch } from '@/mastodon/store'; @@ -28,8 +28,8 @@ export const EmptyMessage: React.FC = ({ blockedBy, withoutAddCollectionButton, }) => { - const { acct } = useParams<{ acct?: string }>(); const me = useCurrentAccountId(); + const account = useAccount(accountId); const dispatch = useAppDispatch(); @@ -49,56 +49,39 @@ export const EmptyMessage: React.FC = ({ let title: React.ReactNode = null; let message: React.ReactNode = null; - const hasCollections = areCollectionsEnabled(); - if (me === accountId) { - if (hasCollections) { - // Return only here to insert the "Create a collection" button as the action for the empty state. - return ( - + } + message={ + + } + > + {!withoutAddCollectionButton && ( + - } - message={ - - } - > - {!withoutAddCollectionButton && ( - - - - )} - - - ); - } else { - title = ( - - ); - message = ( - - ); - } + + )} + + + ); } else if (suspended) { title = ( = ({ /> ); } else { - if (acct) { + if (account) { title = ( }} /> ); } else { diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index 3082f19abc9..b743a7302ec 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -19,7 +19,7 @@ import { } from '@/mastodon/components/scrollable_list/components'; import type { TruncatedListItemInfo } from '@/mastodon/components/truncated_list'; import { TruncatedListItems } from '@/mastodon/components/truncated_list'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import Column from '@/mastodon/features/ui/components/column'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccountId } from '@/mastodon/hooks/useAccountId'; @@ -30,13 +30,10 @@ import AddIcon from '@/material-icons/400-24px/add.svg?react'; import { CollectionListItem } from '../collections/components/collection_list_item'; import { useCollectionsCreatedBy } from '../collections/overview/created_by_you'; -import { areCollectionsEnabled } from '../collections/utils'; import { EmptyMessage } from './components/empty_message'; import { Subheading, SubheadingLink } from './components/subheading'; -const collectionsEnabled = areCollectionsEnabled(); - const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ multiColumn, }) => { @@ -98,14 +95,11 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ ); const hasCollections = - collectionsEnabled && - collectionsLoadStatus === 'idle' && - listedCollections.length > 0; + collectionsLoadStatus === 'idle' && listedCollections.length > 0; const hasFeaturedAccounts = !featuredAccountIds.isEmpty(); - const isLoading = - !accountId || (collectionsEnabled && collectionsLoadStatus !== 'idle'); + const isLoading = !accountId || collectionsLoadStatus !== 'idle'; if (accountId === null) { return ; @@ -165,57 +159,53 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ )} - {collectionsEnabled && ( - <> - -

- -

- {accountId === me && ( - - - - )} -
- {hasCollections ? ( - - - ), - subtitle: ( - - ), - }} - renderListItem={renderListItem} - /> - - ) : ( -