Add Milestone Notifications (#1920)
* Test milestone notifications * Add initial milestone notifications * Add extra milestone goalposts * Move send reaction milestone * Use proper twilio-video package version * Add random giphys gifs to milestones * Make images responsive and add addl gifs * Add guard statement for older articles * Update app/views/notifications/_milestone.html.erb Link to Giphy page instead of source page * Generate gifs via class and factor in aspect ratio * Add border radius to gifs Co-Authored-By: Zhao-Andy <andyzhao.zhao@gmail.com>
This commit is contained in:
parent
e56cc57d8f
commit
8b8dbd0172
9 changed files with 188 additions and 4 deletions
|
|
@ -67,11 +67,21 @@
|
|||
margin: 10px auto;
|
||||
width: 90%;
|
||||
color:#666666;
|
||||
&.milestone-emojis{
|
||||
font-size: xx-large;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
img.badge-image{
|
||||
margin:10px 0px 20px;
|
||||
height: 150px;
|
||||
}
|
||||
img.milestone-gif{
|
||||
margin: 10px 0px 10px;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
}
|
||||
p.badge-reward-message{
|
||||
margin: 8px auto;
|
||||
width: 90%;
|
||||
|
|
|
|||
|
|
@ -10,4 +10,12 @@ class NotificationDecorator < Draper::Decorator
|
|||
end
|
||||
struct.new(json_data[type]["class"]["name"], json_data[type]["id"])
|
||||
end
|
||||
|
||||
def milestone_type
|
||||
action.split("::")[1]
|
||||
end
|
||||
|
||||
def milestone_count
|
||||
action.split("::")[2]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ class ArticleAnalyticsFetcher
|
|||
page_views_obj = pageviews.to_h
|
||||
chunk.each do |article|
|
||||
article.update_columns(previous_positive_reactions_count: article.positive_reactions_count)
|
||||
Notification.send_milestone_notification(type: "Reaction", article: article)
|
||||
next if article.page_views_count > page_views_obj[article.id].to_i
|
||||
|
||||
article.update_columns(page_views_count: page_views_obj[article.id].to_i)
|
||||
Notification.send_milestone_notification(type: "View", article: article)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
27
app/labor/random_gif.rb
Normal file
27
app/labor/random_gif.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
class RandomGif
|
||||
def initialize(_action = "congratulations")
|
||||
@random_gifs = {
|
||||
"xjZtu4qi1biIo" => { aspect_ratio: 1.000 },
|
||||
"12P29BwtrvsbbW" => { aspect_ratio: 0.760 },
|
||||
"9PyhoXey73EpW" => { aspect_ratio: 0.753 },
|
||||
"OcZp0maz6ALok" => { aspect_ratio: 1.000 },
|
||||
"Is1O1TWV0LEJi" => { aspect_ratio: 0.565 },
|
||||
"xjZtu4qi1biIo" => { aspect_ratio: 1.0000 },
|
||||
"lz24Z42jLcTa8" => { aspect_ratio: 0.776 },
|
||||
"g9582DNuQppxC" => { aspect_ratio: 0.562 },
|
||||
"l4HodBpDmoMA5p9bG" => { aspect_ratio: 1.000 },
|
||||
"3oxOCfV7z28QtXXAtO" => { aspect_ratio: 0.750 },
|
||||
"y8Mz1yj13s3kI" => { aspect_ratio: 0.750 },
|
||||
"111ebonMs90YLu" => { aspect_ratio: 0.750 },
|
||||
"Sk5uipPXyBjfW" => { aspect_ratio: 0.422 }
|
||||
}
|
||||
end
|
||||
|
||||
def random_id
|
||||
@random_gifs.keys.sample
|
||||
end
|
||||
|
||||
def get_aspect_ratio(id)
|
||||
(@random_gifs[id] || "xjZtu4qi1biIo")[:aspect_ratio]
|
||||
end
|
||||
end
|
||||
|
|
@ -254,6 +254,29 @@ class Notification < ApplicationRecord
|
|||
end
|
||||
handle_asynchronously :send_tag_adjustment_notification
|
||||
|
||||
def send_milestone_notification(milestone_hash)
|
||||
milestone_hash[:next_milestone] = next_milestone(milestone_hash)
|
||||
return unless should_send_milestone?(milestone_hash)
|
||||
|
||||
Notification.create!(
|
||||
user_id: milestone_hash[:article].user_id,
|
||||
notifiable_id: milestone_hash[:article].id,
|
||||
notifiable_type: "Article",
|
||||
json_data: { article: article_data(milestone_hash[:article]), gif_id: RandomGif.new.random_id },
|
||||
action: "Milestone::#{milestone_hash[:type]}::#{milestone_hash[:next_milestone]}",
|
||||
)
|
||||
if milestone_hash[:article].organization_id
|
||||
Notification.create!(
|
||||
organization_id: milestone_hash[:article].organization_id,
|
||||
notifiable_id: milestone_hash[:article].id,
|
||||
notifiable_type: "Article",
|
||||
json_data: { article: article_data(milestone_hash[:article]) },
|
||||
action: "Milestone::#{milestone_hash[:type]}::#{milestone_hash[:next_milestone]}",
|
||||
)
|
||||
end
|
||||
end
|
||||
handle_asynchronously :send_milestone_notification
|
||||
|
||||
def remove_all(notifiable_hash)
|
||||
Notification.where(
|
||||
notifiable_id: notifiable_hash[:notifiable_id],
|
||||
|
|
@ -348,6 +371,41 @@ class Notification < ApplicationRecord
|
|||
}
|
||||
end
|
||||
|
||||
def should_send_milestone?(milestone_hash)
|
||||
return if milestone_hash[:article].published_at < DateTime.new(2019, 2, 25)
|
||||
|
||||
last_milestone_notification = Notification.find_by(
|
||||
user_id: milestone_hash[:article].user_id,
|
||||
notifiable_type: "Article",
|
||||
notifiable_id: milestone_hash[:article].id,
|
||||
action: "Milestone::#{milestone_hash[:type]}::#{milestone_hash[:next_milestone]}",
|
||||
)
|
||||
|
||||
if milestone_hash[:type] == "View"
|
||||
last_milestone_notification.blank? && milestone_hash[:article].page_views_count > milestone_hash[:next_milestone]
|
||||
elsif milestone_hash[:type] == "Reaction"
|
||||
last_milestone_notification.blank? && milestone_hash[:article].positive_reactions_count > milestone_hash[:next_milestone]
|
||||
end
|
||||
end
|
||||
|
||||
def next_milestone(milestone_hash)
|
||||
case milestone_hash[:type]
|
||||
when "View"
|
||||
milestones = [1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576]
|
||||
milestone_count = milestone_hash[:article].page_views_count
|
||||
when "Reaction"
|
||||
milestones = [64, 128, 256, 512, 1024, 2048, 4096, 8192]
|
||||
milestone_count = milestone_hash[:article].positive_reactions_count
|
||||
end
|
||||
|
||||
closest_number = milestones.min_by { |num| (milestone_count - num).abs }
|
||||
if milestone_count > closest_number
|
||||
closest_number
|
||||
else
|
||||
milestones[milestones.index(closest_number) - 1]
|
||||
end
|
||||
end
|
||||
|
||||
def send_push_notifications(user_id, title, body, path)
|
||||
return unless ApplicationConfig["PUSHER_BEAMS_KEY"] && ApplicationConfig["PUSHER_BEAMS_KEY"].size == 64
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
<% if notification.action == "Reaction" %>
|
||||
<%= render "reaction", notification: notification %>
|
||||
<% elsif notification.action.include? "Milestone" %>
|
||||
<%= render "milestone", notification: notification %>
|
||||
<% else %>
|
||||
<% json_data = notification.json_data %>
|
||||
<% if json_data["organization"] %>
|
||||
<div class="article-organization-headline">
|
||||
<a href="<%= json_data["organization"]["path"] %>" class="article-organization-headline-inner">
|
||||
<img src="<%= json_data["organization"]["profile_image_90"] %>" alt="link to <%= json_data["organization"]["name"] %>"> <%= json_data["organization"]["name"] %>
|
||||
<img src="<%= json_data["organization"]["profile_image_90"] %>" alt="link to <%= json_data["organization"]["name"] %>"> <%= json_data["organization"]["name"] %>
|
||||
</a>
|
||||
<a class="org-headline-filler" href="<%= json_data["organization"]["path"] %>"> </a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% cache "activity-profile-pic-#{json_data["user"]["id"]}-#{json_data["user"]["profile_image_90"]}" do %>
|
||||
<% cache "activity-profile-pic-#{json_data['user']['id']}-#{json_data['user']['profile_image_90']}" do %>
|
||||
<a href="<%= json_data["user"]["path"] %>" class="small-pic-link-wrapper">
|
||||
<div class="small-pic">
|
||||
<img src="<%= json_data["user"]["profile_image_90"] %>" alt="link to <%= json_data["user"]["username"] %>'s profile">
|
||||
|
|
|
|||
17
app/views/notifications/_milestone.html.erb
Normal file
17
app/views/notifications/_milestone.html.erb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<% json_data = notification.json_data %>
|
||||
<div class="content notification-content badge-content">
|
||||
<div class="badge-title">
|
||||
Your post <a href="<%= json_data["article"]["path"] %>"><%= json_data["article"]["title"] %></a> just passed <%= notification.milestone_count %> <%= notification.milestone_type.pluralize.downcase %>!
|
||||
</div>
|
||||
<p class="badge-description milestone-emojis">🎉🎉🎉🎉🎉🎉🎉🎉</p>
|
||||
|
||||
<a href="https:/giphy.com/gifs/<%= json_data["gif_id"] %>" rel="noopener noreferrer" target="_blank">
|
||||
<img style="height: <%= RandomGif.new.get_aspect_ratio(json_data["gif_id"]) * 300 %>px;" class="milestone-gif" src="https://media.giphy.com/media/<%= json_data["gif_id"] %>/giphy-downsized.gif" alt="A random celebratory gif!">
|
||||
</a>
|
||||
<br>
|
||||
<a href="/dashboard">
|
||||
<button class="cta follow-action-button badge-button">
|
||||
CHECK YOUR DASHBOARD
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -5,7 +5,7 @@ RSpec.describe Notification, type: :model do
|
|||
let(:user2) { create(:user) }
|
||||
let(:user3) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
let(:article) { create(:article, user_id: user.id, page_views_count: 4000, positive_reactions_count: 70) }
|
||||
let(:follow_instance) { user.follow(user2) }
|
||||
|
||||
describe "#send_new_follower_notification" do
|
||||
|
|
@ -240,6 +240,66 @@ RSpec.describe Notification, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#send_milestone_notification" do
|
||||
# milestones = [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144]
|
||||
let(:view_milestone_hash) { { type: "View", article: article } }
|
||||
let(:reaction_milestone_hash) { { type: "Reaction", article: article } }
|
||||
|
||||
context "when a user has never received a milestone notification" do
|
||||
it "sends the appropriate level view milestone notification" do
|
||||
Notification.send_milestone_notification_without_delay(view_milestone_hash)
|
||||
expect(user.notifications.first.action).to include "2048"
|
||||
end
|
||||
|
||||
it "sends the appropriate level reaction milestone notification" do
|
||||
Notification.send_milestone_notification_without_delay(reaction_milestone_hash)
|
||||
expect(user.notifications.first.action).to include "64"
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user has received a milestone notification before" do
|
||||
def mock_previous_view_milestone_notification
|
||||
Notification.send_milestone_notification_without_delay(view_milestone_hash)
|
||||
article.update_column(:page_views_count, 9001)
|
||||
Notification.send_milestone_notification_without_delay(view_milestone_hash)
|
||||
end
|
||||
|
||||
def mock_previous_reaction_milestone_notification
|
||||
Notification.send_milestone_notification_without_delay(reaction_milestone_hash)
|
||||
article.update_column(:positive_reactions_count, 150)
|
||||
Notification.send_milestone_notification_without_delay(reaction_milestone_hash)
|
||||
end
|
||||
|
||||
it "sends the appropriate level view milestone notification" do
|
||||
mock_previous_view_milestone_notification
|
||||
expect(user.notifications.second.action).to include "8192"
|
||||
end
|
||||
|
||||
it "sends the appropriate level reaction milestone notification" do
|
||||
mock_previous_reaction_milestone_notification
|
||||
expect(user.notifications.last.action).to include "128"
|
||||
end
|
||||
|
||||
it "adds an additional view milestone notification" do
|
||||
mock_previous_view_milestone_notification
|
||||
expect(user.notifications.count).to eq 2
|
||||
end
|
||||
|
||||
it "does not the same view milestone notification if called again" do
|
||||
mock_previous_view_milestone_notification
|
||||
Notification.send_milestone_notification_without_delay(view_milestone_hash)
|
||||
expect(user.notifications.count).to eq 2
|
||||
end
|
||||
|
||||
it "does not send a view milestone notification again if the latest number of views is not past the next milestone" do
|
||||
mock_previous_view_milestone_notification
|
||||
article.update_column(:page_views_count, rand(9002..16383))
|
||||
Notification.send_milestone_notification_without_delay(view_milestone_hash)
|
||||
expect(user.notifications.count).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update_notifications" do
|
||||
context "when there are no notifications to begin with" do
|
||||
it "returns nil" do
|
||||
|
|
|
|||
|
|
@ -10472,7 +10472,7 @@ tweetnacl@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.0.tgz#713d8b818da42068740bf68386d0479e66fc8a7b"
|
||||
integrity sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=
|
||||
|
||||
twilio-video@^1.15.1:
|
||||
twilio-video@^1.15.2:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/twilio-video/-/twilio-video-1.15.2.tgz#da3379c0e256def317689d9a980c683277f61baf"
|
||||
integrity sha512-C3KQcHmqqJObFWU2I+bMVqwIEUISXV+DSFJHdPrrw9Z+tDE7Y5SNNj+r4e9ArABx7mtWZlEUa4+Ela/dOQLEjA==
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue