users: add username tooltips.

This commit is contained in:
evazion
2020-07-13 11:28:58 -05:00
parent d7f489b68e
commit 88bbd1e3f0
12 changed files with 354 additions and 58 deletions

View File

@@ -27,7 +27,7 @@ class UsersController < ApplicationController
def index
if params[:name].present?
@user = User.find_by_name!(params[:name])
redirect_to user_path(@user)
redirect_to user_path(@user, variant: params[:variant])
return
end
@@ -42,7 +42,9 @@ class UsersController < ApplicationController
def show
@user = authorize User.find(params[:id])
respond_with(@user, methods: @user.full_attributes)
respond_with(@user, methods: @user.full_attributes) do |format|
format.html.tooltip { render layout: false }
end
end
def profile

View File

@@ -121,6 +121,10 @@ module ApplicationHelper
raw content_tag(:time, duration, datetime: datetime, title: title)
end
def humanized_number(number)
number_to_human number, units: { thousand: "k", million: "m" }, format: "%n%u"
end
def time_ago_in_words_tagged(time, compact: false)
if time.nil?
tag.em(tag.time("unknown"))
@@ -162,8 +166,10 @@ module ApplicationHelper
end
end
def link_to_ip(ip)
link_to ip, ip_addresses_path(search: { ip_addr: ip, group_by: "user" })
def link_to_ip(ip, shorten: false, **options)
ip_addr = IPAddr.new(ip.to_s)
ip_addr.prefix = 64 if ip_addr.ipv6? && shorten
link_to ip_addr.to_s, ip_addresses_path(search: { ip_addr: ip, group_by: "user" }), **options
end
def link_to_search(search)
@@ -186,11 +192,13 @@ module ApplicationHelper
def link_to_user(user)
return "anonymous" if user.blank?
user_class = "user-#{user.level_string.downcase}"
user_class = "user user-#{user.level_string.downcase}"
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?
link_to(user.pretty_name, user_path(user), :class => user_class)
data = { "user-id": user.id, "user-name": user.name, "user-level": user.level }
link_to(user.pretty_name, user_path(user), class: user_class, data: data)
end
def mod_link_to_user(user, positive_or_negative)

View File

@@ -24,7 +24,7 @@ PostTooltip.initialize = function () {
interactive: true,
maxWidth: PostTooltip.MAX_WIDTH,
target: PostTooltip.POST_SELECTOR,
theme: "post-tooltip",
theme: "common-tooltip post-tooltip",
touch: false,
onCreate: PostTooltip.on_create,
@@ -63,13 +63,13 @@ PostTooltip.on_show = async function (instance) {
}
try {
$tooltip.addClass("post-tooltip-loading");
$tooltip.addClass("tooltip-loading");
instance._request = $.get(`/posts/${post_id}`, { variant: "tooltip", preview: preview });
let html = await instance._request;
instance.setContent(html);
$tooltip.removeClass("post-tooltip-loading");
$tooltip.removeClass("tooltip-loading");
} catch (error) {
if (error.status !== 0 && error.statusText !== "abort") {
Utility.error(`Error displaying tooltip for post #${post_id} (error: ${error.status} ${error.statusText})`);

View File

@@ -0,0 +1,72 @@
import Utility from './utility';
import { delegate } from 'tippy.js';
import 'tippy.js/dist/tippy.css';
let UserTooltip = {};
UserTooltip.SELECTOR = "*:not(.user-tooltip-name) > a.user, a.dtext-user-id-link, a.dtext-user-mention-link";
UserTooltip.SHOW_DELAY = 500;
UserTooltip.HIDE_DELAY = 125;
UserTooltip.DURATION = 250;
UserTooltip.MAX_WIDTH = 600;
UserTooltip.initialize = function () {
delegate("body", {
allowHTML: true,
appendTo: document.body,
delay: [UserTooltip.SHOW_DELAY, UserTooltip.HIDE_DELAY],
duration: UserTooltip.DURATION,
interactive: true,
maxWidth: UserTooltip.MAX_WIDTH,
target: UserTooltip.SELECTOR,
theme: "common-tooltip user-tooltip",
touch: false,
onShow: UserTooltip.on_show,
onHide: UserTooltip.on_hide,
});
};
UserTooltip.on_show = async function (instance) {
let $target = $(instance.reference);
let $tooltip = $(instance.popper);
// skip if tooltip has already been rendered.
if ($tooltip.has(".user-tooltip-body").length) {
return;
}
try {
$tooltip.addClass("tooltip-loading");
if ($target.is("a.dtext-user-id-link")) {
let user_id = /\/users\/(\d+)/.exec($target.attr("href"))[1];
instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" });
} else if ($target.is("a.user")) {
let user_id = $target.attr("data-user-id");
instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" });
} else if ($target.is("a.dtext-user-mention-link")) {
let user_name = $target.attr("data-user-name");
instance._request = $.get(`/users`, { name: user_name, 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 tooltip (error: ${error.status} ${error.statusText})`);
}
}
};
UserTooltip.on_hide = function (instance) {
if (instance._request?.state() === "pending") {
instance._request.abort();
}
}
$(document).ready(UserTooltip.initialize);
export default UserTooltip

View File

@@ -129,6 +129,14 @@ table tfoot {
font-size: 0.9em;
}
a.link-plain {
color: unset;
&:hover {
text-decoration: underline;
}
}
.fixed-width-container {
max-width: 70em;
}

View File

@@ -2,6 +2,7 @@
--body-background-color: white;
--text-color: hsl(0, 0%, 15%);
--inverse-text-color: white;
--muted-text-color: hsl(0, 0%, 55%);
--header-color: hsl(0, 0%, 15%);
@@ -82,6 +83,9 @@
--post-tooltip-scrollbar-track-background: #EEEEEE;
--post-tooltip-scrollbar-track-border: 0 none white;
--user-tooltip-positive-feedback-color: orange;
--user-tooltip-negative-feedback-color: red;
--preview-current-post-background: rgba(0, 0, 0, 0.1);
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
@@ -201,6 +205,7 @@
--user-platinum-color: gray;
--user-gold-color: #00F;
--user-member-color: var(--link-color);
--user-banned-color: black;
--news-updates-background: #EEE;
--news-updates-border: 2px solid #666;
@@ -261,6 +266,7 @@ body[data-current-user-theme="dark"] {
/* main text colors */
--text-color: var(--grey-5);
--inverse-text-color: white;
--muted-text-color: var(--grey-4);
--header-color: var(--grey-6);
@@ -283,6 +289,7 @@ body[data-current-user-theme="dark"] {
--collection-pool-color: var(--general-tag-color);
--collection-pool-hover-color: var(--general-tag-hover-color);
--user-banned-color: var(--grey-1);
--user-member-color: var(--blue-1);
--user-gold-color: var(--yellow-1);
--user-platinum-color: var(--grey-4);
@@ -395,6 +402,9 @@ body[data-current-user-theme="dark"] {
--post-tooltip-scrollbar-track-background: var(--grey-1);
--post-tooltip-scrollbar-track-border: none;
--user-tooltip-positive-feedback-color: var(--yellow-1);
--user-tooltip-negative-feedback-color: var(--red-1);
--preview-pending-color: var(--blue-1);
--preview-flagged-color: var(--red-1);
--preview-deleted-color: var(--grey-5);

View File

@@ -17,7 +17,8 @@ div.list-of-messages {
a.message-timestamp {
font-style: italic;
color: var(--text-color);
font-size: 0.90em;
color: var(--muted-text-color);
&:hover { text-decoration: underline; }
}
}

View File

@@ -0,0 +1,51 @@
div[data-tippy-root].tooltip-loading {
visibility: hidden !important;
}
.tippy-box[data-theme~="common-tooltip"] {
box-sizing: border-box;
border: 1px solid var(--post-tooltip-border-color);
border-radius: 4px;
color: var(--text-color);
background-color: var(--post-tooltip-background-color);
background-clip: padding-box;
box-shadow: var(--post-tooltip-box-shadow);
/* bordered arrow styling; see https://github.com/atomiks/tippyjs/blob/master/src/scss/themes/light-border.scss */
&[data-placement^=bottom] {
> .tippy-arrow:before {
border-bottom-color: var(--post-tooltip-background-color);
bottom: 16px;
}
> .tippy-arrow:after {
border-bottom-color: var(--post-tooltip-border-color);
border-width: 0 7px 7px;
top: -8px;
left: 1px;
}
}
&[data-placement^=top] {
> .tippy-arrow:before {
border-top-color: var(--post-tooltip-background-color);
}
> .tippy-arrow:after {
border-top-color: var(--post-tooltip-border-color);
border-width: 7px 7px 0;
top: 17px;
left: 1px;
}
}
> .tippy-arrow:after {
border-color: transparent;
border-style: solid;
content: "";
position: absolute;
z-index: -1;
}
}

View File

@@ -47,17 +47,9 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
.tippy-box[data-theme~="post-tooltip"] {
min-width: 200px;
box-sizing: border-box;
font-size: 11px;
line-height: $tooltip-line-height;
border: 1px solid var(--post-tooltip-border-color);
border-radius: 4px;
background-color: var(--post-tooltip-background-color);
background-clip: padding-box;
box-shadow: var(--post-tooltip-box-shadow);
.tippy-content {
padding: 0;
@@ -118,44 +110,4 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
font-size: 10px;
}
}
/* bordered arrow styling; see https://github.com/atomiks/tippyjs/blob/master/src/scss/themes/light-border.scss */
&[data-placement^=bottom] {
> .tippy-arrow:before {
border-bottom-color: var(--post-tooltip-background-color);
bottom: 16px;
}
> .tippy-arrow:after {
border-bottom-color: var(--post-tooltip-border-color);
border-width: 0 7px 7px;
top: -8px;
left: 1px;
}
}
&[data-placement^=top] {
> .tippy-arrow:before {
border-top-color: var(--post-tooltip-background-color);
}
> .tippy-arrow:after {
border-top-color: var(--post-tooltip-border-color);
border-width: 7px 7px 0;
top: 17px;
left: 1px;
}
}
> .tippy-arrow:after {
border-color: transparent;
border-style: solid;
content: "";
position: absolute;
z-index: -1;
}
}
div[data-tippy-root].post-tooltip-loading {
visibility: hidden !important;
}

View File

@@ -0,0 +1,78 @@
.tippy-box[data-theme~="user-tooltip"] {
line-height: 1.25em;
min-width: 350px;
.user-tooltip-header {
margin-bottom: 1em;
display: grid;
grid:
"avatar header-top"
"avatar header-bottom" /
32px 1fr;
column-gap: 0.25em;
.user-tooltip-avatar {
font-size: 32px;
grid-area: avatar;
align-self: center;
}
.user-tooltip-header-top {
grid-area: header-top;
.user-tooltip-badge {
color: var(--inverse-text-color);
font-size: 0.70em;
padding: 2px 4px;
margin-right: 0.25em;
border-radius: 3px;
&.user-tooltip-badge-admin { background-color: var(--user-admin-color); }
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); }
&.user-tooltip-badge-contributor { background-color: var(--user-builder-color); }
&.user-tooltip-badge-builder { background-color: var(--user-builder-color); }
&.user-tooltip-badge-platinum { background-color: var(--user-platinum-color); }
&.user-tooltip-badge-gold { background-color: var(--user-gold-color); }
&.user-tooltip-badge-member { background-color: var(--user-member-color); }
&.user-tooltip-badge-banned { background-color: var(--user-banned-color); }
&.user-tooltip-badge-positive-feedback {
color: var(--user-tooltip-positive-feedback-color);
border: 1px solid;
}
&.user-tooltip-badge-negative-feedback {
color: var(--user-tooltip-negative-feedback-color);
border: 1px solid;
}
}
}
.user-tooltip-header-bottom {
grid-area: header-bottom;
color: var(--muted-text-color);
font-size: 0.75em;
}
}
.user-tooltip-stats {
display: grid;
grid: auto / repeat(3, 1fr);
column-gap: 1em;
row-gap: 0.5em;
.user-tooltip-stat-item {
text-align: center;
.user-tooltip-stat-value {
font-weight: bold;
}
.user-tooltip-stat-name {
font-size: 0.90em;
color: var(--muted-text-color);
}
}
}
}

View File

@@ -0,0 +1,86 @@
<div class="user-tooltip">
<div class="user-tooltip-header">
<i class="fas fa-user-circle user-tooltip-avatar"></i>
<div class="user-tooltip-header-top">
<span class="user-tooltip-name"><%= link_to_user @user %></span>
<% if @user.is_banned? %>
<%= link_to "Banned", users_path(search: { is_banned: true }), class: "user-tooltip-badge user-tooltip-badge-banned" %>
<% elsif @user.is_admin? %>
<%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %>
<% elsif @user.is_moderator? %>
<%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %>
<% elsif @user.can_approve_posts? %>
<%= link_to "Approver", users_path(search: { can_approve_posts: true }), class: "user-tooltip-badge user-tooltip-badge-approver" %>
<% elsif @user.can_upload_free? %>
<%= link_to "Contributor", users_path(search: { can_upload_free: true }), class: "user-tooltip-badge user-tooltip-badge-contributor" %>
<% else %>
<%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %>
<% end %>
<% if @user.positive_feedback_count > 0 %>
<%= link_to user_feedbacks_path(search: { user_id: @user.id }), class: "link-plain user-tooltip-badge user-tooltip-badge-positive-feedback" do %>
<i class="fas fa-medal"></i>
<span><%= @user.positive_feedback_count %>
<% end %>
<% elsif @user.negative_feedback_count > 0 %>
<%= link_to user_feedbacks_path(search: { user_id: @user.id }), class: "link-plain user-tooltip-badge user-tooltip-badge-negative-feedback" do %>
<i class="fas fa-times-circle"></i>
<span><%= @user.negative_feedback_count %>
<% end %>
<% end %>
</div>
<div class="user-tooltip-header-bottom">
<%= time_tag @user.created_at.to_date.iso8601, @user.created_at, class: "user-tooltip-created-at" %>
<% if @user.last_ip_addr.present? && policy(IpAddress).show? %>
· <%= link_to_ip @user.last_ip_addr, shorten: true, class: "link-plain" %>
<% end %>
<% @user.user_name_change_requests.visible(CurrentUser.user).count.tap do |name_change_count| %>
<% if name_change_count > 0 %>
· <%= link_to pluralize(name_change_count, "other name"), user_name_change_requests_path(search: { user_id: @user.id }), class: "link-plain" %>
<% end %>
<% end %>
</div>
</div>
<ul class="user-tooltip-stats">
<li class="user-tooltip-stat-item">
<%= link_to posts_path(tags: "user:#{@user.name}"), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.post_upload_count) %></div>
<div class="user-tooltip-stat-name">Uploads</div>
<% end %>
</li>
<li class="user-tooltip-stat-item">
<%= link_to post_versions_path(search: { updater_id: @user.id }), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.post_update_count) %></div>
<div class="user-tooltip-stat-name">Tag Edits</div>
<% end %>
</li>
<li class="user-tooltip-stat-item">
<%= link_to note_versions_path(search: { updater_id: @user.id }), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.note_update_count) %></div>
<div class="user-tooltip-stat-name">Note Edits</div>
<% end %>
</li>
<li class="user-tooltip-stat-item">
<%= link_to posts_path(tags: "ordfav:#{@user.name}"), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.favorite_count) %></div>
<div class="user-tooltip-stat-name">Favorites</div>
<% end %>
</li>
<li class="user-tooltip-stat-item">
<%= link_to comments_path(search: { creator_id: @user.id }), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.comment_count) %></div>
<div class="user-tooltip-stat-name">Comments</div>
<% end %>
</li>
<li class="user-tooltip-stat-item">
<%= link_to forum_posts_path(search: { creator_id: @user.id }), class: "link-plain" do %>
<div class="user-tooltip-stat-value"><%= humanized_number(@user.forum_post_count) %></div>
<div class="user-tooltip-stat-name">Forum Posts</div>
<% end %>
</li>
</ul>
</div>

View File

@@ -87,6 +87,34 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal(false, xml["user"]["enable_safe_mode"])
end
context "for a tooltip" do
setup do
@banned = create(:banned_user)
@admin = create(:admin_user)
@member = create(:user)
@feedback = create(:user_feedback, user: @member, category: :positive)
end
should "render for a banned user" do
get_auth user_path(@banned, variant: "tooltip"), @user
assert_response :success
end
should "render for a member" do
get_auth user_path(@member, variant: "tooltip"), @user
assert_response :success
get_auth user_path(@member, variant: "tooltip"), @admin
assert_response :success
end
should "render for an admin" do
get_auth user_path(@admin, variant: "tooltip"), @user
assert_response :success
end
end
end
context "profile action" do