FEATURE: don't display new/unread notification for muted topics (#9482)

* FEATURE: don't display new/unread notification for muted topics

Currently, even if user mute topic, when a new reply to that topic arrives, the user will get "See 1 new or updated topic" message. After clicking on that link, nothing is visible (because the topic is muted)

To solve that problem, we will send background message to all users who recently muted that topic that update is coming and they can ignore the next message about that topic.
This commit is contained in:
Krzysztof Kotlarek 2020-04-23 14:57:35 +10:00 committed by GitHub
parent 54ec9f7009
commit 52c1d7337e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 2 deletions

View File

@ -53,6 +53,17 @@ const TopicTrackingState = EmberObject.extend({
const tracker = this; const tracker = this;
const process = data => { const process = data => {
if (data.message_type === "muted") {
tracker.trackMutedTopic(data.topic_id);
return;
}
tracker.pruneOldMutedTopics();
if (tracker.isMutedTopic(data.topic_id)) {
return;
}
if (data.message_type === "delete") { if (data.message_type === "delete") {
tracker.removeTopic(data.topic_id); tracker.removeTopic(data.topic_id);
tracker.incrementMessageCount(); tracker.incrementMessageCount();
@ -132,6 +143,30 @@ const TopicTrackingState = EmberObject.extend({
}); });
}, },
mutedTopics() {
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);
},
pruneOldMutedTopics() {
const now = Date.now();
let mutedTopics = this.mutedTopics().filter(
mutedTopic => now - mutedTopic.createdAt < 60000
);
this.currentUser && this.currentUser.set("muted_topics", mutedTopics);
},
isMutedTopic(topicId) {
return !!this.mutedTopics().findBy("topicId", topicId);
},
updateSeen(topicId, highestSeen) { updateSeen(topicId, highestSeen) {
if (!topicId || !highestSeen) { if (!topicId || !highestSeen) {
return; return;

View File

@ -12,6 +12,7 @@ class TopicTrackingState
CHANNEL = "/user-tracking" CHANNEL = "/user-tracking"
UNREAD_MESSAGE_TYPE = "unread".freeze UNREAD_MESSAGE_TYPE = "unread".freeze
LATEST_MESSAGE_TYPE = "latest".freeze LATEST_MESSAGE_TYPE = "latest".freeze
MUTED_MESSAGE_TYPE = "muted".freeze
attr_accessor :user_id, attr_accessor :user_id,
:topic_id, :topic_id,
@ -71,6 +72,22 @@ class TopicTrackingState
"/unread/#{user_id}" "/unread/#{user_id}"
end end
def self.publish_muted(post)
user_ids = post.topic.topic_users
.where(notification_level: NotificationLevels.all[:muted])
.joins(:user)
.where("users.last_seen_at > ?", 7.days.ago)
.order("users.last_seen_at DESC")
.limit(100)
.pluck(:user_id)
return if user_ids.blank?
message = {
topic_id: post.topic_id,
message_type: MUTED_MESSAGE_TYPE,
}
MessageBus.publish("/latest", message.as_json, user_ids: user_ids)
end
def self.publish_unread(post) def self.publish_unread(post)
return unless post.topic.regular? return unless post.topic.regular?
# TODO at high scale we are going to have to defer this, # TODO at high scale we are going to have to defer this,

View File

@ -57,7 +57,10 @@ class PostJobsEnqueuer
end end
def after_post_create def after_post_create
TopicTrackingState.publish_unread(@post) if @post.post_number > 1 if @post.post_number > 1
TopicTrackingState.publish_muted(@post)
TopicTrackingState.publish_unread(@post)
end
TopicTrackingState.publish_latest(@topic, @post.whisper?) TopicTrackingState.publish_latest(@topic, @post.whisper?)
Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes, Jobs.enqueue_in(SiteSetting.email_time_window_mins.minutes,

View File

@ -72,6 +72,46 @@ describe TopicTrackingState do
end end
end end
describe '#publish_muted' do
let(:user) do
Fabricate(:user, last_seen_at: Date.today)
end
let(:post) do
create_post(user: user)
end
it 'can correctly publish muted' do
TopicUser.find_by(topic: post.topic, user: post.user).update(notification_level: 0)
messages = MessageBus.track_publish("/latest") do
TopicTrackingState.publish_muted(post)
end
muted_message = messages.find { |message| message.data["message_type"] == "muted" }
expect(muted_message.data["topic_id"]).to eq(topic.id)
expect(muted_message.data["message_type"]).to eq(described_class::MUTED_MESSAGE_TYPE)
end
it 'should not publish any message when notification level is not muted' do
messages = MessageBus.track_publish("/latest") do
TopicTrackingState.publish_muted(post)
end
muted_messages = messages.select { |message| message.data["message_type"] == "muted" }
expect(muted_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: post.topic, user: post.user).update(notification_level: 0)
post.user.update(last_seen_at: 8.days.ago)
messages = MessageBus.track_publish("/latest") do
TopicTrackingState.publish_muted(post)
end
muted_messages = messages.select { |message| message.data["message_type"] == "muted" }
expect(muted_messages).to eq([])
end
end
describe '#publish_private_message' do describe '#publish_private_message' do
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }

View File

@ -2,8 +2,17 @@ import TopicTrackingState from "discourse/models/topic-tracking-state";
import createStore from "helpers/create-store"; import createStore from "helpers/create-store";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import { NotificationLevels } from "discourse/lib/notification-levels"; import { NotificationLevels } from "discourse/lib/notification-levels";
import User from "discourse/models/user";
QUnit.module("model:topic-tracking-state"); QUnit.module("model:topic-tracking-state", {
beforeEach() {
this.clock = sinon.useFakeTimers(new Date(2012, 11, 31, 12, 0).getTime());
},
afterEach() {
this.clock.restore();
}
});
QUnit.test("sync", function(assert) { QUnit.test("sync", function(assert) {
const state = TopicTrackingState.create(); const state = TopicTrackingState.create();
@ -168,3 +177,22 @@ QUnit.test("countNew", assert => {
assert.equal(state.countNew(2), 2); assert.equal(state.countNew(2), 2);
assert.equal(state.countNew(3), 1); assert.equal(state.countNew(3), 1);
}); });
QUnit.test("mute topic", function(assert) {
let currentUser = User.create({
username: "chuck",
muted_category_ids: []
});
const state = TopicTrackingState.create({ currentUser });
state.trackMutedTopic(1);
assert.equal(currentUser.muted_topics[0].topicId, 1);
state.pruneOldMutedTopics();
assert.equal(state.isMutedTopic(1), true);
this.clock.tick(60000);
state.pruneOldMutedTopics();
assert.equal(state.isMutedTopic(1), false);
});