diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 6c777982e4e..f0d6848ece4 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -200,6 +200,10 @@ class PostAlerter DiscourseEvent.trigger(:post_alerter_before_post, post, new_record, notified) + if !SiteSetting.watched_precedence_over_muted + notified = notified + category_or_tag_muters(post.topic) + end + if new_record if post.topic.private_message? # private messages @@ -265,6 +269,18 @@ class PostAlerter .pluck(:user_id) end + def category_or_tag_muters(topic) + User + .joins( + "LEFT JOIN category_users ON users.id = category_users.user_id AND category_users.category_id = #{topic.category_id.to_i} AND category_users.notification_level = #{CategoryUser.notification_levels[:muted].to_i}", + ) + .joins("LEFT JOIN topic_tags ON topic_tags.topic_id = #{topic.id.to_i}") + .joins( + "LEFT JOIN tag_users ON users.id = tag_users.user_id AND tag_users.tag_id = topic_tags.tag_id AND tag_users.notification_level = #{TagUser.notification_levels[:muted].to_i}", + ) + .where("category_users.id IS NOT NULL OR tag_users.id IS NOT NULL") + end + def notify_first_post_watchers(post, user_ids, notified = nil) return [] if user_ids.blank? user_ids.uniq! diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 06919ff3f34..072d4127560 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2380,6 +2380,7 @@ en: remove_muted_tags_from_latest: "Don't show topics tagged only with muted tags in the latest topic list." force_lowercase_tags: "Force all new tags to be entirely lowercase." create_post_for_category_and_tag_changes: "Create a small action post when a topic's category or tags change" + watched_precedence_over_muted: "Notify me about topics in categories or tags I’m watching that also belong to one I have muted" company_name: "Company Name" governing_law: "Governing Law" diff --git a/config/site_settings.yml b/config/site_settings.yml index 55f8be4bf8a..6fb199d151d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2820,6 +2820,10 @@ tags: type: enum default: always enum: RemoveMutedTagsFromLatestSiteSetting + watched_precedence_over_muted: + client: true + default: false + force_lowercase_tags: default: true client: true diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 58fcc7bb1a1..5998a6d1d8a 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -897,24 +897,42 @@ class TopicQuery category_id = get_category_id(opts[:exclude]) if opts if user + watched_tag_ids = + if SiteSetting.watched_precedence_over_muted + TagUser + .where(user: user) + .where("notification_level >= ?", TopicUser.notification_levels[:watching]) + .pluck(:tag_id) + else + [] + end + + # OR watched_topic_tags.id IS NOT NULL", list = - list - .references("cu") - .joins( - "LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}", + list.references("cu").joins( + "LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}", + ) + if watched_tag_ids.present? + list = + list.joins( + "LEFT JOIN topic_tags watched_topic_tags ON watched_topic_tags.topic_id = topics.id AND #{DB.sql_fragment("watched_topic_tags.tag_id IN (?)", watched_tag_ids)}", ) - .where( - "topics.category_id = :category_id + end + + list = + list.where( + "topics.category_id = :category_id OR (COALESCE(category_users.notification_level, :default) <> :muted AND (topics.category_id IS NULL OR topics.category_id NOT IN(:indirectly_muted_category_ids))) + #{watched_tag_ids.present? ? "OR watched_topic_tags.id IS NOT NULL" : ""} OR tu.notification_level > :regular", - category_id: category_id || -1, - default: CategoryUser.default_notification_level, - indirectly_muted_category_ids: - CategoryUser.indirectly_muted_category_ids(user).presence || [-1], - muted: CategoryUser.notification_levels[:muted], - regular: TopicUser.notification_levels[:regular], - ) + category_id: category_id || -1, + default: CategoryUser.default_notification_level, + indirectly_muted_category_ids: + CategoryUser.indirectly_muted_category_ids(user).presence || [-1], + muted: CategoryUser.notification_levels[:muted], + regular: TopicUser.notification_levels[:regular], + ) elsif SiteSetting.mute_all_categories_by_default category_ids = [ SiteSetting.default_categories_watching.split("|"), @@ -971,8 +989,18 @@ class TopicQuery SELECT 1 FROM topic_tags tt WHERE tt.tag_id IN (:tag_ids) - AND tt.topic_id = topics.id)", + AND tt.topic_id = topics.id + #{user && !opts[:skip_categories] ? "AND COALESCE(category_users.notification_level, :regular) < :watching_or_infinite" : ""})", tag_ids: muted_tag_ids, + regular: CategoryUser.notification_levels[:regular], + watching_or_infinite: + ( + if SiteSetting.watched_precedence_over_muted + CategoryUser.notification_levels[:watching] + else + 99 + end + ), ) else list = @@ -981,10 +1009,20 @@ class TopicQuery EXISTS ( SELECT 1 FROM topic_tags tt - WHERE tt.tag_id NOT IN (:tag_ids) - AND tt.topic_id = topics.id + WHERE (tt.tag_id NOT IN (:tag_ids) + AND tt.topic_id = topics.id) + #{user && !opts[:skip_categories] ? "OR COALESCE(category_users.notification_level, :regular) >= :watching_or_infinite" : ""} ) OR NOT EXISTS (SELECT 1 FROM topic_tags tt WHERE tt.topic_id = topics.id)", tag_ids: muted_tag_ids, + regular: CategoryUser.notification_levels[:regular], + watching_or_infinite: + ( + if SiteSetting.watched_precedence_over_muted + CategoryUser.notification_levels[:watching] + else + 99 + end + ), ) end end diff --git a/lib/topic_query/private_message_lists.rb b/lib/topic_query/private_message_lists.rb index 24db40ada31..53bd63c64c3 100644 --- a/lib/topic_query/private_message_lists.rb +++ b/lib/topic_query/private_message_lists.rb @@ -49,7 +49,7 @@ class TopicQuery def list_private_messages_new(user, type = :user) list = filter_private_message_new(user, type) - list = TopicQuery.remove_muted_tags(list, user) + list = TopicQuery.remove_muted_tags(list, user, skip_categories: true) list = remove_dismissed(list, user) create_list(:private_messages, {}, list) diff --git a/spec/lib/topic_query_spec.rb b/spec/lib/topic_query_spec.rb index 8a73a1b22fb..603f9f039e0 100644 --- a/spec/lib/topic_query_spec.rb +++ b/spec/lib/topic_query_spec.rb @@ -2092,4 +2092,73 @@ RSpec.describe TopicQuery do expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic2, topic1].map(&:id)) end end + + describe "precedence of categories and tag setting" do + fab!(:watched_category) do + Fabricate(:category).tap do |category| + CategoryUser.create!( + user: user, + category: category, + notification_level: CategoryUser.notification_levels[:watching], + ) + end + end + fab!(:muted_category) do + Fabricate(:category).tap do |category| + CategoryUser.create!( + user: user, + category: category, + notification_level: CategoryUser.notification_levels[:muted], + ) + end + end + fab!(:watched_tag) do + Fabricate(:tag).tap do |tag| + TagUser.create!( + user: user, + tag: tag, + notification_level: TagUser.notification_levels[:watching], + ) + end + end + fab!(:muted_tag) do + Fabricate(:tag).tap do |tag| + TagUser.create!( + user: user, + tag: tag, + notification_level: TagUser.notification_levels[:muted], + ) + end + end + fab!(:topic) { Fabricate(:topic) } + fab!(:topic_in_watched_category_and_muted_tag) do + Fabricate(:topic, category: watched_category, tags: [muted_tag]) + end + fab!(:topic_in_muted_category_and_watched_tag) do + Fabricate(:topic, category: muted_category, tags: [watched_tag]) + end + fab!(:topic_in_watched_and_muted_tag) { Fabricate(:topic, tags: [watched_tag, muted_tag]) } + fab!(:topic_in_muted_category) { Fabricate(:topic, category: muted_category) } + fab!(:topic_in_muted_tag) { Fabricate(:topic, tags: [muted_tag]) } + + context "when enabled" do + it "returns topics even if category or tag is muted but another tag or category is watched" do + SiteSetting.watched_precedence_over_muted = true + query = TopicQuery.new(user).list_latest + expect(query.topics.map(&:id)).to contain_exactly( + topic.id, + topic_in_watched_category_and_muted_tag.id, + topic_in_muted_category_and_watched_tag.id, + ) + end + end + + context "when disabled" do + it "returns topics without muted category or tag" do + SiteSetting.watched_precedence_over_muted = false + query = TopicQuery.new(user).list_latest + expect(query.topics.map(&:id)).to contain_exactly(topic.id) + end + end + end end diff --git a/spec/models/tag_user_spec.rb b/spec/models/tag_user_spec.rb index 4c5956aab20..fd03174e3a8 100644 --- a/spec/models/tag_user_spec.rb +++ b/spec/models/tag_user_spec.rb @@ -230,6 +230,8 @@ RSpec.describe TagUser do end it "sets notification level to the highest one if there are multiple tags" do + SiteSetting.watched_precedence_over_muted = true + TagUser.create!( user: user, tag: tracked_tag, diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index 69d78c4659d..2e49e23bc45 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -1871,6 +1871,77 @@ RSpec.describe PostAlerter do end end + context "with category and tags" do + fab!(:muted_category) do + Fabricate(:category).tap do |category| + CategoryUser.set_notification_level_for_category( + user, + CategoryUser.notification_levels[:muted], + category.id, + ) + end + end + fab!(:muted_tag) do + Fabricate(:tag).tap do |tag| + TagUser.create!( + user: user, + tag: tag, + notification_level: TagUser.notification_levels[:muted], + ) + end + end + fab!(:watched_tag) do + Fabricate(:tag).tap do |tag| + TagUser.create!( + user: user, + tag: tag, + notification_level: TagUser.notification_levels[:watching], + ) + end + end + fab!(:topic_with_muted_tag_and_watched_category) do + Fabricate(:topic, category: category, tags: [muted_tag]) + end + fab!(:topic_with_muted_category_and_watched_tag) do + Fabricate(:topic, category: muted_category, tags: [watched_tag]) + end + fab!(:topic_with_watched_category) { Fabricate(:topic, category: category) } + fab!(:post) { Fabricate(:post, topic: topic_with_muted_tag_and_watched_category) } + fab!(:post_2) { Fabricate(:post, topic: topic_with_muted_category_and_watched_tag) } + fab!(:post_3) { Fabricate(:post, topic: topic_with_watched_category) } + + before do + CategoryUser.set_notification_level_for_category( + user, + CategoryUser.notification_levels[:watching], + category.id, + ) + end + + it "adds notification when watched_precedence_over_mute setting is true" do + SiteSetting.watched_precedence_over_muted = true + expect { + PostAlerter.post_created(topic_with_muted_tag_and_watched_category.posts.first) + }.to change { Notification.count }.by(1) + expect { + PostAlerter.post_created(topic_with_muted_category_and_watched_tag.posts.first) + }.to change { Notification.count }.by(1) + end + + it "does not add notification when watched_precedence_over_mute setting is false" do + SiteSetting.watched_precedence_over_muted = false + expect { + PostAlerter.post_created(topic_with_muted_tag_and_watched_category.posts.first) + }.not_to change { Notification.count } + expect { + PostAlerter.post_created(topic_with_muted_category_and_watched_tag.posts.first) + }.not_to change { Notification.count } + expect { PostAlerter.post_created(topic_with_watched_category.posts.first) }.to change { + Notification.count + }.by(1) + end + end + context "with on change" do fab!(:user) { Fabricate(:user) } fab!(:other_tag) { Fabricate(:tag) }