diff --git a/app/controllers/timeline_controller.rb b/app/controllers/timeline_controller.rb index 30c3fc48..4445af6a 100644 --- a/app/controllers/timeline_controller.rb +++ b/app/controllers/timeline_controller.rb @@ -16,7 +16,7 @@ class TimelineController < ApplicationController def public @title = generate_title(t(".title")) - paginate_timeline { |args| Answer.cursored_public_timeline(**args) } + paginate_timeline { |args| Answer.cursored_public_timeline(**args, current_user:) } end private diff --git a/app/models/answer/timeline_methods.rb b/app/models/answer/timeline_methods.rb index 732ba2bd..8c6085bf 100644 --- a/app/models/answer/timeline_methods.rb +++ b/app/models/answer/timeline_methods.rb @@ -5,8 +5,21 @@ module Answer::TimelineMethods define_cursor_paginator :cursored_public_timeline, :public_timeline - def public_timeline + def public_timeline(current_user: nil) joins(:user) + .then do |query| + next query unless current_user + + blocked_and_muted_user_ids = current_user.blocked_user_ids_cached + current_user.muted_user_ids_cached + next query if blocked_and_muted_user_ids.empty? + + # build a more complex query if we block or mute someone + # otherwise the query ends up as "anon OR (NOT anon AND user_id NOT IN (NULL))" which will only return anonymous questions + query + .joins(:question) + .where("questions.author_is_anonymous OR (NOT questions.author_is_anonymous AND questions.user_id NOT IN (?))", blocked_and_muted_user_ids) + .where.not(answers: { user_id: blocked_and_muted_user_ids }) + end .where(users: { privacy_allow_public_timeline: true }) .order(:created_at) .reverse_order diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb index 12fc9ed4..cd6654c4 100644 --- a/spec/models/answer_spec.rb +++ b/spec/models/answer_spec.rb @@ -23,7 +23,7 @@ describe Answer, type: :model do context "user has the question in their inbox" do before do - Inbox.create(user: user, question: question, new: true) + Inbox.create(user:, question:, new: true) end it "should remove the question from the user's inbox" do @@ -91,4 +91,147 @@ describe Answer, type: :model do expect { subject.destroy }.to change { question.answer_count }.by(-1) end end + + describe ".public_timeline" do + let(:user) { FactoryBot.create(:user) } + let(:user1) { FactoryBot.create(:user) } + let(:user2) { FactoryBot.create(:user) } + + let(:blocked_user) { FactoryBot.create(:user) } + let(:muted_user) { FactoryBot.create(:user) } + let!(:answer_to_anonymous) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a true anonymous coward", + question: FactoryBot.create( + :question, + author_is_anonymous: true + ) + ) + end + let!(:answer_to_normal_user) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a normal user", + question: FactoryBot.create( + :question, + user: user1, + author_is_anonymous: false + ) + ) + end + let!(:answer_to_normal_user_anonymous) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a cowardly user", + question: FactoryBot.create( + :question, + user: user1, + author_is_anonymous: true + ) + ) + end + let!(:answer_from_blocked_user) do + FactoryBot.create( + :answer, + user: blocked_user, + content: "answer from a blocked user", + question: FactoryBot.create(:question) + ) + end + let!(:answer_to_blocked_user) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a blocked user", + question: FactoryBot.create( + :question, + user: blocked_user, + author_is_anonymous: false + ) + ) + end + let!(:answer_to_blocked_user_anonymous) do + FactoryBot.create( + :answer, + user: user1, + content: "answer to a blocked user who's a coward", + question: FactoryBot.create( + :question, + user: blocked_user, + author_is_anonymous: true + ) + ) + end + let!(:answer_from_muted_user) do + FactoryBot.create( + :answer, + user: muted_user, + content: "answer from a muted user", + question: FactoryBot.create(:question) + ) + end + let!(:answer_to_muted_user) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a muted user", + question: FactoryBot.create( + :question, + user: muted_user, + author_is_anonymous: false + ) + ) + end + let!(:answer_to_muted_user_anonymous) do + FactoryBot.create( + :answer, + user: user2, + content: "answer to a muted user who's a coward", + question: FactoryBot.create( + :question, + user: muted_user, + author_is_anonymous: true + ) + ) + end + + subject { Answer.public_timeline } + + it "includes all answers to questions from all the users" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).to include(answer_to_blocked_user) + expect(subject).to include(answer_to_muted_user) + expect(subject).to include(answer_from_blocked_user) + expect(subject).to include(answer_from_muted_user) + end + + context "when given a current user who blocks and mutes some users" do + before do + user.block blocked_user + user.mute muted_user + end + + subject { Answer.public_timeline current_user: user } + + it "only includes answers to questions from users the user doesn't block or mute" do + expect(subject).to include(answer_to_anonymous) + expect(subject).to include(answer_to_normal_user) + expect(subject).to include(answer_to_normal_user_anonymous) + expect(subject).to include(answer_to_blocked_user_anonymous) + expect(subject).to include(answer_to_muted_user_anonymous) + expect(subject).not_to include(answer_to_blocked_user) + expect(subject).not_to include(answer_from_blocked_user) + expect(subject).not_to include(answer_to_muted_user) + expect(subject).not_to include(answer_from_muted_user) + end + end + end end