FEATURE: Ability to dismiss new topics in a specific tag (#11968)

* FEATURE: Ability to dismiss new topics in a specific tag

Follow up of https://github.com/discourse/discourse/pull/11927

Using the same mechanism to disable new topics in a tag.

* FIX: respect when category and tag is selected
This commit is contained in:
Krzysztof Kotlarek 2021-02-09 10:39:30 +11:00 committed by GitHub
parent f9de2f9d23
commit 354ec6694a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 168 additions and 20 deletions

View File

@ -4,6 +4,7 @@ import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import FilterModeMixin from "discourse/mixins/filter-mode"; import FilterModeMixin from "discourse/mixins/filter-mode";
import I18n from "I18n"; import I18n from "I18n";
import NavItem from "discourse/models/nav-item"; import NavItem from "discourse/models/nav-item";
import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed"; import { alias } from "@ember/object/computed";
import bootbox from "bootbox"; import bootbox from "bootbox";
import { queryParams } from "discourse/controllers/discovery-sortable"; import { queryParams } from "discourse/controllers/discovery-sortable";
@ -109,11 +110,34 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
return this.isFilterPage(filter, "unread") && topicsLength > 0; return this.isFilterPage(filter, "unread") && topicsLength > 0;
}, },
@discourseComputed("list.filter", "list.topics.length")
showResetNew(filter, topicsLength) {
return this.isFilterPage(filter, "new") && topicsLength > 0;
},
actions: { actions: {
dismissReadPosts() { dismissReadPosts() {
showModal("dismiss-read", { title: "topics.bulk.dismiss_read" }); showModal("dismiss-read", { title: "topics.bulk.dismiss_read" });
}, },
resetNew() {
const tracked =
(this.router.currentRoute.queryParams["f"] ||
this.router.currentRoute.queryParams["filter"]) === "tracked";
Topic.resetNew(
this.category,
!this.noSubcategories,
tracked,
this.tag
).then(() =>
this.send(
"refresh",
tracked ? { skipResettingParams: ["filter", "f"] } : {}
)
);
},
changeSort(order) { changeSort(order) {
if (order === this.order) { if (order === this.order) {
this.toggleProperty("ascending"); this.toggleProperty("ascending");

View File

@ -106,19 +106,7 @@ const TopicTrackingState = EmberObject.extend({
} }
if (data.message_type === "dismiss_new") { if (data.message_type === "dismiss_new") {
Object.keys(tracker.states).forEach((k) => { tracker.dismissNewTopic(data);
const topic = tracker.states[k];
if (
!data.payload.category_id ||
topic.category_id === parseInt(data.payload.category_id, 0)
) {
tracker.states[k] = Object.assign({}, topic, {
is_seen: true,
});
}
});
tracker.notifyPropertyChange("states");
tracker.incrementMessageCount();
} }
if (["new_topic", "unread", "read"].includes(data.message_type)) { if (["new_topic", "unread", "read"].includes(data.message_type)) {
@ -193,6 +181,23 @@ const TopicTrackingState = EmberObject.extend({
this.currentUser && this.currentUser.set(key, topics); this.currentUser && this.currentUser.set(key, topics);
}, },
dismissNewTopic(data) {
Object.keys(this.states).forEach((k) => {
const topic = this.states[k];
if (
(!data.payload.category_id ||
topic.category_id === parseInt(data.payload.category_id, 10)) &&
(!data.payload.tag_id || topic.tags.includes(data.payload.tag_id))
) {
this.states[k] = Object.assign({}, topic, {
is_seen: true,
});
}
});
this.notifyPropertyChange("states");
this.incrementMessageCount();
},
pruneOldMutedAndUnmutedTopics() { pruneOldMutedAndUnmutedTopics() {
const now = Date.now(); const now = Date.now();
let mutedTopics = this.mutedTopics().filter( let mutedTopics = this.mutedTopics().filter(

View File

@ -766,12 +766,16 @@ Topic.reopenClass({
}); });
}, },
resetNew(category, include_subcategories, tracked = false) { resetNew(category, include_subcategories, tracked = false, tag = false) {
const data = { tracked }; const data = { tracked };
if (category) { if (category) {
data.category_id = category.id; data.category_id = category.id;
data.include_subcategories = include_subcategories; data.include_subcategories = include_subcategories;
} }
if (tag) {
data.tag_id = tag.id;
}
return ajax("/topics/reset-new", { type: "PUT", data }); return ajax("/topics/reset-new", { type: "PUT", data });
}, },

View File

@ -74,6 +74,15 @@
label="topics.bulk.dismiss_button"}} label="topics.bulk.dismiss_button"}}
{{/if}} {{/if}}
{{#if showResetNew}}
{{d-button
class="btn-default dismiss-read"
action=(action "resetNew")
id="dismiss-new"
icon="check"
label="topics.bulk.dismiss_new"}}
{{/if}}
{{#footer-message education=footerEducation message=footerMessage}} {{#footer-message education=footerEducation message=footerMessage}}
{{#link-to "tags"}} {{i18n "topic.browse_all_tags"}}{{/link-to}} {{i18n "or"}} {{#link-to "discovery.latest"}}{{i18n "topic.view_latest_topics"}}{{/link-to}}. {{#link-to "tags"}} {{i18n "topic.browse_all_tags"}}{{/link-to}} {{i18n "or"}} {{#link-to "discovery.latest"}}{{i18n "topic.view_latest_topics"}}{{/link-to}}.
{{/footer-message}} {{/footer-message}}

View File

@ -328,6 +328,58 @@ module("Unit | Model | topic-tracking-state", function (hooks) {
assert.equal(state.countNew(4), 0); assert.equal(state.countNew(4), 0);
}); });
test("dismissNew", function (assert) {
let currentUser = User.create({
username: "chuck",
});
const state = TopicTrackingState.create({ currentUser });
state.states["t112"] = {
last_read_post_number: null,
id: 112,
notification_level: NotificationLevels.TRACKING,
category_id: 1,
is_seen: false,
tags: ["foo"],
};
state.dismissNewTopic({
message_type: "dismiss_new",
topic_id: 112,
payload: { category_id: 2 },
});
assert.equal(state.states["t112"].is_seen, false);
state.dismissNewTopic({
message_type: "dismiss_new",
topic_id: 112,
payload: { category_id: 1 },
});
assert.equal(state.states["t112"].is_seen, true);
state.states["t112"].is_seen = false;
state.dismissNewTopic({
message_type: "dismiss_new",
topic_id: 112,
payload: { tag_id: "bar" },
});
assert.equal(state.states["t112"].is_seen, false);
state.dismissNewTopic({
message_type: "dismiss_new",
topic_id: 112,
payload: { tag_id: "foo" },
});
assert.equal(state.states["t112"].is_seen, true);
state.states["t112"].is_seen = false;
state.dismissNewTopic({
message_type: "dismiss_new",
topic_id: 112,
payload: {},
});
assert.equal(state.states["t112"].is_seen, true);
});
test("mute and unmute topic", function (assert) { test("mute and unmute topic", function (assert) {
let currentUser = User.create({ let currentUser = User.create({
username: "chuck", username: "chuck",

View File

@ -898,10 +898,21 @@ class TopicsController < ApplicationController
if params[:include_subcategories] == 'true' if params[:include_subcategories] == 'true'
category_ids = category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id)) category_ids = category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id))
end end
DismissTopics.new(current_user, Topic.where(category_id: category_ids)).perform!
topic_scope =
if params[:tag_id]
Topic.where(category_id: category_ids).joins(:tags).where(tags: { name: params[:tag_id] })
else
Topic.where(category_id: category_ids)
end
DismissTopics.new(current_user, topic_scope).perform!
category_ids.each do |category_id| category_ids.each do |category_id|
TopicTrackingState.publish_dismiss_new(current_user.id, category_id) TopicTrackingState.publish_dismiss_new(current_user.id, category_id: category_id, tag_id: params[:tag_id])
end end
elsif params[:tag_id].present?
DismissTopics.new(current_user, Topic.joins(:tags).where(tags: { name: params[:tag_id] })).perform!
TopicTrackingState.publish_dismiss_new(current_user.id, tag_id: params[:tag_id])
else else
if params[:tracked].to_s == "true" if params[:tracked].to_s == "true"
topics = TopicQuery.tracked_filter(TopicQuery.new(current_user).new_results, current_user.id) topics = TopicQuery.tracked_filter(TopicQuery.new(current_user).new_results, current_user.id)

View File

@ -215,8 +215,10 @@ class TopicTrackingState
MessageBus.publish(self.unread_channel_key(user_id), message.as_json, user_ids: [user_id]) MessageBus.publish(self.unread_channel_key(user_id), message.as_json, user_ids: [user_id])
end end
def self.publish_dismiss_new(user_id, category_id = nil) def self.publish_dismiss_new(user_id, category_id: nil, tag_id: nil)
payload = category_id ? { category_id: category_id } : {} payload = {}
payload[:category_id] = category_id if category_id
payload[:tag_id] = tag_id if tag_id
message = { message = {
message_type: "dismiss_new", message_type: "dismiss_new",
payload: payload payload: payload

View File

@ -13,7 +13,12 @@ class DismissTopics
private private
def rows def rows
@rows ||= @topics_scope.where("created_at >= ?", since_date).order(created_at: :desc).limit(SiteSetting.max_new_topics).map do |topic| @rows ||= @topics_scope
.joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{@user.id}")
.where("topics.created_at >= ?", since_date)
.where("topic_users.id IS NULL")
.order("topics.created_at DESC")
.limit(SiteSetting.max_new_topics).map do |topic|
{ {
topic_id: topic.id, topic_id: topic.id,
user_id: @user.id, user_id: @user.id,

View File

@ -2873,7 +2873,7 @@ RSpec.describe TopicsController do
it 'dismisses topics for main category' do it 'dismisses topics for main category' do
sign_in(user) sign_in(user)
TopicTrackingState.expects(:publish_dismiss_new).with(user.id, category.id.to_s) TopicTrackingState.expects(:publish_dismiss_new).with(user.id, category_id: category.id.to_s, tag_id: nil)
put "/topics/reset-new.json?category_id=#{category.id}" put "/topics/reset-new.json?category_id=#{category.id}"
@ -2887,6 +2887,36 @@ RSpec.describe TopicsController do
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id).sort).to eq([category_topic.id, subcategory_topic.id].sort) expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id).sort).to eq([category_topic.id, subcategory_topic.id].sort)
end end
end end
context 'tag' do
fab!(:tag) { Fabricate(:tag) }
fab!(:tag_topic) { Fabricate(:topic) }
fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
fab!(:topic) { Fabricate(:topic) }
it 'dismisses topics for tag' do
sign_in(user)
TopicTrackingState.expects(:publish_dismiss_new).with(user.id, tag_id: tag.name)
put "/topics/reset-new.json?tag_id=#{tag.name}"
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([tag_topic.id])
end
end
context 'tag and category' do
fab!(:tag) { Fabricate(:tag) }
fab!(:tag_topic) { Fabricate(:topic) }
fab!(:category) { Fabricate(:category) }
fab!(:tag_and_category_topic) { Fabricate(:topic, category: category) }
fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
fab!(:topic_tag2) { Fabricate(:topic_tag, topic: tag_and_category_topic, tag: tag) }
it 'dismisses topics for tag' do
sign_in(user)
TopicTrackingState.expects(:publish_dismiss_new).with(user.id, tag_id: tag.name, category_id: category.id.to_s)
put "/topics/reset-new.json?tag_id=#{tag.name}&category_id=#{category.id}"
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([tag_and_category_topic.id])
end
end
end end
describe '#feature_stats' do describe '#feature_stats' do

View File

@ -28,6 +28,12 @@ describe DismissTopics do
expect(dismissed_topic_user.created_at).not_to be_nil expect(dismissed_topic_user.created_at).not_to be_nil
end end
it 'respects seen topics' do
Fabricate(:topic_user, user: user, topic: topic1)
Fabricate(:topic_user, user: user, topic: topic2)
expect { described_class.new(user, Topic.all).perform! }.to change { DismissedTopicUser.count }.by(0)
end
it 'respects new_topic_duration_minutes' do it 'respects new_topic_duration_minutes' do
user.user_option.update!(new_topic_duration_minutes: 70) user.user_option.update!(new_topic_duration_minutes: 70)