Noticed
đŹ Screencast
Notifier Objects
Notifiers are essentially the controllers of the Noticed ecosystem and represent an Event. As such, we recommend naming them with the event they model in mind â be it a NewSaleNotifier,
ChargeFailureNotifier
, etc.
Notifiers must inherit from Noticed::Event
. This provides all of their functionality.
A Notifier exists to declare the various delivery methods that should be used for that event and any notification helper methods necessary in those delivery mechanisms. In this example weâll deliver by :action_cable
to provide real-time UI updates to usersâ browsers, :email
if theyâve opted into email notifications, and a bulk notification to :discord
to tell everyone on the Discord server thereâs been a new comment.
# ~/app/notifiers/new_comment_notifier.rb
class NewCommentNotifier < Noticed::Event
deliver_by :action_cable do |config|
config.channel = "NotificationsChannel"
config.stream = :some_stream
end
deliver_by :email do |config|
config.mailer = "CommentMailer"
config.if = ->(recipient) { !!recipient.preferences[:email] }
end
bulk_deliver_by :discord do |config|
config.url = "https://discord.com/xyz/xyz/123"
config.json = -> {
{
message: message,
channel: :general
}
}
end
notification_methods do
# I18n helpers
def message
t(".message")
end
# URL helpers are accessible in notifications
# Don't forget to set your default_url_options so Rails knows how to generate urls
def url
user_post_path(recipient, params[:post])
end
end
end
For deeper specifics on setting up the :action_cable
, :email
, and :discord
(bulk) delivery methods, refer to their docs: action_cable
, email
, and discord
(bulk).
Delivery Method Configuration
Each delivery method can be configured with a block that yields a config
object.
Procs/Lambdas will be evaluated when needed and symbols can be used to call a method.
When a lambda is passed, it will not pass any arguments and evaluates the Proc in the context of the Noticed::Notification
If you are using a symbol to call a method, we pass the notification object as an argument to the method. This allows you to access the notification object within the method.
Your method must accept a single argument. If you don’t need to use the object you can just use (*)
.
Show Example
```ruby class CommentNotifier < Noticed::Event deliver_by :ios do |config| config.format = :ios_format config.apns_key = :ios_cert config.key_id = :ios_key_id config.team_id = :ios_team_id config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_identifier) config.device_tokens = :ios_device_tokens config.if = -> { recipient.send_ios_notification? } end def ios_format(apn) apn.alert = { title:, body: } apn.mutable_content = true apn.content_available = true apn.sound = "notification.m4r" apn.custom_payload = { url:, type: self.class.name, id: record.id, image_url: "" || image_url, params: params.to_json } end def ios_cert(*) Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert) end def ios_key_id(*) Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id) end def ios_team_id(*) Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id) end def ios_bundle_id(*) Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier) end def ios_device_tokens(notification) notification.recipient.ios_device_tokens end def url comment_thread_path(record.thread) end end class Recipient < ApplicationRecord # or whatever your recipient model is has_many :ios_device_tokens def send_ios_notification? # some logic end end ```More examples are in the docs for each delivery method.
Required Params
While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the required_params
method:
class CarSaleNotifier < Noticed::Event
deliver_by :email { |c| c.mailer = "BranchMailer" }
# `record` is the Car record, `Branch` is the dealership
required_params :branch
# To validate the `:record` param, add a validation since it is an association on the Noticed::Event
validates :record, presence: true
end
Which will validate upon any invocation that the specified parameters are present:
CarSaleNotifier.with(record: Car.last).deliver(Branch.last)
#=> Noticed::ValidationError("Param `branch` is required for CarSaleNotifier")
CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
#=> OK
Helper Methods
Notifiers can implement various helper methods, within a notification_methods
block, that make it easier to render the resulting notification directly. These helpers can be helpful depending on where and how you choose to render notifications. A common use is rendering a userâs notifications in your web UI as standard ERB. These notification helper methods make that rendering much simpler:
<div>
<% @user.notifications.each do |notification| %>
<%= link_to notification.message, notification.url %>
<% end %>
</div>
On the other hand, if youâre using email delivery, ActionMailer has its own full stack for setting up objects and rendering. Your notification helper methods will always be available from the notification object, but using ActionMailerâs own paradigms may fit better for that particular delivery method. YMMV.
URL Helpers
Rails url helpers are included in Notifiers by default so you have full access to them in your notification helper methods, just like you would in your controllers and views.
But don’t forget, you’ll need to configure default_url_options
in order for Rails to know what host and port to use when generating URLs.
Rails.application.routes.default_url_options[:host] = 'localhost:3000'
Translations
We’ve also included Railsâ translate
and t
helpers for you to use in your notification helper methods. This also provides an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under notifiers
, the underscored name of the notifier class, and notification
. For example:
From the above Notifier…
class NewCommentNotifier < Noticed::Event
# ...
notification_methods do
def message
t(".message")
end
end
# ...
end
Calling the message
helper in an ERB view:
<%= @user.notifications.last.message %>
Will look for the following translation path:
# ~/config/locales/en.yml
en:
notifiers:
new_comment_notifier:
notification:
message: "Someone posted a new comment!"
Or, if you have your Notifier within another module, such as Admin::NewCommentNotifier
, the resulting lookup path will be en.notifiers.admin.new_comment_notifier.notification.message
(modules become nesting steps).
Tip: Extracting Delivery Method Configurations
If you want to reuse delivery method configurations across multiple Notifiers, you can extract them into a module and include them in your Notifiers.
# /app/notifiers/notifiers/comment_notifier.rb
class CommentNotifier < Noticed::Event
include IosNotifier
include AndriodNotifer
include EmailNotifier
validates :record, presence: true
end
### đ¨ Sending Notifications
Following the `NewCommentNotifier` example above, hereâs how we might invoke the Notifier to send notifications to every author in the thread about a new comment being added:
```ruby
NewCommentNotifier.with(record: @comment, foo: "bar").deliver(@comment.thread.all_authors)
This instantiates a new NewCommentNotifier
with params (similar to ActiveJob, any serializable params are permitted), then delivers notifications to all authors in the thread.
⨠The record:
param is a special param that gets assigned to the record
polymorphic association in the database. You should try to set the record:
param where possible. This may be best understood as âthe record/object this notification is _about_â, and allows for future queries from the record-side: âgive me all notifications that were generated from this commentâ.
This invocation will create a single Noticed::Event
record and a Noticed::Notification
record for each recipient. A background job will then process the Event and fire off a separate background job for each bulk delivery method and each recipient + individual-delivery-method combination. In this case, thatâd be the following jobs kicked off from this event:
- A bulk delivery job for
:discord
bulk delivery - An individual delivery job for
:action_cable
method to the first thread author - An individual delivery job for
:email
method to the first thread author - An individual delivery job for
:action_cable
method to the second thread author - An individual delivery job for
:email
method to the second thread author - Etc…
Custom Noticed Model Methods
In order to extend the Noticed models you’ll need to use a concern and a to_prepare block:
# config/application.rb
module MyApp
class Application < Rails::Application
# ...
config.to_prepare do
Noticed::Event.include Noticed::EventExtensions
Noticed::Notification.include Noticed::NotificationExtensions
end
end
end
â Best Practices
Renaming Notifiers
If you rename a Notifier class your existing data and Noticed setup may break. This is because Noticed serializes the class name and sets it to the type
column on the Noticed::Event
record and the type
column on the Noticed::Notification
record.
When renaming a Notifier class you will need to backfill existing Events and Notifications to reference the new name.
Noticed::Event.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name)
# and
Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: "#{NewNotifierClassName.name}::Notification")
đ Delivery Methods
The delivery methods are designed to be modular so you can customize the way each type gets delivered.
For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notifier and the delivery method will handle the processing of it.
Individual delivery methods:
- ActionCable
- Apple Push Notification Service
- Firebase Cloud Messaging (iOS, Android, and web clients)
- Microsoft Teams
- Slack
- Twilio Messaging - SMS, Whatsapp
- Vonage SMS
- Test
Bulk delivery methods:
No Delivery Methods
Itâs worth pointing out that you can have a fully-functional and useful Notifier that has no delivery methods. This means that invoking the Notifier and âsendingâ the notification will only create new database records (no external surfaces like email, sms, etc.). This is still useful as itâs the database records that allow your app to render a userâs (or other objectâs) notifications in your web UI.
So even with no delivery methods set, this example is still perfectly available and helpful:
<div>
<% @user.notifications.each do |notification| %>
<%= link_to notification.message, notification.url %>
<% end %>
</div>
Sending a notification is entirely an internal-to-your-app function. Delivery methods just get the word out! But many apps may be fully satisfied without that extra layer.
Fallback Notifications
A common pattern is to deliver a notification via a real (or real-ish)-time service, then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the wait
option, and the conditional if
/ unless
option.
class NewCommentNotifier < Noticed::Event
deliver_by :action_cable
deliver_by :email do |config|
config.mailer = "CommentMailer"
config.wait = 15.minutes
config.unless = -> { read? }
end
end
Here a notification will be created immediately in the database (for display directly in your appâs web interface) and sent via ActionCable. If the notification has not been marked read
after 15 minutes, the email notification will be sent. If the notification has already been read in the app, the email will be skipped.
A note here: notifications expose a #mark_as_read
method, but your app must choose when and where to call that method.
You can mix and match the options and delivery methods to suit your application specific needs.
app/notifiers/delivery_methods/turbo_stream.rb
class DeliveryMethods::TurboStream < ApplicationDeliveryMethod def deliver
return unless recipient.is_a?(User)
notification.broadcast_update_to_bell
notification.broadcast_replace_to_index_count
notification.broadcast_prepend_to_index_list
end end
```ruby
#### Callbacks
Callbacks for delivery methods wrap the _actual_ delivery of the notification. You can use `before_deliver`, `around_deliver` and `after_deliver` in your custom delivery methods.
```ruby
class DeliveryMethods::Discord < Noticed::DeliveryMethod
after_deliver do
# Do whatever you want
end
end
đŚ Database Model
The Noticed database models include several helpful features to make working with notifications easier.
Notification
Class methods/scopes
(Assuming your user has_many :notifications, as: :recipient, class_name: "Noticed::Notification"
)
Sorting notifications by newest first:
@user.notifications.newest_first
Query for read or unread notifications:
user.notifications.read
user.notifications.unread
Marking all notifications as read or unread:
user.notifications.mark_as_read
user.notifications.mark_as_unread
Instance methods
Mark notification as read / unread:
@notification.mark_as_read
@notification.mark_as_read!
@notification.mark_as_unread
@notification.mark_as_unread!
Check if read / unread:
@notification.read?
@notification.unread?
Associating Notifications
Adding notification associations to your models makes querying, rendering, and managing notifications easy (and is a pretty critical feature of most applications).
There are two ways to associate your models to notifications:
- Where your object
has_many
notifications as the recipient (who you sent the notification to) - Where your object
has_many
notifications as therecord
(what the notifications were about)
In the former, weâll use a has_many
to :notifications
. In the latter, weâll actually has_many
to :events
, since record
s generate notifiable events (and events generate notifications).
We can illustrate that in the following:
class User < ApplicationRecord
has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
end
# All of the notifications the user has been sent
# @user.notifications.each { |n| render(n) }
class Post < ApplicationRecord
has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
has_many :notifications, through: :noticed_events, class_name: "Noticed::Notification"
end
# All of the notification events this post generated
# @post.notifications
ActiveJob Parent Class
Noticed uses its own Noticed::ApplicationJob
as the base job for all notifications. In the event that you would like to customize the parent job class, there is a parent_class
attribute that can be overridden with your own class. This should be done in a noticed.rb
initializer.
Noticed.parent_class = "ApplicationJob"
Handling Deleted Records
Generally we recommend using a dependent: ___
relationship on your models to avoid cases where Noticed Events or Notifications are left lingering when your models are destroyed. In the case that they are or data becomes mis-matched, youâll likely run into deserialization issues. That may be globally alleviated with the following snippet, but use with caution.
class ApplicationJob < ActiveJob::Base
discard_on ActiveJob::DeserializationError
end
Customizing the Database Models
You can modify the database models by editing the generated migrations.
One common adjustment is to change the IDs to UUIDs (if you’re using UUIDs in your app).
You can also add additional columns to the Noticed::Event
and Noticed::Notification
models.
# This migration comes from noticed (originally 20231215190233)
class CreateNoticedTables < ActiveRecord::Migration[7.1]
def change
create_table :noticed_events, id: :uuid do |t|
t.string :type
t.belongs_to :record, polymorphic: true, type: :uuid
t.jsonb :params
# Custom Fields
t.string :organization_id, type: :uuid, as: "((params ->> 'organization_id')::uuid)", stored: true
t.virtual :action_type, type: :string, as: "((params ->> 'action_type'))", stored: true
t.virtual :url, type: :string, as: "((params ->> 'url'))", stored: true
t.timestamps
end
create_table :noticed_notifications, id: :uuid do |t|
t.string :type
t.belongs_to :event, null: false, type: :uuid
t.belongs_to :recipient, polymorphic: true, null: false, type: :uuid
t.datetime :read_at
t.datetime :seen_at
t.timestamps
end
add_index :noticed_notifications, :read_at
end
end
The custom fields in the above example are stored as virtual columns. These are populated from values passed in the params
hash when creating the notifier.