Fixing design issues with multiple reactions (#19020)
* Try disabling text-select (mobile long-tap) * Try to force button color on mobile safari * Try making notification decorator more helpful * More using notification decorator * Helper so I can see exceptions * As we toggle feature flag, temporary show 'heart' for unsupported reactions * Reaction on Comment is a 'reaction'-type notification * Temporarily add new emojis to i18n * Render full-color, unique SVGs for multiple_reactions * Restore card layout * Aggregate reaction icons also need unique SVGs * This seems unnecessary? * Multiple is -more-than-one- * Display *unique* reaction categories on /notifications * Comment notification no longer needs to re-render as reaction * Decorator needs to handle some data gaps * Fix NotificationHelper * Try a better name for this * Tests for the notification decorator * These partials wound up without an outer layer * Add a descriptive comment * index needs force_unique * Try multiple reactions external img (#19076) * Try external img SVGs * Unique SVG might not be necessary if this works * Can also remove this spec if successful * Restore flash icon behavior * Plus external img for #index as well
This commit is contained in:
parent
b26ff24eae
commit
ea2154d506
23 changed files with 518 additions and 226 deletions
|
|
@ -49,7 +49,7 @@ function showUserReaction(reactionName, animatedClass) {
|
|||
|
||||
if (animatedClass == 'user-animated') {
|
||||
const activeIcon = reactionButton.querySelector(
|
||||
'.crayons-reaction__icon--active svg',
|
||||
'.crayons-reaction__icon--active img',
|
||||
);
|
||||
|
||||
if (activeIcon) {
|
||||
|
|
@ -63,7 +63,7 @@ function showUserReaction(reactionName, animatedClass) {
|
|||
setTimeout(function () {
|
||||
document
|
||||
.getElementById('reaction-drawer-trigger')
|
||||
.querySelector('.crayons-reaction__icon--active svg').outerHTML =
|
||||
.querySelector('.crayons-reaction__icon--active img').outerHTML =
|
||||
reactionDrawerButton.originalIcon;
|
||||
}, 1500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,6 +389,14 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
// Disables text selection to help "long tap" work on mobile
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
@media (min-width: $breakpoint-m) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -436,6 +444,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.crayons-reaction_total_count {
|
||||
color: var(--btn-color); // Mobile safari button text blue bug
|
||||
}
|
||||
|
||||
.reaction-drawer__outer:hover .reaction-drawer {
|
||||
@media (min-width: $breakpoint-m) {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class NotificationDecorator < ApplicationDecorator
|
||||
extend ActiveModel::Naming
|
||||
|
||||
NOTIFIABLE_STUB = Struct.new(:name, :id) do
|
||||
def class
|
||||
Struct.new(:name).new(name)
|
||||
|
|
@ -30,4 +32,114 @@ class NotificationDecorator < ApplicationDecorator
|
|||
|
||||
action.split("::").third
|
||||
end
|
||||
|
||||
def siblings
|
||||
@siblings ||= begin
|
||||
aggregated_data = json_data.dig("reaction", "aggregated_siblings")
|
||||
aggregated_data ||= []
|
||||
aggregated_data.select { |n| n["created_at"] > 24.hours.ago }
|
||||
end
|
||||
end
|
||||
|
||||
def to_model
|
||||
self
|
||||
end
|
||||
|
||||
# In many cases, we render a partial specific to a notification's notifiable_type
|
||||
# (Milestone, Article, Comment, etc.) However, reacting-to-an-article or
|
||||
# reacting-to-a-comment will have a misleading "Article" or "Comment" notifiable_type
|
||||
# (respectively), so a "reaction-type notification" is somewhat more involved. We also have
|
||||
# distinct partials for aggregate reactions ("Username and several others reacted to...")
|
||||
# and individual reactions. This has become further complicated by the multiple_reactions
|
||||
# feature flag.
|
||||
def to_partial_path
|
||||
return "notifications/#{notifiable_type.downcase}" unless reaction?
|
||||
|
||||
prefix = FeatureFlag.enabled?(:multiple_reactions) ? "" : "original_"
|
||||
|
||||
if siblings.any?
|
||||
"notifications/#{prefix}aggregated_reactions"
|
||||
else
|
||||
"notifications/#{prefix}single_reaction"
|
||||
end
|
||||
end
|
||||
|
||||
def actors
|
||||
@actors ||= siblings.pluck("user").uniq
|
||||
end
|
||||
|
||||
def milestone?
|
||||
type_inquirer.milestone? || (type_inquirer.article? && action.include?("Milestone"))
|
||||
end
|
||||
|
||||
def multiple_reactors?
|
||||
actors.size > 1
|
||||
end
|
||||
|
||||
def reaction?
|
||||
type_inquirer.reaction? ||
|
||||
(type_inquirer.article? && action_inquirer.reaction?) ||
|
||||
(type_inquirer.comment? && action_inquirer.reaction?)
|
||||
end
|
||||
|
||||
# TODO: This is an odd one - contrast with reactable_type?
|
||||
def reactable_class
|
||||
@reactable_class ||= json_data.dig "reaction", "reactable", "class", "name"
|
||||
end
|
||||
|
||||
def reactable_path
|
||||
@reactable_path ||= json_data.dig "reaction", "reactable", "path"
|
||||
end
|
||||
|
||||
def reactable_title
|
||||
@reactable_title ||= json_data.dig "reaction", "reactable", "title"
|
||||
end
|
||||
|
||||
def reactable_type
|
||||
@reactable_type ||= json_data.dig "reaction", "reactable", "type"
|
||||
end
|
||||
|
||||
def reaction_category
|
||||
@reaction_category ||= json_data.dig "reaction", "category"
|
||||
end
|
||||
|
||||
# NOTE: this is *siblings* not json_data, breaking a pattern above
|
||||
def reaction_categories
|
||||
@reaction_categories ||= siblings.pluck("category")
|
||||
end
|
||||
|
||||
# NOTE: Using *siblings*, not json_data, via reaction_categories
|
||||
def unique_reaction_categories
|
||||
@unique_reaction_categories ||= reaction_categories.uniq
|
||||
end
|
||||
|
||||
def user_id
|
||||
@user_id ||= json_data.dig "user", "id"
|
||||
end
|
||||
|
||||
def user_name
|
||||
@user_name ||= json_data.dig "user", "name"
|
||||
end
|
||||
|
||||
def user_path
|
||||
@user_path ||= json_data.dig "user", "path"
|
||||
end
|
||||
|
||||
def user_profile_image_90
|
||||
@user_profile_image_90 ||= json_data.dig "user", "profile_image_90"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def action_inquirer
|
||||
@action_inquirer ||= ActiveSupport::StringInquirer.new(action&.downcase || "")
|
||||
end
|
||||
|
||||
def type_inquirer
|
||||
@type_inquirer ||= ActiveSupport::StringInquirer.new(notifiable_type&.downcase || "")
|
||||
end
|
||||
|
||||
def json_data
|
||||
read_attribute(:json_data) || {}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,8 +9,35 @@ module NotificationsHelper
|
|||
"vomit" => "twemoji/suspicious.svg"
|
||||
}.freeze
|
||||
|
||||
def reaction_image(category)
|
||||
REACTION_IMAGES[category]
|
||||
def reaction_image(slug)
|
||||
if FeatureFlag.enabled?(:multiple_reactions)
|
||||
if (category = ReactionCategory[slug] || ReactionCategory["like"])
|
||||
"#{category.icon}.svg"
|
||||
end
|
||||
else
|
||||
# This is mostly original behavior, pre-multiple_reactions, modified to return
|
||||
# a "like" image if the actual reaction is one of the new ones
|
||||
REACTION_IMAGES[slug] || REACTION_IMAGES["like"]
|
||||
end
|
||||
end
|
||||
|
||||
def reaction_category_name(slug)
|
||||
ReactionCategory[slug]&.name.presence || "unknown"
|
||||
end
|
||||
|
||||
def render_each_notification_or_error(notifications, error:, &block)
|
||||
notifications.each do |notification|
|
||||
concat render_notification_or_error(notification, error: error, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def render_notification_or_error(notification, error:)
|
||||
capture { yield(notification) }
|
||||
rescue StandardError => e
|
||||
raise if Rails.env.development?
|
||||
|
||||
Honeybadger.notify(e, context: { notification_id: notification.id })
|
||||
capture { render error }
|
||||
end
|
||||
|
||||
def message_user_acted_maybe_org(data, action, if_org: "")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="multiple_reactions_engagement">
|
||||
<% ReactionCategory.for_view.each do |reaction_type| %>
|
||||
<span class="reaction_engagement_<%= reaction_type.slug %> hidden">
|
||||
<%= crayons_icon_tag(reaction_type.icon, native: true, aria_hidden: true, force_unique: true) %>
|
||||
<%= image_tag reaction_type.icon, size: "24x24" %>
|
||||
<span id="reaction_engagement_<%= reaction_type.slug %>_count"> </span>
|
||||
</span>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@
|
|||
class="crayons-reaction crayons-tooltip__activator relative"
|
||||
data-category="<%= category %>">
|
||||
<span class="crayons-reaction__icon crayons-reaction__icon--inactive">
|
||||
<%= crayons_icon_tag(image_path, native: true, aria_hidden: true, force_unique: true) %>
|
||||
<%#= image_tag image_path, aria_hidden: true, height: 24, width: 24 %>
|
||||
<%= image_tag image_path, aria_hidden: true, height: 24, width: 24 %>
|
||||
</span>
|
||||
<%# TODO: This can be straightened out after feature-flag -- double icons are not required now %>
|
||||
<% if user_signed_in? # We cannot trigger the action state unless the user is signed in, so no need to render. %>
|
||||
<span class="crayons-reaction__icon crayons-reaction__icon--active">
|
||||
<%= crayons_icon_tag(image_active_path, native: true, aria_hidden: true, force_unique: true) %>
|
||||
<%#= image_tag image_active_path, aria_hidden: true, height: 24, width: 24 %>
|
||||
<%= image_tag image_active_path, aria_hidden: true, height: 24, width: 24 %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="crayons-reaction__count" id="reaction-number-<%= category %>"><span class="bg-base-40 opacity-25 p-2 inline-block radius-default"></span></span>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
<% ReactionCategory.for_view.each do |reaction_type| %>
|
||||
<% next unless story.reaction_categories.include?(reaction_type.slug.to_s) %>
|
||||
<span class="crayons_icon_container">
|
||||
<%= crayons_icon_tag(reaction_type.icon, width: "18px", height: "18px", native: true, aria_hidden: true) %>
|
||||
<%= image_tag reaction_type.icon, size: "18x18" %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="aggregate_reactions_counter"><%= t("views.reactions.summary.count_html",
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
<%# TODO: change to map of IDs %>
|
||||
<% actors = siblings.map { |n| n["user"] }.uniq %>
|
||||
<% reactable_data = notification.json_data["reaction"]["reactable"] %>
|
||||
|
||||
<div class="relative shrink-0 self-start">
|
||||
<% if actors.size == 1 %>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l m:crayons-avatar--xl" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% else %>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l mr-4" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l absolute -right-1 -bottom-3 border-solid border-2 border-base-inverted" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.last["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.last["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="crayons-card notification">
|
||||
<div class="relative shrink-0 self-start">
|
||||
<% if notification.multiple_reactors? %>
|
||||
<a href="<%= notification.actors.first["path"] %>" class="crayons-avatar crayons-avatar--l mr-4" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= notification.actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= notification.actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<a href="<%= notification.actors.first["path"] %>" class="crayons-avatar crayons-avatar--l absolute -right-1 -bottom-3 border-solid border-2 border-base-inverted" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= notification.actors.last["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= notification.actors.last["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% else %>
|
||||
<a href="<%= notification.actors.first["path"] %>" class="crayons-avatar crayons-avatar--l m:crayons-avatar--xl" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= notification.actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= notification.actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="notification__content pt-2">
|
||||
<% reaction_categories = siblings.map { |n| n["category"] } %>
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: actors.size,
|
||||
start: tag("span", { class: %w[inline-block py-1] }, true), # rubocop:disable Rails/ContentTag
|
||||
actors: if actors.size == 1
|
||||
link_to actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"
|
||||
elsif actors.size == 2
|
||||
t("views.notifications.reacted.and_html",
|
||||
first: link_to(actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"),
|
||||
last: link_to(actors.last["name"], actors.last["path"], class: "crayons-link fw-bold"))
|
||||
elsif actors.size > 1
|
||||
t("views.notifications.reacted.and_other_html",
|
||||
first: link_to(actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"),
|
||||
count: actors.size - 1)
|
||||
end,
|
||||
# your article/comment or the actual title of the article/comment
|
||||
title: link_to(reactable_data["title"].blank? ? t("views.notifications.reacted.your.#{reactable_data['class']['name'].downcase}") : h(reactable_data["title"]), reactable_data["path"], class: "crayons-link fw-bold"),
|
||||
end: "</span>".html_safe,
|
||||
reactions: safe_join(
|
||||
reaction_categories.filter_map do |cat|
|
||||
image_path = reaction_image(cat)
|
||||
if image_path.present?
|
||||
"<span class='crayons-hover-tooltip inline-block' data-tooltip='#{t("views.reactions.category.#{cat}")}'>
|
||||
#{crayons_icon_tag(image_path, class: "reaction-image mx-1 my-1 reaction-icon--#{cat}")}
|
||||
</span>".html_safe
|
||||
end
|
||||
end, "\n"
|
||||
)) %>
|
||||
<div class="notification__content pt-2">
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: notification.actors.size,
|
||||
start: tag("span", { class: %w[inline-block py-1] }, true),
|
||||
actors: if notification.actors.size == 1
|
||||
link_to notification.actors.first["name"], notification.actors.first["path"], class: "crayons-link fw-bold"
|
||||
elsif notification.actors.size == 2
|
||||
t("views.notifications.reacted.and_html",
|
||||
first: link_to(notification.actors.first["name"], notification.actors.first["path"], class: "crayons-link fw-bold"),
|
||||
last: link_to(notification.actors.last["name"], notification.actors.last["path"], class: "crayons-link fw-bold"))
|
||||
elsif notification.actors.size > 1
|
||||
t("views.notifications.reacted.and_other_html",
|
||||
first: link_to(notification.actors.first["name"], notification.actors.first["path"], class: "crayons-link fw-bold"),
|
||||
count: notification.actors.size - 1)
|
||||
end,
|
||||
# your article/comment or the actual title of the article/comment
|
||||
title: link_to(notification.reactable_title.blank? ? t("views.notifications.reacted.your.#{notification.reactable_class.downcase}") : h(notification.reactable_title), notification.reactable_path, class: "crayons-link fw-bold"),
|
||||
end: "</span>".html_safe,
|
||||
reactions: safe_join(
|
||||
notification.unique_reaction_categories.filter_map do |category|
|
||||
image_path = reaction_image(category)
|
||||
if image_path.present?
|
||||
"<span class='crayons-hover-tooltip inline-block' data-tooltip='#{reaction_category_name(category)}'>
|
||||
#{image_tag image_path, class: "reaction-image mx-1 my-1 reaction-icon--#{category}"}
|
||||
</span>".html_safe
|
||||
end
|
||||
end, "\n"
|
||||
)) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
<div class="crayons-card notification">
|
||||
<% if notification.action == "Reaction" %>
|
||||
<%= render "reaction", notification: notification %>
|
||||
<% elsif notification.action.include? "Milestone" %>
|
||||
<%= render "milestone", notification: notification %>
|
||||
<% else %>
|
||||
<% json_data = notification.json_data %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: json_data %>
|
||||
<% json_data = notification.json_data %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: json_data %>
|
||||
|
||||
<div class="notification__content">
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= message_user_acted_maybe_org(json_data, "views.notifications.new_post.verb_html", if_org: "views.notifications.new_post.if_org_html") %>
|
||||
</h2>
|
||||
<p class="lh-tight"><small class="fs-s color-base-60"><%= time_ago_in_words json_data["article"]["published_at"], scope: :"datetime.distance_in_words_ago" %></small></p>
|
||||
</header>
|
||||
<div class="notification__content">
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= message_user_acted_maybe_org(json_data, "views.notifications.new_post.verb_html", if_org: "views.notifications.new_post.if_org_html") %>
|
||||
</h2>
|
||||
<p class="lh-tight"><small class="fs-s color-base-60"><%= time_ago_in_words json_data["article"]["published_at"], scope: :"datetime.distance_in_words_ago" %></small></p>
|
||||
</header>
|
||||
|
||||
<%= render "notifications/shared/article_preview", json_data: json_data, notification: notification, context: "default" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render "notifications/shared/article_preview", json_data: json_data, notification: notification, context: "default" %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,51 @@
|
|||
<div class="crayons-card notification">
|
||||
<% if notification.action == "Reaction" %>
|
||||
<%= render "reaction", notification: notification %>
|
||||
<% else %>
|
||||
<% json_data = notification.json_data %>
|
||||
<% cache "activity-profile-pic-#{json_data['user']['id']}-#{json_data['user']['profile_image_90']}" do %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: json_data %>
|
||||
<% end %>
|
||||
|
||||
<div class="notification__content">
|
||||
<% params = { user: link_to(json_data["user"]["name"], json_data["user"]["path"], class: "crayons-link fw-bold"), title: link_to(h(json_data["comment"]["commentable"]["title"]), json_data["comment"]["commentable"]["path"], class: "crayons-link fw-bold") } %>
|
||||
<% if notification.action.blank? %>
|
||||
<% if json_data["comment"]["created_at"] %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<% if json_data["comment"]["depth"] && json_data["comment"]["depth"] > 0 %>
|
||||
<%= t("views.notifications.comment.replied_html", **params) %>
|
||||
<% else %>
|
||||
<%= t("views.notifications.comment.commented_html", **params) %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<p class="lh-tight"><a href="<%= json_data["comment"]["path"] %>" class="crayons-link fs-s crayons-link--secondary"><%= time_ago_in_words json_data["comment"]["created_at"], scope: :"datetime.distance_in_words_ago" %></a></p>
|
||||
</header>
|
||||
<% end %>
|
||||
|
||||
<%= render "notifications/shared/comment_box", json_data: json_data, notification: notification, context: "default" %>
|
||||
|
||||
<% elsif notification.action == "Moderation" %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= t("views.notifications.comment.left_html", user: link_to(json_data["comment"]["path"].split("/")[1], "/#{json_data['comment']['path'].split('/')[1]}", class: "crayons-link fw-bold"), title: params[:title]) %>
|
||||
</h2>
|
||||
<p><%= t("views.notifications.comment.welcome_html") %></p>
|
||||
</header>
|
||||
|
||||
<%= render "notifications/shared/comment_box", json_data: json_data, notification: notification, context: "moderation" %>
|
||||
|
||||
<p class="fs-s color-base-60 pt-4"><%= t("views.notifications.comment.report") %></p>
|
||||
|
||||
<% elsif notification.action == "First" %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= t("views.notifications.comment.first_html", **params) %>
|
||||
</h2>
|
||||
<p class="lh-tight"><small class="fs-s color-base-60"><%= time_ago_in_words notification.created_at, scope: :"datetime.distance_in_words_ago" %></small></p>
|
||||
|
||||
<p><%= t("views.notifications.comment.first_reply") %></p>
|
||||
</header>
|
||||
|
||||
<%= render "notifications/shared/comment_box", activity: activity, context: "moderation" %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% json_data = notification.json_data %>
|
||||
<% cache "activity-profile-pic-#{json_data['user']['id']}-#{json_data['user']['profile_image_90']}" do %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: json_data %>
|
||||
<% end %>
|
||||
|
||||
<div class="notification__content">
|
||||
<% params = { user: link_to(json_data["user"]["name"], json_data["user"]["path"], class: "crayons-link fw-bold"), title: link_to(h(json_data["comment"]["commentable"]["title"]), json_data["comment"]["commentable"]["path"], class: "crayons-link fw-bold") } %>
|
||||
<% if notification.action.blank? %>
|
||||
<% if json_data["comment"]["created_at"] %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<% if json_data["comment"]["depth"] && json_data["comment"]["depth"] > 0 %>
|
||||
<%= t("views.notifications.comment.replied_html", **params) %>
|
||||
<% else %>
|
||||
<%= t("views.notifications.comment.commented_html", **params) %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<p class="lh-tight"><a href="<%= json_data["comment"]["path"] %>" class="crayons-link fs-s crayons-link--secondary"><%= time_ago_in_words json_data["comment"]["created_at"], scope: :"datetime.distance_in_words_ago" %></a></p>
|
||||
</header>
|
||||
<% end %>
|
||||
|
||||
<%= render "notifications/shared/comment_box", json_data: json_data, notification: notification, context: "default" %>
|
||||
|
||||
<% elsif notification.action == "Moderation" %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= t("views.notifications.comment.left_html", user: link_to(json_data["comment"]["path"].split("/")[1], "/#{json_data['comment']['path'].split('/')[1]}", class: "crayons-link fw-bold"), title: params[:title]) %>
|
||||
</h2>
|
||||
<p><%= t("views.notifications.comment.welcome_html") %></p>
|
||||
</header>
|
||||
|
||||
<%= render "notifications/shared/comment_box", json_data: json_data, notification: notification, context: "moderation" %>
|
||||
|
||||
<p class="fs-s color-base-60 pt-4"><%= t("views.notifications.comment.report") %></p>
|
||||
|
||||
<% elsif notification.action == "First" %>
|
||||
<header class="mb-4">
|
||||
<h2 class="fs-base fw-normal">
|
||||
<%= t("views.notifications.comment.first_html", **params) %>
|
||||
</h2>
|
||||
<p class="lh-tight"><small class="fs-s color-base-60"><%= time_ago_in_words notification.created_at, scope: :"datetime.distance_in_words_ago" %></small></p>
|
||||
|
||||
<p><%= t("views.notifications.comment.first_reply") %></p>
|
||||
</header>
|
||||
|
||||
<%= render "notifications/shared/comment_box", activity: activity, context: "moderation" %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
<% @notifications.each do |notification| %>
|
||||
<% render_each_notification_or_error @notifications, error: "notifications/shared/error" do |notification| %>
|
||||
<div
|
||||
class="mb-2 spec-notification <%= "unseen" unless notification.read? %>"
|
||||
data-notification-id="<%= notification.id %>">
|
||||
<%= render notification.notifiable_type.downcase.to_s, notification: notification %>
|
||||
</div>
|
||||
|
||||
<% rescue => e %>
|
||||
|
||||
<% Honeybadger.notify(e, context: { notification_id: notification.id }) %>
|
||||
|
||||
<div class="align-center p-9 py-10 color-base-80 crayons-card mb-2">
|
||||
<h2 class="fw-bold fs-l"><%= t("views.notifications.error.subtitle") %></h2>
|
||||
<p class="color-base-60 pt-2"><%= t("views.notifications.error.desc") %></p>
|
||||
<%= render notification, notification: notification %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<%# TODO: change to map of IDs %>
|
||||
<% actors = notification.siblings.pluck("user").uniq %>
|
||||
<% reactable_data = notification.json_data["reaction"]["reactable"] %>
|
||||
|
||||
<div class="crayons-card notification">
|
||||
|
||||
<div class="relative shrink-0 self-start">
|
||||
<% if actors.size == 1 %>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l m:crayons-avatar--xl" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% else %>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l mr-4" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.first["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.first["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<a href="<%= actors.first["path"] %>" class="crayons-avatar crayons-avatar--l absolute -right-1 -bottom-3 border-solid border-2 border-base-inverted" aria-hidden="true" tabindex="-1">
|
||||
<img src="<%= actors.last["profile_image_90"] %>" class="crayons-avatar__image" alt="link to <%= actors.last["username"] %>'s profile" width="48" height="48">
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="notification__content pt-2">
|
||||
<% reaction_categories = notification.siblings.pluck("category") %>
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: actors.size,
|
||||
start: tag("span", { class: %w[inline-block py-1] }, true), # rubocop:disable Rails/ContentTag
|
||||
actors: if actors.size == 1
|
||||
link_to actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"
|
||||
elsif actors.size == 2
|
||||
t("views.notifications.reacted.and_html",
|
||||
first: link_to(actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"),
|
||||
last: link_to(actors.last["name"], actors.last["path"], class: "crayons-link fw-bold"))
|
||||
elsif actors.size > 1
|
||||
t("views.notifications.reacted.and_other_html",
|
||||
first: link_to(actors.first["name"], actors.first["path"], class: "crayons-link fw-bold"),
|
||||
count: actors.size - 1)
|
||||
end,
|
||||
# your article/comment or the actual title of the article/comment
|
||||
title: link_to(reactable_data["title"].blank? ? t("views.notifications.reacted.your.#{reactable_data['class']['name'].downcase}") : h(reactable_data["title"]), reactable_data["path"], class: "crayons-link fw-bold"),
|
||||
end: "</span>".html_safe,
|
||||
reactions: safe_join(
|
||||
reaction_categories.filter_map do |cat|
|
||||
image_path = reaction_image(cat)
|
||||
if image_path.present?
|
||||
"<span class='crayons-hover-tooltip inline-block' data-tooltip='#{t("views.reactions.category.#{cat}")}'>
|
||||
#{crayons_icon_tag(image_path, class: "reaction-image mx-1 my-1 reaction-icon--#{cat}")}
|
||||
</span>".html_safe
|
||||
end
|
||||
end, "\n"
|
||||
)) %>
|
||||
</div>
|
||||
</div>
|
||||
31
app/views/notifications/_original_single_reaction.html.erb
Normal file
31
app/views/notifications/_original_single_reaction.html.erb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<div class="crayons-card notification">
|
||||
<div class="flex items-center">
|
||||
<% cache "activity-profile-pic-#{notification.json_data['user']['id']}-#{notification.json_data['user']['profile_image_90']}" do %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: notification.json_data %>
|
||||
<% end %>
|
||||
<div class="notification__content">
|
||||
<% category = notification.json_data["reaction"]["category"] %>
|
||||
<% if notification.json_data["reaction"]["reactable"]["title"].blank? %>
|
||||
<% title_link = link_to(
|
||||
t("views.notifications.reacted.your.#{notification.json_data['reaction']['reactable_type'].downcase}"),
|
||||
notification.json_data["reaction"]["reactable"]["path"],
|
||||
class: "crayons-link fw-bold",
|
||||
) %>
|
||||
<% else %>
|
||||
<% title_link = link_to(
|
||||
sanitize(notification.json_data["reaction"]["reactable"]["title"]),
|
||||
notification.json_data["reaction"]["reactable"]["path"],
|
||||
class: "crayons-link fw-bold",
|
||||
) %>
|
||||
<% end %>
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: 1,
|
||||
start: "",
|
||||
actors: link_to(notification.json_data["user"]["name"], notification.json_data["user"]["path"], class: "crayons-link fw-bold"),
|
||||
# title is blank when it's a comment with only an image, for example
|
||||
title: title_link,
|
||||
end: "",
|
||||
reactions: tag.span(crayons_icon_tag(reaction_image(category), class: "reaction-image mx-1 reaction-icon--#{category}", title: t("views.reactions.category.#{category}")))) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,7 +1,2 @@
|
|||
<%# missing cache key %>
|
||||
<% siblings = notification.json_data["reaction"]["aggregated_siblings"].select { |n| n["created_at"] > 24.hours.ago } %>
|
||||
<% if siblings.length == 0 %>
|
||||
<%= render "single_reaction", notification: notification %>
|
||||
<% else %>
|
||||
<%= render "aggregated_reactions", notification: notification, siblings: siblings %>
|
||||
<% end %>
|
||||
<%= notification.inspect %>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,27 @@
|
|||
<div class="flex items-center">
|
||||
<% cache "activity-profile-pic-#{notification.json_data['user']['id']}-#{notification.json_data['user']['profile_image_90']}" do %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: notification.json_data %>
|
||||
<% end %>
|
||||
<div class="notification__content">
|
||||
<% category = notification.json_data["reaction"]["category"] %>
|
||||
<% if notification.json_data["reaction"]["reactable"]["title"].blank? %>
|
||||
<% title_link = link_to(
|
||||
t("views.notifications.reacted.your.#{notification.json_data['reaction']['reactable_type'].downcase}"),
|
||||
notification.json_data["reaction"]["reactable"]["path"],
|
||||
class: "crayons-link fw-bold",
|
||||
) %>
|
||||
<% else %>
|
||||
<% title_link = link_to(
|
||||
sanitize(notification.json_data["reaction"]["reactable"]["title"]),
|
||||
notification.json_data["reaction"]["reactable"]["path"],
|
||||
class: "crayons-link fw-bold",
|
||||
) %>
|
||||
<div class="crayons-card notification">
|
||||
<div class="flex items-center">
|
||||
<% cache "activity-profile-pic-#{notification.user_id}-#{notification.user_profile_image_90}" do %>
|
||||
<%= render "notifications/shared/profile_pic", json_data: notification.json_data %>
|
||||
<% end %>
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: 1,
|
||||
start: "",
|
||||
actors: link_to(notification.json_data["user"]["name"], notification.json_data["user"]["path"], class: "crayons-link fw-bold"),
|
||||
# title is blank when it's a comment with only an image, for example
|
||||
title: title_link,
|
||||
end: "",
|
||||
reactions: tag.span(crayons_icon_tag(reaction_image(category), class: "reaction-image mx-1 reaction-icon--#{category}", title: t("views.reactions.category.#{category}")))) %>
|
||||
<div class="notification__content">
|
||||
<%# title is blank when it's a comment with only an image, for example %>
|
||||
<% title = if notification.reactable_title.present?
|
||||
sanitize(notification.reactable_title)
|
||||
else
|
||||
t("views.notifications.reacted.your.#{notification.reactable_type.downcase}")
|
||||
end %>
|
||||
|
||||
<% title_link = link_to(title, notification.reactable_path, class: "crayons-link fw-bold") %>
|
||||
|
||||
<%= t("views.notifications.reacted.verb_html",
|
||||
count: 1,
|
||||
start: "",
|
||||
actors: link_to(notification.user_name, notification.user_path, class: "crayons-link fw-bold"),
|
||||
title: title_link,
|
||||
end: "",
|
||||
reactions: tag.span(image_tag(reaction_image(notification.reaction_category),
|
||||
class: "reaction-image mx-1 reaction-icon--#{notification.reaction_category}",
|
||||
title: t("views.reactions.category.#{notification.reaction_category}")))) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
4
app/views/notifications/shared/_error.html.erb
Normal file
4
app/views/notifications/shared/_error.html.erb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div class="align-center p-9 py-10 color-base-80 crayons-card mb-2">
|
||||
<h2 class="fw-bold fs-l"><%= t("views.notifications.error.subtitle") %></h2>
|
||||
<p class="color-base-60 pt-2"><%= t("views.notifications.error.desc") %></p>
|
||||
</div>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
require "unique_svg_transform"
|
||||
|
||||
InlineSvg.configure do |config|
|
||||
# NOTE: There is currently a bug in the inline_svg gem where custom attributes
|
||||
# are de-facto required in order for a custom transformation to be activated.
|
||||
config.add_custom_transformation attribute: :force_unique, transform: UniqueSvgTransform
|
||||
end
|
||||
|
|
@ -14,6 +14,8 @@ en:
|
|||
lightbulb: Lightbulb
|
||||
raised_hands: Raised hands
|
||||
rocket: Rocket
|
||||
exploding-head: Exploding head
|
||||
fire: Fire
|
||||
drawer_button: Add reaction
|
||||
summary:
|
||||
count_html:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ fr:
|
|||
lightbulb: Lightbulb
|
||||
raised_hands: Raised hands
|
||||
rocket: Rocket
|
||||
exploding-head: Exploding head
|
||||
fire: Fire
|
||||
drawer_button: Add reaction
|
||||
summary:
|
||||
count_html:
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
require "securerandom"
|
||||
|
||||
# rubocop:disable Style/StringLiterals
|
||||
|
||||
class UniqueSvgTransform < InlineSvg::CustomTransformation
|
||||
def transform(doc, uniq_str = SecureRandom.hex[0..6])
|
||||
# nodes with id attributes
|
||||
doc.xpath('//*[@id]').each do |node|
|
||||
node_id = node.get_attribute 'id'
|
||||
node.set_attribute 'id', [node_id, uniq_str].join('-')
|
||||
end
|
||||
|
||||
# nodes with id references
|
||||
doc.xpath('//*[@href]').each do |node|
|
||||
node_href = node.get_attribute 'href'
|
||||
node.set_attribute 'href', [node_href, uniq_str].join('-')
|
||||
end
|
||||
|
||||
# nodes with fill references
|
||||
doc.xpath('//*[@fill]').each do |node|
|
||||
node_url = node.get_attribute 'fill'
|
||||
if (md = /url\((.+)\)/.match node_url)
|
||||
id = md[1]
|
||||
end
|
||||
next unless id
|
||||
|
||||
new_url = "url(#{id}-#{uniq_str})"
|
||||
node.set_attribute 'fill', new_url
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Style/StringLiterals
|
||||
|
|
@ -74,4 +74,119 @@ RSpec.describe NotificationDecorator, type: :decorator do
|
|||
expect(notification.decorate.milestone_count).to eq("64")
|
||||
end
|
||||
end
|
||||
|
||||
describe "reaction to a article" do
|
||||
subject(:decorated) { notification.decorate }
|
||||
|
||||
let!(:notification) do
|
||||
article = build(:article)
|
||||
reaction = build(:reaction, reactable: article)
|
||||
|
||||
build(:notification,
|
||||
notifiable: reaction,
|
||||
action: "Reaction",
|
||||
json_data: {
|
||||
reaction: {
|
||||
category: "like",
|
||||
reactable_type: "Article",
|
||||
reactable_id: 123,
|
||||
reactable: {
|
||||
path: "path/to/article",
|
||||
title: "This is the article's title here",
|
||||
class: {
|
||||
name: "Article"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
it "responds to reactable_class" do
|
||||
expect(decorated.reactable_class).to eq("Article")
|
||||
end
|
||||
|
||||
it "responds to reactable_path" do
|
||||
expect(decorated.reactable_path).to eq("path/to/article")
|
||||
end
|
||||
|
||||
it "responds to reactable_title (even if blank)" do
|
||||
expect(decorated.reactable_title).to eq("This is the article's title here")
|
||||
end
|
||||
|
||||
it "responds to reaction_category" do
|
||||
expect(decorated.reaction_category).to eq("like")
|
||||
end
|
||||
|
||||
it "is a reaction?" do
|
||||
expect(decorated).to be_reaction
|
||||
end
|
||||
|
||||
it "responds to user fields (even if blank)" do
|
||||
expect(decorated.user_id).to be_nil
|
||||
expect(decorated.user_name).to be_nil
|
||||
expect(decorated.user_path).to be_nil
|
||||
expect(decorated.user_profile_image_90).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "reaction to a comment" do
|
||||
subject(:decorated) { notification.decorate }
|
||||
|
||||
let!(:notification) do
|
||||
article = build(:article)
|
||||
comment = build(:comment, commentable: article)
|
||||
reaction = build(:reaction, reactable: comment)
|
||||
|
||||
build(:notification,
|
||||
notifiable: reaction,
|
||||
action: "Reaction",
|
||||
json_data: {
|
||||
reaction: {
|
||||
category: "like",
|
||||
reactable_type: "Comment",
|
||||
reactable_id: 123,
|
||||
reactable: {
|
||||
path: "path/to/comment",
|
||||
title: nil,
|
||||
class: {
|
||||
name: "Comment"
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
id: 456,
|
||||
name: "Commentator",
|
||||
path: "path/to/user",
|
||||
profile_image_90: "path/to/profile/image"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
it "responds to reactable_class" do
|
||||
expect(decorated.reactable_class).to eq("Comment")
|
||||
end
|
||||
|
||||
it "responds to reactable_path" do
|
||||
expect(decorated.reactable_path).to eq("path/to/comment")
|
||||
end
|
||||
|
||||
it "responds to reactable_title (even if blank)" do
|
||||
expect(decorated.reactable_title).to be_nil
|
||||
end
|
||||
|
||||
it "responds to reaction_category" do
|
||||
expect(decorated.reaction_category).to eq("like")
|
||||
end
|
||||
|
||||
it "is a reaction?" do
|
||||
expect(decorated).to be_reaction
|
||||
end
|
||||
|
||||
it "responds to user fields (even if blank)" do
|
||||
expect(decorated.user_id).to eq(456)
|
||||
expect(decorated.user_name).to eq("Commentator")
|
||||
expect(decorated.user_path).to eq("path/to/user")
|
||||
expect(decorated.user_profile_image_90).to eq("path/to/profile/image")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe NotificationsHelper, type: :helper do
|
||||
it "returns a category image" do
|
||||
expect(helper.reaction_image("unicorn")).to eq("unicorn-filled.svg")
|
||||
RSpec.describe NotificationsHelper do
|
||||
context "when feature flag enabled" do
|
||||
before { allow(FeatureFlag).to receive(:enabled?).with(:multiple_reactions).and_return(true) }
|
||||
|
||||
it "returns a new category image from ReactionCategory" do
|
||||
expect(helper.reaction_image("unicorn")).to eq("multi-unicorn.svg")
|
||||
end
|
||||
|
||||
it "returns a heart for unrecognized category" do
|
||||
expect(helper.reaction_image("asdf")).to eq("sparkle-heart.svg")
|
||||
end
|
||||
end
|
||||
|
||||
context "when feature flag disabled" do
|
||||
before { allow(FeatureFlag).to receive(:enabled?).with(:multiple_reactions).and_return(false) }
|
||||
|
||||
it "returns an original category image from REACTION_IMAGES" do
|
||||
expect(helper.reaction_image("unicorn")).to eq("unicorn-filled.svg")
|
||||
end
|
||||
|
||||
it "returns a heart for unrecognized category" do
|
||||
expect(helper.reaction_image("asdf")).to eq("heart-filled.svg")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
require "unique_svg_transform"
|
||||
|
||||
RSpec.describe UniqueSvgTransform do
|
||||
let(:input) { "#{fixture_path}/files/unicorn.svg" }
|
||||
let(:expectation) { File.read("#{fixture_path}/files/unicorn-unique.svg") }
|
||||
let(:pseudo_unique) { "87a63a" }
|
||||
|
||||
it "transforms input doc into uniquely identified SVG" do
|
||||
doc = Nokogiri::XML(File.open(input))
|
||||
output = described_class.new(true).transform(doc, pseudo_unique).to_xml
|
||||
expect(output).to eq expectation
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue