mirror of
https://github.com/discourse/discourse.git
synced 2025-02-19 17:54:59 +08:00

posts can be in the DOM in a non-ready state between willInsertElement and didInsertElement didInsertElement takes care of the scrolling, ignore analysing anything about that post until its properly inserted
549 lines
16 KiB
JavaScript
549 lines
16 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.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");
|
|
}
|
|
|
|
// Disable animation for now so it performs better
|
|
bg.width(progressWidth);
|
|
}.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'),
|
|
|
|
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;
|
|
} 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() {
|
|
|
|
this.unbindScrolling();
|
|
$(window).unbind('resize.discourse-on-scroll');
|
|
|
|
// Unbind link tracking
|
|
this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
|
|
|
|
this.get('controller').set('onPostRendered', null);
|
|
|
|
this.resetExamineDockCache();
|
|
|
|
// this happens after route exit, stuff could have trickled in
|
|
this.set('controller.controllers.header.showExtraInfo', false);
|
|
},
|
|
|
|
didInsertElement: function(e) {
|
|
this.bindScrolling({debounce: 0});
|
|
|
|
var topicView = this;
|
|
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); });
|
|
|
|
var controller = this.get('controller');
|
|
controller.set('onPostRendered', function(){
|
|
topicView.postsRendered.apply(topicView);
|
|
});
|
|
|
|
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
|
return Discourse.ClickTrack.trackClick(e);
|
|
});
|
|
|
|
this.updatePosition(true);
|
|
},
|
|
|
|
debounceLoadSuggested: Discourse.debounce(function(){
|
|
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
|
|
|
|
var incoming = this.get('topicTrackingState.newIncoming');
|
|
var suggested = this.get('topic.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'),
|
|
|
|
// Triggered whenever any posts are rendered, debounced to save over calling
|
|
postsRendered: Discourse.debounce(function() {
|
|
this.updatePosition(false);
|
|
}, 50),
|
|
|
|
resetRead: function(e) {
|
|
Discourse.ScreenTrack.instance().reset();
|
|
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 = this.getPost($post);
|
|
|
|
if (post) {
|
|
var 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[0].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[0].post_number;
|
|
_.each(result.posts,function(post) {
|
|
var newPost;
|
|
newPost = Discourse.Post.create(post, topicView.get('topic'));
|
|
if (post.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[this.topic.posts.length-1].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(_.map(result.posts,function(p) {
|
|
return Discourse.Post.create(p, topic);
|
|
}));
|
|
if (result.suggested_topics) {
|
|
var suggested = Em.A();
|
|
_.each(result.suggested_topics,function(topic) {
|
|
suggested.pushObject(Discourse.Topic.create(topic));
|
|
});
|
|
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) {
|
|
Discourse.ScreenTrack.instance().scrolled();
|
|
var model = this.get('controller.model');
|
|
if (model) {
|
|
this.set('controller.currentPost', opts.currentPost);
|
|
}
|
|
},500),
|
|
|
|
scrolled: function(){
|
|
this.updatePosition(true);
|
|
},
|
|
|
|
updatePosition: function(userActive) {
|
|
|
|
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; }
|
|
|
|
// top on screen
|
|
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
|
|
this.prevPage($(rows[0]));
|
|
}
|
|
|
|
// bottom of screen
|
|
var currentPost;
|
|
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
|
|
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.nonUrgentPositionUpdate({
|
|
userActive: userActive,
|
|
currentPost: currentPost || currentForPositionUpdate
|
|
});
|
|
} else {
|
|
console.error("can't update position ");
|
|
}
|
|
|
|
var offset = window.pageYOffset || $('html').scrollTop();
|
|
var firstLoaded = this.get('firstPostLoaded');
|
|
if (!this.docAt) {
|
|
var 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
|
|
var $lastPost = $('.last-post');
|
|
var 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: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>"
|
|
};
|
|
|
|
category = this.get('controller.content.category');
|
|
if (category) {
|
|
opts.catLink = Discourse.Utilities.categoryLink(category);
|
|
} else {
|
|
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("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 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;
|
|
}
|
|
});
|