From a968849e9c70944c160d8e15b903409095d762f2 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 5 Aug 2025 10:10:28 -0400 Subject: [PATCH 01/11] Use `normalizes` api for `UsernameBlock` username change (#35606) --- app/models/username_block.rb | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/models/username_block.rb b/app/models/username_block.rb index 18db4ca34e..33169a47f8 100644 --- a/app/models/username_block.rb +++ b/app/models/username_block.rb @@ -29,10 +29,12 @@ class UsernameBlock < ApplicationRecord validates :username, presence: true, uniqueness: true scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) } - scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) } + scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(normalize_value_for(:normalized_username, str)).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) } before_save :set_normalized_username + normalizes :normalized_username, with: ->(value) { value.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) } + def comparison exact? ? 'equals' : 'contains' end @@ -42,8 +44,7 @@ class UsernameBlock < ApplicationRecord end def self.matches?(str, allow_with_approval: false) - normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) - matches_exactly(normalized_str).or(matches_partially(normalized_str)).where(allow_with_approval: allow_with_approval).any? + matches_exactly(str).or(matches_partially(str)).where(allow_with_approval: allow_with_approval).any? end def to_log_human_identifier @@ -53,10 +54,6 @@ class UsernameBlock < ApplicationRecord private def set_normalized_username - self.normalized_username = normalize(username) - end - - def normalize(str) - str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) + self.normalized_username = username end end From a4c05c694fd88ded8b0a977d80b54ec7cd142add Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 5 Aug 2025 18:15:16 +0200 Subject: [PATCH 02/11] Fix quote notification filtering and add settings (#35690) --- .../mastodon/actions/notification_groups.ts | 4 +++- app/javascript/mastodon/actions/notifications.js | 2 +- .../notifications/components/column_settings.jsx | 11 +++++++++++ app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/reducers/settings.js | 3 +++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 4386325481..19d8750fa7 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -161,7 +161,9 @@ export const processNewNotificationForGroups = createAppAsyncThunk( if (!showInColumn) return; if ( - (notification.type === 'mention' || notification.type === 'update') && + (notification.type === 'mention' || + notification.type === 'update' || + notification.type === 'quote') && notification.status?.filtered ) { const filters = notification.status.filtered.filter((result) => diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 2499b8da1d..cbfddc750f 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { + if (['mention', 'status', 'quote'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); if (filters.some(result => result.filter.filter_action === 'hide')) { diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 9616adcb93..b1f4e59818 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -143,6 +143,17 @@ class ColumnSettings extends PureComponent { +
+

+ +
+ + {showPushSettings && } + + +
+
+

diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index c91516695b..aa18fc39ce 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Poll results:", "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.quote": "Quotes:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9e2ee5f55a..cea8949f23 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -36,6 +36,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: false, reblog: false, + quote: false, mention: false, poll: false, status: false, @@ -59,6 +60,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, @@ -72,6 +74,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, From 4c2ddbf2c4113e7bf5e89e7ac2681b2937fdeef3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 6 Aug 2025 04:42:53 -0400 Subject: [PATCH 03/11] Update rubocop to version 1.79.2 (#35688) --- .rubocop_todo.yml | 2 +- Gemfile.lock | 2 +- app/lib/emoji_formatter.rb | 4 +--- spec/models/form/import_spec.rb | 14 +++++++------- .../accounts_statuses_cleanup_scheduler_spec.rb | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8706ca0ddd..625fbf17ab 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.0. +# using RuboCop version 1.79.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new diff --git a/Gemfile.lock b/Gemfile.lock index 475172934a..b554211a3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -765,7 +765,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.79.1) + rubocop (1.79.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index a5f5103fff..c193df9bb6 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -45,9 +45,7 @@ class EmojiFormatter i += 1 if inside_shortname && text[i] == ':' - # https://github.com/rubocop/rubocop/issues/14383 - # False positive in line below, remove disable when resolved - inside_shortname = false # rubocop:disable Lint/UselessAssignment + inside_shortname = false shortcode = text[(shortname_start_index + 1)..(i - 1)] char_after = text[i + 1] diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb index dd8fd35a05..d682e13ecb 100644 --- a/spec/models/form/import_spec.rb +++ b/spec/models/form/import_spec.rb @@ -258,13 +258,13 @@ RSpec.describe Form::Import do end end - it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) - it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) + it_behaves_like('on successful import', 'following', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'following', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'blocking', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'blocking', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'muting', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', %w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) + it_behaves_like('on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', %w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [ { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil }, diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 55b66629e0..1cd01eb3b9 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -112,7 +112,7 @@ RSpec.describe Scheduler::AccountsStatusesCleanupScheduler do expect { subject.perform } .to change(Status, :count).by(-subject.compute_budget) # Cleanable statuses - .and (not_change { account_bob.statuses.count }) # No cleanup policy for account + .and not_change { account_bob.statuses.count } # No cleanup policy for account .and(not_change { account_dave.statuses.count }) # Disabled cleanup policy end From fcbd4b7afbaba4a599150fbb89c6d9b33b29e338 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:43:14 +0200 Subject: [PATCH 04/11] Update dependency annotaterb to v4.18.0 (#35676) 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 b554211a3f..943334b03d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) - annotaterb (4.17.0) + annotaterb (4.18.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) ast (2.4.3) From 6f6e7d8d49143e168a4fa257d9bca21fc7804b5f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:06:22 +0000 Subject: [PATCH 05/11] New Crowdin Translations (automated) (#35694) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/cs.json | 2 ++ app/javascript/mastodon/locales/da.json | 1 + app/javascript/mastodon/locales/de.json | 2 ++ app/javascript/mastodon/locales/el.json | 1 + app/javascript/mastodon/locales/es-AR.json | 1 + app/javascript/mastodon/locales/es-MX.json | 1 + app/javascript/mastodon/locales/es.json | 1 + app/javascript/mastodon/locales/fi.json | 2 ++ app/javascript/mastodon/locales/gl.json | 1 + app/javascript/mastodon/locales/he.json | 1 + app/javascript/mastodon/locales/is.json | 2 ++ app/javascript/mastodon/locales/it.json | 1 + app/javascript/mastodon/locales/kab.json | 8 ++++++ app/javascript/mastodon/locales/nl.json | 1 + app/javascript/mastodon/locales/pt-PT.json | 9 ++++++ app/javascript/mastodon/locales/uk.json | 1 + app/javascript/mastodon/locales/vi.json | 1 + app/javascript/mastodon/locales/zh-TW.json | 1 + config/locales/da.yml | 4 +-- config/locales/kab.yml | 13 +++++++++ config/locales/pt-PT.yml | 31 ++++++++++++++++++++ config/locales/ru.yml | 33 +++++++++++++--------- config/locales/simple_form.ca.yml | 1 + config/locales/simple_form.da.yml | 4 +-- config/locales/simple_form.kab.yml | 7 +++++ config/locales/simple_form.pt-PT.yml | 10 +++++++ config/locales/uk.yml | 9 ++++++ 27 files changed, 131 insertions(+), 18 deletions(-) diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 17fb3eb2b8..690a35990e 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -600,6 +600,7 @@ "notification.label.mention": "Zmínka", "notification.label.private_mention": "Soukromá zmínka", "notification.label.private_reply": "Privátní odpověď", + "notification.label.quote": "{name} citovali váš příspěvek", "notification.label.reply": "Odpověď", "notification.mention": "Zmínka", "notification.mentioned_you": "{name} vás zmínil", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Zmínky:", "notifications.column_settings.poll": "Výsledky anket:", "notifications.column_settings.push": "Push oznámení", + "notifications.column_settings.quote": "Citace:", "notifications.column_settings.reblog": "Boosty:", "notifications.column_settings.show": "Zobrazit ve sloupci", "notifications.column_settings.sound": "Přehrát zvuk", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 645808695f..b401c13d32 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.poll": "Afstemningsresultater:", "notifications.column_settings.push": "Push-notifikationer", + "notifications.column_settings.quote": "Citater:", "notifications.column_settings.reblog": "Fremhævelser:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Afspil lyd", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 9025144284..0ada8ba8c1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -600,6 +600,7 @@ "notification.label.mention": "Erwähnung", "notification.label.private_mention": "Private Erwähnung", "notification.label.private_reply": "Private Antwort", + "notification.label.quote": "{name} zitierte deinen Beitrag", "notification.label.reply": "Antwort", "notification.mention": "Erwähnung", "notification.mentioned_you": "{name} erwähnte dich", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.poll": "Umfrageergebnisse:", "notifications.column_settings.push": "Push-Benachrichtigungen", + "notifications.column_settings.quote": "Zitate:", "notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.show": "In dieser Spalte anzeigen", "notifications.column_settings.sound": "Ton abspielen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index a2dfbb8bd3..58de44bce3 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Επισημάνσεις:", "notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:", "notifications.column_settings.push": "Ειδοποιήσεις Push", + "notifications.column_settings.quote": "Παραθέσεις:", "notifications.column_settings.reblog": "Ενισχύσεις:", "notifications.column_settings.show": "Εμφάνισε σε στήλη", "notifications.column_settings.sound": "Αναπαραγωγή ήχου", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 75460017da..41051d493a 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la encuesta:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Adhesiones:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index d0c2290717..80a0138d93 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 62c296ad52..18ba22e95a 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 9e52dcb069..2f7c13bfaf 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -600,6 +600,7 @@ "notification.label.mention": "Maininta", "notification.label.private_mention": "Yksityismaininta", "notification.label.private_reply": "Yksityinen vastaus", + "notification.label.quote": "{name} lainasi julkaisuasi", "notification.label.reply": "Vastaus", "notification.mention": "Maininta", "notification.mentioned_you": "{name} mainitsi sinut", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Maininnat:", "notifications.column_settings.poll": "Äänestystulokset:", "notifications.column_settings.push": "Puskuilmoitukset", + "notifications.column_settings.quote": "Lainaukset:", "notifications.column_settings.reblog": "Tehostukset:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Äänimerkki", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 4cd1f9718f..c1da87693e 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -600,6 +600,7 @@ "notification.label.mention": "Mención", "notification.label.private_mention": "Mención privada", "notification.label.private_reply": "Resposta privada", + "notification.label.quote": "{name} citou a túa publicación", "notification.label.reply": "Resposta", "notification.mention": "Mención", "notification.mentioned_you": "{name} mencionoute", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index e9a755fab7..725130e75b 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -600,6 +600,7 @@ "notification.label.mention": "אזכור", "notification.label.private_mention": "אזכור פרטי", "notification.label.private_reply": "תשובה בפרטי", + "notification.label.quote": "{name} ציטט.ה את הודעתך", "notification.label.reply": "תשובה", "notification.mention": "אזכור", "notification.mentioned_you": "אוזכרת על ידי {name}", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 54dcc70a09..b27db1c57a 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -600,6 +600,7 @@ "notification.label.mention": "Minnst á", "notification.label.private_mention": "Einkaspjall", "notification.label.private_reply": "Einkasvar", + "notification.label.quote": "{name} vitnaði í færsluna þína", "notification.label.reply": "Svara", "notification.mention": "Minnst á", "notification.mentioned_you": "{name} minntist á þig", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Tilvísanir:", "notifications.column_settings.poll": "Niðurstöður könnunar:", "notifications.column_settings.push": "Ýti-tilkynningar", + "notifications.column_settings.quote": "Tilvitnanir:", "notifications.column_settings.reblog": "Endurbirtingar:", "notifications.column_settings.show": "Sýna í dálki", "notifications.column_settings.sound": "Spila hljóð", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 9edc9ae567..072ee445b5 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Menzioni:", "notifications.column_settings.poll": "Risultati del sondaggio:", "notifications.column_settings.push": "Notifiche push", + "notifications.column_settings.quote": "Citazioni:", "notifications.column_settings.reblog": "Reblog:", "notifications.column_settings.show": "Mostra nella colonna", "notifications.column_settings.sound": "Riproduci suono", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 52305fec2b..b0bee442cf 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -34,6 +34,7 @@ "account.followers": "Imeḍfaren", "account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.", "account.followers_counter": "{count, plural, one {{counter} n umḍfar} other {{counter} n yimeḍfaren}}", + "account.followers_you_know_counter": "Tessneḍ {counter}", "account.following": "Yeṭṭafaṛ", "account.following_counter": "{count, plural, one {{counter} yettwaḍfaren} other {{counter} yettwaḍfaren}}", "account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.", @@ -51,10 +52,12 @@ "account.mute_notifications_short": "Susem ilɣa", "account.mute_short": "Sgugem", "account.muted": "Yettwasgugem", + "account.mutual": "Temmeḍfaṛem", "account.no_bio": "Ulac aglam i d-yettunefken.", "account.open_original_page": "Ldi asebter anasli", "account.posts": "Tisuffaɣ", "account.posts_with_replies": "Tisuffaɣ d tririyin", + "account.remove_from_followers": "Kkes {name} seg ineḍfaren", "account.report": "Cetki ɣef @{name}", "account.requested": "Di laɛḍil ad yettwaqbel. Ssit i wakken ad yefsex usuter n uḍfar", "account.requested_follow": "{name} yessuter ad k·m-yeḍfer", @@ -85,6 +88,7 @@ "annual_report.summary.most_used_app.most_used_app": "asnas yettwasqedcen s waṭas", "annual_report.summary.most_used_hashtag.none": "Ula yiwen", "annual_report.summary.new_posts.new_posts": "tisuffaɣ timaynutin", + "annual_report.summary.thanks": "Tanemmirt imi i tettekkiḍ deg Mastodon!", "audio.hide": "Ffer amesli", "block_modal.show_less": "Ssken-d drus", "block_modal.show_more": "Ssken-d ugar", @@ -352,6 +356,8 @@ "keyboard_shortcuts.toot": "i wakken attebdud tajewwaqt tamaynut", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.up": "i tulin ɣer d asawen n tebdart", + "learn_more_link.got_it": "Gziɣ-t", + "learn_more_link.learn_more": "Issin ugar", "lightbox.close": "Mdel", "lightbox.next": "Ɣer zdat", "lightbox.previous": "Ɣer deffir", @@ -417,6 +423,8 @@ "notification.admin.sign_up": "Ijerred {name}", "notification.annual_report.view": "Wali #Wrapstodon", "notification.favourite": "{name} yesmenyaf addad-ik·im", + "notification.favourite_pm": "{name} yesmenyef abdar-ik·im uslig", + "notification.favourite_pm.name_and_others_with_link": "{name} akked {count, plural, one {# nnayeḍ} other {# nniḍen}} rnan abdar-ik·im uslig ar ismenyafen-nsen", "notification.follow": "iṭṭafar-ik·em-id {name}", "notification.follow.name_and_others": "{name} akked {count, plural, one {# nniḍen} other {# nniḍen}} iḍfeṛ-k·m-id", "notification.follow_request": "{name} yessuter-d ad k·m-yeḍfeṛ", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 22cae4b455..3ae1fd3f24 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Vermeldingen:", "notifications.column_settings.poll": "Peilingresultaten:", "notifications.column_settings.push": "Pushmeldingen", + "notifications.column_settings.quote": "Citaten:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.sound": "Geluid afspelen", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 8b0f2cbddf..cf2e828a45 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -498,6 +498,8 @@ "keyboard_shortcuts.translate": "traduzir uma publicação", "keyboard_shortcuts.unfocus": "remover o foco da área de texto / pesquisa", "keyboard_shortcuts.up": "mover para cima na lista", + "learn_more_link.got_it": "Entendido", + "learn_more_link.learn_more": "Saber mais", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", @@ -598,6 +600,7 @@ "notification.label.mention": "Menção", "notification.label.private_mention": "Menção privada", "notification.label.private_reply": "Resposta privada", + "notification.label.quote": "{name} citou a sua publicação", "notification.label.reply": "Resposta", "notification.mention": "Menção", "notification.mentioned_you": "{name} mencionou-te", @@ -655,6 +658,7 @@ "notifications.column_settings.mention": "Menções:", "notifications.column_settings.poll": "Resultados da sondagem:", "notifications.column_settings.push": "Notificações \"push\"", + "notifications.column_settings.quote": "Citações:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar na coluna", "notifications.column_settings.sound": "Reproduzir som", @@ -873,6 +877,11 @@ "status.open": "Expandir esta publicação", "status.pin": "Afixar no perfil", "status.quote_error.filtered": "Oculto devido a um dos seus filtros", + "status.quote_error.not_available": "Publicação indisponível", + "status.quote_error.pending_approval": "Publicação pendente", + "status.quote_error.pending_approval_popout.body": "As citações partilhadas no Fediverso podem demorar algum tempo a ser exibidas, uma vez que diferentes servidores têm protocolos diferentes.", + "status.quote_error.pending_approval_popout.title": "Citação pendente? Mantenha a calma", + "status.quote_post_author": "Citou uma publicação de @{name}", "status.read_more": "Ler mais", "status.reblog": "Impulsionar", "status.reblog_private": "Impulsionar com a visibilidade original", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 38401f18fa..f33558e6e8 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -321,6 +321,7 @@ "explore.trending_links": "Новини", "explore.trending_statuses": "Дописи", "explore.trending_tags": "Хештеґи", + "featured_carousel.next": "Далі", "filter_modal.added.context_mismatch_explanation": "Ця категорія фільтра не застосовується до контексту, в якому ви отримали доступ до цього допису. Якщо ви хочете, щоб дописи також фільтрувалися за цим контекстом, вам доведеться редагувати фільтр.", "filter_modal.added.context_mismatch_title": "Невідповідність контексту!", "filter_modal.added.expired_explanation": "Категорія цього фільтра застаріла, Вам потрібно змінити дату закінчення терміну дії, щоб застосувати її.", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index fc67e1d7ae..2d7b6bbf31 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "Lượt nhắc đến:", "notifications.column_settings.poll": "Kết quả vốt:", "notifications.column_settings.push": "Thông báo đẩy", + "notifications.column_settings.quote": "Trích dẫn:", "notifications.column_settings.reblog": "Lượt đăng lại:", "notifications.column_settings.show": "Báo trên thanh bên", "notifications.column_settings.sound": "Kèm âm báo", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index cafd061818..f917ad623c 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -658,6 +658,7 @@ "notifications.column_settings.mention": "提及:", "notifications.column_settings.poll": "投票結果:", "notifications.column_settings.push": "推播通知", + "notifications.column_settings.quote": "引用嘟文:", "notifications.column_settings.reblog": "轉嘟:", "notifications.column_settings.show": "於欄位中顯示", "notifications.column_settings.sound": "播放音效", diff --git a/config/locales/da.yml b/config/locales/da.yml index 85de82443e..20e3173c5a 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1428,7 +1428,7 @@ da: keywords: Nøgleord statuses: Individuelle indlæg statuses_hint_html: Dette filter gælder for udvalgte, individuelle indlæg, uanset om de matcher nøgleordene nedenfor. Gennemgå eller fjern indlæg fra filteret. - title: Redigere filter + title: Rediger filter errors: deprecated_api_multiple_keywords: Disse parametre kan ikke ændres fra denne applikation, da de gælder for flere end ét filternøgleord. Brug en nyere applikation eller webgrænsefladen. invalid_context: Ingen eller ugyldig kontekst angivet @@ -1748,7 +1748,7 @@ da: privacy: Privatliv privacy_hint_html: Styr, hvor meget du vil afsløre til gavn for andre. Folk opdager interessante profiler og apps ved at gennemse andres følgere og se, hvilke apps de sender fra, men du foretrækker måske at holde det skjult. reach: Rækkevidde - reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en? + reach_hint_html: Indstil, om du vil opdages og følges af nye personer. Vil du have, at dine indlæg skal vises på Udforsk-siden? Vil du have, at andre skal kunne se dig i deres følg-anbefalinger? Vil du acceptere alle nye følgere automatisk eller have detaljeret kontrol over hver enkelt? search: Søgning search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner. title: Fortrolighed og rækkevidde diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 96f69dcbdc..4685887a96 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -485,12 +485,20 @@ kab: title: Ihacṭagen inezzaɣ trending_rank: 'Anezzuɣ #%{rank}' trending: Inezzaɣ + username_blocks: + add_new: Rnu amaynut + comparison: + contains: Igber + new: + create: Rnu alugen + title: Rnu alugen n useqdac amaynut warning_presets: add_new: Rnu amaynut delete: Kkes webhooks: delete: Kkes enable: Rmed + enabled: D urmid admin_mailer: new_report: body: "%{reporter} yettwazen ɣef %{target}" @@ -623,6 +631,7 @@ kab: other: Ayen nniḍen emoji_styles: auto: Awurman + twemoji: Twemoji errors: '404': Asebter i tettnadiḍ ulac-it da. '500': @@ -858,6 +867,8 @@ kab: other: "%{count} n tbidyutin" edited_at_html: Tettwaẓreg ass n %{date} quote_policies: + followers: Ala wid i k·m-yeṭṭafaṛen + nobody: Ula yiwen public: Yal yiwen title: '%{name} : "%{quote}"' visibilities: @@ -904,6 +915,8 @@ kab: user_mailer: appeal_approved: action: Iɣewwaṛen n umiḍan + suspicious_sign_in: + change_password: snifel awal-ik·im n uɛeddi terms_of_service_changed: sign_off: Agraw n %{domain} warning: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index e8c357ee0f..3be630dbcd 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -190,6 +190,7 @@ pt-PT: create_relay: Criar retransmissor create_unavailable_domain: Criar domínio indisponível create_user_role: Criar função + create_username_block: Criar Regra de Nome de Utilizador demote_user: Despromover utilizador destroy_announcement: Eliminar mensagem de manutenção destroy_canonical_email_block: Eliminar bloqueio de e-mail @@ -203,6 +204,7 @@ pt-PT: destroy_status: Eliminar publicação destroy_unavailable_domain: Eliminar domínio indisponível destroy_user_role: Eliminar função + destroy_username_block: Eliminar Regra de Nome de Utilizador disable_2fa_user: Desativar 2FA disable_custom_emoji: Desativar emoji personalizado disable_relay: Desativar retransmissor @@ -237,6 +239,7 @@ pt-PT: update_report: Atualizar denúncia update_status: Atualizar publicação update_user_role: Atualizar função + update_username_block: Atualizar Regra de Nome de Utilizador actions: approve_appeal_html: "%{name} aprovou a contestação da decisão de moderação de %{target}" approve_user_html: "%{name} aprovou a inscrição de %{target}" @@ -255,6 +258,7 @@ pt-PT: create_relay_html: "%{name} criou o retransmissor %{target}" create_unavailable_domain_html: "%{name} parou as entregas ao domínio %{target}" create_user_role_html: "%{name} criou a função %{target}" + create_username_block_html: "%{name} adicionou regra para nomes de utilizadores que contêm %{target}" demote_user_html: "%{name} despromoveu o utilizador %{target}" destroy_announcement_html: "%{name} eliminou a mensagem de manutenção %{target}" destroy_canonical_email_block_html: "%{name} desbloqueou o e-mail com a hash %{target}" @@ -268,6 +272,7 @@ pt-PT: destroy_status_html: "%{name} removeu a publicação de %{target}" destroy_unavailable_domain_html: "%{name} retomou as entregas ao domínio %{target}" destroy_user_role_html: "%{name} eliminou a função %{target}" + destroy_username_block_html: "%{name} removeu regra para nomes de utilizadores que contêm %{target}" disable_2fa_user_html: "%{name} desativou o requerimento de autenticação em dois passos para o utilizador %{target}" disable_custom_emoji_html: "%{name} desativou o emoji %{target}" disable_relay_html: "%{name} desativou o retransmissor %{target}" @@ -302,6 +307,7 @@ pt-PT: update_report_html: "%{name} atualizou a denúncia %{target}" update_status_html: "%{name} atualizou a publicação de %{target}" update_user_role_html: "%{name} alterou a função %{target}" + update_username_block_html: "%{name} atualizou regra para nomes de utilizadores que contêm %{target}" deleted_account: conta eliminada empty: Não foram encontrados registos. filter_by_action: Filtrar por ação @@ -1085,6 +1091,25 @@ pt-PT: other: Utilizada por %{count} pessoas na última semana title: Recomendações e destaques trending: Em destaque + username_blocks: + add_new: Adicionar novo + block_registrations: Bloquear inscrições + comparison: + contains: Contém + equals: Igual a + contains_html: Contém %{string} + created_msg: Regra de nome de utilizador criada com sucesso + delete: Eliminar + edit: + title: Editar regra de nome de utilizador + matches_exactly_html: Igual a %{string} + new: + create: Criar regra + title: Criar nova regra de utilizador + no_username_block_selected: Não foi alterada nenhuma regra de nome de utilizador, pois nenhuma foi selecionada + not_permitted: Não permitido + title: Regras de nome de utilizador + updated_msg: Regra de nome de utilizador atualizada com sucesso warning_presets: add_new: Adicionar novo delete: Eliminar @@ -1662,6 +1687,10 @@ pt-PT: title: Nova menção poll: subject: A sondagem de %{name} terminou + quote: + body: 'A sua publicação foi citada por %{name}:' + subject: "%{name} citou a sua publicação" + title: Nova citação reblog: body: 'A tua publicação foi impulsionada por %{name}:' subject: "%{name} impulsionou a tua publicação" @@ -1880,6 +1909,8 @@ pt-PT: ownership: Não podem ser fixadas publicações de outras pessoas reblog: Não é possível fixar um impulso quote_policies: + followers: Apenas os seus seguidores + nobody: Ninguém public: Todos title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 17ba89bef0..a25ac88c54 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1922,24 +1922,29 @@ ru: other: 'содержались запрещённые хэштеги: %{tags}' edited_at_html: 'Дата последнего изменения: %{date}' errors: - in_reply_not_found: Пост, на который вы пытаетесь ответить, не существует или удалён. - over_character_limit: превышен лимит символов (%{max}) + in_reply_not_found: Пост, на который вы собирались ответить, был удалён или не существует. + quoted_status_not_found: Пост, который вы собирались процитировать, был удалён или не существует. + over_character_limit: превышает максимально допустимую длину (%{max} символов) pin_errors: - direct: Сообщения, видимые только упомянутым пользователям, не могут быть закреплены - limit: Вы закрепили максимально возможное число постов + direct: Нельзя закрепить пост, который доступен только тем, кто был упомянут в нём + limit: Вы достигли максимального количества постов, которые можно закрепить в профиле ownership: Нельзя закрепить чужой пост - reblog: Нельзя закрепить продвинутый пост - title: '%{name}: "%{quote}"' + reblog: Нельзя закрепить продвижение + quote_policies: + followers: Только ваши подписчики + nobody: Никто + public: Кто угодно + title: "%{name}: «%{quote}»" visibilities: - direct: Адресованный - private: Для подписчиков - private_long: Показывать только подписчикам - public: Для всех - public_long: Показывать всем - unlisted: Скрывать из лент - unlisted_long: Показывать всем, но не отображать в публичных лентах + direct: Личное упоминание + private: Только для подписчиков + private_long: Доступен только вашим подписчикам + public: Публичный + public_long: Доступен кому угодно + unlisted: Скрытый + unlisted_long: Доступен кому угодно, но не отображается в публичных лентах statuses_cleanup: - enabled: Автоматически удалять устаревшие посты + enabled: Автоматически удалять старые посты enabled_hint: Автоматически удаляет ваши посты после того, как они достигли определённого возрастного порога, за некоторыми исключениями ниже. exceptions: Исключения explanation: Из-за того, что удаление постов — это ресурсоёмкий процесс, оно производится медленно со временем, когда сервер менее всего загружен. По этой причине, посты могут удаляться не сразу, а спустя определённое время, по достижению возрастного порога. diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index f979e68323..ee24975b5d 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -56,6 +56,7 @@ ca: scopes: API permeses per a accedir a l'aplicació. Si selecciones un àmbit de nivell superior, no cal que en seleccionis un d'individual. setting_aggregate_reblogs: No mostra els nous impulsos dels tuts que ja s'han impulsat recentment (només afecta als impulsos nous rebuts) setting_always_send_emails: Normalment, no s'enviarà cap notificació per correu electrònic mentre facis servir Mastodon + setting_default_quote_policy: Aquesta configuració només tindrà efecte en les publicacions creades amb la següent versió de Mastodon, però podeu seleccionar-la en preparació. setting_default_sensitive: El contingut sensible està ocult per defecte i es pot mostrar fent-hi clic setting_display_media_default: Amaga el contingut gràfic marcat com a sensible setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 58f6a129bb..296ce49131 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -10,7 +10,7 @@ da: 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 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. + unlocked: Folk vil kunne følge dig uden at anmode om godkendelse. Fjern markeringen, hvis du vil gennemgå anmodninger om at følge, og vælge, om du vil acceptere eller afvise nye følgere. account_alias: acct: Angiv brugernavn@domain for den konto, hvorfra du vil flytte account_migration: @@ -329,7 +329,7 @@ da: follow_request: Nogen anmodede om at følge dig mention: Nogen omtalte dig pending_account: Ny konto kræver gennemgang - quote: Nogle citerede dig + quote: Nogen citerede dig reblog: Nogen fremhævede dit indlæg report: Ny anmeldelse indsendt software_updates: diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml index c0ff7e598e..d1a288e213 100644 --- a/config/locales/simple_form.kab.yml +++ b/config/locales/simple_form.kab.yml @@ -27,6 +27,8 @@ kab: username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda featured_tag: name: 'Ha-t-an kra seg ihacṭagen i tesseqdaceḍ ussan-a ineggura maḍi :' + form_challenge: + current_password: Tkecmeḍ ɣer temnaḍt taɣellsant imports: data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun invite_request: @@ -143,7 +145,12 @@ kab: name: Ahacṭag terms_of_service: text: Tiwtilin n useqdec + terms_of_service_generator: + domain: Taɣult user: + date_of_birth_1i: Ass + date_of_birth_2i: Ayyur + date_of_birth_3i: Aseggas role: Tamlilt time_zone: Tamnaḍt tasragant user_role: diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 0e65d82fd8..adc58a7aed 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -56,6 +56,7 @@ pt-PT: scopes: Quais as API a que a aplicação terá permissão para aceder. Se selecionar um âmbito de nível superior, não precisa de selecionar âmbitos individuais. setting_aggregate_reblogs: Não mostrar os novos impulsos para publicações que tenham sido recentemente impulsionadas (apenas afeta os impulsos recentemente recebidos) setting_always_send_emails: Normalmente as notificações por e-mail não serão enviadas quando estiver a utilizar ativamente o Mastodon + setting_default_quote_policy: Esta configuração só terá efeito nas publicações criadas com a próxima versão do Mastodon, mas pode desde já selecionar a sua preferência. setting_default_sensitive: Os multimédia sensíveis são ocultados por predefinição e podem ser revelados com um clique/toque setting_display_media_default: Esconder multimédia marcada como sensível setting_display_media_hide_all: Esconder sempre toda a multimédia @@ -159,6 +160,10 @@ pt-PT: name: Nome público da função, se esta estiver definida para ser apresentada com um emblema permissions_as_keys: Utilizadores com esta função terão acesso a... position: Funções mais altas decidem a resolução de conflitos em certas situações. Certas ações só podem ser executadas com certas funções com uma menor prioridade + username_block: + allow_with_approval: Em vez de impedir totalmente a inscrição, as inscrições correspondentes exigirão a sua aprovação + comparison: Tenha em atenção o Problema de Scunthorpe ao bloquear correspondências parciais + username: Terá correspondência independentemente das maiúsculas e minúsculas e de homógrafos comuns, como "4" para "a" ou "3" para "e" webhook: events: Selecione os eventos a enviar template: Componha o seu próprio conteúdo JSON utilizando a interpolação de variáveis. Deixar em branco para o JSON predefinido. @@ -324,6 +329,7 @@ pt-PT: follow_request: Alguém pediu para ser seu seguidor mention: Alguém o mencionou pending_account: Uma nova conta aguarda aprovação + quote: Alguém o citou reblog: Alguém impulsionou uma publicação sua report: Uma nova denúncia foi submetida software_updates: @@ -370,6 +376,10 @@ pt-PT: name: Nome permissions_as_keys: Permissões position: Prioridade + username_block: + allow_with_approval: Permitir inscrições com aprovação + comparison: Método de comparação + username: Palavra a corresponder webhook: events: Eventos ativados template: Modelo de conteúdo diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ed0ce8d7b5..721655d9a4 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -496,6 +496,15 @@ uk: new: title: Імпорт блокувань домену no_file: Файл не вибрано + fasp: + debug: + callbacks: + delete: Видалити + providers: + delete: Видалити + registrations: + confirm: Підтвердити + save: Зберегти follow_recommendations: description_html: "Слідувати рекомендаціям та допомогти новим користувачам швидко знайти цікавий вміст. Коли користувачі не взаємодіяли з іншими людьми достатньо, щоб сформувати персоналізовані рекомендації, радимо замість цього вказувати ці облікові записи. Вони щоденно переобчислюються з масиву облікових записів з найбільшою кількістю недавніх взаємодій і найбільшою кількістю місцевих підписників розраховується для цієї мови." language: Для мови From c8f263c419368ccc469e32a462bb308e94b2ae3c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Aug 2025 13:31:36 +0200 Subject: [PATCH 06/11] Export interaction policies for local posts over ActivityPub (#35697) --- .../activitypub/note_serializer.rb | 21 ++++++++++++++++++- .../activitypub/note_serializer_spec.rb | 15 +++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 972f146baf..05e3cf1bd2 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :interaction_policies attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -35,6 +35,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :quote, key: :quote_uri, if: :quote? attribute :quote_authorization, if: :quote_authorization? + attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } + def id ActivityPub::TagManager.instance.uri_for(object) end @@ -216,6 +218,23 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer ActivityPub::TagManager.instance.approval_uri_for(object.quote) end + def interaction_policy + approved_uris = [] + + # On outgoing posts, only automatic approval is supported + policy = object.quote_approval_policy >> 16 + approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]) + approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed]) + approved_uris << ActivityPub::TagManager.instance.uri_for(object.account) if approved_uris.empty? + + { + canQuote: { + automaticApproval: approved_uris, + }, + } + end + class MediaAttachmentSerializer < ActivityPub::Serializer context_extensions :blurhash, :focal_point diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 9c898e5212..d5d02a0d49 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -56,4 +56,19 @@ RSpec.describe ActivityPub::NoteSerializer do }) end end + + context 'with a quote policy', feature: :outgoing_quotes do + let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + + it 'has the expected shape' do + expect(subject).to include({ + 'type' => 'Note', + 'interactionPolicy' => a_hash_including( + 'canQuote' => a_hash_including( + 'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(parent.account)] + ) + ), + }) + end + end end From 55a98580aacecb2c4ef69fdf8720a3189c250fdf Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Aug 2025 13:52:56 +0200 Subject: [PATCH 07/11] Add UI for revoking quote posts (#35689) --- .../mastodon/actions/interactions_typed.ts | 22 ++++++++- app/javascript/mastodon/api/interactions.ts | 5 ++ .../mastodon/components/status_action_bar.jsx | 24 ++++++++-- .../mastodon/containers/status_container.jsx | 4 ++ .../features/status/components/action_bar.jsx | 23 +++++++-- .../mastodon/features/status/index.jsx | 7 +++ .../components/confirmation_modals/index.ts | 1 + .../confirmation_modals/revoke_quote.tsx | 48 +++++++++++++++++++ .../features/ui/components/modal_root.jsx | 2 + app/javascript/mastodon/locales/en.json | 4 ++ 10 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx diff --git a/app/javascript/mastodon/actions/interactions_typed.ts b/app/javascript/mastodon/actions/interactions_typed.ts index f58faffa86..832ea18910 100644 --- a/app/javascript/mastodon/actions/interactions_typed.ts +++ b/app/javascript/mastodon/actions/interactions_typed.ts @@ -1,4 +1,8 @@ -import { apiReblog, apiUnreblog } from 'mastodon/api/interactions'; +import { + apiReblog, + apiUnreblog, + apiRevokeQuote, +} from 'mastodon/api/interactions'; import type { StatusVisibility } from 'mastodon/models/status'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; @@ -33,3 +37,19 @@ export const unreblog = createDataLoadingThunk( return discardLoadData; }, ); + +export const revokeQuote = createDataLoadingThunk( + 'status/revoke_quote', + ({ + statusId, + quotedStatusId, + }: { + statusId: string; + quotedStatusId: string; + }) => apiRevokeQuote(quotedStatusId, statusId), + (data, { dispatch, discardLoadData }) => { + dispatch(importFetchedStatus(data)); + + return discardLoadData; + }, +); diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts index 118b5f06d2..5ffa5d1507 100644 --- a/app/javascript/mastodon/api/interactions.ts +++ b/app/javascript/mastodon/api/interactions.ts @@ -8,3 +8,8 @@ export const apiReblog = (statusId: string, visibility: StatusVisibility) => export const apiUnreblog = (statusId: string) => apiRequestPost(`v1/statuses/${statusId}/unreblog`); + +export const apiRevokeQuote = (quotedStatusId: string, statusId: string) => + apiRequestPost( + `v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`, + ); diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index ec5a9780cb..663fc53407 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -67,21 +67,28 @@ const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, + revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' }, }); -const mapStateToProps = (state, { status }) => ({ - relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), -}); +const mapStateToProps = (state, { status }) => { + const quotedStatusId = status.getIn(['quote', 'quoted_status']); + return ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), + quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + }); +}; class StatusActionBar extends ImmutablePureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, + quotedAccountId: ImmutablePropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onRevokeQuote: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, @@ -110,6 +117,7 @@ class StatusActionBar extends ImmutablePureComponent { updateOnProps = [ 'status', 'relationship', + 'quotedAccountId', 'withDismiss', ]; @@ -190,6 +198,10 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleRevokeQuoteClick = () => { + this.props.onRevokeQuote(this.props.status); + } + handleBlockClick = () => { const { status, relationship, onBlock, onUnblock } = this.props; const account = status.get('account'); @@ -241,7 +253,7 @@ class StatusActionBar extends ImmutablePureComponent { }; render () { - const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; + const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -291,6 +303,10 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); + if (quotedAccountId === me) { + menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true }); + } + if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); } else { diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 0fb5f25560..f3bc8fe5ff 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -111,6 +111,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ } }, + onRevokeQuote (status) { + dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }})); + }, + onEdit (status) { dispatch((_, getState) => { let state = getState(); diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 81f8163fff..5d6625fc10 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -61,22 +61,29 @@ const messages = defineMessages({ unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, + revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' }, }); -const mapStateToProps = (state, { status }) => ({ - relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), -}); +const mapStateToProps = (state, { status }) => { + const quotedStatusId = status.getIn(['quote', 'quoted_status']); + return ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), + quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + }); +}; class ActionBar extends PureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, + quotedAccountId: ImmutablePropTypes.string, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, + onRevokeQuote: PropTypes.func, onEdit: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -113,6 +120,10 @@ class ActionBar extends PureComponent { this.props.onDelete(this.props.status); }; + handleRevokeQuoteClick = () => { + this.props.onRevokeQuote(this.props.status); + } + handleRedraftClick = () => { this.props.onDelete(this.props.status, true); }; @@ -193,7 +204,7 @@ class ActionBar extends PureComponent { }; render () { - const { status, relationship, intl } = this.props; + const { status, relationship, quotedAccountId, intl } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -237,6 +248,10 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push(null); + if (quotedAccountId === me) { + menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true }); + } + if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); } else { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 4a714fa2c5..7bdcdb8971 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -259,6 +259,12 @@ class Status extends ImmutablePureComponent { } }; + handleRevokeQuoteClick = (status) => { + const { dispatch } = this.props; + + dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }})); + }; + handleEditClick = (status) => { const { dispatch, askReplyConfirmation } = this.props; @@ -635,6 +641,7 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} + onRevokeQuote={this.handleRevokeQuoteClick} onEdit={this.handleEditClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 25ffb3b629..139b6f8ba2 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -10,3 +10,4 @@ export { ConfirmClearNotificationsModal } from './clear_notifications'; export { ConfirmLogOutModal } from './log_out'; export { ConfirmFollowToListModal } from './follow_to_list'; export { ConfirmMissingAltTextModal } from './missing_alt_text'; +export { ConfirmRevokeQuoteModal } from './revoke_quote'; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx new file mode 100644 index 0000000000..83964aa5fe --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { revokeQuote } from 'mastodon/actions/interactions_typed'; +import { useAppDispatch } from 'mastodon/store'; + +import type { BaseConfirmationModalProps } from './confirmation_modal'; +import { ConfirmationModal } from './confirmation_modal'; + +const messages = defineMessages({ + revokeQuoteTitle: { + id: 'confirmations.revoke_quote.title', + defaultMessage: 'Remove post?', + }, + revokeQuoteMessage: { + id: 'confirmations.revoke_quote.message', + defaultMessage: 'This action cannot be undone.', + }, + revokeQuoteConfirm: { + id: 'confirmations.revoke_quote.confirm', + defaultMessage: 'Remove post', + }, +}); + +export const ConfirmRevokeQuoteModal: React.FC< + { + statusId: string; + quotedStatusId: string; + } & BaseConfirmationModalProps +> = ({ statusId, quotedStatusId, onClose }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const onConfirm = useCallback(() => { + void dispatch(revokeQuote({ quotedStatusId, statusId })); + }, [dispatch, statusId, quotedStatusId]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 4a98de0a31..3b7a24faaf 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -37,6 +37,7 @@ import { ConfirmLogOutModal, ConfirmFollowToListModal, ConfirmMissingAltTextModal, + ConfirmRevokeQuoteModal, } from './confirmation_modals'; import { ImageModal } from './image_modal'; import MediaModal from './media_modal'; @@ -59,6 +60,7 @@ export const MODAL_COMPONENTS = { 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }), + 'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }), 'MUTE': MuteModal, 'BLOCK': BlockModal, 'DOMAIN_BLOCK': DomainBlockModal, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index aa18fc39ce..6b796f9f81 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.title": "Remove follower?", + "confirmations.revoke_quote.confirm": "Remove post", + "confirmations.revoke_quote.message": "This action cannot be undone.", + "confirmations.revoke_quote.title": "Remove post?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.title": "Unfollow user?", @@ -896,6 +899,7 @@ "status.reply": "Reply", "status.replyAll": "Reply to thread", "status.report": "Report @{name}", + "status.revoke_quote": "Remove my post from @{name}’s post", "status.sensitive_warning": "Sensitive content", "status.share": "Share", "status.show_less_all": "Show less for all", From 6e48322055cf09e1b133f6193e29661e486a1353 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 6 Aug 2025 08:19:43 -0400 Subject: [PATCH 08/11] Add spec for `CanonicalEmailBlock.matching_email` scope (#35692) --- spec/models/canonical_email_block_spec.rb | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb index c63483f968..0b8dcfb9c1 100644 --- a/spec/models/canonical_email_block_spec.rb +++ b/spec/models/canonical_email_block_spec.rb @@ -3,6 +3,36 @@ require 'rails_helper' RSpec.describe CanonicalEmailBlock do + describe 'Associations' do + it { is_expected.to belong_to(:reference_account).class_name('Account').optional } + end + + describe 'Scopes' do + describe '.matching_email' do + subject { described_class.matching_email(email) } + + let!(:block) { Fabricate :canonical_email_block, email: 'test@example.com' } + + context 'when email is exact match' do + let(:email) { 'test@example.com' } + + it { is_expected.to contain_exactly(block) } + end + + context 'when email does not match' do + let(:email) { 'test@example.ORG' } + + it { is_expected.to be_empty } + end + + context 'when email is different but normalizes to same hash' do + let(:email) { 'te.st+more@EXAMPLE.com' } + + it { is_expected.to contain_exactly(block) } + end + end + end + describe '#email=' do let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' } From 9ec99ffef123e796c4ddbcdf0ed14ad06b76e209 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Aug 2025 16:23:12 +0200 Subject: [PATCH 09/11] Add `quote_approval_policy` parameter when posting and editing statuses (#35699) --- app/controllers/api/v1/statuses_controller.rb | 22 +++++++++- app/services/post_status_service.rb | 2 + app/services/update_status_service.rb | 1 + spec/requests/api/v1/statuses_spec.rb | 41 ++++++++++++++++++- spec/services/post_status_service_spec.rb | 6 +++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 57977e14b8..fdf1e7a468 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -82,6 +82,7 @@ class Api::V1::StatusesController < Api::BaseController text: status_params[:status], thread: @thread, quoted_status: @quoted_status, + quote_approval_policy: quote_approval_policy, media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], @@ -113,7 +114,8 @@ class Api::V1::StatusesController < Api::BaseController sensitive: status_params[:sensitive], language: status_params[:language], spoiler_text: status_params[:spoiler_text], - poll: status_params[:poll] + poll: status_params[:poll], + quote_approval_policy: quote_approval_policy ) render json: @status, serializer: REST::StatusSerializer @@ -180,6 +182,7 @@ class Api::V1::StatusesController < Api::BaseController :status, :in_reply_to_id, :quoted_status_id, + :quote_approval_policy, :sensitive, :spoiler_text, :visibility, @@ -202,6 +205,23 @@ class Api::V1::StatusesController < Api::BaseController ) end + def quote_approval_policy + # TODO: handle `nil` separately + return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present? + + case status_params[:quote_approval_policy] + when 'public' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 + when 'followers' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16 + when 'nobody' + 0 + else + # TODO: raise more useful message + raise ActiveRecord::RecordInvalid + end + end + def serializer_for_status @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 75116e2a26..103186b21e 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -19,6 +19,7 @@ class PostStatusService < BaseService # @option [String] :text Message # @option [Status] :thread Optional status to reply to # @option [Status] :quoted_status Optional status to quote + # @option [String] :quote_approval_policy Approval policy for quotes, one of `public`, `followers` or `nobody` # @option [Boolean] :sensitive # @option [String] :visibility # @option [String] :spoiler_text @@ -215,6 +216,7 @@ class PostStatusService < BaseService language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale), application: @options[:application], rate_limit: @options[:with_rate_limit], + quote_approval_policy: @options[:quote_approval_policy], }.compact end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 7837d37c95..4b871211a4 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -115,6 +115,7 @@ class UpdateStatusService < BaseService @status.spoiler_text = @options[:spoiler_text] || '' if @options.key?(:spoiler_text) @status.sensitive = @options[:sensitive] || @options[:spoiler_text].present? if @options.key?(:sensitive) || @options.key?(:spoiler_text) @status.language = valid_locale_cascade(@options[:language], @status.language, @status.account.user&.preferred_posting_language, I18n.default_locale) + @status.quote_approval_policy = @options[:quote_approval_policy] if @options[:quote_approval_policy].present? # We raise here to rollback the entire transaction raise NoChangesSubmittedError unless significant_changes? diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index ac15ae2462..ba18623302 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -158,6 +158,31 @@ RSpec.describe '/api/v1/statuses' do end end + context 'with a quote policy', feature: :outgoing_quotes do + let(:quoted_status) { Fabricate(:status, account: user.account) } + let(:params) do + { + status: 'Hello world, this is a self-quote', + quote_approval_policy: 'followers', + } + end + + it 'returns post with appropriate quote policy, as well as rate limit headers', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote_approval]).to include({ + automatic: ['followers'], + manual: [], + current_user: 'automatic', + }) + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + context 'with a self-quote post', feature: :outgoing_quotes do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do @@ -307,9 +332,10 @@ RSpec.describe '/api/v1/statuses' do describe 'PUT /api/v1/statuses/:id' do subject do - put "/api/v1/statuses/#{status.id}", headers: headers, params: { status: 'I am updated' } + put "/api/v1/statuses/#{status.id}", headers: headers, params: params end + let(:params) { { status: 'I am updated' } } let(:scopes) { 'write:statuses' } let(:status) { Fabricate(:status, account: user.account) } @@ -323,6 +349,19 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') expect(status.reload.text).to eq 'I am updated' end + + context 'when updating only the quote policy' do + let(:params) { { status: status.text, quote_approval_policy: 'public' } } + + it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do + expect { subject } + .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + end + end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 7e47506a9f..64bb5e32e2 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -160,6 +160,12 @@ RSpec.describe PostStatusService do expect(status.language).to eq 'en' end + it 'creates a status with the quote approval policy set' do + status = create_status_with_options(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + + expect(status.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + end + it 'processes mentions' do mention_service = instance_double(ProcessMentionsService) allow(mention_service).to receive(:call) From 4838085d662287d6647e9ea568b65963429cdd7f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Aug 2025 16:54:03 +0200 Subject: [PATCH 10/11] Bundle quotes and mentions in the same quickfilter bar since quotes don't have their own icon (#35700) --- app/javascript/mastodon/actions/notification_groups.ts | 7 +++++-- app/javascript/mastodon/selectors/notifications.ts | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 19d8750fa7..7e162e5e51 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -31,7 +31,9 @@ import { NOTIFICATIONS_FILTER_SET } from './notifications'; import { saveSettings } from './settings'; function excludeAllTypesExcept(filter: string) { - return allNotificationTypes.filter((item) => item !== filter); + return allNotificationTypes.filter( + (item) => item !== filter && !(item === 'quote' && filter === 'mention'), + ); } function getExcludedTypes(state: RootState) { @@ -156,7 +158,8 @@ export const processNewNotificationForGroups = createAppAsyncThunk( const showInColumn = activeFilter === 'all' ? notificationShows[notification.type] !== false - : activeFilter === notification.type; + : activeFilter === notification.type || + (activeFilter === 'mention' && notification.type === 'quote'); if (!showInColumn) return; diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index ea640406ea..8c808a2dff 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -26,7 +26,10 @@ const filterNotificationsByAllowedTypes = ( ); } return notifications.filter( - (item) => item.type === 'gap' || allowedType === item.type, + (item) => + item.type === 'gap' || + allowedType === item.type || + (allowedType === 'mention' && item.type === 'quote'), ); }; From ac59772dc6cda646258e61debfd792f9057c1c39 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Aug 2025 17:20:16 +0200 Subject: [PATCH 11/11] Change icon of quote notification mails (#35701) --- .../images/mailer-new/heading/README.md | 2 ++ .../images/mailer-new/heading/quote.png | Bin 0 -> 1837 bytes app/views/notification_mailer/quote.html.haml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 app/javascript/images/mailer-new/heading/quote.png diff --git a/app/javascript/images/mailer-new/heading/README.md b/app/javascript/images/mailer-new/heading/README.md index ecd4b949e7..9fb6841f14 100644 --- a/app/javascript/images/mailer-new/heading/README.md +++ b/app/javascript/images/mailer-new/heading/README.md @@ -1 +1,3 @@ Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). + +Seems to be 1.5 width icons scaled to 64×64px and centered above a blue square with round corners (24px). diff --git a/app/javascript/images/mailer-new/heading/quote.png b/app/javascript/images/mailer-new/heading/quote.png new file mode 100644 index 0000000000000000000000000000000000000000..c2af73282f1eaf770fa34b513c919b8a98309dfc GIT binary patch literal 1837 zcma)7X*3&%7EUO!CA4Bf2(?vO>lC%rR*5AHV=0ZT2|;D3B|)sAwme3?>R`rRw5Wz6 z#=gXEIz{alaPq4zKr+EQ z)K4RX^q_Ft2o3=7w`0(E?nV`DErbNPM92*99B_(`9x$f!`*rllsozvSe>w^>Kf8{6 z2jV}&+%u%KK(p)jqOU^X7Um<7+zh)DtM40CoJOt*d=yc=ERp*iy~%aiNw4^z1Sh(^ zzTFkvan3n9{p(ATvHn5HZWnHww%f6Bo-xy@s(_P}AuTCygoJn9lqnk2= z4<7xCX^Nyy6bRdpCnJc#0n0JXgD#c8*=dJLPgi}1T5rqd$42E;vxYfgh9X9JL9YbXmNzGCK>b;`psvAIe2TQdMQTM9ZsM=?@2-yuo#+at9rT5_Dl z(S@8mj54#`urjJ3nVF*&3w>$8w!p%L!ZhYH?Hi?dfO)Y6u1cNeF}-0#FX!Q>zSVM%aeE37cJO!YgtKyV$j^ zjPS9z6Xo6zcTbUaWhn~D+y>{l`m%IJ5t@DU0dQffZcsmeT6oAqo2YfE^>!I-V@hv> z<`5ri{x^T$z$q-e5+WztR#qt8J?sy5+cUnPcyz{8|8}J!8?zq zOu9Qeqw?7#7oMi%q>Bj%mWe%tBz~xO>SNH-d|U=@ZpxbH`n>2-Pj4dD0PlJg%S!JY z_H(G{Kf;yX>Z{Q6Hz+C!if<4Ds#z7$^BjA0no*4mc>HLAHuA)$k=N)0MNWY*ZpgIm z=@64rjd;TL2-aVA(BpSZV%|>xbrqnyJx^27r2qrLccd%N{;g#8UkT6b zCGeh~IiH&6CGpa<%ZmIZEnSGZ6Cqpzc+RP_!)S>sjH!J&+Do=1Ro4i)zUG;;3u&{y z6v;^wS2?Z41z3X1DlB_5S|-m)^6hUkBqre0nS*3U;8+oG(9KiszNg3(Ztr!sT;?%Y z1}AIUbqTuWL#|{Nxn}R58nx7lcuoT2yfB{(VO2x2>^orU{7p+%XneRg&B4@_) zxIhzwYt-WH!Imd1Oc8SW$^>|?Cjv-Vze1Rk~R;t%Q?UqD5`@npT(IObWEm=5-2iMlt zC-mfVg?m|mD!XJC4)gQM>G%{$WPj_&;Kf4M9*0bDeS7=4=E1Bwi=oo-I3sRO6H4Iy z(>C^Y+!sVs)7paswI%NEg}ks}dF#_AXF6Xy^w>(D0_pR<{Rx&4)`I_DzgtWU*_dL%R(alc7sqC;+A-^g=LMFtlKg0p!BXRx z%{V+k5%z2OH(H0yLsnT;co(e^lwQn1v^nMC-azT&2{;AijK{rKaa^uz&x5t(JCWfu zWMFwhQ<7-pt-qoZw2{SY`%K*kHOB%mEw#CYbl`$(eS44YYpY2BkOx=-g8cAH!K>|o z#IaV0tu*{;=_lKd<(2p+yIK9;K{@h20R3$od9J8$o`R$2=iwc8hC4|C{Z}geiM*u>T zkA|!Z@)~chnbZVWuK$c3)Eja6be_mthinBDA#apN4@1u0G#@4;>ZzWg`lpL7UL62q LVUMme$0z*_$oXbM literal 0 HcmV?d00001 diff --git a/app/views/notification_mailer/quote.html.haml b/app/views/notification_mailer/quote.html.haml index 1a2d8f8c76..139f4b2e8d 100644 --- a/app/views/notification_mailer/quote.html.haml +++ b/app/views/notification_mailer/quote.html.haml @@ -1,6 +1,6 @@ = content_for :heading do = render 'application/mailer/heading', - image_url: frontend_asset_url('images/mailer-new/heading/boost.png'), + image_url: frontend_asset_url('images/mailer-new/heading/quote.png'), subtitle: t('notification_mailer.quote.body', name: @status.account.pretty_acct), title: t('notification_mailer.quote.title') %table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }