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/views/topic_view.js
Robin Ward d23ef1d090 FIX: You could update a topic to have a title that's too short if the TextCleaner
removed extra characters. Additionally, updating the title will not return an error
message to the client app if the operation fails (rather than failing silently.)
2013-05-31 15:24:13 -04:00

521 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");
}
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);
},
// 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: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>"
};
if (category = this.get('controller.content.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) {
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;
}
});