From e537292e2a549726a4e4b53395432f5bbd709f20 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 18 Mar 2026 09:21:03 -0400 Subject: [PATCH 1/5] Prefer `to_json` in self destruct scheduler (#38263) --- .../scheduler/self_destruct_scheduler.rb | 19 ++++++++++++------- .../scheduler/self_destruct_scheduler_spec.rb | 5 +++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/workers/scheduler/self_destruct_scheduler.rb b/app/workers/scheduler/self_destruct_scheduler.rb index 93f8d19f16..4d8ee2cd85 100644 --- a/app/workers/scheduler/self_destruct_scheduler.rb +++ b/app/workers/scheduler/self_destruct_scheduler.rb @@ -55,13 +55,10 @@ class Scheduler::SelfDestructScheduler end def delete_account!(account) - payload = ActiveModelSerializers::SerializableResource.new( - account, - serializer: ActivityPub::DeleteActorSerializer, - adapter: ActivityPub::Adapter - ).as_json - - json = JSON.generate(ActivityPub::LinkedDataSignature.new(payload).sign!(account)) + json = ActivityPub::LinkedDataSignature + .new(deletion_payload(account)) + .sign!(account) + .to_json ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| [json, account.id, inbox_url] @@ -70,4 +67,12 @@ class Scheduler::SelfDestructScheduler # Do not call `Account#suspend!` because we don't want to issue a deletion request account.update!(suspended_at: Time.now.utc, suspension_origin: :local) end + + def deletion_payload(account) + ActiveModelSerializers::SerializableResource.new( + account, + serializer: ActivityPub::DeleteActorSerializer, + adapter: ActivityPub::Adapter + ).as_json + end end diff --git a/spec/workers/scheduler/self_destruct_scheduler_spec.rb b/spec/workers/scheduler/self_destruct_scheduler_spec.rb index a79559efdd..0ff0faf05a 100644 --- a/spec/workers/scheduler/self_destruct_scheduler_spec.rb +++ b/spec/workers/scheduler/self_destruct_scheduler_spec.rb @@ -39,6 +39,8 @@ RSpec.describe Scheduler::SelfDestructScheduler do end context 'when sidekiq is operational' do + let!(:other_account) { Fabricate :account, inbox_url: 'https://host.example/inbox', domain: 'host.example', protocol: :activitypub } + it 'suspends local non-suspended accounts' do worker.perform @@ -51,6 +53,9 @@ RSpec.describe Scheduler::SelfDestructScheduler do worker.perform + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Delete', signature: be_present), account.id, other_account.inbox_url) + expect(account.reload.suspended_at).to be > 1.day.ago expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound) end From bd9b24f1ef81f6ba6ca0f0c606995d0b54a9c208 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 18 Mar 2026 09:21:54 -0400 Subject: [PATCH 2/5] Prefer rspec-sidekiq matchers over "expect push bulk to match" approach (#38274) --- spec/spec_helper.rb | 9 ----- .../distribute_poll_update_worker_spec.rb | 7 ++-- .../activitypub/distribution_worker_spec.rb | 35 +++++++++++-------- .../move_distribution_worker_spec.rb | 13 +++---- .../update_distribution_worker_spec.rb | 7 ++-- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 13683e404e..90e7e40578 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,12 +44,3 @@ def serialized_record_json(record, serializer, adapter: nil, options: {}) ).to_json ) end - -def expect_push_bulk_to_match(klass, matcher) - allow(Sidekiq::Client).to receive(:push_bulk) - yield - expect(Sidekiq::Client).to have_received(:push_bulk).with(hash_including({ - 'class' => klass, - 'args' => matcher, - })) -end diff --git a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb index 9ff4731f96..d04b2331a6 100644 --- a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb +++ b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb @@ -16,9 +16,10 @@ RSpec.describe ActivityPub::DistributePollUpdateWorker do end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com']]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Update'), account.id, 'http://example.com') end end end diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb index 1551ba63ba..738ba44be6 100644 --- a/spec/workers/activitypub/distribution_worker_spec.rb +++ b/spec/workers/activitypub/distribution_worker_spec.rb @@ -19,9 +19,10 @@ RSpec.describe ActivityPub::DistributionWorker do end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything) end end @@ -31,9 +32,10 @@ RSpec.describe ActivityPub::DistributionWorker do end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything) end end @@ -46,9 +48,10 @@ RSpec.describe ActivityPub::DistributionWorker do end it 'delivers to mentioned accounts' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'https://foo.bar/inbox', anything]]) do - subject.perform(status.id) - end + subject.perform(status.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Create'), status.account.id, 'https://foo.bar/inbox', anything) end end @@ -67,9 +70,10 @@ RSpec.describe ActivityPub::DistributionWorker do object: ActivityPub::TagManager.instance.uri_for(status), } - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(expected_json), reblog.account.id, 'http://example.com', anything]]) do - subject.perform(reblog.id) - end + subject.perform(reblog.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(expected_json), reblog.account.id, 'http://example.com', anything) end end @@ -86,9 +90,10 @@ RSpec.describe ActivityPub::DistributionWorker do }), } - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(expected_json), reblog.account.id, 'http://example.com', anything]]) do - subject.perform(reblog.id) - end + subject.perform(reblog.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(expected_json), reblog.account.id, 'http://example.com', anything) end end end diff --git a/spec/workers/activitypub/move_distribution_worker_spec.rb b/spec/workers/activitypub/move_distribution_worker_spec.rb index 63396834de..bde0449186 100644 --- a/spec/workers/activitypub/move_distribution_worker_spec.rb +++ b/spec/workers/activitypub/move_distribution_worker_spec.rb @@ -16,16 +16,11 @@ RSpec.describe ActivityPub::MoveDistributionWorker do end it 'delivers to followers and known blockers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, expected_migration_deliveries) do - subject.perform(migration.id) - end - end + subject.perform(migration.id) - def expected_migration_deliveries - [ - [match_json_values(type: 'Move'), migration.account.id, 'http://example.com'], - [match_json_values(type: 'Move'), migration.account.id, 'http://example2.com'], - ] + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Move'), migration.account.id, 'http://example.com') + .and have_enqueued_sidekiq_job(match_json_values(type: 'Move'), migration.account.id, 'http://example2.com') end end end diff --git a/spec/workers/activitypub/update_distribution_worker_spec.rb b/spec/workers/activitypub/update_distribution_worker_spec.rb index 7d78606398..b156c329e8 100644 --- a/spec/workers/activitypub/update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/update_distribution_worker_spec.rb @@ -14,9 +14,10 @@ RSpec.describe ActivityPub::UpdateDistributionWorker do end it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com', anything]]) do - subject.perform(account.id) - end + subject.perform(account.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Update'), account.id, 'http://example.com', anything) end end end From 815c2cf8e93750bc99b4f6b46b9dc12183576126 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 18 Mar 2026 09:23:06 -0400 Subject: [PATCH 3/5] Clean up `Webfinger` lib spec (#38259) --- spec/lib/webfinger_spec.rb | 55 ++++++++++++++----- spec/services/resolve_account_service_spec.rb | 8 +-- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb index 68ce841a53..0bf682eca9 100644 --- a/spec/lib/webfinger_spec.rb +++ b/spec/lib/webfinger_spec.rb @@ -3,45 +3,72 @@ require 'rails_helper' RSpec.describe Webfinger do - describe 'self link' do + describe '#initialize' do + subject { described_class.new(uri) } + + context 'when called with local account' do + let(:uri) { 'acct:alice' } + + it 'handles value and raises error' do + expect { subject }.to raise_error(ArgumentError, /for local account/) + end + end + + context 'when called with remote account' do + let(:uri) { 'acct:alice@host.example' } + + it 'handles value and sets attributes' do + expect { subject }.to_not raise_error + end + end + end + + describe '#perform' 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' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - expect(subject.self_link_href).to eq 'https://example.com/alice' end end context 'when self link is specified with type application/ld+json' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - expect(subject.self_link_href).to eq 'https://example.com/alice' end end context 'when self link is specified with incorrect type' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) + end it 'raises an error' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - expect { subject } .to raise_error(Webfinger::Error) end end context 'when response body is not parsable' do - it 'raises an error' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com') - .to_return(body: 'XXX', headers: { 'Content-Type': 'application/jrd+json' }) + before do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: 'XXX', headers: { 'Content-Type': 'application/jrd+json' }) + end + it 'raises an error' do expect { subject } .to raise_error(Webfinger::Error) end @@ -63,7 +90,7 @@ RSpec.describe Webfinger do 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: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'uses host meta details' do diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 4714606343..9710141e4e 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -103,7 +103,7 @@ RSpec.describe ResolveAccountService do context 'with a legitimate webfinger redirection' do before do webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -121,7 +121,7 @@ RSpec.describe ResolveAccountService do context 'with a misconfigured redirection' do before do webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -139,9 +139,9 @@ RSpec.describe ResolveAccountService do context 'with too many webfinger redirections' do before do webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: JSON.generate(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: webfinger2.to_json, headers: { 'Content-Type': 'application/jrd+json' }) end it 'does not return a new remote account' do From 380b898d0d90da1d73db776243a24f0f4ac2a654 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 18 Mar 2026 17:08:23 +0100 Subject: [PATCH 4/5] Improve accessibility of server rules list in sign-up flow (#38257) --- app/javascript/entrypoints/public.tsx | 60 +++++++++++++--- app/javascript/styles/mastodon/about.scss | 70 +++++++++++-------- app/javascript/styles/mastodon/reset.scss | 2 +- app/views/auth/registrations/rules.html.haml | 2 +- .../_rule_translation.html.haml | 10 ++- config/locales/en.yml | 1 + 6 files changed, 103 insertions(+), 42 deletions(-) diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index f891410a3c..4089575e41 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -149,6 +149,8 @@ function loaded() { document.querySelector('#user_settings_attributes_default_privacy'), ); + truncateRuleHints(); + const reactComponents = document.querySelectorAll('[data-component]'); if (reactComponents.length > 0) { @@ -425,21 +427,61 @@ on('submit', '#registration_new_user,#new_user', () => { }); }); +// Truncate long rule hints + +const MAX_RULE_HINT_LENGTH = 100; + +function truncateRuleHints() { + const ruleListItems = + document.querySelectorAll('.rules-list li'); + if (!ruleListItems.length) return; + + ruleListItems.forEach((item) => { + toggleRuleHint(item, true); + }); +} + +function toggleRuleHint(listItem: HTMLLIElement, isInitialSetup?: boolean) { + const hint = listItem.querySelector( + '.rules-list__hint-text', + ); + if (!hint) return; + + const hintText = hint.innerHTML; + const hintToggleButton = listItem.querySelector('button'); + + if (hintText.length > MAX_RULE_HINT_LENGTH) { + // Store full hint in a data attribute, then truncate it with an '…' + hint.dataset.fullHint = hintText; + hint.innerHTML = `${hintText.slice(0, MAX_RULE_HINT_LENGTH - 1).trim()}…`; + + if (hintToggleButton) { + // Reveal toggle button if needed + hintToggleButton.removeAttribute('hidden'); + hintToggleButton.setAttribute('aria-expanded', 'false'); + } + } else if (!isInitialSetup) { + const { fullHint } = hint.dataset; + if (fullHint) { + // Restore full hint from data attribute, then delete attribute + hint.innerHTML = fullHint; + delete hint.dataset.fullHint; + + hintToggleButton?.setAttribute('aria-expanded', 'true'); + hint.parentElement?.focus(); + } + } +} + on('click', '.rules-list button', ({ target }) => { if (!(target instanceof HTMLElement)) { return; } - const button = target.closest('button'); + const listItem = target.closest('li'); - if (!button) { - return; - } - - if (button.ariaExpanded === 'true') { - button.ariaExpanded = 'false'; - } else { - button.ariaExpanded = 'true'; + if (listItem) { + toggleRuleHint(listItem); } }); diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 0bb2c8c9eb..cb4885a7e8 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -34,34 +34,6 @@ $fluid-breakpoint: $maximum-width + 20px; counter-increment: list-counter; min-height: 4ch; - button { - background: transparent; - border: 0; - padding: 0; - margin: 0; - text-align: start; - font: inherit; - - &:hover, - &:focus, - &:active { - background: transparent; - } - - &[aria-expanded='false'] .rules-list__hint { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - @supports (-webkit-line-clamp: 2) { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - white-space: normal; - } - } - } - &::before { content: counter(list-counter); position: absolute; @@ -91,6 +63,48 @@ $fluid-breakpoint: $maximum-width + 20px; font-size: 14px; font-weight: 400; color: var(--color-text-secondary); + + // Giving this a focus outline as the hint + // will be focused when toggling the full hint + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + &__toggle-button { + position: relative; + display: inline-flex; + vertical-align: -0.25em; + border: none; + border-radius: 4px; + color: var(--color-text-primary); + background: var(--color-bg-secondary); + + &[hidden] { + display: none; + } + + .icon { + width: 1lh; + height: 1lh; + } + + &:hover { + background: var(--color-bg-tertiary); + } + + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::before { + // Increase clickable size + content: ''; + position: absolute; + inset: -12px; + } } } diff --git a/app/javascript/styles/mastodon/reset.scss b/app/javascript/styles/mastodon/reset.scss index 2c3efbddc4..b6b5837136 100644 --- a/app/javascript/styles/mastodon/reset.scss +++ b/app/javascript/styles/mastodon/reset.scss @@ -35,7 +35,7 @@ body { } ol, ul { - list-style: none; + list-style-type: none; } blockquote, q { diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml index 1e3d934c37..54c21e7798 100644 --- a/app/views/auth/registrations/rules.html.haml +++ b/app/views/auth/registrations/rules.html.haml @@ -16,7 +16,7 @@ %h1.title= t('auth.rules.title') %p.lead= t('auth.rules.preamble', domain: site_hostname) - %ol.rules-list + %ol.rules-list{ role: 'list' } = render collection: @rule_translations, partial: 'auth/rule_translations/rule_translation' .stacked-actions diff --git a/app/views/auth/rule_translations/_rule_translation.html.haml b/app/views/auth/rule_translations/_rule_translation.html.haml index 32b9cc28af..a8c307270f 100644 --- a/app/views/auth/rule_translations/_rule_translation.html.haml +++ b/app/views/auth/rule_translations/_rule_translation.html.haml @@ -1,4 +1,8 @@ %li - %button{ type: 'button', aria: { expanded: 'false' } } - .rules-list__text= rule_translation.text - .rules-list__hint= rule_translation.hint + .rules-list__text= rule_translation.text + - if rule_translation.hint? + .rules-list__hint{ tabIndex: -1 } + %span.rules-list__hint-text= rule_translation.hint + %button.rules-list__toggle-button{ type: 'button', hidden: true, 'aria-expanded': 'false' } + = material_symbol('more_horiz') + %span.sr-only= t('auth.rules.read_more') diff --git a/config/locales/en.yml b/config/locales/en.yml index 5db5384a47..fc20a1da8c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1295,6 +1295,7 @@ en: invited_by: 'You can join %{domain} thanks to the invitation you have received from:' preamble: These are set and enforced by the %{domain} moderators. preamble_invited: Before you proceed, please consider the ground rules set by the moderators of %{domain}. + read_more: Read more title: Some ground rules. title_invited: You've been invited. security: Security From db074fc3e2b671e28a44ae379e6df1fcbdbbce53 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 18 Mar 2026 12:22:24 -0400 Subject: [PATCH 5/5] Add constant for backup service placeholder (#38280) --- app/services/backup_service.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 0aede0145d..56a3a23c6a 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -7,6 +7,7 @@ class BackupService < BaseService include ContextHelper CHUNK_SIZE = 1.megabyte + PLACEHOLDER = '!PLACEHOLDER!' attr_reader :account, :backup @@ -22,9 +23,9 @@ class BackupService < BaseService def build_outbox_json!(file) skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) skeleton[:@context] = full_context - skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton[:orderedItems] = [PLACEHOLDER] skeleton = JSON.generate(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') + prepend, append = skeleton.split(PLACEHOLDER.to_json) file.write(prepend) @@ -115,9 +116,9 @@ class BackupService < BaseService def dump_likes!(zipfile) skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) skeleton.delete(:totalItems) - skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton[:orderedItems] = [PLACEHOLDER] skeleton = JSON.generate(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') + prepend, append = skeleton.split(PLACEHOLDER.to_json) zipfile.get_output_stream('likes.json') do |io| io.write(prepend) @@ -139,9 +140,9 @@ class BackupService < BaseService def dump_bookmarks!(zipfile) skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) skeleton.delete(:totalItems) - skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton[:orderedItems] = [PLACEHOLDER] skeleton = JSON.generate(skeleton) - prepend, append = skeleton.split('"!PLACEHOLDER!"') + prepend, append = skeleton.split(PLACEHOLDER.to_json) zipfile.get_output_stream('bookmarks.json') do |io| io.write(prepend)