From f3786e08165e304efab23579832e5b5159ce33f8 Mon Sep 17 00:00:00 2001 From: william light Date: Sat, 26 Jul 2025 07:51:50 +0200 Subject: [PATCH 01/53] hotkeys: only match `just()` when no modifiers are held (#35535) --- app/javascript/mastodon/components/hotkeys/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/hotkeys/index.tsx b/app/javascript/mastodon/components/hotkeys/index.tsx index b5e0de4c59..7e840ab955 100644 --- a/app/javascript/mastodon/components/hotkeys/index.tsx +++ b/app/javascript/mastodon/components/hotkeys/index.tsx @@ -40,7 +40,11 @@ type KeyMatcher = ( */ function just(keyName: string): KeyMatcher { return (event) => ({ - isMatch: normalizeKey(event.key) === keyName, + isMatch: + normalizeKey(event.key) === keyName && + !event.altKey && + !event.ctrlKey && + !event.metaKey, priority: hotkeyPriority.singleKey, }); } From 9d0d6f011c9760a9d166aa0f412825124e0a7045 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 03:59:32 -0400 Subject: [PATCH 02/53] Add coverage for `AnnouncementReaction` model (#35543) --- app/models/announcement_reaction.rb | 4 +- spec/models/announcement_reaction_spec.rb | 63 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 spec/models/announcement_reaction_spec.rb diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb index 46d9fc290f..855dfc9e3a 100644 --- a/app/models/announcement_reaction.rb +++ b/app/models/announcement_reaction.rb @@ -14,7 +14,7 @@ # class AnnouncementReaction < ApplicationRecord - before_validation :set_custom_emoji + before_validation :set_custom_emoji, if: :name? after_commit :queue_publish belongs_to :account @@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord private def set_custom_emoji - self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) if name.present? + self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) end def queue_publish diff --git a/spec/models/announcement_reaction_spec.rb b/spec/models/announcement_reaction_spec.rb new file mode 100644 index 0000000000..e02a8dcd06 --- /dev/null +++ b/spec/models/announcement_reaction_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AnnouncementReaction do + describe 'Associations' do + it { is_expected.to belong_to(:account) } + it { is_expected.to belong_to(:announcement).inverse_of(:announcement_reactions) } + it { is_expected.to belong_to(:custom_emoji).optional } + end + + describe 'Validations' do + subject { Fabricate.build :announcement_reaction } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_values('😀').for(:name) } + it { is_expected.to_not allow_values('INVALID').for(:name) } + + context 'when reaction limit is reached' do + subject { Fabricate.build :announcement_reaction, announcement: announcement_reaction.announcement } + + let(:announcement_reaction) { Fabricate :announcement_reaction, name: '😊' } + + before { stub_const 'ReactionValidator::LIMIT', 1 } + + it { is_expected.to_not allow_values('😀').for(:name).against(:base) } + end + end + + describe 'Callbacks' do + describe 'Setting custom emoji association' do + subject { Fabricate.build :announcement_reaction, name: } + + context 'when name is missing' do + let(:name) { '' } + + it 'does not set association' do + expect { subject.valid? } + .to not_change(subject, :custom_emoji).from(be_blank) + end + end + + context 'when name matches a custom emoji shortcode' do + let(:name) { 'custom' } + let!(:custom_emoji) { Fabricate :custom_emoji, shortcode: 'custom' } + + it 'sets association' do + expect { subject.valid? } + .to change(subject, :custom_emoji).from(be_blank).to(custom_emoji) + end + end + + context 'when name does not match a custom emoji' do + let(:name) { 'custom' } + + it 'does not set association' do + expect { subject.valid? } + .to not_change(subject, :custom_emoji).from(be_blank) + end + end + end + end +end From b464b87c2be774ce2972b217ae791366ed35bfbc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:02:21 -0400 Subject: [PATCH 03/53] Use `moved?` query method where relevant (#35542) --- app/controllers/settings/migration/redirects_controller.rb | 2 +- app/models/account_migration.rb | 2 +- app/models/form/redirect.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index d850e05e94..08b01d6b10 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController end def destroy - if current_account.moved_to_account_id.present? + if current_account.moved? current_account.update!(moved_to_account: nil) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) end diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index 7bda388f2a..6cdc7128ef 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -74,7 +74,7 @@ class AccountMigration < ApplicationRecord errors.add(:acct, I18n.t('migrations.errors.not_found')) else errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account)) - errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id + errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id end end diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb index c5b3c1f8f3..6ab95f21f1 100644 --- a/app/models/form/redirect.rb +++ b/app/models/form/redirect.rb @@ -40,7 +40,7 @@ class Form::Redirect if target_account.nil? errors.add(:acct, I18n.t('migrations.errors.not_found')) else - errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id + errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id end end From 5d69157e629de797ab7bff53bfc91b4c762205eb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:03:23 -0400 Subject: [PATCH 04/53] Prefer delegated nil-wrapping methods to safe navigation (#35541) --- app/views/admin/accounts/_counters.html.haml | 4 ++-- app/views/admin/accounts/_local_account.html.haml | 2 +- app/views/admin/reports/index.html.haml | 2 +- app/workers/move_worker.rb | 4 ++-- app/workers/publish_scheduled_status_worker.rb | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/admin/accounts/_counters.html.haml b/app/views/admin/accounts/_counters.html.haml index 00ab98d094..3c99da9f2c 100644 --- a/app/views/admin/accounts/_counters.html.haml +++ b/app/views/admin/accounts/_counters.html.haml @@ -30,9 +30,9 @@ = t('admin.accounts.suspended') - elsif account.silenced? = t('admin.accounts.silenced') - - elsif account.local? && account.user&.disabled? + - elsif account.local? && account.user_disabled? = t('admin.accounts.disabled') - - elsif account.local? && !account.user&.confirmed? + - elsif account.local? && !account.user_confirmed? = t('admin.accounts.confirming') - elsif account.local? && !account.user_approved? = t('admin.accounts.pending') diff --git a/app/views/admin/accounts/_local_account.html.haml b/app/views/admin/accounts/_local_account.html.haml index 892afcc542..bff752332c 100644 --- a/app/views/admin/accounts/_local_account.html.haml +++ b/app/views/admin/accounts/_local_account.html.haml @@ -34,7 +34,7 @@ %tr %th= t('admin.accounts.email_status') %td - - if account.user&.confirmed? + - if account.user_confirmed? = t('admin.accounts.confirmed') - else = t('admin.accounts.confirming') diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 6f762d94eb..44664b85fd 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -42,7 +42,7 @@ %span.red= t('admin.accounts.suspended') - elsif target_account.silenced? %span.red= t('admin.accounts.silenced') - - elsif target_account.user&.disabled? + - elsif target_account.user_disabled? %span.red= t('admin.accounts.disabled') - else %span.neutral= t('admin.accounts.no_limits_imposed') diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index a18f38556b..9b9c6879e5 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -81,7 +81,7 @@ class MoveWorker def copy_account_notes! AccountNote.where(target_account: @source_account).find_each do |note| - text = I18n.with_locale(note.account.user&.locale.presence || I18n.default_locale) do + text = I18n.with_locale(note.account.user_locale.presence || I18n.default_locale) do I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct) end @@ -124,7 +124,7 @@ class MoveWorker def add_account_note_if_needed!(account, id) unless AccountNote.exists?(account: account, target_account: @target_account) - text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do + text = I18n.with_locale(account.user_locale.presence || I18n.default_locale) do I18n.t(id, acct: @source_account.acct) end AccountNote.create!(account: account, target_account: @target_account, comment: text) diff --git a/app/workers/publish_scheduled_status_worker.rb b/app/workers/publish_scheduled_status_worker.rb index 0ec081de91..bcf20b4943 100644 --- a/app/workers/publish_scheduled_status_worker.rb +++ b/app/workers/publish_scheduled_status_worker.rb @@ -9,7 +9,7 @@ class PublishScheduledStatusWorker scheduled_status = ScheduledStatus.find(scheduled_status_id) scheduled_status.destroy! - return true if scheduled_status.account.user.disabled? + return true if scheduled_status.account.user_disabled? PostStatusService.new.call( scheduled_status.account, From 73f72ec8fe1ae19b22f5a1d9cfcb3b44a6c124a0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:08:31 -0400 Subject: [PATCH 05/53] Use `attribute` for defining `rate_limit` boolean (#35555) --- app/models/concerns/rate_limitable.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/concerns/rate_limitable.rb b/app/models/concerns/rate_limitable.rb index ad1b5e44e3..c6b5d3e084 100644 --- a/app/models/concerns/rate_limitable.rb +++ b/app/models/concerns/rate_limitable.rb @@ -3,12 +3,8 @@ module RateLimitable extend ActiveSupport::Concern - def rate_limit=(value) - @rate_limit = value - end - - def rate_limit? - @rate_limit + included do + attribute :rate_limit, :boolean, default: false end def rate_limiter(by, options = {}) From 720ee969692302b8020c5b7da393aff0c687c093 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:14:06 -0400 Subject: [PATCH 06/53] Move repeated snowflake ID gather to base classes (#35554) --- app/lib/admin/metrics/dimension/base_dimension.rb | 12 ++++++++++++ .../dimension/instance_languages_dimension.rb | 10 +--------- app/lib/admin/metrics/dimension/servers_dimension.rb | 10 +--------- .../metrics/dimension/tag_languages_dimension.rb | 10 +--------- .../admin/metrics/dimension/tag_servers_dimension.rb | 10 +--------- app/lib/admin/metrics/measure/base_measure.rb | 12 ++++++++++++ .../metrics/measure/instance_statuses_measure.rb | 10 +--------- app/lib/admin/metrics/measure/tag_servers_measure.rb | 10 +--------- 8 files changed, 30 insertions(+), 54 deletions(-) diff --git a/app/lib/admin/metrics/dimension/base_dimension.rb b/app/lib/admin/metrics/dimension/base_dimension.rb index bd2e4ececb..0e055e0e75 100644 --- a/app/lib/admin/metrics/dimension/base_dimension.rb +++ b/app/lib/admin/metrics/dimension/base_dimension.rb @@ -65,4 +65,16 @@ class Admin::Metrics::Dimension::BaseDimension def canonicalized_params params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';') end + + def earliest_status_id + snowflake_id(@start_at.beginning_of_day) + end + + def latest_status_id + snowflake_id(@end_at.end_of_day) + end + + def snowflake_id(datetime) + Mastodon::Snowflake.id_at(datetime, with_random: false) + end end diff --git a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb index 661e6d93b7..5f4bb95cb8 100644 --- a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb @@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di end def sql_array - [sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { domain: params[:domain], earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -36,14 +36,6 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:domain) end diff --git a/app/lib/admin/metrics/dimension/servers_dimension.rb b/app/lib/admin/metrics/dimension/servers_dimension.rb index 2c8406d52f..7e3ab603d0 100644 --- a/app/lib/admin/metrics/dimension/servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/servers_dimension.rb @@ -14,7 +14,7 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B end def sql_array - [sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -28,12 +28,4 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B LIMIT :limit SQL end - - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end end diff --git a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb index 6e283d2c65..b7b9abc8b6 100644 --- a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb @@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi end def sql_array - [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi params[:id] end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:id) end diff --git a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb index db820e965c..29145e1487 100644 --- a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb @@ -18,7 +18,7 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension end def sql_array - [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension params[:id] end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:id) end diff --git a/app/lib/admin/metrics/measure/base_measure.rb b/app/lib/admin/metrics/measure/base_measure.rb index 8b7fe39b55..eabbe0890b 100644 --- a/app/lib/admin/metrics/measure/base_measure.rb +++ b/app/lib/admin/metrics/measure/base_measure.rb @@ -104,4 +104,16 @@ class Admin::Metrics::Measure::BaseMeasure def canonicalized_params params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';') end + + def earliest_status_id + snowflake_id(@start_at.beginning_of_day) + end + + def latest_status_id + snowflake_id(@end_at.end_of_day) + end + + def snowflake_id(datetime) + Mastodon::Snowflake.id_at(datetime, with_random: false) + end end diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index 324d427b18..f0f797876e 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -28,7 +28,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure end def sql_array - [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id:, latest_status_id: }] end def sql_query_string @@ -50,14 +50,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index 5db1076062..e8d9cc43b8 100644 --- a/app/lib/admin/metrics/measure/tag_servers_measure.rb +++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb @@ -22,7 +22,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base end def sql_array - [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id:, latest_status_id: }] end def sql_query_string @@ -45,14 +45,6 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def tag @tag ||= Tag.find(params[:id]) end From a57a9505d40e3a8884b82f3d693a72e39a370bb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 08:14:11 +0000 Subject: [PATCH 07/53] New Crowdin Translations (automated) (#35537) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/ar.json | 3 +- app/javascript/mastodon/locales/da.json | 8 +- app/javascript/mastodon/locales/de.json | 2 +- app/javascript/mastodon/locales/en-GB.json | 8 +- app/javascript/mastodon/locales/lt.json | 3 + app/javascript/mastodon/locales/nan.json | 2 + app/javascript/mastodon/locales/sk.json | 2 + app/javascript/mastodon/locales/tr.json | 2 + app/javascript/mastodon/locales/zh-CN.json | 11 +++ config/locales/br.yml | 2 + config/locales/ca.yml | 1 + config/locales/cs.yml | 1 + config/locales/cy.yml | 1 + config/locales/da.yml | 3 +- config/locales/de.yml | 1 + config/locales/el.yml | 1 + config/locales/es-AR.yml | 1 + config/locales/es-MX.yml | 1 + config/locales/es.yml | 1 + config/locales/fo.yml | 1 + config/locales/gl.yml | 1 + config/locales/he.yml | 3 +- config/locales/hu.yml | 1 + config/locales/is.yml | 1 + config/locales/lt.yml | 6 ++ config/locales/nan.yml | 101 ++++++++++++++++++++- config/locales/nl.yml | 1 + config/locales/pt-PT.yml | 1 + config/locales/ru.yml | 61 +++++++------ config/locales/simple_form.da.yml | 4 +- config/locales/simple_form.lt.yml | 7 ++ config/locales/tr.yml | 1 + config/locales/uk.yml | 1 + config/locales/vi.yml | 1 + config/locales/zh-TW.yml | 3 +- 35 files changed, 201 insertions(+), 47 deletions(-) diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 24acd77e31..0443710638 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -110,7 +110,7 @@ "announcement.announcement": "إعلان", "annual_report.summary.archetype.booster": "The cool-hunter", "annual_report.summary.archetype.lurker": "المتصفح الصامت", - "annual_report.summary.archetype.oracle": "حكيم", + "annual_report.summary.archetype.oracle": "الحكيم", "annual_report.summary.archetype.pollster": "مستطلع للرأي", "annual_report.summary.archetype.replier": "الفراشة الاجتماعية", "annual_report.summary.followers.followers": "المُتابِعُون", @@ -845,6 +845,7 @@ "status.bookmark": "أضفه إلى الفواصل المرجعية", "status.cancel_reblog_private": "إلغاء إعادة النشر", "status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور", + "status.context.load_new_replies": "الردود الجديدة المتاحة", "status.continued_thread": "تكملة للخيط", "status.copy": "انسخ رابط الرسالة", "status.delete": "احذف", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 1cd633c747..15597e9a27 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -324,7 +324,7 @@ "empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.", "empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.", "empty_column.hashtag": "Der er intet med dette hashtag endnu.", - "empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at fylde den op.", + "empty_column.home": "Din hjem-tidslinje er tom! Følg nogle personer, for at fylde den op.", "empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.", "empty_column.mutes": "Du har endnu ikke skjult nogle brugere.", "empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.", @@ -476,7 +476,7 @@ "keyboard_shortcuts.favourites": "Åbn favoritlisten", "keyboard_shortcuts.federated": "Åbn fødereret tidslinje", "keyboard_shortcuts.heading": "Tastaturgenveje", - "keyboard_shortcuts.home": "Åbn hjemmetidslinje", + "keyboard_shortcuts.home": "Åbn hjem-tidslinje", "keyboard_shortcuts.hotkey": "Hurtigtast", "keyboard_shortcuts.legend": "Vis dette symbol", "keyboard_shortcuts.local": "Åbn lokal tidslinje", @@ -518,7 +518,7 @@ "lists.done": "Færdig", "lists.edit": "Redigér liste", "lists.exclusive": "Skjul medlemmer i Hjem", - "lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.", + "lists.exclusive_hint": "Hvis nogen er på denne liste, så skjul dem i hjem-feed for at undgå at se deres indlæg to gange.", "lists.find_users_to_add": "Find brugere at tilføje", "lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}", "lists.list_name": "Listetitel", @@ -792,7 +792,7 @@ "report.thanks.title": "Ønsker ikke at se dette?", "report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.", "report.unfollow": "Følg ikke længere @{name}", - "report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i din hjemmestrøm, kan du stoppe med at følge dem.", + "report.unfollow_explanation": "Du følger denne konto. Hvis du ikke længere vil se vedkommendes indlæg i dit hjem-feed, så stop med at følge dem.", "report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet", "report_notification.categories.legal": "Juridisk", "report_notification.categories.legal_sentence": "ikke-tilladt indhold", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 75f99fef4c..fc34c0fe19 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -43,7 +43,7 @@ "account.followers": "Follower", "account.followers.empty": "Diesem Profil folgt noch niemand.", "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}", - "account.followers_you_know_counter": "{counter} Follower kennst Du", + "account.followers_you_know_counter": "{counter} bekannt", "account.following": "Folge ich", "account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}", "account.follows.empty": "Dieses Profil folgt noch niemandem.", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 441cfee6d1..31d1c71b0e 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -1,7 +1,7 @@ { "about.blocks": "Moderated servers", "about.contact": "Contact:", - "about.default_locale": "Default", + "about.default_locale": "Varsayılan", "about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Reason not available", "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the Fediverse. These are the exceptions that have been made on this particular server.", @@ -9,11 +9,11 @@ "about.domain_blocks.silenced.title": "Limited", "about.domain_blocks.suspended.explanation": "No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.", "about.domain_blocks.suspended.title": "Suspended", - "about.language_label": "Language", + "about.language_label": "Dil", "about.not_available": "This information has not been made available on this server.", "about.powered_by": "Decentralised social media powered by {mastodon}", "about.rules": "Server rules", - "account.account_note_header": "Personal note", + "account.account_note_header": "Kişisel not", "account.add_or_remove_from_list": "Add or Remove from lists", "account.badges.bot": "Automated", "account.badges.group": "Group", @@ -845,6 +845,8 @@ "status.bookmark": "Bookmark", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.context.load_new_replies": "Yeni yanıtlar geldi.", + "status.context.loading": "Daha fazla yanıt kontrol ediliyor", "status.continued_thread": "Continued thread", "status.copy": "Copy link to status", "status.delete": "Delete", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index ed5fa83171..6060cfeda3 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -292,6 +292,7 @@ "emoji_button.search_results": "Paieškos rezultatai", "emoji_button.symbols": "Simboliai", "emoji_button.travel": "Kelionės ir vietos", + "empty_column.account_featured_other.unknown": "Ši paskyra dar nieko neparodė.", "empty_column.account_hides_collections": "Šis (-i) naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą.", "empty_column.account_suspended": "Paskyra pristabdyta.", "empty_column.account_timeline": "Nėra čia įrašų.", @@ -794,6 +795,8 @@ "status.bookmark": "Pridėti į žymės", "status.cancel_reblog_private": "Nebepasidalinti", "status.cannot_reblog": "Šis įrašas negali būti pakeltas.", + "status.context.load_new_replies": "Yra naujų atsakymų", + "status.context.loading": "Tikrinama dėl daugiau atsakymų", "status.continued_thread": "Tęsiama gijoje", "status.copy": "Kopijuoti nuorodą į įrašą", "status.delete": "Ištrinti", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index e7e8d2c0cd..8120312d80 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -845,6 +845,8 @@ "status.bookmark": "冊籤", "status.cancel_reblog_private": "取消轉送", "status.cannot_reblog": "Tsit篇PO文bē當轉送", + "status.context.load_new_replies": "有新ê回應", + "status.context.loading": "Leh檢查其他ê回應", "status.continued_thread": "接續ê討論線", "status.copy": "Khóo-pih PO文ê連結", "status.delete": "Thâi掉", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 091ddf8ca2..723207a4be 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -27,6 +27,8 @@ "account.edit_profile": "Upraviť profil", "account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}", "account.endorse": "Zobraziť na vlastnom profile", + "account.familiar_followers_one": "Nasledovanie od {name1}", + "account.familiar_followers_two": "Nasledovanie od {name1} a {name2}", "account.featured": "Zviditeľnené", "account.featured.accounts": "Profily", "account.featured.hashtags": "Hashtagy", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index c50ac3cbaa..9a8354d052 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -845,6 +845,8 @@ "status.bookmark": "Yer işareti ekle", "status.cancel_reblog_private": "Yeniden paylaşımı geri al", "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz", + "status.context.load_new_replies": "Yeni yanıtlar mevcut", + "status.context.loading": "Daha fazla yanıt için kontrol ediliyor", "status.continued_thread": "Devam eden akış", "status.copy": "Gönderi bağlantısını kopyala", "status.delete": "Sil", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index c6e0aa7f27..127ae87f63 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -301,6 +301,9 @@ "emoji_button.search_results": "搜索结果", "emoji_button.symbols": "符号", "emoji_button.travel": "旅行与地点", + "empty_column.account_featured.me": "你尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。", + "empty_column.account_featured.other": "{acct} 尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。", + "empty_column.account_featured_other.unknown": "该用户尚未设置任何精选。", "empty_column.account_hides_collections": "该用户选择不公开此信息", "empty_column.account_suspended": "账号已被停用", "empty_column.account_timeline": "这里没有嘟文!", @@ -402,8 +405,10 @@ "hashtag.counter_by_accounts": "{count, plural,other {{counter} 人讨论}}", "hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}", "hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}", + "hashtag.feature": "设为精选", "hashtag.follow": "关注话题", "hashtag.mute": "停止提醒 #{hashtag}", + "hashtag.unfeature": "取消精选", "hashtag.unfollow": "取消关注话题", "hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}", "hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。", @@ -558,6 +563,7 @@ "navigation_bar.preferences": "偏好设置", "navigation_bar.privacy_and_reach": "隐私与可达性", "navigation_bar.search": "搜索", + "navigation_bar.search_trends": "搜索/热门趋势", "navigation_panel.collapse_lists": "收起菜单列表", "navigation_panel.expand_lists": "展开菜单列表", "not_signed_in_indicator.not_signed_in": "你需要登录才能访问此资源。", @@ -786,6 +792,7 @@ "report_notification.categories.violation": "违反规则", "report_notification.categories.violation_sentence": "违反规则", "report_notification.open": "打开举报", + "search.clear": "清空搜索内容", "search.no_recent_searches": "无最近搜索", "search.placeholder": "搜索", "search.quick_action.account_search": "包含 {x} 的账号", @@ -827,6 +834,8 @@ "status.bookmark": "添加到书签", "status.cancel_reblog_private": "取消转嘟", "status.cannot_reblog": "不能转嘟这条嘟文", + "status.context.load_new_replies": "有新回复", + "status.context.loading": "正在检查更多回复", "status.continued_thread": "上接嘟文串", "status.copy": "复制嘟文链接", "status.delete": "删除", @@ -854,8 +863,10 @@ "status.pin": "在个人资料页面置顶", "status.quote_error.filtered": "已根据你的筛选器过滤", "status.quote_error.not_found": "无法显示这篇贴文。", + "status.quote_error.pending_approval": "此嘟文正在等待原作者批准。", "status.quote_error.rejected": "由于原作者不允许引用转发,无法显示这篇贴文。", "status.quote_error.removed": "该帖子已被作者删除。", + "status.quote_error.unauthorized": "你无权查看此嘟文,因此无法显示。", "status.quote_post_author": "{name} 的嘟文", "status.read_more": "查看更多", "status.reblog": "转嘟", diff --git a/config/locales/br.yml b/config/locales/br.yml index e5b9ff2559..4bfd268d15 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -560,6 +560,8 @@ br: one: "%{count} skeudenn" other: "%{count} skeudenn" two: "%{count} skeudenn" + errors: + quoted_status_not_found: War a seblant, n'eus ket eus an embannadenn emaoc'h o klask menegiñ. pin_errors: ownership: N'hallit ket spilhennañ embannadurioù ar re all quote_policies: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 1c858fe4cd..a3e62d7d72 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1870,6 +1870,7 @@ ca: edited_at_html: Editat %{date} errors: in_reply_not_found: El tut al qual intentes respondre sembla que no existeix. + quoted_status_not_found: Sembla que la publicació que vols citar no existeix. over_character_limit: Límit de caràcters de %{max} superat pin_errors: direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 1ad8df6e71..7df98b90bb 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1958,6 +1958,7 @@ cs: edited_at_html: Upraven %{date} errors: in_reply_not_found: Příspěvek, na který se pokoušíte odpovědět, neexistuje. + quoted_status_not_found: Zdá se, že příspěvek, který se pokoušíte citovat neexistuje. over_character_limit: byl překročen limit %{max} znaků pin_errors: direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout diff --git a/config/locales/cy.yml b/config/locales/cy.yml index d7242fbf2e..ab45a60012 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -2043,6 +2043,7 @@ cy: edited_at_html: Wedi'i olygu %{date} errors: in_reply_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio ei ateb yn bodoli. + quoted_status_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio'i ddyfynnu yn bodoli. over_character_limit: wedi mynd y tu hwnt i'r terfyn nodau o %{max} pin_errors: direct: Nid oes modd pinio postiadau sy'n weladwy i ddefnyddwyr a grybwyllwyd yn unig diff --git a/config/locales/da.yml b/config/locales/da.yml index b822ae9cb1..d2f6753181 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1394,7 +1394,7 @@ da: filters: contexts: account: Profiler - home: Hjemmetidslinje + home: Hjem og lister notifications: Notifikationer public: Offentlig tidslinje thread: Samtaler @@ -1872,6 +1872,7 @@ da: edited_at_html: Redigeret %{date} errors: in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere. + quoted_status_not_found: Indlægget, du forsøger at citere, ser ikke ud til at eksistere. over_character_limit: grænsen på %{max} tegn overskredet pin_errors: direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres diff --git a/config/locales/de.yml b/config/locales/de.yml index 1eb5ef1c4f..4b29dbac5d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1872,6 +1872,7 @@ de: edited_at_html: 'Bearbeitet: %{date}' errors: in_reply_not_found: Der Beitrag, auf den du antworten möchtest, scheint nicht zu existieren. + quoted_status_not_found: Der Beitrag, den du zitieren möchtest, scheint nicht zu existieren. over_character_limit: Begrenzung von %{max} Zeichen überschritten pin_errors: direct: Beiträge, die nur für erwähnte Profile sichtbar sind, können nicht angeheftet werden diff --git a/config/locales/el.yml b/config/locales/el.yml index f7c3df0f54..b1df6962ec 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1872,6 +1872,7 @@ el: edited_at_html: Επεξεργάστηκε στις %{date} errors: in_reply_not_found: Η ανάρτηση στην οποία προσπαθείς να απαντήσεις δεν φαίνεται να υπάρχει. + quoted_status_not_found: Η ανάρτηση την οποία προσπαθείς να παραθέσεις δεν φαίνεται να υπάρχει. over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων pin_errors: direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 3b26eebd29..93d4c3c31a 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1872,6 +1872,7 @@ es-AR: edited_at_html: Editado el %{date} errors: in_reply_not_found: El mensaje al que intentás responder no existe. + quoted_status_not_found: El mensaje al que intentás citar parece que no existe. over_character_limit: se excedió el límite de %{max} caracteres pin_errors: direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index cfc573fab8..e839a605e1 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1872,6 +1872,7 @@ es-MX: edited_at_html: Editado %{date} errors: in_reply_not_found: La publicación a la que estás intentando responder no existe. + quoted_status_not_found: La publicación que intentas citar no parece existir. over_character_limit: Límite de caracteres de %{max} superado pin_errors: direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse diff --git a/config/locales/es.yml b/config/locales/es.yml index 4b10f23b13..0381647115 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1872,6 +1872,7 @@ es: edited_at_html: Editado %{date} errors: in_reply_not_found: La publicación a la que intentas responder no existe. + quoted_status_not_found: La publicación que estás intentando citar no existe. over_character_limit: Límite de caracteres de %{max} superado pin_errors: direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 23a6b58976..9af87098a4 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1872,6 +1872,7 @@ fo: edited_at_html: Rættað %{date} errors: in_reply_not_found: Posturin, sum tú roynir at svara, sýnist ikki at finnast. + quoted_status_not_found: Posturin, sum tú roynir at sitera, sýnist ikki at finnast. over_character_limit: mesta tal av teknum, %{max}, rokkið pin_errors: direct: Postar, sum einans eru sjónligir hjá nevndum brúkarum, kunnu ikki festast diff --git a/config/locales/gl.yml b/config/locales/gl.yml index d19d47ab26..0b3a4133f5 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1872,6 +1872,7 @@ gl: edited_at_html: Editado %{date} errors: in_reply_not_found: A publicación á que tentas responder semella que non existe. + quoted_status_not_found: Parece que a publicación que intentas citar non existe. over_character_limit: Excedeu o límite de caracteres %{max} pin_errors: direct: As publicacións que só son visibles para as usuarias mencionadas non se poden fixar diff --git a/config/locales/he.yml b/config/locales/he.yml index 5071138764..ed80dfd6c9 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1957,7 +1957,8 @@ he: two: 'מכיל את התגיות האסורות: %{tags}' edited_at_html: נערך ב-%{date} errors: - in_reply_not_found: נראה שההודעה שאת/ה מנסה להגיב לה לא קיימת. + in_reply_not_found: נראה שההודעה שנסית להגיב לה לא קיימת. + quoted_status_not_found: נראה שההודעה שנסית לצטט לא קיימת. over_character_limit: חריגה מגבול התווים של %{max} pin_errors: direct: לא ניתן לקבע הודעות שנראותן מוגבלת למכותבים בלבד diff --git a/config/locales/hu.yml b/config/locales/hu.yml index b46062b2c7..ddb1841f1e 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1872,6 +1872,7 @@ hu: edited_at_html: 'Szerkesztve: %{date}' errors: in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél. + quoted_status_not_found: Már nem létezik az a bejegyzés, amelyből idézni szeretnél. over_character_limit: túllépted a maximális %{max} karakteres keretet pin_errors: direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki diff --git a/config/locales/is.yml b/config/locales/is.yml index 5e33189cfb..2e47f3245a 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1876,6 +1876,7 @@ is: edited_at_html: Breytt %{date} errors: in_reply_not_found: Færslan sem þú ert að reyna að svara að er líklega ekki til. + quoted_status_not_found: Færslan sem þú ert að reyna að vitna í virðist ekki vera til. over_character_limit: hámarksfjölda stafa (%{max}) náð pin_errors: direct: Ekki er hægt að festa færslur sem einungis eru sýnilegar þeim notendum sem minnst er á diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 41cf7cae96..3b31b138b8 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -912,6 +912,10 @@ lt: your_appeal_rejected: Tavo apeliacija buvo atmesta edit_profile: hint_html: "Tinkink tai, ką žmonės mato tavo viešame profilyje ir šalia įrašų. Kiti žmonės labiau linkę sekti atgal ir bendrauti su tavimi, jei tavo profilis yra užpildytas ir turi profilio nuotrauką." + emoji_styles: + auto: Automatinis + native: Vietiniai + twemoji: Tvejaustukai errors: '403': Jūs neturie prieigos matyti šiam puslapiui. '404': Puslapis nerastas. @@ -1183,6 +1187,8 @@ lt: other: "%{count} vaizdų" boosted_from_html: Pakelta iš %{acct_link} content_warning: 'Turinio įspėjimas: %{warning}' + errors: + quoted_status_not_found: Įrašas, kurį bandote cituoti, atrodo, neegzistuoja. over_character_limit: pasiektas %{max} simbolių limitas pin_errors: limit: Jūs jau prisegėte maksimalų toot'ų skaičų diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 354ef8b0f8..007ca847a8 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -97,7 +97,7 @@ nan: silenced: 受限制 suspended: 權限中止ah title: 管理 - moderation_notes: 管理ê註釋 + moderation_notes: 管理ê筆記 most_recent_activity: 最近ê活動時間 most_recent_ip: 最近ê IP no_account_selected: 因為無揀任何口座,所以lóng無改變 @@ -245,7 +245,7 @@ nan: create_announcement_html: "%{name} kā公告 %{target} 建立ah" create_canonical_email_block_html: "%{name} kā hash是 %{target} ê電子phue封鎖ah" create_custom_emoji_html: "%{name} kā 新ê emoji %{target} 傳上去ah" - create_domain_allow_html: "%{name} 允准 %{target} 域名加入聯邦宇宙" + create_domain_allow_html: "%{name} 允准 %{target} 域名加入聯邦" create_domain_block_html: "%{name} 封鎖域名 %{target}" create_email_domain_block_html: "%{name} kā 電子phue域名 %{target} 封鎖ah" create_ip_block_html: "%{name} 建立 IP %{target} ê規則" @@ -256,7 +256,7 @@ nan: destroy_announcement_html: "%{name} kā公告 %{target} thâi掉ah" destroy_canonical_email_block_html: "%{name} kā hash是 %{target} ê電子phue取消封鎖ah" destroy_custom_emoji_html: "%{name} kā 新ê emoji %{target} thâi掉ah" - destroy_domain_allow_html: "%{name} 無允准 %{target} 域名加入聯邦宇宙" + destroy_domain_allow_html: "%{name} 無允准 %{target} 域名加入聯邦" destroy_domain_block_html: "%{name} 取消封鎖域名 %{target}" destroy_email_domain_block_html: "%{name} kā 電子phue域名 %{target} 取消封鎖ah" destroy_instance_html: "%{name} 清除域名 %{target}" @@ -531,16 +531,111 @@ nan: content_policies: comment: 內部ê筆記 description_html: Lí ē當定義用tī所有tuì tsit ê域名kap伊ê子域名來ê口座ê內容政策。 + limited_federation_mode_description_html: Lí通選擇kám beh允准tsit ê域名加入聯邦。 + policies: + reject_media: 拒絕媒體 + reject_reports: 拒絕檢舉 + silence: 限制 + suspend: 中止權限 + policy: 政策 + reason: 公開ê理由 + title: 內容政策 dashboard: + instance_accounts_dimension: 上tsē lâng跟tuè ê口座 + instance_accounts_measure: 儲存ê口座 + instance_followers_measure: lán tī hia ê跟tuè者 + instance_follows_measure: in tī tsia ê跟tuè者 instance_languages_dimension: Tsia̍p用ê語言 + instance_media_attachments_measure: 儲存ê媒體附件 + instance_reports_measure: 關係in ê檢舉 + instance_statuses_measure: 儲存ê PO文 + delivery: + all: 全部 + clear: 清寄送ê錯誤 + failing: 失敗 + restart: 重頭啟動寄送 + stop: 停止寄送 + unavailable: Bē當用 + delivery_available: 通寄送 + delivery_error_days: 寄送錯誤ê日數 + delivery_error_hint: Nā連續 %{count} kang bē當寄送,就ē自動標做bē當寄送。 + destroyed_msg: Tuì %{domain} 來ê資料,teh排隊beh thâi掉。 + empty: Tshuē無域名。 + known_accounts: + other: "%{count} ê知影ê口座" + moderation: + all: 全部 + limited: 受限制 + title: 管理 + moderation_notes: + create: 加添管理筆記 + created_msg: 站臺ê管理記錄成功建立! + description_html: 檢視á是替別ê管理者kap未來ê家己留筆記 + destroyed_msg: 站臺ê管理記錄成功thâi掉! + placeholder: 關係本站、行ê行動,á是其他通幫tsān lí未來管本站ê資訊。 + title: 管理ê筆記 + private_comment: 私人評論 + public_comment: 公開ê評論 + purge: 清除 + purge_description_html: Nā lí想講tsit ê域名ē永永斷線,ē當tuì儲存內底thâi掉uì tsit ê域名來ê所有口座記錄kap相關資料。Huân-sè ē開點á時間。 + title: 聯邦 + total_blocked_by_us: Hōo lán封鎖 + total_followed_by_them: Hōo in跟tuè + total_followed_by_us: Hōo lán跟tuè + total_reported: 關係in ê檢舉 + total_storage: 媒體ê附件 + totals_time_period_hint_html: 下kha顯示ê總計包含ta̍k時ê資料。 + unknown_instance: 佇本服務器,現tsú時iáu無tsit ê域名ê記錄。 invites: + deactivate_all: Lóng停用 filter: + all: 全部 available: 通用ê expired: 過期ê title: 過濾器 title: 邀請 ip_blocks: add_new: 建立規則 + created_msg: 成功加添新ê IP規則 + delete: Thâi掉 + expires_in: + '1209600': 2 禮拜 + '15778476': 6個月 + '2629746': 1 個月 + '31556952': 1 年 + '86400': 1 kang + '94670856': 3 年 + new: + title: 建立新ê IP規則 + no_ip_block_selected: 因為無揀任何IP規則,所以lóng無改變 + title: IP規則 + relationships: + title: "%{acct} ê關係" + relays: + add_new: 加添新ê中繼 + delete: Thâi掉 + description_html: "聯邦ê中繼站 是中lâng ê服侍器,ē tī訂koh公開kàu hit ê中繼站ê服侍器之間,交換tsē-tsē ê 公開PO文。中繼站通幫tsān小型kap中型服侍器tuì聯邦宇宙發現內容,本地ê用者免手動跟tuè遠距離ê服侍器ê別lâng。" + disable: 停止使用 + disabled: 停止使用ê + enable: 啟用 + enable_hint: Lí ê服侍器tsi̍t-ē啟動,ē訂tuì tsit ê中繼逐ê公開PO文,mā ē開始送tsit ê服侍器ê公開PO文kàu hia。 + enabled: 啟用ê + inbox_url: 中繼 URL + pending: Teh等中繼站允准 + save_and_enable: 儲存koh啟用 + setup: 設定中繼ê連結 + signatures_not_enabled: Nā啟用安全模式á是受限ê聯邦模式,中繼可能buē-tàng正常運作 + status: 狀態 + title: 中繼 + report_notes: + created_msg: 檢舉記錄成功建立! + destroyed_msg: 檢舉記錄成功thâi掉! + reports: + account: + notes: + other: "%{count} 篇筆記" + action_log: 審查日誌 + action_taken_by: 操作由 statuses: language: 語言 trends: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index b53aa68a65..6d26903a20 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1872,6 +1872,7 @@ nl: edited_at_html: Bewerkt op %{date} errors: in_reply_not_found: Het bericht waarop je probeert te reageren lijkt niet te bestaan. + quoted_status_not_found: Het bericht die je probeert te citeren lijkt niet te bestaan. over_character_limit: Limiet van %{max} tekens overschreden pin_errors: direct: Berichten die alleen zichtbaar zijn voor vermelde gebruikers, kunnen niet worden vastgezet diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index f2171b8078..1e988b5bae 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1872,6 +1872,7 @@ pt-PT: edited_at_html: Editado em %{date} errors: in_reply_not_found: A publicação a que estás a tentar responder parece não existir. + quoted_status_not_found: A publicação que está a tentar citar parece não existir. over_character_limit: limite de caracteres %{max} excedido pin_errors: direct: As publicações que só são visíveis para os utilizadores mencionados não podem ser fixadas diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 35f79c67c0..4cc2e15b5a 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1352,7 +1352,7 @@ ru: edit_profile: basic_information: Основные данные hint_html: "Здесь вы можете изменить всё то, что будет отображаться в вашем публичном профиле и рядом с вашими постами. На вас будут чаще подписываться и с вами будут чаще взаимодействовать, если у вас будет заполнен профиль и добавлено фото профиля." - other: Прочее + other: Разное emoji_styles: auto: Автоматически native: Как в системе @@ -1630,19 +1630,19 @@ ru: media_attachments: validations: images_and_video: Нельзя добавить видео к посту с изображениями - not_found: Медиа %{ids} не найдено или уже прикреплено к другому сообщению - not_ready: Не удаётся прикрепить файлы, обработка которых не завершена. Повторите попытку чуть позже! - too_many: Нельзя добавить более 4 файлов + not_found: Медиа %{ids} не найдены или уже прикреплены к другому посту + not_ready: Обработка некоторых прикреплённых файлов ещё не окончена. Подождите немного и попробуйте снова! + too_many: Можно прикрепить не более 4 файлов migrations: - acct: имя@домен новой учётной записи + acct: Куда cancel: Отменить переезд - cancel_explanation: Отмена перенаправления повторно активирует текущую учётную запись, но не вернёт обратно подписчиков, которые были перемещены на другую. - cancelled_msg: Переезд был успешно отменён. + cancel_explanation: После отмены перенаправления ваша текущая учётная запись снова станет активна, но ранее перенесённые подписчики не будут возвращены. + cancelled_msg: Переезд отменён. errors: - already_moved: это та же учётная запись, на которую вы мигрировали - missing_also_known_as: не ссылается на эту учетную запись + already_moved: не может быть той учётной записью, куда уже настроен переезд + missing_also_known_as: должна быть связанной учётной записью move_to_self: не может быть текущей учётной записью - not_found: не удалось найти + not_found: не найдена on_cooldown: Вы пока не можете переезжать followers_count: Подписчиков на момент переезда incoming_migrations: Переезд со старой учётной записи @@ -1650,25 +1650,25 @@ ru: moved_msg: Теперь ваша учётная запись перенаправляет к %{acct}, туда же перемещаются подписчики. not_redirecting: Для вашей учётной записи пока не настроено перенаправление. on_cooldown: Вы уже недавно переносили свою учётную запись. Эта возможность будет снова доступна через %{count} дн. - past_migrations: Прошлые переезды + past_migrations: История переездов proceed_with_move: Перенести подписчиков redirected_msg: Ваша учётная запись теперь перенаправляется на %{acct}. redirecting_to: Ваша учётная запись перенаправляет к %{acct}. set_redirect: Настроить перенаправление warning: - backreference_required: Новая учётная запись должна быть сначала настроена так, чтоб ссылаться на текущую - before: 'Прежде чем продолжить, внимательно прочитайте следующую информацию:' - cooldown: После переезда наступает период, в течение которого вы не сможете ещё раз переехать + backreference_required: Текущая учётная запись сначала должна быть добавлена как связанная в настройках новой учётной записи + before: 'Внимательно ознакомьтесь со следующими замечаниями перед тем как продолжить:' + cooldown: После переезда наступит период ожидания, в течение которого переезд будет невозможен disabled_account: Вашу текущую учётная запись впоследствии нельзя будет больше использовать. При этом, у вас будет доступ к экспорту данных, а также к повторной активации учётной записи. - followers: Это действие перенесёт всех ваших подписчиков с текущей учётной записи на новую - only_redirect_html: Или же вы можете просто настроить перенаправление в ваш профиль. + followers: В результате переезда все ваши подписчики будут перенесены с текущей учётной записи на новую + only_redirect_html: Также вы можете настроить перенаправление без переноса подписчиков. other_data: Никакие другие данные не будут автоматически перенесены - redirect: Профиль этой учётной записи будет обновлён с заметкой о перенаправлении, а также исключён из поиска + redirect: Профиль текущей учётной записи будет исключён из поиска, а в нём появится объявление о переезде moderation: title: Модерация move_handler: carry_blocks_over_text: Этот пользователь переехал с учётной записи %{acct}, которую вы заблокировали. - carry_mutes_over_text: Этот пользователь перешёл с учётной записи %{acct}, которую вы игнорируете. + carry_mutes_over_text: Этот пользователь переехал с учётной записи %{acct}, которую вы игнорируете. copy_account_note_text: 'Этот пользователь переехал с %{acct}, вот ваша предыдущая заметка о нём:' navigation: toggle_menu: Переключить меню @@ -1717,8 +1717,9 @@ ru: billion: млрд million: млн quadrillion: квадрлн - thousand: тыс + thousand: тыс. trillion: трлн + unit: '' otp_authentication: code_hint: Для подтверждения введите код, сгенерированный приложением-аутентификатором description_html: Подключив двуфакторную авторизацию, для входа в свою учётную запись вам будет необходим смартфон и приложение-аутентификатор на нём, которое будет генерировать специальные временные коды. Без этих кодов войти в учётную запись не получиться, даже если все данные верны, что существенно увеличивает безопасность вашей учётной записи. @@ -1728,32 +1729,32 @@ ru: setup: Настроить wrong_code: Введенный код недействителен! Время сервера и время устройства правильно? pagination: - newer: Новее - next: След - older: Старше - prev: Пред + newer: Позже + next: Вперёд + older: Раньше + prev: Назад truncate: "…" polls: errors: - already_voted: Вы уже голосовали в этом опросе + already_voted: Вы уже проголосовали в этом опросе duplicate_options: не должны повторяться duration_too_long: слишком велика duration_too_short: слишком мала - expired: Опрос уже завершился - invalid_choice: Выбранного варианта голосования не существует + expired: Этот опрос уже завершился + invalid_choice: Выбранного вами варианта ответа не существует over_character_limit: не должны превышать %{max} символов self_vote: Вы не можете голосовать в своих опросах too_few_options: должны содержать не меньше двух опций too_many_options: должны ограничиваться максимум %{max} опциями preferences: - other: Остальное - posting_defaults: Настройки отправки по умолчанию + other: Разное + posting_defaults: Предустановки для новых постов public_timelines: Публичные ленты privacy: hint_html: "Настройте, как вы хотите, чтобы ваш профиль и ваши сообщения были найдены. Различные функции в Mastodon могут помочь вам охватить более широкую аудиторию, если они включены. Уделите время изучению этих настроек, чтобы убедиться, что они подходят для вашего случая использования." - privacy: Конфиденциальность + privacy: Приватность privacy_hint_html: Определите, какую информацию вы хотите раскрыть в интересах других. Люди находят интересные профили и приложения, просматривая список подписчиков других людей и узнавая, из каких приложений они публикуют свои сообщения, но вы можете предпочесть скрыть это. - reach: Охват + reach: Видимость reach_hint_html: Укажите, хотите ли вы, чтобы новые люди обнаруживали вас и могли следить за вами. Хотите ли вы, чтобы ваши сообщения появлялись на экране Обзора? Хотите ли вы, чтобы другие люди видели вас в своих рекомендациях? Хотите ли вы автоматически принимать всех новых подписчиков или иметь возможность детально контролировать каждого из них? search: Поиск search_hint_html: Определите, как вас могут найти. Хотите ли вы, чтобы люди находили вас по тому, о чём вы публично писали? Хотите ли вы, чтобы люди за пределами Mastodon находили ваш профиль при поиске в Интернете? Следует помнить, что полное исключение из всех поисковых систем не может быть гарантировано для публичной информации. diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 09041a4113..025e2decd5 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -9,7 +9,7 @@ da: fields: Din hjemmeside, dine pronominer, din alder, eller hvad du har lyst til. indexable: Dine offentlige indlæg vil kunne vises i Mastodon-søgeresultater. Folk, som har interageret med dem, vil kunne finde dem uanset. note: 'Du kan @omtale andre personer eller #hashtags.' - show_collections: Folk vil ikke kunne tjekke dine Følger og Følgere. Folk, du selv følger, vil stadig kunne se dette. + show_collections: Folk vil kunne se, hvem du følger, og hvem der følger dig. Personer, som du følger, vil kunne se, at du følger dem. unlocked: Man vil kunne følges af folk uden først at godkende dem. Ønsker man at gennemgå Følg-anmodninger og individuelt acceptere/afvise nye følgere, så fjern markeringen. account_alias: acct: Angiv brugernavn@domain for den konto, hvorfra du vil flytte @@ -75,7 +75,7 @@ da: featured_tag: name: 'Her er nogle af dine hyppigst brugte hashtags:' filters: - action: Vælg handlingen til eksekvering, når et indlæg matcher filteret + action: Vælg, hvilken handling, der skal udføres, når et indlæg matcher filteret actions: blur: Skjul medier bag en advarsel, uden at skjule selve teksten hide: Skjul det filtrerede indhold fuldstændigt og gør, som om det ikke eksisterer diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml index 4aa89ed9ec..bbd22e3f76 100644 --- a/config/locales/simple_form.lt.yml +++ b/config/locales/simple_form.lt.yml @@ -60,6 +60,7 @@ lt: setting_display_media_default: Slėpti mediją, pažymėtą kaip jautrią setting_display_media_hide_all: Visada slėpti mediją setting_display_media_show_all: Visada rodyti mediją + setting_emoji_style: Kaip rodyti emodžius. „Auto“ bandys naudoti vietinius jaustukus, bet senesnėse naršyklėse grįš prie Tvejaustukų. setting_system_scrollbars_ui: Taikoma tik darbalaukio naršyklėms, karkasiniais „Safari“ ir „Chrome“. setting_use_blurhash: Gradientai pagrįsti paslėptų vizualizacijų spalvomis, bet užgožia bet kokias detales. setting_use_pending_items: Slėpti laiko skalės naujienas po paspaudimo, vietoj automatinio srauto slinkimo. @@ -120,6 +121,11 @@ lt: min_age: Neturėtų būti žemiau mažiausio amžiaus, reikalaujamo pagal jūsų jurisdikcijos įstatymus. user: chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis. + date_of_birth: + few: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + many: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + one: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + other: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. role: Vaidmuo valdo, kokius leidimus naudotojas turi. labels: account: @@ -162,6 +168,7 @@ lt: setting_display_media: Medijos rodymas setting_display_media_hide_all: Slėpti viską setting_display_media_show_all: Rodyti viską + setting_emoji_style: Jaustuko stilius setting_expand_spoilers: Visada išplėsti įrašus, pažymėtus turinio įspėjimais setting_hide_network: Slėpti savo socialinę diagramą setting_missing_alt_text_modal: Rodyti patvirtinimo dialogo langą prieš skelbiant mediją be alternatyvaus teksto. diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 89c51ca6e0..2b0164e9bb 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1872,6 +1872,7 @@ tr: edited_at_html: "%{date} tarihinde düzenlendi" errors: in_reply_not_found: Yanıtlamaya çalıştığınız durum yok gibi görünüyor. + quoted_status_not_found: Alıntılamaya çalıştığınız gönderi mevcut görünmüyor. over_character_limit: "%{max} karakter limiti aşıldı" pin_errors: direct: Sadece değinilen kullanıcıların görebileceği gönderiler üstte tutulamaz diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ceadfc3466..88a14b0aa4 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1846,6 +1846,7 @@ uk: edited_at_html: Відредаговано %{date} errors: in_reply_not_found: Допису, на який ви намагаєтеся відповісти, не існує. + quoted_status_not_found: Повідомлення, яке ви намагаєтеся цитувати, не існує. over_character_limit: перевищено ліміт символів %{max} pin_errors: direct: Не можливо прикріпити дописи, які видимі лише згаданим користувачам diff --git a/config/locales/vi.yml b/config/locales/vi.yml index d56878d161..adc801c449 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1829,6 +1829,7 @@ vi: edited_at_html: Sửa %{date} errors: in_reply_not_found: Bạn đang trả lời một tút không còn tồn tại. + quoted_status_not_found: Bạn đang trích dẫn một tút không còn tồn tại. over_character_limit: vượt quá giới hạn %{max} ký tự pin_errors: direct: Không thể ghim những tút nhắn riêng diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 546ac7d989..bc29a30b33 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1830,7 +1830,8 @@ zh-TW: other: 含有不得使用的標籤: %{tags} edited_at_html: 編輯於 %{date} errors: - in_reply_not_found: 您嘗試回覆的嘟文看起來不存在。 + in_reply_not_found: 您嘗試回覆之嘟文似乎不存在。 + quoted_status_not_found: 您嘗試引用之嘟文似乎不存在。 over_character_limit: 已超過 %{max} 字的限制 pin_errors: direct: 無法釘選只有僅提及使用者可見之嘟文 From 018e5e303fd85f05ec7684fb4f1152b1ac6d9794 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 28 Jul 2025 10:20:12 +0200 Subject: [PATCH 08/53] Fix jobs being added to batch after they might already execute (#35496) --- app/models/worker_batch.rb | 40 +++++++++++++++---- .../activitypub/fetch_replies_service.rb | 7 +++- config/initializers/sidekiq.rb | 6 +-- lib/mastodon/worker_batch_middleware.rb | 11 +++++ 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 lib/mastodon/worker_batch_middleware.rb diff --git a/app/models/worker_batch.rb b/app/models/worker_batch.rb index f741071ba9..0a22ce6141 100644 --- a/app/models/worker_batch.rb +++ b/app/models/worker_batch.rb @@ -19,17 +19,22 @@ class WorkerBatch redis.hset(key, { 'async_refresh_key' => async_refresh_key, 'threshold' => threshold }) end + def within + raise NoBlockGivenError unless block_given? + + begin + Thread.current[:batch] = self + yield + ensure + Thread.current[:batch] = nil + end + end + # Add jobs to the batch. Usually when the batch is created. # @param [Array] jids def add_jobs(jids) if jids.blank? - async_refresh_key = redis.hget(key, 'async_refresh_key') - - if async_refresh_key.present? - async_refresh = AsyncRefresh.new(async_refresh_key) - async_refresh.finish! - end - + finish! return end @@ -55,8 +60,23 @@ class WorkerBatch if async_refresh_key.present? async_refresh = AsyncRefresh.new(async_refresh_key) async_refresh.increment_result_count(by: 1) - async_refresh.finish! if pending.zero? || processed >= threshold.to_f * (processed + pending) end + + if pending.zero? || processed >= (threshold || 1.0).to_f * (processed + pending) + async_refresh&.finish! + cleanup + end + end + + def finish! + async_refresh_key = redis.hget(key, 'async_refresh_key') + + if async_refresh_key.present? + async_refresh = AsyncRefresh.new(async_refresh_key) + async_refresh.finish! + end + + cleanup end # Get pending jobs. @@ -76,4 +96,8 @@ class WorkerBatch def key(suffix = nil) "worker_batch:#{@id}#{":#{suffix}" if suffix}" end + + def cleanup + redis.del(key, key('jobs')) + end end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index 25eb275ca5..239df0ba58 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -17,7 +17,12 @@ class ActivityPub::FetchRepliesService < BaseService batch = WorkerBatch.new batch.connect(async_refresh_key) if async_refresh_key.present? - batch.add_jobs(FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] }) + batch.finish! if @items.empty? + batch.within do + FetchReplyWorker.push_bulk(@items) do |reply_uri| + [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] + end + end [@items, n_pages] end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 3c2f12780c..7edaf38a60 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../lib/mastodon/sidekiq_middleware' +require_relative '../../lib/mastodon/worker_batch_middleware' Sidekiq.configure_server do |config| config.redis = REDIS_CONFIGURATION.sidekiq @@ -72,14 +73,12 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Mastodon::SidekiqMiddleware - end - - config.server_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Server end config.client_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Client + chain.add Mastodon::WorkerBatchMiddleware end config.on(:startup) do @@ -105,6 +104,7 @@ Sidekiq.configure_client do |config| config.client_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Client + chain.add Mastodon::WorkerBatchMiddleware end config.logger.level = Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s) diff --git a/lib/mastodon/worker_batch_middleware.rb b/lib/mastodon/worker_batch_middleware.rb new file mode 100644 index 0000000000..c462301332 --- /dev/null +++ b/lib/mastodon/worker_batch_middleware.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Mastodon::WorkerBatchMiddleware + def call(_worker, msg, _queue, _redis_pool = nil) + if (batch = Thread.current[:batch]) + batch.add_jobs([msg['jid']]) + end + + yield + end +end From 7cd3738c19860b5ad3997ce16e8f8056d321eb62 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:26:29 -0400 Subject: [PATCH 09/53] Add `with_list_account` scope to `List` model (#35539) --- app/models/follow_request.rb | 2 +- app/models/list.rb | 2 ++ app/services/follow_service.rb | 2 +- app/services/unfollow_service.rb | 2 +- app/services/unmute_service.rb | 2 +- spec/models/list_spec.rb | 22 ++++++++++++++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 964d4e279a..0b518036b1 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -37,7 +37,7 @@ class FollowRequest < ApplicationRecord if account.local? ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id) MergeWorker.perform_async(target_account.id, account.id, 'home') - MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id| [target_account.id, list_id, 'list'] end end diff --git a/app/models/list.rb b/app/models/list.rb index 76c116ce24..8fd1953ab3 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -32,6 +32,8 @@ class List < ApplicationRecord before_destroy :clean_feed_manager + scope :with_list_account, ->(account) { joins(:list_accounts).where(list_accounts: { account: }) } + private def validate_account_lists_limit diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 2928c85390..5ff1b63503 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -82,7 +82,7 @@ class FollowService < BaseService LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow') MergeWorker.perform_async(@target_account.id, @source_account.id, 'home') - MergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:id)) do |list_id| [@target_account.id, list_id, 'list'] end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index b3f2cd66f6..1ea8af6992 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -34,7 +34,7 @@ class UnfollowService < BaseService unless @options[:skip_unmerge] UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home') - UnmergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:list_id)) do |list_id| + UnmergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:list_id)) do |list_id| [@target_account.id, list_id, 'list'] end end diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb index 9262961f7d..0a9604bae2 100644 --- a/app/services/unmute_service.rb +++ b/app/services/unmute_service.rb @@ -9,7 +9,7 @@ class UnmuteService < BaseService if account.following?(target_account) MergeWorker.perform_async(target_account.id, account.id, 'home') - MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id| [target_account.id, list_id, 'list'] end end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 48c273d3ec..e2d91835ec 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -28,4 +28,26 @@ RSpec.describe List do end end end + + describe 'Scopes' do + describe '.with_list_account' do + let(:alice) { Fabricate :account } + let(:bob) { Fabricate :account } + let(:list) { Fabricate :list } + let(:other_list) { Fabricate :list } + + before do + Fabricate :follow, account: list.account, target_account: alice + Fabricate :follow, account: other_list.account, target_account: bob + Fabricate :list_account, list: list, account: alice + Fabricate :list_account, list: other_list, account: bob + end + + it 'returns lists connected to the account' do + expect(described_class.with_list_account(alice)) + .to include(list) + .and not_include(other_list) + end + end + end end From 3b01f98c11d2dc0c77a581be8f53c974b39f63d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:32:47 +0200 Subject: [PATCH 10/53] fix(deps): update dependency vite-plugin-pwa to v1.0.2 (#35529) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2c1faaa91a..746487e065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13791,8 +13791,8 @@ __metadata: linkType: hard "vite-plugin-pwa@npm:^1.0.0": - version: 1.0.1 - resolution: "vite-plugin-pwa@npm:1.0.1" + version: 1.0.2 + resolution: "vite-plugin-pwa@npm:1.0.2" dependencies: debug: "npm:^4.3.6" pretty-bytes: "npm:^6.1.1" @@ -13807,7 +13807,7 @@ __metadata: peerDependenciesMeta: "@vite-pwa/assets-generator": optional: true - checksum: 10c0/ceca04df97877ca97eb30805207d4826bd6340796194c9015afeefeb781931bf9019a630c5a0bdaa6dffcada11ce1fdf8595ac48a08d751dff81601aa0c7db38 + checksum: 10c0/e4f2f4dfff843ee2585a0d89e74187168ba20da77cd0d127ce7ad7eebcf5a68b2bf09000afb6bb86d43a2034fea9f568cd6db2a2d4b47a72e175d999a5e07eb1 languageName: node linkType: hard From 038de44110b4142d65698f08fcf733e1cd69d4bf Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:38:55 -0400 Subject: [PATCH 11/53] Fix `Style/GuardClause` in `Webfinger` lib (#35532) --- app/lib/webfinger.rb | 16 +++++------- spec/lib/webfinger_spec.rb | 50 ++++++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb index 83b5415a33..c39c25e994 100644 --- a/app/lib/webfinger.rb +++ b/app/lib/webfinger.rb @@ -84,22 +84,18 @@ class Webfinger def body_from_host_meta host_meta_request.perform do |res| - if res.code == 200 - body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false) - else - raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" - end + raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" unless res.code == 200 + + body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false) end end def url_from_template(str) link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]') - if link.present? - link['template'].gsub('{uri}', @uri) - else - raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" - end + raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" if link.blank? + + link['template'].gsub('{uri}', @uri) rescue Nokogiri::XML::XPath::SyntaxError raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}" end diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb index 5015deac7f..e214a03536 100644 --- a/spec/lib/webfinger_spec.rb +++ b/spec/lib/webfinger_spec.rb @@ -4,15 +4,15 @@ require 'rails_helper' RSpec.describe Webfinger do describe 'self link' do + subject { described_class.new('acct:alice@example.com').perform } + context 'when self link is specified with type application/activity+json' do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } it 'correctly parses the response' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' + expect(subject.self_link_href).to eq 'https://example.com/alice' end end @@ -22,9 +22,7 @@ RSpec.describe Webfinger do it 'correctly parses the response' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' + expect(subject.self_link_href).to eq 'https://example.com/alice' end end @@ -34,7 +32,45 @@ RSpec.describe Webfinger do it 'raises an error' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - expect { described_class.new('acct:alice@example.com').perform }.to raise_error(Webfinger::Error) + expect { subject } + .to raise_error(Webfinger::Error) + end + end + + context 'when webfinger fails and host meta is used' do + before { stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(status: 404) } + + context 'when host meta succeeds' do + let(:host_meta) do + <<~XML + + + + + XML + end + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice-from-NS', type: 'application/activity+json' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(body: host_meta, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + it 'uses host meta details' do + expect(subject.self_link_href) + .to eq 'https://example.com/alice-from-NS' + end + end + + context 'when host meta fails' do + before do + stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 500) + end + + it 'raises error' do + expect { subject } + .to raise_error(Webfinger::Error) + end end end end From 2acc942bb4fbed87620be3cdc9e93fc3abc77a63 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:39:11 -0400 Subject: [PATCH 12/53] Fix `Style/GuardClause` in `WebfingerResource` (#35531) --- app/lib/webfinger_resource.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/lib/webfinger_resource.rb b/app/lib/webfinger_resource.rb index e2027e164d..95de496a6d 100644 --- a/app/lib/webfinger_resource.rb +++ b/app/lib/webfinger_resource.rb @@ -54,11 +54,9 @@ class WebfingerResource end def username_from_acct - if domain_matches_local? - local_username - else - raise ActiveRecord::RecordNotFound - end + raise ActiveRecord::RecordNotFound unless domain_matches_local? + + local_username end def split_acct From e183d7dd9af4ef8ea449f1394bf63434fc3a7de0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:40:20 -0400 Subject: [PATCH 13/53] Fix `Style/GuardClause` in app/helpers (#35526) --- app/helpers/formatting_helper.rb | 20 ++++++++++---------- app/helpers/theme_helper.rb | 26 +++++++++++++------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 1b364a000c..c27edbb073 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -65,12 +65,12 @@ module FormattingHelper end def rss_content_preroll(status) - if status.spoiler_text? - safe_join [ - tag.p { spoiler_with_warning(status) }, - tag.hr, - ] - end + return unless status.spoiler_text? + + safe_join [ + tag.p { spoiler_with_warning(status) }, + tag.hr, + ] end def spoiler_with_warning(status) @@ -81,10 +81,10 @@ module FormattingHelper end def rss_content_postroll(status) - if status.preloadable_poll - tag.p do - poll_option_tags(status) - end + return unless status.preloadable_poll + + tag.p do + poll_option_tags(status) end end diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 0f24063385..00b4a6d2b3 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -24,24 +24,24 @@ module ThemeHelper end def custom_stylesheet - if active_custom_stylesheet.present? - stylesheet_link_tag( - custom_css_path(active_custom_stylesheet), - host: root_url, - media: :all, - skip_pipeline: true - ) - end + return if active_custom_stylesheet.blank? + + stylesheet_link_tag( + custom_css_path(active_custom_stylesheet), + host: root_url, + media: :all, + skip_pipeline: true + ) end private def active_custom_stylesheet - if cached_custom_css_digest.present? - [:custom, cached_custom_css_digest.to_s.first(8)] - .compact_blank - .join('-') - end + return if cached_custom_css_digest.blank? + + [:custom, cached_custom_css_digest.to_s.first(8)] + .compact_blank + .join('-') end def cached_custom_css_digest From 63daf6b317e7b491fe631b42fcea497baacb5dea Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:40:37 -0400 Subject: [PATCH 14/53] Fix `Style/GuardClause` in `PreviewCard` (#35525) --- app/models/preview_card.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 56fe483635..8e0e13cdb9 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -170,10 +170,9 @@ class PreviewCard < ApplicationRecord private def serialized_authors - if author_name? || author_url? || author_account_id? - PreviewCard::Author - .new(self) - end + return unless author_name? || author_url? || author_account_id? + + PreviewCard::Author.new(self) end def extract_dimensions From 456c3bda0bdcd9d3250b5ccc4c3abe8cdfcdc182 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 08:41:31 +0000 Subject: [PATCH 15/53] chore(deps): update dependency omniauth-cas to v3.0.2 (#35558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d0472d538c..dab65477bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -468,7 +468,7 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.1) + omniauth-cas (3.0.2) addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) From 86ef4d48840e439af9a0ae3ebf0e7b9b8868f75d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:50:19 -0400 Subject: [PATCH 16/53] Add `skip_*` methods to check move worker process (#35538) --- app/workers/move_worker.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 9b9c6879e5..667efd6916 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -104,7 +104,7 @@ class MoveWorker def carry_blocks_over! @source_account.blocked_by_relationships.where(account: Account.local).find_each do |block| - unless block.account.blocking?(@target_account) || block.account.following?(@target_account) + unless skip_block_move?(block) BlockService.new.call(block.account, @target_account) add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text') end @@ -115,7 +115,7 @@ class MoveWorker def carry_mutes_over! @source_account.muted_by_relationships.where(account: Account.local).find_each do |mute| - MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account) + MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless skip_mute_move?(mute) add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') rescue => e @deferred_error = e @@ -130,4 +130,12 @@ class MoveWorker AccountNote.create!(account: account, target_account: @target_account, comment: text) end end + + def skip_mute_move?(mute) + mute.account.muting?(@target_account) || mute.account.following?(@target_account) + end + + def skip_block_move?(block) + block.account.blocking?(@target_account) || block.account.following?(@target_account) + end end From 916cc1365eb5cdd9bd021e26eb13937c02d7cdb8 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:52:59 -0400 Subject: [PATCH 17/53] Fix `Style/GuardClause` in `User#wrap_email_confirmation` (#35524) --- app/models/user.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 762522f282..781a348216 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -466,16 +466,17 @@ class User < ApplicationRecord yield - if new_user - # Avoid extremely unlikely race condition when approving and confirming - # the user at the same time - reload unless approved? + after_confirmation_tasks if new_user + end - if approved? - prepare_new_user! - else - notify_staff_about_pending_account! - end + def after_confirmation_tasks + # Handle condition when approving and confirming a user at the same time + reload unless approved? + + if approved? + prepare_new_user! + else + notify_staff_about_pending_account! end end From eb73ae2f86d5dfe8dd328ecac77e93a857b5eeef Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:53:52 -0400 Subject: [PATCH 18/53] Fix `Style/GuardClause` in `User#regenerate_feed!` (#35523) --- app/models/user.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 781a348216..2ba8c2926d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -540,10 +540,10 @@ class User < ApplicationRecord def regenerate_feed! home_feed = HomeFeed.new(account) - unless home_feed.regenerating? - home_feed.regeneration_in_progress! - RegenerationWorker.perform_async(account_id) - end + return if home_feed.regenerating? + + home_feed.regeneration_in_progress! + RegenerationWorker.perform_async(account_id) end def needs_feed_update? From 040a638ab953f919c3ea9836e541054d59ce5b3d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 04:54:29 -0400 Subject: [PATCH 19/53] Fix `Style/GuardClause` in `Tag` (#35522) --- app/models/tag.rb | 9 +++++---- spec/models/tag_spec.rb | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 8e21ddca82..dff1011112 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -164,9 +164,10 @@ class Tag < ApplicationRecord end def validate_display_name_change - unless HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? - errors.add(:display_name, - I18n.t('tags.does_not_match_previous_name')) - end + errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless display_name_matches_name? + end + + def display_name_matches_name? + HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 0831ac34b8..18378c000d 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Tag do subject { Fabricate :tag, name: 'original' } it { is_expected.to_not allow_value('changed').for(:name).with_message(previous_name_error_message) } + it { is_expected.to allow_value('ORIGINAL').for(:name) } end end @@ -31,6 +32,7 @@ RSpec.describe Tag do subject { Fabricate :tag, name: 'original', display_name: 'OriginalDisplayName' } it { is_expected.to_not allow_value('ChangedDisplayName').for(:display_name).with_message(previous_name_error_message) } + it { is_expected.to allow_value('ORIGINAL').for(:display_name) } end end From 513b6289d6293537edaff889cf88e8baaf6c2ca7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:34:14 +0200 Subject: [PATCH 20/53] chore(deps): update dependency strong_migrations to v2.5.0 (#35560) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index dab65477bf..8c537e52b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -860,7 +860,7 @@ GEM stoplight (4.1.1) redlock (~> 1.0) stringio (3.1.7) - strong_migrations (2.4.0) + strong_migrations (2.5.0) activerecord (>= 7.1) swd (2.0.3) activesupport (>= 3) From b5cebf45ea122c7516ffc1e4b2d7214aa1cd376f Mon Sep 17 00:00:00 2001 From: Colin Dean Date: Mon, 28 Jul 2025 07:33:11 -0400 Subject: [PATCH 21/53] Swap order of translation restoration and service credit on post card (#33619) --- app/javascript/mastodon/components/status_content.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index e1fd7734e9..5f0f7079ae 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -48,13 +48,13 @@ class TranslateButton extends PureComponent { return (
-
- -
- + +
+ +
); } From 8d6f033326e3db91d879dd950f589b10313394b7 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 08:55:05 -0400 Subject: [PATCH 22/53] Fix `Style/GuardClause` in move worker (#35520) --- app/workers/move_worker.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 667efd6916..58d20ba94b 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -123,12 +123,12 @@ class MoveWorker end def add_account_note_if_needed!(account, id) - unless AccountNote.exists?(account: account, target_account: @target_account) - text = I18n.with_locale(account.user_locale.presence || I18n.default_locale) do - I18n.t(id, acct: @source_account.acct) - end - AccountNote.create!(account: account, target_account: @target_account, comment: text) + return if AccountNote.exists?(account: account, target_account: @target_account) + + text = I18n.with_locale(account.user_locale.presence || I18n.default_locale) do + I18n.t(id, acct: @source_account.acct) end + AccountNote.create!(account: account, target_account: @target_account, comment: text) end def skip_mute_move?(mute) From f1b9868980b1b86da7034b988b9e62536bdf182f Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 28 Jul 2025 15:25:16 +0200 Subject: [PATCH 23/53] Bypass registration checks for seeded admin user (#35565) --- db/seeds/04_admin.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/db/seeds/04_admin.rb b/db/seeds/04_admin.rb index 887b4a2213..43290c47a4 100644 --- a/db/seeds/04_admin.rb +++ b/db/seeds/04_admin.rb @@ -7,7 +7,17 @@ if Rails.env.development? admin = Account.where(username: 'admin').first_or_initialize(username: 'admin') admin.save(validate: false) - user = User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, role: UserRole.find_by(name: 'Owner'), account: admin, agreement: true, approved: true) + user = User.where(email: "admin@#{domain}").first_or_initialize( + email: "admin@#{domain}", + password: 'mastodonadmin', + password_confirmation: 'mastodonadmin', + confirmed_at: Time.now.utc, + role: UserRole.find_by(name: 'Owner'), + account: admin, + agreement: true, + approved: true, + bypass_registration_checks: true + ) user.save! user.approve! end From c587c44975b0a4a2cdf1f8589d14a4228bfb27f0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 28 Jul 2025 11:35:37 -0400 Subject: [PATCH 24/53] Fix `Lint/NonLocalExitFromIterator` cop in JSON-LD helper (#34948) --- .rubocop_todo.yml | 4 ---- app/helpers/json_ld_helper.rb | 2 +- spec/helpers/json_ld_helper_spec.rb | 8 ++++++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9e69426fcf..8706ca0ddd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,10 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -Lint/NonLocalExitFromIterator: - Exclude: - - 'app/helpers/json_ld_helper.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 82 diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb index 078aba456a..675d8b8730 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -134,7 +134,7 @@ module JsonLdHelper patch_for_forwarding!(value, compacted_value) elsif value.is_a?(Array) compacted_value = [compacted_value] unless compacted_value.is_a?(Array) - return if value.size != compacted_value.size + return nil if value.size != compacted_value.size compacted[key] = value.zip(compacted_value).map do |v, vc| if v.is_a?(Hash) && vc.is_a?(Hash) diff --git a/spec/helpers/json_ld_helper_spec.rb b/spec/helpers/json_ld_helper_spec.rb index d76c5167a7..f216588d97 100644 --- a/spec/helpers/json_ld_helper_spec.rb +++ b/spec/helpers/json_ld_helper_spec.rb @@ -180,6 +180,14 @@ RSpec.describe JsonLdHelper do expect(compacted.dig('object', 'tag', 0, 'href')).to eq ['foo'] expect(safe_for_forwarding?(json, compacted)).to be true end + + context 'when array size mismatch exists' do + subject { helper.patch_for_forwarding!(json, alternate) } + + let(:alternate) { json.merge('to' => %w(one two three)) } + + it { is_expected.to be_nil } + end end describe 'safe_for_forwarding?' do From bedbab74b98e5d2872b6b02461aac3dd54504ec5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 29 Jul 2025 04:22:04 -0400 Subject: [PATCH 25/53] Use bundler version 2.7.1 (#35567) --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8c537e52b9..b8813b7211 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,7 +96,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1131.0) + aws-partitions (1.1135.0) aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -233,7 +233,7 @@ GEM fabrication (3.0.0) faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.2) + faraday (2.13.4) faraday-net_http (>= 2.0, < 3.5) json logger @@ -345,7 +345,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.13.0) + json (2.13.2) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -438,7 +438,7 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0715) + mime-types-data (3.2025.0722) mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -601,13 +601,13 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.8.0) + parser (3.3.9.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.9) + pg (1.6.0) pghero (3.7.0) activerecord (>= 7.1) playwright-ruby-client (1.54.0) @@ -731,7 +731,7 @@ GEM railties (>= 5.2) rexml (3.4.1) rotp (6.3.0) - rouge (4.5.2) + rouge (4.6.0) rpam2 (4.0.2) rqrcode (3.1.0) chunky_png (~> 1.0) @@ -868,7 +868,7 @@ GEM faraday (~> 2.0) faraday-follow_redirects sysexits (1.2.0) - temple (0.10.3) + temple (0.10.4) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) @@ -1108,4 +1108,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.7.0 + 2.7.1 From ea976a5ffb3dd936b5d20c783073c10194bc704d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 29 Jul 2025 04:23:19 -0400 Subject: [PATCH 26/53] Fix unnecessary account note addition for already-muted moved-to users (#35566) --- app/workers/move_worker.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 58d20ba94b..1a5745a86a 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -115,8 +115,10 @@ class MoveWorker def carry_mutes_over! @source_account.muted_by_relationships.where(account: Account.local).find_each do |mute| - MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless skip_mute_move?(mute) - add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') + unless skip_mute_move?(mute) + MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) + add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') + end rescue => e @deferred_error = e end From d299b0d5765f23f33078053aff8ac8f484cb85b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:59:00 +0200 Subject: [PATCH 27/53] New Crowdin Translations (automated) (#35574) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/fa.json | 8 +++-- app/javascript/mastodon/locales/pt-BR.json | 7 ++++ app/javascript/mastodon/locales/sv.json | 2 ++ config/locales/devise.ru.yml | 4 +-- config/locales/fa.yml | 9 +++--- config/locales/ga.yml | 1 + config/locales/ru.yml | 37 +++++++++++++--------- config/locales/simple_form.fa.yml | 7 ++-- config/locales/sv.yml | 1 + 9 files changed, 50 insertions(+), 26 deletions(-) diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index c35687e031..0217d7bb2f 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -235,7 +235,7 @@ "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟", "confirmations.logout.title": "خروج؟", "confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید", - "confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.", + "confirmations.missing_alt_text.message": "فرسته‌تان رسانه‌هایی بدون متن جایگزین دارد. افزودن شرح به دسترس‌پذیر شدن محتوایتان برای افراد بیش‌تری کمک می‌کند.", "confirmations.missing_alt_text.secondary": "به هر حال پست کن", "confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟", "confirmations.mute.confirm": "خموش", @@ -424,7 +424,7 @@ "hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}", "hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}", "hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}", - "home.column_settings.show_quotes": "نمایش نقل‌قول‌ها", + "home.column_settings.show_quotes": "نمایش نقل‌ها", "home.column_settings.show_reblogs": "نمایش تقویت‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها", "home.hide_announcements": "نهفتن اعلامیه‌ها", @@ -845,6 +845,8 @@ "status.bookmark": "نشانک", "status.cancel_reblog_private": "ناتقویت", "status.cannot_reblog": "این فرسته قابل تقویت نیست", + "status.context.load_new_replies": "پاسخ‌های جدیدی موجودند", + "status.context.loading": "بررسی کردن برای پاسخ‌های بیش‌تر", "status.continued_thread": "رشتهٔ دنباله دار", "status.copy": "رونوشت از پیوند فرسته", "status.delete": "حذف", @@ -873,7 +875,7 @@ "status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان", "status.quote_error.not_found": "این فرسته قابل نمایش نیست.", "status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.", - "status.quote_error.rejected": "از آن‌جا که نگارندهٔ اصلی فرسته اجازهٔ نقل قولش را نمی‌دهد این فرسته قابل نمایش نیست.", + "status.quote_error.rejected": "از آن‌جا که نگارندهٔ اصلی این فرسته اجازهٔ نقلش را نمی‌دهد قابل نمایش نیست.", "status.quote_error.removed": "این فرسته به دست نگارنده‌اش برداشته شده.", "status.quote_error.unauthorized": "از آن‌جا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.", "status.quote_post_author": "فرسته توسط {name}", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index f576270193..d00f44495d 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -224,6 +224,8 @@ "confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.", "confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?", "confirmations.discard_draft.post.cancel": "Continuar rascunho", + "confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.", + "confirmations.discard_draft.post.title": "Eliminar seu esboço de publicação?", "confirmations.discard_edit_media.confirm": "Descartar", "confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?", "confirmations.follow_to_list.confirm": "Seguir e adicionar à lista", @@ -333,9 +335,13 @@ "errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência", "errors.unexpected_crash.report_issue": "Reportar problema", "explore.suggested_follows": "Pessoas", + "explore.title": "Em alta", "explore.trending_links": "Notícias", "explore.trending_statuses": "Publicações", "explore.trending_tags": "Hashtags", + "featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}", + "featured_carousel.next": "Próximo", + "featured_carousel.previous": "Anterior", "filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.", "filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!", "filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.", @@ -550,6 +556,7 @@ "navigation_bar.lists": "Listas", "navigation_bar.logout": "Sair", "navigation_bar.moderation": "Moderação", + "navigation_bar.more": "Mais", "navigation_bar.mutes": "Usuários silenciados", "navigation_bar.opened_in_classic_interface": "Publicações, contas e outras páginas específicas são abertas por padrão na interface 'web' clássica.", "navigation_bar.preferences": "Preferências", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 48f15b28b8..1697b0dcb0 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -845,6 +845,8 @@ "status.bookmark": "Bokmärk", "status.cancel_reblog_private": "Sluta boosta", "status.cannot_reblog": "Detta inlägg kan inte boostas", + "status.context.load_new_replies": "Nya svar finns", + "status.context.loading": "Letar efter fler svar", "status.continued_thread": "Fortsatt tråd", "status.copy": "Kopiera inläggslänk", "status.delete": "Radera", diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml index 79a988f189..cc95f73061 100644 --- a/config/locales/devise.ru.yml +++ b/config/locales/devise.ru.yml @@ -53,7 +53,7 @@ ru: subtitle: Двухфакторная аутентификация отключена для вашей учетной записи. title: 2FA отключена two_factor_enabled: - explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP. + explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением-аутентификатором. subject: 'Mastodon: Двухфакторная аутентификация включена' subtitle: Двухфакторная аутентификация включена для вашей учётной записи. title: 2FA включена @@ -75,7 +75,7 @@ ru: title: Один из ваших электронных ключей удалён webauthn_disabled: explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи. - extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP. + extra: Теперь вход возможен с использованием только с помощью одноразового кода, сгенерированного сопряжённым приложением-аутентификатором. subject: 'Mastodon: Аутентификация по электронным ключам деактивирована' title: Вход по электронным ключам деактивирован webauthn_enabled: diff --git a/config/locales/fa.yml b/config/locales/fa.yml index ec0ed1ca99..b9d87d600f 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -653,8 +653,8 @@ fa: mark_as_sensitive_description_html: رسانهٔ درون فرستهٔ گزارش شده به عنوان حسّاس علامت خورده و شکایتی ضبط خواهد شد تا بتوانید خلاف‌های آینده از همین حساب را بهتر مدیریت کنید. other_description_html: دیدن انتخاب های بیشتر برای کنترل رفتار حساب و سفارشی سازی ارتباط با حساب گزارش شده. resolve_description_html: هیچ کنشی علیه حساب گزارش شده انجام نخواهد شد. هیچ شکایتی ضبط نشده و گزارش بسته خواهد شد. - silence_description_html: این حساب فقط برای کسانی قابل مشاهده خواهد بود که قبلاً آن را دنبال می کنند یا به صورت دستی آن را جستجو می کنند و دسترسی آن را به شدت محدود می کند. همیشه می توان برگرداند. همه گزارش‌های مربوط به این حساب را می‌بندد. - suspend_description_html: اکانت و تمامی محتویات آن غیرقابل دسترسی و در نهایت حذف خواهد شد و تعامل با آن غیر ممکن خواهد بود. قابل برگشت در عرض 30 روز همه گزارش‌های مربوط به این حساب را می‌بندد. + silence_description_html: حساب فقط برای کسانی که از پیش پی می‌گرفتندش یا به صورت دستی به دنیالش گشته‌اند نمایان خواهد بود که رسشش را شدیداً محدود می‌کند. همواره برگشت‌پذیر است. همهٔ گزارش‌ها علیه این حساب را خواهد بست. + suspend_description_html: حساب و همهٔ محتوایش غیرقابل دسترس شده و در نهایت حذف خواهند شد. تعامل با آن ممکن نخواهد بود. بازگشت‌پذیر تا ۳۰ روز. همهٔ گزارش‌ها علیه این حساب را خواهد بست. actions_description_html: تصمیم گیری کنش اقدامی برای حل این گزارش. در صورت انجام کنش تنبیهی روی حساب گزارش شده، غیر از زمان یکه دستهٔ هرزنامه گزیده باشد، برایش آگاهی رایانامه‌ای فرستاده خواهد شد. actions_description_remote_html: تصمیم بگیرید که چه اقدامی برای حل این گزارش انجام دهید. این فقط بر نحوه ارتباط سرور شما با این حساب راه دور و مدیریت محتوای آن تأثیر می گذارد. actions_no_posts: این گزارش هیچ پست مرتبطی برای حذف ندارد @@ -714,7 +714,7 @@ fa: actions: delete_html: پست های توهین آمیز را حذف کنید mark_as_sensitive_html: رسانه پست های توهین آمیز را به عنوان حساس علامت گذاری کنید - silence_html: دسترسی @%{acct} را به شدت محدود کنید و نمایه و محتویات آنها را فقط برای افرادی که قبلاً آنها را دنبال می‌کنند قابل مشاهده کنید یا به صورت دستی نمایه آن را جستجو کنید + silence_html: محدودیت شدید رسش ‪@%{acct}‬ با نمایان کردن نماگر و محتوایش فقط به افرادی که از پیش پی می‌گرفتندش و به صورت دستی به دنبالش گشته‌اند suspend_html: تعلیق @%{acct}، غیرقابل دسترس کردن نمایه و محتوای آنها و تعامل با آنها غیر ممکن close_report: 'علامت گذاری گزارش #%{id} به عنوان حل شده است' close_reports_html: "همه گزارش‌ها در برابر @%{acct} را به‌عنوان حل‌وفصل علامت‌گذاری کنید" @@ -1872,6 +1872,7 @@ fa: edited_at_html: ویراسته در %{date} errors: in_reply_not_found: به نظر نمی‌رسد وضعیتی که می‌خواهید به آن پاسخ دهید، وجود داشته باشد. + quoted_status_not_found: به نظر نمی‌رسد فرسته‌ای که می‌خواهید نقلش کنید وجود داشته باشد. over_character_limit: از حد مجاز %{max} حرف فراتر رفتید pin_errors: direct: فرسته‌هایی که فقط برای کاربران اشاره شده نمایانند نمی‌توانند سنجاق شوند @@ -2002,7 +2003,7 @@ fa: details: 'جزییات ورود:' explanation: ما ورود به حساب شما را از یک آدرس آی پی جدید شناسایی کرده ایم. further_actions_html: اگر این شما نبودید، توصیه می کنیم فورا %{action} را فعال کنید و برای ایمن نگه داشتن حساب خود، احراز هویت دو مرحله ای را فعال کنید. - subject: حساب شما از یک آدرس آی پی جدید قابل دسترسی است + subject: نشانی آی‌پی جدیدی به حسابتان دسترسی پیدا کرده title: یک ورود جدید terms_of_service_changed: agreement: با ادامه استفاده از %{domain}، با این شرایط موافقت می کنید. اگر با شرایط به‌روزرسانی شده مخالف هستید، می‌توانید در هر زمان با حذف حساب خود، قرارداد خود را با %{domain} فسخ کنید. diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 111ae9c56f..8fff68b3d9 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -2001,6 +2001,7 @@ ga: edited_at_html: "%{date} curtha in eagar" errors: in_reply_not_found: Is cosúil nach ann don phostáil a bhfuil tú ag iarraidh freagra a thabhairt air. + quoted_status_not_found: Is cosúil nach bhfuil an post atá tú ag iarraidh a lua ann. over_character_limit: teorainn carachtar %{max} sáraithe pin_errors: direct: Ní féidir postálacha nach bhfuil le feiceáil ach ag úsáideoirí luaite a phinnáil diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 4cc2e15b5a..ad4b9d5122 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1611,7 +1611,7 @@ ru: limit: Вы достигли максимального количества списков login_activities: authentication_methods: - otp: приложения двухфакторной аутентификации + otp: приложения для генерации кодов password: пароля webauthn: электронного ключа description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию. @@ -1621,11 +1621,18 @@ ru: title: История входов mail_subscriptions: unsubscribe: - action: Да, отписаться + action: Да, я хочу отписаться complete: Подписка отменена - confirmation_html: Вы точно желаете отписаться от всех уведомления типа «%{type}», доставляемых из сервера Mastodon %{domain} на ваш адрес электронной почты %{email}? Вы всегда сможете подписаться снова в настройках e-mail уведомлений. - resubscribe_html: Если вы отписались от рассылки по ошибке, вы можете повторно подписаться на рассылку в настройках настроек почтовых уведомлений. - success_html: Вы больше не будете получать %{type} для Mastodon на %{domain} на вашу электронную почту %{email}. + confirmation_html: Вы уверены в том, что хотите отписаться от всех %{type}, которые вы получаете на адрес %{email} для учётной записи на сервере Mastodon %{domain}? Вы всегда сможете подписаться снова в настройках уведомлений по электронной почте. + emails: + notification_emails: + favourite: уведомлений о добавлении ваших постов в избранное + follow: уведомлений о новых подписчиках + follow_request: уведомлений о новых запросах на подписку + mention: уведомлений о новых упоминаниях + reblog: уведомлений о продвижении ваших постов + resubscribe_html: Если вы отписались по ошибке и хотите подписаться снова, перейдите на страницу настройки уведомлений по электронной почте. + success_html: Вы отказались от %{type}, которые вы получали на адрес %{email} для вашей учётной записи на сервере Mastodon %{domain}. title: Отписаться media_attachments: validations: @@ -1721,13 +1728,13 @@ ru: trillion: трлн unit: '' otp_authentication: - code_hint: Для подтверждения введите код, сгенерированный приложением-аутентификатором - description_html: Подключив двуфакторную авторизацию, для входа в свою учётную запись вам будет необходим смартфон и приложение-аутентификатор на нём, которое будет генерировать специальные временные коды. Без этих кодов войти в учётную запись не получиться, даже если все данные верны, что существенно увеличивает безопасность вашей учётной записи. + code_hint: Для подтверждения введите код из приложения-аутентификатора + description_html: Подключите двухфакторную аутентификацию с использованием специального приложения-аутентификатора, и тогда для входа в вашу учётную запись необходимо будет иметь при себе смартфон, который будет генерировать одноразовые коды. enable: Включить - instructions_html: "Отсканируйте этот QR-код с помощью приложения-аутентификатора, такого как Google Authenticator, Яндекс.Ключ или andOTP. После сканирования и добавления, приложение начнёт генерировать коды, которые потребуется вводить для завершения входа в учётную запись." - manual_instructions: 'Если отсканировать QR-код не получается или не представляется возможным, вы можете ввести ключ настройки вручную:' - setup: Настроить - wrong_code: Введенный код недействителен! Время сервера и время устройства правильно? + instructions_html: "Откройте Google Authenticator или другое приложение-аутентификатор на вашем смартфоне и отсканируйте этот QR-код. В дальнейшем это приложение будет генерировать одноразовые коды, которые потребуется вводить для подтверждения входа в вашу учётную запись." + manual_instructions: 'Если отсканировать QR-код не получается, введите секретный ключ вручную:' + setup: Подключить + wrong_code: Одноразовый код, который вы ввели, не подходит! Совпадает ли время на устройстве с временем на сервере? pagination: newer: Позже next: Вперёд @@ -1751,14 +1758,14 @@ ru: posting_defaults: Предустановки для новых постов public_timelines: Публичные ленты privacy: - hint_html: "Настройте, как вы хотите, чтобы ваш профиль и ваши сообщения были найдены. Различные функции в Mastodon могут помочь вам охватить более широкую аудиторию, если они включены. Уделите время изучению этих настроек, чтобы убедиться, что они подходят для вашего случая использования." + hint_html: "Здесь вы можете определить то, как другие смогут обнаружить ваши посты и ваш профиль. Множество разных функций в Mastodon существуют для того, чтобы помочь вам выйти на более широкую аудиторию, если вы того захотите. Ознакомьтесь с этими настройками и в случае необходимости измените их согласно вашим желаниям." privacy: Приватность - privacy_hint_html: Определите, какую информацию вы хотите раскрыть в интересах других. Люди находят интересные профили и приложения, просматривая список подписчиков других людей и узнавая, из каких приложений они публикуют свои сообщения, но вы можете предпочесть скрыть это. + privacy_hint_html: Решите, сколько данных о себе вы готовы раскрыть ради того, чтобы они пошли на пользу другим. Просматривая ваши подписки, кто-то может обнаружить профили интересных людей, а ещё кто-нибудь может найти своё любимое приложение, увидев его название рядом с вашими постами. Тем не менее вы можете предпочесть не раскрывать эту информацию. reach: Видимость - reach_hint_html: Укажите, хотите ли вы, чтобы новые люди обнаруживали вас и могли следить за вами. Хотите ли вы, чтобы ваши сообщения появлялись на экране Обзора? Хотите ли вы, чтобы другие люди видели вас в своих рекомендациях? Хотите ли вы автоматически принимать всех новых подписчиков или иметь возможность детально контролировать каждого из них? + reach_hint_html: Решите, нужна ли вам новая аудитория и новые подписчики. Настройте по своему желанию, показывать ли ваши посты в разделе «Актуальное», рекомендовать ли ваш профиль другим людям, принимать ли всех новых подписчиков автоматически или рассматривать каждый запрос на подписку в отдельности. search: Поиск search_hint_html: Определите, как вас могут найти. Хотите ли вы, чтобы люди находили вас по тому, о чём вы публично писали? Хотите ли вы, чтобы люди за пределами Mastodon находили ваш профиль при поиске в Интернете? Следует помнить, что полное исключение из всех поисковых систем не может быть гарантировано для публичной информации. - title: Приватность и доступ + title: Приватность и видимость privacy_policy: title: Политика конфиденциальности reactions: diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml index f80097832c..bc7c4703da 100644 --- a/config/locales/simple_form.fa.yml +++ b/config/locales/simple_form.fa.yml @@ -56,7 +56,7 @@ fa: scopes: واسط‌های برنامه‌نویسی که این برنامه به آن دسترسی دارد. اگر بالاترین سطح دسترسی را انتخاب کنید، دیگر نیازی به انتخاب سطح‌های پایینی ندارید. setting_aggregate_reblogs: برای تقویت‌هایی که به تازگی برایتان نمایش داده شده‌اند، تقویت‌های بیشتر را نمایش نده (فقط روی تقویت‌های اخیر تأثیر می‌گذارد) setting_always_send_emails: در حالت عادی آگاهی‌های رایانامه‌ای هنگامی که فعّالانه از ماستودون استفاده می‌کنید فرستاده نمی‌شوند - setting_default_quote_policy: کاربران اشاره شده همواره مجاز به نقل قولند. این تنظیمات تنها روی فرسته‌های ایجاد شده با نگارش بعدی ماستودون موثّر است، ولی می‌توانید ترجیحاتتان را پیشاپیش بگزینید + setting_default_quote_policy: کاربران اشاره شده همواره مجاز به نقلند. این تنظیمات تنها روی فرسته‌های ایجاد شده با نگارش بعدی ماستودون موثّر است، ولی می‌توانید ترجیحاتتان را پیشاپیش بگزینید setting_default_sensitive: تصاویر حساس به طور پیش‌فرض پنهان هستند و می‌توانند با یک کلیک آشکار شوند setting_display_media_default: تصویرهایی را که به عنوان حساس علامت زده شده‌اند پنهان کن setting_display_media_hide_all: همیشه همهٔ عکس‌ها و ویدیوها را پنهان کن @@ -150,6 +150,9 @@ fa: min_age: نباید کم‌تر از کمینهٔ زمان لازم از سوی قوانین حقوقیتان باشد. user: chosen_languages: اگر انتخاب کنید، تنها نوشته‌هایی که به زبان‌های برگزیدهٔ شما نوشته شده‌اند در فهرست نوشته‌های عمومی نشان داده می‌شوند + date_of_birth: + one: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد. + other: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد. role: نقش کنترل می کند که کاربر چه مجوزهایی دارد. user_role: color: رنگی که برای نقش در سرتاسر UI استفاده می شود، به عنوان RGB در قالب هگز @@ -230,7 +233,7 @@ fa: setting_boost_modal: نمایش پیغام تأیید پیش از تقویت کردن setting_default_language: زبان نوشته‌های شما setting_default_privacy: حریم خصوصی نوشته‌ها - setting_default_quote_policy: افراد مجاز به نقل قول + setting_default_quote_policy: افراد مجاز به نقل setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته setting_disable_hover_cards: از کار انداختن پیش‌نمایش نمایه هنگام رفتن رویش diff --git a/config/locales/sv.yml b/config/locales/sv.yml index ca23e35054..d406eabba3 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1872,6 +1872,7 @@ sv: edited_at_html: 'Ändrad: %{date}' errors: in_reply_not_found: Inlägget du försöker svara på verkar inte existera. + quoted_status_not_found: Inlägget du försöker svara på verkar inte existera. over_character_limit: teckengräns på %{max} har överskridits pin_errors: direct: Inlägg som endast är synliga för nämnda användare kan inte fästas From 3eca8cce1ce501aebcbf140e8c7fe90316d75ff6 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 29 Jul 2025 10:59:16 +0200 Subject: [PATCH 28/53] Add second set of blocked text that applies to accounts regardless of account age (#35563) --- app/lib/antispam.rb | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/lib/antispam.rb b/app/lib/antispam.rb index 4ebf192485..69c862a5c1 100644 --- a/app/lib/antispam.rb +++ b/app/lib/antispam.rb @@ -33,9 +33,7 @@ class Antispam end def local_preflight_check!(status) - return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } - return unless suspicious_reply_or_mention?(status) - return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago + return unless considered_spam?(status) report_if_needed!(status.account) @@ -44,10 +42,26 @@ class Antispam private + def considered_spam?(status) + (all_time_suspicious?(status) || recent_suspicious?(status)) && suspicious_reply_or_mention?(status) + end + + def all_time_suspicious?(status) + all_time_spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } + end + + def recent_suspicious?(status) + status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } + end + def spammy_texts redis.smembers('antispam:spammy_texts') end + def all_time_spammy_texts + redis.smembers('antispam:all_time_spammy_texts') + end + def suspicious_reply_or_mention?(status) parent = status.thread return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id) From d121007927b59831937c0e8352fc394019f2fef9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 29 Jul 2025 11:00:27 +0200 Subject: [PATCH 29/53] Change "new replies available" notice to be above replies in web UI (#35575) --- .../status/components/refresh_controller.tsx | 16 +++------------- .../mastodon/features/status/index.jsx | 3 +-- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/refresh_controller.tsx b/app/javascript/mastodon/features/status/components/refresh_controller.tsx index 04046302b6..2765d5d9f4 100644 --- a/app/javascript/mastodon/features/status/components/refresh_controller.tsx +++ b/app/javascript/mastodon/features/status/components/refresh_controller.tsx @@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; - import { fetchContext, completeContextRefresh, @@ -22,8 +20,7 @@ const messages = defineMessages({ export const RefreshController: React.FC<{ statusId: string; - withBorder?: boolean; -}> = ({ statusId, withBorder }) => { +}> = ({ statusId }) => { const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); @@ -78,12 +75,7 @@ export const RefreshController: React.FC<{ if (ready && !loading) { return ( - + + + {({ props }) => ( +
+
{children}
+ +
+ +
+
+ )} +
+ + ); +}; diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 5f0f7079ae..38d24921c5 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -138,6 +138,16 @@ class StatusContent extends PureComponent { onCollapsedToggle(collapsed); } + + // Remove quote fallback link from the DOM so it doesn't + // mess with paragraph margins + if (!!status.get('quote')) { + const inlineQuote = node.querySelector('.quote-inline'); + + if (inlineQuote) { + inlineQuote.remove(); + } + } } handleMouseEnter = ({ currentTarget }) => { diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index d3d8b58c33..8d43ea1819 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link } from 'react-router-dom'; import type { Map as ImmutableMap } from 'immutable'; -import ArticleIcon from '@/material-icons/400-24px/article.svg?react'; -import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; -import { Icon } from 'mastodon/components/icon'; +import { LearnMoreLink } from 'mastodon/components/learn_more_link'; import StatusContainer from 'mastodon/containers/status_container'; import type { Status } from 'mastodon/models/status'; import type { RootState } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import QuoteIcon from '../../images/quote.svg?react'; import { fetchStatus } from '../actions/statuses'; import { makeGetStatus } from '../selectors'; @@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{ 'status__quote--error': isError, })} > - {children} ); @@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{ accountId ? state.accounts.get(accountId) : undefined, ); - const quoteAuthorName = account?.display_name_html; + const quoteAuthorName = account?.acct; if (!quoteAuthorName) { return null; } - const quoteAuthorElement = ( - - ); - const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; - return ( - +
- - - +
); }; @@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{ defaultMessage='Hidden due to one of your filters' /> ); - } else if (quoteState === 'deleted') { - quoteError = ( - - ); - } else if (quoteState === 'unauthorized') { - quoteError = ( - - ); } else if (quoteState === 'pending') { quoteError = ( - + <> + + + +
+ +
+

+ +

+
+ ); - } else if (quoteState === 'rejected' || quoteState === 'revoked') { + } else if ( + !status || + !quotedStatusId || + quoteState === 'deleted' || + quoteState === 'rejected' || + quoteState === 'revoked' || + quoteState === 'unauthorized' + ) { quoteError = ( - ); - } else if (!status || !quotedStatusId) { - quoteError = ( - ); } @@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{ isQuotedPost id={quotedStatusId} contextType={contextType} - avatarSize={40} + avatarSize={32} > {canRenderChildQuote && ( [data-popper-placement] { } } -.status--has-quote .quote-inline { - display: none; -} - .status { padding: 16px; min-height: 54px; @@ -1470,10 +1466,6 @@ body > [data-popper-placement] { margin-top: 16px; } - &--is-quote { - border: none; - } - &--in-thread { --thread-margin: calc(46px + 8px); @@ -1860,79 +1852,99 @@ body > [data-popper-placement] { // --status-gutter-width is currently only set inside of // .notification-ungrouped, so everywhere else this will fall back // to the pixel values - --quote-margin: var(--status-gutter-width, 36px); + --quote-margin: var(--status-gutter-width); position: relative; margin-block-start: 16px; margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px)); - border-radius: 8px; + border-radius: 12px; color: var(--nested-card-text); - background: var(--nested-card-background); - border: var(--nested-card-border); - - @container (width > 460px) { - --quote-margin: var(--status-gutter-width, 56px); - } + border: 1px solid var(--surface-border-color); } .status__quote--error { + box-sizing: border-box; display: flex; align-items: center; + justify-content: space-between; gap: 8px; padding: 12px; - font-size: 15px; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + min-height: 56px; + + .link-button { + font-size: inherit; + line-height: inherit; + letter-spacing: inherit; + } } .status__quote-author-button { position: relative; overflow: hidden; - display: inline-flex; - width: auto; - margin-block-start: 10px; - padding: 5px 12px; + display: flex; + margin-top: 8px; + padding: 8px 12px; align-items: center; - gap: 6px; font-family: inherit; font-size: 14px; - font-weight: 700; - line-height: normal; - letter-spacing: 0; - text-decoration: none; - color: $highlight-text-color; - background: var(--nested-card-background); - border: var(--nested-card-border); - border-radius: 4px; - - &:active, - &:focus, - &:hover { - border-color: lighten($highlight-text-color, 4%); - color: lighten($highlight-text-color, 4%); - } - - &:focus-visible { - outline: $ui-button-icon-focus-outline; - } + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + color: $darker-text-color; + background: var(--surface-variant-background-color); + border-radius: 8px; + cursor: default; } -.status__quote-icon { - position: absolute; - inset-block-start: 18px; - inset-inline-start: -40px; - display: block; - width: 26px; - height: 26px; - padding: 5px; - color: #6a49ba; - z-index: 10; +.status--is-quote { + border: none; + padding: 12px; - .status__quote--error & { - inset-block-start: 50%; - transform: translateY(-50%); + .status__info { + padding-bottom: 8px; } - @container (width > 460px) { - inset-inline-start: -50px; + .display-name, + .status__relative-time { + font-size: 14px; + line-height: 20px; + letter-spacing: 0.1px; + } + + .display-name__account { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + } + + .status__content { + display: -webkit-box; + font-size: 14px; + letter-spacing: 0.25px; + line-height: 20px; + -webkit-line-clamp: 4; + line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + + p { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + } + + .media-gallery, + .video-player, + .audio-player, + .attachment-list, + .poll { + margin-top: 8px; } } @@ -2152,6 +2164,27 @@ body > [data-popper-placement] { } } +.learn-more__popout { + gap: 8px; + + &__content { + display: flex; + flex-direction: column; + gap: 4px; + } + + h6 { + font-size: inherit; + font-weight: 500; + line-height: inherit; + letter-spacing: 0.1px; + } + + .link-button { + font-weight: 500; + } +} + .account__wrapper { display: flex; gap: 10px; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index 431cdd7a8e..16ed033b96 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -16,6 +16,7 @@ --surface-background-color: #{darken($ui-base-color, 4%)}; --surface-variant-background-color: #{$ui-base-color}; --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; + --surface-border-color: #{lighten($ui-base-color, 8%)}; --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)}; --avatar-border-radius: 8px; --media-outline-color: #{rgba(#fcf8ff, 0.15)}; From 2257612deb518ff74a5b12b08173a09a843cb975 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 30 Jul 2025 18:09:16 +0200 Subject: [PATCH 51/53] Fix "new replies available" reporting a false positive for re-fetched root status (#35602) --- app/workers/activitypub/fetch_all_replies_worker.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index 14142b9cd5..128bfe7e8a 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -86,9 +86,7 @@ class ActivityPub::FetchAllRepliesWorker return if root_status_body.nil? - @batch.within do - FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body }) - end + FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys.except('batch_id'), 'prefetched_body' => root_status_body }) get_replies(root_status_body, MAX_PAGES, options) end From b80e95b2aaeef7a274f92293e00ccd52892c10be Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 30 Jul 2025 18:18:58 +0200 Subject: [PATCH 52/53] Change new replies to be loaded automatically if thread previously empty (#35603) --- .../status/components/refresh_controller.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/status/components/refresh_controller.tsx b/app/javascript/mastodon/features/status/components/refresh_controller.tsx index 2765d5d9f4..9788b2849f 100644 --- a/app/javascript/mastodon/features/status/components/refresh_controller.tsx +++ b/app/javascript/mastodon/features/status/components/refresh_controller.tsx @@ -24,6 +24,11 @@ export const RefreshController: React.FC<{ const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); + const autoRefresh = useAppSelector( + (state) => + !state.contexts.replies[statusId] || + state.contexts.replies[statusId].length === 0, + ); const dispatch = useAppDispatch(); const intl = useIntl(); const [ready, setReady] = useState(false); @@ -39,6 +44,11 @@ export const RefreshController: React.FC<{ dispatch(completeContextRefresh({ statusId })); if (result.async_refresh.result_count > 0) { + if (autoRefresh) { + void dispatch(fetchContext({ statusId })); + return ''; + } + setReady(true); } } else { @@ -57,7 +67,7 @@ export const RefreshController: React.FC<{ return () => { clearTimeout(timeoutId); }; - }, [dispatch, setReady, statusId, refresh]); + }, [dispatch, setReady, statusId, refresh, autoRefresh]); const handleClick = useCallback(() => { setLoading(true); From e5826777b6c06a32b97388657beaca1e5eccb421 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 30 Jul 2025 18:28:26 +0200 Subject: [PATCH 53/53] Fix friends-of-friends recommendations suggesting already-requested accounts (#35604) --- .../account_suggestions/friends_of_friends_source.rb | 1 + .../friends_of_friends_source_spec.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/account_suggestions/friends_of_friends_source.rb b/app/models/account_suggestions/friends_of_friends_source.rb index 707c6ccaec..d4accd2cea 100644 --- a/app/models/account_suggestions/friends_of_friends_source.rb +++ b/app/models/account_suggestions/friends_of_friends_source.rb @@ -26,6 +26,7 @@ class AccountSuggestions::FriendsOfFriendsSource < AccountSuggestions::Source AND NOT EXISTS (SELECT 1 FROM mutes m WHERE m.target_account_id = follows.target_account_id AND m.account_id = :id) AND (accounts.domain IS NULL OR NOT EXISTS (SELECT 1 FROM account_domain_blocks b WHERE b.account_id = :id AND b.domain = accounts.domain)) AND NOT EXISTS (SELECT 1 FROM follows f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id) + AND NOT EXISTS (SELECT 1 FROM follow_requests f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id) AND follows.target_account_id <> :id AND accounts.discoverable AND accounts.suspended_at IS NULL diff --git a/spec/models/account_suggestions/friends_of_friends_source_spec.rb b/spec/models/account_suggestions/friends_of_friends_source_spec.rb index 9daaa233bf..af1e6e9889 100644 --- a/spec/models/account_suggestions/friends_of_friends_source_spec.rb +++ b/spec/models/account_suggestions/friends_of_friends_source_spec.rb @@ -16,10 +16,12 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do let!(:jerk) { Fabricate(:account, discoverable: true, hide_collections: false) } let!(:larry) { Fabricate(:account, discoverable: true, hide_collections: false) } let!(:morty) { Fabricate(:account, discoverable: true, hide_collections: false, memorial: true) } + let!(:joyce) { Fabricate(:account, discoverable: true, hide_collections: false) } context 'with follows and blocks' do before do bob.block!(jerk) + bob.request_follow!(joyce) FollowRecommendationMute.create!(account: bob, target_account: neil) # bob follows eugen, alice and larry @@ -28,8 +30,8 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do # alice follows eve and mallory [john, mallory].each { |account| alice.follow!(account) } - # eugen follows eve, john, jerk, larry, neil and morty - [eve, mallory, jerk, larry, neil, morty].each { |account| eugen.follow!(account) } + # eugen follows eve, john, jerk, larry, neil, morty and joyce + [eve, mallory, jerk, larry, neil, morty, joyce].each { |account| eugen.follow!(account) } end it 'returns eligible accounts', :aggregate_failures do @@ -55,6 +57,9 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do # morty is not included because his account is in memoriam expect(results).to_not include([morty.id, :friends_of_friends]) + + # joyce is not included because there is already a pending follow request + expect(results).to_not include([joyce.id, :friends_of_friends]) end end