Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated! This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality: * We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them. * Posts now have a new button in their actions menu that has the icon of an actual book * Clicking the button opens the new bookmark modal. * Both name and the reminder type are optional. * If you close the modal without doing anything, the bookmark is saved with no reminder. * If you click the Cancel button, no bookmark is saved at all. * All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you). * If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark. * A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked. This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
1481 lines
40 KiB
JavaScript
1481 lines
40 KiB
JavaScript
import { isEmpty } from "@ember/utils";
|
|
import { or, and, not, alias } from "@ember/object/computed";
|
|
import EmberObject from "@ember/object";
|
|
import { next } from "@ember/runloop";
|
|
import { scheduleOnce } from "@ember/runloop";
|
|
import { inject } from "@ember/controller";
|
|
import Controller from "@ember/controller";
|
|
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
|
import Composer from "discourse/models/composer";
|
|
import DiscourseURL from "discourse/lib/url";
|
|
import Post from "discourse/models/post";
|
|
import Quote from "discourse/lib/quote";
|
|
import QuoteState from "discourse/lib/quote-state";
|
|
import Topic from "discourse/models/topic";
|
|
import discourseDebounce from "discourse/lib/debounce";
|
|
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import {
|
|
default as discourseComputed,
|
|
observes
|
|
} from "discourse-common/utils/decorators";
|
|
import { extractLinkMeta } from "discourse/lib/render-topic-featured-link";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
|
import { userPath } from "discourse/lib/url";
|
|
import showModal from "discourse/lib/show-modal";
|
|
import TopicTimer from "discourse/models/topic-timer";
|
|
import { Promise } from "rsvp";
|
|
|
|
let customPostMessageCallbacks = {};
|
|
|
|
export function resetCustomPostMessageCallbacks() {
|
|
customPostMessageCallbacks = {};
|
|
}
|
|
|
|
export function registerCustomPostMessageCallback(type, callback) {
|
|
if (customPostMessageCallbacks[type]) {
|
|
throw new Error(`Error ${type} is an already registered post message!`);
|
|
}
|
|
|
|
customPostMessageCallbacks[type] = callback;
|
|
}
|
|
|
|
export default Controller.extend(bufferedProperty("model"), {
|
|
composer: inject(),
|
|
application: inject(),
|
|
multiSelect: false,
|
|
selectedPostIds: null,
|
|
editingTopic: false,
|
|
queryParams: ["filter", "username_filters"],
|
|
loadedAllPosts: or(
|
|
"model.postStream.loadedAllPosts",
|
|
"model.postStream.loadingLastPost"
|
|
),
|
|
enteredAt: null,
|
|
enteredIndex: null,
|
|
retrying: false,
|
|
userTriggeredProgress: null,
|
|
_progressIndex: null,
|
|
hasScrolled: null,
|
|
username_filters: null,
|
|
filter: null,
|
|
quoteState: null,
|
|
|
|
canRemoveTopicFeaturedLink: and(
|
|
"canEditTopicFeaturedLink",
|
|
"buffered.featured_link"
|
|
),
|
|
|
|
updateQueryParams() {
|
|
this.setProperties(this.get("model.postStream.streamFilters"));
|
|
},
|
|
|
|
@observes("model.title", "category")
|
|
_titleChanged() {
|
|
const title = this.get("model.title");
|
|
if (!isEmpty(title)) {
|
|
// force update lazily loaded titles
|
|
this.send("refreshTitle");
|
|
}
|
|
},
|
|
|
|
@discourseComputed("model.details.can_create_post")
|
|
embedQuoteButton(canCreatePost) {
|
|
return (
|
|
canCreatePost &&
|
|
this.currentUser &&
|
|
this.currentUser.get("enable_quoting")
|
|
);
|
|
},
|
|
|
|
@discourseComputed("model.postStream.loaded", "model.category_id")
|
|
showSharedDraftControls(loaded, categoryId) {
|
|
let draftCat = this.site.shared_drafts_category_id;
|
|
return loaded && draftCat && categoryId && draftCat === categoryId;
|
|
},
|
|
|
|
@discourseComputed("site.mobileView", "model.posts_count")
|
|
showSelectedPostsAtBottom(mobileView, postsCount) {
|
|
return mobileView && postsCount > 3;
|
|
},
|
|
|
|
@discourseComputed(
|
|
"model.postStream.posts",
|
|
"model.postStream.postsWithPlaceholders"
|
|
)
|
|
postsToRender(posts, postsWithPlaceholders) {
|
|
return this.capabilities.isAndroid ? posts : postsWithPlaceholders;
|
|
},
|
|
|
|
@discourseComputed("model.postStream.loadingFilter")
|
|
androidLoading(loading) {
|
|
return this.capabilities.isAndroid && loading;
|
|
},
|
|
|
|
@discourseComputed("model")
|
|
pmPath(topic) {
|
|
return this.currentUser && this.currentUser.pmPath(topic);
|
|
},
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
|
|
this.appEvents.on("post:show-revision", this, "_showRevision");
|
|
|
|
this.setProperties({
|
|
selectedPostIds: [],
|
|
quoteState: new QuoteState()
|
|
});
|
|
},
|
|
|
|
willDestroy() {
|
|
this._super(...arguments);
|
|
|
|
this.appEvents.off("post:show-revision", this, "_showRevision");
|
|
},
|
|
|
|
_showRevision(postNumber, revision) {
|
|
const post = this.model.get("postStream").postForPostNumber(postNumber);
|
|
if (!post) {
|
|
return;
|
|
}
|
|
|
|
scheduleOnce("afterRender", () => {
|
|
this.send("showHistory", post, revision);
|
|
});
|
|
},
|
|
|
|
showCategoryChooser: not("model.isPrivateMessage"),
|
|
|
|
gotoInbox(name) {
|
|
let url = userPath(this.get("currentUser.username_lower") + "/messages");
|
|
if (name) {
|
|
url = url + "/group/" + name;
|
|
}
|
|
DiscourseURL.routeTo(url);
|
|
},
|
|
|
|
@discourseComputed
|
|
selectedQuery() {
|
|
return post => this.postSelected(post);
|
|
},
|
|
|
|
@discourseComputed("model.isPrivateMessage", "model.category.id")
|
|
canEditTopicFeaturedLink(isPrivateMessage, categoryId) {
|
|
if (!this.siteSettings.topic_featured_link_enabled || isPrivateMessage) {
|
|
return false;
|
|
}
|
|
|
|
const categoryIds = this.site.get(
|
|
"topic_featured_link_allowed_category_ids"
|
|
);
|
|
return (
|
|
categoryIds === undefined ||
|
|
!categoryIds.length ||
|
|
categoryIds.includes(categoryId)
|
|
);
|
|
},
|
|
|
|
@discourseComputed("model")
|
|
featuredLinkDomain(topic) {
|
|
return extractLinkMeta(topic).domain;
|
|
},
|
|
|
|
@discourseComputed("model.isPrivateMessage")
|
|
canEditTags(isPrivateMessage) {
|
|
return (
|
|
this.site.get("can_tag_topics") &&
|
|
(!isPrivateMessage || this.site.get("can_tag_pms"))
|
|
);
|
|
},
|
|
|
|
_forceRefreshPostStream() {
|
|
this.appEvents.trigger("post-stream:refresh", { force: true });
|
|
},
|
|
|
|
_updateSelectedPostIds(postIds) {
|
|
const smallActionsPostIds = this._smallActionPostIds();
|
|
this.selectedPostIds.pushObjects(
|
|
postIds.filter(postId => !smallActionsPostIds.has(postId))
|
|
);
|
|
this.set("selectedPostIds", [...new Set(this.selectedPostIds)]);
|
|
this._forceRefreshPostStream();
|
|
},
|
|
|
|
_smallActionPostIds() {
|
|
const smallActionsPostIds = new Set();
|
|
const posts = this.get("model.postStream.posts");
|
|
if (posts) {
|
|
const small_action = this.site.get("post_types.small_action");
|
|
const whisper = this.site.get("post_types.whisper");
|
|
posts.forEach(post => {
|
|
if (
|
|
post.post_type === small_action ||
|
|
(!post.cooked && post.post_type === whisper)
|
|
) {
|
|
smallActionsPostIds.add(post.id);
|
|
}
|
|
});
|
|
}
|
|
return smallActionsPostIds;
|
|
},
|
|
|
|
_loadPostIds(post) {
|
|
if (this.loadingPostIds) return;
|
|
|
|
const postStream = this.get("model.postStream");
|
|
const url = `/t/${this.get("model.id")}/post_ids.json`;
|
|
|
|
this.set("loadingPostIds", true);
|
|
|
|
return ajax(url, {
|
|
data: _.merge(
|
|
{ post_number: post.get("post_number") },
|
|
postStream.get("streamFilters")
|
|
)
|
|
})
|
|
.then(result => {
|
|
result.post_ids.pushObject(post.get("id"));
|
|
this._updateSelectedPostIds(result.post_ids);
|
|
})
|
|
.finally(() => {
|
|
this.set("loadingPostIds", false);
|
|
});
|
|
},
|
|
|
|
actions: {
|
|
topicCategoryChanged(selection) {
|
|
this.set("buffered.category_id", selection.value);
|
|
},
|
|
|
|
topicTagsChanged({ target }) {
|
|
this.set("buffered.tags", target.value);
|
|
},
|
|
|
|
deletePending(pending) {
|
|
return ajax(`/review/${pending.id}`, { type: "DELETE" })
|
|
.then(() => {
|
|
this.get("model.pending_posts").removeObject(pending);
|
|
})
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
showPostFlags(post) {
|
|
return this.send("showFlags", post);
|
|
},
|
|
|
|
openFeatureTopic() {
|
|
this.send("showFeatureTopic");
|
|
},
|
|
|
|
selectText(postId, buffer) {
|
|
const loadedPost = this.get("model.postStream").findLoadedPost(postId);
|
|
const promise = loadedPost
|
|
? Promise.resolve(loadedPost)
|
|
: this.get("model.postStream").loadPost(postId);
|
|
|
|
return promise.then(post => {
|
|
const composer = this.composer;
|
|
const viewOpen = composer.get("model.viewOpen");
|
|
const quotedText = Quote.build(post, buffer);
|
|
|
|
// If we can't create a post, delegate to reply as new topic
|
|
if (!viewOpen && !this.get("model.details.can_create_post")) {
|
|
this.send("replyAsNewTopic", post, quotedText);
|
|
return;
|
|
}
|
|
|
|
const composerOpts = {
|
|
action: Composer.REPLY,
|
|
draftSequence: post.get("topic.draft_sequence"),
|
|
draftKey: post.get("topic.draft_key")
|
|
};
|
|
|
|
if (post.get("post_number") === 1) {
|
|
composerOpts.topic = post.get("topic");
|
|
} else {
|
|
composerOpts.post = post;
|
|
}
|
|
|
|
// If the composer is associated with a different post, we don't change it.
|
|
const composerPost = composer.get("model.post");
|
|
if (composerPost && composerPost.get("id") !== this.get("post.id")) {
|
|
composerOpts.post = composerPost;
|
|
}
|
|
|
|
composerOpts.quote = quotedText;
|
|
if (composer.get("model.viewOpen")) {
|
|
this.appEvents.trigger("composer:insert-block", quotedText);
|
|
} else if (composer.get("model.viewDraft")) {
|
|
const model = composer.get("model");
|
|
model.set("reply", model.get("reply") + quotedText);
|
|
composer.send("openIfDraft");
|
|
} else {
|
|
composer.open(composerOpts);
|
|
}
|
|
});
|
|
},
|
|
|
|
fillGapBefore(args) {
|
|
return this.get("model.postStream").fillGapBefore(args.post, args.gap);
|
|
},
|
|
|
|
fillGapAfter(args) {
|
|
return this.get("model.postStream").fillGapAfter(args.post, args.gap);
|
|
},
|
|
|
|
currentPostChanged(event) {
|
|
const { post } = event;
|
|
if (!post) {
|
|
return;
|
|
}
|
|
|
|
const postNumber = post.get("post_number");
|
|
const topic = this.model;
|
|
topic.set("currentPost", postNumber);
|
|
if (postNumber > (topic.get("last_read_post_number") || 0)) {
|
|
topic.set("last_read_post_id", post.get("id"));
|
|
topic.set("last_read_post_number", postNumber);
|
|
}
|
|
|
|
this.send("postChangedRoute", postNumber);
|
|
this._progressIndex = topic.get("postStream").progressIndexOfPost(post);
|
|
|
|
this.appEvents.trigger("topic:current-post-changed", { post });
|
|
},
|
|
|
|
currentPostScrolled(event) {
|
|
const total = this.get("model.postStream.filteredPostsCount");
|
|
const percent =
|
|
parseFloat(this._progressIndex + event.percent - 1) / total;
|
|
this.appEvents.trigger("topic:current-post-scrolled", {
|
|
postIndex: this._progressIndex,
|
|
percent: Math.max(Math.min(percent, 1.0), 0.0)
|
|
});
|
|
},
|
|
|
|
// Called when the topmost visible post on the page changes.
|
|
topVisibleChanged(event) {
|
|
const { post, refresh } = event;
|
|
if (!post) {
|
|
return;
|
|
}
|
|
|
|
const postStream = this.get("model.postStream");
|
|
const firstLoadedPost = postStream.get("posts.firstObject");
|
|
|
|
if (post.get && post.get("post_number") === 1) {
|
|
return;
|
|
}
|
|
|
|
if (firstLoadedPost && firstLoadedPost === post) {
|
|
postStream.prependMore().then(() => refresh());
|
|
}
|
|
},
|
|
|
|
// Called the the bottommost visible post on the page changes.
|
|
bottomVisibleChanged(event) {
|
|
const { post, refresh } = event;
|
|
|
|
const postStream = this.get("model.postStream");
|
|
const lastLoadedPost = postStream.get("posts.lastObject");
|
|
|
|
if (
|
|
lastLoadedPost &&
|
|
lastLoadedPost === post &&
|
|
postStream.get("canAppendMore")
|
|
) {
|
|
postStream.appendMore().then(() => refresh());
|
|
// show loading stuff
|
|
refresh();
|
|
}
|
|
},
|
|
|
|
toggleSummary() {
|
|
return this.get("model.postStream")
|
|
.toggleSummary()
|
|
.then(() => {
|
|
this.updateQueryParams();
|
|
});
|
|
},
|
|
|
|
removeAllowedUser(user) {
|
|
return this.get("model.details")
|
|
.removeAllowedUser(user)
|
|
.then(() => {
|
|
if (this.currentUser.id === user.id) {
|
|
this.transitionToRoute("userPrivateMessages", user);
|
|
}
|
|
});
|
|
},
|
|
|
|
removeAllowedGroup(group) {
|
|
return this.get("model.details").removeAllowedGroup(group);
|
|
},
|
|
|
|
deleteTopic() {
|
|
this.deleteTopic();
|
|
},
|
|
|
|
// Archive a PM (as opposed to archiving a topic)
|
|
toggleArchiveMessage() {
|
|
const topic = this.model;
|
|
|
|
if (topic.get("archiving")) {
|
|
return;
|
|
}
|
|
|
|
const backToInbox = () => this.gotoInbox(topic.get("inboxGroupName"));
|
|
|
|
if (topic.get("message_archived")) {
|
|
topic.moveToInbox().then(backToInbox);
|
|
} else {
|
|
topic.archiveMessage().then(backToInbox);
|
|
}
|
|
},
|
|
|
|
deferTopic() {
|
|
const screenTrack = Discourse.__container__.lookup("screen-track:main");
|
|
const currentUser = this.currentUser;
|
|
const topic = this.model;
|
|
|
|
screenTrack.reset();
|
|
screenTrack.stop();
|
|
const goToPath = topic.get("isPrivateMessage")
|
|
? currentUser.pmPath(topic)
|
|
: "/";
|
|
ajax("/t/" + topic.get("id") + "/timings.json?last=1", { type: "DELETE" })
|
|
.then(() => {
|
|
const highestSeenByTopic = this.session.get("highestSeenByTopic");
|
|
highestSeenByTopic[topic.get("id")] = null;
|
|
DiscourseURL.routeTo(goToPath);
|
|
})
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
editFirstPost() {
|
|
const postStream = this.get("model.postStream");
|
|
let firstPost = postStream.get("posts.firstObject");
|
|
|
|
if (firstPost.get("post_number") !== 1) {
|
|
const postId = postStream.findPostIdForPostNumber(1);
|
|
// try loading from identity map first
|
|
firstPost = postStream.findLoadedPost(postId);
|
|
if (firstPost === undefined) {
|
|
return this.get("model.postStream")
|
|
.loadPost(postId)
|
|
.then(post => {
|
|
this.send("editPost", post);
|
|
});
|
|
}
|
|
}
|
|
this.send("editPost", firstPost);
|
|
},
|
|
|
|
// Post related methods
|
|
replyToPost(post) {
|
|
const composerController = this.composer;
|
|
const topic = post ? post.get("topic") : this.model;
|
|
const quoteState = this.quoteState;
|
|
const postStream = this.get("model.postStream");
|
|
|
|
this.appEvents.trigger("page:compose-reply", topic);
|
|
|
|
if (!postStream || !topic || !topic.get("details.can_create_post")) {
|
|
return;
|
|
}
|
|
|
|
const quotedPost = postStream.findLoadedPost(quoteState.postId);
|
|
const quotedText = Quote.build(quotedPost, quoteState.buffer);
|
|
|
|
quoteState.clear();
|
|
|
|
if (
|
|
composerController.get("model.topic.id") === topic.get("id") &&
|
|
composerController.get("model.action") === Composer.REPLY
|
|
) {
|
|
composerController.set("model.post", post);
|
|
composerController.set("model.composeState", Composer.OPEN);
|
|
this.appEvents.trigger("composer:insert-block", quotedText.trim());
|
|
} else {
|
|
const opts = {
|
|
action: Composer.REPLY,
|
|
draftKey: topic.get("draft_key"),
|
|
draftSequence: topic.get("draft_sequence")
|
|
};
|
|
|
|
if (quotedText) {
|
|
opts.quote = quotedText;
|
|
}
|
|
|
|
if (post && post.get("post_number") !== 1) {
|
|
opts.post = post;
|
|
} else {
|
|
opts.topic = topic;
|
|
}
|
|
|
|
composerController.open(opts);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
recoverPost(post) {
|
|
post.get("post_number") === 1 ? this.recoverTopic() : post.recover();
|
|
},
|
|
|
|
deletePost(post) {
|
|
if (post.get("post_number") === 1) {
|
|
return this.deleteTopic();
|
|
} else if (!post.can_delete) {
|
|
return false;
|
|
}
|
|
|
|
const user = this.currentUser;
|
|
const refresh = () => this.appEvents.trigger("post-stream:refresh");
|
|
const hasReplies = post.get("reply_count") > 0;
|
|
const loadedPosts = this.get("model.postStream.posts");
|
|
|
|
if (user.get("staff") && hasReplies) {
|
|
ajax(`/posts/${post.id}/reply-ids.json`).then(replies => {
|
|
if (replies.length === 0) {
|
|
return post
|
|
.destroy(user)
|
|
.then(refresh)
|
|
.catch(error => {
|
|
popupAjaxError(error);
|
|
post.undoDeleteState();
|
|
});
|
|
}
|
|
|
|
const buttons = [];
|
|
|
|
buttons.push({
|
|
label: I18n.t("cancel"),
|
|
class: "btn-danger right"
|
|
});
|
|
|
|
buttons.push({
|
|
label: I18n.t("post.controls.delete_replies.just_the_post"),
|
|
callback() {
|
|
post
|
|
.destroy(user)
|
|
.then(refresh)
|
|
.catch(error => {
|
|
popupAjaxError(error);
|
|
post.undoDeleteState();
|
|
});
|
|
}
|
|
});
|
|
|
|
if (replies.some(r => r.level > 1)) {
|
|
buttons.push({
|
|
label: I18n.t("post.controls.delete_replies.all_replies", {
|
|
count: replies.length
|
|
}),
|
|
callback() {
|
|
loadedPosts.forEach(
|
|
p =>
|
|
(p === post || replies.some(r => r.id === p.id)) &&
|
|
p.setDeletedState(user)
|
|
);
|
|
Post.deleteMany([post.id, ...replies.map(r => r.id)])
|
|
.then(refresh)
|
|
.catch(popupAjaxError);
|
|
}
|
|
});
|
|
}
|
|
|
|
const directReplyIds = replies
|
|
.filter(r => r.level === 1)
|
|
.map(r => r.id);
|
|
|
|
buttons.push({
|
|
label: I18n.t("post.controls.delete_replies.direct_replies", {
|
|
count: directReplyIds.length
|
|
}),
|
|
class: "btn-primary",
|
|
callback() {
|
|
loadedPosts.forEach(
|
|
p =>
|
|
(p === post || directReplyIds.includes(p.id)) &&
|
|
p.setDeletedState(user)
|
|
);
|
|
Post.deleteMany([post.id, ...directReplyIds])
|
|
.then(refresh)
|
|
.catch(popupAjaxError);
|
|
}
|
|
});
|
|
|
|
bootbox.dialog(
|
|
I18n.t("post.controls.delete_replies.confirm"),
|
|
buttons
|
|
);
|
|
});
|
|
} else {
|
|
return post
|
|
.destroy(user)
|
|
.then(refresh)
|
|
.catch(error => {
|
|
popupAjaxError(error);
|
|
post.undoDeleteState();
|
|
});
|
|
}
|
|
},
|
|
|
|
editPost(post) {
|
|
if (!this.currentUser) {
|
|
return bootbox.alert(I18n.t("post.controls.edit_anonymous"));
|
|
} else if (!post.can_edit) {
|
|
return false;
|
|
}
|
|
|
|
const composer = this.composer;
|
|
let topic = this.model;
|
|
const composerModel = composer.get("model");
|
|
let editingFirst =
|
|
composerModel &&
|
|
(post.get("firstPost") || composerModel.get("editingFirstPost"));
|
|
|
|
let editingSharedDraft = false;
|
|
let draftsCategoryId = this.get("site.shared_drafts_category_id");
|
|
if (draftsCategoryId && draftsCategoryId === topic.get("category.id")) {
|
|
editingSharedDraft = post.get("firstPost");
|
|
}
|
|
|
|
const opts = {
|
|
post,
|
|
action: editingSharedDraft ? Composer.EDIT_SHARED_DRAFT : Composer.EDIT,
|
|
draftKey: post.get("topic.draft_key"),
|
|
draftSequence: post.get("topic.draft_sequence")
|
|
};
|
|
|
|
if (editingSharedDraft) {
|
|
opts.destinationCategoryId = topic.get("destination_category_id");
|
|
}
|
|
|
|
// Cancel and reopen the composer for the first post
|
|
if (editingFirst) {
|
|
composer.cancelComposer().then(() => composer.open(opts));
|
|
} else {
|
|
composer.open(opts);
|
|
}
|
|
},
|
|
|
|
toggleBookmark(post) {
|
|
if (!this.currentUser) {
|
|
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
|
|
} else if (post) {
|
|
return post.toggleBookmark().catch(popupAjaxError);
|
|
} else {
|
|
return this.model.toggleBookmark().then(changedIds => {
|
|
if (!changedIds) {
|
|
return;
|
|
}
|
|
changedIds.forEach(id =>
|
|
this.appEvents.trigger("post-stream:refresh", { id })
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
toggleBookmarkWithReminder(post) {
|
|
if (!this.currentUser) {
|
|
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
|
|
} else if (post) {
|
|
return post.toggleBookmarkWithReminder();
|
|
} else {
|
|
return this.model.toggleBookmarkWithReminder();
|
|
}
|
|
},
|
|
|
|
toggleFeaturedOnProfile() {
|
|
if (!this.currentUser) return;
|
|
|
|
if (
|
|
this.currentUser.featured_topic &&
|
|
this.currentUser.featured_topic.id !== this.model.id
|
|
) {
|
|
bootbox.confirm(I18n.t("topic.remove_from_profile.warning"), result => {
|
|
if (result) return this._performToggleFeaturedOnProfile();
|
|
});
|
|
} else return this._performToggleFeaturedOnProfile();
|
|
},
|
|
|
|
jumpToIndex(index) {
|
|
this._jumpToIndex(index);
|
|
},
|
|
|
|
jumpToDate(date) {
|
|
this._jumpToDate(date);
|
|
},
|
|
|
|
jumpToPostPrompt() {
|
|
const topic = this.model;
|
|
const controller = showModal("jump-to-post", {
|
|
modalClass: "jump-to-post-modal"
|
|
});
|
|
controller.setProperties({
|
|
topic,
|
|
postNumber: null,
|
|
jumpToIndex: index => this.send("jumpToIndex", index),
|
|
jumpToDate: date => this.send("jumpToDate", date)
|
|
});
|
|
},
|
|
|
|
jumpToPost(postNumber) {
|
|
this._jumpToPostNumber(postNumber);
|
|
},
|
|
|
|
jumpTop() {
|
|
DiscourseURL.routeTo(this.get("model.firstPostUrl"), {
|
|
skipIfOnScreen: false
|
|
});
|
|
},
|
|
|
|
jumpBottom() {
|
|
DiscourseURL.routeTo(this.get("model.lastPostUrl"), {
|
|
skipIfOnScreen: false
|
|
});
|
|
},
|
|
|
|
jumpEnd() {
|
|
DiscourseURL.routeTo(this.get("model.lastPostUrl"), {
|
|
jumpEnd: true
|
|
});
|
|
},
|
|
|
|
jumpUnread() {
|
|
this._jumpToPostId(this.get("model.last_read_post_id"));
|
|
},
|
|
|
|
jumpToPostId(postId) {
|
|
this._jumpToPostId(postId);
|
|
},
|
|
|
|
toggleMultiSelect() {
|
|
this.toggleProperty("multiSelect");
|
|
this._forceRefreshPostStream();
|
|
},
|
|
|
|
selectAll() {
|
|
const smallActionsPostIds = this._smallActionPostIds();
|
|
this.set("selectedPostIds", [
|
|
...this.get("model.postStream.stream").filter(
|
|
postId => !smallActionsPostIds.has(postId)
|
|
)
|
|
]);
|
|
this._forceRefreshPostStream();
|
|
},
|
|
|
|
deselectAll() {
|
|
this.set("selectedPostIds", []);
|
|
this._forceRefreshPostStream();
|
|
},
|
|
|
|
togglePostSelection(post) {
|
|
const selected = this.selectedPostIds;
|
|
selected.includes(post.id)
|
|
? selected.removeObject(post.id)
|
|
: selected.addObject(post.id);
|
|
},
|
|
|
|
selectReplies(post) {
|
|
ajax(`/posts/${post.id}/reply-ids.json`).then(replies => {
|
|
const replyIds = replies.map(r => r.id);
|
|
this.selectedPostIds.pushObjects([post.id, ...replyIds]);
|
|
this._forceRefreshPostStream();
|
|
});
|
|
},
|
|
|
|
selectBelow(post) {
|
|
if (this.get("model.postStream.isMegaTopic")) {
|
|
this._loadPostIds(post);
|
|
} else {
|
|
const stream = [...this.get("model.postStream.stream")];
|
|
const below = stream.slice(stream.indexOf(post.id));
|
|
this._updateSelectedPostIds(below);
|
|
}
|
|
},
|
|
|
|
deleteSelected() {
|
|
const user = this.currentUser;
|
|
|
|
bootbox.confirm(
|
|
I18n.t("post.delete.confirm", {
|
|
count: this.selectedPostsCount
|
|
}),
|
|
result => {
|
|
if (result) {
|
|
// If all posts are selected, it's the same thing as deleting the topic
|
|
if (this.selectedAllPosts) return this.deleteTopic();
|
|
|
|
Post.deleteMany(this.selectedPostIds);
|
|
this.get("model.postStream.posts").forEach(
|
|
p => this.postSelected(p) && p.setDeletedState(user)
|
|
);
|
|
this.send("toggleMultiSelect");
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
mergePosts() {
|
|
bootbox.confirm(
|
|
I18n.t("post.merge.confirm", { count: this.selectedPostsCount }),
|
|
result => {
|
|
if (result) {
|
|
Post.mergePosts(this.selectedPostIds);
|
|
this.send("toggleMultiSelect");
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
changePostOwner(post) {
|
|
this.set("selectedPostIds", [post.id]);
|
|
this.send("changeOwner");
|
|
},
|
|
|
|
lockPost(post) {
|
|
return post.updatePostField("locked", true);
|
|
},
|
|
|
|
unlockPost(post) {
|
|
return post.updatePostField("locked", false);
|
|
},
|
|
|
|
grantBadge(post) {
|
|
this.set("selectedPostIds", [post.id]);
|
|
this.send("showGrantBadgeModal");
|
|
},
|
|
|
|
addNotice(post) {
|
|
return new Promise(function(resolve, reject) {
|
|
const controller = showModal("add-post-notice");
|
|
controller.setProperties({ post, resolve, reject });
|
|
});
|
|
},
|
|
|
|
removeNotice(post) {
|
|
return post.updatePostField("notice", null).then(() =>
|
|
post.setProperties({
|
|
notice_type: null,
|
|
notice_args: null
|
|
})
|
|
);
|
|
},
|
|
|
|
toggleParticipant(user) {
|
|
this.get("model.postStream")
|
|
.toggleParticipant(user.get("username"))
|
|
.then(() => this.updateQueryParams);
|
|
},
|
|
|
|
editTopic() {
|
|
if (this.get("model.details.can_edit")) {
|
|
this.set("editingTopic", true);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
cancelEditingTopic() {
|
|
this.set("editingTopic", false);
|
|
this.rollbackBuffer();
|
|
},
|
|
|
|
finishedEditingTopic() {
|
|
if (!this.editingTopic) {
|
|
return;
|
|
}
|
|
|
|
// save the modifications
|
|
const props = this.get("buffered.buffer");
|
|
|
|
Topic.update(this.model, props)
|
|
.then(() => {
|
|
// We roll back on success here because `update` saves the properties to the topic
|
|
this.rollbackBuffer();
|
|
this.set("editingTopic", false);
|
|
})
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
expandHidden(post) {
|
|
return post.expandHidden();
|
|
},
|
|
|
|
toggleVisibility() {
|
|
this.model.toggleStatus("visible");
|
|
},
|
|
|
|
toggleClosed() {
|
|
const topic = this.model;
|
|
|
|
this.model.toggleStatus("closed").then(result => {
|
|
topic.set("topic_status_update", result.topic_status_update);
|
|
});
|
|
},
|
|
|
|
recoverTopic() {
|
|
this.model.recover();
|
|
},
|
|
|
|
makeBanner() {
|
|
this.model.makeBanner();
|
|
},
|
|
|
|
removeBanner() {
|
|
this.model.removeBanner();
|
|
},
|
|
|
|
togglePinned() {
|
|
const value = this.get("model.pinned_at") ? false : true,
|
|
topic = this.model,
|
|
until = this.get("model.pinnedInCategoryUntil");
|
|
|
|
// optimistic update
|
|
topic.setProperties({
|
|
pinned_at: value ? moment() : null,
|
|
pinned_globally: false,
|
|
pinned_until: value ? until : null
|
|
});
|
|
|
|
return topic.saveStatus("pinned", value, until);
|
|
},
|
|
|
|
pinGlobally() {
|
|
const topic = this.model,
|
|
until = this.get("model.pinnedGloballyUntil");
|
|
|
|
// optimistic update
|
|
topic.setProperties({
|
|
pinned_at: moment(),
|
|
pinned_globally: true,
|
|
pinned_until: until
|
|
});
|
|
|
|
return topic.saveStatus("pinned_globally", true, until);
|
|
},
|
|
|
|
toggleArchived() {
|
|
this.model.toggleStatus("archived");
|
|
},
|
|
|
|
clearPin() {
|
|
this.model.clearPin();
|
|
},
|
|
|
|
togglePinnedForUser() {
|
|
if (this.get("model.pinned_at")) {
|
|
const topic = this.model;
|
|
if (topic.get("pinned")) {
|
|
topic.clearPin();
|
|
} else {
|
|
topic.rePin();
|
|
}
|
|
}
|
|
},
|
|
|
|
replyAsNewTopic(post, quotedText) {
|
|
const composerController = this.composer;
|
|
|
|
const { quoteState } = this;
|
|
quotedText = quotedText || Quote.build(post, quoteState.buffer);
|
|
quoteState.clear();
|
|
|
|
var options;
|
|
if (this.get("model.isPrivateMessage")) {
|
|
let users = this.get("model.details.allowed_users");
|
|
let groups = this.get("model.details.allowed_groups");
|
|
|
|
let usernames = [];
|
|
users.forEach(user => usernames.push(user.username));
|
|
groups.forEach(group => usernames.push(group.name));
|
|
usernames = usernames.join();
|
|
|
|
options = {
|
|
action: Composer.PRIVATE_MESSAGE,
|
|
archetypeId: "private_message",
|
|
draftKey: post.topic.draft_key,
|
|
usernames: usernames
|
|
};
|
|
} else {
|
|
options = {
|
|
action: Composer.CREATE_TOPIC,
|
|
draftKey: post.topic.draft_key,
|
|
categoryId: this.get("model.category.id")
|
|
};
|
|
}
|
|
|
|
composerController
|
|
.open(options)
|
|
.then(() => {
|
|
return isEmpty(quotedText) ? "" : quotedText;
|
|
})
|
|
.then(q => {
|
|
const postUrl = `${location.protocol}//${location.host}${post.get(
|
|
"url"
|
|
)}`;
|
|
const postLink = `[${Handlebars.escapeExpression(
|
|
this.get("model.title")
|
|
)}](${postUrl})`;
|
|
composerController
|
|
.get("model")
|
|
.prependText(
|
|
`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`,
|
|
{ new_line: true }
|
|
);
|
|
});
|
|
},
|
|
|
|
retryLoading() {
|
|
this.set("retrying", true);
|
|
const rollback = () => this.set("retrying", false);
|
|
this.get("model.postStream")
|
|
.refresh()
|
|
.then(rollback, rollback);
|
|
},
|
|
|
|
toggleWiki(post) {
|
|
return post.updatePostField("wiki", !post.get("wiki"));
|
|
},
|
|
|
|
togglePostType(post) {
|
|
const regular = this.site.get("post_types.regular");
|
|
const moderator = this.site.get("post_types.moderator_action");
|
|
return post.updatePostField(
|
|
"post_type",
|
|
post.get("post_type") === moderator ? regular : moderator
|
|
);
|
|
},
|
|
|
|
rebakePost(post) {
|
|
return post.rebake();
|
|
},
|
|
|
|
unhidePost(post) {
|
|
return post.unhide();
|
|
},
|
|
|
|
convertToPublicTopic() {
|
|
showModal("convert-to-public-topic", {
|
|
model: this.model,
|
|
modalClass: "convert-to-public-topic"
|
|
});
|
|
},
|
|
|
|
convertToPrivateMessage() {
|
|
this.model
|
|
.convertTopic("private")
|
|
.then(() => window.location.reload())
|
|
.catch(popupAjaxError);
|
|
},
|
|
|
|
removeFeaturedLink() {
|
|
this.set("buffered.featured_link", null);
|
|
},
|
|
|
|
resetBumpDate() {
|
|
this.model.resetBumpDate();
|
|
},
|
|
|
|
removeTopicTimer(statusType, topicTimer) {
|
|
TopicTimer.updateStatus(
|
|
this.get("model.id"),
|
|
null,
|
|
null,
|
|
statusType,
|
|
null
|
|
)
|
|
.then(() => this.set(`model.${topicTimer}`, EmberObject.create({})))
|
|
.catch(error => popupAjaxError(error));
|
|
}
|
|
},
|
|
|
|
_performToggleFeaturedOnProfile() {
|
|
this.model.toggleFeaturedOnProfile(this.currentUser).catch(popupAjaxError);
|
|
},
|
|
|
|
_jumpToIndex(index) {
|
|
const postStream = this.get("model.postStream");
|
|
|
|
if (postStream.get("isMegaTopic")) {
|
|
this._jumpToPostNumber(index);
|
|
} else {
|
|
const stream = postStream.get("stream");
|
|
const streamIndex = Math.max(1, Math.min(stream.length, index));
|
|
this._jumpToPostId(stream[streamIndex - 1]);
|
|
}
|
|
},
|
|
|
|
_jumpToDate(date) {
|
|
const postStream = this.get("model.postStream");
|
|
|
|
postStream
|
|
.loadNearestPostToDate(date)
|
|
.then(post => {
|
|
DiscourseURL.routeTo(
|
|
this.model.urlForPostNumber(post.get("post_number"))
|
|
);
|
|
})
|
|
.catch(() => {
|
|
this._jumpToIndex(postStream.get("topic.highest_post_number"));
|
|
});
|
|
},
|
|
|
|
_jumpToPostNumber(postNumber) {
|
|
const postStream = this.get("model.postStream");
|
|
const post = postStream.get("posts").findBy("post_number", postNumber);
|
|
|
|
if (post) {
|
|
DiscourseURL.routeTo(
|
|
this.model.urlForPostNumber(post.get("post_number"))
|
|
);
|
|
} else {
|
|
postStream.loadPostByPostNumber(postNumber).then(p => {
|
|
DiscourseURL.routeTo(this.model.urlForPostNumber(p.get("post_number")));
|
|
});
|
|
}
|
|
},
|
|
|
|
_jumpToPostId(postId) {
|
|
if (!postId) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
"jump-post code broken - requested an index outside the stream array"
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.appEvents.trigger("topic:jump-to-post", postId);
|
|
|
|
const topic = this.model;
|
|
const postStream = topic.get("postStream");
|
|
const post = postStream.findLoadedPost(postId);
|
|
|
|
if (post) {
|
|
DiscourseURL.routeTo(topic.urlForPostNumber(post.get("post_number")));
|
|
} else {
|
|
// need to load it
|
|
postStream.findPostsByIds([postId]).then(arr => {
|
|
DiscourseURL.routeTo(topic.urlForPostNumber(arr[0].get("post_number")));
|
|
});
|
|
}
|
|
},
|
|
|
|
togglePinnedState() {
|
|
this.send("togglePinnedForUser");
|
|
},
|
|
|
|
print() {
|
|
if (this.siteSettings.max_prints_per_hour_per_user > 0) {
|
|
window.open(
|
|
this.get("model.printUrl"),
|
|
"",
|
|
"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=315"
|
|
);
|
|
}
|
|
},
|
|
|
|
hasError: or("model.errorHtml", "model.errorMessage"),
|
|
noErrorYet: not("hasError"),
|
|
|
|
categories: alias("site.categoriesList"),
|
|
|
|
selectedPostsCount: alias("selectedPostIds.length"),
|
|
|
|
@discourseComputed(
|
|
"selectedPostIds",
|
|
"model.postStream.posts",
|
|
"selectedPostIds.[]",
|
|
"model.postStream.posts.[]"
|
|
)
|
|
selectedPosts(selectedPostIds, loadedPosts) {
|
|
return selectedPostIds
|
|
.map(id => loadedPosts.find(p => p.id === id))
|
|
.filter(post => post !== undefined);
|
|
},
|
|
|
|
@discourseComputed("selectedPostsCount", "selectedPosts", "selectedPosts.[]")
|
|
selectedPostsUsername(selectedPostsCount, selectedPosts) {
|
|
if (selectedPosts.length < 1 || selectedPostsCount > selectedPosts.length) {
|
|
return undefined;
|
|
}
|
|
const username = selectedPosts[0].username;
|
|
return selectedPosts.every(p => p.username === username)
|
|
? username
|
|
: undefined;
|
|
},
|
|
|
|
@discourseComputed(
|
|
"selectedPostsCount",
|
|
"model.postStream.isMegaTopic",
|
|
"model.postStream.stream.length",
|
|
"model.posts_count"
|
|
)
|
|
selectedAllPosts(
|
|
selectedPostsCount,
|
|
isMegaTopic,
|
|
postsCount,
|
|
topicPostsCount
|
|
) {
|
|
if (isMegaTopic) {
|
|
return selectedPostsCount >= topicPostsCount;
|
|
} else {
|
|
return selectedPostsCount >= postsCount;
|
|
}
|
|
},
|
|
|
|
@discourseComputed("selectedAllPosts", "model.postStream.isMegaTopic")
|
|
canSelectAll(selectedAllPosts, isMegaTopic) {
|
|
return isMegaTopic ? false : !selectedAllPosts;
|
|
},
|
|
|
|
canDeselectAll: alias("selectedAllPosts"),
|
|
|
|
@discourseComputed(
|
|
"currentUser.staff",
|
|
"selectedPostsCount",
|
|
"selectedAllPosts",
|
|
"selectedPosts",
|
|
"selectedPosts.[]"
|
|
)
|
|
canDeleteSelected(
|
|
isStaff,
|
|
selectedPostsCount,
|
|
selectedAllPosts,
|
|
selectedPosts
|
|
) {
|
|
return (
|
|
selectedPostsCount > 0 &&
|
|
((selectedAllPosts && isStaff) || selectedPosts.every(p => p.can_delete))
|
|
);
|
|
},
|
|
|
|
@discourseComputed("model.details.can_move_posts", "selectedPostsCount")
|
|
canMergeTopic(canMovePosts, selectedPostsCount) {
|
|
return canMovePosts && selectedPostsCount > 0;
|
|
},
|
|
|
|
@discourseComputed(
|
|
"currentUser.admin",
|
|
"selectedPostsCount",
|
|
"selectedPostsUsername"
|
|
)
|
|
canChangeOwner(isAdmin, selectedPostsCount, selectedPostsUsername) {
|
|
return (
|
|
isAdmin && selectedPostsCount > 0 && selectedPostsUsername !== undefined
|
|
);
|
|
},
|
|
|
|
@discourseComputed(
|
|
"selectedPostsCount",
|
|
"selectedPostsUsername",
|
|
"selectedPosts",
|
|
"selectedPosts.[]"
|
|
)
|
|
canMergePosts(selectedPostsCount, selectedPostsUsername, selectedPosts) {
|
|
return (
|
|
selectedPostsCount > 1 &&
|
|
selectedPostsUsername !== undefined &&
|
|
selectedPosts.every(p => p.can_delete)
|
|
);
|
|
},
|
|
|
|
@observes("multiSelect")
|
|
_multiSelectChanged() {
|
|
this.set("selectedPostIds", []);
|
|
},
|
|
|
|
postSelected(post) {
|
|
return this.selectedAllPost || this.selectedPostIds.includes(post.id);
|
|
},
|
|
|
|
@discourseComputed
|
|
loadingHTML() {
|
|
return spinnerHTML;
|
|
},
|
|
|
|
recoverTopic() {
|
|
this.model.recover();
|
|
},
|
|
|
|
deleteTopic() {
|
|
this.model.destroy(this.currentUser);
|
|
},
|
|
|
|
subscribe() {
|
|
this.unsubscribe();
|
|
|
|
const refresh = args => this.appEvents.trigger("post-stream:refresh", args);
|
|
|
|
this.messageBus.subscribe(
|
|
`/topic/${this.get("model.id")}`,
|
|
data => {
|
|
const topic = this.model;
|
|
|
|
if (Ember.isPresent(data.notification_level_change)) {
|
|
topic.set(
|
|
"details.notification_level",
|
|
data.notification_level_change
|
|
);
|
|
topic.set(
|
|
"details.notifications_reason_id",
|
|
data.notifications_reason_id
|
|
);
|
|
return;
|
|
}
|
|
|
|
const postStream = this.get("model.postStream");
|
|
|
|
if (data.reload_topic) {
|
|
topic.reload().then(() => {
|
|
this.send("postChangedRoute", topic.get("post_number") || 1);
|
|
this.appEvents.trigger("header:update-topic", topic);
|
|
if (data.refresh_stream) postStream.refresh();
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
switch (data.type) {
|
|
case "acted":
|
|
postStream
|
|
.triggerChangedPost(data.id, data.updated_at, {
|
|
preserveCooked: true
|
|
})
|
|
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
|
break;
|
|
case "read": {
|
|
postStream
|
|
.triggerReadPost(data.id, data.readers_count)
|
|
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
|
break;
|
|
}
|
|
case "revised":
|
|
case "rebaked": {
|
|
postStream
|
|
.triggerChangedPost(data.id, data.updated_at)
|
|
.then(() => refresh({ id: data.id }));
|
|
break;
|
|
}
|
|
case "deleted": {
|
|
postStream
|
|
.triggerDeletedPost(data.id)
|
|
.then(() => refresh({ id: data.id }));
|
|
break;
|
|
}
|
|
case "recovered": {
|
|
postStream
|
|
.triggerRecoveredPost(data.id)
|
|
.then(() => refresh({ id: data.id }));
|
|
break;
|
|
}
|
|
case "created": {
|
|
postStream.triggerNewPostInStream(data.id).then(() => refresh());
|
|
if (this.get("currentUser.id") !== data.user_id) {
|
|
Discourse.incrementBackgroundContextCount();
|
|
}
|
|
break;
|
|
}
|
|
case "move_to_inbox": {
|
|
topic.set("message_archived", false);
|
|
break;
|
|
}
|
|
case "archived": {
|
|
topic.set("message_archived", true);
|
|
break;
|
|
}
|
|
default: {
|
|
let callback = customPostMessageCallbacks[data.type];
|
|
if (callback) {
|
|
callback(this, data);
|
|
} else {
|
|
// eslint-disable-next-line no-console
|
|
console.warn("unknown topic bus message type", data);
|
|
}
|
|
}
|
|
}
|
|
|
|
// scroll to bottom is very specific to new posts from discobot
|
|
// hence the -2 check (dicobot id). We can shift all this code
|
|
// to discobot plugin longer term
|
|
if (
|
|
topic.get("isPrivateMessage") &&
|
|
this.currentUser &&
|
|
this.currentUser.get("id") !== data.user_id &&
|
|
data.user_id === -2 &&
|
|
data.type === "created"
|
|
) {
|
|
const postNumber = data.post_number;
|
|
const notInPostStream =
|
|
topic.get("highest_post_number") <= postNumber;
|
|
const postNumberDifference = postNumber - topic.get("currentPost");
|
|
|
|
if (
|
|
notInPostStream &&
|
|
postNumberDifference > 0 &&
|
|
postNumberDifference < 7
|
|
) {
|
|
this._scrollToPost(data.post_number);
|
|
}
|
|
}
|
|
},
|
|
this.get("model.message_bus_last_id")
|
|
);
|
|
},
|
|
|
|
_scrollToPost: discourseDebounce(function(postNumber) {
|
|
const $post = $(`.topic-post article#post_${postNumber}`);
|
|
|
|
if ($post.length === 0 || isElementInViewport($post)) return;
|
|
|
|
$("html, body").animate({ scrollTop: $post.offset().top }, 1000);
|
|
}, 500),
|
|
|
|
unsubscribe() {
|
|
// never unsubscribe when navigating from topic to topic
|
|
if (!this.get("model.id")) return;
|
|
this.messageBus.unsubscribe("/topic/*");
|
|
},
|
|
|
|
reply() {
|
|
this.replyToPost();
|
|
},
|
|
|
|
readPosts(topicId, postNumbers) {
|
|
const topic = this.model;
|
|
const postStream = topic.get("postStream");
|
|
|
|
if (topic.get("id") === topicId) {
|
|
postStream.get("posts").forEach(post => {
|
|
if (!post.read && postNumbers.includes(post.post_number)) {
|
|
post.set("read", true);
|
|
this.appEvents.trigger("post-stream:refresh", { id: post.get("id") });
|
|
}
|
|
});
|
|
|
|
if (
|
|
this.siteSettings.automatically_unpin_topics &&
|
|
this.currentUser &&
|
|
this.currentUser.automatically_unpin_topics
|
|
) {
|
|
// automatically unpin topics when the user reaches the bottom
|
|
const max = _.max(postNumbers);
|
|
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
|
|
next(() => topic.clearPin());
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
@observes("model.postStream.loaded", "model.postStream.loadedAllPosts")
|
|
_showFooter() {
|
|
const showFooter =
|
|
this.get("model.postStream.loaded") &&
|
|
this.get("model.postStream.loadedAllPosts");
|
|
this.set("application.showFooter", showFooter);
|
|
}
|
|
});
|