discourse/app/assets/javascripts/discourse/views/topic_view.js

426 lines
13 KiB
JavaScript

/**
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.model',
userFiltersBinding: 'controller.userFilters',
classNameBindings: ['controller.multiSelect:multi-select',
'topic.archetype',
'topic.category.read_restricted:read_restricted',
'topic.deleted:deleted-topic'],
menuVisible: true,
SHORT_POST: 1200,
postStream: Em.computed.alias('controller.postStream'),
updateBar: function() {
Em.run.scheduleOnce('afterRender', this, 'updateProgressBar');
}.observes('controller.streamPercentage'),
updateProgressBar: function() {
var $topicProgress = $('#topic-progress');
if (!$topicProgress.length) return;
var totalWidth = $topicProgress.width();
var progressWidth = this.get('controller.streamPercentage') * totalWidth;
$topicProgress.find('.bg')
.css("border-right-width", (progressWidth === totalWidth) ? "0px" : "1px")
.width(progressWidth);
},
updateTitle: function() {
var 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; }
// TODO: @Robin, this should all be integrated into the router,
// the view should not be performing routing work
//
// This workaround ensures the router is aware the route changed,
// without it, the up button was broken on long topics.
// To repro, go to a topic with 50 posts, go to first post,
// scroll to end, click up button ... nothing happens
var handler =_.first(
_.where(Discourse.URL.get("router.router.currentHandlerInfos"),
function(o) {
return o.name === "topic.fromParams";
})
);
if(handler){
handler.context = {nearPost: current};
}
Discourse.URL.replaceState(postUrl);
}.observes('controller.currentPost', 'highest_post_number'),
composeChanged: function() {
var composerController = Discourse.get('router.composerController');
composerController.clearState();
composerController.set('topic', this.get('topic'));
}.observes('composer'),
enteredTopic: function() {
if (this.present('controller.enteredAt')) {
var topicView = this;
Em.run.schedule('afterRender', function() {
topicView.updatePosition();
});
}
}.observes('controller.enteredAt'),
didInsertElement: function(e) {
this.bindScrolling({debounce: 0});
var topicView = this;
Em.run.schedule('afterRender', function () {
$(window).resize('resize.discourse-on-scroll', function() {
topicView.updatePosition();
});
});
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
if ($(e.target).hasClass('mention')) { return false; }
return Discourse.ClickTrack.trackClick(e);
});
},
// This view is being removed. Shut down operations
willDestroyElement: function() {
this.unbindScrolling();
$(window).unbind('resize.discourse-on-scroll');
// Unbind link tracking
this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
this.resetExamineDockCache();
// this happens after route exit, stuff could have trickled in
this.set('controller.controllers.header.showExtraInfo', false);
},
debounceLoadSuggested: Discourse.debounce(function(){
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
var incoming = this.get('topicTrackingState.newIncoming');
var suggested = this.get('topic.details.suggested_topics');
var topicId = this.get('topic.id');
if(suggested) {
var existing = _.invoke(suggested, 'get', 'id');
var lookup = _.chain(incoming)
.last(5)
.reverse()
.union(existing)
.uniq()
.without(topicId)
.first(5)
.value();
Discourse.TopicList.loadTopics(lookup, "").then(function(topics){
suggested.clear();
suggested.pushObjects(topics);
});
}
}, 1000),
hasNewSuggested: function(){
this.debounceLoadSuggested();
}.observes('topicTrackingState.incomingCount'),
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 = this.getPost($post);
if (post) {
var postNumber = post.get('post_number');
if (postNumber > (this.get('controller.last_read_post_number') || 0)) {
this.set('controller.last_read_post_number', postNumber);
}
if (!post.get('read')) {
post.set('read', true);
}
return post.get('post_number');
}
},
resetExamineDockCache: function() {
this.set('docAt', false);
},
updateDock: function(postView) {
if (!postView) return;
var post = postView.get('post');
if (!post) return;
this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1);
},
throttledPositionUpdate: Discourse.debounce(function() {
Discourse.ScreenTrack.current().scrolled();
var model = this.get('controller.model');
if (model && this.get('nextPositionUpdate')) {
this.set('controller.currentPost', this.get('nextPositionUpdate'));
}
},500),
scrolled: function(){
this.updatePosition();
},
/**
Process the posts the current user has seen in the topic.
@private
@method processSeenPosts
**/
processSeenPosts: function() {
var rows = $('.topic-post.ready');
if (!rows || rows.length === 0) { return; }
// if we have no rows
var info = Discourse.Eyeline.analyze(rows);
if(!info) { return; }
// are we scrolling upwards?
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
var $body = $('body');
var $elem = $(rows[0]);
var distToElement = $body.scrollTop() - $elem.position().top;
this.get('postStream').prependMore().then(function() {
Em.run.next(function () {
$('html, body').scrollTop($elem.position().top + distToElement);
});
});
}
// are we scrolling down?
var currentPost;
if(info.bottom === rows.length-1) {
currentPost = this.postSeen($(rows[info.bottom]));
this.get('postStream').appendMore();
}
// update dock
this.updateDock(Ember.View.views[rows[info.bottom].id]);
// mark everything on screen read
var topicView = this;
_.each(info.onScreen,function(item){
var seen = topicView.postSeen($(rows[item]));
currentPost = currentPost || seen;
});
var currentForPositionUpdate = currentPost;
if (!currentForPositionUpdate) {
var postView = this.getPost($(rows[info.bottom]));
if (postView) { currentForPositionUpdate = postView.get('post_number'); }
}
if (currentForPositionUpdate) {
this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
this.throttledPositionUpdate();
} else {
console.error("can't update position ");
}
},
/**
The user has scrolled the window, or it is finished rendering and ready for processing.
@method updatePosition
**/
updatePosition: function() {
this.processSeenPosts();
var offset = window.pageYOffset || $('html').scrollTop();
if (!this.get('docAt')) {
var title = $('#topic-title');
if (title && title.length === 1) {
this.set('docAt', title.offset().top);
}
}
var headerController = this.get('controller.controllers.header'),
topic = this.get('controller.model');
if (this.get('docAt')) {
headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded'));
} else {
headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded'));
}
// Dock the counter if necessary
var $lastPost = $('article[data-post-id=' + topic.get('postStream.lastPostId') + "]");
var lastPostOffset = $lastPost.offset();
if (!lastPostOffset) {
this.set('controller.dockedCounter', false);
return;
}
this.set('controller.dockedCounter', (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()));
},
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),
browseMoreMessage: function() {
var opts = {
latestLink: "<a href=\"/\">" + (I18n.t("topic.view_latest_topics")) + "</a>"
};
var category = this.get('controller.content.category');
if (category) {
opts.catLink = Discourse.Utilities.categoryLink(category);
} else {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>";
}
var tracking = this.get('topicTrackingState');
var unreadTopics = tracking.countUnread();
var newTopics = tracking.countNew();
if (newTopics + unreadTopics > 0) {
var 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);
}
}.property('topicTrackingState.messageCount')
});
Discourse.TopicView.reopenClass({
// Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
jumpToPost: function(topicId, postNumber, avoidScrollIfPossible) {
Em.run.scheduleOnce('afterRender', function() {
var rows = $('.topic-post.ready');
// Make sure we're looking at the topic we want to scroll to
if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }
var $post = $("#post_" + postNumber);
if ($post.length) {
var postTop = $post.offset().top;
var highlight = true;
var header = $('header');
var title = $('#topic-title');
var expectedOffset = title.height() - header.find('.contents').height();
if (expectedOffset < 0) {
expectedOffset = 0;
}
var offset = (header.outerHeight(true) + expectedOffset);
var windowScrollTop = $('html, body').scrollTop();
if (avoidScrollIfPossible && postTop > windowScrollTop + offset && postTop < windowScrollTop + $(window).height() + 100) {
// in view
} else {
// not in view ... bring into view
if (postNumber === 1) {
$(window).scrollTop(0);
highlight = false;
} else {
var desired = $post.offset().top - offset;
$(window).scrollTop(desired);
// TODO @Robin, I am seeing multiple events in chrome issued after
// jumpToPost if I refresh a page, sometimes I see 2, sometimes 3
//
// 1. Where are they coming from?
// 2. On refresh we should only issue a single scrollTop
// 3. If you are scrolled down in BoingBoing desired sometimes is wrong
// due to vanishing header, we should not be rendering it imho until after
// we render the posts
var first = true;
var t = new Date();
// console.log("DESIRED:" + desired);
var enforceDesired = function(){
if($(window).scrollTop() !== desired) {
console.log("GOT EVENT " + $(window).scrollTop());
console.log("Time " + (new Date() - t));
console.trace();
if(first) {
$(window).scrollTop(desired);
first = false;
}
// $(document).unbind("scroll", enforceDesired);
}
};
// uncomment this line to help debug this issue.
// $(document).scroll(enforceDesired);
}
}
if(highlight) {
var $contents = $('.topic-body .contents', $post);
var origColor = $contents.data('orig-color') || $contents.css('backgroundColor');
$contents.data("orig-color", origColor);
$contents
.addClass('highlighted')
.stop()
.animate({ backgroundColor: origColor }, 2500, 'swing', function(){
$contents.removeClass('highlighted');
});
}
}
});
}
});