diff --git a/Changelog.md b/Changelog.md index 721209c86ef697906227581db14b7377ab3764b3..003bb91e3ca5a233b5edb4871ab33c06561e8ef7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -57,6 +57,7 @@ With the port to Bootstrap 3, app/views/terms/default.haml has a new structure. * Improve accessibility of a couple pages [#6227](https://github.com/diaspora/diaspora/pull/6227) * Capitalize "Powered by diaspora" [#6254](https://github.com/diaspora/diaspora/pull/6254) * Display username and avatar for NSFW posts in mobile view [#6245](https://github.com/diaspora/diaspora/6245) +* Prevent multiple comment boxes on mobile [#6363](https://github.com/diaspora/diaspora/pull/6363) ## Features * Support color themes [#6033](https://github.com/diaspora/diaspora/pull/6033) diff --git a/app/assets/javascripts/helpers/i18n.js b/app/assets/javascripts/helpers/i18n.js index 9be574c459e155380a64b165755c86d322dafaca..f273165d4688b14768f94e1f62cb8f956bb18cd0 100644 --- a/app/assets/javascripts/helpers/i18n.js +++ b/app/assets/javascripts/helpers/i18n.js @@ -77,9 +77,12 @@ Diaspora.I18n = { reset: function() { this.locale.data = {}; + this.locale.fallback.data = {}; - if( arguments.length > 0 && !(_.isEmpty(arguments[0])) ) + if(arguments.length > 0 && !(_.isEmpty(arguments[0]))) { this.locale.data = arguments[0]; + this.locale.fallback.data = arguments[0]; + } } }; // @license-end diff --git a/app/assets/javascripts/mobile/mobile_comments.js b/app/assets/javascripts/mobile/mobile_comments.js index ed50b4d7f7045d197044a9606118cd8e1d295b78..3e6300b5f727897f81d07ac6862068c90b1a7523 100644 --- a/app/assets/javascripts/mobile/mobile_comments.js +++ b/app/assets/javascripts/mobile/mobile_comments.js @@ -4,162 +4,180 @@ * the COPYRIGHT file. */ -$(document).ready(function() { +(function() { + Diaspora.Mobile = {}; + Diaspora.Mobile.Comments = { + initialize: function() { + var self = this; + $(".stream").on("tap click", "a.back_to_stream_element_top", function() { + var bottomBar = $(this).closest(".bottom_bar").first(); + var streamElement = bottomBar.parent(); + $("html, body").animate({ + scrollTop: streamElement.offset().top - 54 + }, 1000); + }); - $(".stream").on("tap click", "a.back_to_stream_element_top", function() { - var bottomBar = $(this).closest(".bottom_bar").first(); - var streamElement = bottomBar.parent(); - $("html, body").animate({ - scrollTop: streamElement.offset().top - 54 - }, 1000); - }); - - $(".stream").on("tap click", "a.show_comments", function(evt){ - evt.preventDefault(); - toggleComments($(this)); - }); - - function toggleComments(toggleReactionsLink) { - if (toggleReactionsLink.hasClass("active")) { - hideComments(toggleReactionsLink); - } else { - showComments(toggleReactionsLink); - } - } - - function hideComments(toggleReactionsLink) { - var bottomBar = toggleReactionsLink.closest(".bottom_bar").first(), - commentsContainer = commentsContainerLazy(bottomBar), - existingCommentsContainer = commentsContainer(); - existingCommentsContainer.hide(); - toggleReactionsLink.removeClass("active"); - } - - function showComments(toggleReactionsLink) { - var bottomBar = toggleReactionsLink.closest(".bottom_bar").first(), - commentsContainer = commentsContainerLazy(bottomBar), - existingCommentsContainer = commentsContainer(), - commentActionLink = bottomBar.find("a.comment-action"); - if (existingCommentsContainer.length > 0) { - showLoadedComments(toggleReactionsLink, existingCommentsContainer, commentActionLink); - } else { - showUnloadedComments(toggleReactionsLink, bottomBar, commentActionLink); - } - } - - function showLoadedComments(toggleReactionsLink, existingCommentsContainer, commentActionLink) { - existingCommentsContainer.show(); - showCommentBox(commentActionLink); - toggleReactionsLink.addClass("active"); - existingCommentsContainer.find("time.timeago").timeago(); - } - - function showUnloadedComments(toggleReactionsLink, bottomBar, commentActionLink) { - var commentsContainer = commentsContainerLazy(bottomBar); - $.ajax({ - url: toggleReactionsLink.attr("href"), - success: function (data) { - $(data).insertAfter(bottomBar.children(".show_comments").first()); - showCommentBox(commentActionLink); - toggleReactionsLink.addClass("active"); - commentsContainer().find("time.timeago").timeago(); + $(".stream").on("tap click", "a.show_comments", function(evt){ + evt.preventDefault(); + self.toggleComments($(this)); + }); + + $(".stream").on("tap click", "a.comment-action", function(evt) { + evt.preventDefault(); + self.showCommentBox($(this)); + var bottomBar = $(this).closest(".bottom_bar").first(); + var commentContainer = bottomBar.find(".comment_container").first(); + self.scrollToOffset(commentContainer); + }); + + $(".stream").on("submit", ".new_comment", function(evt) { + evt.preventDefault(); + var form = $(this); + $.post(form.attr("action")+"?format=mobile", form.serialize(), function(data) { + self.updateStream(form, data); + }, "html"); + }); + }, + + toggleComments: function(toggleReactionsLink) { + if(toggleReactionsLink.hasClass("loading")) { return; } + if (toggleReactionsLink.hasClass("active")) { + this.hideComments(toggleReactionsLink); + } else { + this.showComments(toggleReactionsLink); } - }); - } - - function commentsContainerLazy(bottomBar) { - return function() { - return bottomBar.find(".comment_container").first(); - }; - } - - $(".stream").on("tap click", "a.comment-action", function(evt) { - evt.preventDefault(); - showCommentBox(this); - var bottomBar = $(this).closest(".bottom_bar").first(); - var commentContainer = bottomBar.find(".comment_container").first(); - scrollToOffset(commentContainer); - }); - var scrollToOffset = function(commentsContainer){ - var commentCount = commentsContainer.find("li.comment").length; - if ( commentCount > 3 ) { - var lastComment = commentsContainer.find("li:nth-child("+(commentCount-3)+")"); - $("html,body").animate({ - scrollTop: lastComment.offset().top - }, 1000); - } - }; + }, + + hideComments: function(toggleReactionsLink) { + var bottomBar = toggleReactionsLink.closest(".bottom_bar").first(), + commentsContainer = this.commentsContainerLazy(bottomBar), + existingCommentsContainer = commentsContainer(); + existingCommentsContainer.hide(); + toggleReactionsLink.removeClass("active"); + }, + + showComments: function(toggleReactionsLink) { + var bottomBar = toggleReactionsLink.closest(".bottom_bar").first(), + commentsContainer = this.commentsContainerLazy(bottomBar), + existingCommentsContainer = commentsContainer(), + commentActionLink = bottomBar.find("a.comment-action"); + if (existingCommentsContainer.length > 0) { + this.showLoadedComments(toggleReactionsLink, existingCommentsContainer, commentActionLink); + } else { + this.showUnloadedComments(toggleReactionsLink, bottomBar, commentActionLink); + } + }, + + showLoadedComments: function(toggleReactionsLink, existingCommentsContainer, commentActionLink) { + toggleReactionsLink.addClass("active"); + existingCommentsContainer.show(); + this.showCommentBox(commentActionLink); + existingCommentsContainer.find("time.timeago").timeago(); + }, + + showUnloadedComments: function(toggleReactionsLink, bottomBar, commentActionLink) { + toggleReactionsLink.addClass("loading"); + var commentsContainer = this.commentsContainerLazy(bottomBar); + var self = this; + $.ajax({ + url: toggleReactionsLink.attr("href"), + success: function (data) { + toggleReactionsLink.addClass("active").removeClass("loading"); + $(data).insertAfter(bottomBar.children(".show_comments").first()); + self.showCommentBox(commentActionLink); + commentsContainer().find("time.timeago").timeago(); + }, + error: function() { + toggleReactionsLink.removeClass("loading"); + } + }); + }, + + commentsContainerLazy: function(bottomBar) { + return function() { + return bottomBar.find(".comment_container").first(); + }; + }, + + scrollToOffset: function(commentsContainer){ + var commentCount = commentsContainer.find("li.comment").length; + if ( commentCount > 3 ) { + var lastComment = commentsContainer.find("li:nth-child("+(commentCount-3)+")"); + $("html,body").animate({ + scrollTop: lastComment.offset().top + }, 1000); + } + }, - function showCommentBox(link){ - var commentActionLink = $(link); - if(commentActionLink.hasClass("inactive")) { + showCommentBox: function(link){ + if(!link.hasClass("inactive") || link.hasClass("loading")) { return; } + var self = this; $.ajax({ - url: commentActionLink.attr("href"), + url: link.attr("href"), beforeSend: function(){ - commentActionLink.addClass("loading"); + link.addClass("loading"); + }, + context: link, + success: function(data) { + self.appendCommentBox.call(this, link, data); }, - context: commentActionLink, - success: function(data){ - appendCommentBox.call(this, commentActionLink, data); + error: function() { + link.removeClass("loading"); } }); + }, + + appendCommentBox: function(link, data) { + link.removeClass("loading"); + link.removeClass("inactive"); + var bottomBar = link.closest(".bottom_bar").first(); + bottomBar.append(data); + var textArea = bottomBar.find("textarea.comment_box").first()[0]; + autosize(textArea); + }, + + updateStream: function(form, data) { + var bottomBar = form.closest(".bottom_bar").first(); + this.addNewComments(bottomBar, data); + this.updateCommentCount(bottomBar); + this.updateReactionCount(bottomBar); + this.handleCommentShowing(form, bottomBar); + bottomBar.find("time.timeago").timeago(); + }, + + addNewComments: function(bottomBar, data) { + var commentsContainer = bottomBar.find(".comment_container").first(); + var comments = commentsContainer.find(".comments").first(); + comments.append(data); + }, + + // Fix for no comments + updateCommentCount: function(bottomBar) { + var commentCount = bottomBar.find(".comment_count"); + commentCount.text(commentCount.text().replace(/(\d+)/, function (match) { + return parseInt(match) + 1; + })); + }, + + // Fix for no reactions + updateReactionCount: function(bottomBar) { + var toggleReactionsLink = bottomBar.find(".show_comments").first(); + toggleReactionsLink.text(toggleReactionsLink.text().replace(/(\d+)/, function (match) { + return parseInt(match) + 1; + })); + }, + + handleCommentShowing: function(form, bottomBar) { + var formContainer = form.parent(); + formContainer.remove(); + var commentActionLink = bottomBar.find("a.comment-action").first(); + commentActionLink.addClass("inactive"); + var toggleReactionsLink = bottomBar.find(".show_comments").first(); + this.showComments(toggleReactionsLink); } - } - - function appendCommentBox(link, data) { - link.removeClass("loading"); - link.removeClass("inactive"); - var bottomBar = link.closest(".bottom_bar").first(); - bottomBar.append(data); - var textArea = bottomBar.find("textarea.comment_box").first()[0]; - autosize(textArea); - } - - $(".stream").on("submit", ".new_comment", function(evt) { - evt.preventDefault(); - var form = $(this); - $.post(form.attr("action")+"?format=mobile", form.serialize(), function(data) { - updateStream(form, data); - }, "html"); - }); - - function updateStream(form, data) { - var bottomBar = form.closest(".bottom_bar").first(); - addNewComments(bottomBar, data); - updateCommentCount(bottomBar); - updateReactionCount(bottomBar); - handleCommentShowing(form, bottomBar); - bottomBar.find("time.timeago").timeago(); - } - - function addNewComments(bottomBar, data) { - var commentsContainer = bottomBar.find(".comment_container").first(); - var comments = commentsContainer.find(".comments").first(); - comments.append(data); - } - - // Fix for no comments - function updateCommentCount(bottomBar) { - var commentCount = bottomBar.find(".comment_count"); - commentCount.text(commentCount.text().replace(/(\d+)/, function (match) { - return parseInt(match) + 1; - })); - } - - // Fix for no reactions - function updateReactionCount(bottomBar) { - var toggleReactionsLink = bottomBar.find(".show_comments").first(); - toggleReactionsLink.text(toggleReactionsLink.text().replace(/(\d+)/, function (match) { - return parseInt(match) + 1; - })); - } - - function handleCommentShowing(form, bottomBar) { - var formContainer = form.parent(); - formContainer.remove(); - var commentActionLink = bottomBar.find("a.comment-action").first(); - commentActionLink.addClass("inactive"); - var toggleReactionsLink = bottomBar.find(".show_comments").first(); - showComments(toggleReactionsLink); - } + }; +})(); + +$(document).ready(function() { + Diaspora.Mobile.Comments.initialize(); }); diff --git a/spec/controllers/jasmine_fixtures/aspects_spec.rb b/spec/controllers/jasmine_fixtures/aspects_spec.rb index bdbf026baef8ea68148840e1af91d3f5fba33a0c..e61faba7b1eb10c31cb8344b433bd458dafbc25f 100644 --- a/spec/controllers/jasmine_fixtures/aspects_spec.rb +++ b/spec/controllers/jasmine_fixtures/aspects_spec.rb @@ -58,6 +58,13 @@ describe StreamsController, :type => :controller do save_fixture(html_for("body"), "aspects_index_post_with_comments") end + it "generates a mobile jasmine fixture with a post with comments", fixture: true do + message = bob.post(:status_message, text: "HALO WHIRLED", to: @bob.aspects.where(name: "generic").first.id) + 5.times { bob.comment!(message, "what") } + get :aspects, format: :mobile + save_fixture(html_for("body"), "aspects_index_mobile_post_with_comments") + end + it 'generates a jasmine fixture with a followed tag', :fixture => true do @tag = ActsAsTaggableOn::Tag.create!(:name => "partytimeexcellent") TagFollowing.create!(:tag => @tag, :user => alice) diff --git a/spec/javascripts/mobile/mobile_comments_spec.js b/spec/javascripts/mobile/mobile_comments_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..24f4bdbcb8e769e961fe0b1a58b802b6d365112f --- /dev/null +++ b/spec/javascripts/mobile/mobile_comments_spec.js @@ -0,0 +1,119 @@ +describe("Diaspora.Mobile.Comments", function(){ + describe("toggleComments", function() { + beforeEach(function() { + spec.loadFixture("aspects_index_mobile_post_with_comments"); + this.link = $(".stream .show_comments").first(); + spyOn(Diaspora.Mobile.Comments, "showComments"); + spyOn(Diaspora.Mobile.Comments, "hideComments"); + }); + + it("calls showComments", function() { + Diaspora.Mobile.Comments.toggleComments(this.link); + expect(Diaspora.Mobile.Comments.showComments).toHaveBeenCalled(); + expect(Diaspora.Mobile.Comments.hideComments).not.toHaveBeenCalled(); + }); + + it("calls hideComments if the link class is 'active'", function() { + this.link.addClass("active"); + Diaspora.Mobile.Comments.toggleComments(this.link); + expect(Diaspora.Mobile.Comments.showComments).not.toHaveBeenCalled(); + expect(Diaspora.Mobile.Comments.hideComments).toHaveBeenCalled(); + }); + + it("doesn't call any function if the link class is 'loading'", function() { + this.link.addClass("loading"); + Diaspora.Mobile.Comments.toggleComments(this.link); + expect(Diaspora.Mobile.Comments.showComments).not.toHaveBeenCalled(); + expect(Diaspora.Mobile.Comments.hideComments).not.toHaveBeenCalled(); + }); + }); + + describe("showUnloadedComments", function() { + beforeEach(function() { + spec.loadFixture("aspects_index_mobile_post_with_comments"); + this.link = $(".stream .show_comments").first(); + this.bottomBar = this.link.closest(".bottom_bar").first(); + this.commentActionLink = this.bottomBar.find("a.comment-action"); + }); + + it("adds the 'loading' class to the link", function() { + Diaspora.Mobile.Comments.showUnloadedComments(this.link, this.bottomBar, this.commentActionLink); + expect($(".show_comments").first()).toHaveClass("loading"); + }); + + it("removes the 'loading' class if the request failed", function() { + Diaspora.Mobile.Comments.showUnloadedComments(this.link, this.bottomBar, this.commentActionLink); + jasmine.Ajax.requests.mostRecent().respondWith({status: 400}); + expect($(".show_comments").first()).not.toHaveClass("loading"); + }); + + it("adds the 'active' class if the request succeeded", function() { + Diaspora.Mobile.Comments.showUnloadedComments(this.link, this.bottomBar, this.commentActionLink); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, contentType: "text/plain", responseText: "test"}); + expect($(".show_comments").first()).toHaveClass("active"); + expect($(".show_comments").first()).not.toHaveClass("loading"); + }); + + it("calls showCommentBox", function() { + spyOn(Diaspora.Mobile.Comments, "showCommentBox"); + Diaspora.Mobile.Comments.showUnloadedComments(this.link, this.bottomBar, this.commentActionLink); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, contentType: "text/plain", responseText: "test"}); + expect(Diaspora.Mobile.Comments.showCommentBox).toHaveBeenCalledWith(this.commentActionLink); + }); + + it("adds the response text to the comments list", function() { + Diaspora.Mobile.Comments.showUnloadedComments(this.link, this.bottomBar, this.commentActionLink); + jasmine.Ajax.requests.mostRecent().respondWith({ + status: 200, + contentType: "text/plain", + responseText: "<div class=\"commentContainerForTest\">new comments</div>" + }); + expect($(".stream .stream_element").first()).toContainElement(".commentContainerForTest"); + }); + }); + + describe("showCommentBox", function() { + beforeEach(function() { + spec.loadFixture("aspects_index_mobile_post_with_comments"); + this.link = $(".stream .comment-action").first(); + }); + + it("adds the 'loading' class to the link", function() { + Diaspora.Mobile.Comments.showCommentBox(this.link); + expect($(".comment-action").first()).toHaveClass("loading"); + }); + + it("removes the 'loading' class if the request failed", function() { + Diaspora.Mobile.Comments.showCommentBox(this.link); + jasmine.Ajax.requests.mostRecent().respondWith({status: 400}); + expect($(".comment-action").first()).not.toHaveClass("loading"); + }); + + it("fires an AJAX call", function() { + spyOn(jQuery, "ajax"); + Diaspora.Mobile.Comments.showCommentBox(this.link); + expect(jQuery.ajax).toHaveBeenCalled(); + }); + + it("calls appendCommentBox", function() { + spyOn(Diaspora.Mobile.Comments, "appendCommentBox"); + Diaspora.Mobile.Comments.showCommentBox(this.link); + jasmine.Ajax.requests.mostRecent().respondWith({status: 200, contentType: "text/plain", responseText: "test"}); + expect(Diaspora.Mobile.Comments.appendCommentBox).toHaveBeenCalledWith(this.link, "test"); + }); + + it("doesn't do anything if the link class is 'loading'", function() { + spyOn(jQuery, "ajax"); + this.link.addClass("loading"); + Diaspora.Mobile.Comments.showCommentBox(this.link); + expect(jQuery.ajax).not.toHaveBeenCalled(); + }); + + it("doesn't do anything if the link class is not 'inactive'", function() { + spyOn(jQuery, "ajax"); + this.link.removeClass("inactive"); + Diaspora.Mobile.Comments.showCommentBox(this.link); + expect(jQuery.ajax).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/widgets/i18n-spec.js b/spec/javascripts/widgets/i18n-spec.js index 2315957ce93afb27cbb0acd9aa2796cc8d038fa2..17a469611b1e0318a3d3091365912ca10eaa8f06 100644 --- a/spec/javascripts/widgets/i18n-spec.js +++ b/spec/javascripts/widgets/i18n-spec.js @@ -90,6 +90,7 @@ describe("Diaspora.I18n", function() { Diaspora.I18n.load(locale, "en", locale); Diaspora.I18n.reset(); expect(Diaspora.I18n.locale.data).toEqual({}); + expect(Diaspora.I18n.locale.fallback.data).toEqual({}); }); it("sets the locale to only a specific value", function() { @@ -97,6 +98,7 @@ describe("Diaspora.I18n", function() { Diaspora.I18n.load(locale, "en", locale); Diaspora.I18n.reset(data); expect(Diaspora.I18n.locale.data).toEqual(data); + expect(Diaspora.I18n.locale.fallback.data).toEqual(data); }); }); });