noticed 2.0.0 → 2.1.1

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.
@@ -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
 
@@ -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
@@ -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,13 +4,11 @@ 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) }
11
11
 
12
12
  delegate :params, :record, to: :event
13
-
14
- attribute :params, default: {}
15
13
  end
16
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
@@ -7,12 +7,21 @@ module Noticed
7
7
  class NotifierGenerator < Rails::Generators::NamedBase
8
8
  include Rails::Generators::ResourceHelpers
9
9
 
10
+ check_class_collision suffix: "Notifier"
11
+
10
12
  source_root File.expand_path("../templates", __FILE__)
11
13
 
12
14
  desc "Generates a notification with the given NAME."
13
15
 
14
16
  def generate_notification
15
- template "notifier.rb", "app/notifiers/#{file_path}.rb"
17
+ template "application_notifier.rb", "app/notifiers/application_notifier.rb"
18
+ template "notifier.rb", "app/notifiers/#{file_path}_notifier.rb"
19
+ end
20
+
21
+ private
22
+
23
+ def file_name # :doc:
24
+ @_file_name ||= super.sub(/_notifier\z/i, "")
16
25
  end
17
26
  end
18
27
  end
@@ -0,0 +1,2 @@
1
+ class ApplicationDeliveryMethod < Noticed::DeliveryMethod
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationNotifier < Noticed::Event
2
+ end
@@ -1,12 +1,8 @@
1
- class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethods::Base
1
+ class DeliveryMethods::<%= class_name %> < ApplicationDeliveryMethod
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
@@ -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 %> < 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,16 +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
13
+ delegate :record, :params, to: :event
10
14
 
11
- def perform(delivery_method_name, notification, overrides: {})
12
- @notification = notification
13
- @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
14
24
 
15
25
  # Look up config from Notifier and merge overrides
16
26
  @config = event.delivery_methods.fetch(delivery_method_name).config.merge(overrides)
@@ -18,7 +28,9 @@ module Noticed
18
28
  return false if config.has_key?(:if) && !evaluate_option(:if)
19
29
  return false if config.has_key?(:unless) && evaluate_option(:unless)
20
30
 
21
- deliver
31
+ run_callbacks :deliver do
32
+ deliver
33
+ end
22
34
  end
23
35
 
24
36
  def deliver
@@ -39,7 +51,7 @@ module Noticed
39
51
 
40
52
  # Call method if symbol and matching method on Notifier
41
53
  elsif option.is_a?(Symbol) && event.respond_to?(option)
42
- event.send(option, self)
54
+ event.send(option, notification)
43
55
 
44
56
  # Return the value
45
57
  else
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.1"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -14,12 +14,15 @@ 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"
18
+ autoload :NotificationChannel, "noticed/notification_channel"
17
19
  autoload :RequiredOptions, "noticed/required_options"
18
20
  autoload :Translation, "noticed/translation"
19
21
 
20
22
  module BulkDeliveryMethods
21
23
  autoload :Discord, "noticed/bulk_delivery_methods/discord"
22
24
  autoload :Slack, "noticed/bulk_delivery_methods/slack"
25
+ autoload :Test, "noticed/bulk_delivery_methods/test"
23
26
  autoload :Webhook, "noticed/bulk_delivery_methods/webhook"
24
27
  end
25
28
 
@@ -39,6 +42,9 @@ module Noticed
39
42
  autoload :Webhook, "noticed/delivery_methods/webhook"
40
43
  end
41
44
 
45
+ mattr_accessor :parent_class
46
+ @@parent_class = "Noticed::ApplicationJob"
47
+
42
48
  class ValidationError < StandardError
43
49
  end
44
50