noticed 1.6.3 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -237
  3. data/app/jobs/noticed/application_job.rb +9 -0
  4. data/app/jobs/noticed/event_job.rb +19 -0
  5. data/app/models/concerns/noticed/deliverable.rb +115 -0
  6. data/app/models/concerns/noticed/notification_methods.rb +17 -0
  7. data/app/models/concerns/noticed/readable.rb +78 -0
  8. data/app/models/noticed/application_record.rb +6 -0
  9. data/app/models/noticed/deliverable/deliver_by.rb +43 -0
  10. data/app/models/noticed/event.rb +15 -0
  11. data/app/models/noticed/notification.rb +14 -0
  12. data/db/migrate/20231215190233_create_noticed_tables.rb +25 -0
  13. data/lib/generators/noticed/delivery_method_generator.rb +1 -1
  14. data/lib/generators/noticed/install_generator.rb +19 -0
  15. data/lib/generators/noticed/{notification_generator.rb → notifier_generator.rb} +2 -2
  16. data/lib/generators/noticed/templates/README +5 -4
  17. data/lib/generators/noticed/templates/delivery_method.rb.tt +4 -8
  18. data/lib/generators/noticed/templates/notifier.rb.tt +24 -0
  19. data/lib/noticed/api_client.rb +44 -0
  20. data/lib/noticed/bulk_delivery_method.rb +46 -0
  21. data/lib/noticed/bulk_delivery_methods/discord.rb +11 -0
  22. data/lib/noticed/bulk_delivery_methods/slack.rb +17 -0
  23. data/lib/noticed/bulk_delivery_methods/webhook.rb +18 -0
  24. data/lib/noticed/coder.rb +2 -0
  25. data/lib/noticed/delivery_method.rb +51 -0
  26. data/lib/noticed/delivery_methods/action_cable.rb +7 -39
  27. data/lib/noticed/delivery_methods/discord.rb +11 -0
  28. data/lib/noticed/delivery_methods/email.rb +13 -45
  29. data/lib/noticed/delivery_methods/fcm.rb +23 -64
  30. data/lib/noticed/delivery_methods/ios.rb +25 -112
  31. data/lib/noticed/delivery_methods/microsoft_teams.rb +5 -22
  32. data/lib/noticed/delivery_methods/slack.rb +6 -16
  33. data/lib/noticed/delivery_methods/test.rb +2 -12
  34. data/lib/noticed/delivery_methods/twilio_messaging.rb +37 -0
  35. data/lib/noticed/delivery_methods/vonage_sms.rb +20 -0
  36. data/lib/noticed/delivery_methods/webhook.rb +17 -0
  37. data/lib/noticed/engine.rb +1 -9
  38. data/lib/noticed/required_options.rb +21 -0
  39. data/lib/noticed/translation.rb +7 -3
  40. data/lib/noticed/version.rb +1 -1
  41. data/lib/noticed.rb +30 -15
  42. metadata +29 -40
  43. data/lib/generators/noticed/model/base_generator.rb +0 -47
  44. data/lib/generators/noticed/model/mysql_generator.rb +0 -18
  45. data/lib/generators/noticed/model/postgresql_generator.rb +0 -18
  46. data/lib/generators/noticed/model/sqlite3_generator.rb +0 -18
  47. data/lib/generators/noticed/model_generator.rb +0 -63
  48. data/lib/generators/noticed/templates/notification.rb.tt +0 -27
  49. data/lib/noticed/base.rb +0 -160
  50. data/lib/noticed/delivery_methods/base.rb +0 -95
  51. data/lib/noticed/delivery_methods/database.rb +0 -34
  52. data/lib/noticed/delivery_methods/twilio.rb +0 -51
  53. data/lib/noticed/delivery_methods/vonage.rb +0 -40
  54. data/lib/noticed/has_notifications.rb +0 -49
  55. data/lib/noticed/model.rb +0 -85
  56. data/lib/noticed/notification_channel.rb +0 -15
  57. data/lib/noticed/text_coder.rb +0 -16
  58. data/lib/rails_6_polyfills/actioncable/test_adapter.rb +0 -70
  59. data/lib/rails_6_polyfills/actioncable/test_helper.rb +0 -143
  60. data/lib/rails_6_polyfills/activejob/serializers.rb +0 -240
  61. data/lib/rails_6_polyfills/base.rb +0 -18
  62. data/lib/tasks/noticed_tasks.rake +0 -4
data/README.md CHANGED
@@ -1,385 +1,419 @@
1
- <p align="center">
2
- <h1>Noticed</h1>
3
- </p>
1
+ # Noticed
4
2
 
5
3
  ### 🎉 Notifications for your Ruby on Rails app.
6
4
 
7
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)
8
6
 
9
- Currently, we support these notification delivery methods out of the box:
7
+ **⚠️⚠️ Upgrading from V1? Read the [Upgrade Guide](https://github.com/excid3/noticed/blob/main/UPGRADE.md)!**
10
8
 
11
- * Database
12
- * Email
13
- * ActionCable channels
14
- * Slack
15
- * Microsoft Teams
16
- * Twilio (SMS)
17
- * Vonage / Nexmo (SMS)
18
- * iOS Apple Push Notifications
19
- * Firebase Cloud Messaging (Android and more)
9
+ 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)!
20
10
 
21
- And you can easily add new notification types for any other delivery methods.
11
+ Noticed implements two top-level types of delivery methods:
12
+
13
+ 1. Individual Deliveries: Where each recipient gets their own notification
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
+
17
+ 2. Bulk Deliveries - one notification for all recipients. This is useful for sending a notification to your Slack team, for example.
18
+
19
+ 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.
20
+
21
+ Bulk deliveries are typically used to push notifications to other platforms where users are managed (Slack, Discord, etc.) instead of your own.
22
+
23
+ Delivery methods we officially support:
24
+
25
+ * [ActionCable](docs/delivery_methods/action_cable.md)
26
+ * [Apple Push Notification Service](docs/delivery_methods/ios.md)
27
+ * [Email](docs/delivery_methods/email.md)
28
+ * [Firebase Cloud Messaging](docs/delivery_methods/fcm.md) (iOS, Android, and web clients)
29
+ * [Microsoft Teams](docs/delivery_methods/microsoft_teams.md)
30
+ * [Slack](docs/delivery_methods/slack.md)
31
+ * [Twilio Messaging](docs/delivery_methods/twilio_messaging.md) - SMS, Whatsapp
32
+ * [Vonage SMS](docs/delivery_methods/vonage_sms.md)
33
+ * [Test](docs/delivery_methods/test.md)
34
+
35
+ Bulk delivery methods we support:
36
+
37
+ * [Discord](docs/bulk_delivery_methods/discord.md)
38
+ * [Slack](docs/bulk_delivery_methods/slack.md)
39
+ * [Webhook](docs/bulk_delivery_methods/webhook.md)
22
40
 
23
41
  ## 🎬 Screencast
24
42
 
25
- <div style="width:50%">
26
- <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" /></a>
27
- </div>
43
+ <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>
28
44
 
29
- [Watch Screencast](https://www.youtube.com/watch?v=Scffi4otlFc)
45
+ [Watch Screencast](https://www.youtube.com/watch?v=SzX-aBEqnAc)
30
46
 
31
47
  ## 🚀 Installation
32
- Run the following command to add Noticed to your Gemfile
48
+ Run the following command to add Noticed to your Gemfile:
33
49
 
34
50
  ```ruby
35
51
  bundle add "noticed"
36
52
  ```
37
53
 
38
- To save notifications to your database, use the following command to generate a Notification model.
54
+ Generate then run the migrations:
39
55
 
40
- ```ruby
41
- rails generate noticed:model
56
+ ```bash
57
+ rails noticed:install:migrations
58
+ rails db:migrate
42
59
  ```
43
60
 
44
- This will generate a Notification model and instructions for associating User models with the notifications table.
45
-
46
61
  ## 📝 Usage
47
62
 
48
- To generate a notification object, simply run:
63
+ Noticed operates with a few constructs: Notifiers, delivery methods, and Notification records.
49
64
 
50
- `rails generate noticed:notification CommentNotification`
65
+ To start, generate a Notifier:
51
66
 
52
- #### Sending Notifications
67
+ ```sh
68
+ rails generate noticed:notifier NewCommentNotifier
69
+ ```
70
+
71
+ #### Notifier Objects
53
72
 
54
- To send a notification to a user:
73
+ 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.
74
+
75
+ Notifiers must inherit from `Noticed::Event`. This provides all of their functionality.
76
+
77
+ 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.
55
78
 
56
79
  ```ruby
57
- # Instantiate a new notification
58
- notification = CommentNotification.with(comment: @comment)
80
+ # ~/app/notifiers/new_comment_notifier.rb
59
81
 
60
- # Deliver notification in background job
61
- notification.deliver_later(@comment.post.author)
82
+ class NewCommentNotifier < Noticed::Event
83
+ deliver_by :action_cable do |config|
84
+ config.channel = "NotificationsChannel"
85
+ config.stream = :some_stream
86
+ end
62
87
 
63
- # Deliver notification immediately
64
- notification.deliver(@comment.post.author)
88
+ deliver_by :email do |config|
89
+ config.mailer = "CommentMailer"
90
+ config.if = ->(recipient) { !!recipient.preferences[:email] }
91
+ end
65
92
 
66
- # Deliver notification to multiple recipients
67
- notification.deliver_later(User.all)
68
- ```
93
+ bulk_deliver_by :discord do |config|
94
+ config.url = "https://discord.com/xyz/xyz/123"
95
+ config.json = -> {
96
+ {
97
+ message: message,
98
+ channel: :general
99
+ }
100
+ }
101
+ end
69
102
 
70
- This will instantiate a new notification with the `comment` stored in the notification's params.
103
+ notification_methods do
104
+ # I18n helpers
105
+ def message
106
+ t(".message")
107
+ end
71
108
 
72
- Each delivery method is able to transform this metadata that's best for the format. For example, the database may simply store the comment so it can be linked when rendering in the navbar. The websocket mechanism may transform this into a browser notification or insert it into the navbar.
109
+ # URL helpers are accessible in notifications
110
+ # Don't forget to set your default_url_options so Rails knows how to generate urls
111
+ def url
112
+ user_post_path(recipient, params[:post])
113
+ end
114
+ end
115
+ end
116
+ ```
73
117
 
74
- #### Notification Objects
118
+ 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).
75
119
 
76
- Notifications inherit from `Noticed::Base`. This provides all their functionality and allows them to be delivered.
120
+ ##### Required Params
77
121
 
78
- To add delivery methods, simply `include` the module for the delivery methods you would like to use.
122
+ While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the `required_params` method:
79
123
 
80
124
  ```ruby
81
- class CommentNotification < Noticed::Base
82
- deliver_by :database
83
- deliver_by :action_cable
84
- deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
125
+ class CarSaleNotifier < Noticed::Event
126
+ deliver_by :email { |c| c.mailer = "BranchMailer" }
85
127
 
86
- # I18n helpers
87
- def message
88
- t(".message")
89
- end
128
+ # `record` is the Car record, `Branch` is the dealership
129
+ required_params :record, :branch
130
+ end
131
+ ```
90
132
 
91
- # URL helpers are accessible in notifications
92
- # Don't forget to set your default_url_options so Rails knows how to generate urls
93
- def url
94
- post_path(params[:post])
95
- end
133
+ Which will validate upon any invocation that the specified parameters are present:
96
134
 
97
- def email_notifications?
98
- !!recipient.preferences[:email]
99
- end
135
+ ```ruby
136
+ CarSaleNotifier.with(record: Car.last).deliver(Branch.last)
137
+ #=> Noticed::ValidationError("Param `branch` is required for CarSaleNotifier")
100
138
 
101
- after_deliver do
102
- # Anything you want
103
- end
104
- end
139
+ CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
140
+ #=> OK
105
141
  ```
106
142
 
107
- **Shared Options**
108
143
 
109
- * `if: :method_name` - Calls `method_name` and cancels delivery method if `false` is returned
110
- * `unless: :method_name` - Calls `method_name` and cancels delivery method if `true` is returned
111
- * `delay: ActiveSupport::Duration` - Delays the delivery for the given duration of time
112
- * `delay: :method_name` - Calls `method_name` which should return an `ActiveSupport::Duration` and delays the delivery for the given duration of time
113
144
 
114
145
  ##### Helper Methods
115
146
 
116
- You can define helper methods inside your Notification object to make it easier to render.
147
+ 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:
148
+
149
+ ```erb
150
+ <div>
151
+ <% @user.notifications.each do |notification| %>
152
+ <%= link_to notification.message, notification.url %>
153
+ <% end %>
154
+ </div>
155
+ ```
117
156
 
118
- ##### URL Helpers
157
+ 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.
119
158
 
120
- Rails url helpers are included in notification classes by default so you have full access to them just like you would in your controllers and views.
159
+ ###### URL Helpers
121
160
 
122
- 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.
161
+ 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.
162
+
163
+ _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.
123
164
 
124
165
  ```ruby
125
166
  Rails.application.routes.default_url_options[:host] = 'localhost:3000'
126
167
  ```
127
168
 
128
- **Callbacks**
169
+ ###### Translations
170
+
171
+ 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:
129
172
 
130
- Like ActiveRecord, notifications have several different types of callbacks.
173
+ From the above Notifier...
131
174
 
132
175
  ```ruby
133
- class CommentNotification < Noticed::Base
134
- deliver_by :database
135
- deliver_by :email, mailer: 'CommentMailer'
136
-
137
- # Callbacks for the entire delivery
138
- before_deliver :whatever
139
- around_deliver :whatever
140
- after_deliver :whatever
141
-
142
- # Callbacks for each delivery method
143
- before_database :whatever
144
- around_database :whatever
145
- after_database :whatever
146
-
147
- before_email :whatever
148
- around_email :whatever
149
- after_email :whatever
176
+ class NewCommentNotifier < Noticed::Event
177
+ # ...
178
+
179
+ notification_methods do
180
+ def message
181
+ t(".message")
182
+ end
183
+ end
184
+
185
+ # ...
150
186
  end
151
187
  ```
152
188
 
153
- When using `deliver_later` callbacks will be run around queuing the delivery method jobs (not inside the jobs as they actually execute).
154
-
155
- Defining custom delivery methods allows you to add callbacks that run inside the background job as each individual delivery is executed. See the Custom Delivery Methods section for more information.
156
-
157
- ##### Translations
189
+ Calling the `message` helper in an ERB view:
158
190
 
159
- We've added `translate` and `t` helpers like Rails has to provide an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under `notifications` and the underscored name of the notification class it is used in.
191
+ ```erb
192
+ <%= @user.notifications.last.message %>
193
+ ```
160
194
 
161
- For example:
195
+ Will look for the following translation path:
162
196
 
163
- `t(".message")` looks up `en.notifications.new_comment.message`
197
+ ```yml
198
+ # ~/config/locales/en.yml
164
199
 
165
- Or when notification class is in module:
200
+ en:
201
+ notifiers:
202
+ new_comment_notifier:
203
+ notification:
204
+ message: "Someone posted a new comment!"
205
+ ```
166
206
 
167
- `t(".message") # in Admin::NewComment` looks up `en.notifications.admin.new_comment.message`
207
+ 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).
168
208
 
169
- ##### User Preferences
209
+ ##### Tip: Capture User Preferences
170
210
 
171
211
  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.
172
212
 
173
213
  For example:
174
214
 
175
215
  ```ruby
176
- class CommentNotification < Noticed::Base
177
- deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
178
-
179
- def email_notifications?
180
- recipient.email_notifications?
216
+ class CommentNotifier < Noticed::Event
217
+ deliver_by :email do |config|
218
+ config.mailer = 'CommentMailer'
219
+ config.method = :new_comment
220
+ config.if = ->{ recipient.email_notifications? }
181
221
  end
182
222
  end
183
223
  ```
184
224
 
185
- ## 🐞 Debugging
186
-
187
- In order to figure out what's up when you run in to errors, you can set the `debug` parameter to `true` in your notification, which will give you a more detailed error message about what went wrong.
225
+ **Shared Delivery Method Options**
188
226
 
189
- Example:
227
+ 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.
190
228
 
191
- ```ruby
192
- deliver_by :slack, debug: true
193
- ```
229
+ * `config.if` — Intended for a lambda or method; runs after the `wait` if configured; cancels the delivery method if returns falsey
230
+ * `config.unless` — Intended for a lambda or method; runs after the `wait` if configured; cancels the delivery method if returns truthy
231
+ * `config.wait` — (Should yield an `ActiveSupport::Duration`) Delays the job that runs this delivery method for the given duration of time
232
+ * `config.wait_until` — (Should yield a specific time object) Delays the job that runs this delivery method until the specific time specified
233
+ * `config.queue` — Sets the ActiveJob queue name to be used for the job that runs this delivery method
194
234
 
195
- ## Best Practices
196
-
197
- ### Creating a notification from an Active Record callback
235
+ #### Sending Notifications
198
236
 
199
- A common use case is to trigger a notification when a record is created. For example,
237
+ 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:
200
238
 
201
239
  ```ruby
202
- class Message < ApplicationRecord
203
- belongs_to :recipient, class_name: "User"
204
-
205
- after_create_commit :notify_recipient
206
-
207
- private
208
-
209
- def notify_recipient
210
- NewMessageNotification.with(message: self).deliver_later(recipient)
211
- end
240
+ NewCommentNotifier.with(record: @comment, foo: "bar").deliver(@comment.thread.all_authors)
212
241
  ```
213
242
 
214
- If you are creating the notification on a background job (i.e. via `#deliver_later`), make sure you use a `commit` hook such as `after_create_commit` or `after_commit`.
215
-
216
- 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.
243
+ This instantiates a new `NewCommentNotifier` with params (similar to ActiveJob, any serializable params are permitted), then delivers notifications to all authors in the thread.
217
244
 
218
- A common symptom of this problem is undelivered notifications and the following error in your logs.
245
+ 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”.
219
246
 
220
- > `Discarded Noticed::DeliveryMethods::Email due to a ActiveJob::DeserializationError.`
247
+ 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:
221
248
 
222
- ### Renaming notifications
249
+ - A bulk delivery job for `:discord` bulk delivery
250
+ - An individual delivery job for `:action_cable` method to the first thread author
251
+ - An individual delivery job for `:email` method to the first thread author
252
+ - An individual delivery job for `:action_cable` method to the second thread author
253
+ - An individual delivery job for `:email` method to the second thread author
254
+ - Etc...
223
255
 
224
- If you rename the class of a notification object your existing queries can break. This is because Noticed serializes the class name and sets it to the `type` column on the `Notification` record.
256
+ ## Best Practices
225
257
 
226
- You can catch these errors at runtime by using `YourNotificationClassName.name` instead of hardcoding the string when performing a query.
258
+ ### Renaming Notifiers
227
259
 
228
- ```ruby
229
- Notification.where(type: YourNotificationClassName.name) # good
230
- Notification.where(type: "YourNotificationClassName") # bad
231
- ```
260
+ 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.
232
261
 
233
- When renaming a notification class you will need to backfill existing notifications to reference the new name.
262
+ When renaming a Notifier class you will need to backfill existing Events and Notifications to reference the new name.
234
263
 
235
264
  ```ruby
236
- Notification.where(type: "OldNotificationClassName").update_all(type: NewNotificationClassName.name)
265
+ Noticed::Event.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name)
266
+ # and
267
+ Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: "#{NewNotifierClassName.name}::Notification")
237
268
  ```
238
269
 
239
270
  ## 🚛 Delivery Methods
240
271
 
241
272
  The delivery methods are designed to be modular so you can customize the way each type gets delivered.
242
273
 
243
- 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 Notification and the delivery method will handle the processing of it.
274
+ 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.
275
+
276
+ Individual delivery methods:
244
277
 
245
- * [Database](docs/delivery_methods/database.md)
246
- * [Email](docs/delivery_methods/email.md)
247
278
  * [ActionCable](docs/delivery_methods/action_cable.md)
248
- * [iOS Apple Push Notifications](docs/delivery_methods/ios.md)
279
+ * [Apple Push Notification Service](docs/delivery_methods/ios.md)
280
+ * [Email](docs/delivery_methods/email.md)
281
+ * [Firebase Cloud Messaging](docs/delivery_methods/fcm.md) (iOS, Android, and web clients)
249
282
  * [Microsoft Teams](docs/delivery_methods/microsoft_teams.md)
250
283
  * [Slack](docs/delivery_methods/slack.md)
284
+ * [Twilio Messaging](docs/delivery_methods/twilio_messaging.md) - SMS, Whatsapp
285
+ * [Vonage SMS](docs/delivery_methods/vonage_sms.md)
251
286
  * [Test](docs/delivery_methods/test.md)
252
- * [Twilio](docs/delivery_methods/twilio.md)
253
- * [Vonage](docs/delivery_methods/vonage.md)
254
- * [Firebase Cloud Messaging](docs/delivery_methods/fcm.md)
255
287
 
256
- ### Fallback Notifications
288
+ Bulk delivery methods:
257
289
 
258
- 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.
290
+ * [Discord](docs/bulk_delivery_methods/discord.md)
291
+ * [Slack](docs/bulk_delivery_methods/slack.md)
292
+ * [Webhook](docs/bulk_delivery_methods/webhook.md)
259
293
 
260
- ```ruby
261
- class CommentNotification < Noticed::Base
262
- deliver_by :database
263
- deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
264
- end
294
+ ### No Delivery Methods
295
+
296
+ 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.
297
+
298
+ So even with no delivery methods set, this example is still perfectly available and helpful:
299
+
300
+ ```erb
301
+ <div>
302
+ <% @user.notifications.each do |notification| %>
303
+ <%= link_to notification.message, notification.url %>
304
+ <% end %>
305
+ </div>
265
306
  ```
266
307
 
267
- 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.
308
+ 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.
309
+
310
+ ### Fallback Notifications
268
311
 
269
- You can also configure multiple fallback options:
312
+ 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.
270
313
 
271
314
  ```ruby
272
- class CriticalSystemNotification < Noticed::Base
273
- deliver_by :database
274
- deliver_by :slack
275
- deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, if: :unread?
276
- deliver_by :twilio, delay: 20.minutes, if: :unread?
315
+ class NewCommentNotifier< Noticed::Event
316
+ deliver_by :action_cable
317
+ deliver_by :email do |config|
318
+ config.mailer = "CommentMailer"
319
+ config.wait = 15.minutes
320
+ config.unless = -> { read? }
321
+ end
277
322
  end
278
323
  ```
279
324
 
280
- In this scenario, you have created an escalating notification system that
325
+ 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.
281
326
 
282
- - Immediately creates a record in the database (for display directly in the app)
283
- - Immediately issues a ping in Slack.
284
- - If the notification remains unread after 10 minutes, it emails the team.
285
- - If the notification remains unread after 20 minutes, it sends an SMS to the on-call phone.
327
+ _A note here: notifications expose a `#mark_as_read` method, but your app must choose when and where to call that method._
286
328
 
287
329
  You can mix and match the options and delivery methods to suit your application specific needs.
288
330
 
289
- 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.
290
-
291
331
  ### 🚚 Custom Delivery Methods
292
332
 
293
- To generate a custom delivery method, simply run
333
+ 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
294
334
 
295
335
  `rails generate noticed:delivery_method Discord`
296
336
 
297
- This will generate a new `DeliveryMethods::Discord` class inside the `app/notifications/delivery_methods` folder, which can be used to deliver notifications to Discord.
337
+ This will generate a new `DeliveryMethods::Discord` class inside the `app/notifiers/delivery_methods` folder, which can be used to deliver notifications to Discord.
298
338
 
299
339
  ```ruby
300
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
340
+ class DeliveryMethods::Discord < Noticed::DeliveryMethod
341
+ # Specify the config options your delivery method requires in its config block
342
+ required_options # :foo, :bar
343
+
301
344
  def deliver
302
- # Logic for sending a Discord notification
345
+ # Logic for sending the notification
303
346
  end
304
347
  end
348
+
305
349
  ```
306
350
 
307
351
  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.
308
352
 
309
353
  ```ruby
310
- class MyNotification < Noticed::Base
354
+ class MyNotifier < Noticed::Event
311
355
  deliver_by :discord, class: "DeliveryMethods::Discord"
312
356
  end
313
357
  ```
314
358
 
315
359
  Delivery methods have access to the following methods and attributes:
316
360
 
317
- * `notification` - 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.
318
- * `options` - Any configuration options on the `deliver_by` line.
319
- * `recipient` - The object who should receive the notification. This is typically a User, Account, or other ActiveRecord model.
320
- * `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 }`
321
-
322
- #### Validating options passed to Custom Delivery methods
361
+ * `event` — The `Noticed::Event` record that spawned the notification object currently being delivered
362
+ * `record` — The object originally passed into the Notifier as the `record:` param (see the ✨ note above)
363
+ * `notification` — The `Noticed::Notification` instance being delivered. All notification helper methods are available on this object
364
+ * `recipient` — The individual recipient object being delivered to for this notification (remember that each recipient gets their own instance of the Delivery Method `#deliver`)
365
+ * `config` — The hash of configuration options declared by the Notifier that generated this notification and delivery
366
+ * `params` — The parameters given to the Notifier in the invocation (via `.with()`)
323
367
 
324
- The presence of the delivery method options is automatically validated if using the `option(s)` method.
368
+ #### Validating config options passed to Custom Delivery methods
325
369
 
326
- 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.
370
+ 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:
327
371
 
328
372
  ```ruby
329
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
330
- option :username # Requires the username option to be passed
373
+ class DeliveryMethods::Email < Noticed::DeliveryMethod
374
+ required_options :mailer, :method
331
375
 
332
376
  def deliver
333
- # Logic for sending a Discord notification
377
+ # ...
378
+ method = config.method
334
379
  end
335
-
336
- def self.validate!(delivery_method_options)
337
- super # Don't forget to call super, otherwise option presence won't be validated
338
-
339
-   # Custom validations
340
- if delivery_method_options[:username].blank?
341
- raise Noticed::ValidationError, 'the `username` option must be present'
342
- end
343
- end
344
- end
345
-
346
- class CommentNotification < Noticed::Base
347
- deliver_by :discord, class: 'DeliveryMethods::Discord'
348
380
  end
349
381
  ```
350
382
 
351
- Now it will raise an error because a required argument is missing.
352
-
353
- To fix the error, the argument has to be passed correctly. For example:
383
+ 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`:
354
384
 
355
385
  ```ruby
356
- class CommentNotification < Noticed::Base
357
- deliver_by :discord, class: 'DeliveryMethods::Discord', username: User.admin.username
386
+ class NewSaleNotifier < Noticed::Event
387
+ deliver_by :whats_app do |config|
388
+ config.day = -> { is_tuesday? "Tuesday" : "Not Tuesday" }
389
+ end
358
390
  end
359
- ```
360
-
361
- #### Callbacks
362
391
 
363
- 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.
392
+ class DeliveryMethods::WhatsApp < Noticed::DeliveryMethod
393
+ required_options :day
364
394
 
365
- ```ruby
366
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
367
- after_deliver do
368
- # Do whatever you want
395
+ def deliver
396
+ # ...
397
+ config.day #=> #<Proc:0x000f7c8 (lambda)>
398
+ evaluate_option(config.day) #=> "Tuesday"
369
399
  end
370
400
  end
371
401
  ```
372
402
 
373
403
  ### 📦 Database Model
374
404
 
375
- The Notification database model includes several helpful features to make working with database notifications easier.
405
+ The Noticed database models include several helpful features to make working with notifications easier.
406
+
407
+ #### Notification
376
408
 
377
- #### Class methods
409
+ ##### Class methods/scopes
410
+
411
+ (Assuming your user `has_many :notifications, as: :recipient, class_name: "Noticed::Notification"`)
378
412
 
379
413
  Sorting notifications by newest first:
380
414
 
381
415
  ```ruby
382
- user.notifications.newest_first
416
+ @user.notifications.newest_first
383
417
  ```
384
418
 
385
419
  Query for read or unread notifications:
@@ -399,16 +433,18 @@ user.notifications.mark_as_unread!
399
433
 
400
434
  #### Instance methods
401
435
 
402
- Convert back into a Noticed notification object:
436
+ Convert back into a Noticed notifier object:
403
437
 
404
438
  ```ruby
405
- @notification.to_notification
439
+ @notification.to_notifier
406
440
  ```
407
441
 
408
442
  Mark notification as read / unread:
409
443
 
410
444
  ```ruby
445
+ @notification.mark_as_read
411
446
  @notification.mark_as_read!
447
+ @notification.mark_as_unread
412
448
  @notification.mark_as_unread!
413
449
  ```
414
450
 
@@ -421,41 +457,36 @@ Check if read / unread:
421
457
 
422
458
  #### Associating Notifications
423
459
 
424
- Adding notification associations to your models makes querying and deleting notifications easy and is a pretty critical feature of most applications.
460
+ Adding notification associations to your models makes querying, rendering, and managing notifications easy (and is a pretty critical feature of most applications).
425
461
 
426
- For example, in most cases, you'll want to delete notifications for records that are destroyed.
462
+ There are two ways to associate your models to notifications:
427
463
 
428
- We'll need two associations for this:
464
+ 1. Where your object `has_many` notifications as the recipient (who you sent the notification to)
465
+ 2. Where your object `has_many` notifications as the `record` (what the notifications were about)
429
466
 
430
- 1. Notifications where the record is the recipient
431
- 2. Notifications where the record is in the notification params
467
+ 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).
432
468
 
433
- For example, we can query the notifications and delete them on destroy like so:
469
+ We can illustrate that in the following:
434
470
 
435
471
  ```ruby
436
- class Post < ApplicationRecord
437
- # Standard association for deleting notifications when you're the recipient
438
- has_many :notifications, as: :recipient, dependent: :destroy
472
+ class User < ApplicationRecord
473
+ has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
474
+ end
439
475
 
440
- # Helper for associating and destroying Notification records where(params: {post: self})
441
- has_noticed_notifications
476
+ # All of the notifications the user has been sent
477
+ # @user.notifications.each { |n| render(n) }
442
478
 
443
- # You can override the param_name, the notification model name, or disable the before_destroy callback
444
- has_noticed_notifications param_name: :parent, destroy: false, model_name: "Notification"
479
+ class Post < ApplicationRecord
480
+ has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
445
481
  end
446
482
 
447
- # Create a CommentNotification with a post param
448
- CommentNotification.with(post: @post).deliver(user)
449
- # Lookup Notifications where params: {post: @post}
450
- @post.notifications_as_post
451
-
452
- CommentNotification.with(parent: @post).deliver(user)
453
- @post.notifications_as_parent
483
+ # All of the notification events this post generated
484
+ # @post.noticed_events.each { |ne| ne.notifications... }
454
485
  ```
455
486
 
456
487
  #### Handling Deleted Records
457
488
 
458
- 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`:
489
+ 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.
459
490
 
460
491
  ```ruby
461
492
  class ApplicationJob < ActiveJob::Base
@@ -477,3 +508,4 @@ DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
477
508
 
478
509
  ## 📝 License
479
510
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
511
+