mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 00:56:01 +08:00

* 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.
490 lines
13 KiB
JavaScript
490 lines
13 KiB
JavaScript
import EmberObject, { get } from "@ember/object";
|
|
import { isEmpty } from "@ember/utils";
|
|
import { NotificationLevels } from "discourse/lib/notification-levels";
|
|
import discourseComputed, { on } from "discourse-common/utils/decorators";
|
|
import PreloadStore from "preload-store";
|
|
import Category from "discourse/models/category";
|
|
import User from "discourse/models/user";
|
|
|
|
function isNew(topic) {
|
|
return (
|
|
topic.last_read_post_number === null &&
|
|
((topic.notification_level !== 0 && !topic.notification_level) ||
|
|
topic.notification_level >= NotificationLevels.TRACKING) &&
|
|
isUnseen(topic)
|
|
);
|
|
}
|
|
|
|
function isUnread(topic) {
|
|
return (
|
|
topic.last_read_post_number !== null &&
|
|
topic.last_read_post_number < topic.highest_post_number &&
|
|
topic.notification_level >= NotificationLevels.TRACKING
|
|
);
|
|
}
|
|
|
|
function isUnseen(topic) {
|
|
return !topic.is_seen;
|
|
}
|
|
|
|
function hasMutedTags(topicTagIds, mutedTagIds) {
|
|
if (!mutedTagIds || !topicTagIds) {
|
|
return false;
|
|
}
|
|
return (
|
|
(Discourse.SiteSettings.remove_muted_tags_from_latest === "always" &&
|
|
topicTagIds.any(tagId => mutedTagIds.includes(tagId))) ||
|
|
(Discourse.SiteSettings.remove_muted_tags_from_latest === "only_muted" &&
|
|
topicTagIds.every(tagId => mutedTagIds.includes(tagId)))
|
|
);
|
|
}
|
|
|
|
const TopicTrackingState = EmberObject.extend({
|
|
messageCount: 0,
|
|
|
|
@on("init")
|
|
_setup() {
|
|
this.unreadSequence = [];
|
|
this.newSequence = [];
|
|
this.states = {};
|
|
},
|
|
|
|
establishChannels() {
|
|
const tracker = this;
|
|
|
|
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") {
|
|
tracker.removeTopic(data.topic_id);
|
|
tracker.incrementMessageCount();
|
|
}
|
|
|
|
if (["new_topic", "latest"].includes(data.message_type)) {
|
|
const muted_category_ids = User.currentProp("muted_category_ids");
|
|
if (
|
|
muted_category_ids &&
|
|
muted_category_ids.includes(data.payload.category_id)
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (["new_topic", "latest"].includes(data.message_type)) {
|
|
const mutedTagIds = User.currentProp("muted_tag_ids");
|
|
if (hasMutedTags(data.payload.topic_tag_ids, mutedTagIds)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (data.message_type === "latest") {
|
|
tracker.notify(data);
|
|
}
|
|
|
|
if (data.message_type === "dismiss_new") {
|
|
Object.keys(tracker.states).forEach(k => {
|
|
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)) {
|
|
tracker.notify(data);
|
|
const old = tracker.states["t" + data.topic_id];
|
|
if (!_.isEqual(old, data.payload)) {
|
|
tracker.states["t" + data.topic_id] = data.payload;
|
|
tracker.notifyPropertyChange("states");
|
|
tracker.incrementMessageCount();
|
|
}
|
|
}
|
|
};
|
|
|
|
this.messageBus.subscribe("/new", process);
|
|
this.messageBus.subscribe("/latest", process);
|
|
if (this.currentUser) {
|
|
this.messageBus.subscribe(
|
|
"/unread/" + this.currentUser.get("id"),
|
|
process
|
|
);
|
|
}
|
|
|
|
this.messageBus.subscribe("/delete", msg => {
|
|
const old = tracker.states["t" + msg.topic_id];
|
|
if (old) {
|
|
old.deleted = true;
|
|
}
|
|
tracker.incrementMessageCount();
|
|
});
|
|
|
|
this.messageBus.subscribe("/recover", msg => {
|
|
const old = tracker.states["t" + msg.topic_id];
|
|
if (old) {
|
|
delete old.deleted;
|
|
}
|
|
tracker.incrementMessageCount();
|
|
});
|
|
},
|
|
|
|
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) {
|
|
if (!topicId || !highestSeen) {
|
|
return;
|
|
}
|
|
const state = this.states["t" + topicId];
|
|
if (
|
|
state &&
|
|
(!state.last_read_post_number ||
|
|
state.last_read_post_number < highestSeen)
|
|
) {
|
|
state.last_read_post_number = highestSeen;
|
|
this.incrementMessageCount();
|
|
}
|
|
},
|
|
|
|
notify(data) {
|
|
if (!this.newIncoming) {
|
|
return;
|
|
}
|
|
if (data.payload && data.payload.archetype === "private_message") {
|
|
return;
|
|
}
|
|
|
|
const filter = this.filter;
|
|
const filterCategory = this.filterCategory;
|
|
const categoryId = data.payload && data.payload.category_id;
|
|
|
|
if (filterCategory && filterCategory.get("id") !== categoryId) {
|
|
const category = categoryId && Category.findById(categoryId);
|
|
if (
|
|
!category ||
|
|
category.get("parentCategory.id") !== filterCategory.get("id")
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (
|
|
["all", "latest", "new"].includes(filter) &&
|
|
data.message_type === "new_topic"
|
|
) {
|
|
this.addIncoming(data.topic_id);
|
|
}
|
|
|
|
if (["all", "unread"].includes(filter) && data.message_type === "unread") {
|
|
const old = this.states["t" + data.topic_id];
|
|
if (!old || old.highest_post_number === old.last_read_post_number) {
|
|
this.addIncoming(data.topic_id);
|
|
}
|
|
}
|
|
|
|
if (filter === "latest" && data.message_type === "latest") {
|
|
this.addIncoming(data.topic_id);
|
|
}
|
|
|
|
this.set("incomingCount", this.newIncoming.length);
|
|
},
|
|
|
|
addIncoming(topicId) {
|
|
if (this.newIncoming.indexOf(topicId) === -1) {
|
|
this.newIncoming.push(topicId);
|
|
}
|
|
},
|
|
|
|
resetTracking() {
|
|
this.newIncoming = [];
|
|
this.set("incomingCount", 0);
|
|
},
|
|
|
|
// track how many new topics came for this filter
|
|
trackIncoming(filter) {
|
|
this.newIncoming = [];
|
|
const split = filter.split("/");
|
|
|
|
if (split.length >= 4) {
|
|
filter = split[split.length - 1];
|
|
// c/cat/subcat/6/l/latest
|
|
var category = Category.findSingleBySlug(
|
|
split.splice(1, split.length - 4).join("/")
|
|
);
|
|
this.set("filterCategory", category);
|
|
} else {
|
|
this.set("filterCategory", null);
|
|
}
|
|
|
|
this.set("filter", filter);
|
|
this.set("incomingCount", 0);
|
|
},
|
|
|
|
@discourseComputed("incomingCount")
|
|
hasIncoming(incomingCount) {
|
|
return incomingCount && incomingCount > 0;
|
|
},
|
|
|
|
removeTopic(topic_id) {
|
|
delete this.states["t" + topic_id];
|
|
},
|
|
|
|
// If we have a cached topic list, we can update it from our tracking information.
|
|
updateTopics(topics) {
|
|
if (isEmpty(topics)) {
|
|
return;
|
|
}
|
|
|
|
const states = this.states;
|
|
topics.forEach(t => {
|
|
const state = states["t" + t.get("id")];
|
|
|
|
if (state) {
|
|
const lastRead = t.get("last_read_post_number");
|
|
const isSeen = t.get("is_seen");
|
|
if (
|
|
lastRead !== state.last_read_post_number ||
|
|
isSeen !== state.is_seen
|
|
) {
|
|
const postsCount = t.get("posts_count");
|
|
let newPosts = postsCount - state.highest_post_number,
|
|
unread = postsCount - state.last_read_post_number;
|
|
|
|
if (newPosts < 0) {
|
|
newPosts = 0;
|
|
}
|
|
if (!state.last_read_post_number) {
|
|
unread = 0;
|
|
}
|
|
if (unread < 0) {
|
|
unread = 0;
|
|
}
|
|
|
|
t.setProperties({
|
|
highest_post_number: state.highest_post_number,
|
|
last_read_post_number: state.last_read_post_number,
|
|
new_posts: newPosts,
|
|
unread: unread,
|
|
is_seen: state.is_seen,
|
|
unseen: !state.last_read_post_number && isUnseen(state)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
sync(list, filter) {
|
|
const tracker = this,
|
|
states = tracker.states;
|
|
|
|
if (!list || !list.topics) {
|
|
return;
|
|
}
|
|
|
|
// compensate for delayed "new" topics
|
|
// client side we know they are not new, server side we think they are
|
|
for (let i = list.topics.length - 1; i >= 0; i--) {
|
|
const state = states["t" + list.topics[i].id];
|
|
if (state && state.last_read_post_number > 0) {
|
|
if (filter === "new") {
|
|
list.topics.splice(i, 1);
|
|
} else {
|
|
list.topics[i].set("unseen", false);
|
|
list.topics[i].set("dont_sync", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
list.topics.forEach(function(topic) {
|
|
const row = tracker.states["t" + topic.id] || {};
|
|
row.topic_id = topic.id;
|
|
row.notification_level = topic.notification_level;
|
|
|
|
if (topic.unseen) {
|
|
row.last_read_post_number = null;
|
|
} else if (topic.unread || topic.new_posts) {
|
|
row.last_read_post_number =
|
|
topic.highest_post_number -
|
|
((topic.unread || 0) + (topic.new_posts || 0));
|
|
} else {
|
|
if (!topic.dont_sync) {
|
|
delete tracker.states["t" + topic.id];
|
|
}
|
|
return;
|
|
}
|
|
|
|
row.highest_post_number = topic.highest_post_number;
|
|
if (topic.category) {
|
|
row.category_id = topic.category.id;
|
|
}
|
|
|
|
tracker.states["t" + topic.id] = row;
|
|
});
|
|
|
|
// Correct missing states, safeguard in case message bus is corrupt
|
|
if ((filter === "new" || filter === "unread") && !list.more_topics_url) {
|
|
const ids = {};
|
|
list.topics.forEach(r => (ids["t" + r.id] = true));
|
|
|
|
Object.keys(tracker.states).forEach(k => {
|
|
// we are good if we are on the list
|
|
if (ids[k]) {
|
|
return;
|
|
}
|
|
|
|
const v = tracker.states[k];
|
|
|
|
if (filter === "unread" && isUnread(v)) {
|
|
// pretend read
|
|
v.last_read_post_number = v.highest_post_number;
|
|
}
|
|
|
|
if (filter === "new" && isNew(v)) {
|
|
// pretend not new
|
|
v.last_read_post_number = 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
this.incrementMessageCount();
|
|
},
|
|
|
|
incrementMessageCount() {
|
|
this.incrementProperty("messageCount");
|
|
},
|
|
|
|
getSubCategoryIds(categoryId) {
|
|
const result = [categoryId];
|
|
const categories = Category.list();
|
|
|
|
for (let i = 0; i < result.length; ++i) {
|
|
for (let j = 0; j < categories.length; ++j) {
|
|
if (result[i] === categories[j].parent_category_id) {
|
|
result[result.length] = categories[j].id;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Set(result);
|
|
},
|
|
|
|
countNew(categoryId) {
|
|
const subcategoryIds = this.getSubCategoryIds(categoryId);
|
|
return _.chain(this.states)
|
|
.filter(isNew)
|
|
.filter(
|
|
topic =>
|
|
topic.archetype !== "private_message" &&
|
|
!topic.deleted &&
|
|
(!categoryId || subcategoryIds.has(topic.category_id))
|
|
)
|
|
.value().length;
|
|
},
|
|
|
|
countUnread(categoryId) {
|
|
const subcategoryIds = this.getSubCategoryIds(categoryId);
|
|
return _.chain(this.states)
|
|
.filter(isUnread)
|
|
.filter(
|
|
topic =>
|
|
topic.archetype !== "private_message" &&
|
|
!topic.deleted &&
|
|
(!categoryId || subcategoryIds.has(topic.category_id))
|
|
)
|
|
.value().length;
|
|
},
|
|
|
|
countCategory(category_id) {
|
|
let sum = 0;
|
|
Object.values(this.states).forEach(topic => {
|
|
if (topic.category_id === category_id && !topic.deleted) {
|
|
sum +=
|
|
topic.last_read_post_number === null ||
|
|
topic.last_read_post_number < topic.highest_post_number
|
|
? 1
|
|
: 0;
|
|
}
|
|
});
|
|
return sum;
|
|
},
|
|
|
|
lookupCount(name, category) {
|
|
if (name === "latest") {
|
|
return (
|
|
this.lookupCount("new", category) + this.lookupCount("unread", category)
|
|
);
|
|
}
|
|
|
|
let categoryId = category ? get(category, "id") : null;
|
|
let categoryName = category ? get(category, "name") : null;
|
|
|
|
if (name === "new") {
|
|
return this.countNew(categoryId);
|
|
} else if (name === "unread") {
|
|
return this.countUnread(categoryId);
|
|
} else {
|
|
categoryName = name.split("/")[1];
|
|
if (categoryName) {
|
|
return this.countCategory(categoryId);
|
|
}
|
|
}
|
|
},
|
|
|
|
loadStates(data) {
|
|
const states = this.states;
|
|
|
|
// I am taking some shortcuts here to avoid 500 gets for a large list
|
|
if (data) {
|
|
data.forEach(topic => {
|
|
states["t" + topic.topic_id] = topic;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
export function startTracking(tracking) {
|
|
const data = PreloadStore.get("topicTrackingStates");
|
|
tracking.loadStates(data);
|
|
tracking.initialStatesLength = data && data.length;
|
|
tracking.establishChannels();
|
|
PreloadStore.remove("topicTrackingStates");
|
|
}
|
|
|
|
export default TopicTrackingState;
|