mirror of
https://github.com/discourse/discourse.git
synced 2025-05-28 01:56:58 +08:00
436 lines
12 KiB
JavaScript
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;
|