diff --git a/app/assets/javascripts/utilities/buildArticleHTML.js b/app/assets/javascripts/utilities/buildArticleHTML.js index 380196090..7c272945c 100644 --- a/app/assets/javascripts/utilities/buildArticleHTML.js +++ b/app/assets/javascripts/utilities/buildArticleHTML.js @@ -112,14 +112,30 @@ function buildArticleHTML(article, currentUserId = null) { var reactionsText = reactionsCount === 1 ? 'reaction' : 'reactions'; if (article.class_name !== 'User' && reactionsCount > 0) { - reactionsDisplay = - '' + - reactionsCount + - ``; + var icons = []; + for (var category of article.public_reaction_categories) { + var icon = ``; + icons = icons.concat( + `${icon}`, + ); + } + icons.reverse(); + + reactionsDisplay = ` +
+ + ${icons.join()} + + + + + +
+
`; } var picUrl; diff --git a/app/javascript/articles/__tests__/utilities/articleUtilities.js b/app/javascript/articles/__tests__/utilities/articleUtilities.js index b0993a18a..ed603186b 100644 --- a/app/javascript/articles/__tests__/utilities/articleUtilities.js +++ b/app/javascript/articles/__tests__/utilities/articleUtilities.js @@ -128,6 +128,32 @@ export const articleWithReactions = { readable_publish_date: 'February 18', top_comments: [], public_reactions_count: 232, + public_reaction_categories: [ + { + slug: 'like', + name: 'Like', + icon: 'sparkle-heart', + position: 1, + }, + { + slug: 'unicorn', + name: 'Unicorn', + icon: 'multi-unicorn', + position: 2, + }, + { + slug: 'raised_hands', + name: 'Raised Hands', + icon: 'raised-hands', + position: 4, + }, + { + slug: 'fire', + name: 'Fire', + icon: 'fire', + position: 5, + }, + ], }; export const articleWithoutReactions = { diff --git a/app/javascript/articles/components/ReactionsCount.jsx b/app/javascript/articles/components/ReactionsCount.jsx index 6da4c139e..b7fbbb231 100644 --- a/app/javascript/articles/components/ReactionsCount.jsx +++ b/app/javascript/articles/components/ReactionsCount.jsx @@ -1,42 +1,65 @@ import { h } from 'preact'; import { articlePropTypes } from '../../common-prop-types'; -import { Button } from '../../crayons/Button'; import { locale } from '../../utilities/locale'; export const ReactionsCount = ({ article }) => { const totalReactions = article.public_reactions_count || 0; - const reactionsSVG = () => ( - - - - ); if (totalReactions === 0) { return; } - return ( - + ); + } + + return ( + +
+ {buildIcons()} + {buildCounter()} +
+
); }; diff --git a/app/javascript/articles/components/__tests__/ReactionsCount.test.jsx b/app/javascript/articles/components/__tests__/ReactionsCount.test.jsx index 156fa36e5..b872329fa 100644 --- a/app/javascript/articles/components/__tests__/ReactionsCount.test.jsx +++ b/app/javascript/articles/components/__tests__/ReactionsCount.test.jsx @@ -23,4 +23,16 @@ describe(' component', () => { expect(findByText('232 reactions')).toBeDefined(); }); + + it('should display multiple reactions when there are reactions', async () => { + const { findByText } = render( + , + ); + + expect( + findByText( + 'Like', + ), + ).toBeDefined(); + }); }); diff --git a/app/models/concerns/reactable.rb b/app/models/concerns/reactable.rb index 6611d6857..2758aba28 100644 --- a/app/models/concerns/reactable.rb +++ b/app/models/concerns/reactable.rb @@ -3,13 +3,24 @@ module Reactable included do has_many :reactions, as: :reactable, inverse_of: :reactable, dependent: :destroy + has_many :distinct_reaction_categories, -> { order(:category).merge(Reaction.distinct_categories) }, + as: :reactable, + inverse_of: :reactable, + dependent: nil, + class_name: "Reaction" end def sync_reactions_count update_column(:public_reactions_count, reactions.public_category.size) end - def reaction_categories - reactions.distinct(:category).pluck(:category) + def public_reaction_categories + @public_reaction_categories ||= begin + # .map is intentional below - .pluck would break eager-loaded association! + reacted = distinct_reaction_categories.map(&:category) + ReactionCategory.for_view.select do |reaction_type| + reacted.include?(reaction_type.slug.to_s) + end + end end end diff --git a/app/models/reaction.rb b/app/models/reaction.rb index a5c72e966..533e9bd3d 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -39,6 +39,7 @@ class Reaction < ApplicationRecord scope :unarchived, -> { where.not(status: "archived") } scope :from_user, ->(user) { where(user: user) } scope :readinglist_for_user, ->(user) { readinglist.unarchived.only_articles.from_user(user) } + scope :distinct_categories, -> { select("distinct(reactions.category) as category, reactable_id, reactable_type") } validates :category, inclusion: { in: ReactionCategory.all_slugs.map(&:to_s) } validates :reactable_type, inclusion: { in: REACTABLE_TYPES } diff --git a/app/models/reaction_category.rb b/app/models/reaction_category.rb index 608d40f03..c54916e49 100644 --- a/app/models/reaction_category.rb +++ b/app/models/reaction_category.rb @@ -68,4 +68,13 @@ class ReactionCategory def visible_to_public? !privileged? && published? end + + def as_json(_options = nil) + { + slug: slug, + name: name, + icon: icon, + position: position + } + end end diff --git a/app/queries/homepage/articles_query.rb b/app/queries/homepage/articles_query.rb index ae2fc2537..0e7ce6525 100644 --- a/app/queries/homepage/articles_query.rb +++ b/app/queries/homepage/articles_query.rb @@ -67,6 +67,7 @@ module Homepage @relation = @relation.where(user_id: user_id) if user_id.present? @relation = @relation.where(organization_id: organization_id) if organization_id.present? @relation = @relation.cached_tagged_with_any(tags) if tags.any? + @relation = @relation.includes(:distinct_reaction_categories) relation end diff --git a/app/serializers/homepage/article_serializer.rb b/app/serializers/homepage/article_serializer.rb index 5c9d27777..072d8565a 100644 --- a/app/serializers/homepage/article_serializer.rb +++ b/app/serializers/homepage/article_serializer.rb @@ -28,6 +28,7 @@ module Homepage :reading_time, :title, :user_id, + :public_reaction_categories, ) attribute :video_duration_string, &:video_duration_in_minutes diff --git a/app/services/articles/feeds/basic.rb b/app/services/articles/feeds/basic.rb index 8b457b37c..8bf8ab153 100644 --- a/app/services/articles/feeds/basic.rb +++ b/app/services/articles/feeds/basic.rb @@ -15,6 +15,7 @@ module Articles .with_at_least_home_feed_minimum_score .limit(@number_of_articles) .limited_column_select.includes(top_comments: :user) + .includes(:distinct_reaction_categories) return articles unless @user articles = articles.where.not(user_id: UserBlock.cached_blocked_ids_for_blocker(@user.id)) diff --git a/app/services/articles/feeds/large_forem_experimental.rb b/app/services/articles/feeds/large_forem_experimental.rb index 566a038b5..e0fb97de3 100644 --- a/app/services/articles/feeds/large_forem_experimental.rb +++ b/app/services/articles/feeds/large_forem_experimental.rb @@ -103,6 +103,7 @@ module Articles def experimental_hot_story_grab start_time = Articles::Feeds.oldest_published_at_to_consider_for(user: @user) Article.published.limited_column_select.includes(top_comments: :user) + .includes(:distinct_reaction_categories) .where("published_at > ?", start_time) .page(@page).per(@number_of_articles) .order(score: :desc) diff --git a/app/services/articles/feeds/tag.rb b/app/services/articles/feeds/tag.rb index 8bdd6015b..49eb7abd0 100644 --- a/app/services/articles/feeds/tag.rb +++ b/app/services/articles/feeds/tag.rb @@ -17,6 +17,7 @@ module Articles .published .limited_column_select .includes(top_comments: :user) + .includes(:distinct_reaction_categories) .page(page) .per(number_of_articles) end diff --git a/app/services/articles/feeds/variant_query.rb b/app/services/articles/feeds/variant_query.rb index 47c172fd6..6e2372e8a 100644 --- a/app/services/articles/feeds/variant_query.rb +++ b/app/services/articles/feeds/variant_query.rb @@ -134,6 +134,7 @@ module Articles Article.joins(join_fragment) .limited_column_select .includes(top_comments: :user) + .includes(:distinct_reaction_categories) .order(config.order_by.to_sql) end diff --git a/app/views/articles/_single_story.html.erb b/app/views/articles/_single_story.html.erb index 8c66f2a7d..743b89cbb 100644 --- a/app/views/articles/_single_story.html.erb +++ b/app/views/articles/_single_story.html.erb @@ -120,13 +120,21 @@
<% if story.public_reactions_count > 0 %> - "> - <%= crayons_icon_tag("small-heart", title: t("views.reactions.summary.title")) %> - <%= t("views.reactions.summary.count_html", - count: story.public_reactions_count, - start: tag("span", { class: %w[hidden s:inline] }, true), - end: "".html_safe) %> - + "> +
+ + <% story.public_reaction_categories.reverse_each do |reaction_type| %> + + <%= image_tag "#{reaction_type.icon}.svg", size: "18x18" %> + + <% end %> + + <%= t("views.reactions.summary.count_html", + count: story.public_reactions_count, + start: tag("span", { class: %w[hidden s:inline] }, true), + end: "".html_safe) %> +
+
<% end %> <% if story.comments_count > 0 %> "> diff --git a/app/views/stories/feeds/show.json.jbuilder b/app/views/stories/feeds/show.json.jbuilder index 9ba1b6570..f7ade201c 100644 --- a/app/views/stories/feeds/show.json.jbuilder +++ b/app/views/stories/feeds/show.json.jbuilder @@ -7,6 +7,7 @@ article_methods_to_include = %i[ readable_publish_date flare_tag class_name cloudinary_video_url video_duration_in_minutes published_at_int published_timestamp main_image_background_hex_color + public_reaction_categories ] json.array!(@stories) do |article| diff --git a/app/assets/images/fire.svg b/public/assets/fire.svg similarity index 100% rename from app/assets/images/fire.svg rename to public/assets/fire.svg diff --git a/app/assets/images/lightbulb.svg b/public/assets/lightbulb.svg similarity index 100% rename from app/assets/images/lightbulb.svg rename to public/assets/lightbulb.svg diff --git a/app/assets/images/multi-unicorn.svg b/public/assets/multi-unicorn.svg similarity index 100% rename from app/assets/images/multi-unicorn.svg rename to public/assets/multi-unicorn.svg diff --git a/app/assets/images/raised-hands.svg b/public/assets/raised-hands.svg similarity index 100% rename from app/assets/images/raised-hands.svg rename to public/assets/raised-hands.svg diff --git a/app/assets/images/sparkle-heart.svg b/public/assets/sparkle-heart.svg similarity index 100% rename from app/assets/images/sparkle-heart.svg rename to public/assets/sparkle-heart.svg diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 46a694455..ed732bd1b 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -1342,7 +1342,7 @@ RSpec.describe Article do end end - describe "#reaction_categories reports unique associated reaction categories" do + describe "#public_reaction_categories reports unique associated reaction categories" do before do user2 = create(:user) user2.add_role(:trusted) @@ -1354,7 +1354,8 @@ RSpec.describe Article do end it "reports accurately" do - expect(article.reaction_categories).to contain_exactly("like", "readinglist", "vomit") + categories = article.public_reaction_categories + expect(categories.map(&:slug)).to contain_exactly(*%i[like]) end end end diff --git a/spec/services/homepage/fetch_articles_spec.rb b/spec/services/homepage/fetch_articles_spec.rb index aadff3e68..437d54b2a 100644 --- a/spec/services/homepage/fetch_articles_spec.rb +++ b/spec/services/homepage/fetch_articles_spec.rb @@ -12,11 +12,12 @@ RSpec.describe Homepage::FetchArticles, type: :service do result = described_class.call.first keys = %i[ - class_name cloudinary_video_url comments_count flare_tag id path public_reactions_count + class_name cloudinary_video_url comments_count flare_tag id path + public_reactions_count public_reaction_categories published_at_int readable_publish_date reading_time tag_list title user user_id video_duration_string ] - expect(result.keys.sort).to eq(keys) + expect(result.keys.sort).to contain_exactly(*keys) expect(result[:class_name]).to eq("Article") expect(result[:cloudinary_video_url]).to eq(article.cloudinary_video_url)