There is now an explicit "Delete Bookmark" button in the edit modal. A confirmation is shown before deleting. Along with this, when the bookmarked post icon is clicked the modal is now shown instead of just deleting the bookmark. Also, the "Delete Bookmark" button from the user bookmark list now confirms the action. Add a `d d` shortcut in the modal to delete the bookmark.
889 lines
23 KiB
JavaScript
889 lines
23 KiB
JavaScript
import EmberObject from "@ember/object";
|
|
import { not, notEmpty, equal, and, or } from "@ember/object/computed";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { flushMap } from "discourse/models/store";
|
|
import RestModel from "discourse/models/rest";
|
|
import { propertyEqual, fmt } from "discourse/lib/computed";
|
|
import { longDate } from "discourse/lib/formatter";
|
|
import { isRTL } from "discourse/lib/text-direction";
|
|
import ActionSummary from "discourse/models/action-summary";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { censor } from "pretty-text/censored-words";
|
|
import { emojiUnescape } from "discourse/lib/text";
|
|
import PreloadStore from "preload-store";
|
|
import { userPath } from "discourse/lib/url";
|
|
import discourseComputed, {
|
|
observes,
|
|
on
|
|
} from "discourse-common/utils/decorators";
|
|
import Category from "discourse/models/category";
|
|
import Session from "discourse/models/session";
|
|
import { Promise } from "rsvp";
|
|
import Site from "discourse/models/site";
|
|
import User from "discourse/models/user";
|
|
|
|
export function loadTopicView(topic, args) {
|
|
const data = _.merge({}, args);
|
|
const url = `${Discourse.getURL("/t/")}${topic.id}`;
|
|
const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json";
|
|
|
|
delete data.nearPost;
|
|
delete data.__type;
|
|
delete data.store;
|
|
|
|
return PreloadStore.getAndRemove(`topic_${topic.id}`, () =>
|
|
ajax(jsonUrl, { data })
|
|
).then(json => {
|
|
topic.updateFromJson(json);
|
|
return json;
|
|
});
|
|
}
|
|
|
|
export const ID_CONSTRAINT = /^\d+$/;
|
|
|
|
const Topic = RestModel.extend({
|
|
message: null,
|
|
errorLoading: false,
|
|
|
|
@discourseComputed("last_read_post_number", "highest_post_number")
|
|
visited(lastReadPostNumber, highestPostNumber) {
|
|
// >= to handle case where there are deleted posts at the end of the topic
|
|
return lastReadPostNumber >= highestPostNumber;
|
|
},
|
|
|
|
@discourseComputed("posters.firstObject")
|
|
creator(poster) {
|
|
return poster && poster.user;
|
|
},
|
|
|
|
@discourseComputed("posters.[]")
|
|
lastPoster(posters) {
|
|
let user;
|
|
if (posters && posters.length > 0) {
|
|
const latest = posters.filter(
|
|
p => p.extras && p.extras.indexOf("latest") >= 0
|
|
)[0];
|
|
user = latest;
|
|
}
|
|
return user || posters.firstObject;
|
|
},
|
|
|
|
@discourseComputed("lastPoster")
|
|
lastPosterUser(poster) {
|
|
return poster.user;
|
|
},
|
|
|
|
@discourseComputed("lastPoster")
|
|
lastPosterGroup(poster) {
|
|
return poster.primary_group;
|
|
},
|
|
|
|
@discourseComputed("posters.[]", "participants.[]", "allowed_user_count")
|
|
featuredUsers(posters, participants, allowedUserCount) {
|
|
let users = posters;
|
|
const maxUserCount = 5;
|
|
const posterCount = users.length;
|
|
|
|
if (this.isPrivateMessage && participants && posterCount < maxUserCount) {
|
|
let pushOffset = 0;
|
|
if (posterCount > 1) {
|
|
const lastUser = users[posterCount - 1];
|
|
if (lastUser.extras && lastUser.extras.includes("latest")) {
|
|
pushOffset = 1;
|
|
}
|
|
}
|
|
|
|
const poster_ids = posters.map(p => p.user && p.user.id).filter(id => id);
|
|
participants.some(p => {
|
|
if (!poster_ids.includes(p.user_id)) {
|
|
users.splice(users.length - pushOffset, 0, p);
|
|
if (users.length === maxUserCount) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
if (this.isPrivateMessage && allowedUserCount > maxUserCount) {
|
|
users.splice(maxUserCount - 2, 1); // remove second-last avatar
|
|
users.push({
|
|
moreCount: `+${allowedUserCount - maxUserCount + 1}`
|
|
});
|
|
}
|
|
|
|
return users;
|
|
},
|
|
|
|
@discourseComputed("fancy_title")
|
|
fancyTitle(title) {
|
|
let fancyTitle = censor(
|
|
emojiUnescape(title) || "",
|
|
Site.currentProp("censored_regexp")
|
|
);
|
|
|
|
if (Discourse.SiteSettings.support_mixed_text_direction) {
|
|
const titleDir = isRTL(title) ? "rtl" : "ltr";
|
|
return `<span dir="${titleDir}">${fancyTitle}</span>`;
|
|
}
|
|
return fancyTitle;
|
|
},
|
|
|
|
// returns createdAt if there's no bumped date
|
|
@discourseComputed("bumped_at", "createdAt")
|
|
bumpedAt(bumped_at, createdAt) {
|
|
if (bumped_at) {
|
|
return new Date(bumped_at);
|
|
} else {
|
|
return createdAt;
|
|
}
|
|
},
|
|
|
|
@discourseComputed("bumpedAt", "createdAt")
|
|
bumpedAtTitle(bumpedAt, createdAt) {
|
|
const firstPost = I18n.t("first_post");
|
|
const lastPost = I18n.t("last_post");
|
|
const createdAtDate = longDate(createdAt);
|
|
const bumpedAtDate = longDate(bumpedAt);
|
|
|
|
return I18n.messageFormat("topic.bumped_at_title_MF", {
|
|
FIRST_POST: firstPost,
|
|
CREATED_AT: createdAtDate,
|
|
LAST_POST: lastPost,
|
|
BUMPED_AT: bumpedAtDate
|
|
});
|
|
},
|
|
|
|
@discourseComputed("created_at")
|
|
createdAt(created_at) {
|
|
return new Date(created_at);
|
|
},
|
|
|
|
@discourseComputed
|
|
postStream() {
|
|
return this.store.createRecord("postStream", {
|
|
id: this.id,
|
|
topic: this
|
|
});
|
|
},
|
|
|
|
@discourseComputed("tags")
|
|
visibleListTags(tags) {
|
|
if (!tags || !Discourse.SiteSettings.suppress_overlapping_tags_in_list) {
|
|
return tags;
|
|
}
|
|
|
|
const title = this.title;
|
|
const newTags = [];
|
|
|
|
tags.forEach(function(tag) {
|
|
if (title.toLowerCase().indexOf(tag) === -1) {
|
|
newTags.push(tag);
|
|
}
|
|
});
|
|
|
|
return newTags;
|
|
},
|
|
|
|
@discourseComputed("related_messages")
|
|
relatedMessages(relatedMessages) {
|
|
if (relatedMessages) {
|
|
const store = this.store;
|
|
|
|
return this.set(
|
|
"related_messages",
|
|
relatedMessages.map(st => store.createRecord("topic", st))
|
|
);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("suggested_topics")
|
|
suggestedTopics(suggestedTopics) {
|
|
if (suggestedTopics) {
|
|
const store = this.store;
|
|
|
|
return this.set(
|
|
"suggested_topics",
|
|
suggestedTopics.map(st => store.createRecord("topic", st))
|
|
);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("posts_count")
|
|
replyCount(postsCount) {
|
|
return postsCount - 1;
|
|
},
|
|
|
|
@discourseComputed
|
|
details() {
|
|
return this.store.createRecord("topicDetails", {
|
|
id: this.id,
|
|
topic: this
|
|
});
|
|
},
|
|
|
|
invisible: not("visible"),
|
|
deleted: notEmpty("deleted_at"),
|
|
|
|
@discourseComputed("id")
|
|
searchContext(id) {
|
|
return { type: "topic", id };
|
|
},
|
|
|
|
@on("init")
|
|
@observes("category_id")
|
|
_categoryIdChanged() {
|
|
this.set("category", Category.findById(this.category_id));
|
|
},
|
|
|
|
@observes("categoryName")
|
|
_categoryNameChanged() {
|
|
const categoryName = this.categoryName;
|
|
let category;
|
|
if (categoryName) {
|
|
category = this.site.get("categories").findBy("name", categoryName);
|
|
}
|
|
this.set("category", category);
|
|
},
|
|
|
|
categoryClass: fmt("category.fullSlug", "category-%@"),
|
|
|
|
@discourseComputed("tags")
|
|
tagClasses(tags) {
|
|
return tags && tags.map(t => `tag-${t}`).join(" ");
|
|
},
|
|
|
|
@discourseComputed("url")
|
|
shareUrl(url) {
|
|
const user = User.current();
|
|
const userQueryString = user ? `?u=${user.get("username_lower")}` : "";
|
|
return `${url}${userQueryString}`;
|
|
},
|
|
|
|
printUrl: fmt("url", "%@/print"),
|
|
|
|
@discourseComputed("id", "slug")
|
|
url(id, slug) {
|
|
slug = slug || "";
|
|
if (slug.trim().length === 0) {
|
|
slug = "topic";
|
|
}
|
|
return `${Discourse.getURL("/t/")}${slug}/${id}`;
|
|
},
|
|
|
|
// Helper to build a Url with a post number
|
|
urlForPostNumber(postNumber) {
|
|
let url = this.url;
|
|
if (postNumber > 0) {
|
|
url += `/${postNumber}`;
|
|
}
|
|
return url;
|
|
},
|
|
|
|
@discourseComputed("new_posts", "unread")
|
|
totalUnread(newPosts, unread) {
|
|
const count = (unread || 0) + (newPosts || 0);
|
|
return count > 0 ? count : null;
|
|
},
|
|
|
|
@discourseComputed("last_read_post_number", "url")
|
|
lastReadUrl(lastReadPostNumber) {
|
|
return this.urlForPostNumber(lastReadPostNumber);
|
|
},
|
|
|
|
@discourseComputed("last_read_post_number", "highest_post_number", "url")
|
|
lastUnreadUrl(lastReadPostNumber, highestPostNumber) {
|
|
if (highestPostNumber <= lastReadPostNumber) {
|
|
if (this.get("category.navigate_to_first_post_after_read")) {
|
|
return this.urlForPostNumber(1);
|
|
} else {
|
|
return this.urlForPostNumber(lastReadPostNumber + 1);
|
|
}
|
|
} else {
|
|
return this.urlForPostNumber(lastReadPostNumber + 1);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("highest_post_number", "url")
|
|
lastPostUrl(highestPostNumber) {
|
|
return this.urlForPostNumber(highestPostNumber);
|
|
},
|
|
|
|
@discourseComputed("url")
|
|
firstPostUrl() {
|
|
return this.urlForPostNumber(1);
|
|
},
|
|
|
|
@discourseComputed("url")
|
|
summaryUrl() {
|
|
const summaryQueryString = this.has_summary ? "?filter=summary" : "";
|
|
return `${this.urlForPostNumber(1)}${summaryQueryString}`;
|
|
},
|
|
|
|
@discourseComputed("last_poster.username")
|
|
lastPosterUrl(username) {
|
|
return userPath(username);
|
|
},
|
|
|
|
// 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.
|
|
// So take what the browser has seen into consideration.
|
|
@discourseComputed("new_posts", "id")
|
|
displayNewPosts(newPosts, id) {
|
|
const highestSeen = Session.currentProp("highestSeenByTopic")[id];
|
|
if (highestSeen) {
|
|
const delta = highestSeen - this.last_read_post_number;
|
|
if (delta > 0) {
|
|
let result = newPosts - delta;
|
|
if (result < 0) {
|
|
result = 0;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
return newPosts;
|
|
},
|
|
|
|
@discourseComputed("views")
|
|
viewsHeat(v) {
|
|
if (v >= Discourse.SiteSettings.topic_views_heat_high) {
|
|
return "heatmap-high";
|
|
}
|
|
if (v >= Discourse.SiteSettings.topic_views_heat_medium) {
|
|
return "heatmap-med";
|
|
}
|
|
if (v >= Discourse.SiteSettings.topic_views_heat_low) {
|
|
return "heatmap-low";
|
|
}
|
|
return null;
|
|
},
|
|
|
|
@discourseComputed("archetype")
|
|
archetypeObject(archetype) {
|
|
return Site.currentProp("archetypes").findBy("id", archetype);
|
|
},
|
|
|
|
isPrivateMessage: equal("archetype", "private_message"),
|
|
isBanner: equal("archetype", "banner"),
|
|
|
|
toggleStatus(property) {
|
|
this.toggleProperty(property);
|
|
return this.saveStatus(property, !!this.get(property));
|
|
},
|
|
|
|
saveStatus(property, value, until) {
|
|
if (property === "closed") {
|
|
this.incrementProperty("posts_count");
|
|
}
|
|
return ajax(`${this.url}/status`, {
|
|
type: "PUT",
|
|
data: {
|
|
status: property,
|
|
enabled: !!value,
|
|
until
|
|
}
|
|
});
|
|
},
|
|
|
|
makeBanner() {
|
|
return ajax(`/t/${this.id}/make-banner`, { type: "PUT" }).then(() =>
|
|
this.set("archetype", "banner")
|
|
);
|
|
},
|
|
|
|
removeBanner() {
|
|
return ajax(`/t/${this.id}/remove-banner`, {
|
|
type: "PUT"
|
|
}).then(() => this.set("archetype", "regular"));
|
|
},
|
|
|
|
afterTopicBookmarked(firstPost) {
|
|
if (firstPost) {
|
|
firstPost.set("bookmarked", true);
|
|
if (this.siteSettings.enable_bookmarks_with_reminders) {
|
|
this.set("bookmark_reminder_at", firstPost.bookmark_reminder_at);
|
|
firstPost.set("bookmarked_with_reminder", true);
|
|
}
|
|
return [firstPost.id];
|
|
}
|
|
},
|
|
|
|
firstPost() {
|
|
const postStream = this.postStream;
|
|
let firstPost = postStream.get("posts.firstObject");
|
|
|
|
if (firstPost.post_number === 1) {
|
|
return Promise.resolve(firstPost);
|
|
}
|
|
|
|
const postId = postStream.findPostIdForPostNumber(1);
|
|
|
|
// try loading from identity map first
|
|
firstPost = postStream.findLoadedPost(postId);
|
|
if (firstPost) {
|
|
return Promise.resolve(firstPost);
|
|
}
|
|
|
|
return this.postStream.loadPost(postId);
|
|
},
|
|
|
|
toggleBookmark() {
|
|
if (this.bookmarking) {
|
|
return Promise.resolve();
|
|
}
|
|
this.set("bookmarking", true);
|
|
const bookmark = !this.bookmarked;
|
|
let posts = this.postStream.posts;
|
|
|
|
return this.firstPost().then(firstPost => {
|
|
const toggleBookmarkOnServer = () => {
|
|
if (bookmark) {
|
|
if (this.siteSettings.enable_bookmarks_with_reminders) {
|
|
return firstPost.toggleBookmarkWithReminder().then(response => {
|
|
this.set("bookmarking", false);
|
|
if (response && response.closedWithoutSaving) {
|
|
this.set("bookmarked", false);
|
|
} else {
|
|
return this.afterTopicBookmarked(firstPost);
|
|
}
|
|
});
|
|
} else {
|
|
return ajax(`/t/${this.id}/bookmark`, { type: "PUT" })
|
|
.then(() => {
|
|
this.toggleProperty("bookmarked");
|
|
return this.afterTopicBookmarked(firstPost);
|
|
})
|
|
.catch(popupAjaxError)
|
|
.finally(() => this.set("bookmarking", false));
|
|
}
|
|
} else {
|
|
return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" })
|
|
.then(() => {
|
|
this.toggleProperty("bookmarked");
|
|
this.set("bookmark_reminder_at", null);
|
|
let clearedBookmarkProps = {
|
|
bookmarked_with_reminder: false,
|
|
bookmark_id: null,
|
|
bookmark_name: null,
|
|
bookmark_reminder_at: null
|
|
};
|
|
if (posts) {
|
|
const updated = [];
|
|
posts.forEach(post => {
|
|
if (post.bookmarked) {
|
|
post.set("bookmarked", false);
|
|
updated.push(post.id);
|
|
}
|
|
if (
|
|
this.siteSettings.enable_bookmarks_with_reminders &&
|
|
post.bookmarked_with_reminder
|
|
) {
|
|
post.setProperties(clearedBookmarkProps);
|
|
updated.push(post.id);
|
|
}
|
|
});
|
|
firstPost.setProperties(clearedBookmarkProps);
|
|
return updated;
|
|
}
|
|
})
|
|
.catch(popupAjaxError)
|
|
.finally(() => this.set("bookmarking", false));
|
|
}
|
|
};
|
|
|
|
const unbookmarkedPosts = [];
|
|
if (!bookmark && posts) {
|
|
posts.forEach(
|
|
post =>
|
|
(post.bookmarked || post.bookmarked_with_reminder) &&
|
|
unbookmarkedPosts.push(post)
|
|
);
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
if (unbookmarkedPosts.length > 1) {
|
|
bootbox.confirm(
|
|
I18n.t("bookmarks.confirm_clear"),
|
|
I18n.t("no_value"),
|
|
I18n.t("yes_value"),
|
|
confirmed =>
|
|
confirmed ? toggleBookmarkOnServer().then(resolve) : resolve()
|
|
);
|
|
} else {
|
|
toggleBookmarkOnServer().then(resolve);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
createGroupInvite(group) {
|
|
return ajax(`/t/${this.id}/invite-group`, {
|
|
type: "POST",
|
|
data: { group }
|
|
});
|
|
},
|
|
|
|
createInvite(user, group_names, custom_message) {
|
|
return ajax(`/t/${this.id}/invite`, {
|
|
type: "POST",
|
|
data: { user, group_names, custom_message }
|
|
});
|
|
},
|
|
|
|
generateInviteLink(email, groupNames, topicId) {
|
|
return ajax("/invites/link", {
|
|
type: "POST",
|
|
data: { email, group_names: groupNames, topic_id: topicId }
|
|
});
|
|
},
|
|
|
|
// Delete this topic
|
|
destroy(deleted_by) {
|
|
return ajax(`/t/${this.id}`, {
|
|
data: { context: window.location.pathname },
|
|
type: "DELETE"
|
|
})
|
|
.then(() => {
|
|
this.setProperties({
|
|
deleted_at: new Date(),
|
|
deleted_by: deleted_by,
|
|
"details.can_delete": false,
|
|
"details.can_recover": true
|
|
});
|
|
})
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
// Recover this topic if deleted
|
|
recover() {
|
|
this.setProperties({
|
|
deleted_at: null,
|
|
deleted_by: null,
|
|
"details.can_delete": true,
|
|
"details.can_recover": false
|
|
});
|
|
return ajax(`/t/${this.id}/recover`, {
|
|
data: { context: window.location.pathname },
|
|
type: "PUT"
|
|
});
|
|
},
|
|
|
|
// Update our attributes from a JSON result
|
|
updateFromJson(json) {
|
|
const keys = Object.keys(json);
|
|
if (!json.view_hidden) {
|
|
this.details.updateFromJson(json.details);
|
|
|
|
keys.removeObjects(["details", "post_stream"]);
|
|
|
|
if (json.published_page) {
|
|
this.set(
|
|
"publishedPage",
|
|
this.store.createRecord("published-page", json.published_page)
|
|
);
|
|
}
|
|
}
|
|
keys.forEach(key => this.set(key, json[key]));
|
|
},
|
|
|
|
reload() {
|
|
return ajax(`/t/${this.id}`, { type: "GET" }).then(topic_json =>
|
|
this.updateFromJson(topic_json)
|
|
);
|
|
},
|
|
|
|
isPinnedUncategorized: and("pinned", "category.isUncategorizedCategory"),
|
|
|
|
clearPin() {
|
|
// Clear the pin optimistically from the object
|
|
this.setProperties({ pinned: false, unpinned: true });
|
|
|
|
ajax(`/t/${this.id}/clear-pin`, {
|
|
type: "PUT"
|
|
}).then(null, () => {
|
|
// On error, put the pin back
|
|
this.setProperties({ pinned: true, unpinned: false });
|
|
});
|
|
},
|
|
|
|
togglePinnedForUser() {
|
|
if (this.pinned) {
|
|
this.clearPin();
|
|
} else {
|
|
this.rePin();
|
|
}
|
|
},
|
|
|
|
rePin() {
|
|
// Clear the pin optimistically from the object
|
|
this.setProperties({ pinned: true, unpinned: false });
|
|
|
|
ajax(`/t/${this.id}/re-pin`, {
|
|
type: "PUT"
|
|
}).then(null, () => {
|
|
// On error, put the pin back
|
|
this.setProperties({ pinned: true, unpinned: false });
|
|
});
|
|
},
|
|
|
|
@discourseComputed("excerpt")
|
|
escapedExcerpt(excerpt) {
|
|
return emojiUnescape(excerpt);
|
|
},
|
|
|
|
hasExcerpt: notEmpty("excerpt"),
|
|
|
|
@discourseComputed("excerpt")
|
|
excerptTruncated(excerpt) {
|
|
return excerpt && excerpt.substr(excerpt.length - 8, 8) === "…";
|
|
},
|
|
|
|
readLastPost: propertyEqual("last_read_post_number", "highest_post_number"),
|
|
canClearPin: and("pinned", "readLastPost"),
|
|
canEditTags: or("details.can_edit", "details.can_edit_tags"),
|
|
|
|
archiveMessage() {
|
|
this.set("archiving", true);
|
|
const promise = ajax(`/t/${this.id}/archive-message`, {
|
|
type: "PUT"
|
|
});
|
|
|
|
promise
|
|
.then(msg => {
|
|
this.set("message_archived", true);
|
|
if (msg && msg.group_name) {
|
|
this.set("inboxGroupName", msg.group_name);
|
|
}
|
|
})
|
|
.finally(() => this.set("archiving", false));
|
|
|
|
return promise;
|
|
},
|
|
|
|
moveToInbox() {
|
|
this.set("archiving", true);
|
|
const promise = ajax(`/t/${this.id}/move-to-inbox`, { type: "PUT" });
|
|
|
|
promise
|
|
.then(msg => {
|
|
this.set("message_archived", false);
|
|
if (msg && msg.group_name) {
|
|
this.set("inboxGroupName", msg.group_name);
|
|
}
|
|
})
|
|
.finally(() => this.set("archiving", false));
|
|
|
|
return promise;
|
|
},
|
|
|
|
publish() {
|
|
return ajax(`/t/${this.id}/publish`, {
|
|
type: "PUT",
|
|
data: this.getProperties("destination_category_id")
|
|
})
|
|
.then(() => this.set("destination_category_id", null))
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
updateDestinationCategory(categoryId) {
|
|
this.set("destination_category_id", categoryId);
|
|
return ajax(`/t/${this.id}/shared-draft`, {
|
|
type: "PUT",
|
|
data: { category_id: categoryId }
|
|
});
|
|
},
|
|
|
|
convertTopic(type, opts) {
|
|
let args = { type: "PUT" };
|
|
if (opts && opts.categoryId) {
|
|
args.data = { category_id: opts.categoryId };
|
|
}
|
|
return ajax(`/t/${this.id}/convert-topic/${type}`, args);
|
|
},
|
|
|
|
resetBumpDate() {
|
|
return ajax(`/t/${this.id}/reset-bump-date`, { type: "PUT" }).catch(
|
|
popupAjaxError
|
|
);
|
|
},
|
|
|
|
updateTags(tags) {
|
|
if (!tags || tags.length === 0) {
|
|
tags = [""];
|
|
}
|
|
|
|
return ajax(`/t/${this.id}/tags`, {
|
|
type: "PUT",
|
|
data: { tags: tags }
|
|
});
|
|
}
|
|
});
|
|
|
|
Topic.reopenClass({
|
|
NotificationLevel: {
|
|
WATCHING: 3,
|
|
TRACKING: 2,
|
|
REGULAR: 1,
|
|
MUTED: 0
|
|
},
|
|
|
|
createActionSummary(result) {
|
|
if (result.actions_summary) {
|
|
const lookup = EmberObject.create();
|
|
result.actions_summary = result.actions_summary.map(a => {
|
|
a.post = result;
|
|
a.actionType = Site.current().postActionTypeById(a.id);
|
|
const actionSummary = ActionSummary.create(a);
|
|
lookup.set(a.actionType.get("name_key"), actionSummary);
|
|
return actionSummary;
|
|
});
|
|
result.set("actionByName", lookup);
|
|
}
|
|
},
|
|
|
|
update(topic, props) {
|
|
// We support `category_id` and `categoryId` for compatibility
|
|
if (typeof props.categoryId !== "undefined") {
|
|
props.category_id = props.categoryId;
|
|
delete props.categoryId;
|
|
}
|
|
|
|
// Make sure we never change the category for private messages
|
|
if (topic.get("isPrivateMessage")) {
|
|
delete props.category_id;
|
|
}
|
|
|
|
return ajax(topic.get("url"), {
|
|
type: "PUT",
|
|
data: JSON.stringify(props),
|
|
contentType: "application/json"
|
|
}).then(result => {
|
|
// The title can be cleaned up server side
|
|
props.title = result.basic_topic.title;
|
|
props.fancy_title = result.basic_topic.fancy_title;
|
|
topic.setProperties(props);
|
|
});
|
|
},
|
|
|
|
create() {
|
|
const result = this._super.apply(this, arguments);
|
|
this.createActionSummary(result);
|
|
return result;
|
|
},
|
|
|
|
// Load a topic, but accepts a set of filters
|
|
find(topicId, opts) {
|
|
let url = Discourse.getURL("/t/") + topicId;
|
|
if (opts.nearPost) {
|
|
url += `/${opts.nearPost}`;
|
|
}
|
|
|
|
const data = {};
|
|
if (opts.postsAfter) {
|
|
data.posts_after = opts.postsAfter;
|
|
}
|
|
if (opts.postsBefore) {
|
|
data.posts_before = opts.postsBefore;
|
|
}
|
|
if (opts.trackVisit) {
|
|
data.track_visit = true;
|
|
}
|
|
|
|
// Add username filters if we have them
|
|
if (opts.userFilters && opts.userFilters.length > 0) {
|
|
data.username_filters = [];
|
|
opts.userFilters.forEach(function(username) {
|
|
data.username_filters.push(username);
|
|
});
|
|
}
|
|
|
|
// Add the summary of filter if we have it
|
|
if (opts.summary === true) {
|
|
data.summary = true;
|
|
}
|
|
|
|
// Check the preload store. If not, load it via JSON
|
|
return ajax(`${url}.json`, { data });
|
|
},
|
|
|
|
changeOwners(topicId, opts) {
|
|
const promise = ajax(`/t/${topicId}/change-owner`, {
|
|
type: "POST",
|
|
data: opts
|
|
}).then(result => {
|
|
if (result.success) return result;
|
|
promise.reject(new Error("error changing ownership of posts"));
|
|
});
|
|
return promise;
|
|
},
|
|
|
|
changeTimestamp(topicId, timestamp) {
|
|
const promise = ajax(`/t/${topicId}/change-timestamp`, {
|
|
type: "PUT",
|
|
data: { timestamp }
|
|
}).then(result => {
|
|
if (result.success) return result;
|
|
promise.reject(new Error("error updating timestamp of topic"));
|
|
});
|
|
return promise;
|
|
},
|
|
|
|
bulkOperation(topics, operation) {
|
|
return ajax("/topics/bulk", {
|
|
type: "PUT",
|
|
data: {
|
|
topic_ids: topics.map(t => t.get("id")),
|
|
operation
|
|
}
|
|
});
|
|
},
|
|
|
|
bulkOperationByFilter(filter, operation, categoryId, options) {
|
|
let data = { filter, operation };
|
|
|
|
if (options && options.includeSubcategories) {
|
|
data.include_subcategories = true;
|
|
}
|
|
|
|
if (categoryId) data.category_id = categoryId;
|
|
return ajax("/topics/bulk", {
|
|
type: "PUT",
|
|
data
|
|
});
|
|
},
|
|
|
|
resetNew(category, include_subcategories) {
|
|
const data = category
|
|
? { category_id: category.id, include_subcategories }
|
|
: {};
|
|
return ajax("/topics/reset-new", { type: "PUT", data });
|
|
},
|
|
|
|
idForSlug(slug) {
|
|
return ajax(`/t/id_for/${slug}`);
|
|
}
|
|
});
|
|
|
|
function moveResult(result) {
|
|
if (result.success) {
|
|
// We should be hesitant to flush the map but moving ids is one rare case
|
|
flushMap();
|
|
return result;
|
|
}
|
|
throw new Error("error moving posts topic");
|
|
}
|
|
|
|
export function movePosts(topicId, data) {
|
|
return ajax(`/t/${topicId}/move-posts`, { type: "POST", data }).then(
|
|
moveResult
|
|
);
|
|
}
|
|
|
|
export function mergeTopic(topicId, data) {
|
|
return ajax(`/t/${topicId}/merge-topic`, { type: "POST", data }).then(
|
|
moveResult
|
|
);
|
|
}
|
|
|
|
export default Topic;
|