This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/controllers/topic.js.es6
Sam 6ddd8d9166 FIX: when entering topics "tracking" would not be set
There was a timing issue when subscribing to messages for topics.

Old flow:

- We generate JSON for topic
- We subscribe to messages for topic

New flow:

- We keep track of last id in the topic message bus channel
- We generate JSON
- We subscribe to messages for topic starting at saved message id

This ensures that there is complete overlap for message consumption
and that there are no cases where an update may go missing due to timing
2017-05-16 15:04:21 -04:00

1018 lines
31 KiB
JavaScript

import BufferedContent from 'discourse/mixins/buffered-content';
import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
import Topic from 'discourse/models/topic';
import Quote from 'discourse/lib/quote';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import Composer from 'discourse/models/composer';
import DiscourseURL from 'discourse/lib/url';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Post from 'discourse/models/post';
import debounce from 'discourse/lib/debounce';
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import QuoteState from 'discourse/lib/quote-state';
import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composer: Ember.inject.controller(),
application: Ember.inject.controller(),
multiSelect: false,
allPostsSelected: false,
editingTopic: false,
selectedPosts: null,
selectedReplies: null,
queryParams: ['filter', 'username_filters'],
loadedAllPosts: Ember.computed.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,
updateQueryParams() {
const postStream = this.get('model.postStream');
this.setProperties(postStream.get('streamFilters'));
},
_titleChanged: function() {
const title = this.get('model.title');
if (!Ember.isEmpty(title)) {
// Note normally you don't have to trigger this, but topic titles can be updated
// and are sometimes lazily loaded.
this.send('refreshTitle');
}
}.observes('model.title', 'category'),
@computed('site.mobileView', 'model.posts_count')
showSelectedPostsAtBottom(mobileView, postsCount) {
return mobileView && (postsCount > 3);
},
@computed('model.postStream.posts')
postsToRender() {
return this.capabilities.isAndroid ? this.get('model.postStream.posts')
: this.get('model.postStream.postsWithPlaceholders');
},
@computed('model.postStream.loadingFilter')
androidLoading(loading) {
return this.capabilities.isAndroid && loading;
},
@computed('model', 'topicTrackingState.messageCount')
browseMoreMessage(model) {
// TODO decide what to show for pms
if (model.get('isPrivateMessage')) { return; }
const opts = { latestLink: `<a href="${Discourse.getURL("/latest")}">${I18n.t("topic.view_latest_topics")}</a>` };
let category = model.get('category');
if (category && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) {
category = null;
}
if (category) {
opts.catLink = categoryBadgeHTML(category);
} else {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + I18n.t("topic.browse_all_categories") + "</a>";
}
const unreadTopics = this.topicTrackingState.countUnread();
const newTopics = this.topicTrackingState.countNew();
if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0;
return I18n.messageFormat("topic.read_more_MF", {
"BOTH": hasBoth,
"UNREAD": unreadTopics,
"NEW": newTopics,
"CATEGORY": category ? true : false,
latestLink: opts.latestLink,
catLink: opts.catLink
});
} else if (category) {
return I18n.t("topic.read_more_in_category", opts);
} else {
return I18n.t("topic.read_more", opts);
}
},
@computed('model')
pmPath(model) {
return this.currentUser && this.currentUser.pmPath(model);
},
@computed('model')
suggestedTitle(model) {
return model.get('isPrivateMessage') ?
`<a href="${this.get('pmPath')}"><i class='private-message-glyph fa fa-envelope'></i></a> ${I18n.t("suggested_topics.pm_title")}` :
I18n.t("suggested_topics.title");
},
init() {
this._super();
this.set('selectedPosts', []);
this.set('selectedReplies', []);
this.set('quoteState', new QuoteState());
},
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
gotoInbox(name) {
let url = userPath(this.get('currentUser.username_lower') + '/messages');
if (name) {
url = url + '/group/' + name;
}
DiscourseURL.routeTo(url);
},
selectedQuery: function() {
return post => this.postSelected(post);
}.property(),
@computed('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.indexOf(categoryId) !== -1;
},
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');
},
actions: {
showPostFlags(post) {
return this.send('showFlags', post);
},
topicRouteAction(name, model) {
return this.send(name, model);
},
openFeatureTopic() {
this.send('showFeatureTopic');
},
selectText(postId, buffer) {
return this.get('model.postStream').loadPost(postId).then(post => {
const composer = this.get('composer');
const viewOpen = composer.get('model.viewOpen');
// 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);
return;
}
const composerOpts = {
action: Composer.REPLY,
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;
}
const quotedText = Quote.build(post, buffer);
composerOpts.quote = quotedText;
if (composer.get('model.viewOpen')) {
this.appEvents.trigger('composer:insert-text', 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.get('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);
},
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 the 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_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);
},
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.get('model');
if (topic.get('archiving')) { return; }
if (topic.get('message_archived')) {
topic.moveToInbox().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
} else {
topic.archiveMessage().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
}
},
// Post related methods
replyToPost(post) {
const composerController = this.get('composer');
const topic = post ? post.get('topic') : this.get('model');
const quoteState = this.get('quoteState');
const postStream = this.get('model.postStream');
if (!postStream) return;
const quotedPost = postStream.findLoadedPost(quoteState.postId);
const quotedText = Quote.build(quotedPost, quoteState.buffer);
quoteState.clear();
if (composerController.get('content.topic.id') === topic.get('id') &&
composerController.get('content.action') === Composer.REPLY) {
composerController.set('content.post', post);
composerController.set('content.composeState', Composer.OPEN);
this.appEvents.trigger('composer:insert-text', 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) {
// Recovering the first post recovers the topic instead
if (post.get('post_number') === 1) {
this.recoverTopic();
return;
}
post.recover();
},
deletePost(post) {
// Deleting the first post deletes the topic
if (post.get('post_number') === 1) {
return this.deleteTopic();
} else if (!post.can_delete) {
// check if current user can delete post
return false;
}
const user = Discourse.User.current(),
replyCount = post.get('reply_count'),
self = this;
// If the user is staff and the post has replies, ask if they want to delete replies too.
if (user.get('staff') && replyCount > 0) {
bootbox.dialog(I18n.t("post.controls.delete_replies.confirm", {count: replyCount}), [
{label: I18n.t("cancel"),
'class': 'btn-danger right'},
{label: I18n.t("post.controls.delete_replies.no_value"),
callback() {
post.destroy(user);
}
},
{label: I18n.t("post.controls.delete_replies.yes_value"),
'class': 'btn-primary',
callback() {
Discourse.Post.deleteMany([post], [post]);
self.get('model.postStream.posts').forEach(function (p) {
if (p === post || p.get('reply_to_post_number') === post.get('post_number')) {
p.setDeletedState(user);
}
});
}
}
]);
} else {
return post.destroy(user).then(() => {
this.appEvents.trigger('post-stream:refresh');
}).catch(error => {
popupAjaxError(error);
post.undoDeleteState();
});
}
},
editPost(post) {
if (!Discourse.User.current()) {
return bootbox.alert(I18n.t('post.controls.edit_anonymous'));
}
// check if current user can edit post
if (!post.can_edit) {
return false;
}
const composer = this.get('composer'),
composerModel = composer.get('model'),
opts = {
post: post,
action: Composer.EDIT,
draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence')
};
// Cancel and reopen the composer for the first post
if (composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'))) {
composer.cancelComposer().then(() => composer.open(opts));
} else {
composer.open(opts);
}
},
toggleBookmark(post) {
if (!this.currentUser) {
alert(I18n.t("bookmarks.not_bookmarked"));
return;
}
if (post) {
return post.toggleBookmark().catch(popupAjaxError);
} else {
return this.get("model").toggleBookmark().then(changedIds => {
if (!changedIds) { return; }
changedIds.forEach(id => this.appEvents.trigger('post-stream:refresh', { id }));
});
}
},
jumpToIndex(index) {
this._jumpToPostId(this.get('model.postStream.stream')[index-1]);
},
jumpToPostPrompt() {
const postText = prompt(I18n.t('topic.progress.jump_prompt_long'));
if (postText === null) { return; }
const postNumber = parseInt(postText, 10);
if (postNumber === 0) { return; }
this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber));
},
jumpToPost(postNumber) {
const postStream = this.get('model.postStream');
let postId = postStream.findPostIdForPostNumber(postNumber);
// If we couldn't find the post, find the closest post to it
if (!postId) {
const closest = postStream.closestPostNumberFor(postNumber);
postId = postStream.findPostIdForPostNumber(closest);
}
this._jumpToPostId(postId);
},
jumpTop() {
DiscourseURL.routeTo(this.get('model.firstPostUrl'), { skipIfOnScreen: false });
},
jumpBottom() {
DiscourseURL.routeTo(this.get('model.lastPostUrl'), { skipIfOnScreen: false });
},
jumpUnread() {
this._jumpToPostId(this.get('model.last_read_post_id'));
},
selectAll() {
const posts = this.get('model.postStream.posts');
const selectedPosts = this.get('selectedPosts');
if (posts) {
selectedPosts.addObjects(posts);
}
this.set('allPostsSelected', true);
this.appEvents.trigger('post-stream:refresh', { force: true });
},
deselectAll() {
this.get('selectedPosts').clear();
this.get('selectedReplies').clear();
this.set('allPostsSelected', false);
this.appEvents.trigger('post-stream:refresh', { force: true });
},
toggleParticipant(user) {
const postStream = this.get('model.postStream');
postStream.toggleParticipant(Ember.get(user, 'username')).then(() => {
this.updateQueryParams();
});
},
editTopic() {
if (!this.get('model.details.can_edit')) return false;
this.set('editingTopic', true);
return false;
},
cancelEditingTopic() {
this.set('editingTopic', false);
this.rollbackBuffer();
},
toggleMultiSelect() {
this.toggleProperty('multiSelect');
this.appEvents.trigger('post-stream:refresh', { force: true });
},
finishedEditingTopic() {
if (!this.get('editingTopic')) { return; }
// save the modifications
const self = this,
props = this.get('buffered.buffer');
Topic.update(this.get('model'), props).then(function() {
// Note we roll back on success here because `update` saves
// the properties to the topic.
self.rollbackBuffer();
self.set('editingTopic', false);
}).catch(popupAjaxError);
},
toggledSelectedPost(post) {
this.performTogglePost(post);
},
toggledSelectedPostReplies(post) {
const selectedReplies = this.get('selectedReplies');
if (this.performTogglePost(post)) {
selectedReplies.addObject(post);
} else {
selectedReplies.removeObject(post);
}
},
deleteSelected() {
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), result => {
if (result) {
// If all posts are selected, it's the same thing as deleting the topic
if (this.get('allPostsSelected')) {
return this.deleteTopic();
}
const selectedPosts = this.get('selectedPosts');
const selectedReplies = this.get('selectedReplies');
const postStream = this.get('model.postStream');
Discourse.Post.deleteMany(selectedPosts, selectedReplies);
postStream.get('posts').forEach(p => {
if (this.postSelected(p)) {
p.set('deleted_at', new Date());
}
});
this.send('toggleMultiSelect');
}
});
},
mergePosts() {
bootbox.confirm(I18n.t("post.merge.confirm", { count: this.get('selectedPostsCount') }), result => {
if (result) {
const selectedPosts = this.get('selectedPosts');
Post.mergePosts(selectedPosts);
this.send('toggleMultiSelect');
}
});
},
expandHidden(post) {
post.expandHidden();
},
toggleVisibility() {
this.get('content').toggleStatus('visible');
},
toggleClosed() {
const topic = this.get('content');
this.get('content').toggleStatus('closed').then(result => {
topic.set('topic_status_update', result.topic_status_update);
});
},
recoverTopic() {
this.get('content').recover();
},
makeBanner() {
this.get('content').makeBanner();
},
removeBanner() {
this.get('content').removeBanner();
},
togglePinned() {
const value = this.get('model.pinned_at') ? false : true,
topic = this.get('content'),
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.get('content'),
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.get('content').toggleStatus('archived');
},
clearPin() {
this.get('content').clearPin();
},
togglePinnedForUser() {
if (this.get('model.pinned_at')) {
const topic = this.get('content');
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
}
}
},
replyAsNewTopic(post) {
const composerController = this.get('composer');
const { quoteState } = this;
const 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: Composer.REPLY_AS_NEW_PRIVATE_MESSAGE_KEY,
usernames: usernames
};
} else {
options = {
action: Composer.CREATE_TOPIC,
draftKey: Composer.REPLY_AS_NEW_TOPIC_KEY,
categoryId: this.get('model.category.id')
};
}
composerController.open(options).then(() => {
return Em.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() {
const self = this;
self.set('retrying', true);
this.get('model.postStream').refresh().then(function() {
self.set('retrying', false);
}, function() {
self.set('retrying', false);
});
},
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();
},
changePostOwner(post) {
this.get('selectedPosts').addObject(post);
this.send('changeOwner');
},
convertToPublicTopic() {
this.get('content').convertTopic("public");
},
convertToPrivateMessage() {
this.get('content').convertTopic("private");
}
},
_jumpToPostId(postId) {
if (!postId) {
Ember.Logger.warn("jump-post code broken - requested an index outside the stream array");
return;
}
this.appEvents.trigger('topic:jump-to-post', postId);
const topic = this.get('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');
}
},
canMergeTopic: function() {
if (!this.get('model.details.can_move_posts')) return false;
return this.get('selectedPostsCount') > 0;
}.property('selectedPostsCount'),
canSplitTopic: function() {
if (!this.get('model.details.can_move_posts')) return false;
if (this.get('allPostsSelected')) return false;
return this.get('selectedPostsCount') > 0;
}.property('selectedPostsCount'),
canChangeOwner: function() {
if (!Discourse.User.current() || !Discourse.User.current().admin) return false;
return this.get('selectedPostsUsername') !== undefined;
}.property('selectedPostsUsername'),
@computed('selectedPosts', 'selectedPostsCount', 'selectedPostsUsername')
canMergePosts(selectedPosts, selectedPostsCount, selectedPostsUsername) {
if (selectedPostsCount < 2) return false;
if (!selectedPosts.every(p => p.get('can_delete'))) return false;
return selectedPostsUsername !== undefined;
},
categories: function() {
return Discourse.Category.list();
}.property(),
canSelectAll: Em.computed.not('allPostsSelected'),
canDeselectAll: function () {
if (this.get('selectedPostsCount') > 0) return true;
if (this.get('allPostsSelected')) return true;
}.property('selectedPostsCount', 'allPostsSelected'),
canDeleteSelected: function() {
const selectedPosts = this.get('selectedPosts');
if (this.get('allPostsSelected')) return true;
if (this.get('selectedPostsCount') === 0) return false;
let canDelete = true;
selectedPosts.forEach(function(p) {
if (!p.get('can_delete')) {
canDelete = false;
return false;
}
});
return canDelete;
}.property('selectedPostsCount'),
hasError: Ember.computed.or('model.notFoundHtml', 'model.message'),
noErrorYet: Ember.computed.not('hasError'),
multiSelectChanged: function() {
// Deselect all posts when multi select is turned off
if (!this.get('multiSelect')) {
this.send('deselectAll');
}
}.observes('multiSelect'),
deselectPost(post) {
this.get('selectedPosts').removeObject(post);
const selectedReplies = this.get('selectedReplies');
selectedReplies.removeObject(post);
const selectedReply = selectedReplies.findBy('post_number', post.get('reply_to_post_number'));
if (selectedReply) { selectedReplies.removeObject(selectedReply); }
this.set('allPostsSelected', false);
},
postSelected(post) {
if (this.get('allPostsSelected')) { return true; }
if (this.get('selectedPosts').includes(post)) { return true; }
if (this.get('selectedReplies').findBy('post_number', post.get('reply_to_post_number'))) { return true; }
return false;
},
loadingHTML: function() {
return spinnerHTML;
}.property(),
recoverTopic() {
this.get('content').recover();
},
deleteTopic() {
this.get('content').destroy(Discourse.User.current());
},
// Receive notifications for this topic
subscribe() {
// Unsubscribe before subscribing again
this.unsubscribe();
const refresh = (args) => this.appEvents.trigger('post-stream:refresh', args);
this.messageBus.subscribe(`/topic/${this.get('model.id')}`, data => {
const topic = this.get('model');
if (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 "revised":
case "rebaked": {
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id }));
break;
}
case "deleted": {
postStream.triggerDeletedPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
break;
}
case "recovered": {
postStream.triggerRecoveredPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
break;
}
case "created": {
postStream.triggerNewPostInStream(data.id).then(() => refresh());
if (this.get('currentUser.id') !== data.user_id) {
Discourse.notifyBackgroundCountIncrement();
}
break;
}
case "move_to_inbox": {
topic.set("message_archived",false);
break;
}
case "archived": {
topic.set("message_archived",true);
break;
}
default: {
Em.Logger.warn("unknown topic bus message type", data);
}
}
if (topic.get('isPrivateMessage') &&
this.currentUser &&
this.currentUser.get('id') !== data.user_id &&
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: debounce(function(postNumber) {
const $post = $(`.topic-post article#post_${postNumber}`);
if ($post.length === 0 || isElementInViewport($post)) return;
$('body').animate({ scrollTop: $post.offset().top }, 1000);
}, 500),
unsubscribe() {
const topicId = this.get('content.id');
if (!topicId) return;
// there is a condition where the view never calls unsubscribe, navigate to a topic from a topic
this.messageBus.unsubscribe('/topic/*');
},
// Topic related
reply() {
this.replyToPost();
},
performTogglePost(post) {
const selectedPosts = this.get('selectedPosts');
if (this.postSelected(post)) {
this.deselectPost(post);
return false;
} else {
selectedPosts.addObject(post);
// If the user manually selects all posts, all posts are selected
this.set('allPostsSelected', selectedPosts.length === this.get('model.posts_count'));
return true;
}
},
readPosts(topicId, postNumbers) {
const topic = this.get("model");
const postStream = topic.get("postStream");
if (topic.get('id') === topicId) {
postStream.get('posts').forEach(post => {
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
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")) {
Em.run.next(() => topic.clearPin());
}
}
}
},
_showFooter: function() {
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
this.set("application.showFooter", showFooter);
}.observes("model.postStream.{loaded,loadedAllPosts}")
});