/** This view is for rendering an icon representing the status of a topic @class TopicView @extends Discourse.View @namespace Discourse @uses Discourse.Scrolling @module Discourse **/ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { templateName: 'topic', topicBinding: 'controller.content', userFiltersBinding: 'controller.userFilters', classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.category.secure:secure_category'], progressPosition: 1, menuVisible: true, SHORT_POST: 1200, // Update the progress bar using sweet animations updateBar: function() { var $topicProgress, bg, currentWidth, progressWidth, ratio, totalWidth; if (!this.get('topic.loaded')) return; $topicProgress = $('#topic-progress'); if (!$topicProgress.length) return; ratio = this.get('progressPosition') / this.get('topic.filtered_posts_count'); totalWidth = $topicProgress.width(); progressWidth = ratio * totalWidth; bg = $topicProgress.find('.bg'); bg.stop(true, true); currentWidth = bg.width(); if (currentWidth === totalWidth) { bg.width(currentWidth - 1); } if (progressWidth === totalWidth) { bg.css("border-right-width", "0px"); } else { bg.css("border-right-width", "1px"); } if (currentWidth === 0) { bg.width(progressWidth); } else { bg.animate({ width: progressWidth }, 400); } }.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'), updateTitle: function() { var title; title = this.get('topic.title'); if (title) return Discourse.set('title', title); }.observes('topic.loaded', 'topic.title'), currentPostChanged: function() { var current = this.get('controller.currentPost'); var topic = this.get('topic'); if (!(current && topic)) return; if (current > (this.get('maxPost') || 0)) { this.set('maxPost', current); } var postUrl = topic.get('url'); if (current > 1) { postUrl += "/" + current; } else { if (this.get('controller.bestOf')) { postUrl += "/best_of"; } } Discourse.URL.replaceState(postUrl); // Show appropriate jump tools if (current === 1) { $('#jump-top').attr('disabled', true); } else { $('#jump-top').attr('disabled', false); } if (current === this.get('topic.highest_post_number')) { $('#jump-bottom').attr('disabled', true); } else { $('#jump-bottom').attr('disabled', false); } }.observes('controller.currentPost', 'controller.bestOf', 'topic.highest_post_number'), composeChanged: function() { var composerController = Discourse.get('router.composerController'); composerController.clearState(); composerController.set('topic', this.get('topic')); }.observes('composer'), // This view is being removed. Shut down operations willDestroyElement: function() { var screenTrack, controller; this.unbindScrolling(); $(window).unbind('resize.discourse-on-scroll'); controller = this.get('controller'); controller.unsubscribe(); controller.set('onPostRendered', null); screenTrack = this.get('screenTrack'); if (screenTrack) { screenTrack.stop(); } this.set('screenTrack', null); this.resetExamineDockCache(); // this happens after route exit, stuff could have trickled in this.set('controller.controllers.header.showExtraInfo', false) }, didInsertElement: function(e) { var topicView = this; this.bindScrolling({debounce: 0}); $(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); }); var controller = this.get('controller'); controller.subscribe(); controller.set('onPostRendered', function(){ topicView.postsRendered.apply(topicView); }); // Insert our screen tracker var screenTrack = Discourse.ScreenTrack.create({ topic_id: this.get('topic.id') }); screenTrack.start(); this.set('screenTrack', screenTrack); this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) { return Discourse.ClickTrack.trackClick(e); }); this.updatePosition(true); // Watch all incoming topic changes this.get('topicTrackingState').trackIncoming("all"); }, hasNewSuggested: function(){ var incoming = this.get('topicTrackingState.newIncoming'); var suggested = this.get('topic.suggested_topics'); if(suggested) { var lookup = incoming.slice(-5).reverse().unique(); if(lookup.length < 5) { suggested.each(function(topic){ if (topic) { lookup.push(topic.get('id')); lookup = lookup.unique(); return lookup.length < 5; } }); } Discourse.TopicList.loadTopics(lookup, "").then(function(topics){ suggested.clear(); suggested.pushObjects(topics); }); } }.observes('topicTrackingState.incomingCount'), // Triggered whenever any posts are rendered, debounced to save over calling postsRendered: Discourse.debounce(function() { this.set('renderedPosts', $('.topic-post')); this.updatePosition(false); }, 50), resetRead: function(e) { this.get('screenTrack').cancel(); this.set('screenTrack', null); this.get('controller').unsubscribe(); var topicView = this; this.get('topic').resetRead().then(function() { topicView.set('controller.message', Em.String.i18n("topic.read_position_reset")); topicView.set('controller.loaded', false); }); }, gotFocus: function(){ if (Discourse.get('hasFocus')){ this.scrolled(); } }.observes("Discourse.hasFocus"), getPost: function($post){ var post, postView; postView = Ember.View.views[$post.prop('id')]; if (postView) { return postView.get('post'); } return null; }, // Called for every post seen, returns the post number postSeen: function($post) { var post, postNumber, screenTrack; post = this.getPost($post); if (post) { postNumber = post.get('post_number'); if (postNumber > (this.get('topic.last_read_post_number') || 0)) { this.set('topic.last_read_post_number', postNumber); } if (!post.get('read')) { post.set('read', true); } return post.get('post_number'); } }, observeFirstPostLoaded: (function() { var loaded, old, posts; posts = this.get('topic.posts'); // TODO topic.posts stores non ember objects in it for a period of time, this is bad loaded = posts && posts[0] && posts[0].post_number === 1; // I avoided a computed property cause I did not want to set it, over and over again old = this.get('firstPostLoaded'); if (loaded) { if (old !== true) { this.set('firstPostLoaded', true); } } else { if (old !== false) { this.set('firstPostLoaded', false); } } }).observes('topic.posts.@each'), // Load previous posts if there are some prevPage: function($post) { var postView = Ember.View.views[$post.prop('id')]; if (!postView) return; var post = postView.get('post'); if (!post) return; // We don't load upwards from the first page if (post.post_number === 1) return; // double check if (this.topic && this.topic.posts && this.topic.posts.length > 0 && this.topic.posts.first().post_number !== post.post_number) return; // half mutex if (this.get('controller.loading')) return; this.set('controller.loading', true); this.set('controller.loadingAbove', true); var opts = $.extend({ postsBefore: post.get('post_number') }, this.get('controller.postFilters')); var topicView = this; return Discourse.Topic.find(this.get('topic.id'), opts).then(function(result) { var lastPostNum, posts; posts = topicView.get('topic.posts'); // Add a scrollTo record to the last post inserted to the DOM lastPostNum = result.posts.first().post_number; result.posts.each(function(p) { var newPost; newPost = Discourse.Post.create(p, topicView.get('topic')); if (p.post_number === lastPostNum) { newPost.set('scrollTo', { top: $(window).scrollTop(), height: $(document).height() }); } return posts.unshiftObject(newPost); }); topicView.set('controller.loading', false); return topicView.set('controller.loadingAbove', false); }); }, fullyLoaded: (function() { return this.get('controller.seenBottom') || this.get('topic.at_bottom'); }).property('topic.at_bottom', 'controller.seenBottom'), // Load new posts if there are some nextPage: function($post) { if (this.get('controller.loading') || this.get('controller.seenBottom')) return; return this.loadMore(this.getPost($post)); }, postCountChanged: function() { this.set('controller.seenBottom', false); }.observes('topic.highest_post_number'), loadMore: function(post) { if (!post) return; if (this.get('controller.loading')) return; // Don't load if we know we're at the bottom if (this.get('topic.highest_post_number') === post.get('post_number')) return; if (this.get('controller.seenBottom')) return; // Don't double load ever if (this.topic.posts.last().post_number !== post.post_number) return; this.set('controller.loadingBelow', true); this.set('controller.loading', true); var opts = $.extend({ postsAfter: post.get('post_number') }, this.get('controller.postFilters')); var topicView = this; var topic = this.get('controller.content'); return Discourse.Topic.find(topic.get('id'), opts).then(function(result) { if (result.at_bottom || result.posts.length === 0) { topicView.set('controller.seenBottom', 'true'); } topic.pushPosts(result.posts.map(function(p) { return Discourse.Post.create(p, topic); })); if (result.suggested_topics) { var suggested = Em.A(); result.suggested_topics.each(function(st) { suggested.pushObject(Discourse.Topic.create(st)); }); topicView.set('topic.suggested_topics', suggested); } topicView.set('controller.loadingBelow', false); return topicView.set('controller.loading', false); }); }, cancelEdit: function() { // close editing mode this.set('editingTopic', false); }, finishedEdit: function() { // TODO: This should be in a controller and use proper text fields var topicView = this; if (this.get('editingTopic')) { var topic = this.get('topic'); // retrieve the title from the text field var newTitle = $('#edit-title').val(); // retrieve the category from the combox box var newCategoryName = $('#topic-title select option:selected').val(); // manually update the titles & category topic.setProperties({ title: newTitle, fancy_title: newTitle, categoryName: newCategoryName }); // save the modifications topic.save().then(function(result){ // update the title if it has been changed (cleaned up) server-side var title = result.basic_topic.fancy_title; topic.setProperties({ title: title, fancy_title: title }); }, function(error) { topicView.set('editingTopic', true); if (error && error.responseText) { bootbox.alert($.parseJSON(error.responseText).errors[0]); } else { bootbox.alert(Em.String.i18n('generic_error')); } }); // close editing mode topicView.set('editingTopic', false); } }, editTopic: function() { if (!this.get('topic.can_edit')) return false; // enable editing mode this.set('editingTopic', true); return false; }, showFavoriteButton: function() { return Discourse.User.current() && !this.get('topic.isPrivateMessage'); }.property('topic.isPrivateMessage'), resetExamineDockCache: function() { this.docAt = null; this.dockedTitle = false; this.dockedCounter = false; }, updateDock: function(postView) { if (!postView) return; var post = postView.get('post'); if (!post) return; this.set('progressPosition', post.get('index')); }, nonUrgentPositionUpdate: Discourse.debounce(function(opts){ var screenTrack = this.get('screenTrack'); if(opts.userActive && screenTrack) { screenTrack.scrolled(); } this.set('controller.currentPost', opts.currentPost); },500), scrolled: function(){ this.updatePosition(true); }, updatePosition: function(userActive) { var $lastPost, firstLoaded, lastPostOffset, offset, title, info, rows, screenTrack, _this, currentPost; _this = this; rows = this.get('renderedPosts'); if (!rows || rows.length === 0) { return; } info = Discourse.Eyeline.analyze(rows); // if we have no rows if(!info) { return; } // top on screen if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) { this.prevPage($(rows[0])); } // bottom of screen if(info.bottom === rows.length-1) { currentPost = _this.postSeen($(rows[info.bottom])); this.nextPage($(rows[info.bottom])); } // update dock this.updateDock(Ember.View.views[rows[info.bottom].id]); // mark everything on screen read $.each(info.onScreen,function(){ var seen = _this.postSeen($(rows[this])); currentPost = currentPost || seen; }); this.nonUrgentPositionUpdate({ userActive: userActive, currentPost: currentPost || this.getPost($(rows[info.bottom])).get('post_number') }); offset = window.pageYOffset || $('html').scrollTop(); firstLoaded = this.get('firstPostLoaded'); if (!this.docAt) { title = $('#topic-title'); if (title && title.length === 1) { this.docAt = title.offset().top; } } var headerController = this.get('controller.controllers.header'); if (this.docAt) { headerController.set('showExtraInfo', offset >= this.docAt || !firstLoaded); } else { headerController.set('showExtraInfo', !firstLoaded); } // there is a whole bunch of caching we could add here $lastPost = $('.last-post'); lastPostOffset = $lastPost.offset(); if (!lastPostOffset) return; if (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()) { if (!this.dockedCounter) { $('#topic-progress-wrapper').addClass('docked'); this.dockedCounter = true; } } else { if (this.dockedCounter) { $('#topic-progress-wrapper').removeClass('docked'); this.dockedCounter = false; } } }, topicTrackingState: function(){ return Discourse.TopicTrackingState.current(); }.property(), browseMoreMessage: (function() { var category, opts; opts = { latestLink: "" + (Em.String.i18n("topic.view_latest_topics")) + "" }; if (category = this.get('controller.content.category')) { opts.catLink = Discourse.Utilities.categoryLink(category); } else { opts.catLink = "" + (Em.String.i18n("topic.browse_all_categories")) + ""; } var tracking = this.get('topicTrackingState'); var unreadTopics = tracking.countUnread(); var newTopics = tracking.countNew(); if (newTopics + unreadTopics > 0) { if(category) { return I18n.messageFormat("topic.read_more_in_category_MF", {"UNREAD": unreadTopics, "NEW": newTopics, catLink: opts.catLink}) } else { return I18n.messageFormat("topic.read_more_MF", {"UNREAD": unreadTopics, "NEW": newTopics, latestLink: opts.latestLink}) } } else if (category) { return Ember.String.i18n("topic.read_more_in_category", opts); } else { return Ember.String.i18n("topic.read_more", opts); } }).property('topicTrackingState.messageCount') }); Discourse.TopicView.reopenClass({ // Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not. scrollTo: function(topicId, postNumber, callback) { // Make sure we're looking at the topic we want to scroll to var existing, header, title, expectedOffset; if (parseInt(topicId, 10) !== parseInt($('#topic').data('topic-id'), 10)) return false; existing = $("#post_" + postNumber); if (existing.length) { if (postNumber === 1) { $('html, body').scrollTop(0); } else { header = $('header'); title = $('#topic-title'); expectedOffset = title.height() - header.find('.contents').height(); if (expectedOffset < 0) { expectedOffset = 0; } $('html, body').scrollTop(existing.offset().top - (header.outerHeight(true) + expectedOffset)); } return true; } return false; } });