Files
discourse/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6
2019-11-13 15:34:30 -05:00

436 lines
12 KiB
JavaScript

import { get } from "@ember/object";
import { isEmpty } from "@ember/utils";
import { NotificationLevels } from "discourse/lib/notification-levels";
import {
default as discourseComputed,
on
} from "discourse-common/utils/decorators";
import { defaultHomepage } from "discourse/lib/utilities";
import PreloadStore from "preload-store";
import Category from "discourse/models/category";
import EmberObject from "@ember/object";
import Site from "discourse/models/site";
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)
);
}
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 = EmberObject.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 (["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;
}
}
// fill parent_category_id we need it for counting new/unread
if (data.payload && data.payload.category_id) {
var category = 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 (["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();
});
},
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 (filter === defaultHomepage()) {
const suppressed_from_latest_category_ids = Site.currentProp(
"suppressed_from_latest_category_ids"
);
if (
suppressed_from_latest_category_ids &&
suppressed_from_latest_category_ids.includes(data.payload.category_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/l/latest
var category = 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);
},
@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");
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));
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");
},
countNew(category_id) {
return _.chain(this.states)
.filter(isNew)
.filter(
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)
.filter(isUnread)
.filter(
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;
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 => {
let category = Category.findById(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;