177 lines
5.2 KiB
Ruby
177 lines
5.2 KiB
Ruby
class Reaction < ApplicationRecord
|
|
include AlgoliaSearch
|
|
|
|
CATEGORIES = %w[like readinglist unicorn thinking hands thumbsdown vomit].freeze
|
|
REACTABLE_TYPES = %w[Comment Article User].freeze
|
|
STATUSES = %w[valid invalid confirmed archived].freeze
|
|
|
|
belongs_to :reactable, polymorphic: true
|
|
belongs_to :user
|
|
|
|
counter_culture :reactable,
|
|
column_name: proc { |model|
|
|
model.points.positive? ? "positive_reactions_count" : "reactions_count"
|
|
}
|
|
counter_culture :user
|
|
|
|
validates :category, inclusion: { in: CATEGORIES }
|
|
validates :reactable_type, inclusion: { in: REACTABLE_TYPES }
|
|
validates :status, inclusion: { in: STATUSES }
|
|
validates :user_id, uniqueness: { scope: %i[reactable_id reactable_type category] }
|
|
validate :permissions
|
|
|
|
before_save :assign_points
|
|
after_commit :async_bust, :bust_reactable_cache
|
|
after_save :index_to_algolia
|
|
after_save :update_reactable, :touch_user
|
|
before_destroy :update_reactable_without_delay, unless: :destroyed_by_association
|
|
before_destroy :bust_reactable_cache_without_delay
|
|
before_destroy :remove_algolia
|
|
|
|
algoliasearch index_name: "SecuredReactions_#{Rails.env}", auto_index: false, auto_remove: false do
|
|
attribute :id, :reactable_user, :searchable_reactable_title, :searchable_reactable_path, :status, :reading_time,
|
|
:searchable_reactable_text, :searchable_reactable_tags, :viewable_by, :reactable_tags, :reactable_published_date
|
|
searchableAttributes %i[searchable_reactable_title searchable_reactable_text
|
|
searchable_reactable_tags reactable_user]
|
|
tags do
|
|
reactable_tags
|
|
end
|
|
attributesForFaceting ["filterOnly(viewable_by)", "filterOnly(status)"]
|
|
customRanking ["desc(id)"]
|
|
end
|
|
|
|
def index_to_algolia
|
|
index! if category == "readinglist" && reactable && reactable.published
|
|
end
|
|
|
|
class << self
|
|
def count_for_article(id)
|
|
Rails.cache.fetch("count_for_reactable-Article-#{id}", expires_in: 1.hour) do
|
|
reactions = Reaction.where(reactable_id: id, reactable_type: "Article")
|
|
%w[like readinglist unicorn].map do |type|
|
|
{ category: type, count: reactions.where(category: type).size }
|
|
end
|
|
end
|
|
end
|
|
|
|
def cached_any_reactions_for?(reactable, user, category)
|
|
class_name = reactable.class.name == "ArticleDecorator" ? "Article" : reactable.class.name
|
|
cache_name = "any_reactions_for-#{class_name}-#{reactable.id}-#{user.updated_at&.rfc3339}-#{category}"
|
|
Rails.cache.fetch(cache_name, expires_in: 24.hours) do
|
|
Reaction.where(reactable_id: reactable.id, reactable_type: class_name, user: user, category: category).any?
|
|
end
|
|
end
|
|
end
|
|
|
|
# no need to send notification if:
|
|
# - reaction is negative
|
|
# - receiver is the same user as the one who reacted
|
|
# - receive_notification is disabled
|
|
def skip_notification_for?(receiver)
|
|
points.negative? ||
|
|
(user_id == reactable.user_id) ||
|
|
(receiver.is_a?(User) && reactable.receive_notifications == false)
|
|
end
|
|
|
|
private
|
|
|
|
def touch_user
|
|
user.touch
|
|
end
|
|
|
|
def update_reactable
|
|
Reactions::UpdateReactableJob.perform_later(id)
|
|
end
|
|
|
|
def bust_reactable_cache
|
|
Reactions::BustReactableCacheWorker.perform_async(id)
|
|
end
|
|
|
|
def async_bust
|
|
Reactions::BustHomepageCacheWorker.perform_async(id)
|
|
end
|
|
|
|
def bust_reactable_cache_without_delay
|
|
Reactions::BustReactableCacheWorker.new.perform(id)
|
|
end
|
|
|
|
def update_reactable_without_delay
|
|
Reactions::UpdateReactableJob.perform_now(id)
|
|
end
|
|
|
|
def reading_time
|
|
reactable.reading_time if category == "readinglist"
|
|
end
|
|
|
|
def remove_from_index
|
|
remove_from_index!
|
|
end
|
|
|
|
def reactable_user
|
|
return unless category == "readinglist"
|
|
|
|
{
|
|
username: reactable.user_username,
|
|
name: reactable.user_name,
|
|
profile_image_90: reactable.user.profile_image_90
|
|
}
|
|
end
|
|
|
|
def reactable_published_date
|
|
reactable.readable_publish_date if category == "readinglist"
|
|
end
|
|
|
|
def searchable_reactable_title
|
|
reactable.title if category == "readinglist"
|
|
end
|
|
|
|
def searchable_reactable_text
|
|
reactable.body_text[0..350] if category == "readinglist"
|
|
end
|
|
|
|
def searchable_reactable_tags
|
|
reactable.cached_tag_list if category == "readinglist"
|
|
end
|
|
|
|
def searchable_reactable_path
|
|
reactable.path if category == "readinglist"
|
|
end
|
|
|
|
def reactable_tags
|
|
reactable.decorate.cached_tag_list_array if category == "readinglist"
|
|
end
|
|
|
|
def viewable_by
|
|
user_id
|
|
end
|
|
|
|
BASE_POINTS = {
|
|
"vomit" => -50.0,
|
|
"thumbsdown" => -10.0
|
|
}.freeze
|
|
|
|
def assign_points
|
|
base_points = BASE_POINTS.fetch(category, 1.0)
|
|
base_points = 0 if status == "invalid"
|
|
base_points *= 2 if status == "confirmed"
|
|
self.points = user ? (base_points * user.reputation_modifier) : -5
|
|
end
|
|
|
|
def permissions
|
|
errors.add(:category, "is not valid.") if negative_reaction_from_untrusted_user?
|
|
|
|
errors.add(:reactable_id, "is not valid.") if reactable_type == "Article" && !reactable&.published
|
|
end
|
|
|
|
def negative_reaction_from_untrusted_user?
|
|
negative? && !user.trusted
|
|
end
|
|
|
|
def remove_algolia
|
|
remove_from_index!
|
|
end
|
|
|
|
def negative?
|
|
category == "vomit" || category == "thumbsdown"
|
|
end
|
|
end
|