REFACTOR: topic model (#7020)

This commit is contained in:
Joffrey JAFFEUX 2019-02-19 10:13:46 +01:00 committed by GitHub
parent 15fd875855
commit 8a4cd15e46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,30 +1,34 @@
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { flushMap } from "discourse/models/store"; import { flushMap } from "discourse/models/store";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import { propertyEqual } from "discourse/lib/computed"; import { propertyEqual, fmt } from "discourse/lib/computed";
import { longDate } from "discourse/lib/formatter"; import { longDate } from "discourse/lib/formatter";
import { isRTL } from "discourse/lib/text-direction"; import { isRTL } from "discourse/lib/text-direction";
import computed from "ember-addons/ember-computed-decorators";
import ActionSummary from "discourse/models/action-summary"; import ActionSummary from "discourse/models/action-summary";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { censor } from "pretty-text/censored-words"; import { censor } from "pretty-text/censored-words";
import { emojiUnescape } from "discourse/lib/text"; import { emojiUnescape } from "discourse/lib/text";
import PreloadStore from "preload-store"; import PreloadStore from "preload-store";
import { userPath } from "discourse/lib/url"; import { userPath } from "discourse/lib/url";
import {
default as computed,
observes,
on
} from "ember-addons/ember-computed-decorators";
export function loadTopicView(topic, args) { export function loadTopicView(topic, args) {
const topicId = topic.get("id"); const topicId = topic.get("id");
const data = _.merge({}, args); const data = _.merge({}, args);
const url = Discourse.getURL("/t/") + topicId; const url = `${Discourse.getURL("/t/")}${topicId}`;
const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json"; const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json";
delete data.nearPost; delete data.nearPost;
delete data.__type; delete data.__type;
delete data.store; delete data.store;
return PreloadStore.getAndRemove(`topic_${topicId}`, () => { return PreloadStore.getAndRemove(`topic_${topicId}`, () =>
return ajax(jsonUrl, { data }); ajax(jsonUrl, { data })
}).then(json => { ).then(json => {
topic.updateFromJson(json); topic.updateFromJson(json);
return json; return json;
}); });
@ -101,44 +105,44 @@ const Topic = RestModel.extend({
); );
if (Discourse.SiteSettings.support_mixed_text_direction) { if (Discourse.SiteSettings.support_mixed_text_direction) {
let titleDir = isRTL(title) ? "rtl" : "ltr"; const titleDir = isRTL(title) ? "rtl" : "ltr";
return `<span dir="${titleDir}">${fancyTitle}</span>`; return `<span dir="${titleDir}">${fancyTitle}</span>`;
} }
return fancyTitle; return fancyTitle;
}, },
// returns createdAt if there's no bumped date // returns createdAt if there's no bumped date
bumpedAt: function() { @computed("bumped_at", "createdAt")
const bumpedAt = this.get("bumped_at"); bumpedAt(bumped_at, createdAt) {
if (bumpedAt) { if (bumped_at) {
return new Date(bumpedAt); return new Date(bumped_at);
} else { } else {
return this.get("createdAt"); return createdAt;
} }
}.property("bumped_at", "createdAt"), },
bumpedAtTitle: function() { @computed("bumpedAt", "createdAt")
return ( bumpedAtTitle(bumpedAt, createdAt) {
I18n.t("first_post") + const firstPost = I18n.t("first_post");
": " + const lastPost = I18n.t("last_post");
longDate(this.get("createdAt")) + const createdAtDate = longDate(createdAt);
"\n" + const bumpedAtDate = longDate(bumpedAt);
I18n.t("last_post") +
": " +
longDate(this.get("bumpedAt"))
);
}.property("bumpedAt"),
createdAt: function() { return `${firstPost}: ${createdAtDate}\n${lastPost}: ${bumpedAtDate}`;
return new Date(this.get("created_at")); },
}.property("created_at"),
postStream: function() { @computed("created_at")
createdAt(created_at) {
return new Date(created_at);
},
@computed
postStream() {
return this.store.createRecord("postStream", { return this.store.createRecord("postStream", {
id: this.get("id"), id: this.get("id"),
topic: this topic: this
}); });
}.property(), },
@computed("tags") @computed("tags")
visibleListTags(tags) { visibleListTags(tags) {
@ -165,9 +169,7 @@ const Topic = RestModel.extend({
return this.set( return this.set(
"related_messages", "related_messages",
relatedMessages.map(st => { relatedMessages.map(st => store.createRecord("topic", st))
return store.createRecord("topic", st);
})
); );
} }
}, },
@ -179,115 +181,116 @@ const Topic = RestModel.extend({
return this.set( return this.set(
"suggested_topics", "suggested_topics",
suggestedTopics.map(st => { suggestedTopics.map(st => store.createRecord("topic", st))
return store.createRecord("topic", st);
})
); );
} }
}, },
replyCount: function() { @computed("posts_count")
return this.get("posts_count") - 1; replyCount(postsCount) {
}.property("posts_count"), return postsCount - 1;
},
details: function() { @computed
details() {
return this.store.createRecord("topicDetails", { return this.store.createRecord("topicDetails", {
id: this.get("id"), id: this.get("id"),
topic: this topic: this
}); });
}.property(), },
invisible: Ember.computed.not("visible"), invisible: Ember.computed.not("visible"),
deleted: Ember.computed.notEmpty("deleted_at"), deleted: Ember.computed.notEmpty("deleted_at"),
searchContext: function() { @computed("id")
return { type: "topic", id: this.get("id") }; searchContext(id) {
}.property("id"), return { type: "topic", id };
},
_categoryIdChanged: function() { @on("init")
@observes("category_id")
_categoryIdChanged() {
this.set("category", Discourse.Category.findById(this.get("category_id"))); this.set("category", Discourse.Category.findById(this.get("category_id")));
} },
.observes("category_id")
.on("init"),
_categoryNameChanged: function() { @observes("categoryName")
_categoryNameChanged() {
const categoryName = this.get("categoryName"); const categoryName = this.get("categoryName");
let category; let category;
if (categoryName) { if (categoryName) {
category = this.site.get("categories").findBy("name", categoryName); category = this.site.get("categories").findBy("name", categoryName);
} }
this.set("category", category); this.set("category", category);
}.observes("categoryName"),
categoryClass: function() {
return "category-" + this.get("category.fullSlug");
}.property("category.fullSlug"),
shareUrl: function() {
const user = Discourse.User.current();
return this.get("url") + (user ? "?u=" + user.get("username_lower") : "");
}.property("url"),
@computed("url")
printUrl(url) {
return url + "/print";
}, },
url: function() { categoryClass: fmt("category.fullSlug", "category-%@"),
let slug = this.get("slug") || "";
@computed("url")
shareUrl(url) {
const user = Discourse.User.current();
const userQueryString = user ? `?u=${user.get("username_lower")}` : "";
return `${url}${userQueryString}`;
},
printUrl: fmt("url", "%@/print"),
@computed("id", "slug")
url(id, slug) {
slug = slug || "";
if (slug.trim().length === 0) { if (slug.trim().length === 0) {
slug = "topic"; slug = "topic";
} }
return Discourse.getURL("/t/") + slug + "/" + this.get("id"); return `${Discourse.getURL("/t/")}${slug}/${id}`;
}.property("id", "slug"), },
// Helper to build a Url with a post number // Helper to build a Url with a post number
urlForPostNumber(postNumber) { urlForPostNumber(postNumber) {
let url = this.get("url"); let url = this.get("url");
if (postNumber && postNumber > 0) { if (postNumber && postNumber > 0) {
url += "/" + postNumber; url += `/${postNumber}`;
} }
return url; return url;
}, },
totalUnread: function() { @computed("new_posts", "unread")
const count = (this.get("unread") || 0) + (this.get("new_posts") || 0); totalUnread(newPosts, unread) {
const count = (unread || 0) + (newPosts || 0);
return count > 0 ? count : null; return count > 0 ? count : null;
}.property("new_posts", "unread"), },
lastReadUrl: function() { @computed("last_read_post_number", "url")
return this.urlForPostNumber(this.get("last_read_post_number")); lastReadUrl(lastReadPostNumber) {
}.property("url", "last_read_post_number"), return this.urlForPostNumber(lastReadPostNumber);
},
lastUnreadUrl: function() { @computed("last_read_post_number", "highest_post_number", "url")
const highest = this.get("highest_post_number"); lastUnreadUrl(lastReadPostNumber, highestPostNumber) {
const lastRead = this.get("last_read_post_number"); if (highestPostNumber <= lastReadPostNumber) {
if (highest <= lastRead) {
if (this.get("category.navigate_to_first_post_after_read")) { if (this.get("category.navigate_to_first_post_after_read")) {
return this.urlForPostNumber(1); return this.urlForPostNumber(1);
} else { } else {
return this.urlForPostNumber(lastRead + 1); return this.urlForPostNumber(lastReadPostNumber + 1);
} }
} else { } else {
return this.urlForPostNumber(lastRead + 1); return this.urlForPostNumber(lastReadPostNumber + 1);
} }
}.property("url", "last_read_post_number", "highest_post_number"), },
lastPostUrl: function() { @computed("highest_post_number", "url")
return this.urlForPostNumber(this.get("highest_post_number")); lastPostUrl(highestPostNumber) {
}.property("url", "highest_post_number"), return this.urlForPostNumber(highestPostNumber);
},
firstPostUrl: function() { @computed("url")
firstPostUrl() {
return this.urlForPostNumber(1); return this.urlForPostNumber(1);
}.property("url"), },
summaryUrl: function() { @computed("url")
return ( summaryUrl() {
this.urlForPostNumber(1) + const summaryQueryString = this.get("has_summary") ? "?filter=summary" : "";
(this.get("has_summary") ? "?filter=summary" : "") return `${this.urlForPostNumber(1)}${summaryQueryString}`;
); },
}.property("url"),
@computed("last_poster.username") @computed("last_poster.username")
lastPosterUrl(username) { lastPosterUrl(username) {
@ -297,39 +300,40 @@ const Topic = RestModel.extend({
// The amount of new posts to display. It might be different than what the server // The amount of new posts to display. It might be different than what the server
// tells us if we are still asynchronously flushing our "recently read" data. // tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration. // So take what the browser has seen into consideration.
displayNewPosts: function() { @computed("new_posts", "id")
const highestSeen = Discourse.Session.currentProp("highestSeenByTopic")[ displayNewPosts(newPosts, id) {
this.get("id") const highestSeen = Discourse.Session.currentProp("highestSeenByTopic")[id];
];
if (highestSeen) { if (highestSeen) {
let delta = highestSeen - this.get("last_read_post_number"); const delta = highestSeen - this.get("last_read_post_number");
if (delta > 0) { if (delta > 0) {
let result = this.get("new_posts") - delta; let result = newPosts - delta;
if (result < 0) { if (result < 0) {
result = 0; result = 0;
} }
return result; return result;
} }
} }
return this.get("new_posts"); return newPosts;
}.property("new_posts", "id"), },
viewsHeat: function() { @computed("views")
const v = this.get("views"); viewsHeat(v) {
if (v >= Discourse.SiteSettings.topic_views_heat_high) if (v >= Discourse.SiteSettings.topic_views_heat_high) {
return "heatmap-high"; return "heatmap-high";
if (v >= Discourse.SiteSettings.topic_views_heat_medium) }
if (v >= Discourse.SiteSettings.topic_views_heat_medium) {
return "heatmap-med"; return "heatmap-med";
if (v >= Discourse.SiteSettings.topic_views_heat_low) return "heatmap-low"; }
if (v >= Discourse.SiteSettings.topic_views_heat_low) {
return "heatmap-low";
}
return null; return null;
}.property("views"), },
archetypeObject: function() { @computed("archetype")
return Discourse.Site.currentProp("archetypes").findBy( archetypeObject(archetype) {
"id", return Discourse.Site.currentProp("archetypes").findBy("id", archetype);
this.get("archetype") },
);
}.property("archetype"),
isPrivateMessage: Ember.computed.equal("archetype", "private_message"), isPrivateMessage: Ember.computed.equal("archetype", "private_message"),
isBanner: Ember.computed.equal("archetype", "banner"), isBanner: Ember.computed.equal("archetype", "banner"),
@ -343,32 +347,26 @@ const Topic = RestModel.extend({
if (property === "closed") { if (property === "closed") {
this.incrementProperty("posts_count"); this.incrementProperty("posts_count");
} }
return ajax(this.get("url") + "/status", { return ajax(`${this.get("url")}/status`, {
type: "PUT", type: "PUT",
data: { data: {
status: property, status: property,
enabled: !!value, enabled: !!value,
until: until until
} }
}); });
}, },
makeBanner() { makeBanner() {
const self = this; return ajax(`/t/${this.get("id")}/make-banner`, { type: "PUT" }).then(() =>
return ajax("/t/" + this.get("id") + "/make-banner", { type: "PUT" }).then( this.set("archetype", "banner")
function() {
self.set("archetype", "banner");
}
); );
}, },
removeBanner() { removeBanner() {
const self = this; return ajax(`/t/${this.get("id")}/remove-banner`, {
return ajax("/t/" + this.get("id") + "/remove-banner", {
type: "PUT" type: "PUT"
}).then(function() { }).then(() => this.set("archetype", "regular"));
self.set("archetype", "regular");
});
}, },
toggleBookmark() { toggleBookmark() {
@ -432,23 +430,23 @@ const Topic = RestModel.extend({
}, },
createGroupInvite(group) { createGroupInvite(group) {
return ajax("/t/" + this.get("id") + "/invite-group", { return ajax(`/t/${this.get("id")}/invite-group`, {
type: "POST", type: "POST",
data: { group } data: { group }
}); });
}, },
createInvite(user, group_names, custom_message) { createInvite(user, group_names, custom_message) {
return ajax("/t/" + this.get("id") + "/invite", { return ajax(`/t/${this.get("id")}/invite`, {
type: "POST", type: "POST",
data: { user, group_names, custom_message } data: { user, group_names, custom_message }
}); });
}, },
generateInviteLink: function(email, groupNames, topicId) { generateInviteLink(email, groupNames, topicId) {
return ajax("/invites/link", { return ajax("/invites/link", {
type: "POST", type: "POST",
data: { email: email, group_names: groupNames, topic_id: topicId } data: { email, group_names: groupNames, topic_id: topicId }
}); });
}, },
@ -492,28 +490,25 @@ const Topic = RestModel.extend({
}, },
reload() { reload() {
return ajax(`/t/${this.get("id")}`, { type: "GET" }).then(topic_json => { return ajax(`/t/${this.get("id")}`, { type: "GET" }).then(topic_json =>
this.updateFromJson(topic_json); this.updateFromJson(topic_json)
}); );
}, },
isPinnedUncategorized: function() { isPinnedUncategorized: Ember.computed.and(
return this.get("pinned") && this.get("category.isUncategorizedCategory"); "pinned",
}.property("pinned", "category.isUncategorizedCategory"), "category.isUncategorizedCategory"
),
clearPin() { clearPin() {
const topic = this;
// Clear the pin optimistically from the object // Clear the pin optimistically from the object
topic.set("pinned", false); this.setProperties({ pinned: false, unpinned: true });
topic.set("unpinned", true);
ajax("/t/" + this.get("id") + "/clear-pin", { ajax(`/t/${this.get("id")}/clear-pin`, {
type: "PUT" type: "PUT"
}).then(null, function() { }).then(null, () => {
// On error, put the pin back // On error, put the pin back
topic.set("pinned", true); this.setProperties({ pinned: true, unpinned: false });
topic.set("unpinned", false);
}); });
}, },
@ -526,18 +521,14 @@ const Topic = RestModel.extend({
}, },
rePin() { rePin() {
const topic = this;
// Clear the pin optimistically from the object // Clear the pin optimistically from the object
topic.set("pinned", true); this.setProperties({ pinned: true, unpinned: false });
topic.set("unpinned", false);
ajax("/t/" + this.get("id") + "/re-pin", { ajax(`/t/${this.get("id")}/re-pin`, {
type: "PUT" type: "PUT"
}).then(null, function() { }).then(null, () => {
// On error, put the pin back // On error, put the pin back
topic.set("pinned", true); this.setProperties({ pinned: true, unpinned: false });
topic.set("unpinned", false);
}); });
}, },
@ -548,17 +539,19 @@ const Topic = RestModel.extend({
hasExcerpt: Ember.computed.notEmpty("excerpt"), hasExcerpt: Ember.computed.notEmpty("excerpt"),
excerptTruncated: function() { @computed("excerpt")
const e = this.get("excerpt"); excerptTruncated(excerpt) {
return e && e.substr(e.length - 8, 8) === "&hellip;"; return excerpt && excerpt.substr(excerpt.length - 8, 8) === "&hellip;";
}.property("excerpt"), },
readLastPost: propertyEqual("last_read_post_number", "highest_post_number"), readLastPost: propertyEqual("last_read_post_number", "highest_post_number"),
canClearPin: Ember.computed.and("pinned", "readLastPost"), canClearPin: Ember.computed.and("pinned", "readLastPost"),
archiveMessage() { archiveMessage() {
this.set("archiving", true); this.set("archiving", true);
var promise = ajax(`/t/${this.get("id")}/archive-message`, { type: "PUT" }); const promise = ajax(`/t/${this.get("id")}/archive-message`, {
type: "PUT"
});
promise promise
.then(msg => { .then(msg => {
@ -574,7 +567,7 @@ const Topic = RestModel.extend({
moveToInbox() { moveToInbox() {
this.set("archiving", true); this.set("archiving", true);
var promise = ajax(`/t/${this.get("id")}/move-to-inbox`, { type: "PUT" }); const promise = ajax(`/t/${this.get("id")}/move-to-inbox`, { type: "PUT" });
promise promise
.then(msg => { .then(msg => {
@ -593,9 +586,7 @@ const Topic = RestModel.extend({
type: "PUT", type: "PUT",
data: this.getProperties("destination_category_id") data: this.getProperties("destination_category_id")
}) })
.then(() => { .then(() => this.set("destination_category_id", null))
this.set("destination_category_id", null);
})
.catch(popupAjaxError); .catch(popupAjaxError);
}, },
@ -609,9 +600,7 @@ const Topic = RestModel.extend({
convertTopic(type) { convertTopic(type) {
return ajax(`/t/${this.get("id")}/convert-topic/${type}`, { type: "PUT" }) return ajax(`/t/${this.get("id")}/convert-topic/${type}`, { type: "PUT" })
.then(() => { .then(() => window.location.reload())
window.location.reload();
})
.catch(popupAjaxError); .catch(popupAjaxError);
}, },
@ -633,7 +622,7 @@ Topic.reopenClass({
createActionSummary(result) { createActionSummary(result) {
if (result.actions_summary) { if (result.actions_summary) {
const lookup = Ember.Object.create(); const lookup = Ember.Object.create();
result.actions_summary = result.actions_summary.map(function(a) { result.actions_summary = result.actions_summary.map(a => {
a.post = result; a.post = result;
a.actionType = Discourse.Site.current().postActionTypeById(a.id); a.actionType = Discourse.Site.current().postActionTypeById(a.id);
const actionSummary = ActionSummary.create(a); const actionSummary = ActionSummary.create(a);
@ -664,13 +653,11 @@ Topic.reopenClass({
Object.keys(props).forEach(function(k) { Object.keys(props).forEach(function(k) {
const v = props[k]; const v = props[k];
if (v instanceof Array && v.length === 0) { if (v instanceof Array && v.length === 0) {
props[k + "_empty_array"] = true; props[`${k}_empty_array`] = true;
} }
}); });
return ajax(topic.get("url"), { type: "PUT", data: props }).then(function( return ajax(topic.get("url"), { type: "PUT", data: props }).then(result => {
result
) {
// The title can be cleaned up server side // The title can be cleaned up server side
props.title = result.basic_topic.title; props.title = result.basic_topic.title;
props.fancy_title = result.basic_topic.fancy_title; props.fancy_title = result.basic_topic.fancy_title;
@ -688,7 +675,7 @@ Topic.reopenClass({
find(topicId, opts) { find(topicId, opts) {
let url = Discourse.getURL("/t/") + topicId; let url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) { if (opts.nearPost) {
url += "/" + opts.nearPost; url += `/${opts.nearPost}`;
} }
const data = {}; const data = {};
@ -716,14 +703,14 @@ Topic.reopenClass({
} }
// Check the preload store. If not, load it via JSON // Check the preload store. If not, load it via JSON
return ajax(url + ".json", { data: data }); return ajax(`${url}.json`, { data });
}, },
changeOwners(topicId, opts) { changeOwners(topicId, opts) {
const promise = ajax("/t/" + topicId + "/change-owner", { const promise = ajax(`/t/${topicId}/change-owner`, {
type: "POST", type: "POST",
data: opts data: opts
}).then(function(result) { }).then(result => {
if (result.success) return result; if (result.success) return result;
promise.reject(new Error("error changing ownership of posts")); promise.reject(new Error("error changing ownership of posts"));
}); });
@ -731,10 +718,10 @@ Topic.reopenClass({
}, },
changeTimestamp(topicId, timestamp) { changeTimestamp(topicId, timestamp) {
const promise = ajax("/t/" + topicId + "/change-timestamp", { const promise = ajax(`/t/${topicId}/change-timestamp`, {
type: "PUT", type: "PUT",
data: { timestamp: timestamp } data: { timestamp }
}).then(function(result) { }).then(result => {
if (result.success) return result; if (result.success) return result;
promise.reject(new Error("error updating timestamp of topic")); promise.reject(new Error("error updating timestamp of topic"));
}); });
@ -745,20 +732,18 @@ Topic.reopenClass({
return ajax("/topics/bulk", { return ajax("/topics/bulk", {
type: "PUT", type: "PUT",
data: { data: {
topic_ids: topics.map(function(t) { topic_ids: topics.map(t => t.get("id")),
return t.get("id"); operation
}),
operation: operation
} }
}); });
}, },
bulkOperationByFilter(filter, operation, categoryId) { bulkOperationByFilter(filter, operation, categoryId) {
const data = { filter: filter, operation: operation }; const data = { filter, operation };
if (categoryId) data["category_id"] = categoryId; if (categoryId) data.category_id = categoryId;
return ajax("/topics/bulk", { return ajax("/topics/bulk", {
type: "PUT", type: "PUT",
data: data data
}); });
}, },
@ -767,7 +752,7 @@ Topic.reopenClass({
}, },
idForSlug(slug) { idForSlug(slug) {
return ajax("/t/id_for/" + slug); return ajax(`/t/id_for/${slug}`);
} }
}); });
@ -781,13 +766,13 @@ function moveResult(result) {
} }
export function movePosts(topicId, data) { export function movePosts(topicId, data) {
return ajax("/t/" + topicId + "/move-posts", { type: "POST", data }).then( return ajax(`/t/${topicId}/move-posts`, { type: "POST", data }).then(
moveResult moveResult
); );
} }
export function mergeTopic(topicId, data) { export function mergeTopic(topicId, data) {
return ajax("/t/" + topicId + "/merge-topic", { type: "POST", data }).then( return ajax(`/t/${topicId}/merge-topic`, { type: "POST", data }).then(
moveResult moveResult
); );
} }