favorites: show favlist when hovering over favcount.
Changes: * Make it so you can click or hover over a post's favorite count to see the list of public favorites. * Remove the "Show »" button next to the favorite count. * Make the favorites list visible to all users. Before favorites were only visible to Gold users. * Make the /favorites page show the list of all public favorites, instead of redirecting to the current user's favorites. * Add /posts/:id/favorites endpoint. * Add /users/:id/favorites endpoint. This is for several reasons: * To make viewing favorites work the same way as viewing upvotes. * To make posts load faster for Gold users. Before, we loaded all the favorites when viewing a post, even when the user didn't look at them. This made pageloads slower for posts that had hundreds or thousands of favorites. Now we only load the favlist if the user hovers over the favcount. * To make the favorite list visible to all users. Before, it wasn't visible to non-Gold users, because of the performance issue listed above. * To make it more obvious that favorites are public by default. Before, since regular users could only see the favcount, they may have mistakenly believed other users couldn't see their favorites.
This commit is contained in:
24
app/components/favorites_tooltip_component.rb
Normal file
24
app/components/favorites_tooltip_component.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This component represents the tooltip that displays when you hover over a post's favorite count.
|
||||
class FavoritesTooltipComponent < ApplicationComponent
|
||||
attr_reader :post, :current_user
|
||||
|
||||
def initialize(post:, current_user:)
|
||||
super
|
||||
@post = post
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def favorites
|
||||
post.favorites.includes(:user).order(id: :desc)
|
||||
end
|
||||
|
||||
def favoriter_name(favorite)
|
||||
if policy(favorite).can_see_favoriter?
|
||||
link_to_user(favorite.user)
|
||||
else
|
||||
tag.i("hidden")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="favorites-tooltip thin-scrollbar">
|
||||
<div class="post-favoriters">
|
||||
<% favorites.each do |favorite| %>
|
||||
<div class="post-favoriter truncate">
|
||||
<%= favoriter_name(favorite) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 FavoritesTooltipComponent {
|
||||
// Trigger on the post favcount link.
|
||||
static TARGET_SELECTOR = "span.post-favcount a";
|
||||
static SHOW_DELAY = 125;
|
||||
static HIDE_DELAY = 125;
|
||||
static DURATION = 250;
|
||||
static instance = null;
|
||||
|
||||
static initialize() {
|
||||
if ($(FavoritesTooltipComponent.TARGET_SELECTOR).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FavoritesTooltipComponent.instance = delegate("body", {
|
||||
allowHTML: true,
|
||||
appendTo: document.querySelector("#post-favorites-tooltips"),
|
||||
delay: [FavoritesTooltipComponent.SHOW_DELAY, FavoritesTooltipComponent.HIDE_DELAY],
|
||||
duration: FavoritesTooltipComponent.DURATION,
|
||||
interactive: true,
|
||||
maxWidth: "none",
|
||||
target: FavoritesTooltipComponent.TARGET_SELECTOR,
|
||||
theme: "common-tooltip",
|
||||
touch: false,
|
||||
|
||||
onShow: FavoritesTooltipComponent.onShow,
|
||||
onHide: FavoritesTooltipComponent.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(`/posts/${postId}/favorites?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 favorites for post #${postId} (error: ${error.status} ${error.statusText})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async onHide(instance) {
|
||||
if (instance._request?.state() === "pending") {
|
||||
instance._request.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(FavoritesTooltipComponent.initialize);
|
||||
|
||||
export default FavoritesTooltipComponent;
|
||||
@@ -0,0 +1,13 @@
|
||||
.favorites-tooltip {
|
||||
font-size: var(--text-xs);
|
||||
max-height: 240px;
|
||||
|
||||
.post-favoriter {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
span.post-favcount a {
|
||||
color: var(--text-color);
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
class FavoritesController < ApplicationController
|
||||
respond_to :html, :xml, :json, :js
|
||||
respond_to :js, :json, :html, :xml
|
||||
|
||||
def index
|
||||
authorize Favorite
|
||||
if !request.format.html?
|
||||
@favorites = Favorite.visible(CurrentUser.user).paginated_search(params)
|
||||
respond_with(@favorites)
|
||||
elsif params[:user_id].present?
|
||||
user = User.find(params[:user_id])
|
||||
redirect_to posts_path(tags: "ordfav:#{user.name}", format: request.format.symbol)
|
||||
elsif !CurrentUser.is_anonymous?
|
||||
redirect_to posts_path(tags: "ordfav:#{CurrentUser.user.name}", format: request.format.symbol)
|
||||
else
|
||||
redirect_to posts_path(format: request.format.symbol)
|
||||
end
|
||||
post_id = params[:post_id] || params[:search][:post_id]
|
||||
user_id = params[:user_id] || params[:search][:user_id]
|
||||
user_name = params[:search][:user_name]
|
||||
@post = Post.find(post_id) if post_id
|
||||
@user = User.find(user_id) if user_id
|
||||
@user = User.find_by_name(user_name) if user_name
|
||||
|
||||
@favorites = authorize Favorite.visible(CurrentUser.user).paginated_search(params, defaults: { post_id: @post&.id, user_id: @user&.id })
|
||||
respond_with(@favorites)
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
@@ -24,6 +24,10 @@ module ComponentsHelper
|
||||
render PostVotesTooltipComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
def render_favorites_tooltip(post, **options)
|
||||
render FavoritesTooltipComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
def render_post_navbar(post, **options)
|
||||
render PostNavbarComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
@@ -37,6 +37,7 @@ import Blacklist from "../src/javascripts/blacklists.js";
|
||||
import CommentComponent from "../../components/comment_component/comment_component.js";
|
||||
import CurrentUser from "../src/javascripts/current_user.js";
|
||||
import Dtext from "../src/javascripts/dtext.js";
|
||||
import FavoritesTooltipComponent from "../../components/favorites_tooltip_component/favorites_tooltip_component.js";
|
||||
import IqdbQuery from "../src/javascripts/iqdb_queries.js";
|
||||
import Note from "../src/javascripts/notes.js";
|
||||
import PopupMenuComponent from "../../components/popup_menu_component/popup_menu_component.js";
|
||||
@@ -59,6 +60,7 @@ Danbooru.Blacklist = Blacklist;
|
||||
Danbooru.CommentComponent = CommentComponent;
|
||||
Danbooru.CurrentUser = CurrentUser;
|
||||
Danbooru.Dtext = Dtext;
|
||||
Danbooru.FavoritesTooltipComponent = FavoritesTooltipComponent;
|
||||
Danbooru.IqdbQuery = IqdbQuery;
|
||||
Danbooru.Note = Note;
|
||||
Danbooru.PopupMenuComponent = PopupMenuComponent;
|
||||
|
||||
@@ -30,7 +30,6 @@ Post.initialize_all = function() {
|
||||
if ($("#c-posts").length && $("#a-show").length) {
|
||||
this.initialize_links();
|
||||
this.initialize_post_relationship_previews();
|
||||
this.initialize_favlist();
|
||||
this.initialize_post_sections();
|
||||
this.initialize_post_image_resize_links();
|
||||
this.initialize_recommended();
|
||||
@@ -242,13 +241,6 @@ Post.toggle_relationship_preview = function(preview, preview_link) {
|
||||
}
|
||||
}
|
||||
|
||||
Post.initialize_favlist = function() {
|
||||
$("#show-favlist-link, #hide-favlist-link").on("click.danbooru", function(e) {
|
||||
$("#favlist, #show-favlist-link, #hide-favlist-link").toggle();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
Post.view_original = function(e = null) {
|
||||
if (Utility.test_max_width(660)) {
|
||||
// Do the default behavior (navigate to image)
|
||||
|
||||
@@ -170,10 +170,6 @@ div#c-posts {
|
||||
}
|
||||
}
|
||||
|
||||
#favlist {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#recommended.loading-recommended-posts {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -6,10 +6,16 @@ class Favorite < ApplicationRecord
|
||||
after_create :upvote_post_on_create
|
||||
after_destroy :unvote_post_on_destroy
|
||||
|
||||
scope :public_favorites, -> { where(user: User.bit_prefs_match(:enable_private_favorites, false)) }
|
||||
scope :public_favorites, -> { where(user: User.has_public_favorites) }
|
||||
|
||||
def self.visible(user)
|
||||
user.is_admin? ? all : where(user: user).or(public_favorites)
|
||||
if user.is_admin?
|
||||
all
|
||||
elsif user.is_anonymous?
|
||||
public_favorites
|
||||
else
|
||||
where(user: user).or(public_favorites)
|
||||
end
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
|
||||
@@ -50,7 +50,6 @@ class Post < ApplicationRecord
|
||||
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
||||
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
||||
has_many :favorites, dependent: :destroy
|
||||
has_many :favorited_users, through: :favorites, source: :user
|
||||
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
|
||||
|
||||
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning
|
||||
@@ -667,13 +666,6 @@ class Post < ApplicationRecord
|
||||
Favorite.exists?(post: self, user: user)
|
||||
end
|
||||
|
||||
# Users who publicly favorited this post, ordered by time of favorite.
|
||||
def visible_favorited_users(viewer)
|
||||
favorited_users.order("favorites.id DESC").select do |fav_user|
|
||||
Pundit.policy!(viewer, fav_user).can_see_favorites?
|
||||
end
|
||||
end
|
||||
|
||||
def favorite_groups
|
||||
FavoriteGroup.for_post(id)
|
||||
end
|
||||
|
||||
@@ -6,4 +6,8 @@ class FavoritePolicy < ApplicationPolicy
|
||||
def destroy?
|
||||
record.user_id == user.id
|
||||
end
|
||||
|
||||
def can_see_favoriter?
|
||||
user.is_admin? || record.user == user || !record.user.enable_private_favorites?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,10 +59,6 @@ class PostPolicy < ApplicationPolicy
|
||||
user.is_gold?
|
||||
end
|
||||
|
||||
def can_view_favlist?
|
||||
user.is_gold?
|
||||
end
|
||||
|
||||
# whether to show the + - links in the tag list.
|
||||
def show_extra_links?
|
||||
user.is_gold?
|
||||
|
||||
6
app/views/favorites/_search.html.erb
Normal file
6
app/views/favorites/_search.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= search_form_for(favorites_path) do |f| %>
|
||||
<%= f.input :user_name, label: "Favoriter", input_html: { value: @user&.name, "data-autocomplete": "user" } %>
|
||||
<%= f.input :post_id, label: "Post", input_html: { value: @post&.id } %>
|
||||
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
@@ -4,19 +4,8 @@
|
||||
$("#add-to-favorites, #add-fav-button, #remove-from-favorites, #remove-fav-button").toggle();
|
||||
$("#remove-fav-button").addClass("animate");
|
||||
$("span.post-votes[data-id=<%= @post.id %>]").replaceWith("<%= j render_post_votes @post, current_user: CurrentUser.user %>");
|
||||
$("#favcount-for-post-<%= @post.id %>").text(<%= @post.fav_count %>);
|
||||
$("span.post-favcount[data-id=<%= @post.id %>]").html("<%= j link_to @post.fav_count, favorites_path(post_id: @post.id, variant: :compact) %>");
|
||||
$(".fav-buttons").toggleClass("fav-buttons-false").toggleClass("fav-buttons-true");
|
||||
|
||||
<% if policy(@post).can_view_favlist? %>
|
||||
var fav_count = <%= @post.fav_count %>;
|
||||
$("#favlist").html("<%= j render "posts/partials/show/favorite_list", post: @post %>");
|
||||
|
||||
if (fav_count === 0) {
|
||||
$("#show-favlist-link, #hide-favlist-link, #favlist").hide();
|
||||
} else if (!$("#favlist").is(":visible")) {
|
||||
$("#show-favlist-link").show();
|
||||
}
|
||||
<% end %>
|
||||
|
||||
Danbooru.Utility.notice("<%= j flash[:notice] %>");
|
||||
<% end %>
|
||||
|
||||
3
app/views/favorites/index.html+tooltip.erb
Normal file
3
app/views/favorites/index.html+tooltip.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<% if @post.present? %>
|
||||
<%= render_favorites_tooltip(@post, current_user: CurrentUser.user) %>
|
||||
<% end %>
|
||||
44
app/views/favorites/index.html.erb
Normal file
44
app/views/favorites/index.html.erb
Normal file
@@ -0,0 +1,44 @@
|
||||
<div id="c-favorites">
|
||||
<div id="a-index">
|
||||
<% if @post %>
|
||||
<h1><%= link_to "Favorites", favorites_path %>/<%= link_to @post.dtext_shortlink, @post %></h1>
|
||||
<% elsif @user %>
|
||||
<h1><%= link_to "Favorites", favorites_path %>/<%= link_to_user @user %></h1>
|
||||
<% else %>
|
||||
<h1><%= link_to "Favorites", favorites_path %></h1>
|
||||
<% end %>
|
||||
|
||||
<%= render "search" %>
|
||||
|
||||
<%= table_for @favorites.includes(:user, post: [:uploader, :media_asset]), class: "striped autofit" do |t| %>
|
||||
<% if @post.nil? %>
|
||||
<% t.column "Post" do |favorite| %>
|
||||
<%= post_preview(favorite.post, show_deleted: true) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Tags", td: {class: "col-expand"} do |favorite| %>
|
||||
<%= render_inline_tag_list(favorite.post) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Uploader" do |favorite| %>
|
||||
<%= link_to_user favorite.post.uploader %>
|
||||
<%= link_to "»", favorites_path(search: { post_tags_match: "user:#{favorite.post.uploader.name}" }) %>
|
||||
<div><%= time_ago_in_words_tagged(favorite.post.created_at) %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @user.nil? %>
|
||||
<% t.column "Favoriter" do |favorite| %>
|
||||
<% if policy(favorite).can_see_favoriter? %>
|
||||
<%= link_to_user favorite.user %>
|
||||
<%= link_to "»", favorites_path(search: { user_name: favorite.user.name }) %>
|
||||
<% else %>
|
||||
<i>hidden</i>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@favorites) %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,6 +102,7 @@
|
||||
<div id="post-tooltips"></div>
|
||||
<div id="user-tooltips"></div>
|
||||
<div id="post-votes-tooltips"></div>
|
||||
<div id="post-favorites-tooltips"></div>
|
||||
<div id="popup-menus"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<%# post %>
|
||||
<%= safe_join(post.visible_favorited_users(CurrentUser.user).map { |user| link_to_user(user) }, ", ") %>
|
||||
@@ -23,14 +23,12 @@
|
||||
<li id="post-info-score">
|
||||
Score: <%= render_post_votes post, current_user: CurrentUser.user %>
|
||||
</li>
|
||||
<li id="post-info-favorites">Favorites: <span id="favcount-for-post-<%= post.id %>"><%= post.fav_count %></span>
|
||||
<% if policy(post).can_view_favlist? %>
|
||||
<%= link_to "Show »", "#", id: "show-favlist-link", style: ("display: none;" if post.fav_count == 0) %>
|
||||
<%= link_to "« Hide", "#", id: "hide-favlist-link", style: "display: none;" %>
|
||||
<div id="favlist" style="display: none;" class="ml-4">
|
||||
<%= render "posts/partials/show/favorite_list", post: post %>
|
||||
</div>
|
||||
<% end %></li>
|
||||
<li id="post-info-favorites">
|
||||
Favorites:
|
||||
<%= tag.span class: "post-favcount", "data-id": post.id do %>
|
||||
<%= link_to post.fav_count, post_favorites_path(post) %>
|
||||
<% end %>
|
||||
</li>
|
||||
<li id="post-info-status">
|
||||
Status:
|
||||
<% if post.is_pending? %>
|
||||
|
||||
Reference in New Issue
Block a user