From 5585d1f7d6908f32c91d3638d2c8ecaec3f90841 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 18 Nov 2021 01:05:24 -0600 Subject: [PATCH] votes: show votes when hovering over post score. Make it so you can hover over a post's score to see the list of public upvotes. Also show the upvote count, the downvote count, and the upvote ratio. --- .../post_votes_component.html.erb | 4 +- .../post_votes_component.scss | 6 ++ .../post_votes_tooltip_component.rb | 34 ++++++++++ .../post_votes_tooltip_component.html.erb | 13 ++++ .../post_votes_tooltip_component.js | 65 +++++++++++++++++++ .../post_votes_tooltip_component.scss | 16 +++++ app/controllers/post_votes_controller.rb | 1 + app/helpers/application_helper.rb | 4 +- app/helpers/components_helper.rb | 4 ++ app/javascript/packs/application.js | 2 + app/javascript/src/styles/base/040_colors.css | 6 ++ .../src/styles/common/utilities.scss | 63 ++++++++++++++++++ .../src/styles/specific/post_tooltips.scss | 45 ------------- app/views/layouts/default.html.erb | 1 + app/views/post_votes/_search.html.erb | 7 ++ app/views/post_votes/index.html+compact.erb | 24 +++++++ app/views/post_votes/index.html+tooltip.erb | 1 + app/views/post_votes/index.html.erb | 8 +-- app/views/posts/show.html+tooltip.erb | 2 +- test/functional/post_votes_controller_test.rb | 14 +++- 20 files changed, 262 insertions(+), 58 deletions(-) create mode 100644 app/components/post_votes_tooltip_component.rb create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.js create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss create mode 100644 app/views/post_votes/_search.html.erb create mode 100644 app/views/post_votes/index.html+compact.erb create mode 100644 app/views/post_votes/index.html+tooltip.erb diff --git a/app/components/post_votes_component/post_votes_component.html.erb b/app/components/post_votes_component/post_votes_component.html.erb index f9186994a..5934c1a42 100644 --- a/app/components/post_votes_component/post_votes_component.html.erb +++ b/app/components/post_votes_component/post_votes_component.html.erb @@ -7,7 +7,9 @@ <% end %> <% end %> - <%= post.score %> + + <%= link_to post.score, post_votes_path(search: { post_id: post.id }, variant: :compact) %> + <% if can_vote? %> <% if downvoted? %> diff --git a/app/components/post_votes_component/post_votes_component.scss b/app/components/post_votes_component/post_votes_component.scss index 4d2fd99ac..0dcd21510 100644 --- a/app/components/post_votes_component/post_votes_component.scss +++ b/app/components/post_votes_component/post_votes_component.scss @@ -6,5 +6,11 @@ text-align: center; min-width: 1.25em; white-space: nowrap; + vertical-align: middle; + + a { + color: var(--text-color); + &:hover { text-decoration: underline; } + } } } diff --git a/app/components/post_votes_tooltip_component.rb b/app/components/post_votes_tooltip_component.rb new file mode 100644 index 000000000..b8b1f11a4 --- /dev/null +++ b/app/components/post_votes_tooltip_component.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# This component represents the tooltip that displays when you hover over a post's score. +class PostVotesTooltipComponent < ApplicationComponent + attr_reader :post, :current_user + delegate :upvote_icon, :downvote_icon, to: :helpers + + def initialize(post:, current_user:) + super + @post = post + @current_user = current_user + end + + def votes + @post.votes.includes(:user).order(id: :desc) + end + + def vote_icon(vote) + vote.is_positive? ? upvote_icon : downvote_icon + end + + def upvote_ratio + return nil if votes.length == 0 + sprintf("(%.1f%%)", 100.0 * votes.select(&:is_positive?).length / votes.length) + end + + def voter_name(vote) + if policy(vote).can_see_voter? + link_to_user(vote.user, classes: "align-middle") + else + tag.i("hidden", class: "align-middle") + end + end +end diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb new file mode 100644 index 000000000..24937ae58 --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb @@ -0,0 +1,13 @@ +
+
+ +<%= post.up_score %> / <%= post.down_score %> <%= upvote_ratio %> +
+ +
+ <% votes.each do |vote| %> +
+ <%= vote_icon(vote) %> <%= voter_name(vote) %> +
+ <% end %> +
+
diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js new file mode 100644 index 000000000..d9223d9ab --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js @@ -0,0 +1,65 @@ +import Utility from "../../javascript/src/javascripts/utility.js"; +import { delegate, hideAll } from 'tippy.js'; +import 'tippy.js/dist/tippy.css'; + +class PostVotesTooltipComponent { + // Trigger on the post score link; see PostVotesComponent. + static TARGET_SELECTOR = "span.post-votes span.post-score a"; + static SHOW_DELAY = 125; + static HIDE_DELAY = 125; + static DURATION = 250; + static instance = null; + + static initialize() { + if ($(PostVotesTooltipComponent.TARGET_SELECTOR).length === 0) { + return; + } + + PostVotesTooltipComponent.instance = delegate("body", { + allowHTML: true, + appendTo: document.querySelector("#post-votes-tooltips"), + delay: [PostVotesTooltipComponent.SHOW_DELAY, PostVotesTooltipComponent.HIDE_DELAY], + duration: PostVotesTooltipComponent.DURATION, + interactive: true, + maxWidth: "none", + target: PostVotesTooltipComponent.TARGET_SELECTOR, + theme: "common-tooltip", + touch: false, + + onShow: PostVotesTooltipComponent.onShow, + onHide: PostVotesTooltipComponent.onHide, + }); + } + + static async onShow(instance) { + let $target = $(instance.reference); + let $tooltip = $(instance.popper); + let postId = $target.parents("[data-id]").data("id"); + + hideAll({ exclude: instance }); + + try { + $tooltip.addClass("tooltip-loading"); + + instance._request = $.get(`/post_votes?search[post_id]=${postId}`, { variant: "tooltip" }); + let html = await instance._request; + instance.setContent(html); + + $tooltip.removeClass("tooltip-loading"); + } catch (error) { + if (error.status !== 0 && error.statusText !== "abort") { + Utility.error(`Error displaying votes for post #${postId} (error: ${error.status} ${error.statusText})`); + } + } + } + + static async onHide(instance) { + if (instance._request?.state() === "pending") { + instance._request.abort(); + } + } +} + +$(document).ready(PostVotesTooltipComponent.initialize); + +export default PostVotesTooltipComponent; diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss new file mode 100644 index 000000000..2cc839b5d --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss @@ -0,0 +1,16 @@ +.post-votes-tooltip { + font-size: var(--text-xs); + max-height: 240px; + + .upvote-icon { + color: var(--post-upvote-color); + } + + .downvote-icon { + color: var(--post-downvote-color); + } + + .post-voter { + max-width: 160px; + } +} diff --git a/app/controllers/post_votes_controller.rb b/app/controllers/post_votes_controller.rb index c21c1d102..24484deaa 100644 --- a/app/controllers/post_votes_controller.rb +++ b/app/controllers/post_votes_controller.rb @@ -4,6 +4,7 @@ class PostVotesController < ApplicationController def index @post_votes = authorize PostVote.visible(CurrentUser.user).paginated_search(params, count_pages: true) @post_votes = @post_votes.includes(:user, post: [:uploader, :media_asset]) if request.format.html? + @post = Post.find(params.dig(:search, :post_id)) if params.dig(:search, :post_id).present? respond_with(@post_votes) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 10f23b3a0..0b63f33b3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -193,10 +193,10 @@ module ApplicationHelper to_sentence(links, **options) end - def link_to_user(user, text = nil) + def link_to_user(user, text = nil, classes: nil, **options) return "anonymous" if user.blank? - user_class = "user user-#{user.level_string.downcase}" + user_class = "user user-#{user.level_string.downcase} #{classes}" user_class += " user-post-approver" if user.can_approve_posts? user_class += " user-post-uploader" if user.can_upload_free? user_class += " user-banned" if user.is_banned? diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 9191c17b9..db731eb71 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -20,6 +20,10 @@ module ComponentsHelper render PostVotesComponent.new(post: post, **options) end + def render_post_votes_tooltip(post, **options) + render PostVotesTooltipComponent.new(post: post, **options) + end + def render_post_navbar(post, **options) render PostNavbarComponent.new(post: post, **options) end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7daf6f1cd..094c96578 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -43,6 +43,7 @@ import PopupMenuComponent from "../../components/popup_menu_component/popup_menu import Post from "../src/javascripts/posts.js"; import PostModeMenu from "../src/javascripts/post_mode_menu.js"; import PostTooltip from "../src/javascripts/post_tooltips.js"; +import PostVotesTooltipComponent from "../../components/post_votes_tooltip_component/post_votes_tooltip_component.js"; import RelatedTag from "../src/javascripts/related_tag.js"; import Shortcuts from "../src/javascripts/shortcuts.js"; import TagCounter from "../src/javascripts/tag_counter.js"; @@ -64,6 +65,7 @@ Danbooru.PopupMenuComponent = PopupMenuComponent; Danbooru.Post = Post; Danbooru.PostModeMenu = PostModeMenu; Danbooru.PostTooltip = PostTooltip; +Danbooru.PostVotesTooltipComponent = PostVotesTooltipComponent; Danbooru.RelatedTag = RelatedTag; Danbooru.Shortcuts = Shortcuts; Danbooru.TagCounter = TagCounter; diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index 8aca7d161..ea7860aa3 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -218,6 +218,9 @@ html { --post-artist-commentary-container-background: var(--grey-0); --post-artist-commentary-container-border-color: var(--grey-1); + --post-upvote-color: var(--link-color); + --post-downvote-color: var(--red-5); + --note-body-background: #FFE; --note-body-text-color: var(--black); --note-body-border-color: var(--black); @@ -418,6 +421,9 @@ body[data-current-user-theme="dark"] { --post-artist-commentary-container-background: var(--grey-8); --post-artist-commentary-container-border-color: var(--grey-7); + --post-upvote-color: var(--link-color); + --post-downvote-color: var(--red-4); + --unsaved-note-box-border-color: var(--red-5); --movable-note-box-border-color: var(--blue-5); --note-preview-border-color: var(--red-5); diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss index 2b5a2f4e7..6433d2967 100644 --- a/app/javascript/src/styles/common/utilities.scss +++ b/app/javascript/src/styles/common/utilities.scss @@ -22,6 +22,12 @@ $spacer: 0.25rem; /* 4px */ .text-xs { font-size: var(--text-xs); } .text-sm { font-size: var(--text-sm); } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .leading-none { line-height: 1; } .absolute { position: absolute; } @@ -52,6 +58,9 @@ $spacer: 0.25rem; /* 4px */ .p-0\.5 { padding: 0.5 * $spacer; } .p-4 { padding: 4 * $spacer; } +.pr-2 { padding-right: 2 * $spacer; } +.pr-4 { padding-right: 4 * $spacer; } + .w-1\/4 { width: 25%; } .w-full { width: 100%; } @@ -73,6 +82,60 @@ $spacer: 0.25rem; /* 4px */ .items-center { align-items: center; } .justify-center { justify-content: center; } +.thin-scrollbar { + overflow-x: hidden; + overflow-y: auto; + padding-right: 2 * $spacer; + overscroll-behavior: contain; // https://caniuse.com/css-overscroll-behavior + + // Firefox only + // https://caniuse.com/?search=scrollbar-width + // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + + &::-webkit-scrollbar-button { + width: 0; + height: 0; + } + + &::-webkit-scrollbar-thumb { + background: var(--post-tooltip-scrollbar-background); + border: none; + border-radius: 0; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--post-tooltip-scrollbar-thumb-color); + } + + &::-webkit-scrollbar-thumb:active { + background: var(--post-tooltip-scrollbar-thumb-color); + } + + &::-webkit-scrollbar-track { + background: var(--post-tooltip-scrollbar-track-background); + border: none; + border-radius: 0; + } + + &::-webkit-scrollbar-track:hover { + background: var(--post-tooltip-scrollbar-track-background); + } + + &::-webkit-scrollbar-track:active { + background: var(--post-tooltip-scrollbar-track-background); + } + + &::-webkit-scrollbar-corner { + background: transparent; + } +} + @media screen and (min-width: 660px) { .md\:inline-block { display: inline-block; } .md\:flex { display: flex; } diff --git a/app/javascript/src/styles/specific/post_tooltips.scss b/app/javascript/src/styles/specific/post_tooltips.scss index 7d947c54c..c23ddfcc3 100644 --- a/app/javascript/src/styles/specific/post_tooltips.scss +++ b/app/javascript/src/styles/specific/post_tooltips.scss @@ -1,50 +1,6 @@ $tooltip-line-height: 16px; $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. -@mixin thin-scrollbar { - &::-webkit-scrollbar { - width: 5px; - height: 5px; - } - - &::-webkit-scrollbar-button { - width: 0; - height: 0; - } - - &::-webkit-scrollbar-thumb { - background: var(--post-tooltip-scrollbar-background); - border: none; - border-radius: 0; - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--post-tooltip-scrollbar-thumb-color); - } - - &::-webkit-scrollbar-thumb:active { - background: var(--post-tooltip-scrollbar-thumb-color); - } - - &::-webkit-scrollbar-track { - background: var(--post-tooltip-scrollbar-track-background); - border: none; - border-radius: 0; - } - - &::-webkit-scrollbar-track:hover { - background: var(--post-tooltip-scrollbar-track-background); - } - - &::-webkit-scrollbar-track:active { - background: var(--post-tooltip-scrollbar-track-background); - } - - &::-webkit-scrollbar-corner { - background: transparent; - } -} - .tippy-box[data-theme~="post-tooltip"] { min-width: 20em; max-width: 40em !important; @@ -59,7 +15,6 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. } .post-tooltip-body { - @include thin-scrollbar; max-height: $tooltip-body-height; overflow-y: auto; display: flex; diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index d33f6bd77..e927f4ab8 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -101,6 +101,7 @@
+
diff --git a/app/views/post_votes/_search.html.erb b/app/views/post_votes/_search.html.erb new file mode 100644 index 000000000..c9f4071e0 --- /dev/null +++ b/app/views/post_votes/_search.html.erb @@ -0,0 +1,7 @@ +<%= search_form_for(post_votes_path) do |f| %> + <%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %> + <%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %> + <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> + <%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %> + <%= f.submit "Search" %> +<% end %> diff --git a/app/views/post_votes/index.html+compact.erb b/app/views/post_votes/index.html+compact.erb new file mode 100644 index 000000000..cd861d79e --- /dev/null +++ b/app/views/post_votes/index.html+compact.erb @@ -0,0 +1,24 @@ +<%= render "posts/partials/common/secondary_links" %> + +
+
+ <%= render "search" %> + + <%= table_for @post_votes, class: "striped autofit" do |t| %> + <% t.column "Score" do |vote| %> + <%= link_to sprintf("%+d", vote.score), post_votes_path(search: { score: vote.score }) %> + <% end %> + + <% t.column "Voter" do |vote| %> + <%= link_to_user vote.user %> + <%= link_to "ยป", post_votes_path(search: { user_name: vote.user.name }) %> + <% end %> + + <% t.column "Created" do |vote| %> + <%= time_ago_in_words_tagged(vote.created_at) %> + <% end %> + <% end %> + + <%= numbered_paginator(@post_votes) %> +
+
diff --git a/app/views/post_votes/index.html+tooltip.erb b/app/views/post_votes/index.html+tooltip.erb new file mode 100644 index 000000000..b351de668 --- /dev/null +++ b/app/views/post_votes/index.html+tooltip.erb @@ -0,0 +1 @@ +<%= render_post_votes_tooltip(@post, current_user: CurrentUser.user) %> diff --git a/app/views/post_votes/index.html.erb b/app/views/post_votes/index.html.erb index c93eb5e77..096c683d3 100644 --- a/app/views/post_votes/index.html.erb +++ b/app/views/post_votes/index.html.erb @@ -1,12 +1,6 @@
- <%= search_form_for(post_votes_path) do |f| %> - <%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %> - <%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %> - <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> - <%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %> - <%= f.submit "Search" %> - <% end %> + <%= render "search" %> <%= table_for @post_votes, class: "striped autofit" do |t| %> <% t.column "Post" do |vote| %> diff --git a/app/views/posts/show.html+tooltip.erb b/app/views/posts/show.html+tooltip.erb index ca24f442e..bbaa83ad8 100644 --- a/app/views/posts/show.html+tooltip.erb +++ b/app/views/posts/show.html+tooltip.erb @@ -40,7 +40,7 @@ <% end %>
-
"> +
">
<% if params[:preview].truthy? %> <%= post_preview(@post, show_deleted: true, compact: true) %> diff --git a/test/functional/post_votes_controller_test.rb b/test/functional/post_votes_controller_test.rb index d329cda62..ff5c47605 100644 --- a/test/functional/post_votes_controller_test.rb +++ b/test/functional/post_votes_controller_test.rb @@ -10,8 +10,8 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest context "index action" do setup do @user = create(:user, enable_private_favorites: true) - create(:post_vote, user: @user, score: 1) - create(:post_vote, user: @user, score: -1) + @upvote = create(:post_vote, user: @user, score: 1) + @downvote = create(:post_vote, user: @user, score: -1) end should "render" do @@ -19,6 +19,16 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "render for a compact view" do + get post_votes_path(variant: "compact") + assert_response :success + end + + should "render for a tooltip" do + get post_votes_path(search: { post_id: @upvote.post_id }, variant: "tooltip") + assert_response :success + end + context "as a user" do should "show the user all their own votes" do get_auth post_votes_path, @user