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 @@ +
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 @@