In the past we used suppress_from_homepage, it had mixed semantics it would remove from category list if category list was on home and unconditionally remove from latest. New setting explicitly only removes from latest list but leaves the category list alond
384 lines
11 KiB
JavaScript
384 lines
11 KiB
JavaScript
import { NotificationLevels } from 'discourse/lib/notification-levels';
|
|
import computed from "ember-addons/ember-computed-decorators";
|
|
import { on } from "ember-addons/ember-computed-decorators";
|
|
import { defaultHomepage } from 'discourse/lib/utilities';
|
|
import PreloadStore from 'preload-store';
|
|
|
|
function isNew(topic) {
|
|
return topic.last_read_post_number === null &&
|
|
((topic.notification_level !== 0 && !topic.notification_level) ||
|
|
topic.notification_level >= NotificationLevels.TRACKING);
|
|
}
|
|
|
|
function isUnread(topic) {
|
|
return topic.last_read_post_number !== null &&
|
|
topic.last_read_post_number < topic.highest_post_number &&
|
|
topic.notification_level >= NotificationLevels.TRACKING;
|
|
}
|
|
|
|
const TopicTrackingState = Discourse.Model.extend({
|
|
messageCount: 0,
|
|
|
|
@on("init")
|
|
_setup() {
|
|
this.unreadSequence = [];
|
|
this.newSequence = [];
|
|
this.states = {};
|
|
},
|
|
|
|
establishChannels() {
|
|
const tracker = this;
|
|
|
|
const process = data => {
|
|
if (data.message_type === "delete") {
|
|
tracker.removeTopic(data.topic_id);
|
|
tracker.incrementMessageCount();
|
|
}
|
|
|
|
if (data.message_type === "new_topic" || data.message_type === "latest") {
|
|
const muted_category_ids = Discourse.User.currentProp("muted_category_ids");
|
|
if (_.include(muted_category_ids, data.payload.category_id)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// fill parent_category_id we need it for counting new/unread
|
|
if (data.payload && data.payload.category_id) {
|
|
var category = Discourse.Category.findById(data.payload.category_id);
|
|
|
|
if (category && category.parent_category_id) {
|
|
data.payload.parent_category_id = category.parent_category_id;
|
|
}
|
|
}
|
|
|
|
if (data.message_type === "latest"){
|
|
tracker.notify(data);
|
|
}
|
|
|
|
if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") {
|
|
tracker.notify(data);
|
|
const old = tracker.states["t" + data.topic_id];
|
|
|
|
// don't add tracking state for read stuff that was not tracked in first place
|
|
if (old || data.message_type !== "read") {
|
|
if (!_.isEqual(old, data.payload)) {
|
|
tracker.states["t" + data.topic_id] = data.payload;
|
|
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();
|
|
});
|
|
},
|
|
|
|
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.get("filter");
|
|
const filterCategory = this.get("filterCategory");
|
|
const categoryId = data.payload && data.payload.category_id;
|
|
|
|
if (filterCategory && filterCategory.get("id") !== categoryId) {
|
|
const category = categoryId && Discourse.Category.findById(categoryId);
|
|
if (!category || category.get("parentCategory.id") !== filterCategory.get('id')) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (filter === defaultHomepage()) {
|
|
const suppressed_from_latest_category_ids = Discourse.Site.currentProp("suppressed_from_latest_category_ids");
|
|
if (_.include(suppressed_from_latest_category_ids, data.payload.category_id)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") {
|
|
this.addIncoming(data.topic_id);
|
|
}
|
|
|
|
if ((filter === "all" || filter === "unread") && 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/l/latest
|
|
var category = Discourse.Category.findSingleBySlug(split.splice(1,split.length - 3).join('/'));
|
|
this.set("filterCategory", category);
|
|
} else {
|
|
this.set("filterCategory", null);
|
|
}
|
|
|
|
this.set("filter", filter);
|
|
this.set("incomingCount", 0);
|
|
},
|
|
|
|
@computed("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 (Em.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');
|
|
if (lastRead !== state.last_read_post_number) {
|
|
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,
|
|
unseen: !state.last_read_post_number
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
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);
|
|
|
|
_.each(tracker.states, (v, k) => {
|
|
|
|
// we are good if we are on the list
|
|
if (ids[k]) { return; }
|
|
|
|
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");
|
|
},
|
|
|
|
countNew(category_id) {
|
|
return _.chain(this.states)
|
|
.where(isNew)
|
|
.where(topic =>
|
|
topic.archetype !== "private_message" &&
|
|
!topic.deleted && (
|
|
topic.category_id === category_id ||
|
|
topic.parent_category_id === category_id ||
|
|
!category_id)
|
|
)
|
|
.value()
|
|
.length;
|
|
},
|
|
|
|
resetNew() {
|
|
Object.keys(this.states).forEach(id => {
|
|
if (this.states[id].last_read_post_number === null) {
|
|
delete this.states[id];
|
|
}
|
|
});
|
|
},
|
|
|
|
countUnread(category_id) {
|
|
return _.chain(this.states)
|
|
.where(isUnread)
|
|
.where(topic =>
|
|
topic.archetype !== "private_message" &&
|
|
!topic.deleted && (
|
|
topic.category_id === category_id ||
|
|
topic.parent_category_id === category_id ||
|
|
!category_id)
|
|
)
|
|
.value()
|
|
.length;
|
|
},
|
|
|
|
countCategory(category_id) {
|
|
let sum = 0;
|
|
_.each(this.states, function(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 ? Em.get(category, "id") : null;
|
|
let categoryName = category ? Em.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;
|
|
const idMap = Discourse.Category.idMap();
|
|
|
|
// I am taking some shortcuts here to avoid 500 gets for a large list
|
|
if (data) {
|
|
_.each(data,topic => {
|
|
var category = idMap[topic.category_id];
|
|
if (category && category.parent_category_id) {
|
|
topic.parent_category_id = category.parent_category_id;
|
|
}
|
|
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;
|