From 4351856ce9de9c9c8c31e5a92fa9352412bbbb2e Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 10 Apr 2025 11:43:57 +0200 Subject: [PATCH] Fix streaming of quote posts --- app/lib/status_cache_hydrator.rb | 19 ++++- spec/lib/status_cache_hydrator_spec.rb | 109 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index dab9b4cd93e..8821c23d139 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -6,8 +6,6 @@ class StatusCacheHydrator end def hydrate(account_id) - # TODO: handle quotes - # The cache of the serialized hash is generated by the fan-out-on-write service payload = Rails.cache.fetch("fan-out/#{@status.id}") { InlineRenderer.render(@status, nil, :status) } @@ -73,6 +71,23 @@ class StatusCacheHydrator payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id) payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id payload[:filtered] = mapped_applied_custom_filter(account_id, status) + payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id) if payload[:quote] + end + + def hydrate_quote_payload(empty_payload, quote, account_id) + # TODO: properly handle quotes, including visibility and access control + + empty_payload.tap do |payload| + # Nothing to do if we're in the shallow (depth limit) case + next unless payload.key?(:quoted_status) + + # TODO: handle hiding a rendered status or showing a non-rendered status according to visibility + if quote&.quoted_status.nil? + payload[:quoted_status] = nil + elsif payload[:quoted_status].present? + payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account_id) + end + end end def mapped_applied_custom_filter(account_id, status) diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 958e2f62d74..a0a82e3923d 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -40,10 +40,119 @@ RSpec.describe StatusCacheHydrator do end end + context 'when handling an unapproved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: status, quoted_status: quoted_status, state: :pending) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when handling an approved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + + context 'when the quoted post has been favourited' do + before do + FavouriteService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when the quoted post has been reblogged' do + before do + ReblogService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when the quoted post matches account filters' do + let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') } + + before do + account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + end + context 'when handling a reblog' do let(:reblog) { Fabricate(:status) } let(:status) { Fabricate(:status, reblog: reblog) } + context 'when the reblog has an approved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: reblog, quoted_status: quoted_status, state: :accepted) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + + context 'when the quoted post has been favourited' do + before do + FavouriteService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + + context 'when the quoted post has been reblogged' do + before do + ReblogService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + + context 'when the quoted post matches account filters' do + let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') } + + before do + account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + end + context 'when it has been favourited' do before do FavouriteService.new.call(account, reblog)