diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index 983818cd40e..b121ae7dd4e 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -55,17 +55,24 @@ const TopicTrackingState = EmberObject.extend({ const tracker = this; const process = (data) => { - if (data.message_type === "muted") { - tracker.trackMutedTopic(data.topic_id); + if (["muted", "unmuted"].includes(data.message_type)) { + tracker.trackMutedOrUnmutedTopic(data); return; } - tracker.pruneOldMutedTopics(); + tracker.pruneOldMutedAndUnmutedTopics(); if (tracker.isMutedTopic(data.topic_id)) { return; } + if ( + this.siteSettings.mute_all_categories_by_default && + !tracker.isUnmutedTopic(data.topic_id) + ) { + return; + } + if (data.message_type === "delete") { tracker.removeTopic(data.topic_id); tracker.incrementMessageCount(); @@ -166,26 +173,47 @@ const TopicTrackingState = EmberObject.extend({ return (this.currentUser && this.currentUser.muted_topics) || []; }, - trackMutedTopic(topicId) { - let mutedTopics = this.mutedTopics().concat({ - topicId: topicId, - createdAt: Date.now(), - }); - this.currentUser && this.currentUser.set("muted_topics", mutedTopics); + unmutedTopics() { + return (this.currentUser && this.currentUser.unmuted_topics) || []; }, - pruneOldMutedTopics() { + trackMutedOrUnmutedTopic(data) { + let topics, key; + if (data.message_type === "muted") { + key = "muted_topics"; + topics = this.mutedTopics(); + } else { + key = "unmuted_topics"; + topics = this.unmutedTopics(); + } + topics = topics.concat({ + topicId: data.topic_id, + createdAt: Date.now(), + }); + this.currentUser && this.currentUser.set(key, topics); + }, + + pruneOldMutedAndUnmutedTopics() { const now = Date.now(); let mutedTopics = this.mutedTopics().filter( (mutedTopic) => now - mutedTopic.createdAt < 60000 ); - this.currentUser && this.currentUser.set("muted_topics", mutedTopics); + let unmutedTopics = this.unmutedTopics().filter( + (unmutedTopic) => now - unmutedTopic.createdAt < 60000 + ); + this.currentUser && + this.currentUser.set("muted_topics", mutedTopics) && + this.currentUser.set("unmuted_topics", unmutedTopics); }, isMutedTopic(topicId) { return !!this.mutedTopics().findBy("topicId", topicId); }, + isUnmutedTopic(topicId) { + return !!this.unmutedTopics().findBy("topicId", topicId); + }, + updateSeen(topicId, highestSeen) { if (!topicId || !highestSeen) { return; diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js index 761f955f6f3..96ef3b2e3d6 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js @@ -327,7 +327,7 @@ module("Unit | Model | topic-tracking-state", function (hooks) { assert.equal(state.countNew(4), 0); }); - test("mute topic", function (assert) { + test("mute and unmute topic", function (assert) { let currentUser = User.create({ username: "chuck", muted_category_ids: [], @@ -335,14 +335,19 @@ module("Unit | Model | topic-tracking-state", function (hooks) { const state = TopicTrackingState.create({ currentUser }); - state.trackMutedTopic(1); + state.trackMutedOrUnmutedTopic({ topic_id: 1, message_type: "muted" }); assert.equal(currentUser.muted_topics[0].topicId, 1); - state.pruneOldMutedTopics(); + state.trackMutedOrUnmutedTopic({ topic_id: 2, message_type: "unmuted" }); + assert.equal(currentUser.unmuted_topics[0].topicId, 2); + + state.pruneOldMutedAndUnmutedTopics(); assert.equal(state.isMutedTopic(1), true); + assert.equal(state.isUnmutedTopic(2), true); this.clock.tick(60000); - state.pruneOldMutedTopics(); + state.pruneOldMutedAndUnmutedTopics(); assert.equal(state.isMutedTopic(1), false); + assert.equal(state.isUnmutedTopic(2), false); }); }); diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index e018d961328..e0fa47889bd 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -12,6 +12,7 @@ class TopicTrackingState UNREAD_MESSAGE_TYPE = "unread" LATEST_MESSAGE_TYPE = "latest" MUTED_MESSAGE_TYPE = "muted" + UNMUTED_MESSAGE_TYPE = "unmuted" attr_accessor :user_id, :topic_id, @@ -104,6 +105,25 @@ class TopicTrackingState MessageBus.publish("/latest", message.as_json, user_ids: user_ids) end + def self.publish_unmuted(topic) + return if !SiteSetting.mute_all_categories_by_default + user_ids = User + .joins(DB.sql_fragment("LEFT JOIN category_users ON category_users.user_id = users.id AND category_users.category_id = :category_id", category_id: topic.category_id)) + .joins(DB.sql_fragment("LEFT JOIN topic_users ON topic_users.user_id = users.id AND topic_users.topic_id = :topic_id", topic_id: topic.id)) + .joins("LEFT JOIN tag_users ON tag_users.user_id = users.id AND tag_users.tag_id IN (#{topic.tag_ids.join(",").presence || 'NULL'})") + .where("category_users.notification_level > 0 OR topic_users.notification_level > 0 OR tag_users.notification_level > 0") + .where("users.last_seen_at > ?", 7.days.ago) + .order("users.last_seen_at DESC") + .limit(100) + .pluck(:id) + return if user_ids.blank? + message = { + topic_id: topic.id, + message_type: UNMUTED_MESSAGE_TYPE, + } + MessageBus.publish("/latest", message.as_json, user_ids: user_ids) + end + def self.publish_unread(post) return unless post.topic.regular? # TODO at high scale we are going to have to defer this, diff --git a/lib/post_jobs_enqueuer.rb b/lib/post_jobs_enqueuer.rb index afdd3cd1c93..cd80eaa61ca 100644 --- a/lib/post_jobs_enqueuer.rb +++ b/lib/post_jobs_enqueuer.rb @@ -57,6 +57,7 @@ class PostJobsEnqueuer end def after_post_create + TopicTrackingState.publish_unmuted(@post.topic) if @post.post_number > 1 TopicTrackingState.publish_muted(@post.topic) TopicTrackingState.publish_unread(@post) diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index fc521f9e848..46797a76da1 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -537,6 +537,7 @@ class PostRevisor return if bypass_bump? || !is_last_post? @topic.update_column(:bumped_at, Time.now) TopicTrackingState.publish_muted(@topic) + TopicTrackingState.publish_unmuted(@topic) TopicTrackingState.publish_latest(@topic) end diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb index b73093e3a35..e84f8e85ffb 100644 --- a/spec/models/topic_tracking_state_spec.rb +++ b/spec/models/topic_tracking_state_spec.rb @@ -112,6 +112,59 @@ describe TopicTrackingState do end end + describe '#publish_unmuted' do + let(:user) do + Fabricate(:user, last_seen_at: Date.today) + end + let(:second_user) do + Fabricate(:user, last_seen_at: Date.today) + end + let(:third_user) do + Fabricate(:user, last_seen_at: Date.today) + end + let(:post) do + create_post(user: user) + end + + it 'can correctly publish unmuted' do + Fabricate(:topic_tag, topic: topic) + SiteSetting.mute_all_categories_by_default = true + TopicUser.find_by(topic: topic, user: post.user).update(notification_level: 1) + CategoryUser.create!(category: topic.category, user: second_user, notification_level: 1) + TagUser.create!(tag: topic.tags.first, user: third_user, notification_level: 1) + TagUser.create!(tag: topic.tags.first, user: Fabricate(:user), notification_level: 0) + messages = MessageBus.track_publish("/latest") do + TopicTrackingState.publish_unmuted(topic) + end + + unmuted_message = messages.find { |message| message.data["message_type"] == "unmuted" } + expect(unmuted_message.user_ids.sort).to eq([user.id, second_user.id, third_user.id].sort) + expect(unmuted_message.data["topic_id"]).to eq(topic.id) + expect(unmuted_message.data["message_type"]).to eq(described_class::UNMUTED_MESSAGE_TYPE) + end + + it 'should not publish any message when notification level is not muted' do + SiteSetting.mute_all_categories_by_default = true + TopicUser.find_by(topic: topic, user: post.user).update(notification_level: 0) + messages = MessageBus.track_publish("/latest") do + TopicTrackingState.publish_unmuted(topic) + end + unmuted_messages = messages.select { |message| message.data["message_type"] == "unmuted" } + + expect(unmuted_messages).to eq([]) + end + + it 'should not publish any message when the user was not seen in the last 7 days' do + TopicUser.find_by(topic: topic, user: post.user).update(notification_level: 1) + post.user.update(last_seen_at: 8.days.ago) + messages = MessageBus.track_publish("/latest") do + TopicTrackingState.publish_unmuted(topic) + end + unmuted_messages = messages.select { |message| message.data["message_type"] == "unmuted" } + expect(unmuted_messages).to eq([]) + end + end + describe '#publish_private_message' do fab!(:admin) { Fabricate(:admin) }