noticed 2.0.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2466298cbeef0bde1ae34d07ed5317032659ba413cc15797e211012c9d7847c
4
- data.tar.gz: b4edc8f7a819331a2d66a39c04d491f8c534f44fae8c14edbc0ce2b366556c8f
3
+ metadata.gz: 2526071997479bc9e570c51d5b148ce569f9afb45912b6c2b1bea71be65fc30e
4
+ data.tar.gz: ffa64bdc283245497a456f125680a41da2cf0ba7ad52c2c6320af24b5ec3d956
5
5
  SHA512:
6
- metadata.gz: bf817631a96cf372b864e6df07c38f3c0a4e97f288ae63b934af36fa9102e22408802176e9642f6a1c3a22b46854c764dfd6a25d19da00c3da5c0eeeec778694
7
- data.tar.gz: 4d7a596f35331bc8f38e1337398f72fdf38af34b631bae27db48c32b4f58f702023c3a5d44e00ebb9f7e8e990b1053d8bc76c5941db8c9b7a296e03e9f4c2a96
6
+ metadata.gz: 1cf980bb8d8ae9057ff80bd6313158997b1cee1fd664e3e4fef819d1cf3ba5cac469e55e02e8c4d1a2e70bf53658a58cd6c524e93da00fb2dc7a228bdda1a195
7
+ data.tar.gz: 109fe7195032238f53ed001910db4bdce5e58a89543b45c2aefc88046f7e649b68cd77421dc005fa932fe1984e610349597b1f5ffb60fde9550356b91585f5b9
data/README.md CHANGED
@@ -1,19 +1,26 @@
1
- # Noticed
1
+ # Noticed
2
+
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
+ **⚠️⚠️ Upgrading from V1? Read the [Upgrade Guide](https://github.com/excid3/noticed/blob/main/UPGRADE.md)!**
8
+
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)!
10
+
11
+ Noticed implements two top-level types of delivery methods:
12
+
13
+ 1. Individual Deliveries: Where each recipient gets their own notification
7
14
 
8
- The core concepts of Noticed are:
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.
9
16
 
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.
17
+ 2. Bulk Deliveries - one notification for all recipients. This is useful for sending a notification to your Slack team, for example.
14
18
 
15
- ## Delivery Methods
16
- Individual Delivery methods (one notification to each recipient):
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:
17
24
 
18
25
  * [ActionCable](docs/delivery_methods/action_cable.md)
19
26
  * [Apple Push Notification Service](docs/delivery_methods/ios.md)
@@ -25,7 +32,7 @@ Individual Delivery methods (one notification to each recipient):
25
32
  * [Vonage SMS](docs/delivery_methods/vonage_sms.md)
26
33
  * [Test](docs/delivery_methods/test.md)
27
34
 
28
- Bulk delivery methods (one notification for all recipients):
35
+ Bulk delivery methods we support:
29
36
 
30
37
  * [Discord](docs/bulk_delivery_methods/discord.md)
31
38
  * [Slack](docs/bulk_delivery_methods/slack.md)
@@ -33,9 +40,9 @@ Bulk delivery methods (one notification for all recipients):
33
40
 
34
41
  ## 🎬 Screencast
35
42
 
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>
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>
37
44
 
38
- [Watch Screencast](https://www.youtube.com/watch?v=Scffi4otlFc)
45
+ [Watch Screencast](https://www.youtube.com/watch?v=SzX-aBEqnAc)
39
46
 
40
47
  ## 🚀 Installation
41
48
  Run the following command to add Noticed to your Gemfile:
@@ -44,7 +51,7 @@ Run the following command to add Noticed to your Gemfile:
44
51
  bundle add "noticed"
45
52
  ```
46
53
 
47
- Add the migrations:
54
+ Generate then run the migrations:
48
55
 
49
56
  ```bash
50
57
  rails noticed:install:migrations
@@ -53,124 +60,160 @@ rails db:migrate
53
60
 
54
61
  ## 📝 Usage
55
62
 
56
- To generate a Notifier, simply run:
63
+ Noticed operates with a few constructs: Notifiers, delivery methods, and Notification records.
57
64
 
58
- `rails generate noticed:notifier CommentNotifier`
65
+ To start, generate a Notifier:
59
66
 
60
- #### Add Delivery Methods
61
- Then add delivery methods to the Notifier. See [docs/delivery_methods](docs/) for a full list.
67
+ ```sh
68
+ rails generate noticed:notifier NewCommentNotifier
69
+ ```
70
+
71
+ #### Notifier Objects
72
+
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.
62
78
 
63
79
  ```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}" }
80
+ # ~/app/notifiers/new_comment_notifier.rb
81
+
82
+ class NewCommentNotifier < Noticed::Event
83
+ deliver_by :action_cable do |config|
84
+ config.channel = "NotificationsChannel"
85
+ config.stream = :some_stream
69
86
  end
70
87
 
71
88
  deliver_by :email do |config|
72
- config.mailer = "UserMailer"
73
- config.method = :new_comment
89
+ config.mailer = "CommentMailer"
90
+ config.if = ->(recipient) { !!recipient.preferences[:email] }
91
+ end
92
+
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
102
+
103
+ notification_methods do
104
+ # I18n helpers
105
+ def message
106
+ t(".message")
107
+ end
108
+
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
74
114
  end
75
115
  end
76
116
  ```
77
117
 
78
- #### Sending Notifications
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).
79
119
 
80
- To send a notification to user(s):
120
+ ##### Required Params
121
+
122
+ While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the `required_params` method:
81
123
 
82
124
  ```ruby
83
- # Instantiate a new notifier
84
- CommentNotifier.with(record: @comment, foo: "bar").deliver_later(User.all)
125
+ class CarSaleNotifier < Noticed::Event
126
+ deliver_by :email { |c| c.mailer = "BranchMailer" }
127
+
128
+ # `record` is the Car record, `Branch` is the dealership
129
+ required_params :record, :branch
130
+ end
85
131
  ```
86
132
 
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.
133
+ Which will validate upon any invocation that the specified parameters are present:
88
134
 
89
- Delivering will create a `Noticed::Event` record and associated `Noticed::Notification` records for each recipient.
135
+ ```ruby
136
+ CarSaleNotifier.with(record: Car.last).deliver(Branch.last)
137
+ #=> Noticed::ValidationError("Param `branch` is required for CarSaleNotifier")
90
138
 
91
- After saving, a job will be enqueued for processing this notification and delivering it to all recipients.
139
+ CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
140
+ #=> OK
141
+ ```
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.
94
143
 
95
- #### Notifier Objects
96
144
 
97
- Notifiers inherit from `Noticed::Event`. This provides all their functionality and allows them to be delivered.
145
+ ##### Helper Methods
98
146
 
99
- ```ruby
100
- 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
106
- end
107
- end
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>
108
155
  ```
109
156
 
110
- **Shared Options**
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.
111
158
 
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.
159
+ ###### URL Helpers
115
160
 
116
- ##### Helper Methods
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.
117
162
 
118
- You can define helper methods inside your Notifier object to make it easier to render.
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.
119
164
 
120
165
  ```ruby
121
- class CommentNotifier < Noticed::Event
122
- # I18n helpers
123
- def message
124
- t(".message")
125
- end
166
+ Rails.application.routes.default_url_options[:host] = 'localhost:3000'
167
+ ```
126
168
 
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
- def url
130
- post_path(params[:post])
131
- end
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:
172
+
173
+ From the above Notifier...
132
174
 
133
- # Defines methods added to the Noticed::Notification
175
+ ```ruby
176
+ class NewCommentNotifier < Noticed::Event
177
+ # ...
178
+
134
179
  notification_methods do
135
- def personalized_welcome
136
- "Hello #{recipient.first_name}."
180
+ def message
181
+ t(".message")
137
182
  end
138
183
  end
184
+
185
+ # ...
139
186
  end
140
187
  ```
141
188
 
142
- In your views, you can loop through notifications and access
189
+ Calling the `message` helper in an ERB view:
190
+
143
191
  ```erb
144
- <%= current_user.notifications.includes(:event).each do |notification| %>
145
- <%= link_to notification.personalized_welcome, notification.event.url %>
146
- <% end %>
192
+ <%= @user.notifications.last.message %>
147
193
  ```
148
194
 
149
- ##### URL Helpers
195
+ Will look for the following translation path:
150
196
 
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.
197
+ ```yml
198
+ # ~/config/locales/en.yml
152
199
 
153
- ```ruby
154
- Rails.application.routes.default_url_options[:host] = 'localhost:3000'
200
+ en:
201
+ notifiers:
202
+ new_comment_notifier:
203
+ notification:
204
+ message: "Someone posted a new comment!"
155
205
  ```
156
206
 
157
- ##### Translations
158
-
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.
160
-
161
- For example:
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).
162
208
 
163
- `t(".message")` looks up `en.notifiers.new_comment.message`
164
- `t(".message") # in Admin::NewComment` looks up `en.notifiers.admin.new_comment.message`
165
-
166
- ##### User Preferences
209
+ ##### Tip: Capture User Preferences
167
210
 
168
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.
169
212
 
170
213
  For example:
171
214
 
172
215
  ```ruby
173
- class CommentNotifier < Noticed::Base
216
+ class CommentNotifier < Noticed::Event
174
217
  deliver_by :email do |config|
175
218
  config.mailer = 'CommentMailer'
176
219
  config.method = :new_comment
@@ -179,182 +222,198 @@ class CommentNotifier < Noticed::Base
179
222
  end
180
223
  ```
181
224
 
182
- ## Best Practices
183
-
184
- ### Creating a notification from an Active Record callback
225
+ **Shared Delivery Method Options**
185
226
 
186
- Always use `after_commit` hooks to send notifications from ActiveRecord callbacks. For example, to send a notification automatically after a message is created:
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.
187
228
 
188
- ```ruby
189
- class Message < ApplicationRecord
190
- belongs_to :recipient, class_name: "User"
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
191
234
 
192
- after_create_commit :notify_recipient
235
+ #### Sending Notifications
193
236
 
194
- private
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:
195
238
 
196
- def notify_recipient
197
- NewMessageNotifier.with(message: self).deliver_later(recipient)
198
- end
239
+ ```ruby
240
+ NewCommentNotifier.with(record: @comment, foo: "bar").deliver(@comment.thread.all_authors)
199
241
  ```
200
242
 
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.
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.
202
244
 
203
- 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”.
204
246
 
205
- > `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:
206
248
 
207
- ### Renaming Notifiers
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...
208
255
 
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.
256
+ ## Best Practices
210
257
 
211
- You can catch these errors at runtime by using `YourNotifierClassName.name` instead of hardcoding the string when performing a query.
258
+ ### Renaming Notifiers
212
259
 
213
- ```ruby
214
- Noticed::Event.where(type: YourNotifierClassName.name) # good
215
- Noticed::Event.where(type: "YourNotifierClassName") # bad
216
- ```
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.
217
261
 
218
- 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.
219
263
 
220
264
  ```ruby
221
265
  Noticed::Event.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name)
222
- Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: NewNotifierClassName::Notification.name)
266
+ # and
267
+ Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: "#{NewNotifierClassName.name}::Notification")
223
268
  ```
224
269
 
225
270
  ## 🚛 Delivery Methods
226
271
 
227
- The delivery methods are modular so you can customize the way each type gets delivered.
272
+ The delivery methods are designed to be modular so you can customize the way each type gets delivered.
228
273
 
229
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.
230
275
 
231
- ### Fallback Notifications
276
+ Individual delivery methods:
277
+
278
+ * [ActionCable](docs/delivery_methods/action_cable.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)
282
+ * [Microsoft Teams](docs/delivery_methods/microsoft_teams.md)
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)
286
+ * [Test](docs/delivery_methods/test.md)
232
287
 
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.
288
+ Bulk delivery methods:
234
289
 
235
- ```ruby
236
- class CommentNotifier< Noticed::Base
237
- deliver_by :database
238
- deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
239
- end
290
+ * [Discord](docs/bulk_delivery_methods/discord.md)
291
+ * [Slack](docs/bulk_delivery_methods/slack.md)
292
+ * [Webhook](docs/bulk_delivery_methods/webhook.md)
293
+
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>
240
306
  ```
241
307
 
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.
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
243
311
 
244
- 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.
245
313
 
246
314
  ```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?
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
252
322
  end
253
323
  ```
254
324
 
255
- 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.
256
326
 
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.
327
+ _A note here: notifications expose a `#mark_as_read` method, but your app must choose when and where to call that method._
261
328
 
262
329
  You can mix and match the options and delivery methods to suit your application specific needs.
263
330
 
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
331
  ### 🚚 Custom Delivery Methods
267
332
 
268
- 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
269
334
 
270
335
  `rails generate noticed:delivery_method Discord`
271
336
 
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.
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.
273
338
 
274
339
  ```ruby
275
- 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
+
276
344
  def deliver
277
- # Logic for sending a Discord notification
345
+ # Logic for sending the notification
278
346
  end
279
347
  end
348
+
280
349
  ```
281
350
 
282
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.
283
352
 
284
353
  ```ruby
285
- class MyNotifier < Noticed::Base
354
+ class MyNotifier < Noticed::Event
286
355
  deliver_by :discord, class: "DeliveryMethods::Discord"
287
356
  end
288
357
  ```
289
358
 
290
359
  Delivery methods have access to the following methods and attributes:
291
360
 
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 }`
296
-
297
- #### 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()`)
298
367
 
299
- 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
300
369
 
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.
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:
302
371
 
303
372
  ```ruby
304
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
305
- option :username # Requires the username option to be passed
373
+ class DeliveryMethods::Email < Noticed::DeliveryMethod
374
+ required_options :mailer, :method
306
375
 
307
376
  def deliver
308
- # Logic for sending a Discord notification
309
- end
310
-
311
- def self.validate!(delivery_method_options)
312
- super # Don't forget to call super, otherwise option presence won't be validated
313
-
314
-   # Custom validations
315
- if delivery_method_options[:username].blank?
316
- raise Noticed::ValidationError, 'the `username` option must be present'
317
- end
377
+ # ...
378
+ method = config.method
318
379
  end
319
380
  end
320
-
321
- class CommentNotifier < Noticed::Base
322
- deliver_by :discord, class: 'DeliveryMethods::Discord'
323
- end
324
381
  ```
325
382
 
326
- Now it will raise an error because a required argument is missing.
327
-
328
- 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`:
329
384
 
330
385
  ```ruby
331
- class CommentNotifier < Noticed::Base
332
- 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
333
390
  end
334
- ```
335
391
 
336
- #### Callbacks
392
+ class DeliveryMethods::WhatsApp < Noticed::DeliveryMethod
393
+ required_options :day
337
394
 
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.
339
-
340
- ```ruby
341
- class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
342
- after_deliver do
343
- # Do whatever you want
395
+ def deliver
396
+ # ...
397
+ config.day #=> #<Proc:0x000f7c8 (lambda)>
398
+ evaluate_option(config.day) #=> "Tuesday"
344
399
  end
345
400
  end
346
401
  ```
347
402
 
348
403
  ### 📦 Database Model
349
404
 
350
- 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.
351
406
 
352
- #### Class methods
407
+ #### Notification
408
+
409
+ ##### Class methods/scopes
410
+
411
+ (Assuming your user `has_many :notifications, as: :recipient, class_name: "Noticed::Notification"`)
353
412
 
354
413
  Sorting notifications by newest first:
355
414
 
356
415
  ```ruby
357
- user.notifications.newest_first
416
+ @user.notifications.newest_first
358
417
  ```
359
418
 
360
419
  Query for read or unread notifications:
@@ -383,7 +442,9 @@ Convert back into a Noticed notifier object:
383
442
  Mark notification as read / unread:
384
443
 
385
444
  ```ruby
445
+ @notification.mark_as_read
386
446
  @notification.mark_as_read!
447
+ @notification.mark_as_unread
387
448
  @notification.mark_as_unread!
388
449
  ```
389
450
 
@@ -396,41 +457,36 @@ Check if read / unread:
396
457
 
397
458
  #### Associating Notifications
398
459
 
399
- 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).
400
461
 
401
- 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:
402
463
 
403
- 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)
404
466
 
405
- 1. Notifications where the record is the recipient
406
- 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).
407
468
 
408
- For example, we can query the notifications and delete them on destroy like so:
469
+ We can illustrate that in the following:
409
470
 
410
471
  ```ruby
411
- class Post < ApplicationRecord
412
- # Standard association for deleting notifications when you're the recipient
413
- 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
414
475
 
415
- # Helper for associating and destroying Notification records where(params: {post: self})
416
- has_noticed_notifications
476
+ # All of the notifications the user has been sent
477
+ # @user.notifications.each { |n| render(n) }
417
478
 
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"
479
+ class Post < ApplicationRecord
480
+ has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
420
481
  end
421
482
 
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
426
-
427
- CommentNotifier.with(parent: @post).deliver(user)
428
- @post.notifications_as_parent
483
+ # All of the notification events this post generated
484
+ # @post.noticed_events.each { |ne| ne.notifications... }
429
485
  ```
430
486
 
431
487
  #### Handling Deleted Records
432
488
 
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`:
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.
434
490
 
435
491
  ```ruby
436
492
  class ApplicationJob < ActiveJob::Base
@@ -452,3 +508,4 @@ DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
452
508
 
453
509
  ## 📝 License
454
510
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
511
+
@@ -0,0 +1,19 @@
1
+ module Noticed
2
+ class NotificationChannel < ApplicationCable::Channel
3
+ def subscribed
4
+ stream_for current_user
5
+ end
6
+
7
+ def unsubscribed
8
+ stop_all_streams
9
+ end
10
+
11
+ def mark_as_seen(data)
12
+ current_user.notifications.where(id: data["ids"]).mark_as_seen
13
+ end
14
+
15
+ def mark_as_read(data)
16
+ current_user.notifications.where(id: data["ids"]).mark_as_read
17
+ end
18
+ end
19
+ end
@@ -31,18 +31,34 @@ module Noticed
31
31
  update(read_at: Time.current)
32
32
  end
33
33
 
34
+ def mark_as_read!
35
+ update!(read_at: Time.current)
36
+ end
37
+
34
38
  def mark_as_unread
35
39
  update(read_at: nil)
36
40
  end
37
41
 
42
+ def mark_as_unread!
43
+ update!(read_at: nil)
44
+ end
45
+
38
46
  def mark_as_seen
39
47
  update(seen_at: Time.current)
40
48
  end
41
49
 
50
+ def mark_as_seen!
51
+ update!(seen_at: Time.current)
52
+ end
53
+
42
54
  def mark_as_unseen
43
55
  update(seen_at: nil)
44
56
  end
45
57
 
58
+ def mark_as_unseen!
59
+ update!(seen_at: nil)
60
+ end
61
+
46
62
  def read?
47
63
  read_at?
48
64
  end
@@ -10,7 +10,5 @@ module Noticed
10
10
  scope :newest_first, -> { order(created_at: :desc) }
11
11
 
12
12
  delegate :params, :record, to: :event
13
-
14
- attribute :params, default: {}
15
13
  end
16
14
  end
@@ -1,12 +1,8 @@
1
- class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethods::Base
1
+ class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethod
2
+ # Specify the config options your delivery method requires in its config block
3
+ required_options # :foo, :bar
4
+
2
5
  def deliver
3
6
  # Logic for sending the notification
4
7
  end
5
-
6
- # You may override this method to validate options for the delivery method
7
- # Invalid options should raise a ValidationError
8
- #
9
- # def self.validate!(options)
10
- # raise ValidationError, "required_option missing" unless options[:required_option]
11
- # end
12
8
  end
@@ -7,6 +7,7 @@ module Noticed
7
7
 
8
8
  attr_reader :config, :event, :notification
9
9
  delegate :recipient, to: :notification
10
+ delegate :record, :params, to: :event
10
11
 
11
12
  def perform(delivery_method_name, notification, overrides: {})
12
13
  @notification = notification
@@ -1,11 +1,11 @@
1
1
  module Noticed
2
2
  module DeliveryMethods
3
3
  class ActionCable < DeliveryMethod
4
- required_options :channel, :stream, :message
4
+ required_options :message
5
5
 
6
6
  def deliver
7
- channel = fetch_constant(:channel)
8
- stream = evaluate_option(:stream)
7
+ channel = fetch_constant(:channel) || Noticed::NotificationChannel
8
+ stream = evaluate_option(:stream) || recipient
9
9
  message = evaluate_option(:message)
10
10
 
11
11
  channel.broadcast_to stream, message
@@ -6,14 +6,18 @@ module Noticed
6
6
  def deliver
7
7
  mailer = fetch_constant(:mailer)
8
8
  email = evaluate_option(:method)
9
- params = (evaluate_option(:params) || notification&.params || {}).merge(record: notification&.record)
10
- args = evaluate_option(:args)
11
-
12
- mail = mailer.with(params)
13
- mail = args.present? ? mail.send(email, *args) : mail.send(email)
14
-
9
+ args = evaluate_option(:args) || []
10
+ mail = mailer.with(params).send(email, *args)
15
11
  (!!evaluate_option(:enqueue)) ? mail.deliver_later : mail.deliver_now
16
12
  end
13
+
14
+ def params
15
+ (evaluate_option(:params) || notification&.params || {}).merge(
16
+ notification: notification,
17
+ record: notification&.record,
18
+ recipient: notification&.recipient
19
+ )
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noticed
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-15 00:00:00.000000000 Z
11
+ date: 2024-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -35,6 +35,7 @@ files:
35
35
  - MIT-LICENSE
36
36
  - README.md
37
37
  - Rakefile
38
+ - app/channels/noticed/notification_channel.rb
38
39
  - app/jobs/noticed/application_job.rb
39
40
  - app/jobs/noticed/event_job.rb
40
41
  - app/models/concerns/noticed/deliverable.rb
@@ -93,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
94
  - !ruby/object:Gem::Version
94
95
  version: '0'
95
96
  requirements: []
96
- rubygems_version: 3.5.3
97
+ rubygems_version: 3.5.4
97
98
  signing_key:
98
99
  specification_version: 4
99
100
  summary: Notifications for Ruby on Rails applications