diff --git a/app/models/user/relationship/block.rb b/app/models/user/relationship/block.rb index 674fdb46..01cd25ed 100644 --- a/app/models/user/relationship/block.rb +++ b/app/models/user/relationship/block.rb @@ -21,12 +21,16 @@ class User raise Errors::BlockingSelf if target_user == self unfollow_and_remove(target_user) - create_relationship(active_block_relationships, target_user) + create_relationship(active_block_relationships, target_user).tap do + expire_blocked_user_ids_cache + end end # Unblock an user def unblock(target_user) - destroy_relationship(active_block_relationships, target_user) + destroy_relationship(active_block_relationships, target_user).tap do + expire_blocked_user_ids_cache + end end # Is self blocking target_user? @@ -34,6 +38,16 @@ class User relationship_active?(blocked_users, target_user) end + # Expire the blocked user ids cache + def expire_blocked_user_ids_cache = Rails.cache.delete(cache_key_blocked_user_ids) + + # Cached ids of the blocked users + def blocked_user_ids_cached + Rails.cache.fetch(cache_key_blocked_user_ids, expires_in: 1.hour) do + blocked_user_ids + end + end + private def unfollow_and_remove(target_user) @@ -43,6 +57,8 @@ class User inboxes.joins(:question).where(questions: { user_id: target_user.id, author_is_anonymous: false }).destroy_all ListMember.joins(:list).where(list: { user_id: target_user.id }, user_id: id).destroy_all end + + def cache_key_blocked_user_ids = "#{cache_key}/blocked_user_ids" end end end diff --git a/app/models/user/relationship/mute.rb b/app/models/user/relationship/mute.rb index 08fe286e..58e2b0a1 100644 --- a/app/models/user/relationship/mute.rb +++ b/app/models/user/relationship/mute.rb @@ -16,19 +16,41 @@ class User has_many :muted_by_users, through: :passive_mute_relationships, source: :source end + # Mute an user def mute(target_user) raise Errors::MutingSelf if target_user == self - create_relationship(active_mute_relationships, target_user) + create_relationship(active_mute_relationships, target_user).tap do + expire_muted_user_ids_cache + end end + # Unmute an user def unmute(target_user) - destroy_relationship(active_mute_relationships, target_user) + destroy_relationship(active_mute_relationships, target_user).tap do + expire_muted_user_ids_cache + end end + # Is self muting target_user? def muting?(target_user) relationship_active?(muted_users, target_user) end + + # Expires the muted user ids cache + def expire_muted_user_ids_cache = Rails.cache.delete(cache_key_muted_user_ids) + + # Cached ids of the muted users + def muted_user_ids_cached + Rails.cache.fetch(cache_key_muted_user_ids, expires_in: 1.hour) do + muted_user_ids + end + end + + private + + # Cache key for the muted_user_ids + def cache_key_muted_user_ids = "#{cache_key}/muted_user_ids" end end end diff --git a/app/models/user/timeline_methods.rb b/app/models/user/timeline_methods.rb index a0fa8c55..822f9d8f 100644 --- a/app/models/user/timeline_methods.rb +++ b/app/models/user/timeline_methods.rb @@ -8,7 +8,17 @@ module User::TimelineMethods # @return [ActiveRecord::Relation] the user's timeline def timeline Answer - .where("user_id in (?) OR user_id = ?", following_ids, id) + .then do |query| + blocked_and_muted_user_ids = blocked_user_ids_cached + 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) + end + .where("answers.user_id in (?) OR answers.user_id = ?", following_ids, id) .order(:created_at) .reverse_order .includes(comments: %i[user smiles], question: { user: :profile }, user: [:profile], smiles: [:user]) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2981df53..4097c4a2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -69,7 +69,7 @@ RSpec.describe User, type: :model do describe "email validation" do subject do - FactoryBot.build(:user, email: email).tap(&:validate).errors[:email] + FactoryBot.build(:user, email:).tap(&:validate).errors[:email] end shared_examples_for "valid email" do |example_email| @@ -211,12 +211,134 @@ RSpec.describe User, type: :model do expect(subject).to eq(expected) end end + + context "user follows users with answers to questions from blocked or muted users" do + let(:blocked_user) { FactoryBot.create(:user) } + let(:muted_user) { FactoryBot.create(:user) } + let(:user1) { FactoryBot.create(:user) } + let(:user2) { 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_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_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 + + before do + me.follow user1 + me.follow user2 + end + + it "includes all answers to questions the user follows" 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) + end + + context "when blocking and muting some users" do + before do + me.block blocked_user + me.mute muted_user + end + + 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_to_muted_user) + end + end + end end describe "#cursored_timeline" do let(:last_id) { nil } - subject { me.cursored_timeline(last_id: last_id, size: 3) } + subject { me.cursored_timeline(last_id:, size: 3) } context "user answered nothing and is not following anyone" do include_examples "result is blank"