noticed 2.0.3 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf12a81661e3c6fa7e6240e54f6cb802420b13e2cfd35feabdc713a415986b5a
4
- data.tar.gz: '08074571747ce14478e7f45de5917700fa997caefd0bb93b3bd154287f3b7830'
3
+ metadata.gz: 457449f2026b5ae977841ac9c0c0d30373fe1f8715670290ff49b75e021fea9a
4
+ data.tar.gz: 51adcf9c865b36de52eed8066a8d622867a897caad615879632c63939b5aebf2
5
5
  SHA512:
6
- metadata.gz: c5ed4a60a134a094a9837127d2ac5aaab7ab2bae500222861ebb99e3053ef8450a439093543f03f58fefe5d1df04b47e519bfcfb3193b2aa82262bb5d725fe7a
7
- data.tar.gz: 4cfcb122083862b7400459b99695619fabafb8676ea44e5185a06f85eaa693cc12a4eca0fb3094a0c502017fe0b67767d5853ec72e5928b4b560209189e95c4e
6
+ metadata.gz: 2d57c184f5c1b72848795ab0350e3fe2f6a05a4dc7698e26acb8bda186ac7f8e9ef5c1451fd9bba77e6afa5b118288a1bdc39a293b269d8645491fb9d026ea23
7
+ data.tar.gz: daf3bb3225f9273821d9918da31700e7c0c65e89c441e8340847d085b9f76bdfde347e920f861dff3eb697e3083fbbfe0fb7a3ee6e0759172d89988b90c68381
data/README.md CHANGED
@@ -1,24 +1,31 @@
1
1
  # Noticed
2
2
 
3
- ### 🎉 Notifications for your Ruby on Rails app.
3
+ ## 🎉 Notifications for your Ruby on Rails app.
4
4
 
5
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)
6
6
 
7
- **⚠️⚠️ Upgrading from V1? Read the [Upgrade Guide](https://github.com/excid3/noticed/blob/main/UPGRADE.md)!**
7
+ > [!IMPORTANT]
8
+ > **⚠️ Upgrading from V1? Read the [Upgrade Guide](https://github.com/excid3/noticed/blob/main/UPGRADE.md)!**
8
9
 
9
10
  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
 
11
12
  Noticed implements two top-level types of delivery methods:
12
13
 
13
- 1. Individual Deliveries: Where each recipient gets their own notification
14
+ 1. **Individual Deliveries**: Where each recipient gets their own notification
15
+ <details>
16
+ <summary> Show Example </summary>
14
17
 
15
18
  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.
19
+ </details>
16
20
 
17
- 2. Bulk Deliveries - one notification for all recipients. This is useful for sending a notification to your Slack team, for example.
21
+ 2. **Bulk Deliveries**: One notification for all recipients. This is useful for sending a notification to your Slack team, for example.
18
22
 
23
+ <details>
24
+ <summary> Show Example </summary>
19
25
  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
26
 
21
27
  Bulk deliveries are typically used to push notifications to other platforms where users are managed (Slack, Discord, etc.) instead of your own.
28
+ </details>
22
29
 
23
30
  Delivery methods we officially support:
24
31
 
@@ -62,13 +69,26 @@ rails db:migrate
62
69
 
63
70
  Noticed operates with a few constructs: Notifiers, delivery methods, and Notification records.
64
71
 
65
- To start, generate a Notifier:
72
+ To start, generate a Notifier:
66
73
 
67
74
  ```sh
68
75
  rails generate noticed:notifier NewCommentNotifier
69
76
  ```
70
77
 
71
- #### Notifier Objects
78
+ ### Usage Contents
79
+ - [Notifier Objects](#notifier-objects)
80
+ - [Delivery Method Configuration](#delivery-method-configuration)
81
+ - [Required Params](#required-params)
82
+ - [Helper Methods](#helper-methods)
83
+ - [URL Helpers](#url-helpers)
84
+ - [Translations](#translations)
85
+ - [Tip: Capture User Preferences](#tip-capture-user-preferences)
86
+ - [Tip: Extracting Delivery Method Configurations](#tip-extracting-delivery-method-configurations)
87
+ - [Shared Delivery Method Options](#shared-delivery-method-options)
88
+ - [Sending Notifications](#sending-notifications)
89
+ - [Custom Noticed Model Methods](#custom-noticed-model-methods)
90
+
91
+ ### Notifier Objects
72
92
 
73
93
  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
94
 
@@ -117,7 +137,84 @@ end
117
137
 
118
138
  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).
119
139
 
120
- ##### Required Params
140
+ #### Delivery Method Configuration
141
+
142
+ Each delivery method can be configured with a block that yields a `config` object.
143
+
144
+ Procs/Lambdas will be evaluated when needed and symbols can be used to call a method.
145
+
146
+ When a lambda is passed, it will not pass any arguments and evaluates the Proc in the context of the Noticed::Notification
147
+
148
+ 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.
149
+ Your method must accept a single argument. If you don't need to use the object you can just use `(*)`.
150
+
151
+ <details>
152
+ <summary> Show Example </summary>
153
+
154
+ ```ruby
155
+ class CommentNotifier < Noticed::Event
156
+ deliver_by :ios do |config|
157
+ config.format = :ios_format
158
+ config.apns_key = :ios_cert
159
+ config.key_id = :ios_key_id
160
+ config.team_id = :ios_team_id
161
+ config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_identifier)
162
+ config.device_tokens = :ios_device_tokens
163
+ config.if = -> { recipient.send_ios_notification? }
164
+ end
165
+
166
+ def ios_format(apn)
167
+ apn.alert = { title:, body: }
168
+ apn.mutable_content = true
169
+ apn.content_available = true
170
+ apn.sound = "notification.m4r"
171
+ apn.custom_payload = {
172
+ url:,
173
+ type: self.class.name,
174
+ id: record.id,
175
+ image_url: "" || image_url,
176
+ params: params.to_json
177
+ }
178
+ end
179
+
180
+ def ios_cert(*)
181
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert)
182
+ end
183
+
184
+ def ios_key_id(*)
185
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id)
186
+ end
187
+
188
+ def ios_team_id(*)
189
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id)
190
+ end
191
+
192
+ def ios_bundle_id(*)
193
+ Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier)
194
+ end
195
+
196
+ def ios_device_tokens(notification)
197
+ notification.recipient.ios_device_tokens
198
+ end
199
+
200
+ def url
201
+ comment_thread_path(record.thread)
202
+ end
203
+ end
204
+
205
+ class Recipent < ApplicationRecord # or whatever your recipient model is
206
+ has_many :ios_device_tokens
207
+
208
+ def send_ios_notification?
209
+ # some logic
210
+ end
211
+ end
212
+ ```
213
+ </details>
214
+
215
+ More examples are in the docs for each delivery method.
216
+
217
+ #### Required Params
121
218
 
122
219
  While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the `required_params` method:
123
220
 
@@ -126,7 +223,10 @@ class CarSaleNotifier < Noticed::Event
126
223
  deliver_by :email { |c| c.mailer = "BranchMailer" }
127
224
 
128
225
  # `record` is the Car record, `Branch` is the dealership
129
- required_params :record, :branch
226
+ required_params :branch
227
+
228
+ # To validate the `:record` param, add a validation since it is an association on the Noticed::Event
229
+ validates :record, presence: true
130
230
  end
131
231
  ```
132
232
 
@@ -140,9 +240,7 @@ CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
140
240
  #=> OK
141
241
  ```
142
242
 
143
-
144
-
145
- ##### Helper Methods
243
+ #### Helper Methods
146
244
 
147
245
  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
246
 
@@ -156,7 +254,7 @@ Notifiers can implement various helper methods, within a `notification_methods`
156
254
 
157
255
  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.
158
256
 
159
- ###### URL Helpers
257
+ #### URL Helpers
160
258
 
161
259
  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
260
 
@@ -166,7 +264,7 @@ _But don't forget_, you'll need to configure `default_url_options` in order for
166
264
  Rails.application.routes.default_url_options[:host] = 'localhost:3000'
167
265
  ```
168
266
 
169
- ###### Translations
267
+ #### Translations
170
268
 
171
269
  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
270
 
@@ -175,13 +273,13 @@ From the above Notifier...
175
273
  ```ruby
176
274
  class NewCommentNotifier < Noticed::Event
177
275
  # ...
178
-
276
+
179
277
  notification_methods do
180
278
  def message
181
279
  t(".message")
182
280
  end
183
281
  end
184
-
282
+
185
283
  # ...
186
284
  end
187
285
  ```
@@ -206,7 +304,7 @@ en:
206
304
 
207
305
  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).
208
306
 
209
- ##### Tip: Capture User Preferences
307
+ #### Tip: Capture User Preferences
210
308
 
211
309
  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.
212
310
 
@@ -221,8 +319,42 @@ class CommentNotifier < Noticed::Event
221
319
  end
222
320
  end
223
321
  ```
322
+ #### Tip: Extracting Delivery Method Configurations
323
+
324
+ If you want to reuse delivery method configurations across multiple Notifiers, you can extract them into a module and include them in your Notifiers.
325
+
326
+ ```ruby
327
+ # /app/notifiers/notifiers/comment_notifier.rb
328
+ class CommentNotifier < Noticed::Event
329
+ include IosNotifier
330
+ include AndriodNotifer
331
+ include EmailNotifier
332
+
333
+ validates :record, presence: true
334
+ end
224
335
 
225
- **Shared Delivery Method Options**
336
+ # /app/notifiers/concerns/ios_notifier.rb
337
+ module IosNotifier
338
+ extend ActiveSupport::Concern
339
+
340
+ included do
341
+ deliver_by :ios do |config|
342
+ config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) }
343
+ config.format = ->(apn) {
344
+ apn.alert = "Hello world"
345
+ apn.custom_payload = {url: root_url(host: "example.org")}
346
+ }
347
+ config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_id)
348
+ config.key_id = Rails.application.credentials.dig(:ios, :key_id)
349
+ config.team_id = Rails.application.credentials.dig(:ios, :team_id)
350
+ config.apns_key = Rails.application.credentials.dig(:ios, :apns_key)
351
+ config.if = ->(recipient) { recipient.ios_notifications? }
352
+ end
353
+ end
354
+ end
355
+ ```
356
+
357
+ #### Shared Delivery Method Options
226
358
 
227
359
  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.
228
360
 
@@ -232,7 +364,7 @@ Each of these options are available for every delivery method (individual or bul
232
364
  * `config.wait_until` — (Should yield a specific time object) Delays the job that runs this delivery method until the specific time specified
233
365
  * `config.queue` — Sets the ActiveJob queue name to be used for the job that runs this delivery method
234
366
 
235
- #### Sending Notifications
367
+ ### 📨 Sending Notifications
236
368
 
237
369
  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:
238
370
 
@@ -253,6 +385,51 @@ This invocation will create a single `Noticed::Event` record and a `Noticed::Not
253
385
  - An individual delivery job for `:email` method to the second thread author
254
386
  - Etc...
255
387
 
388
+ ### Custom Noticed Model Methods
389
+
390
+ In order to extend the Noticed models you'll need to use a concern an a to_prepare block:
391
+
392
+ ```ruby
393
+ # config/initializers/noticed.rb
394
+ module NotificationExtensions
395
+ extend ActiveSupport::Concern
396
+
397
+ included do
398
+ belongs_to :organisation
399
+
400
+ scope :filter_by_type, ->(type) { where(type:) }
401
+ scope :exclude_type, ->(type) { where.not(type:) }
402
+ end
403
+
404
+ # You can also add instance methods here
405
+ end
406
+
407
+ Rails.application.config.to_prepare do
408
+ # You can extend Noticed::Event or Noticed::Notification here
409
+ Noticed::Event.include EventExtensions
410
+ Noticed::Notification.include NotificationExtensions
411
+ end
412
+ ```
413
+
414
+ The `NotificationExtensions` class could be separated into it's own file and live somewhere like `app/models/concerns/notification_extensions.rb`.
415
+
416
+ If you do this, the `to_prepare` block will need to be in `application.rb` instead of an initializer.
417
+
418
+ ```ruby
419
+ # config/application.rb
420
+ module MyApp
421
+ class Application < Rails::Application
422
+
423
+ # ...
424
+
425
+ config.to_prepare do
426
+ Noticed::Event.include Noticed::EventExtensions
427
+ Noticed::Notification.include Noticed::NotificationExtensions
428
+ end
429
+ end
430
+ end
431
+ ```
432
+
256
433
  ## ✅ Best Practices
257
434
 
258
435
  ### Renaming Notifiers
@@ -337,7 +514,7 @@ If you want to build your own delivery method to deliver notifications to a spec
337
514
  This will generate a new `DeliveryMethods::Discord` class inside the `app/notifiers/delivery_methods` folder, which can be used to deliver notifications to Discord.
338
515
 
339
516
  ```ruby
340
- class DeliveryMethods::Discord < Noticed::DeliveryMethod
517
+ class DeliveryMethods::Discord < ApplicationDeliveryMethod
341
518
  # Specify the config options your delivery method requires in its config block
342
519
  required_options # :foo, :bar
343
520
 
@@ -356,6 +533,63 @@ class MyNotifier < Noticed::Event
356
533
  end
357
534
  ```
358
535
 
536
+ <details>
537
+ <summary>Turbo Stream Custom Delivery Method Example</summary>
538
+
539
+ A common custom delivery method in the Rails world might be to Delivery to the web via turbo stream.
540
+
541
+ Note: This example users custom methods that extend the `Noticed::Notification` class.
542
+
543
+ See the [Custom Noticed Model Methods](#custom-noticed-model-methods) section for more information.
544
+
545
+ ```ruby
546
+ # app/notifiers/delivery_methods/turbo_stream.rb
547
+ class DeliveryMethods::TurboStream < ApplicationDeliveryMethod
548
+ def deliver
549
+ return unless recipient.is_a?(User)
550
+
551
+ notification.broadcast_update_to_bell
552
+ notification.broadcast_replace_to_index_count
553
+ notification.broadcast_prepend_to_index_list
554
+ end
555
+ end
556
+ ```
557
+
558
+ ```ruby
559
+ # app/models/concerns/noticed/notification_extensions.rb
560
+ module Noticed::NotificationExtensions
561
+ extend ActiveSupport::Concern
562
+
563
+ def broadcast_update_to_bell
564
+ broadcast_update_to(
565
+ "notifications_#{recipient.id}",
566
+ target: "notification_bell",
567
+ partial: "navbar/notifications/bell",
568
+ locals: { user: recipient }
569
+ )
570
+ end
571
+
572
+ def broadcast_replace_to_index_count
573
+ broadcast_replace_to(
574
+ "notifications_index_#{recipient.id}",
575
+ target: "notification_index_count",
576
+ partial: "notifications/notifications_count",
577
+ locals: { count: recipient.reload.notifications_count, unread: recipient.reload.unread_notifications_count }
578
+ )
579
+ end
580
+
581
+ def broadcast_prepend_to_index_list
582
+ broadcast_prepend_to(
583
+ "notifications_index_list_#{recipient.id}",
584
+ target: "notifications",
585
+ partial: "notifications/notification",
586
+ locals: { notification: self }
587
+ )
588
+ end
589
+ end
590
+ ```
591
+ </details>
592
+
359
593
  Delivery methods have access to the following methods and attributes:
360
594
 
361
595
  * `event` — The `Noticed::Event` record that spawned the notification object currently being delivered
@@ -400,13 +634,25 @@ class DeliveryMethods::WhatsApp < Noticed::DeliveryMethod
400
634
  end
401
635
  ```
402
636
 
403
- ### 📦 Database Model
637
+ #### Callbacks
638
+
639
+ 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.
640
+
641
+ ```ruby
642
+ class DeliveryMethods::Discord < Noticed::DeliveryMethod
643
+ after_deliver do
644
+ # Do whatever you want
645
+ end
646
+ end
647
+ ```
648
+
649
+ ## 📦 Database Model
404
650
 
405
651
  The Noticed database models include several helpful features to make working with notifications easier.
406
652
 
407
- #### Notification
653
+ ### Notification
408
654
 
409
- ##### Class methods/scopes
655
+ #### Class methods/scopes
410
656
 
411
657
  (Assuming your user `has_many :notifications, as: :recipient, class_name: "Noticed::Notification"`)
412
658
 
@@ -423,12 +669,11 @@ user.notifications.read
423
669
  user.notifications.unread
424
670
  ```
425
671
 
426
-
427
672
  Marking all notifications as read or unread:
428
673
 
429
674
  ```ruby
430
- user.notifications.mark_as_read!
431
- user.notifications.mark_as_unread!
675
+ user.notifications.mark_as_read
676
+ user.notifications.mark_as_unread
432
677
  ```
433
678
 
434
679
  #### Instance methods
@@ -478,10 +723,19 @@ end
478
723
 
479
724
  class Post < ApplicationRecord
480
725
  has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
726
+ has_many :notifications, through: :noticed_events, class_name: "Noticed::Notification"
481
727
  end
482
728
 
483
729
  # All of the notification events this post generated
484
- # @post.noticed_events.each { |ne| ne.notifications... }
730
+ # @post.notifications
731
+ ```
732
+
733
+ #### ActiveJob Parent Class
734
+
735
+ 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.
736
+
737
+ ```ruby
738
+ Noticed.parent_class = "ApplicationJob"
485
739
  ```
486
740
 
487
741
  #### Handling Deleted Records
@@ -494,6 +748,48 @@ class ApplicationJob < ActiveJob::Base
494
748
  end
495
749
  ```
496
750
 
751
+ ### Customizing the Database Models
752
+
753
+ You can modify the database models by editing the generated migrations.
754
+
755
+ One common adjustment is to change the IDs to UUIDs (if you're using UUIDs in your app).
756
+
757
+ You can also add additional columns to the `Noticed::Event` and `Noticed::Notification` models.
758
+
759
+ ```ruby
760
+ # This migration comes from noticed (originally 20231215190233)
761
+ class CreateNoticedTables < ActiveRecord::Migration[7.1]
762
+ def change
763
+ create_table :noticed_events, id: :uuid do |t|
764
+ t.string :type
765
+ t.belongs_to :record, polymorphic: true, type: :uuid
766
+ t.jsonb :params
767
+
768
+ # Custom Fields
769
+ t.string :organisation_id, type: :uuid, as: "((params ->> 'organisation_id')::uuid)", stored: true
770
+ t.virtual :action_type, type: :string, as: "((params ->> 'action_type'))", stored: true
771
+ t.virtual :url, type: :string, as: "((params ->> 'url'))", stored: true
772
+
773
+ t.timestamps
774
+ end
775
+
776
+ create_table :noticed_notifications, id: :uuid do |t|
777
+ t.string :type
778
+ t.belongs_to :event, null: false, type: :uuid
779
+ t.belongs_to :recipient, polymorphic: true, null: false, type: :uuid
780
+ t.datetime :read_at
781
+ t.datetime :seen_at
782
+
783
+ t.timestamps
784
+ end
785
+
786
+ add_index :noticed_notifications, :read_at
787
+ end
788
+ end
789
+ ```
790
+
791
+ 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.
792
+
497
793
  ## 🙏 Contributing
498
794
 
499
795
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
@@ -508,4 +804,3 @@ DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
508
804
 
509
805
  ## 📝 License
510
806
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
511
-
@@ -1,5 +1,5 @@
1
1
  module Noticed
2
- class NotificationChannel < ApplicationCable::Channel
2
+ class NotificationChannel < ::ApplicationCable::Channel
3
3
  def subscribed
4
4
  stream_for current_user
5
5
  end
@@ -9,10 +9,13 @@ module Noticed
9
9
 
10
10
  attribute :params, default: {}
11
11
 
12
- if Rails.gem_version >= Gem::Version.new("7.1.0.alpha")
13
- serialize :params, coder: Coder
14
- else
15
- serialize :params, Coder
12
+ # Ephemeral notifiers cannot serialize params since they aren't ActiveRecord backed
13
+ if respond_to? :serialize
14
+ if Rails.gem_version >= Gem::Version.new("7.1.0.alpha")
15
+ serialize :params, coder: Coder
16
+ else
17
+ serialize :params, Coder
18
+ end
16
19
  end
17
20
  end
18
21
 
@@ -35,6 +38,13 @@ module Noticed
35
38
  def deliver_by(name, options = {})
36
39
  raise NameError, "#{name} has already been used for this Notifier." if delivery_methods.has_key?(name)
37
40
 
41
+ if name == :database
42
+ Noticed.deprecator.warn <<-WARNING.squish
43
+ The :database delivery method has been deprecated and does nothing. Notifiers automatically save to the database now.
44
+ WARNING
45
+ return
46
+ end
47
+
38
48
  config = ActiveSupport::OrderedOptions.new.merge(options)
39
49
  yield config if block_given?
40
50
  delivery_methods[name] = DeliverBy.new(name, config)
@@ -45,14 +55,29 @@ module Noticed
45
55
  end
46
56
  alias_method :required_param, :required_params
47
57
 
58
+ def params(*names)
59
+ Noticed.deprecator.warn <<-WARNING.squish
60
+ `params` is deprecated and has been renamed to `required_params`
61
+ WARNING
62
+ required_params(*names)
63
+ end
64
+
65
+ def param(*names)
66
+ Noticed.deprecator.warn <<-WARNING.squish
67
+ `param :name` is deprecated and has been renamed to `required_param :name`
68
+ WARNING
69
+ required_params(*names)
70
+ end
71
+
48
72
  def with(params)
49
73
  record = params.delete(:record)
50
74
  new(params: params, record: record)
51
75
  end
52
76
 
53
- def deliver(recipients = nil, options = {})
54
- new.deliver(recipients, options)
77
+ def deliver(recipients = nil, **options)
78
+ new.deliver(recipients, **options)
55
79
  end
80
+ alias_method :deliver_later, :deliver
56
81
  end
57
82
 
58
83
  # CommentNotifier.deliver(User.all)
@@ -60,16 +85,17 @@ module Noticed
60
85
  # CommentNotifier.deliver(User.all, queue: :low_priority)
61
86
  # CommentNotifier.deliver(User.all, wait: 5.minutes)
62
87
  # CommentNotifier.deliver(User.all, wait_until: 1.hour.from_now)
63
- def deliver(recipients = nil, options = {})
88
+ def deliver(recipients = nil, enqueue_job: true, **options)
64
89
  validate!
65
90
 
66
91
  transaction do
67
- save!
68
-
69
92
  recipients_attributes = Array.wrap(recipients).map do |recipient|
70
93
  recipient_attributes_for(recipient)
71
94
  end
72
95
 
96
+ self.notifications_count = recipients_attributes.size
97
+ save!
98
+
73
99
  if Rails.gem_version >= Gem::Version.new("7.0.0.alpha1")
74
100
  notifications.insert_all!(recipients_attributes, record_timestamps: true) if recipients_attributes.any?
75
101
  else
@@ -83,15 +109,16 @@ module Noticed
83
109
  end
84
110
 
85
111
  # Enqueue delivery job
86
- EventJob.set(options).perform_later(self)
112
+ EventJob.set(options).perform_later(self) if enqueue_job
87
113
 
88
114
  self
89
115
  end
116
+ alias_method :deliver_later, :deliver
90
117
 
91
118
  def recipient_attributes_for(recipient)
92
119
  {
93
120
  type: "#{self.class.name}::Notification",
94
- recipient_type: recipient.class.name,
121
+ recipient_type: recipient.class.base_class.name,
95
122
  recipient_id: recipient.id
96
123
  }
97
124
  end
@@ -103,7 +130,7 @@ module Noticed
103
130
 
104
131
  def validate_params!
105
132
  required_param_names.each do |param_name|
106
- raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params[param_name].present?
133
+ raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params.has_key?(param_name)
107
134
  end
108
135
  end
109
136
 
@@ -11,19 +11,19 @@ module Noticed
11
11
 
12
12
  class_methods do
13
13
  def mark_as_read
14
- update_all(read_at: Time.current)
14
+ update_all(read_at: Time.current, updated_at: Time.current)
15
15
  end
16
16
 
17
17
  def mark_as_unread
18
- update_all(read_at: nil)
18
+ update_all(read_at: nil, updated_at: Time.current)
19
19
  end
20
20
 
21
21
  def mark_as_seen
22
- update_all(seen_at: Time.current)
22
+ update_all(seen_at: Time.current, updated_at: Time.current)
23
23
  end
24
24
 
25
25
  def mark_as_unseen
26
- update_all(seen_at: nil)
26
+ update_all(seen_at: nil, updated_at: Time.current)
27
27
  end
28
28
  end
29
29
 
@@ -19,14 +19,23 @@ module Noticed
19
19
  end
20
20
 
21
21
  def perform_later(event_or_notification, options = {})
22
- options[:wait] = evaluate_option(:wait, event_or_notification) if config.has_key?(:wait)
23
- options[:wait_until] = evaluate_option(:wait_until, event_or_notification) if config.has_key?(:wait_until)
24
- options[:queue] = evaluate_option(:queue, event_or_notification) if config.has_key?(:queue)
25
- options[:priority] = evaluate_option(:priority, event_or_notification) if config.has_key?(:priority)
22
+ options[:wait] ||= evaluate_option(:wait, event_or_notification) if config.has_key?(:wait)
23
+ options[:wait_until] ||= evaluate_option(:wait_until, event_or_notification) if config.has_key?(:wait_until)
24
+ options[:queue] ||= evaluate_option(:queue, event_or_notification) if config.has_key?(:queue)
25
+ options[:priority] ||= evaluate_option(:priority, event_or_notification) if config.has_key?(:priority)
26
26
 
27
27
  constant.set(options).perform_later(name, event_or_notification)
28
28
  end
29
29
 
30
+ def ephemeral_perform_later(notifier, recipient, params, options = {})
31
+ options[:wait] ||= evaluate_option(:wait, recipient) if config.has_key?(:wait)
32
+ options[:wait_until] ||= evaluate_option(:wait_until, recipient) if config.has_key?(:wait_until)
33
+ options[:queue] ||= evaluate_option(:queue, recipient) if config.has_key?(:queue)
34
+ options[:priority] ||= evaluate_option(:priority, recipient) if config.has_key?(:priority)
35
+
36
+ constant.set(options).perform_later(name, "#{notifier}::Notification", recipient: recipient, params: params)
37
+ end
38
+
30
39
  def evaluate_option(name, context)
31
40
  option = config[name]
32
41
 
@@ -0,0 +1,53 @@
1
+ module Noticed
2
+ class Ephemeral
3
+ include ActiveModel::Model
4
+ include ActiveModel::Attributes
5
+ include Noticed::Deliverable
6
+
7
+ attribute :record
8
+ attribute :params, default: {}
9
+
10
+ class Notification
11
+ include ActiveModel::Model
12
+ include ActiveModel::Attributes
13
+
14
+ attribute :recipient
15
+ attribute :event
16
+
17
+ delegate :params, :record, to: :event
18
+
19
+ def self.new_with_params(recipient, params)
20
+ instance = new(recipient: recipient)
21
+ instance.event = module_parent.new(params: params)
22
+ instance
23
+ end
24
+ end
25
+
26
+ # Dynamically define Notification on each Ephemeral Notifier
27
+ def self.inherited(notifier)
28
+ super
29
+ notifier.const_set :Notification, Class.new(Noticed::Ephemeral::Notification)
30
+ end
31
+
32
+ def deliver(recipients)
33
+ recipients = Array.wrap(recipients)
34
+ bulk_delivery_methods.each do |_, deliver_by|
35
+ deliver_by.ephemeral_perform_later(self.class.name, recipients, params)
36
+ end
37
+
38
+ recipients.each do |recipient|
39
+ delivery_methods.each do |_, deliver_by|
40
+ deliver_by.ephemeral_perform_later(self.class.name, recipient, params)
41
+ end
42
+ end
43
+ end
44
+
45
+ def record
46
+ params[:record]
47
+ end
48
+
49
+ def notification_methods(&block)
50
+ const_get(:Notification).class_eval(&block)
51
+ end
52
+ end
53
+ end
@@ -4,7 +4,7 @@ module Noticed
4
4
  include Readable
5
5
  include Translation
6
6
 
7
- belongs_to :event
7
+ belongs_to :event, counter_cache: true
8
8
  belongs_to :recipient, polymorphic: true
9
9
 
10
10
  scope :newest_first, -> { order(created_at: :desc) }
@@ -8,6 +8,7 @@ class CreateNoticedTables < ActiveRecord::Migration[6.1]
8
8
  else
9
9
  t.json :params
10
10
  end
11
+ t.integer :notifications_count
11
12
 
12
13
  t.timestamps
13
14
  end
@@ -0,0 +1,5 @@
1
+ class AddNotificationsCountToNoticedEvent < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :noticed_events, :notifications_count, :integer
4
+ end
5
+ end
@@ -12,6 +12,7 @@ module Noticed
12
12
  desc "Generates a class for a custom delivery method with the given NAME."
13
13
 
14
14
  def generate_notification
15
+ template "application_delivery_method.rb", "app/notifiers/application_delivery_method.rb"
15
16
  template "delivery_method.rb", "app/notifiers/delivery_methods/#{singular_name}.rb"
16
17
  end
17
18
  end
@@ -14,6 +14,7 @@ module Noticed
14
14
  desc "Generates a notification with the given NAME."
15
15
 
16
16
  def generate_notification
17
+ template "application_notifier.rb", "app/notifiers/application_notifier.rb"
17
18
  template "notifier.rb", "app/notifiers/#{file_path}_notifier.rb"
18
19
  end
19
20
 
@@ -0,0 +1,2 @@
1
+ class ApplicationDeliveryMethod < Noticed::DeliveryMethod
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationNotifier < Noticed::Event
2
+ end
@@ -1,4 +1,4 @@
1
- class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethod
1
+ class DeliveryMethods::<%= class_name %> < ApplicationDeliveryMethod
2
2
  # Specify the config options your delivery method requires in its config block
3
3
  required_options # :foo, :bar
4
4
 
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # <%= class_name %>.with(record: @post, message: "New post").deliver(User.all)
4
4
 
5
- class <%= class_name %>Notifier < Noticed::Event
5
+ class <%= class_name %>Notifier < ApplicationNotifier
6
6
  # Add your delivery methods
7
7
  #
8
8
  # deliver_by :email do |config|
@@ -7,9 +7,15 @@ module Noticed
7
7
 
8
8
  attr_reader :config, :event
9
9
 
10
- def perform(delivery_method_name, event)
11
- @event = event
12
- @config = event.bulk_delivery_methods.fetch(delivery_method_name).config
10
+ def perform(delivery_method_name, event, recipients: nil, params: {}, overrides: {})
11
+ # Ephemeral notifications
12
+ if event.is_a? String
13
+ @event = @notification.event
14
+ @config = overrides
15
+ else
16
+ @event = event
17
+ @config = event.bulk_delivery_methods.fetch(delivery_method_name).config.merge(overrides)
18
+ end
13
19
 
14
20
  return false if config.has_key?(:if) && !evaluate_option(:if)
15
21
  return false if config.has_key?(:unless) && evaluate_option(:unless)
@@ -0,0 +1,11 @@
1
+ module Noticed
2
+ module BulkDeliveryMethods
3
+ class Test < DeliveryMethod
4
+ class_attribute :delivered, default: []
5
+
6
+ def deliver
7
+ delivered << notification
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,17 +1,26 @@
1
1
  module Noticed
2
- class DeliveryMethod < ApplicationJob
2
+ class DeliveryMethod < Noticed.parent_class.constantize
3
3
  include ApiClient
4
4
  include RequiredOptions
5
5
 
6
+ extend ActiveModel::Callbacks
7
+ define_model_callbacks :deliver
8
+
6
9
  class_attribute :logger, default: Rails.logger
7
10
 
8
11
  attr_reader :config, :event, :notification
9
12
  delegate :recipient, to: :notification
10
13
  delegate :record, :params, to: :event
11
14
 
12
- def perform(delivery_method_name, notification, overrides: {})
13
- @notification = notification
14
- @event = notification.event
15
+ def perform(delivery_method_name, notification, recipient: nil, params: {}, overrides: {})
16
+ # Ephemeral notifications
17
+ if notification.is_a? String
18
+ @notification = notification.constantize.new_with_params(recipient, params)
19
+ @event = @notification.event
20
+ else
21
+ @notification = notification
22
+ @event = notification.event
23
+ end
15
24
 
16
25
  # Look up config from Notifier and merge overrides
17
26
  @config = event.delivery_methods.fetch(delivery_method_name).config.merge(overrides)
@@ -19,7 +28,9 @@ module Noticed
19
28
  return false if config.has_key?(:if) && !evaluate_option(:if)
20
29
  return false if config.has_key?(:unless) && evaluate_option(:unless)
21
30
 
22
- deliver
31
+ run_callbacks :deliver do
32
+ deliver
33
+ end
23
34
  end
24
35
 
25
36
  def deliver
@@ -14,7 +14,7 @@ module Noticed
14
14
  def send_notification(device_token)
15
15
  post_request("https://fcm.googleapis.com/v1/projects/#{credentials[:project_id]}/messages:send",
16
16
  headers: {authorization: "Bearer #{access_token}"},
17
- json: notification.instance_exec(device_token, &config[:json]))
17
+ json: format_notification(device_token))
18
18
  rescue Noticed::ResponseUnsuccessful => exception
19
19
  if exception.response.code == "404" && config[:invalid_token]
20
20
  notification.instance_exec(device_token, &config[:invalid_token])
@@ -23,6 +23,15 @@ module Noticed
23
23
  end
24
24
  end
25
25
 
26
+ def format_notification(device_token)
27
+ method = config[:json]
28
+ if method.is_a?(Symbol) && event.respond_to?(method)
29
+ event.send(method, device_token)
30
+ else
31
+ notification.instance_exec(device_token, &method)
32
+ end
33
+ end
34
+
26
35
  def credentials
27
36
  @credentials ||= begin
28
37
  value = evaluate_option(:credentials)
@@ -32,7 +32,12 @@ module Noticed
32
32
  def format_notification(apn)
33
33
  apn.topic = evaluate_option(:bundle_identifier)
34
34
 
35
- if (method = config[:format])
35
+ method = config[:format]
36
+ # Call method on Notifier if defined
37
+ if method&.is_a?(Symbol) && event.respond_to?(method)
38
+ event.send(method, apn)
39
+ # If Proc, evaluate it on the Notification
40
+ elsif method&.respond_to?(:call)
36
41
  notification.instance_exec(apn, &method)
37
42
  elsif notification.params.try(:has_key?, :message)
38
43
  apn.alert = notification.params[:message]
@@ -70,9 +75,9 @@ module Noticed
70
75
  def connection_pool_options
71
76
  {
72
77
  auth_method: :token,
73
- cert_path: StringIO.new(config.fetch(:apns_key)),
74
- key_id: config.fetch(:key_id),
75
- team_id: config.fetch(:team_id)
78
+ cert_path: StringIO.new(evaluate_option(:apns_key)),
79
+ key_id: evaluate_option(:key_id),
80
+ team_id: evaluate_option(:team_id)
76
81
  }
77
82
  end
78
83
 
@@ -1,5 +1,11 @@
1
1
  module Noticed
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Noticed
4
+
5
+ initializer "noticed.has_notifications" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ include Noticed::HasNotifications
8
+ end
9
+ end
4
10
  end
5
11
  end
@@ -0,0 +1,49 @@
1
+ module Noticed
2
+ module HasNotifications
3
+ # Defines a method for the association and a before_destroy callback to remove notifications
4
+ # where this record is a param
5
+ #
6
+ # class User < ApplicationRecord
7
+ # has_noticed_notifications
8
+ # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
9
+ # end
10
+ #
11
+ # @user.notifications_as_user
12
+ # @user.notifications_as_owner
13
+
14
+ extend ActiveSupport::Concern
15
+
16
+ class_methods do
17
+ def has_noticed_notifications(param_name: model_name.singular, **options)
18
+ define_method :"notifications_as_#{param_name}" do
19
+ model = options.fetch(:model_name, "Noticed::Event").constantize
20
+ case current_adapter
21
+ when "postgresql", "postgis"
22
+ model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
23
+ when "mysql2"
24
+ model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
25
+ when "sqlite3"
26
+ model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
27
+ else
28
+ # This will perform an exact match which isn't ideal
29
+ model.where(params: {param_name.to_sym => self})
30
+ end
31
+ end
32
+
33
+ if options.fetch(:destroy, true)
34
+ before_destroy do
35
+ send(:"notifications_as_#{param_name}").destroy_all
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def current_adapter
42
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
43
+ ActiveRecord::Base.connection_db_config.adapter
44
+ else
45
+ ActiveRecord::Base.connection_config[:adapter]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "2.0.3"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -14,12 +14,14 @@ module Noticed
14
14
  autoload :BulkDeliveryMethod, "noticed/bulk_delivery_method"
15
15
  autoload :Coder, "noticed/coder"
16
16
  autoload :DeliveryMethod, "noticed/delivery_method"
17
+ autoload :HasNotifications, "noticed/has_notifications"
17
18
  autoload :RequiredOptions, "noticed/required_options"
18
19
  autoload :Translation, "noticed/translation"
19
20
 
20
21
  module BulkDeliveryMethods
21
22
  autoload :Discord, "noticed/bulk_delivery_methods/discord"
22
23
  autoload :Slack, "noticed/bulk_delivery_methods/slack"
24
+ autoload :Test, "noticed/bulk_delivery_methods/test"
23
25
  autoload :Webhook, "noticed/bulk_delivery_methods/webhook"
24
26
  end
25
27
 
@@ -39,6 +41,9 @@ module Noticed
39
41
  autoload :Webhook, "noticed/delivery_methods/webhook"
40
42
  end
41
43
 
44
+ mattr_accessor :parent_class
45
+ @@parent_class = "Noticed::ApplicationJob"
46
+
42
47
  class ValidationError < StandardError
43
48
  end
44
49
 
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.3
4
+ version: 2.1.0
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-20 00:00:00.000000000 Z
11
+ date: 2024-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,13 +43,17 @@ files:
43
43
  - app/models/concerns/noticed/readable.rb
44
44
  - app/models/noticed/application_record.rb
45
45
  - app/models/noticed/deliverable/deliver_by.rb
46
+ - app/models/noticed/ephemeral.rb
46
47
  - app/models/noticed/event.rb
47
48
  - app/models/noticed/notification.rb
48
49
  - db/migrate/20231215190233_create_noticed_tables.rb
50
+ - db/migrate/20240129184740_add_notifications_count_to_noticed_event.rb
49
51
  - lib/generators/noticed/delivery_method_generator.rb
50
52
  - lib/generators/noticed/install_generator.rb
51
53
  - lib/generators/noticed/notifier_generator.rb
52
54
  - lib/generators/noticed/templates/README
55
+ - lib/generators/noticed/templates/application_delivery_method.rb.tt
56
+ - lib/generators/noticed/templates/application_notifier.rb.tt
53
57
  - lib/generators/noticed/templates/delivery_method.rb.tt
54
58
  - lib/generators/noticed/templates/notifier.rb.tt
55
59
  - lib/noticed.rb
@@ -57,6 +61,7 @@ files:
57
61
  - lib/noticed/bulk_delivery_method.rb
58
62
  - lib/noticed/bulk_delivery_methods/discord.rb
59
63
  - lib/noticed/bulk_delivery_methods/slack.rb
64
+ - lib/noticed/bulk_delivery_methods/test.rb
60
65
  - lib/noticed/bulk_delivery_methods/webhook.rb
61
66
  - lib/noticed/coder.rb
62
67
  - lib/noticed/delivery_method.rb
@@ -72,6 +77,7 @@ files:
72
77
  - lib/noticed/delivery_methods/vonage_sms.rb
73
78
  - lib/noticed/delivery_methods/webhook.rb
74
79
  - lib/noticed/engine.rb
80
+ - lib/noticed/has_notifications.rb
75
81
  - lib/noticed/required_options.rb
76
82
  - lib/noticed/translation.rb
77
83
  - lib/noticed/version.rb
@@ -94,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
100
  - !ruby/object:Gem::Version
95
101
  version: '0'
96
102
  requirements: []
97
- rubygems_version: 3.5.3
103
+ rubygems_version: 3.5.4
98
104
  signing_key:
99
105
  specification_version: 4
100
106
  summary: Notifications for Ruby on Rails applications