noticed 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +624 -185
  3. data/app/jobs/noticed/event_job.rb +5 -7
  4. data/app/models/concerns/noticed/deliverable.rb +60 -18
  5. data/app/models/concerns/noticed/notification_methods.rb +1 -1
  6. data/app/models/concerns/noticed/readable.rb +32 -8
  7. data/app/models/noticed/deliverable/deliver_by.rb +28 -7
  8. data/app/models/noticed/ephemeral.rb +63 -0
  9. data/app/models/noticed/event.rb +13 -0
  10. data/app/models/noticed/notification.rb +3 -3
  11. data/db/migrate/20231215190233_create_noticed_tables.rb +16 -5
  12. data/db/migrate/20240129184740_add_notifications_count_to_noticed_event.rb +5 -0
  13. data/lib/generators/noticed/delivery_method_generator.rb +9 -1
  14. data/lib/generators/noticed/notifier_generator.rb +14 -1
  15. data/lib/generators/noticed/templates/application_bulk_delivery_method.rb.tt +2 -0
  16. data/lib/generators/noticed/templates/application_delivery_method.rb.tt +2 -0
  17. data/lib/generators/noticed/templates/application_notifier.rb.tt +2 -0
  18. data/lib/generators/noticed/templates/bulk_delivery_method.rb.tt +14 -0
  19. data/lib/generators/noticed/templates/delivery_method.rb.tt +10 -8
  20. data/lib/generators/noticed/templates/notifier.rb.tt +8 -2
  21. data/lib/noticed/api_client.rb +3 -1
  22. data/lib/noticed/bulk_delivery_method.rb +19 -7
  23. data/lib/noticed/bulk_delivery_methods/bluesky.rb +51 -0
  24. data/lib/noticed/bulk_delivery_methods/discord.rb +2 -0
  25. data/lib/noticed/bulk_delivery_methods/slack.rb +25 -1
  26. data/lib/noticed/bulk_delivery_methods/test.rb +13 -0
  27. data/lib/noticed/bulk_delivery_methods/webhook.rb +4 -1
  28. data/lib/noticed/coder.rb +5 -1
  29. data/lib/noticed/delivery_method.rb +21 -8
  30. data/lib/noticed/delivery_methods/action_cable.rb +11 -5
  31. data/lib/noticed/delivery_methods/action_push_native.rb +24 -0
  32. data/lib/noticed/delivery_methods/discord.rb +2 -0
  33. data/lib/noticed/delivery_methods/email.rb +13 -5
  34. data/lib/noticed/delivery_methods/fcm.rb +19 -2
  35. data/lib/noticed/delivery_methods/ios.rb +15 -4
  36. data/lib/noticed/delivery_methods/microsoft_teams.rb +2 -0
  37. data/lib/noticed/delivery_methods/slack.rb +25 -1
  38. data/lib/noticed/delivery_methods/test.rb +2 -0
  39. data/lib/noticed/delivery_methods/twilio_messaging.rb +9 -1
  40. data/lib/noticed/delivery_methods/vonage_sms.rb +2 -0
  41. data/lib/noticed/delivery_methods/webhook.rb +4 -1
  42. data/lib/noticed/engine.rb +10 -0
  43. data/lib/noticed/has_notifications.rb +49 -0
  44. data/lib/noticed/notification_channel.rb +19 -0
  45. data/lib/noticed/version.rb +1 -1
  46. data/lib/noticed.rb +15 -27
  47. metadata +14 -6
data/README.md CHANGED
@@ -1,21 +1,33 @@
1
- # Noticed
2
- ### 🎉 Notifications for your Ruby on Rails app.
1
+ # Noticed
2
+
3
+ ## 🎉 Notifications for your Ruby on Rails app.
3
4
 
4
5
  [![Build Status](https://github.com/excid3/noticed/workflows/Tests/badge.svg)](https://github.com/excid3/noticed/actions) [![Gem Version](https://badge.fury.io/rb/noticed.svg)](https://badge.fury.io/rb/noticed)
5
6
 
6
- Noticed helps you send notifications in your Rails apps. Notifications can be sent to any number of recipients. You might want a Slack notification with 0 recipients to let your team know when something happens. A notification can also be sent to 1+ recipients with individual deliveries (like an email to each recipient).
7
+ Noticed is a gem that allows your application to send notifications of varying types, over various mediums, to various recipients. Be it a Slack notification to your own team when some internal event occurs or a notification to your user, sent as a text message, email, and real-time UI element in the browser, Noticed supports all of the above (at the same time)!
8
+
9
+ Noticed implements two top-level types of delivery methods:
10
+
11
+ 1. **Individual Deliveries**: Where each recipient gets their own notification
12
+ <details>
13
+ <summary> Show Example </summary>
14
+
15
+ Let’s use a car dealership as an example here. When someone purchases a car, a notification will be sent to the buyer with some contract details (“Congrats on your new 2024 XYZ Model...”), another to the car sales-person with different details (“You closed X deal; your commission is Y”), and another to the bank handling the loan with financial details (“New loan issued; amount $20,000...”). The event (the car being sold) necessitates multiple notifications being sent out to different recipients, but each contains its own unique information and should be separate from the others. These are individual deliveries.
16
+ </details>
7
17
 
8
- The core concepts of Noticed are:
18
+ 2. **Bulk Deliveries**: One notification for all recipients. This is useful for sending a notification to your Slack team, for example.
9
19
 
10
- 1. `Notifier` - Classes that define how notifications are delivered and when.
11
- 2. `Noticed::Event` - When a `Notifier` is delivered, a `Noticed::Event` record is created in the database to store params for the delivery.`Notifiers` are ActiveRecord objects inherited from `Noticed::Event` using Single Table Inheritance.
12
- 3. `Noticed::Notification` - Keeps track of each recipient for `Noticed::Event` and the seen & read status for each.
13
- 4. Delivery methods are ActiveJob instances and support the same features like wait, queue, and priority.
20
+ <details>
21
+ <summary> Show Example </summary>
22
+ Let’s continue with the car-sale example here. Consider that your development team created the car-sales application that processed the deal above and sent out the notifications to the three parties. For the sake of team morale and feeling the ‘wins’, you may want to implement a notification that notifies your internal development team whenever a car sells through your platform. In this case, you’ll be notifying many people (your development team, maybe others at your company) but with the same content (“someone just bought a car through our platform!”). This is a bulk delivery. It’s generally a single notification that many people just need to be made aware of.
14
23
 
15
- ## Delivery Methods
16
- Individual Delivery methods (one notification to each recipient):
24
+ Bulk deliveries are typically used to push notifications to other platforms where users are managed (Slack, Discord, etc.) instead of your own.
25
+ </details>
26
+
27
+ Delivery methods we officially support:
17
28
 
18
29
  * [ActionCable](docs/delivery_methods/action_cable.md)
30
+ * [Action Push Native](docs/delivery_methods/action_push_native.md)
19
31
  * [Apple Push Notification Service](docs/delivery_methods/ios.md)
20
32
  * [Email](docs/delivery_methods/email.md)
21
33
  * [Firebase Cloud Messaging](docs/delivery_methods/fcm.md) (iOS, Android, and web clients)
@@ -25,17 +37,18 @@ Individual Delivery methods (one notification to each recipient):
25
37
  * [Vonage SMS](docs/delivery_methods/vonage_sms.md)
26
38
  * [Test](docs/delivery_methods/test.md)
27
39
 
28
- Bulk delivery methods (one notification for all recipients):
40
+ Bulk delivery methods we support:
29
41
 
42
+ * [Bluesky](docs/bulk_delivery_methods/bluesky.md)
30
43
  * [Discord](docs/bulk_delivery_methods/discord.md)
31
44
  * [Slack](docs/bulk_delivery_methods/slack.md)
32
45
  * [Webhook](docs/bulk_delivery_methods/webhook.md)
33
46
 
34
47
  ## 🎬 Screencast
35
48
 
36
- <a href="https://www.youtube.com/watch?v=Scffi4otlFc"><img src="https://i.imgur.com/UvVKWwD.png" title="How to add Notifications to Rails with Noticed" width="50%" /></a>
49
+ <a href="https://www.youtube.com/watch?v=SzX-aBEqnAc"><img src="https://i.imgur.com/UvVKWwD.png" title="How to add Notifications to Rails with Noticed" width="50%" /></a>
37
50
 
38
- [Watch Screencast](https://www.youtube.com/watch?v=Scffi4otlFc)
51
+ [Watch Screencast](https://www.youtube.com/watch?v=SzX-aBEqnAc)
39
52
 
40
53
  ## 🚀 Installation
41
54
  Run the following command to add Noticed to your Gemfile:
@@ -44,7 +57,7 @@ Run the following command to add Noticed to your Gemfile:
44
57
  bundle add "noticed"
45
58
  ```
46
59
 
47
- Add the migrations:
60
+ Generate then run the migrations:
48
61
 
49
62
  ```bash
50
63
  rails noticed:install:migrations
@@ -53,124 +66,251 @@ rails db:migrate
53
66
 
54
67
  ## 📝 Usage
55
68
 
56
- To generate a Notifier, simply run:
69
+ Noticed operates with a few constructs: Notifiers, delivery methods, and Notification records.
70
+
71
+ To start, generate a Notifier:
72
+
73
+ ```bash
74
+ rails generate noticed:notifier NewCommentNotifier
75
+ ```
76
+
77
+ ### Usage Contents
78
+ - [Notifier Objects](#notifier-objects)
79
+ - [Delivery Method Configuration](#delivery-method-configuration)
80
+ - [Required Params](#required-params)
81
+ - [Helper Methods](#helper-methods)
82
+ - [URL Helpers](#url-helpers)
83
+ - [Translations](#translations)
84
+ - [Tip: Capture User Preferences](#tip-capture-user-preferences)
85
+ - [Tip: Extracting Delivery Method Configurations](#tip-extracting-delivery-method-configurations)
86
+ - [Shared Delivery Method Options](#shared-delivery-method-options)
87
+ - [Sending Notifications](#-sending-notifications)
88
+ - [Custom Noticed Model Methods](#custom-noticed-model-methods)
89
+
90
+ ### Notifier Objects
91
+
92
+ 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.
57
93
 
58
- `rails generate noticed:notifier CommentNotifier`
94
+ Notifiers must inherit from `Noticed::Event`. This provides all of their functionality.
59
95
 
60
- #### Add Delivery Methods
61
- Then add delivery methods to the Notifier. See [docs/delivery_methods](docs/) for a full list.
96
+ 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.
62
97
 
63
98
  ```ruby
64
- # app/notifiers/comment_notifier.rb
65
- class CommentNotifier < Noticed::Event
66
- bulk_deliver_by :webhook do |config|
67
- config.url = "https://example.org..."
68
- config.json = ->{ text: "New comment: #{record.body}" }
99
+ # ~/app/notifiers/new_comment_notifier.rb
100
+
101
+ class NewCommentNotifier < Noticed::Event
102
+ deliver_by :action_cable do |config|
103
+ config.channel = "NotificationsChannel"
104
+ config.stream = :some_stream
69
105
  end
70
106
 
71
107
  deliver_by :email do |config|
72
- config.mailer = "UserMailer"
73
- config.method = :new_comment
108
+ config.mailer = "CommentMailer"
109
+ config.if = -> { !!recipient.preferences[:email] }
74
110
  end
75
- end
76
- ```
77
111
 
78
- #### Sending Notifications
112
+ bulk_deliver_by :discord do |config|
113
+ config.url = "https://discord.com/xyz/xyz/123"
114
+ config.json = -> {
115
+ {
116
+ message: message,
117
+ channel: :general
118
+ }
119
+ }
120
+ end
79
121
 
80
- To send a notification to user(s):
122
+ notification_methods do
123
+ # I18n helpers
124
+ def message
125
+ t(".message")
126
+ end
81
127
 
82
- ```ruby
83
- # Instantiate a new notifier
84
- CommentNotifier.with(record: @comment, foo: "bar").deliver_later(User.all)
128
+ # URL helpers are accessible in notifications
129
+ # Don't forget to set your default_url_options so Rails knows how to generate urls
130
+ def url
131
+ user_post_path(recipient, params[:post])
132
+ end
133
+ end
134
+ end
85
135
  ```
86
136
 
87
- This instantiates a new `CommentNotifier` with params. Similar to ActiveJob, you can pass any params can be serialized. `record:` is a special param that gets assigned to the `record` polymorphic association in the database.
137
+ For deeper specifics on setting up the `:action_cable`, `:email`, and `:discord` (bulk) delivery methods, refer to their docs: [`action_cable`](docs/delivery_methods/action_cable.md), [`email`](docs/delivery_methods/email.md), and [`discord` (bulk)](docs/bulk_delivery_methods/discord.md).
88
138
 
89
- Delivering will create a `Noticed::Event` record and associated `Noticed::Notification` records for each recipient.
139
+ #### Delivery Method Configuration
90
140
 
91
- After saving, a job will be enqueued for processing this notification and delivering it to all recipients.
141
+ Each delivery method can be configured with a block that yields a `config` object.
92
142
 
93
- Each delivery method also spawns its own job. This allows you to skip email notifications if the user had already opened a push notification, for example.
143
+ Procs/Lambdas will be evaluated when needed and symbols can be used to call a method.
94
144
 
95
- #### Notifier Objects
145
+ When a lambda is passed, it will not pass any arguments and evaluates the Proc in the context of the Noticed::Notification
96
146
 
97
- Notifiers inherit from `Noticed::Event`. This provides all their functionality and allows them to be delivered.
147
+ 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.
148
+ Your method must accept a single argument. If you don't need to use the object you can just use `(*)`.
149
+
150
+ <details>
151
+ <summary> Show Example </summary>
98
152
 
99
153
  ```ruby
100
154
  class CommentNotifier < Noticed::Event
101
- deliver_by :action_cable
102
- deliver_by :email do |config|
103
- config.mailer = "UserMailer"
104
- config.if = ->(recipient) { !!recipient.preferences[:email] }
105
- config.wait = 5.minutes
155
+ deliver_by :ios do |config|
156
+ config.format = :ios_format
157
+ config.apns_key = :ios_cert
158
+ config.key_id = :ios_key_id
159
+ config.team_id = :ios_team_id
160
+ config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_identifier)
161
+ config.device_tokens = :ios_device_tokens
162
+ config.if = -> { recipient.send_ios_notification? }
163
+ end
164
+
165
+ def ios_format(apn)
166
+ apn.alert = { title:, body: }
167
+ apn.mutable_content = true
168
+ apn.content_available = true
169
+ apn.sound = "notification.m4r"
170
+ apn.custom_payload = {
171
+ url:,
172
+ type: self.class.name,
173
+ id: record.id,
174
+ image_url: "" || image_url,
175
+ params: params.to_json
176
+ }
106
177
  end
107
- end
108
- ```
109
178
 
110
- **Shared Options**
179
+ def ios_cert(*)
180
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert)
181
+ end
111
182
 
112
- * `if: :method_name` - Calls `method_name` and cancels delivery method if `false` is returned. This can also be specified as a Proc / lambda.
113
- * `unless: :method_name` - Calls `method_name` and cancels delivery method if `true` is returned
114
- * `wait:` - Delays the delivery for the given duration of time. Can be an `ActiveSupport::Duration`, Proc / lambda, or Symbol.
183
+ def ios_key_id(*)
184
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id)
185
+ end
115
186
 
116
- ##### Helper Methods
187
+ def ios_team_id(*)
188
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id)
189
+ end
117
190
 
118
- You can define helper methods inside your Notifier object to make it easier to render.
191
+ def ios_bundle_id(*)
192
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier)
193
+ end
119
194
 
120
- ```ruby
121
- class CommentNotifier < Noticed::Event
122
- # I18n helpers
123
- def message
124
- t(".message")
195
+ def ios_device_tokens(notification)
196
+ notification.recipient.ios_device_tokens
125
197
  end
126
198
 
127
- # URL helpers are accessible in notifications
128
- # Don't forget to set your default_url_options so Rails knows how to generate urls
129
199
  def url
130
- post_path(params[:post])
200
+ comment_thread_path(record.thread)
131
201
  end
202
+ end
132
203
 
133
- # Defines methods added to the Noticed::Notification
134
- notification_methods do
135
- def personalized_welcome
136
- "Hello #{recipient.first_name}."
137
- end
204
+ class Recipient < ApplicationRecord # or whatever your recipient model is
205
+ has_many :ios_device_tokens
206
+
207
+ def send_ios_notification?
208
+ # some logic
138
209
  end
139
210
  end
140
211
  ```
212
+ </details>
213
+
214
+ More examples are in the docs for each delivery method.
215
+
216
+ #### Required Params
217
+
218
+ While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the `required_params` method:
219
+
220
+ ```ruby
221
+ class CarSaleNotifier < Noticed::Event
222
+ deliver_by :email { |c| c.mailer = "BranchMailer" }
223
+
224
+ # `record` is the Car record, `Branch` is the dealership
225
+ required_params :branch
226
+
227
+ # To validate the `:record` param, add a validation since it is an association on the Noticed::Event
228
+ validates :record, presence: true
229
+ end
230
+ ```
231
+
232
+ Which will validate upon any invocation that the specified parameters are present:
233
+
234
+ ```ruby
235
+ CarSaleNotifier.with(record: Car.last).deliver(Branch.last)
236
+ #=> Noticed::ValidationError("Param `branch` is required for CarSaleNotifier")
237
+
238
+ CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
239
+ #=> OK
240
+ ```
241
+
242
+ #### Helper Methods
243
+
244
+ 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:
141
245
 
142
- In your views, you can loop through notifications and access
143
246
  ```erb
144
- <%= current_user.notifications.includes(:event).each do |notification| %>
145
- <%= link_to notification.personalized_welcome, notification.event.url %>
146
- <% end %>
247
+ <div>
248
+ <% @user.notifications.each do |notification| %>
249
+ <%= link_to notification.message, notification.url %>
250
+ <% end %>
251
+ </div>
147
252
  ```
148
253
 
149
- ##### URL Helpers
254
+ 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.
255
+
256
+ #### URL Helpers
257
+
258
+ 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.
150
259
 
151
- URL helpers are included in Notifier classes so you have full access to them just like in your controllers and views. Configure `default_url_options` in order for Rails to know what host and port to use when generating URLs.
260
+ _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.
152
261
 
153
262
  ```ruby
154
263
  Rails.application.routes.default_url_options[:host] = 'localhost:3000'
155
264
  ```
156
265
 
157
- ##### Translations
266
+ #### Translations
158
267
 
159
- `translate` and `t` helpers are available in Notifiers. If the key starts with a period, it will automatically scope the key under `notifiers` and the underscored name of the notification class it is used in.
268
+ 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:
160
269
 
161
- For example:
270
+ From the above Notifier...
162
271
 
163
- `t(".message")` looks up `en.notifiers.new_comment.message`
164
- `t(".message") # in Admin::NewComment` looks up `en.notifiers.admin.new_comment.message`
272
+ ```ruby
273
+ class NewCommentNotifier < Noticed::Event
274
+ # ...
165
275
 
166
- ##### User Preferences
276
+ notification_methods do
277
+ def message
278
+ t(".message")
279
+ end
280
+ end
281
+
282
+ # ...
283
+ end
284
+ ```
285
+
286
+ Calling the `message` helper in an ERB view:
287
+
288
+ ```erb
289
+ <%= @user.notifications.last.message %>
290
+ ```
291
+
292
+ Will look for the following translation path:
293
+
294
+ ```yml
295
+ # ~/config/locales/en.yml
296
+
297
+ en:
298
+ notifiers:
299
+ new_comment_notifier:
300
+ notification:
301
+ message: "Someone posted a new comment!"
302
+ ```
303
+
304
+ 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).
167
305
 
168
- You can use the `if:` and `unless: ` options on your delivery methods to check the user's preferences and skip processing if they have disabled that type of notification.
306
+ #### Tip: Capture User Preferences
307
+
308
+ You can use the `if:` and `unless:` options on your delivery methods to check the user's preferences and skip processing if they have disabled that type of notification.
169
309
 
170
310
  For example:
171
311
 
172
312
  ```ruby
173
- class CommentNotifier < Noticed::Base
313
+ class CommentNotifier < Noticed::Event
174
314
  deliver_by :email do |config|
175
315
  config.mailer = 'CommentMailer'
176
316
  config.method = :new_comment
@@ -179,102 +319,292 @@ class CommentNotifier < Noticed::Base
179
319
  end
180
320
  ```
181
321
 
182
- ## Best Practices
322
+ If you would like to skip the delivery job altogether, for example if you know that a user doesn't support the platform and you would like to save resources by not enqueuing the job, you can use `before_enqueue`.
323
+
324
+ For example:
325
+
326
+ ```ruby
327
+ class IosNotifier < Noticed::Event
328
+ deliver_by :ios do |config|
329
+ # ...
330
+ config.before_enqueue = ->{ throw(:abort) unless recipient.registered_ios? }
331
+ end
332
+ end
333
+ ```
183
334
 
184
- ### Creating a notification from an Active Record callback
335
+ #### Tip: Extracting Delivery Method Configurations
185
336
 
186
- Always use `after_commit` hooks to send notifications from ActiveRecord callbacks. For example, to send a notification automatically after a message is created:
337
+ If you want to reuse delivery method configurations across multiple Notifiers, you can extract them into a module and include them in your Notifiers.
187
338
 
188
339
  ```ruby
189
- class Message < ApplicationRecord
190
- belongs_to :recipient, class_name: "User"
340
+ # /app/notifiers/notifiers/comment_notifier.rb
341
+ class CommentNotifier < Noticed::Event
342
+ include IosNotifier
343
+ include AndroidNotifier
344
+ include EmailNotifier
345
+
346
+ validates :record, presence: true
347
+ end
191
348
 
192
- after_create_commit :notify_recipient
349
+ # /app/notifiers/concerns/ios_notifier.rb
350
+ module IosNotifier
351
+ extend ActiveSupport::Concern
352
+
353
+ included do
354
+ deliver_by :ios do |config|
355
+ config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) }
356
+ config.format = ->(apn) {
357
+ apn.alert = "Hello world"
358
+ apn.custom_payload = {url: root_url(host: "example.org")}
359
+ }
360
+ config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_id)
361
+ config.key_id = Rails.application.credentials.dig(:ios, :key_id)
362
+ config.team_id = Rails.application.credentials.dig(:ios, :team_id)
363
+ config.apns_key = Rails.application.credentials.dig(:ios, :apns_key)
364
+ config.if = -> { recipient.ios_notifications? }
365
+ end
366
+ end
367
+ end
368
+ ```
193
369
 
194
- private
370
+ #### Shared Delivery Method Options
195
371
 
196
- def notify_recipient
197
- NewMessageNotifier.with(message: self).deliver_later(recipient)
372
+ Each of these options are available for every delivery method (individual or bulk). The value passed may be a lambda, a symbol that represents a callable method, a symbol value, or a string value.
373
+
374
+ * `config.if` — Intended for a lambda or method; runs after the `wait` if configured; cancels the delivery method if returns falsey
375
+ * `config.unless` — Intended for a lambda or method; runs after the `wait` if configured; cancels the delivery method if returns truthy
376
+
377
+ The following are evaluated in the context of the Notification so it can be customize to the recipient:
378
+
379
+ * `config.wait` — (Should yield an `ActiveSupport::Duration`) Delays the job that runs this delivery method for the given duration of time
380
+ * `config.wait_until` — (Should yield a specific time object) Delays the job that runs this delivery method until the specific time specified
381
+ * `config.queue` — Sets the ActiveJob queue name to be used for the job that runs this delivery method.
382
+
383
+ When using a symbol, the matching method must be defined inside the `notification_methods` block:
384
+
385
+ ```ruby
386
+ # app/notifiers/message_notifier.rb
387
+ class MessageNotifier < Noticed::Event
388
+ deliver_by :delivery_method do |config|
389
+ config.queue = :queue
198
390
  end
391
+
392
+ notification_methods do
393
+ def queue
394
+ "high_priority"
395
+ end
396
+ end
397
+ end
199
398
  ```
200
399
 
201
- Using `after_create` might cause the notification delivery methods to fail. This is because the job was enqueued while inside a database transaction, and the `Message` record might not yet be saved to the database.
400
+ ### 📨 Sending Notifications
202
401
 
203
- A common symptom of this problem is undelivered notifications and the following error in your logs.
402
+ 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:
204
403
 
205
- > `Discarded Noticed::DeliveryMethods::Email due to a ActiveJob::DeserializationError.`
404
+ ```ruby
405
+ NewCommentNotifier.with(record: @comment, foo: "bar").deliver(@comment.thread.all_authors)
406
+ ```
206
407
 
207
- ### Renaming Notifiers
408
+ This instantiates a new `NewCommentNotifier` with params (similar to ActiveJob, any serializable params are permitted), then delivers notifications to all authors in the thread.
409
+
410
+ ✨ 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”.
208
411
 
209
- If you rename the class of a notification object your existing queries can break. This is because ActiveRecord serializes the class name and sets it to the `type` column on the Noticed records.
412
+ 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:
210
413
 
211
- You can catch these errors at runtime by using `YourNotifierClassName.name` instead of hardcoding the string when performing a query.
414
+ - A bulk delivery job for `:discord` bulk delivery
415
+ - An individual delivery job for `:action_cable` method to the first thread author
416
+ - An individual delivery job for `:email` method to the first thread author
417
+ - An individual delivery job for `:action_cable` method to the second thread author
418
+ - An individual delivery job for `:email` method to the second thread author
419
+ - Etc...
420
+
421
+ #### Tip: Define recipients inside the notifier
422
+
423
+ Recipients can also be computed inside a notifier:
212
424
 
213
425
  ```ruby
214
- Noticed::Event.where(type: YourNotifierClassName.name) # good
215
- Noticed::Event.where(type: "YourNotifierClassName") # bad
426
+ class NewCommentNotifier < ApplicationNotifier
427
+ recipients ->{ params[:record].thread.all_authors }
428
+
429
+ # or
430
+ recipients do
431
+ params[:record].thread.all_authors
432
+ end
433
+
434
+ # or
435
+ recipients :fetch_recipients
436
+
437
+ def fetch_recipients
438
+ # ...
439
+ end
440
+ end
216
441
  ```
217
442
 
218
- When renaming a notification class you will need to backfill existing notifications to reference the new name.
443
+ This makes the code for sending a notification neater:
444
+
445
+ ```ruby
446
+ NewCommentNotifier.with(record: @comment, foo: "bar").deliver
447
+ ```
448
+
449
+ ### Custom Noticed Model Methods
450
+
451
+ In order to extend the Noticed models you'll need to use a concern and a to_prepare block:
452
+
453
+ ```ruby
454
+ # config/initializers/noticed.rb
455
+ module NotificationExtensions
456
+ extend ActiveSupport::Concern
457
+
458
+ included do
459
+ belongs_to :organization
460
+
461
+ scope :filter_by_type, ->(type) { where(type:) }
462
+ scope :exclude_type, ->(type) { where.not(type:) }
463
+ end
464
+
465
+ # You can also add instance methods here
466
+ end
467
+
468
+ Rails.application.config.to_prepare do
469
+ # You can extend Noticed::Event or Noticed::Notification here
470
+ Noticed::Event.include EventExtensions
471
+ Noticed::Notification.include NotificationExtensions
472
+ end
473
+ ```
474
+
475
+ The `NotificationExtensions` class could be separated into it's own file and live somewhere like `app/models/concerns/notification_extensions.rb`.
476
+
477
+ If you do this, the `to_prepare` block will need to be in `application.rb` instead of an initializer.
478
+
479
+ ```ruby
480
+ # config/application.rb
481
+ module MyApp
482
+ class Application < Rails::Application
483
+
484
+ # ...
485
+
486
+ config.to_prepare do
487
+ Noticed::Event.include EventExtensions
488
+ Noticed::Notification.include NotificationExtensions
489
+ end
490
+ end
491
+ end
492
+ ```
493
+
494
+ #### Tip: Custom properties on a notification per recipient
495
+
496
+ In order to have recipient-specific settings on the notification, override the `recipient_attributes_for(recipient)` method in your notifier:
497
+
498
+ ```ruby
499
+ class CommentNotifier < ApplicationNotifier
500
+ #...
501
+ def recipient_attributes_for(recipient)
502
+ data = super
503
+ data[:priority] = if recipient.participant?
504
+ "high"
505
+ else
506
+ "low"
507
+ end
508
+ data
509
+ end
510
+ end
511
+ ```
512
+
513
+ Assuming you have a `priority` column in the `noticed_notifications` table, it will be set to the value from the hash here. The default properties of the hash are `recipient_type` and `recipient_id`.
514
+
515
+ ## ✅ Best Practices
516
+
517
+ ### Renaming Notifiers
518
+
519
+ 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.
520
+
521
+ When renaming a Notifier class you will need to backfill existing Events and Notifications to reference the new name.
219
522
 
220
523
  ```ruby
221
524
  Noticed::Event.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name)
222
- Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: NewNotifierClassName::Notification.name)
525
+ # and
526
+ Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: "#{NewNotifierClassName.name}::Notification")
223
527
  ```
224
528
 
225
529
  ## 🚛 Delivery Methods
226
530
 
227
- The delivery methods are modular so you can customize the way each type gets delivered.
531
+ The delivery methods are designed to be modular so you can customize the way each type gets delivered.
228
532
 
229
533
  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.
230
534
 
231
- ### Fallback Notifications
535
+ Individual delivery methods:
232
536
 
233
- A common pattern is to deliver a notification via the database and 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 `delay` option, and the conditional `if` / `unless` option.
537
+ * [ActionCable](docs/delivery_methods/action_cable.md)
538
+ * [Apple Push Notification Service](docs/delivery_methods/ios.md)
539
+ * [Email](docs/delivery_methods/email.md)
540
+ * [Firebase Cloud Messaging](docs/delivery_methods/fcm.md) (iOS, Android, and web clients)
541
+ * [Microsoft Teams](docs/delivery_methods/microsoft_teams.md)
542
+ * [Slack](docs/delivery_methods/slack.md)
543
+ * [Twilio Messaging](docs/delivery_methods/twilio_messaging.md) - SMS, Whatsapp
544
+ * [Vonage SMS](docs/delivery_methods/vonage_sms.md)
545
+ * [Test](docs/delivery_methods/test.md)
234
546
 
235
- ```ruby
236
- class CommentNotifier< Noticed::Base
237
- deliver_by :database
238
- deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
239
- end
547
+ Bulk delivery methods:
548
+
549
+ * [Bluesky](docs/bulk_delivery_methods/bluesky.md)
550
+ * [Discord](docs/bulk_delivery_methods/discord.md)
551
+ * [Slack](docs/bulk_delivery_methods/slack.md)
552
+ * [Webhook](docs/bulk_delivery_methods/webhook.md)
553
+
554
+ ### No Delivery Methods
555
+
556
+ 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.
557
+
558
+ So even with no delivery methods set, this example is still perfectly available and helpful:
559
+
560
+ ```erb
561
+ <div>
562
+ <% @user.notifications.each do |notification| %>
563
+ <%= link_to notification.message, notification.url %>
564
+ <% end %>
565
+ </div>
240
566
  ```
241
567
 
242
- Here a notification will be created immediately in the database (for display directly in your app). If the notification has not been 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.
568
+ 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.
243
569
 
244
- You can also configure multiple fallback options:
570
+ ### Fallback Notifications
571
+
572
+ 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.
245
573
 
246
574
  ```ruby
247
- class CriticalSystemNotifier < Noticed::Base
248
- deliver_by :database
249
- deliver_by :slack
250
- deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, if: :unread?
251
- deliver_by :twilio, delay: 20.minutes, if: :unread?
575
+ class NewCommentNotifier < Noticed::Event
576
+ deliver_by :action_cable
577
+ deliver_by :email do |config|
578
+ config.mailer = "CommentMailer"
579
+ config.wait = 15.minutes
580
+ config.unless = -> { read? }
581
+ end
252
582
  end
253
583
  ```
254
584
 
255
- In this scenario, you have created an escalating notification system that
585
+ 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.
256
586
 
257
- - Immediately creates a record in the database (for display directly in the app)
258
- - Immediately issues a ping in Slack.
259
- - If the notification remains unread after 10 minutes, it emails the team.
260
- - If the notification remains unread after 20 minutes, it sends an SMS to the on-call phone.
587
+ _A note here: notifications expose a `#mark_as_read` method, but your app must choose when and where to call that method._
261
588
 
262
589
  You can mix and match the options and delivery methods to suit your application specific needs.
263
590
 
264
- Please note that to implement this pattern, it is essential `deliver_by :database` is one among the different delivery methods specified. Without this, a database record of the notification will not be created.
265
-
266
591
  ### 🚚 Custom Delivery Methods
267
592
 
268
- To generate a custom delivery method, simply run
593
+ If you want to build your own delivery method to deliver notifications to a specific service or medium that Noticed doesn’t (or doesn’t _yet_) support, you’re welcome to do so! To generate a custom delivery method, simply run
269
594
 
270
- `rails generate noticed:delivery_method Discord`
595
+ ```bash
596
+ rails generate noticed:delivery_method Discord
597
+ ```
271
598
 
272
- This will generate a new `DeliveryMethods::Discord` class inside the `app/notifications/delivery_methods` folder, which can be used to deliver notifications to Discord.
599
+ This will generate a new `ApplicationDeliveryMethod` and `DeliveryMethods::Discord` class inside the `app/notifiers/delivery_methods` folder, which can be used to deliver notifications to Discord.
273
600
 
274
601
  ```ruby
275
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
602
+ class DeliveryMethods::Discord < ApplicationDeliveryMethod
603
+ # Specify the config options your delivery method requires in its config block
604
+ required_options # :foo, :bar
605
+
276
606
  def deliver
277
- # Logic for sending a Discord notification
607
+ # Logic for sending the notification
278
608
  end
279
609
  end
280
610
  ```
@@ -282,79 +612,146 @@ end
282
612
  You can use the custom delivery method thus created by adding a `deliver_by` line with a unique name and `class` option in your notification class.
283
613
 
284
614
  ```ruby
285
- class MyNotifier < Noticed::Base
615
+ class MyNotifier < Noticed::Event
286
616
  deliver_by :discord, class: "DeliveryMethods::Discord"
287
617
  end
288
618
  ```
289
619
 
290
- Delivery methods have access to the following methods and attributes:
620
+ You can also generate bulk delivery methods with the `--bulk` flag:
621
+
622
+ ```bash
623
+ rails generate noticed:delivery_method Discord --bulk
624
+ ```
291
625
 
292
- * `record` - The instance of the Notification. You can call methods on the notification to let the user easily override formatting and other functionality of the delivery method.
293
- * `options` - Any configuration options on the `deliver_by` line.
294
- * `recipient` - The object who should receive the notification. This is typically a User, Account, or other ActiveRecord model.
295
- * `params` - The params passed into the notification. This is details about the event that happened. For example, a user commenting on a post would have params of `{ user: User.first }`
626
+ <details>
627
+ <summary>Turbo Stream Custom Delivery Method Example</summary>
296
628
 
297
- #### Validating options passed to Custom Delivery methods
629
+ A common custom delivery method in the Rails world might be to Delivery to the web via turbo stream.
298
630
 
299
- The presence of the delivery method options is automatically validated if using the `option(s)` method.
631
+ Note: This example uses custom methods that extend the `Noticed::Notification` class.
300
632
 
301
- If you want to validate that the passed options contain valid values, or to add any custom validations, override the `self.validate!(delivery_method_options)` method from the `Noticed::DeliveryMethods::Base` class.
633
+ See the [Custom Noticed Model Methods](#custom-noticed-model-methods) section for more information.
302
634
 
303
635
  ```ruby
304
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
305
- option :username # Requires the username option to be passed
636
+ # app/notifiers/delivery_methods/turbo_stream.rb
306
637
 
638
+ class DeliveryMethods::TurboStream < ApplicationDeliveryMethod
307
639
  def deliver
308
- # Logic for sending a Discord notification
640
+ return unless recipient.is_a?(User)
641
+
642
+ notification.broadcast_update_to_bell
643
+ notification.broadcast_replace_to_index_count
644
+ notification.broadcast_prepend_to_index_list
309
645
  end
646
+ end
647
+ ```
310
648
 
311
- def self.validate!(delivery_method_options)
312
- super # Don't forget to call super, otherwise option presence won't be validated
649
+ ```ruby
650
+ # app/models/concerns/noticed/notification_extensions.rb
651
+
652
+ module Noticed::NotificationExtensions
653
+ extend ActiveSupport::Concern
654
+
655
+ def broadcast_update_to_bell
656
+ broadcast_update_to(
657
+ "notifications_#{recipient.id}",
658
+ target: "notification_bell",
659
+ partial: "navbar/notifications/bell",
660
+ locals: { user: recipient }
661
+ )
662
+ end
313
663
 
314
-   # Custom validations
315
- if delivery_method_options[:username].blank?
316
- raise Noticed::ValidationError, 'the `username` option must be present'
317
- end
664
+ def broadcast_replace_to_index_count
665
+ broadcast_replace_to(
666
+ "notifications_index_#{recipient.id}",
667
+ target: "notification_index_count",
668
+ partial: "notifications/notifications_count",
669
+ locals: { count: recipient.reload.notifications_count, unread: recipient.reload.unread_notifications_count }
670
+ )
318
671
  end
319
- end
320
672
 
321
- class CommentNotifier < Noticed::Base
322
- deliver_by :discord, class: 'DeliveryMethods::Discord'
673
+ def broadcast_prepend_to_index_list
674
+ broadcast_prepend_to(
675
+ "notifications_index_list_#{recipient.id}",
676
+ target: "notifications",
677
+ partial: "notifications/notification",
678
+ locals: { notification: self }
679
+ )
680
+ end
323
681
  end
324
682
  ```
683
+ </details>
684
+
685
+ Delivery methods have access to the following methods and attributes:
325
686
 
326
- Now it will raise an error because a required argument is missing.
687
+ * `event` The `Noticed::Event` record that spawned the notification object currently being delivered
688
+ * `record` — The object originally passed into the Notifier as the `record:` param (see the ✨ note above)
689
+ * `notification` — The `Noticed::Notification` instance being delivered. All notification helper methods are available on this object
690
+ * `recipient` — The individual recipient object being delivered to for this notification (remember that each recipient gets their own instance of the Delivery Method `#deliver`)
691
+ * `config` — The hash of configuration options declared by the Notifier that generated this notification and delivery
692
+ * `params` — The parameters given to the Notifier in the invocation (via `.with()`)
327
693
 
328
- To fix the error, the argument has to be passed correctly. For example:
694
+ #### Validating config options passed to Custom Delivery methods
695
+
696
+ The presence of delivery method config options are automatically validated when declaring them with the `required_options` method. In the following example, Noticed will ensure that any Notifier using `deliver_by :email` will specify the `mailer` and `method` config keys:
329
697
 
330
698
  ```ruby
331
- class CommentNotifier < Noticed::Base
332
- deliver_by :discord, class: 'DeliveryMethods::Discord', username: User.admin.username
699
+ class DeliveryMethods::Email < Noticed::DeliveryMethod
700
+ required_options :mailer, :method
701
+
702
+ def deliver
703
+ # ...
704
+ method = config.method
705
+ end
706
+ end
707
+ ```
708
+
709
+ If you’d like your config options to support dynamic resolution (set `config.foo` to a lambda or symbol of a method name etc.), you can use `evaluate_option`:
710
+
711
+ ```ruby
712
+ class NewSaleNotifier < Noticed::Event
713
+ deliver_by :whats_app do |config|
714
+ config.day = -> { is_tuesday? "Tuesday" : "Not Tuesday" }
715
+ end
716
+ end
717
+
718
+ class DeliveryMethods::WhatsApp < Noticed::DeliveryMethod
719
+ required_options :day
720
+
721
+ def deliver
722
+ # ...
723
+ config.day #=> #<Proc:0x000f7c8 (lambda)>
724
+ evaluate_option(:day) #=> "Tuesday"
725
+ end
333
726
  end
334
727
  ```
335
728
 
336
729
  #### Callbacks
337
730
 
338
- 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.
731
+ 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.
339
732
 
340
733
  ```ruby
341
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
734
+ class DeliveryMethods::Discord < Noticed::DeliveryMethod
342
735
  after_deliver do
343
736
  # Do whatever you want
344
737
  end
345
738
  end
346
739
  ```
347
740
 
348
- ### 📦 Database Model
741
+ ## 📦 Database Model
349
742
 
350
- The Notification database model includes several helpful features to make working with database notifications easier.
743
+ The Noticed database models include several helpful features to make working with notifications easier.
351
744
 
352
- #### Class methods
745
+ ### Notification
746
+
747
+ #### Class methods/scopes
748
+
749
+ (Assuming your user `has_many :notifications, as: :recipient, class_name: "Noticed::Notification"`)
353
750
 
354
751
  Sorting notifications by newest first:
355
752
 
356
753
  ```ruby
357
- user.notifications.newest_first
754
+ @user.notifications.newest_first
358
755
  ```
359
756
 
360
757
  Query for read or unread notifications:
@@ -364,26 +761,21 @@ user.notifications.read
364
761
  user.notifications.unread
365
762
  ```
366
763
 
367
-
368
764
  Marking all notifications as read or unread:
369
765
 
370
766
  ```ruby
371
- user.notifications.mark_as_read!
372
- user.notifications.mark_as_unread!
767
+ user.notifications.mark_as_read
768
+ user.notifications.mark_as_unread
373
769
  ```
374
770
 
375
771
  #### Instance methods
376
772
 
377
- Convert back into a Noticed notifier object:
378
-
379
- ```ruby
380
- @notification.to_notifier
381
- ```
382
-
383
773
  Mark notification as read / unread:
384
774
 
385
775
  ```ruby
776
+ @notification.mark_as_read
386
777
  @notification.mark_as_read!
778
+ @notification.mark_as_unread
387
779
  @notification.mark_as_unread!
388
780
  ```
389
781
 
@@ -396,41 +788,45 @@ Check if read / unread:
396
788
 
397
789
  #### Associating Notifications
398
790
 
399
- Adding notification associations to your models makes querying and deleting notifications easy and is a pretty critical feature of most applications.
791
+ Adding notification associations to your models makes querying, rendering, and managing notifications easy (and is a pretty critical feature of most applications).
400
792
 
401
- For example, in most cases, you'll want to delete notifications for records that are destroyed.
793
+ There are two ways to associate your models to notifications:
402
794
 
403
- We'll need two associations for this:
795
+ 1. Where your object `has_many` notifications as the recipient (who you sent the notification to)
796
+ 2. Where your object `has_many` notifications as the `record` (what the notifications were about)
404
797
 
405
- 1. Notifications where the record is the recipient
406
- 2. Notifications where the record is in the notification params
798
+ 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).
407
799
 
408
- For example, we can query the notifications and delete them on destroy like so:
800
+ We can illustrate that in the following:
409
801
 
410
802
  ```ruby
411
- class Post < ApplicationRecord
412
- # Standard association for deleting notifications when you're the recipient
413
- has_many :notifications, as: :recipient, dependent: :destroy
803
+ class User < ApplicationRecord
804
+ has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
805
+ end
414
806
 
415
- # Helper for associating and destroying Notification records where(params: {post: self})
416
- has_noticed_notifications
807
+ # All of the notifications the user has been sent
808
+ # @user.notifications.each { |n| render(n) }
417
809
 
418
- # You can override the param_name, the notification model name, or disable the before_destroy callback
419
- has_noticed_notifications param_name: :parent, destroy: false, model_name: "Notification"
810
+ class Post < ApplicationRecord
811
+ has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
812
+ has_many :notifications, through: :noticed_events, class_name: "Noticed::Notification"
420
813
  end
421
814
 
422
- # Create a CommentNotification with a post param
423
- CommentNotifier.with(post: @post).deliver(user)
424
- # Lookup Notifications where params: {post: @post}
425
- @post.notifications_as_post
815
+ # All of the notification events this post generated
816
+ # @post.notifications
817
+ ```
818
+
819
+ #### ActiveJob Parent Class
426
820
 
427
- CommentNotifier.with(parent: @post).deliver(user)
428
- @post.notifications_as_parent
821
+ 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.
822
+
823
+ ```ruby
824
+ Noticed.parent_class = "ApplicationJob"
429
825
  ```
430
826
 
431
827
  #### Handling Deleted Records
432
828
 
433
- If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discard the job on these errors by adding the following to `ApplicationJob`:
829
+ 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.
434
830
 
435
831
  ```ruby
436
832
  class ApplicationJob < ActiveJob::Base
@@ -438,6 +834,48 @@ class ApplicationJob < ActiveJob::Base
438
834
  end
439
835
  ```
440
836
 
837
+ ### Customizing the Database Models
838
+
839
+ You can modify the database models by editing the generated migrations.
840
+
841
+ One common adjustment is to change the IDs to UUIDs (if you're using UUIDs in your app).
842
+
843
+ You can also add additional columns to the `Noticed::Event` and `Noticed::Notification` models.
844
+
845
+ ```ruby
846
+ # This migration comes from noticed (originally 20231215190233)
847
+ class CreateNoticedTables < ActiveRecord::Migration[7.1]
848
+ def change
849
+ create_table :noticed_events, id: :uuid do |t|
850
+ t.string :type
851
+ t.belongs_to :record, polymorphic: true, type: :uuid
852
+ t.jsonb :params
853
+
854
+ # Custom Fields
855
+ t.string :organization_id, type: :uuid, as: "((params ->> 'organization_id')::uuid)", stored: true
856
+ t.virtual :action_type, type: :string, as: "((params ->> 'action_type'))", stored: true
857
+ t.virtual :url, type: :string, as: "((params ->> 'url'))", stored: true
858
+
859
+ t.timestamps
860
+ end
861
+
862
+ create_table :noticed_notifications, id: :uuid do |t|
863
+ t.string :type
864
+ t.belongs_to :event, null: false, type: :uuid
865
+ t.belongs_to :recipient, polymorphic: true, null: false, type: :uuid
866
+ t.datetime :read_at
867
+ t.datetime :seen_at
868
+
869
+ t.timestamps
870
+ end
871
+
872
+ add_index :noticed_notifications, :read_at
873
+ end
874
+ end
875
+ ```
876
+
877
+ 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.
878
+
441
879
  ## 🙏 Contributing
442
880
 
443
881
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
@@ -446,9 +884,10 @@ Running tests against multiple databases locally:
446
884
 
447
885
  ```
448
886
  DATABASE_URL=sqlite3:noticed_test rails test
449
- DATABASE_URL=mysql2://root:@127.0.0.1/noticed_test rails test
887
+ DATABASE_URL=trilogy://root:@127.0.0.1/noticed_test rails test
450
888
  DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
451
889
  ```
452
890
 
453
891
  ## 📝 License
892
+
454
893
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).