Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3c54cd8
Add native push notification infrastructure
olivaresf Jan 14, 2026
5533687
Fix database issues and add APNS configuration
olivaresf Jan 16, 2026
7d04bb5
Add Firebase configuration
olivaresf Jan 20, 2026
2121b6d
Use version of `action_push_native` with proper config paths support
rosa Jan 20, 2026
29d3960
Remove UUID requirement from push notification device registration
olivaresf Jan 21, 2026
ade39b1
Add title/body to android notifications too
olivaresf Jan 21, 2026
7e0470a
Send the URL instead of path in notifications
olivaresf Jan 21, 2026
9d32f3e
Refactor devices controller and extract registration to model
rosa Jan 20, 2026
d3cc79a
Simplify device routes and use ActiveRecord validations
rosa Jan 21, 2026
555132b
Change device ownership from User to Identity
rosa Jan 21, 2026
05819f8
Refactor notification push system with registry pattern
rosa Jan 21, 2026
1d2ed91
Extract payload building into dedicated classes
rosa Jan 21, 2026
06c9ece
Move payload method to Notification and make accessors public
rosa Jan 21, 2026
b5205ce
Rename Push to PushTarget for better readability
rosa Jan 21, 2026
ab6bc25
Link devices to sessions for automatic cleanup on logout
rosa Jan 21, 2026
92f283c
Remove redundant owner index from devices table
rosa Jan 21, 2026
8a77fd7
Squash device migrations into single table creation
rosa Jan 21, 2026
2f9e41d
Remove foreign key constraint from devices to sessions
rosa Jan 21, 2026
c7d0559
Change priority notification level for mentions and assignments
olivaresf Jan 22, 2026
1b53396
Make devices controller untenanted with engine routes
rosa Jan 22, 2026
34fd62f
Move devices table to saas database
rosa Jan 22, 2026
9299300
Move push priority concerns from Event and Mention into Native push t…
rosa Jan 22, 2026
4f19c42
Move category and high_priority to payload classes
rosa Jan 22, 2026
3621df8
Consolidate push jobs into single Notification::PushJob
rosa Jan 22, 2026
0948533
Rename push to process on PushTarget for clearer semantics
rosa Jan 22, 2026
7864748
Simplify PushTarget by removing template method pattern
rosa Jan 22, 2026
6b1598e
Configure APNS and FCM encryption keys in Kamal secrets
rosa Jan 23, 2026
155fa2d
Go back to RubyGems version of `action_push_native`
rosa Jan 23, 2026
1a0d8e2
Fix notification click URL by using correct data property
rosa Jan 23, 2026
126ccb5
Add integration test for notification delivery and simplify test helpers
rosa Jan 23, 2026
dbfb141
Restore PushNotificationJob as shim for in-flight jobs during deploy
rosa Jan 23, 2026
d1500ad
Rename apns-dev to push-dev and unify 1Password credentials
rosa Jan 24, 2026
5ad1e8c
Simplify push.yml to only use B64 encryption keys
rosa Jan 24, 2026
9c165e1
Don't read APNS_TEAM_ID from 1Password
rosa Jan 24, 2026
6ec3cc6
Document --push flag for testing native push notifications
rosa Jan 24, 2026
113e61d
Add avatar_url so we always get an avatar even when the user hasn't s…
olivaresf Jan 26, 2026
b1cdb72
Add creator initials and avatar color to push notification payload
olivaresf Jan 26, 2026
cefc206
Add creator_familiar_name to push notification payload
olivaresf Jan 26, 2026
92cb749
Fix tests for renamed fixtures and new stacked notifications
rosa Feb 20, 2026
abef50c
Fix devices_path route helper in native devices partial
rosa Feb 21, 2026
5bea963
Make APNS and FCM env vars available
rosa Feb 23, 2026
6fb2411
Use internal account ID as `account_id` and add `account_slug`
rosa Feb 24, 2026
13268ef
Add base_url to native push notification payload
rosa Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ GEM
specs:
action_text-trix (2.1.16)
railties
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
autotuner (1.1.0)
aws-eventstream (1.4.0)
Expand Down Expand Up @@ -337,7 +337,7 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.2)
puma (7.2.0)
nio4r (~> 2.0)
raabro (1.4.0)
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.saas
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ gem "fizzy-saas", path: "saas"
gem "console1984", bc: "console1984"
gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api"

# Native push notifications (iOS/Android)
gem "action_push_native"

# Telemetry
gem "rails_structured_logging", bc: "rails-structured-logging"
gem "sentry-ruby"
Expand Down
44 changes: 41 additions & 3 deletions Gemfile.saas.lock
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ PATH
GEM
remote: https://rubygems.org/
specs:
action_push_native (0.3.1)
activejob (>= 8.0)
activerecord (>= 8.0)
googleauth (~> 1.14)
httpx (~> 1.6)
jwt (>= 2)
railties (>= 8.0)
action_text-trix (2.1.16)
railties
actionpack-xml_parser (2.0.1)
Expand All @@ -208,8 +215,8 @@ GEM
activemodel (>= 7.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 7.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
anyway_config (2.7.2)
ruby-next-core (~> 1.0)
ast (2.4.3)
Expand Down Expand Up @@ -279,6 +286,12 @@ GEM
tzinfo
faker (3.6.0)
i18n (>= 1.8.11, < 2)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
Expand All @@ -295,8 +308,23 @@ GEM
addressable (>= 2.5.0)
globalid (1.3.0)
activesupport (>= 6.1)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-logging-utils (0.2.0)
googleauth (1.16.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
gvltools (0.4.0)
hashdiff (1.2.1)
http-2 (1.1.1)
httpx (1.7.1)
http-2 (>= 1.0.0)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
Expand Down Expand Up @@ -369,6 +397,9 @@ GEM
mocha (3.0.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
multi_json (1.19.1)
net-http (0.9.1)
uri (>= 0.11.1)
net-http-persistent (4.0.8)
connection_pool (>= 2.2.4, < 4)
net-imap (0.6.3)
Expand Down Expand Up @@ -403,6 +434,7 @@ GEM
nokogiri (1.19.1-x86_64-linux-musl)
racc (~> 1.4)
openssl (4.0.0)
os (1.1.4)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.0)
Expand Down Expand Up @@ -457,7 +489,7 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.2)
puma (7.2.0)
nio4r (~> 2.0)
raabro (1.4.0)
Expand Down Expand Up @@ -546,6 +578,11 @@ GEM
sentry-ruby (6.2.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
sniffer (0.5.0)
anyway_config (>= 1.0)
dry-initializer (~> 3)
Expand Down Expand Up @@ -663,6 +700,7 @@ PLATFORMS
x86_64-linux-musl

DEPENDENCIES
action_push_native
actionpack-xml_parser
activeresource
audits1984!
Expand Down
9 changes: 1 addition & 8 deletions app/helpers/avatars_helper.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 5 additions & 0 deletions app/jobs/notification/push_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Notification::PushJob < ApplicationJob
def perform(notification)
notification.push
end
end
2 changes: 1 addition & 1 deletion app/jobs/push_notification_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ class PushNotificationJob < ApplicationJob
discard_on ActiveJob::DeserializationError

def perform(notification)
NotificationPusher.new(notification).push
notification.push
end
end
12 changes: 0 additions & 12 deletions app/models/concerns/push_notifiable.rb

This file was deleted.

3 changes: 2 additions & 1 deletion app/models/notification.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Notification < ApplicationRecord
include PushNotifiable
include Notification::Pushable

belongs_to :account, default: -> { user.account }
belongs_to :user
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions app/models/notification/default_payload.rb
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions app/models/notification/event_payload.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions app/models/notification/mention_payload.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions app/models/notification/push_target.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions app/models/notification/push_target/web.rb
Original file line number Diff line number Diff line change
@@ -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
Loading