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:
Andy Zhao 2019-03-01 15:25:45 -05:00 committed by Ben Halpern
parent e56cc57d8f
commit 8b8dbd0172
9 changed files with 188 additions and 4 deletions

View file

@ -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%;

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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"] %>">&nbsp;</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">

View 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>

View file

@ -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

View file

@ -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==