diff --git a/app/controllers/ajax/comment_controller.rb b/app/controllers/ajax/comment_controller.rb index 72dd305e..c7264464 100644 --- a/app/controllers/ajax/comment_controller.rb +++ b/app/controllers/ajax/comment_controller.rb @@ -14,10 +14,12 @@ class Ajax::CommentController < AjaxController return end + comments = Comment.where(answer:).includes([{ user: :profile }, :smiles]) + @response[:status] = :okay @response[:message] = t(".success") @response[:success] = true - @response[:render] = render_to_string(partial: 'answerbox/comments', locals: { a: answer }) + @response[:render] = render_to_string(partial: "answerbox/comments", locals: { a: answer, comments: }) @response[:count] = answer.comment_count end diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb new file mode 100644 index 00000000..d8b9ea6f --- /dev/null +++ b/app/controllers/comment_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CommentController < ApplicationController + def index + answer = Answer.find(params[:id]) + @comments = Comment.where(answer:).includes([{ user: :profile }, :smiles]) + + render "index", locals: { a: answer } + end +end diff --git a/app/controllers/reaction_controller.rb b/app/controllers/reaction_controller.rb new file mode 100644 index 00000000..c6df6000 --- /dev/null +++ b/app/controllers/reaction_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ReactionController < ApplicationController + def index + answer = Answer.includes([smiles: { user: :profile }]).find(params[:id]) + + render "index", locals: { a: answer } + end +end diff --git a/app/views/answerbox/_comments.html.haml b/app/views/answerbox/_comments.html.haml index 023990e9..62a1035c 100644 --- a/app/views/answerbox/_comments.html.haml +++ b/app/views/answerbox/_comments.html.haml @@ -1,8 +1,8 @@ -- if a.comments.all.count.zero? +- if comments.all.count.zero? = t(".none") - else %ul.comment__container - - a.comments.order(:created_at).each do |comment| + - comments.order(:created_at).each do |comment| %li.comment{ data: { comment_id: comment.id } } %div{ style: "height: 0; width: 0" }= render "modal/comment_smiles", comment: comment .d-flex @@ -24,20 +24,3 @@ %button.btn.btn-link.btn-sm.dropdown-toggle{ data: { bs_toggle: :dropdown }, aria: { expanded: false } } %span.caret = render "actions/comment", comment: comment, answer: a -- if user_signed_in? - %button.d-none{ name: "ab-open-and-comment", data: { a_id: a.id, selection_hotkey: "c" } } - .comment__compose-wrapper{ - name: "ab-comment-new-group", - data: { a_id: a.id, controller: "character-count", character_count_max_value: 512 } - } - .form-group.has-feedback.comment__input-group.input-group - %textarea.form-control.comment__input{ type: :text, placeholder: t(".placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } } - .comment__submit-wrapper - %button.btn.btn-primary{ - type: :button, - name: "ab-comment-new-submit", - title: t(".action"), - data: { a_id: a.id, "character-count-target": "action" } - } - %i.fa.fa-paper-plane-o - %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512 diff --git a/app/views/application/_answerbox.html.haml b/app/views/application/_answerbox.html.haml index b84a5f17..4524f405 100644 --- a/app/views/application/_answerbox.html.haml +++ b/app/views/application/_answerbox.html.haml @@ -35,5 +35,28 @@ .col-md-6.d-md-flex.answerbox__actions = render "answerbox/actions", a:, display_all:, subscribed_answer_ids: .card-footer{ id: "ab-comments-section-#{a.id}", class: display_all.nil? ? "d-none" : nil } - %div{ id: "ab-smiles-#{a.id}" }= render "answerbox/smiles", a: a - %div{ id: "ab-comments-#{a.id}" }= render "answerbox/comments", a: a + = turbo_frame_tag("ab-reactions-list-#{a.id}", src: reactions_path(a.question, a), loading: :lazy) do + .d-flex.smiles + .flex-shrink-0.me-1 + %i.fa.fa-smile-o + = turbo_frame_tag("ab-comments-list-#{a.id}", src: comments_path(a.question, a), loading: :lazy) do + .d-flex.justify-content-center + .spinner-border{ role: :status } + .visually-hidden= t("voc.loading") + - if user_signed_in? + %button.d-none{ name: "ab-open-and-comment", data: { a_id: a.id, selection_hotkey: "c" } } + .comment__compose-wrapper{ + name: "ab-comment-new-group", + data: { a_id: a.id, controller: "character-count", character_count_max_value: 512 } + } + .form-group.has-feedback.comment__input-group.input-group + %textarea.form-control.comment__input{ type: :text, placeholder: t(".comments.placeholder"), name: "ab-comment-new", data: { a_id: a.id, "character-count-target": "input" } } + .comment__submit-wrapper + %button.btn.btn-primary{ + type: :button, + name: "ab-comment-new-submit", + title: t(".comments.action"), + data: { a_id: a.id, "character-count-target": "action" } + } + %i.fa.fa-paper-plane-o + %span.text-muted.form-control-feedback.comment__character-count{ id: "ab-comment-charcount-#{a.id}", data: { "character-count-target": "counter" } } 512 diff --git a/app/views/comment/index.html.haml b/app/views/comment/index.html.haml new file mode 100644 index 00000000..e5334203 --- /dev/null +++ b/app/views/comment/index.html.haml @@ -0,0 +1,2 @@ += turbo_frame_tag "ab-comments-list-#{a.id}" do + %div{ id: "ab-comments-#{a.id}" }= render "answerbox/comments", a:, comments: @comments diff --git a/app/views/reaction/index.html.haml b/app/views/reaction/index.html.haml new file mode 100644 index 00000000..b717a281 --- /dev/null +++ b/app/views/reaction/index.html.haml @@ -0,0 +1,2 @@ += turbo_frame_tag "ab-reactions-list-#{a.id}" do + %div{ id: "ab-smiles-#{a.id}" }= render "answerbox/smiles", a: diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 5b909fde..8613ff57 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -117,18 +117,18 @@ en: actions: share: title: "Share" - comments: - none: "There are no comments yet." - placeholder: "Comment..." - action: "Post comment" smiles: none: "No one smiled this yet." + comments: + none: "There are no comments yet." application: answerbox: read: "Read the entire answer" answered: "%{hide} %{user}" # resolves into "Answered by %{user}" hide: "Answered by" pinned: "Pinned" + comments: + placeholder: "Comment…" questionbox: title: "Ask something!" placeholder: "Type your question here…" diff --git a/config/locales/voc.en.yml b/config/locales/voc.en.yml index a05407eb..01f1ff9c 100644 --- a/config/locales/voc.en.yml +++ b/config/locales/voc.en.yml @@ -12,6 +12,7 @@ en: follow: "Follow" format_markdown: "Styling with Markdown is supported" load: "Load more" + loading: "Loading…" login: "Sign in" logout: "Sign out" mute: "Mute" diff --git a/config/routes.rb b/config/routes.rb index 3af4b3a8..e5fcf899 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -153,6 +153,8 @@ Rails.application.routes.draw do get "/@:username/a/:id", to: "answer#show", as: :answer post "/@:username/a/:id/pin", to: "answer#pin", as: :pin_answer delete "/@:username/a/:id/pin", to: "answer#unpin", as: :unpin_answer + get "/@:username/a/:id/comments", to: "comment#index", as: :comments + get "/@:username/a/:id/reactions", to: "reaction#index", as: :reactions get "/@:username/q/:id", to: "question#show", as: :question get "/@:username/followers", to: "user#followers", as: :show_user_followers get "/@:username/followings", to: "user#followings", as: :show_user_followings diff --git a/spec/controllers/comment_controller_spec.rb b/spec/controllers/comment_controller_spec.rb new file mode 100644 index 00000000..ace2aad8 --- /dev/null +++ b/spec/controllers/comment_controller_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe CommentController, type: :controller do + describe "#index" do + shared_examples_for "succeeds" do + it "returns the correct response" do + subject + expect(response).to have_rendered("comment/index") + expect(response).to have_http_status(200) + expect(assigns(:comments)).to eq(comments) + expect(assigns(:comments)).to_not include(unrelated_comment) + end + end + + subject { get :index, params: { username: answer_author.screen_name, id: answer.id } } + + let(:answer_author) { FactoryBot.create(:user) } + let(:answer) { FactoryBot.create(:answer, user: answer_author) } + let(:commenter) { FactoryBot.create(:user) } + let!(:comments) { FactoryBot.create_list(:comment, num_comments, answer:, user: commenter) } + let!(:unrelated_comment) do + FactoryBot.create(:comment, + answer: FactoryBot.create(:answer, user: FactoryBot.create(:user)), + user: commenter,) + end + + [0, 1, 5, 30].each do |num_comments| + context "#{num_comments} comments" do + let(:num_comments) { num_comments } + + include_examples "succeeds" + end + end + end +end diff --git a/spec/controllers/reaction_controller_spec.rb b/spec/controllers/reaction_controller_spec.rb new file mode 100644 index 00000000..1b76617f --- /dev/null +++ b/spec/controllers/reaction_controller_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe ReactionController, type: :controller do + describe "#index" do + shared_examples_for "succeeds" do + it "returns the correct response" do + subject + expect(response).to have_rendered("reaction/index") + expect(response).to have_http_status(200) + end + end + + subject { get :index, params: { username: answer_author.screen_name, id: answer.id } } + + let(:answer_author) { FactoryBot.create(:user) } + let(:answer) { FactoryBot.create(:answer, user: answer_author) } + let!(:reactees) { FactoryBot.create_list(:user, num_comments) } + + [0, 1, 5, 30].each do |num_comments| + context "#{num_comments} reactions" do + let(:num_comments) { num_comments } + + before do + reactees.each { _1.smile(answer) } + end + + include_examples "succeeds" + end + end + + end +end diff --git a/spec/views/answerbox/_comments.html.haml_spec.rb b/spec/views/answerbox/_comments.html.haml_spec.rb new file mode 100644 index 00000000..3af138d6 --- /dev/null +++ b/spec/views/answerbox/_comments.html.haml_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "answerbox/_comments.html.haml", type: :view do + subject(:rendered) do + render partial: "answerbox/comments", locals: { + comments:, a:, + } + end + + let(:a) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) } + let(:comments) { Comment.all } + + context "no comments" do + it "shows an empty list" do + expect(rendered).to eq("There are no comments yet.\n") + end + end + + context "comments are present" do + let!(:expected_comments) { FactoryBot.create_list(:comment, 5, answer: a, user: FactoryBot.create(:user)) } + + it "shows a list of comments" do + html = Nokogiri::HTML.parse(rendered) + selector = %(li.comment .comment__content) + comment_elements = html.css(selector) + expect(comment_elements.size).to eq(5) + expect(comment_elements.map(&:text).map(&:strip)).to eq(expected_comments.map(&:content)) + end + end + + context "containing your own comment" do + let(:user) { FactoryBot.create(:user) } + let!(:comment) { FactoryBot.create(:comment, user:, answer: a) } + + before do + sign_in user + end + + it "shows the delete option" do + html = Nokogiri::HTML.parse(rendered) + selector = %(li.comment[data-comment-id="#{comment.id}"] .btn-group a[data-action="ab-comment-destroy"]) + element = html.css(selector) + expect(element).to_not be_nil + expect(element.text.strip).to eq("Delete") + end + end + + context "containing someone else's comment" do + let(:user) { FactoryBot.create(:user) } + let!(:comment) { FactoryBot.create(:comment, user: FactoryBot.create(:user), answer: a) } + + before do + sign_in user + end + + it "does not show the delete option" do + html = Nokogiri::HTML.parse(rendered) + selector = %(li.comment[data-comment-id="#{comment.id}"] .btn-group a[data-action="ab-comment-destroy"]) + expect(html.css(selector)).to be_empty + end + end + + context "containing a comment with smiles" do + let(:comment_author) { FactoryBot.create(:user) } + let(:comment) { FactoryBot.create(:comment, answer: a, user: comment_author) } + let(:other_comment) { FactoryBot.create(:comment, answer: a, user: comment_author) } + + before do + 5.times do + user = FactoryBot.create(:user) + user.smile comment + end + + User.last.smile other_comment + end + + it "shows the correct number of smiles" do + html = Nokogiri::HTML.parse(rendered) + selector = %(li.comment[data-comment-id="#{comment.id}"] button[data-action="smile"]>span) + expect(html.css(selector).text).to eq("5") + end + end + + context "containing a comment you've smiled" do + let(:user) { FactoryBot.create(:user) } + let!(:comment) { FactoryBot.create(:comment, user: FactoryBot.create(:user), answer: a) } + + before do + sign_in user + user.smile comment + end + + it "displays the comment as smiled" do + html = Nokogiri::HTML.parse(rendered) + selector = %(li.comment[data-comment-id="#{comment.id}"] button[data-action="unsmile"]) + expect(html.css(selector)).to_not be_empty + end + end +end diff --git a/spec/views/answerbox/_smiles.html.haml_spec.rb b/spec/views/answerbox/_smiles.html.haml_spec.rb new file mode 100644 index 00000000..5530d648 --- /dev/null +++ b/spec/views/answerbox/_smiles.html.haml_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "answerbox/_smiles.html.haml", type: :view do + subject(:rendered) do + render partial: "answerbox/smiles", locals: { a: } + end + + let(:a) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) } + + context "no reactions" do + it "shows an empty list" do + expect(rendered).to match("No one smiled this yet.") + end + end + + context "reactions are present" do + let!(:reactions) { FactoryBot.create_list(:smile, 5, parent: a) } + + it "shows a list of users" do + html = Nokogiri::HTML.parse(rendered) + selector = %(.smiles a) + reaction_elements = html.css(selector) + expect(reaction_elements.size).to eq(5) + end + end +end