FEATURE: mute subcategory when parent category is muted (#15966)

When parent category or grandparent category is muted, then category should be muted as well.

Still, it can be overridden by setting individual subcategory notification level.

CategoryUser record is not created, mute for subcategories is purely virtual.
This commit is contained in:
Krzysztof Kotlarek 2022-02-17 10:42:02 +11:00 committed by GitHub
parent effbd6d3e4
commit a7d43cf1ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 11 deletions

View File

@ -3,6 +3,7 @@ import FilterModeMixin from "discourse/mixins/filter-mode";
import NavItem from "discourse/models/nav-item";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { NotificationLevels } from "discourse/lib/notification-levels";
import { inject as service } from "@ember/service";
export default Component.extend(FilterModeMixin, {
@ -22,6 +23,19 @@ export default Component.extend(FilterModeMixin, {
return category && this.currentUser;
},
@discourseComputed("category.notification_level")
categoryNotificationLevel(notificationLevel) {
if (
this.currentUser?.indirectly_muted_category_ids?.includes(
this.category.id
)
) {
return NotificationLevels.MUTED;
} else {
return notificationLevel;
}
},
// don't show tag notification menu on tag intersections
@discourseComputed("tagNotification", "additionalTags")
showTagNotifications(tagNotification, additionalTags) {

View File

@ -324,8 +324,6 @@ const Category = RestModel.extend({
},
setNotification(notification_level) {
this.set("notification_level", notification_level);
User.currentProp(
"muted_category_ids",
User.current().calculateMutedIds(
@ -336,7 +334,16 @@ const Category = RestModel.extend({
);
const url = `/category/${this.id}/notifications`;
return ajax(url, { data: { notification_level }, type: "POST" });
return ajax(url, { data: { notification_level }, type: "POST" }).then(
(data) => {
User.current().set(
"indirectly_muted_category_ids",
data.indirectly_muted_category_ids
);
this.set("notification_level", notification_level);
this.notifyPropertyChange("notification_level");
}
);
},
@discourseComputed("id")

View File

@ -472,8 +472,10 @@ const TopicTrackingState = EmberObject.extend({
const subcategoryIds = noSubcategories
? new Set([categoryId])
: this.getSubCategoryIds(categoryId);
const mutedCategoryIds =
this.currentUser && this.currentUser.muted_category_ids;
const mutedCategoryIds = this.currentUser?.muted_category_ids?.concat(
this.currentUser.indirectly_muted_category_ids
);
let filterFn = type === "new" ? isNew : isUnread;
return Array.from(this.states.values()).filter(
@ -772,10 +774,13 @@ const TopicTrackingState = EmberObject.extend({
}
if (["new_topic", "latest"].includes(data.message_type)) {
const muted_category_ids = User.currentProp("muted_category_ids");
const mutedCategoryIds = User.currentProp("muted_category_ids")?.concat(
User.currentProp("indirectly_muted_category_ids")
);
if (
muted_category_ids &&
muted_category_ids.includes(data.payload.category_id)
mutedCategoryIds &&
mutedCategoryIds.includes(data.payload.category_id)
) {
return;
}

View File

@ -67,7 +67,7 @@
{{!-- don't show category notification menu on tag pages --}}
{{#if showCategoryNotifications}}
{{category-notifications-button
value=category.notification_level
value=categoryNotificationLevel
category=category
onChange=(action "changeCategoryNotificationLevel")
}}

View File

@ -661,6 +661,19 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
);
});
test("topics in indirectly muted categories do not get added to the state", function (assert) {
trackingState.currentUser.setProperties({
muted_category_ids: [],
indirectly_muted_category_ids: [123],
});
publishToMessageBus("/new", newTopicPayload);
assert.strictEqual(
trackingState.findState(222),
undefined,
"the new topic is not in the state"
);
});
test("topics in muted tags do not get added to the state", function (assert) {
trackingState.currentUser.set("muted_tag_ids", [44]);
publishToMessageBus("/new", newTopicPayload);

View File

@ -211,7 +211,7 @@ class CategoriesController < ApplicationController
notification_level = params[:notification_level].to_i
CategoryUser.set_notification_level_for_category(current_user, notification_level, category_id)
render json: success_json
render json: success_json.merge({ indirectly_muted_category_ids: CategoryUser.indirectly_muted_category_ids(current_user) })
end
def destroy

View File

@ -234,6 +234,28 @@ class CategoryUser < ActiveRecord::Base
acc[category_user.category_id] = category_user
end
end
def self.indirectly_muted_category_ids(user)
query = Category.where.not(parent_category_id: nil)
.joins("LEFT JOIN categories categories2 ON categories2.id = categories.parent_category_id")
.joins("LEFT JOIN category_users ON category_users.category_id = categories.id AND category_users.user_id = #{user.id}")
.joins("LEFT JOIN category_users category_users2 ON category_users2.category_id = categories2.id AND category_users2.user_id = #{user.id}")
.where("category_users.id IS NULL")
if SiteSetting.max_category_nesting === 3
query = query
.joins("LEFT JOIN categories categories3 ON categories3.id = categories2.parent_category_id")
.joins("LEFT JOIN category_users category_users3 ON category_users3.category_id = categories3.id AND category_users3.user_id = #{user.id}")
.where("
(category_users2.notification_level = #{notification_levels[:muted]})
OR
(category_users2.id IS NULL AND category_users3.notification_level = #{notification_levels[:muted]})
")
else
query = query.where("category_users2.notification_level = #{notification_levels[:muted]}")
end
query.pluck("categories.id")
end
end
# == Schema Information

View File

@ -28,6 +28,7 @@ class CurrentUserSerializer < BasicUserSerializer
:redirected_to_top,
:custom_fields,
:muted_category_ids,
:indirectly_muted_category_ids,
:regular_category_ids,
:tracked_category_ids,
:watched_first_post_category_ids,
@ -202,6 +203,10 @@ class CurrentUserSerializer < BasicUserSerializer
categories_with_notification_level(:muted)
end
def indirectly_muted_category_ids
CategoryUser.indirectly_muted_category_ids(object)
end
def regular_category_ids
categories_with_notification_level(:regular)
end

View File

@ -846,10 +846,12 @@ class TopicQuery
.references("cu")
.joins("LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id}")
.where("topics.category_id = :category_id
OR COALESCE(category_users.notification_level, :default) <> :muted
OR
(COALESCE(category_users.notification_level, :default) <> :muted AND (topics.category_id IS NULL OR topics.category_id NOT IN(:indirectly_muted_category_ids)))
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])
elsif SiteSetting.mute_all_categories_by_default

View File

@ -268,4 +268,53 @@ describe CategoryUser do
end
end
end
describe "#indirectly_muted_category_ids" do
context "max category nesting 2" do
fab!(:category1) { Fabricate(:category) }
fab!(:category2) { Fabricate(:category, parent_category: category1) }
fab!(:category3) { Fabricate(:category, parent_category: category1) }
it "calculates muted categories based on parent category state" do
expect(CategoryUser.indirectly_muted_category_ids(user)).to eq([])
category_user = CategoryUser.create!(user: user, category: category1, notification_level: CategoryUser.notification_levels[:muted])
expect(CategoryUser.indirectly_muted_category_ids(user)).to contain_exactly(category2.id, category3.id)
CategoryUser.create!(user: user, category: category3, notification_level: CategoryUser.notification_levels[:muted])
expect(CategoryUser.indirectly_muted_category_ids(user)).to contain_exactly(category2.id)
category_user.update(notification_level: CategoryUser.notification_levels[:regular])
expect(CategoryUser.indirectly_muted_category_ids(user)).to eq([])
end
end
context "max category nesting 3" do
let(:category1) { Fabricate(:category) }
let(:category2) { Fabricate(:category, parent_category: category1) }
let(:category3) { Fabricate(:category, parent_category: category2) }
before do
SiteSetting.max_category_nesting = 3
category1
category2
category3
end
it "calculates muted categories based on parent category state" do
expect(CategoryUser.indirectly_muted_category_ids(user)).to eq([])
CategoryUser.create!(user: user, category: category1, notification_level: CategoryUser.notification_levels[:muted])
expect(CategoryUser.indirectly_muted_category_ids(user)).to contain_exactly(category2.id, category3.id)
category_user3 = CategoryUser.create!(user: user, category: category3, notification_level: CategoryUser.notification_levels[:muted])
expect(CategoryUser.indirectly_muted_category_ids(user)).to contain_exactly(category2.id)
category_user3.destroy
category_user2 = CategoryUser.create!(user: user, category: category2, notification_level: CategoryUser.notification_levels[:muted])
expect(CategoryUser.indirectly_muted_category_ids(user)).to contain_exactly(category3.id)
category_user2.update(notification_level: CategoryUser.notification_levels[:regular])
expect(CategoryUser.indirectly_muted_category_ids(user)).to eq([])
end
end
end
end