import BufferedContent 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 debounce from 'discourse/lib/debounce'; import isElementInViewport from "discourse/lib/is-element-in-viewport"; import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from 'ember-addons/ember-computed-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'; export default Ember.Controller.extend(BufferedContent, { composer: Ember.inject.controller(), application: Ember.inject.controller(), multiSelect: false, selectedPostIds: null, editingTopic: false, 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, canRemoveTopicFeaturedLink: Ember.computed.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 (!Ember.isEmpty(title)) { // force update lazily loaded titles this.send('refreshTitle'); } }, @computed('site.mobileView', 'model.posts_count') showSelectedPostsAtBottom(mobileView, postsCount) { return mobileView && postsCount > 3; }, @computed('model.postStream.posts', 'model.postStream.postsWithPlaceholders') postsToRender(posts, postsWithPlaceholders) { return this.capabilities.isAndroid ? posts : postsWithPlaceholders; }, @computed('model.postStream.loadingFilter') androidLoading(loading) { return this.capabilities.isAndroid && loading; }, @computed('model') pmPath(topic) { return this.currentUser && this.currentUser.pmPath(topic); }, init() { this._super(); this.setProperties({ selectedPostIds: [], 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); }, @computed selectedQuery() { return post => this.postSelected(post); }, @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.includes(categoryId); }, @computed('model') featuredLinkDomain(topic) { return extractLinkMeta(topic).domain; }, @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'); 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, 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.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); 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 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).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.get('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); } }, // 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 || !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('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-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 => { 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.get("composer"); const composerModel = composer.get("model"); const opts = { 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) { return bootbox.alert(I18n.t("bookmarks.not_bookmarked")); } else 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')); }, toggleMultiSelect() { this.toggleProperty('multiSelect'); this.appEvents.trigger('post-stream:refresh', { force: true }); }, selectAll() { this.set('selectedPostIds', [...this.get('model.postStream.stream')]); this.appEvents.trigger('post-stream:refresh', { force: true }); }, deselectAll() { this.set('selectedPostIds', []); this.appEvents.trigger('post-stream:refresh', { force: true }); }, togglePostSelection(post) { const selected = this.get('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.get('selectedPostIds').pushObjects([post.id, ...replyIds]); this.appEvents.trigger('post-stream:refresh', { force: true }); }); }, selectBelow(post) { const stream = [...this.get('model.postStream.stream')]; const below = stream.slice(stream.indexOf(post.id)); this.get('selectedPostIds').pushObjects(below); this.appEvents.trigger('post-stream:refresh', { force: true }); }, deleteSelected() { const user = this.currentUser; 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('selectedAllPosts')) return this.deleteTopic(); Post.deleteMany(this.get('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.get('selectedPostsCount') }), result => { if (result) { Post.mergePosts(this.get("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'); }, 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.get('editingTopic')) { return; } // save the modifications const props = this.get('buffered.buffer'); Topic.update(this.get('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.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, quotedText) { const composerController = this.get('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: 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() { 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() { this.get('content').convertTopic("public"); }, convertToPrivateMessage() { this.get('content').convertTopic("private"); }, removeFeaturedLink() { this.set('buffered.featured_link', null); } }, _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'); } }, hasError: Ember.computed.or('model.notFoundHtml', 'model.message'), noErrorYet: Ember.computed.not('hasError'), categories: Ember.computed.alias('site.categoriesList'), selectedPostsCount: Ember.computed.alias('selectedPostIds.length'), @computed('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); }, @computed('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; }, @computed('selectedPostsCount', 'model.postStream.stream.length') selectedAllPosts(selectedPostsCount, postsCount) { return selectedPostsCount >= postsCount; }, canSelectAll: Ember.computed.not('selectedAllPosts'), canDeselectAll: Ember.computed.alias('selectedAllPosts'), @computed('selectedPostsCount', 'selectedAllPosts', 'selectedPosts', 'selectedPosts.[]') canDeleteSelected(selectedPostsCount, selectedAllPosts, selectedPosts) { return selectedPostsCount > 0 && (selectedAllPosts || selectedPosts.every(p => p.can_delete)); }, @computed('canMergeTopic', 'selectedAllPosts', 'selectedPosts', 'selectedPosts.[]') canSplitTopic(canMergeTopic, selectedAllPosts, selectedPosts) { return canMergeTopic && !selectedAllPosts && selectedPosts.length > 0 && selectedPosts.sort((a, b) => a.post_number - b.post_number)[0].post_type === 1; }, @computed('model.details.can_move_posts', 'selectedPostsCount') canMergeTopic(canMovePosts, selectedPostsCount) { return canMovePosts && selectedPostsCount > 0; }, @computed('currentUser.admin', 'selectedPostsCount', 'selectedPostsUsername') canChangeOwner(isAdmin, selectedPostsCount, selectedPostsUsername) { return isAdmin && selectedPostsCount > 0 && selectedPostsUsername !== undefined; }, @computed('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.get('selectedAllPost') || this.get('selectedPostIds').includes(post.id); }, @computed loadingHTML() { return spinnerHTML; }, recoverTopic() { this.get('content').recover(); }, deleteTopic() { this.get('content').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.get('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 "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() { // never unsubscribe when navigating from topic to topic if (!this.get("content.id")) return; this.messageBus.unsubscribe('/topic/*'); }, reply() { this.replyToPost(); }, 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.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")) { Ember.run.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); } });