comments: rework thresholded comments.

Previously thresholded comments were hidden completely. You had to click
the "Show X hidden comments" button to unhide all hidden comments in a
thread. Now it works like this:

* When a comment is below your threshold, the comment text is hidden and
  replaced by a `[hidden]` link, which you can click to unhide the comment.

* When a comment is at half your threshold (for example, your threshold
  is -8 but the comment is at -4), then the comment is greyed out.

This means that comments aren't completely hidden, they're just
collapsed, so you can see the commenter and the score without unhiding
the comment. It also means you don't have to scroll back up to unhide a
comment, and threads aren't disrupted by comments being secretly
hidden (which is confusing when people are replying to hidden comments,
which forces you to go back up and unhide to find).
This commit is contained in:
evazion
2021-01-18 05:03:35 -06:00
parent b6008b02b4
commit 07bdc6eab0
22 changed files with 196 additions and 201 deletions

View File

@@ -1,2 +1,5 @@
class ApplicationComponent < ViewComponent::Base
def policy(subject)
Pundit.policy!(current_user, subject)
end
end

View File

@@ -2,17 +2,8 @@
class CommentComponent < ApplicationComponent
attr_reader :comment, :context, :dtext_data, :show_deleted, :current_user
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :policy, to: :helpers
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, to: :helpers
def self.with_collection(comments, current_user:, **options)
dtext_data = DText.preprocess(comments.map(&:body))
# XXX
#comments = comments.includes(:moderation_reports) if Pundit.policy!(current_user, ModerationReport).show?
super(comments, current_user: current_user, dtext_data: dtext_data, **options)
end
# XXX calls to pundit policy don't respect current_user.
def initialize(comment:, current_user:, context: nil, dtext_data: nil, show_deleted: false)
@comment = comment
@context = context
@@ -25,6 +16,14 @@ class CommentComponent < ApplicationComponent
!comment.is_deleted? || show_deleted || current_user.is_moderator?
end
def dimmed?
!comment.is_sticky? && comment.score < current_user.comment_threshold/2.0
end
def thresholded?
!comment.is_sticky? && comment.score < current_user.comment_threshold
end
def has_moderation_reports?
policy(ModerationReport).show? && comment.moderation_reports.present?
end

View File

@@ -8,7 +8,8 @@
data-do-not-bump-post="<%= comment.do_not_bump_post? %>"
data-is-deleted="<%= comment.is_deleted? %>"
data-is-sticky="<%= comment.is_sticky? %>"
data-below-threshold="<%= comment.score < current_user.comment_threshold %>"
data-is-dimmed="<%= dimmed? %>"
data-is-thresholded="<%= thresholded? %>"
data-is-reported="<%= has_moderation_reports? %>"
data-is-voted="<%= comment.voted_by?(current_user) %>">
<div class="author">
@@ -21,10 +22,14 @@
<%= link_to time_ago_in_words_tagged(comment.created_at), post_path(comment.post, anchor: "comment_#{comment.id}"), class: "message-timestamp" %>
</div>
<div class="content">
<div class="body prose">
<% if thresholded? %>
<%= link_to "[hidden]", "javascript:void(0)", class: "unhide-comment-link" %>
<% end %>
<%= tag.div class: "body prose", style: ("display: none;" if thresholded?) do %>
<%= format_text(comment.body, data: dtext_data) %>
</div>
<%= render "application/update_notice", record: comment %>
<%= render "application/update_notice", record: comment %>
<% end %>
<% if policy(comment).create? %>
<menu>

View File

@@ -21,7 +21,7 @@ article.comment {
}
}
&[data-below-threshold="true"][data-is-sticky="false"] {
&[data-is-dimmed="true"] {
opacity: 0.3;
&:hover {
@@ -29,6 +29,11 @@ article.comment {
}
}
.unhide-comment-link {
margin-bottom: 1em;
display: block;
}
.moderation-report-notice {
font-weight: bold;
color: var(--moderation-report-text-color);

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
class CommentSectionComponent < ApplicationComponent
attr_reader :post, :comments, :current_user, :limit, :dtext_data
def initialize(post:, current_user:, limit: nil)
@post = post
@current_user = current_user
@limit = limit
@comments = @post.comments.order(id: :asc)
@comments = @comments.includes(:creator)
@comments = @comments.includes(:votes) if !current_user.is_anonymous?
@comments = @comments.includes(:moderation_reports) if policy(ModerationReport).show?
@comments = @comments.last(limit) if limit.present?
@dtext_data = DText.preprocess(@comments.map(&:body))
end
def has_unloaded_comments?
unloaded_comment_count > 0
end
def unloaded_comment_count
return 0 if limit.nil?
[post.comments.size - limit, 0].max
end
end

View File

@@ -0,0 +1,20 @@
<div class="comments-for-post" data-post-id="<%= post.id %>">
<% if has_unloaded_comments? %>
<%= link_to "Show #{pluralize unloaded_comment_count, "more comment"}", comments_path(post_id: post.id), class: "show-all-comments-link", remote: true %>
<% end %>
<div class="list-of-comments list-of-messages">
<% if comments.present? %>
<%= render CommentComponent.with_collection(comments, current_user: current_user, context: :index_by_post, dtext_data: dtext_data) %>
<% else %>
<p>There are no comments.</p>
<% end %>
</div>
<% if policy(Comment).create? %>
<div class="new-comment">
<p><%= link_to "Post comment", new_comment_path(comment: { post_id: post.id }), class: "expand-comment-response" %></p>
<%= render "comments/form", comment: post.comments.new, hidden: true %>
</div>
<% end %>
</div>

View File

@@ -23,11 +23,6 @@ class PostsController < ApplicationController
@post = authorize Post.find(params[:id])
if request.format.html?
@comments = @post.comments
@comments = @comments.includes(:creator)
@comments = @comments.includes(:votes) if CurrentUser.is_member?
@comments = @comments.unhidden(CurrentUser.user)
include_deleted = @post.is_deleted? || (@post.parent_id.present? && @post.parent.is_deleted?) || CurrentUser.user.show_deleted_children?
@sibling_posts = @post.parent.present? ? @post.parent.children : Post.none
@sibling_posts = @sibling_posts.undeleted unless include_deleted

View File

@@ -1,9 +0,0 @@
module CommentsHelper
def render_comment(comment, **options)
render CommentComponent.new(comment: comment, **options)
end
def render_comment_list(comments, **options)
render CommentComponent.with_collection(comments, current_user: CurrentUser.user, **options)
end
end

View File

@@ -0,0 +1,17 @@
module ComponentsHelper
def post_preview(post, **options)
render PostPreviewComponent.new(post: post, **options)
end
def post_previews_html(posts, **options)
render PostPreviewComponent.with_collection(posts, **options)
end
def render_comment(comment, **options)
render CommentComponent.new(comment: comment, **options)
end
def render_comment_section(post, **options)
render CommentSectionComponent.new(post: post, **options)
end
end

View File

@@ -1,12 +1,4 @@
module PostsHelper
def post_preview(post, **options)
render PostPreviewComponent.new(post: post, **options)
end
def post_previews_html(posts, **options)
render PostPreviewComponent.with_collection(posts, **options)
end
def reportbooru_enabled?
Danbooru.config.reportbooru_server.present? && Danbooru.config.reportbooru_key.present?
end

View File

@@ -4,6 +4,7 @@ Comment.initialize_all = function() {
if ($("#c-posts").length || $("#c-comments").length) {
$(document).on("click.danbooru.comment", ".edit_comment_link", Comment.show_edit_form);
$(document).on("click.danbooru.comment", ".expand-comment-response", Comment.show_new_comment_form);
$(document).on("click.danbooru.comment", ".unhide-comment-link", Comment.unhide_comment);
}
}
@@ -20,6 +21,13 @@ Comment.show_edit_form = function(e) {
e.preventDefault();
}
Comment.unhide_comment = function(e) {
let $comment = $(this).closest(".comment");
$comment.find(".unhide-comment-link").hide();
$comment.find(".body").show();
e.preventDefault();
}
$(document).ready(function() {
Comment.initialize_all();
});

View File

@@ -1,20 +1,10 @@
@import "../base/000_vars.scss";
div.comments-for-post {
div.moderation-comments-notice {
margin: 1em 0;
font-weight: bold;
color: var(--moderation-report-text-color);
}
div.hidden-comments-notice {
margin: 1em 0;
}
}
div#c-comments {
div#a-index, div#a-show {
div#a-index {
div.header {
margin-bottom: 1em;
span.info {
margin-right: 1.5em;
}
@@ -58,7 +48,7 @@ div#c-comments {
text-align: center;
}
div.comments-for-post {
div.comment-section {
flex: 1;
}
}
@@ -89,12 +79,6 @@ form.edit_comment div.input.boolean {
width: auto;
height: auto;
}
div.comments-for-post {
div.header div.row span.info {
display: block;
}
}
}
}
}

View File

@@ -118,21 +118,6 @@ class Comment < ApplicationRecord
user.id.in?(votes.map(&:user_id))
end
def visibility(user)
return :invisible if is_deleted? && !user.is_moderator?
return :hidden if is_deleted? && user.is_moderator?
return :hidden if score < user.comment_threshold && !is_sticky?
return :visible
end
def self.hidden(user)
select { |comment| comment.visibility(user) == :hidden }
end
def self.unhidden(user)
select { |comment| comment.visibility(user) == :visible }
end
def quoted_response
DText.quote(body, creator.name)
end

View File

@@ -49,7 +49,7 @@ class Post < ApplicationRecord
has_many :appeals, :class_name => "PostAppeal", :dependent => :destroy
has_many :votes, :class_name => "PostVote", :dependent => :destroy
has_many :notes, :dependent => :destroy
has_many :comments, -> {order("comments.id")}, :dependent => :destroy
has_many :comments, :dependent => :destroy
has_many :children, -> {order("posts.id")}, :class_name => "Post", :foreign_key => "parent_id"
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy

View File

@@ -4,13 +4,15 @@
<% end %>
<% @posts.select(&:visible?).each do |post| %>
<% if post.comments.unhidden(CurrentUser.user).any? || post.comments.hidden(CurrentUser.user).any? %>
<%= tag.div id: "post_#{post.id}", **PostPreviewComponent.new(post: post).article_attrs("post") do %>
<div class="preview">
<%= link_to(image_tag(post.preview_file_url), post_path(post)) %>
</div>
<%= render "comments/partials/index/list", post: post, comments: post.comments.unhidden(CurrentUser.user).last(6), page: :comments %>
<% end %>
<%= tag.div id: "post_#{post.id}", **PostPreviewComponent.new(post: post).article_attrs("post") do %>
<div class="preview">
<%= link_to(image_tag(post.preview_file_url), post_path(post)) %>
</div>
<div class="comment-section">
<%= render "comments/partials/index/header", post: post %>
<%= render_comment_section(post, limit: 6, current_user: CurrentUser.user) %>
</div>
<% end %>
<% end %>
</div>

View File

@@ -1,5 +1,3 @@
$("#threshold-comments-notice-for-<%= @post.id %>").hide();
var current_comment_section = $("div.comments-for-post[data-post-id=<%= @post.id %>] div.list-of-comments");
current_comment_section.html("<%= j render_comment_list(@comments, context: :index_by_post) %>");
var current_comment_section = $("div.comments-for-post[data-post-id=<%= @post.id %>]");
current_comment_section.replaceWith("<%= j render_comment_section(@post, limit: nil, current_user: CurrentUser.user) %>");
$(window).trigger("danbooru:index_for_post", [<%= @post.id %>]);

View File

@@ -1,30 +0,0 @@
<div class="comments-for-post" data-post-id="<%= post.id %>">
<% if page == :comments %>
<%= render "comments/partials/index/header", :post => post %>
<% end %>
<% if post.comments.hidden(CurrentUser.user).any? || (page == :comments && post.comments.size > 6) %>
<div class="row hidden-comments-notice">
<span class="info" id="threshold-comments-notice-for-<%= post.id %>">
<%= link_to "Show #{pluralize post.comments.hidden(CurrentUser.user).size, "hidden comment"}", comments_path(post_id: post.id), id: "show-all-comments-link", remote: true %>
</span>
</div>
<% end %>
<div class="list-of-comments list-of-messages">
<% if comments.present? %>
<%= render_comment_list(comments, context: :index_by_post) %>
<% elsif post.last_commented_at.present? %>
<p>There are no visible comments.</p>
<% else %>
<p>There are no comments.</p>
<% end %>
</div>
<% if policy(Comment).create? %>
<div class="new-comment">
<p><%= link_to "Post comment", new_comment_path(comment: { post_id: post.id }), :class => "expand-comment-response" %></p>
<%= render "comments/form", comment: post.comments.new, hidden: true %>
</div>
<% end %>
</div>

View File

@@ -105,7 +105,7 @@
<% end %>
<section id="comments">
<%= render "comments/partials/index/list", comments: @comments, post: @post, page: :post %>
<%= render_comment_section(@post, current_user: CurrentUser.user) %>
</section>
<section id="notes" style="display: none;">

View File

@@ -40,5 +40,33 @@ class CommentComponentTest < ViewComponent::TestCase
assert_no_css(".moderation-report-notice")
end
end
context "for a downvoted comment" do
setup do
@user = create(:user, comment_threshold: -8)
end
context "that is thresholded" do
should "hide the comment" do
as(@user) { @comment.update!(score: -9) }
render_comment(@comment, current_user: @user)
assert_css("article.comment[data-is-thresholded=true]")
assert_css("article.comment[data-is-dimmed=true]")
assert_css("article.comment .unhide-comment-link")
end
end
context "that is dimmed" do
should "dim the comment" do
as(@user) { @comment.update!(score: -5) }
render_comment(@comment, current_user: @user)
assert_css("article.comment[data-is-thresholded=false]")
assert_css("article.comment[data-is-dimmed=true]")
assert_no_css("article.comment .unhide-comment-link")
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
require "test_helper"
class CommentSectionComponentTest < ViewComponent::TestCase
def render_comment_section(post, current_user: User.anonymous, **options)
as(current_user) do
render_inline(CommentSectionComponent.new(post: post, current_user: current_user, **options))
end
end
context "The CommentSectionComponent" do
setup do
as(create(:user)) do
@post = create(:post)
@comment = create_list(:comment, 7, post: @post)
end
end
context "for a comment section with comments" do
context "without a comment limit" do
should "render" do
render_comment_section(@post, current_user: User.anonymous)
assert_css("div.comments-for-post")
assert_css("article.comment", count: 7)
end
end
context "with a comment limit" do
context "higher than the actual number of comments" do
should "render" do
render_comment_section(@post, current_user: User.anonymous, limit: 8)
assert_css("div.comments-for-post")
assert_css("article.comment", count: 7)
end
end
context "lower than the actual number of comments" do
should "render" do
render_comment_section(@post, current_user: User.anonymous, limit: 6)
assert_css("div.comments-for-post")
assert_css("article.comment", count: 6)
assert_css("a.show-all-comments-link", text: "Show 1 more comment")
end
end
end
end
end
end

View File

@@ -34,40 +34,7 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_select "#post_#{@post.id}", 1
assert_select "#post_#{@post.id} .comment", 1
assert_select "#post_#{@post.id} #show-all-comments-link", 0
end
should "show the 'Show hidden comments' link on posts with thresholded comments" do
as(@user) { create(:comment, post: @post, score: -10) }
get comments_path(group_by: "post")
assert_response :success
assert_select "#post_#{@post.id}", 1
assert_select "#post_#{@post.id} #show-all-comments-link", /Show 1 hidden comment/
assert_select "#post_#{@post.id} .comment", 0
assert_select "#post_#{@post.id} .list-of-comments", /There are no visible comments/
end
should "not show the 'Show hidden comments' link on posts with deleted comments to Members" do
@comment1 = as(@user) { create(:comment, post: @post) }
@comment2 = as(@user) { create(:comment, post: @post, is_deleted: true) }
get comments_path(group_by: "post")
assert_response :success
assert_select "#post_#{@post.id}", 1
assert_select "#post_#{@post.id} .comment", 1
assert_select "#post_#{@post.id} #show-all-comments-link", 0
end
should "show the 'Show hidden comments' link on posts with deleted comments to Moderators" do
@comment1 = as(@user) { create(:comment, post: @post) }
@comment2 = as(@user) { create(:comment, post: @post, is_deleted: true) }
get_auth comments_path(group_by: "post"), @mod
assert_response :success
assert_select "#post_#{@post.id}", 1
assert_select "#post_#{@post.id} .comment", 1
assert_select "#post_#{@post.id} #show-all-comments-link", /Show 1 hidden comment/
assert_select "#post_#{@post.id} .show-all-comments-link", 0
end
should "not bump posts with nonbumping comments" do

View File

@@ -560,58 +560,6 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
end
end
context "with only deleted comments" do
setup do
as(@user) { create(:comment, creator: @user, post: @post, is_deleted: true) }
end
should "not show deleted comments to regular members" do
get_auth post_path(@post), @user, params: { id: @post.id }
assert_response :success
assert_select "article.comment", 0
assert_select "a#show-all-comments-link", 0
assert_select "div.list-of-comments p", /There are no comments/
end
should "not show deleted comments to moderators by default, but allow them to be unhidden" do
mod = create(:mod_user)
get_auth post_path(@post), mod, params: { id: @post.id }
assert_response :success
assert_select "article.comment", 0
assert_select "a#show-all-comments-link", 1
assert_select "div.list-of-comments p", /There are no comments/
end
end
context "with only downvoted comments" do
should "not show thresholded comments" do
comment = as(@user) { create(:comment, creator: @user, post: @post, score: -10) }
get_auth post_path(@post), @user, params: { id: @post.id }
assert_response :success
assert_select "article.comment", 0
assert_select "a#show-all-comments-link", 1
assert_select "div.list-of-comments p", /There are no visible comments/
end
end
context "with a mix of comments" do
should "not show deleted or thresholded comments " do
as(@user) { create(:comment, creator: @user, post: @post, do_not_bump_post: true, body: "good") }
as(@user) { create(:comment, creator: @user, post: @post, do_not_bump_post: true, body: "bad", score: -10) }
as(@user) { create(:comment, creator: @user, post: @post, do_not_bump_post: true, body: "ugly", is_deleted: true) }
get_auth post_path(@post), @user, params: { id: @post.id }
assert_response :success
assert_select "article.comment", 1
assert_select "article.comment", /good/
assert_select "a#show-all-comments-link", 1
end
end
context "when the recommend service is enabled" do
setup do
@post2 = create(:post)