/** * Views used in constructing AoPS Blogs * * Relies heavily on views used for forums; many functions are mixed in throughout * these views. An understanding of Views.TopicFull and Views.Post will be very * helpful in sorting out what's going on here. **/ AoPS.Community.Views = (function (ComViews) { var Lang = AoPS.Community.Lang, Modal = AoPS.Ui.Modal; /** * Construct a loader for the blogs. We have the AoPS default as * the background image on this div, but users will be able to change that * with their own CSS. **/ ComViews.buildBlogLoader = function () { return $('
'); }; /** * Build a scrollbar in the blogs that isn't in default community style. * Used in the shoutbox currently. */ ComViews.buildBlogScrollbar = function ($obj) { var scrollbar_defaults = { slider_end_tolerance: 2, stop_scroll_propagation: true, }, scrollbar_args = arguments.length === 1 ? scrollbar_defaults : _.extend(scrollbar_defaults, arguments[1]), $scrollwrap = $obj.wrapWithScroll(scrollbar_args); /** Extra div needed for firefox centering and scrolling to work with stuff in the scrollbar **/ $scrollwrap.addClass("blog-scroll-outer"); return $scrollwrap; }; ComViews.toggleBlogSubscription = function (obj) { var new_text = obj.blog.get("is_subscribed") ? Lang["Subscribe"] : Lang["Unsubscribe"]; obj.blog.set("is_subscribed", !obj.blog.get("is_subscribed")); obj.user.setBlogSubscriptionStatus({ blog_id: obj.blog.get("category_id"), status: obj.blog.get("is_subscribed"), }); AoPS.Ui.Flyout.display( Lang[ obj.blog.get("is_subscribed") ? "blog-subscribed-notify" : "blog-unsubscribed-notify" ] ); $("#blog-subscribe")[0].title = Lang[ obj.blog.get("is_subscribed") ? "blog-unsubscribe-tooltip" : "blog-subscribe-tooltip" ]; $("#blog-subscribe").text(new_text); }; /** * Top bit of a blog, very simple. **/ ComViews.CategoryCellBlogHeading = AoPS.View.extend({ template_id: "#blog-category-cell-heading-tpl", id: "header", initialize: function () { this.$el.html( this.getTemplate(this.template_id, { blog_title: this.model.get("category_name"), category_id: this.model.get("category_id"), }) ); }, }); /** * A list of Blog Topics; may be filtered by tag. * * Collection : FilteredTopicList * * Each Topic in the list produces a TopicCellBlog */ ComViews.TopicsListBlog = AoPS.View.extend({ template_id: "#blog-topic-list-tpl", id: "main", initialize: function (options) { var self = this, no_topics_msg; this.blog = options.blog; // eslint-disable-next-line eqeqeq var is_blog_category = options.blog == this.collection.category; no_topics_msg = Lang[is_blog_category ? "blog-no-topics" : "blog-no-results"]; this.$no_topics = $( '
' + no_topics_msg + "
" ); this.$el.html( this.getTemplate(this.template_id, { can_post: this.blog.getPermission("c_can_start_topic"), lang_post_new: Lang["blog-post-new-entry"], }) ); this.topic_cells = []; this.$loader = ComViews.buildBlogLoader(); // LOOP through existing topics in collection, add them. this.collection.each(function (topic) { self.addTopic(topic); }); if (this.collection.length === 0) { this.$el.append(this.$no_topics); } this.listenTo(this.collection, "add", this.addTopic); this.listenTo(this.collection, "remove", this.removeTopic); // When we scroll to the bottom of the browser, we go get more topics. $(window).scroll(function () { if (self.$el.is(":visible")) { if ( window.innerHeight + window.pageYOffset + 50 >= $(document).height() ) { self.fetchMoreTopics(); } } }); }, events: { "click #post-new-entry": "onClickNewTopic", }, /** * onClickNewTopic fires up the post topic atop the page in a modal. */ onClickNewTopic: function (e) { this.openNewBlogPost(this.blog); e.preventDefault(); e.stopPropagation(); }, openNewBlogPost: function (category) { if (!AoPS.session.logged_in) { AoPS.Ui.Modal.showMessage(Lang["new-topic-not-logged-in"]); } else if (!category.getPermission("c_can_start_topic")) { AoPS.Ui.Modal.showMessage(Lang["cat-cell-no-perm-start-topic"]); } else { // eslint-disable-next-line no-new new ComViews.NewBlogPost({ model: category, master: category.get("master"), }); } }, /** * Any time this collection has a new topic, we build a new cell for the topic. * * @param AoPS.Community.Models.Topic **/ addTopic: function (topic) { var new_topic = new ComViews.TopicCellBlog({ model: topic, category: this.blog, }), i, first_post_time = topic.get("first_post_time"), n_topics = this.topic_cells.length, topic_placed = false, current_topic; // placing the topic in the DOM in the right place for (i = 0; i < n_topics; i++) { current_topic = this.topic_cells[i]; if (current_topic.model.get("first_post_time") < first_post_time) { current_topic.$el.before(new_topic.$el); this.topic_cells.splice(i, 0, new_topic); topic_placed = true; i = n_topics; } } if (!topic_placed) { // Put this topic at the end. this.topic_cells.push(new_topic); this.$el.append(new_topic.$el); } this.$loader.detach(); this.$no_topics.detach(); }, removeTopic: function (topic) { var topic_cell = _.find(this.topic_cells, function (cell) { return cell.model === topic; }); if (!_.isUndefined(topic_cell)) { this.topic_cells = _.without(this.topic_cells, topic_cell); topic_cell.close(); } }, /** * Go get more topics for this awesome blog! **/ fetchMoreTopics: function () { var self = this; if (!this.collection.topics_loading) { if (!this.collection.all_topics_fetched) { this.$el.append(this.$loader); this.collection.fetchMoreTopics({ onFinish: _.bind(function () { this.$loader.detach(); }, this), onError: function (data) { var msg; self.$loader.detach(); if ( typeof Lang["topic-fetch-err-" + data.error_code] === "string" ) { msg = Lang["topic-fetch-err-" + data.error_code]; } else { msg = Lang["unexpected-error-code"] + data.error_code; } // Error; don't want to try fetching any more. self.collection.all_topics_fetched = true; ComViews.showError(msg); }, }); } } }, }); ComViews.BlogTopicTagbox = AoPS.Community.Views.TopicTagbox.extend({ always_show_edit: false, }); /** * A single Topic in the blog: we render the full first post of the topic. * * model : AoPS.Community.Models.Topic */ ComViews.TopicCellBlog = AoPS.View.extend({ template_id: "#blog-topic-cell-tpl", push_state_attribute: "data-blog", staticTagboxView: AoPS.Community.Views.BlogTopicTagbox, editableTagboxView: AoPS.Community.Views.EditableTagBoxExistingTopic, initialize: function (options) { this.category = options.category; this.buildRoutes(); this.render(); this.listenTo(this.model, "change:num_posts", this.buildNumComments); this.listenTo(this.model, "change:locked", this.render); this.listenTo( this.model.get("posts").first(), "change:post_rendered", this.render ); }, render: function () { var can_edit = this.model.getPermission("c_can_edit"), can_moderate = this.model.getPermission("c_can_lock_topic") || this.model.getPermission("c_can_delete"); this.has_poll = this.model.get("poll_id") > 0; this.first_post = this.model.get("posts").first(); this.$el.html( this.getTemplate(this.template_id, { username: this.model.get("first_poster_name"), user_id: this.model.get("first_poster_id"), date: AoPS.Community.Utils.makePrettyTimeStatic( this.model.get("first_post_time") ), lang_permanent_link: Lang["permanent-link"], lang_by: Lang["by"], has_settleable_reports: this.model.get("num_reports") > 0 && this.model.getPermission("c_can_settle_report"), route_url: AoPS.Community.Views.makeLinkUrl(this.route_url), topic_title: this.model.get("topic_title"), can_reply: !this.model.get("locked") && this.model.getPermission("c_can_reply"), //get from permissions route_reply: AoPS.Community.Views.makeLinkUrl(this.comment_url), //TODO post_comment_text: this.category.get("blog_post_comment_text"), //get from category lang_edit_post: Lang["blog-edit-post"], lang_moderate_post: Lang["blog-moderate-post"], has_mod_actions: can_edit || can_moderate, can_edit: can_edit, has_poll: this.has_poll, can_moderate: can_moderate, topic_id: this.model.get("topic_id"), has_tags: this.category.get("show_tags"), //(this.model.get('tags').length > 0), }) ); this.finishRenderingPost(); if (this.category.get("show_tags")) { this.constructStaticTagbox(); this.tagbox_state = "static"; } if (this.has_poll) { this.constructPoll(); } this.buildNumComments(); this.checkSearchHighlighting(); }, /** * We build polls in blogs by mixing in the relevant pieces of ComViews.Post. * * To get this all to work, we make a poll_object property of this view * such that poll_object has all the pieces needed by the functions we're * mixing in (initializePoll, constructPoll, onClickPollVote, onClickTogglePollResults). ***/ constructPoll: function () { this.poll_object = { $el: this.$el, topic: { model: this.model, }, $: this.$, listenTo: this.listenTo, getTemplate: this.getTemplate, constructPoll: ComViews.Post.prototype.constructPoll, }; ComViews.Post.prototype.initializePoll.apply(this.poll_object); }, onClickPollVote: function () { ComViews.Post.prototype.onClickPollVote.apply(this.poll_object); }, onClickTogglePollResults: function () { ComViews.Post.prototype.onClickTogglePollResults.apply(this.poll_object); }, checkSearchHighlighting: function () { var search_text; if (this.model.get("is_search_result")) { search_text = this.model.fetchSearchText("post_text"); if (search_text.length > 0) { this.$(".entry h1").extendedHighlightText( "cmty-highlight", search_text ); this.$(".message").extendedHighlightText( "cmty-highlight", search_text ); this.$(".cmty-tags-itembox-wrapper").extendedHighlightText( "cmty-highlight", search_text ); /*if (this.model.get('post_id') === this.topic.model.get('focus_post').get('post_id')) {// pre-open hidden text in focus text this.$('.cmty-hide-heading').trigger('click'); }*/ } } }, /** Mixed in to BlogComment. **/ finishRenderingPost: function () { // pywindow causes this to be rendered separately. We will put this back in handlebars if we // stick with onclick var $m = this.$(".message"); $m.html(this.first_post.get("post_rendered")); this.parseEditData(); this.listenTo( this.first_post, "change:last_edit_time_rendered", this.parseEditData ); this.$attachments = this.$(".cmty-post-attachments"); this.highlight($m); this.parseAttachments(); this.listenTo( this.first_post, "change:attachment change:attachments", this.parseAttachments ); }, events: { "click .blog-edit-post": "onClickEdit", "click .blog-moderate-topic": "onClickModerate", "click .cmty-itembox .cmty-edit-tag": "toggleTagbox", "click .cmty-poll-results-toggle span": "onClickTogglePollResults", "click .cmty-poll-vote-row .btn": "onClickPollVote", }, /* eslint-disable no-undef */ highlight: function ($m) { if ($m.length > 0 && typeof Prism !== "undefined") { Prism.plugins.autoloader.languages_path = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/"; var elements = $m[0].querySelectorAll("pre code"); for (var i = 0; i < elements.length; i++) { Prism.highlightElement(elements[i]); } } }, /* eslint-enable no-undef */ parseEditData: function () { ComViews.Post.prototype.parseEditData.apply({ $el: this.$el, model: this.first_post, watching_edit_time: false, }); }, parseAttachments: function () { ComViews.Post.prototype.parseAttachments.apply({ $el: this.$el, model: this.first_post, $attachments: this.$attachments, isAttachmentImage: ComViews.Post.prototype.isAttachmentImage, }); }, /** * We mix in the Views.Post.onClickEdit function here; that function * only uses the "model" and "topic" properties of View.Post, * so we create an object with those properties here, and apply * the function to the new object. **/ onClickEdit: function (e) { var obj; e.stopPropagation(); e.preventDefault; obj = { topic: this, model: this.model.get("posts").first(), }; AoPS.Community.Views.Post.prototype.onClickEdit.apply(obj, [e]); }, /** * Throws up a modal for moderating this topic. **/ onClickModerate: function (e) { e.stopPropagation(); e.preventDefault(); // eslint-disable-next-line no-new new AoPS.Community.Views.ModerateTopic({ model: this.model, topic_category_at_initialization: this.model.get("category_id"), }); }, constructStaticTagbox: AoPS.Community.Views.TopicFull.prototype.constructStaticTagbox, constructEditableTagbox: AoPS.Community.Views.TopicFull.prototype.constructEditableTagbox, toggleTagbox: AoPS.Community.Views.TopicFull.prototype.toggleTagbox, /** * Called in the mixed-in functions above; we don't need to do anything here. **/ setHeight: function () { return; }, /** * Create the number of comments text **/ buildNumComments: function () { var num_comments_text, num_comments = this.model.get("num_posts") - 1; if (num_comments === 0) { num_comments_text = this.category.get("blog_no_comments_text"); } else if (num_comments === 1) { num_comments_text = this.category.get("blog_one_comment_text"); } else { num_comments_text = this.category .get("blog_comments_text") .replace("%s", num_comments); } this.$el.find(".post-replies").text(num_comments_text); }, /** * Build the routes that will be used in the template **/ buildRoutes: function () { var title = "_" + ComViews.convertToUrlFragment(this.model.get("topic_title")), route_base = "/c" + this.model.get("category_id") + "h" + this.model.get("topic_id"); this.route_url = route_base + title; this.comment_url = route_base + "s3" + title; }, }); /** * Sidebar panel of a blog. Constructed from pieces the user chooses. **/ ComViews.BlogSidebar = AoPS.View.extend({ template_id: "#blog-sidebar-tpl", id: "side", initialize: function () { // ADD title. Always shown? // if (this.model.get('show_custom_block')) { this.sidebar_title = new AoPS.Community.Views.BlogSidebarTitle({ model: this.model, }); this.$el.append(this.sidebar_title.$el); // } this.archive = new AoPS.Community.Views.BlogArchive({ model: this.model, }); this.$el.append(this.archive.$el); // Add Shoutbox if (this.model.get("show_shoutbox")) { this.shoutbox = new AoPS.Community.Views.BlogShoutbox({ model: this.model.get("shoutbox_topic"), }); this.$el.append(this.shoutbox.$el); } // Only show the contributors if it's not silly (more than one contributor) if ( this.model.get("show_contributors") && this.model.get("contributors").length > 1 ) { this.contributors = new AoPS.Community.Views.BlogContributors({ model: this.model, }); this.$el.append(this.contributors.$el); } if (this.model.get("show_tags")) { this.tags = new AoPS.Community.Views.BlogTagbox({ model: this.model, }); this.$el.append(this.tags.$el); } // Add user profile if (this.model.get("show_profile_info")) { this.about_owner = new AoPS.Community.Views.BlogAboutOwner({ model: this.model, }); this.$el.append(this.about_owner.$el); } // Add stats if (this.model.get("show_stats")) { this.stats = new AoPS.Community.Views.BlogStats({ model: this.model, }); this.$el.append(this.stats.$el); } this.search = new AoPS.Community.Views.BlogSearch({ model: this.model, }); this.$el.append(this.search.$el); // TODO : RSS; Maybe spike? }, onClose: function () { this.sidebar_title.close(); if (this.hasOwnProperty("about_owner")) { this.about_owner.close(); } if (this.hasOwnProperty("stats")) { this.stats.close(); } if (this.hasOwnProperty("contributors")) { this.contributors.close(); } if (this.hasOwnProperty("shoutbox")) { this.shoutbox.close(); } if (this.hasOwnProperty("tags")) { this.tags.close(); } this.search.close(); }, }); ComViews.BlogContributors = AoPS.View.extend({ template_id: "#blog-contributors-tpl", id: "contrib-widget", className: "widget", initialize: function () { var contributors = _.map(this.model.get("contributors"), function (user) { return ( '' + user.username + "" ); }).join(" • "); this.$el.html( this.getTemplate(this.template_id, { lang_contributors: Lang["blog-contributors-title"], contributors: contributors, }) ); }, }); /** * Used to build the tagbox on the side. **/ ComViews.BlogItembox = AoPS.Community.Views.Itembox.extend({ scrollbarCreator: AoPS.Community.Views.buildBlogScrollbar, push_state_attribute: "data-blog", }); /** * Creates the title box at the top. **/ ComViews.BlogSidebarTitle = AoPS.View.extend({ template_id: "#blog-sidebar-title-tpl", id: "user-menu-widget", className: "block user_menu", initialize: function () { var user_route = AoPS.Community.Constants.user_path + this.model.get("created_by"); this.$el.html( this.getTemplate(this.template_id, { description: this.model.get("short_description"), avatar: this.model.get("creator_avatar"), user_route: user_route, username: this.model.get("creator_username"), }) ); }, }); /** * Holds the details for the blog owner. */ ComViews.BlogAboutOwner = AoPS.View.extend({ template_id: "#blog-about-owner-tpl", id: "about-owner-widget", className: "block widget", initialize: function () { // TODO: Sort out location crap when we have it on the back end. this.$el.html( this.getTemplate(this.template_id, { lang_about_owner: Lang["blog-about-owner"], lang_posts: Lang["blog-posts"], posts_count: this.model.get("creator_num_posts"), joined_date: AoPS.Community.Utils.makePrettyTimeStaticDateOnly( this.model.get("creator_joined_time") ), lang_joined: Lang["blog-joined"], has_location: this.model.get("creator_location").length > 0, lang_location: Lang["blog-location"], location: this.model.get("creator_location"), }) ); }, }); /** * Holds the stats for the blog. */ ComViews.BlogStats = AoPS.View.extend({ template_id: "#blog-stats-tpl", id: "stats-widget", className: "block widget", initialize: function () { // TODO: Sort out location crap when we have it on the back end. this.render(); this.listenTo( this.model, "change:num_topics change:num_posts", this.render ); }, render: function () { this.$el.html( this.getTemplate(this.template_id, { lang_stats: Lang["blog-stats"], lang_created: Lang["blog-created"], lang_entries: Lang["blog-entries"], lang_visits: Lang["blog-visits"], lang_comments: Lang["blog-comments"], created_date: this.model.get("created_at"), num_entries: this.model.get("num_topics"), num_visits: this.model.get("num_views"), num_comments: this.model.get("num_posts") - this.model.get("num_topics"), }) ); }, }); ComViews.BlogSearch = AoPS.View.extend({ template_id: "#blog-search-tpl", id: "search-widget", className: "block widget", initialize: function () { this.$el.html( this.getTemplate(this.template_id, { lang_search_heading: Lang["blog-search-heading"], lang_search_placeholder: Lang["blog-search-placeholder"], lang_search_button: Lang["blog-search-button"], lang_search_advanced: Lang["blog-search-advanced"], lang_search_advanced_tooltip: Lang["blog-search-advanced-tooltip"], blog_id: this.model.get("category_id"), }) ); }, events: { "click a": "onClickAdvancedSearch", "submit #blog_searchform": "onSearch", }, onClickAdvancedSearch: function (e) { Modal.showMessage("Made you look! This is not implemented yet."); e.stopPropagation(); e.preventDefault(); }, onKeyDownSearch: function (e) { var key_pressed = e.which || e.keyCode, target; if (key_pressed === 13) { target = e.currentTarget.value; if (target.length > 0) { this.launchSearch(target.length); // un-comment below to turn search input back on e.currentTarget.value = ""; } } }, launchSearch: function (term) { $("#blog_keywords").val(""); console.log(term); console.log( "/c" + this.model.get("category_id") + "/" + encodeURIComponent(term) ); Backbone.history.navigate( "/c" + this.model.get("category_id") + "/" + encodeURIComponent(term), {trigger: true} ); }, onSearch: function (e) { var term = $("#blog_keywords").val(); if (term.length > 0) { this.launchSearch(term); } e.stopPropagation(); e.preventDefault(); }, }); /** * Archive for a blog. * * Model : Community.Models.Topic of the topic that is the shoutbox for this * blog * * All topics for a blog are sorted into expandable menus by month, for easy * navigation. **/ ComViews.BlogArchive = AoPS.View.extend({ template_id: "#blog-archive-tpl", id: "archive-widget", className: "widget", initialize: function () { var archive_topics = this.model.get("blog_archive_topics"); archive_topics = this.convertNumericalMonthsToWords(archive_topics); this.$el.html( this.getTemplate(this.template_id, { lang_archive: Lang["blog-archive-title"], category_id: this.model.get("category_id"), monthly_topic_data: archive_topics, }) ); }, events: { "click .archive-month-header": "toggleMonthMenu", }, // Converts month "02" to February, for example. Uses community language file. convertNumericalMonthsToWords: function (topic_data) { for (var i = 0; i < topic_data.length; i++) { topic_data[i].month_name = Lang["month-" + topic_data[i].month]; } return topic_data; }, // Expand/collapse month menus in archive. toggleMonthMenu: function (e) { var $element = $(e.currentTarget), $month_menu = $element.siblings(".archive-month-topics"), $menu_image = $element.find(".month-image"); if ($month_menu.hasClass("archive-month-collapsed")) { $menu_image.attr("src", "/m/community/img/blogs/hyperion/minus.gif"); $menu_image.attr("alt", "-"); $month_menu.removeClass("archive-month-collapsed"); } else { $menu_image.attr("src", "/m/community/img/blogs/hyperion/plus.gif"); $menu_image.attr("alt", "+"); $month_menu.addClass("archive-month-collapsed"); } }, }); /** * Shoutbox for a blog. * * Model : Community.Models.Topic of the topic that is the shoutbox for this * blog * * The html structure for the shoutbox was set in AoPS2.0, and had to be matched * to allow user CSS to work. * * The posts in a shout topic are displayed with the most recent on top, which * is the opposite of what we typically do in topics. So, a lot of things * are reversed. We filter by those posts which are "show_from_end" posts, * and a lot of the signs/directions in the addPost function are the opposite * of those in Views.TopicFull.addPost() **/ ComViews.BlogShoutbox = AoPS.View.extend({ template_id: "#blog-shoutbox-tpl", id: "shouts-widget", className: "widget", initialize: function () { var self = this; var can_shout = this.model.getPermission("c_can_shout"); this.$el.html( this.getTemplate(this.template_id, { lang_shoutbox: Lang["blog-shoutbox-title"], can_shout: can_shout, lang_Submit: Lang["Submit"], }) ); this.$loader = ComViews.buildBlogLoader(); this.$shouts_outer = this.$el.find("fieldset"); this.$shout_list = $(""); this.$num_shouts = this.$el.find(".blog-num-shouts"); this.setNumShouts(); this.listenTo(this.model, "change:num_posts", this.setNumShouts); this.$shouts_outer.prepend( AoPS.Community.Views.buildBlogScrollbar(this.$shout_list) ); this.$scrollbar = this.$shouts_outer.find(".aops-scroll-bar"); this.posts = []; this.topic = this.model.get("posts").each(function (post) { if (post.get("post_number") > 1) { self.addPost(post); } }); this.applyPostClasses(); this.$scrollbar.on("slider_at_end", function () { self.fetchMorePosts(); }); this.listenTo(this.model.get("posts"), "add", this.addPost); this.listenTo( this.model.get("posts"), "change:deleted", this.onDeletePost ); if (can_shout) { this.$post_box = this.$el.find("#message"); this.$chars_left = this.$el.find("#charsleft"); this.$shout_helper = this.$el.find("#shout-helper"); this.$post_box.on("keydown", function (e) { var key_pressed = e.which || e.keyCode; if (key_pressed === 13 && (e.ctrlKey || e.metaKey)) { self.onClickSubmit(); } }); this.$post_box.on("keyup", _.bind(this.onKeyPressShout, this)); } // Allows us to mix-in AoPS.Community.Views.NewReply this.settings = { category_id: this.model.get("category_id"), topic_id: this.model.get("topic_id"), topic: this.model, master: this.model.get("master"), onSubmit: function () { self.listenToOnce( self.model, "new_post_processed", self.onSubmittedPostProcessed ); }, }; this.extra_options = {}; this.model.startRealtimePostMonitor({ hash: this.model.get("master").get("watcher_hash"), source: "master", }); }, events: { "click #submit": "onClickSubmit", }, /** * We show the user how many characters they have left in their shout. **/ onKeyPressShout: function (e) { var input_length = this.$post_box.val().length, characters_left = 0, character_message; this.$shout_helper.toggle(input_length > 0); if (input_length > AoPS.Community.Constants.max_shout_length) { this.$post_box.val( this.$post_box .val() .substr(0, AoPS.Community.Constants.max_shout_length) ); } else { characters_left = AoPS.Community.Constants.max_shout_length - input_length; } character_message = characters_left === 1 ? Lang["blog-one-char-left"] : Lang["blog-characters-left"]; this.$chars_left.html(characters_left + " " + character_message); }, no_permission_lang_property: "blog-shout-no-permission", no_permission_limited_lang_property: "blog-shout-no-permission-limited", onClickSubmit: function () { AoPS.Community.Views.NewReply.prototype.submitPost.apply(this); }, checkCommonErrors: AoPS.Community.Views.PostingEnviron.prototype.checkCommonErrors, throwLatexErrorMessage: function (code) { var message = Lang["blog-shout-err-" + code]; AoPS.Community.Views.showError(message); }, throwBlockingMessage: function () { ComViews.throwLoaderBlockingMessage(Lang["blog-shout-blocker"]); }, onSubmittedPostProcessed: function () { AoPS.Ui.Modal.closeAllModals(); }, onFinishSubmit: function () { this.$post_box.val(""); this.$shout_helper.hide(); this.listenToOnce(this.model.get("posts"), "add", this.applyPostClasses); }, showError: AoPS.Community.Views.NewReply.prototype.showError, checkPost: function () { var category; // Also, how do we want to organize constants like the 8 below. if ( this.$post_box.val().length < AoPS.Community.Constants.min_post_length ) { this.showError(Lang["new-topic-post-too-short"]); return false; } if (AoPS.session.user_id === this.model.get("last_poster_id")) { category = this.model .get("master") .fetchCategory(this.model.get("category_id")); // A little lazy here :( Hijacking c_can_edit_core_data -- // Blog owners and site admins can multi-shout. if ( _.isUndefined(category) || !category.getPermission("c_can_edit_core_data") ) { this.showError(Lang["blog-no-shoutbox-spamming"]); return false; } } return true; }, filterPost: function (post) { return post.get("show_from_end"); }, applyPostFilter: function () { var self = this; _.each(this.posts, function (post) { post.$el.toggle(self.filterPost(post.model)); }); this.applyPostClasses(); }, setNumShouts: function () { // We subtract 1 for the placeholder. var num_shouts = this.model.get("num_posts") - 1; this.$num_shouts.html( num_shouts + " " + (num_shouts === 1 ? Lang["blog-shout"] : Lang["blog-shouts"]) ); }, /** * Add classes to the visible posts to give the zebra pattern **/ applyPostClasses: function () { var row_one = true, n_posts = this.posts.length, i; for (i = 0; i < n_posts; i++) { if (this.filterPost(this.posts[i].model)) { this.posts[i].$el.removeClass(row_one ? "row2" : "row1"); this.posts[i].$el.addClass(row_one ? "row1" : "row2"); row_one = !row_one; } } }, /** * Add a post in the appropriate place * * @param ComModels.Post **/ addPost: function (post) { var new_post = new ComViews.BlogShout({ model: post, }), i, post_number = post.get("post_number"), current_post, n_posts = this.posts.length, post_placed = false; // placing the post in the DOM to decide if we should show it. new_post.$el.hide(); for (i = 0; i < n_posts; i++) { current_post = this.posts[i]; if (current_post.model.get("post_number") < post_number) { current_post.$el.before(new_post.$el); this.posts.splice(i, 0, new_post); if (this.filterPost(post)) { new_post.$el.show(); } post_placed = true; i = n_posts; } } if (!post_placed) { // Put this post at the end. this.posts.push(new_post); this.$shout_list.append(new_post.$el); if (this.filterPost(post)) { new_post.$el.show(); } } }, fetchMorePosts: function () { if ( this.model.get("all_posts_fetched") || this.model.get("is_fetching_posts") ) { return; } this.$shout_list.append(this.$loader); this.model.fetchMorePosts({ start_post_num: _.last( this.model.get("posts").where({ show_from_end: true, }) ).get("post_number"), onFinish: _.bind(function () { this.applyPostFilter(); this.$loader.detach(); }, this), direction: "backwards", // The shoutbox is in reverse order of a typical topic show_from_setting: "show_from_end", }); }, /** * When a post is deleted from the shoutbox, we need to remove * its view from the posts array and then reset the classes. **/ onDeletePost: function (post) { var post_view = _.findWhere(this.posts, { model: post, }); this.posts = _.without(this.posts, post_view); this.applyPostClasses(); post_view.close(); }, }); /** * A single shout in the shoutbox. * * model : AoPS.Community.Models.Post **/ ComViews.BlogShout = AoPS.View.extend({ tagName: "li", template_id: "#blog-shout-tpl", className: "blog-shout", initialize: function () { var user_route = AoPS.Community.Constants.user_path + this.model.get("poster_id"); this.$el.html( this.getTemplate(this.template_id, { lang_by: Lang["by"], post_rendered: this.model.get("post_rendered"), username: this.model.get("username"), date: this.model.get("date_rendered"), user_route: user_route, can_delete: this.model.get("topic").getPermission("c_can_delete"), }) ); }, events: { "click .blog-delete-shout": "onClickDelete", }, onClickDelete: AoPS.Community.Views.Post.prototype.onClickDelete, }); /** * Tagbox for a blog * * model : AoPS.Community.Models.CategoryBlog */ ComViews.BlogTagbox = AoPS.View.extend({ template_id: "#blog-tagbox-tpl", initialize: function () { this.$el.html( this.getTemplate(this.template_id, { lang_tags: Lang["blog-Tags"], }) ); this.tagbox = new AoPS.Community.Views.BlogItembox({ model: this.model, }); this.$el.find("fieldset").append(this.tagbox.$el); }, onClose: function () { this.tagbox.close(); }, }); /** * View for a single comment on a Blog Post. * * model : AoPS.Community.Models.Post * * Many functions are mixed in from AoPS.Community.Views.Post. * This View is much simpler than Views.Post, and has a few bits that * are a very different structure, so rather than extend Post (or the reverse), * we mix in the functions we need. */ ComViews.BlogComment = AoPS.View.extend({ template_id: "#blog-comment-tpl", className: "comment", // TODO : Permanent link to post initialize: function (options) { this.topic = options.topic; this.render(); this.listenTo(this.model, "change:deleted", this.onChangeDeleted); this.listenTo(this.model, "hard_delete", this.removePostFromTopic); this.listenTo(this.model, "change:post_rendered", this.render); }, events: { "click .blog-edit-post": "onClickEdit", "click .blog-delete-post": "onClickDelete", "click .blog-report-post": "onClickReport", "click .cmty-post-deleted-info": "onClickDeletedInfo", "click .cmty-undelete-post": "onClickUndelete", "click .blog-direct-post-link": "onClickDirectLink", }, render: function () { var report_data = this.constructReportTemplateData(), can_edit = this.topic.model.getPermission("c_can_edit"), can_delete = this.topic.model.getPermission("c_can_delete"), has_mod_actions = report_data.can_report || can_edit || can_delete; this.$el.html( this.getTemplate(this.template_id, { lang_by: Lang["by"], is_reported: report_data.is_reported, highlight_report_button: report_data.highlight_report_button, date: AoPS.Community.Utils.makePrettyTimeStatic( this.model.get("post_time") ), username: this.model.get("username"), route_user: AoPS.Community.Constants.user_path + this.model.get("poster_id"), has_mod_actions: has_mod_actions, can_report: report_data.can_report, can_delete: can_delete, can_undelete: this.model.get("topic").getPermission("c_can_undelete"), can_edit: can_edit, report_title: report_data.title, lang_edit: Lang["blog-edit-post"], lang_delete: Lang["blog-delete-post"], lang_report: Lang["blog-report-post"], }) ); this.first_post = this.model; this.setVisibility(); this.finishRenderingPost(); }, finishRenderingPost: ComViews.TopicCellBlog.prototype.finishRenderingPost, parseEditData: ComViews.TopicCellBlog.prototype.parseEditData, parseAttachments: ComViews.TopicCellBlog.prototype.parseAttachments, highlight: ComViews.TopicCellBlog.prototype.highlight, onClickEdit: AoPS.Community.Views.Post.prototype.onClickEdit, onClickDelete: AoPS.Community.Views.Post.prototype.onClickDelete, onClickReport: AoPS.Community.Views.Post.prototype.onClickReport, onClickDirectLink: AoPS.Community.Views.Post.prototype.onClickDirectLink, constructReportTemplateData: AoPS.Community.Views.Post.prototype.constructReportTemplateData, setVisibility: AoPS.Community.Views.Post.prototype.setVisibility, onClickDeletedInfo: AoPS.Community.Views.Post.prototype.onClickDeletedInfo, onClickUndelete: AoPS.Community.Views.Post.prototype.onClickUndelete, onChangeDeleted: AoPS.Community.Views.Post.prototype.onChangeDeleted, removePostFromTopic: AoPS.Community.Views.Post.prototype.removePostFromTopic, }); /** * A full blog post and its comments. * * model : AoPS.Community.Topic */ ComViews.BlogTopicFull = AoPS.View.extend({ postView: AoPS.Community.Views.BlogComment, id: "main", url_cmty_path: "", initialize: function (options) { var blog_post, i, post, posts_length = this.model.get("posts").length; this.focus_post_id = options.post_id; this.reply_open = false; this.reply_box = null; // Post new topic this.$el.html( this.getTemplate("#blog-topic-list-tpl", { can_post: this.model .get("category") .getPermission("c_can_start_topic"), lang_post_new: Lang["blog-post-new-entry"], }) ); // Initial blog post blog_post = new ComViews.TopicCellBlog({ model: this.model, category: this.model.get("category"), }); this.$el.append(blog_post.$el); this.$el.append( this.getTemplate("#blog-comments-header-tpl", { lang_post_comment: Lang["blog-post-comment"], can_comment: !this.model.get("locked") && this.model.getPermission("c_can_reply"), //TODO : what if user turns off replies? }) ); if (this.model.getPermission("c_can_reply")) { this.$reply_holder = this.$el.find(".cmty-reply-window"); } this.$num_comments = this.$el.find(".blog-num-comments"); this.renderNumComments(); this.$loader = ComViews.buildBlogLoader(); this.$posts_box = this.$el.find(".blog-posts-box"); this.posts = []; for (i = 1; i < posts_length; i++) { post = this.model.get("posts").models[i]; if ( !post.get("deleted") || this.model.getPermission("c_can_read_deleted") ) { this.addPost(post); } } this.applyPostClasses(); this.listenTo( this.model, "change:all_posts_fetched", this.onAllPostsFetched ); this.listenTo(this.model, "change:num_posts", this.renderNumComments); this.listenTo(this.model, "change:locked", this.onChangeLocked); this.listenTo(this.model, "change:deleted", this.onDelete); this.listenTo(this.model.get("posts"), "add", this.addPost); if (!this.model.get("all_posts_fetched")) { this.fetchAllPosts(); } this.listenTo( this.model.get("posts"), "change:deleted", this.onDeletedPost ); // Router can trigger the reply to open this.$el.on( "open_reply", _.bind(function () { this.openReplyWindow(); }, this) ); }, events: { "click .blog-post-comment": "onClickPostComment", "click .post-comment": "onClickPostComment", "click a#post-new-entry": "onClickNewTopic", }, onChangeLocked: function () { var can_reply = !this.model.get("locked") && this.model.getPermission("c_can_reply"); this.$el.find(".blog-post-comment").toggle(can_reply); this.$el.find(".blog-reply-window").toggle(can_reply); }, /** * If the full topic has already been loaded prior to initialization, or if we * somehow can navigate within the blog to a particular post_id, then * we need onAddToPage to tell the already-created view to navigate to the * correct post. **/ onAddToPage: function (options) { if (options.hasOwnProperty("post_id")) { this.focus_post_id = options.post_id; } if (this.model.get("all_posts_fetched")) { this.goToFocusPost(); } }, /** * When user clicks comment, we open up the reply window. **/ onClickPostComment: function (e) { e.stopPropagation(); e.preventDefault(); this.openReplyWindow(); }, /** * Open the window in which users post comments to the blog. **/ openReplyWindow: function () { if (this.reply_open) { return; } this.$reply_holder.addClass("blog-reply-open"); this.$reply_holder.show(); this.reply_open = true; this.reply_box = new ComViews.NewReply({ topic_id: this.model.get("topic_id"), category_id: this.model.get("category_id"), topic: this.model, has_add_to_feed: false, has_email_subscribe: true, master: this.model.get("master"), onSubmit: _.bind(function () { // Will probably edit when we have to deal with // possible errors. this.closeNewReply(); // Here, we throw a modal and watch for new_post_processed, then this.listenToOnce( this.model, "new_post_processed", this.onSubmittedPostProcessed ); }, this), onCancel: _.bind(function () { this.closeNewReply(); }, this), }); this.$reply_holder.append(this.reply_box.$el); this.reply_box.$post_box.focus(); }, /** * Once the submitted post is processed, an event will trigger a jump to that post * (or to the oldest non-fetched post, which will almost always be this one). **/ onSubmittedPostProcessed: function (response) { AoPS.Ui.Modal.closeAllModals(); this.focus_post_id = response.post_id; this.goToFocusPost(); }, /** * closeNewReply resets the page to what it should look like without the NewReply window open. */ closeNewReply: function () { this.reply_open = false; this.$reply_holder.removeClass("blog-reply-open"); this.reply_box.close(); }, /** * Once we've loaded everything, we spike the loader, set the classes on the posts, * and set up any future adds to reset the post classes. We don't set that listener * at the start because we don't want it to fire over and over on the initial post load. **/ onAllPostsFetched: function () { this.$loader.detach(); this.applyPostClasses(); this.listenTo(this.model.get("posts"), "add", this.applyPostClasses); if (this.focus_post_id > 0) { this.goToFocusPost(); } }, onClickNewTopic: function (e) { this.openNewBlogPost(this.model.get("category")); e.preventDefault(); e.stopPropagation(); }, openNewBlogPost: ComViews.TopicsListBlog.prototype.openNewBlogPost, /** * If we are trying to load to a particular post, here's where we move the window * scroll to the appropriate place. **/ goToFocusPost: function () { var target_post_cell, target_id = this.focus_post_id; if (target_id === 0) { return; } target_post_cell = _.find(this.posts, function (post_cell) { return post_cell.model.get("post_id") === target_id; }); if (_.isUndefined(target_post_cell)) { return; } window.scrollTo(0, target_post_cell.$el.offset().top); }, removePost: ComViews.TopicFull.prototype.removePost, /** * If a post is deleted, we need to remove its cell from our view, and * reset the post classes to keep the zebra pattern. **/ onDeletedPost: function (post) { return; // var deleted_post = _.findWhere(this.posts, { // model: post, // }); // if (!_.isUndefined(deleted_post)) { // this.posts = _.without(this.posts, deleted_post); // deleted_post.close(); // this.applyPostClasses(); // } }, /** * We apply the same zebra pattern here as in the shoutbox. **/ applyPostClasses: AoPS.Community.Views.BlogShoutbox.prototype.applyPostClasses, /** * Build the number of comments line in the initial post box. **/ renderNumComments: function () { var num_comments = this.model.get("num_posts") - 1; this.$num_comments.html( num_comments + (num_comments === 1 ? Lang["blog-Comment"] : Lang["blog-Comments"]) ); }, addPost: AoPS.Community.Views.TopicFull.prototype.addPost, /** * No deleted posts should make it to the front end, but just in case... */ filterPost: function (post) { return !post.get("deleted") || this.model.getPermission("c_can_undelete"); }, /** * Put up a loader and then go get all the comments for this blog post. **/ fetchAllPosts: function () { this.$posts_box.append(this.$loader); /*this.model.fetchInitialPosts({ fetch_all : true });*/ }, onDelete: ComViews.TopicFull.prototype.onDelete, navigateAfterDelete: ComViews.TopicFull.prototype.navigateAfterDelete, }); /** * The only difference between a new blog post and a new topic in a forum is the * topic_type field, which we want when we submit this post for processing. **/ ComViews.NewBlogPost = ComViews.NewTopic.extend({ topic_type: "blog_post", has_add_to_feed: false, sending_blocker: Lang["new-blog-post-send-blocker"], has_email_subscribe: true, }); return ComViews; })(AoPS.Community.Views || {});