Merge pull request #2511 from basecamp/native-push-notifications
Support native push notifications
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
require "zlib"
|
||||
|
||||
module AvatarsHelper
|
||||
AVATAR_COLORS = %w[
|
||||
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
|
||||
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
|
||||
]
|
||||
|
||||
def avatar_background_color(user)
|
||||
AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
|
||||
user.avatar_background_color
|
||||
end
|
||||
|
||||
def avatar_tag(user, hidden_for_screen_reader: false, **options)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Notification::PushJob < ApplicationJob
|
||||
def perform(notification)
|
||||
notification.push
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,6 @@ class PushNotificationJob < ApplicationJob
|
||||
discard_on ActiveJob::DeserializationError
|
||||
|
||||
def perform(notification)
|
||||
NotificationPusher.new(notification).push
|
||||
notification.push
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
module PushNotifiable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_save_commit :push_notification_later, if: :source_id_previously_changed?
|
||||
end
|
||||
|
||||
private
|
||||
def push_notification_later
|
||||
PushNotificationJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Notification < ApplicationRecord
|
||||
include PushNotifiable
|
||||
include Notification::Pushable
|
||||
|
||||
belongs_to :account, default: -> { user.account }
|
||||
belongs_to :user
|
||||
@@ -27,6 +27,7 @@ class Notification < ApplicationRecord
|
||||
after_destroy_commit -> { broadcast_remove_to user, :notifications }
|
||||
|
||||
delegate :notifiable_target, to: :source
|
||||
delegate :identity, to: :user
|
||||
|
||||
class << self
|
||||
def read_all
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
class Notification::DefaultPayload
|
||||
attr_reader :notification
|
||||
|
||||
delegate :card, to: :notification
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ title: title, body: body, url: url }
|
||||
end
|
||||
|
||||
def title
|
||||
"New notification"
|
||||
end
|
||||
|
||||
def body
|
||||
"You have a new notification"
|
||||
end
|
||||
|
||||
def url
|
||||
notifications_url
|
||||
end
|
||||
|
||||
def category
|
||||
"default"
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
false
|
||||
end
|
||||
|
||||
def base_url
|
||||
Rails.application.routes.url_helpers.root_url(**url_options.except(:script_name)).chomp("/")
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
Rails.application.routes.url_helpers.user_avatar_url(notification.creator, **url_options)
|
||||
end
|
||||
|
||||
private
|
||||
def card_url(card)
|
||||
Rails.application.routes.url_helpers.card_url(card, **url_options)
|
||||
end
|
||||
|
||||
def notifications_url
|
||||
Rails.application.routes.url_helpers.notifications_url(**url_options)
|
||||
end
|
||||
|
||||
def url_options
|
||||
base_options = Rails.application.routes.default_url_options.presence ||
|
||||
Rails.application.config.action_mailer.default_url_options ||
|
||||
{}
|
||||
base_options.merge(script_name: notification.account.slug)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
class Notification::EventPayload < Notification::DefaultPayload
|
||||
include ExcerptHelper
|
||||
|
||||
def title
|
||||
case event.action
|
||||
when "comment_created"
|
||||
"RE: #{card_title}"
|
||||
else
|
||||
card_title
|
||||
end
|
||||
end
|
||||
|
||||
def body
|
||||
case event.action
|
||||
when "comment_created"
|
||||
format_excerpt(event.eventable.body, length: 200)
|
||||
when "card_assigned"
|
||||
"Assigned to you by #{event.creator.name}"
|
||||
when "card_published"
|
||||
"Added by #{event.creator.name}"
|
||||
when "card_closed"
|
||||
card.closure ? "Moved to Done by #{event.creator.name}" : "Closed by #{event.creator.name}"
|
||||
when "card_reopened"
|
||||
"Reopened by #{event.creator.name}"
|
||||
else
|
||||
event.creator.name
|
||||
end
|
||||
end
|
||||
|
||||
def url
|
||||
case event.action
|
||||
when "comment_created"
|
||||
card_url_with_comment_anchor(event.eventable)
|
||||
else
|
||||
card_url(card)
|
||||
end
|
||||
end
|
||||
|
||||
def category
|
||||
case event.action
|
||||
when "card_assigned" then "assignment"
|
||||
when "comment_created" then "comment"
|
||||
else "card"
|
||||
end
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
event.action.card_assigned?
|
||||
end
|
||||
|
||||
private
|
||||
def event
|
||||
notification.source
|
||||
end
|
||||
|
||||
def card_title
|
||||
card.title.presence || "Card #{card.number}"
|
||||
end
|
||||
|
||||
def card_url_with_comment_anchor(comment)
|
||||
Rails.application.routes.url_helpers.card_url(
|
||||
comment.card,
|
||||
anchor: ActionView::RecordIdentifier.dom_id(comment),
|
||||
**url_options
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
class Notification::MentionPayload < Notification::DefaultPayload
|
||||
include ExcerptHelper
|
||||
|
||||
def title
|
||||
"#{mention.mentioner.first_name} mentioned you"
|
||||
end
|
||||
|
||||
def body
|
||||
format_excerpt(mention.source.mentionable_content, length: 200)
|
||||
end
|
||||
|
||||
def url
|
||||
card_url(card)
|
||||
end
|
||||
|
||||
def category
|
||||
"mention"
|
||||
end
|
||||
|
||||
def high_priority?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
def mention
|
||||
notification.source
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class Notification::PushTarget
|
||||
attr_reader :notification
|
||||
|
||||
delegate :card, to: :notification
|
||||
|
||||
def self.process(notification)
|
||||
new(notification).process
|
||||
end
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def process
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class Notification::PushTarget::Web < Notification::PushTarget
|
||||
def process
|
||||
if subscriptions.any?
|
||||
Rails.configuration.x.web_push_pool.queue(notification.payload.to_h, subscriptions)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def subscriptions
|
||||
@subscriptions ||= notification.user.push_subscriptions
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
module Notification::Pushable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :push_targets, default: []
|
||||
|
||||
after_save_commit :push_later, if: :source_id_previously_changed?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def register_push_target(target)
|
||||
target = resolve_push_target(target)
|
||||
push_targets << target unless push_targets.include?(target)
|
||||
end
|
||||
|
||||
private
|
||||
def resolve_push_target(target)
|
||||
if target.is_a?(Symbol)
|
||||
"Notification::PushTarget::#{target.to_s.classify}".constantize
|
||||
else
|
||||
target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def push_later
|
||||
Notification::PushJob.perform_later(self)
|
||||
end
|
||||
|
||||
def push
|
||||
return unless pushable?
|
||||
|
||||
self.class.push_targets.each { |target| push_to(target) }
|
||||
end
|
||||
|
||||
def payload
|
||||
"Notification::#{payload_type}Payload".constantize.new(self)
|
||||
end
|
||||
|
||||
private
|
||||
def pushable?
|
||||
!creator.system? && user.active? && account.active?
|
||||
end
|
||||
|
||||
def push_to(target)
|
||||
target.process(self)
|
||||
end
|
||||
|
||||
def payload_type
|
||||
source_type.presence_in(%w[ Event Mention ]) || "Default"
|
||||
end
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
class NotificationPusher
|
||||
include Rails.application.routes.url_helpers
|
||||
include ExcerptHelper
|
||||
|
||||
attr_reader :notification
|
||||
|
||||
def initialize(notification)
|
||||
@notification = notification
|
||||
end
|
||||
|
||||
def push
|
||||
return unless should_push?
|
||||
|
||||
build_payload.tap do |payload|
|
||||
push_to_user(payload)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def should_push?
|
||||
notification.user.push_subscriptions.any? &&
|
||||
!notification.creator.system? &&
|
||||
notification.user.active? &&
|
||||
notification.account.active?
|
||||
end
|
||||
|
||||
def build_payload
|
||||
case notification.source_type
|
||||
when "Event"
|
||||
build_event_payload
|
||||
when "Mention"
|
||||
build_mention_payload
|
||||
else
|
||||
build_default_payload
|
||||
end
|
||||
end
|
||||
|
||||
def build_event_payload
|
||||
event = notification.source
|
||||
card = event.card
|
||||
|
||||
base_payload = {
|
||||
title: card_notification_title(card),
|
||||
path: card_path(card)
|
||||
}
|
||||
|
||||
case event.action
|
||||
when "comment_created"
|
||||
base_payload.merge(
|
||||
title: "RE: #{base_payload[:title]}",
|
||||
body: comment_notification_body(event),
|
||||
path: card_path_with_comment_anchor(event.eventable)
|
||||
)
|
||||
when "card_assigned"
|
||||
base_payload.merge(
|
||||
body: "Assigned to you by #{event.creator.name}"
|
||||
)
|
||||
when "card_published"
|
||||
base_payload.merge(
|
||||
body: "Added by #{event.creator.name}"
|
||||
)
|
||||
when "card_closed"
|
||||
base_payload.merge(
|
||||
body: card.closure ? "Moved to Done by #{event.creator.name}" : "Closed by #{event.creator.name}"
|
||||
)
|
||||
when "card_reopened"
|
||||
base_payload.merge(
|
||||
body: "Reopened by #{event.creator.name}"
|
||||
)
|
||||
else
|
||||
base_payload.merge(
|
||||
body: event.creator.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_mention_payload
|
||||
mention = notification.source
|
||||
card = mention.card
|
||||
|
||||
{
|
||||
title: "#{mention.mentioner.first_name} mentioned you",
|
||||
body: format_excerpt(mention.source.mentionable_content, length: 200),
|
||||
path: card_path(card)
|
||||
}
|
||||
end
|
||||
|
||||
def build_default_payload
|
||||
{
|
||||
title: "New notification",
|
||||
body: "You have a new notification",
|
||||
path: notifications_path(script_name: notification.account.slug)
|
||||
}
|
||||
end
|
||||
|
||||
def push_to_user(payload)
|
||||
subscriptions = notification.user.push_subscriptions
|
||||
enqueue_payload_for_delivery(payload, subscriptions)
|
||||
end
|
||||
|
||||
def enqueue_payload_for_delivery(payload, subscriptions)
|
||||
Rails.configuration.x.web_push_pool.queue(payload, subscriptions)
|
||||
end
|
||||
|
||||
def card_notification_title(card)
|
||||
card.title.presence || "Card #{card.number}"
|
||||
end
|
||||
|
||||
def comment_notification_body(event)
|
||||
format_excerpt(event.eventable.body, length: 200)
|
||||
end
|
||||
|
||||
def card_path(card)
|
||||
Rails.application.routes.url_helpers.card_path(card, script_name: notification.account.slug)
|
||||
end
|
||||
|
||||
def card_path_with_comment_anchor(comment)
|
||||
Rails.application.routes.url_helpers.card_path(
|
||||
comment.card,
|
||||
anchor: ActionView::RecordIdentifier.dom_id(comment),
|
||||
script_name: notification.account.slug
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,14 @@
|
||||
require "zlib"
|
||||
|
||||
module User::Avatar
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ALLOWED_AVATAR_CONTENT_TYPES = %w[ image/jpeg image/png image/gif image/webp ].freeze
|
||||
MAX_AVATAR_DIMENSIONS = { width: 4096, height: 4096 }.freeze
|
||||
AVATAR_COLORS = %w[
|
||||
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
|
||||
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
|
||||
].freeze
|
||||
|
||||
included do
|
||||
has_one_attached :avatar do |attachable|
|
||||
@@ -22,6 +28,10 @@ module User::Avatar
|
||||
avatar.variable? ? avatar.variant(:thumb) : avatar
|
||||
end
|
||||
|
||||
def avatar_background_color
|
||||
AVATAR_COLORS[Zlib.crc32(to_param) % AVATAR_COLORS.size]
|
||||
end
|
||||
|
||||
# Avatars are always publicly accessible
|
||||
def publicly_accessible?
|
||||
true
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<div class="settings__panel panel shadow">
|
||||
<%= render "notifications/settings/push_notifications" %>
|
||||
<%= render "notifications/settings/native_devices" if Fizzy.saas? %>
|
||||
<%= render "notifications/settings/email", settings: @settings %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -25,7 +25,7 @@ async function updateBadgeCount({ data: { badge } }) {
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const url = new URL(event.notification.data.path, self.location.origin).href
|
||||
const url = new URL(event.notification.data.url, self.location.origin).href
|
||||
event.waitUntil(openURL(url))
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user