From 92ba2d7c9b4e5cdb2b6005cbbbb5c4d6f08d40cd Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Fri, 9 Sep 2022 18:29:40 +0200 Subject: [PATCH 01/42] Add `rpush` --- Gemfile | 1 + Gemfile.lock | 20 + config/initializers/rpush.rb | 136 +++++++ db/migrate/20220909161542_add_rpush.rb | 352 ++++++++++++++++++ .../20220909161543_rpush_2_0_0_updates.rb | 71 ++++ .../20220909161544_rpush_2_1_0_updates.rb | 11 + .../20220909161545_rpush_2_6_0_updates.rb | 10 + .../20220909161546_rpush_2_7_0_updates.rb | 12 + .../20220909161547_rpush_3_0_0_updates.rb | 11 + .../20220909161548_rpush_3_0_1_updates.rb | 13 + .../20220909161549_rpush_3_1_0_add_pushy.rb | 9 + .../20220909161550_rpush_3_1_1_updates.rb | 15 + .../20220909161551_rpush_3_2_0_add_apns_p8.rb | 15 + .../20220909161552_rpush_3_2_4_updates.rb | 9 + .../20220909161553_rpush_3_3_0_updates.rb | 9 + .../20220909161554_rpush_3_3_1_updates.rb | 11 + .../20220909161555_rpush_4_1_0_updates.rb | 9 + .../20220909161556_rpush_4_1_1_updates.rb | 9 + .../20220909161557_rpush_4_2_0_updates.rb | 10 + db/schema.rb | 69 ++++ 20 files changed, 802 insertions(+) create mode 100644 config/initializers/rpush.rb create mode 100644 db/migrate/20220909161542_add_rpush.rb create mode 100644 db/migrate/20220909161543_rpush_2_0_0_updates.rb create mode 100644 db/migrate/20220909161544_rpush_2_1_0_updates.rb create mode 100644 db/migrate/20220909161545_rpush_2_6_0_updates.rb create mode 100644 db/migrate/20220909161546_rpush_2_7_0_updates.rb create mode 100644 db/migrate/20220909161547_rpush_3_0_0_updates.rb create mode 100644 db/migrate/20220909161548_rpush_3_0_1_updates.rb create mode 100644 db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb create mode 100644 db/migrate/20220909161550_rpush_3_1_1_updates.rb create mode 100644 db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb create mode 100644 db/migrate/20220909161552_rpush_3_2_4_updates.rb create mode 100644 db/migrate/20220909161553_rpush_3_3_0_updates.rb create mode 100644 db/migrate/20220909161554_rpush_3_3_1_updates.rb create mode 100644 db/migrate/20220909161555_rpush_4_1_0_updates.rb create mode 100644 db/migrate/20220909161556_rpush_4_1_1_updates.rb create mode 100644 db/migrate/20220909161557_rpush_4_2_0_updates.rb diff --git a/Gemfile b/Gemfile index dec8be79..e4e5d085 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem "haml", "~> 6.1" gem "hcaptcha", "~> 7.0" gem "mini_magick" gem "oj" +gem "rpush" gem "rqrcode" gem "rolify", "~> 6.0" diff --git a/Gemfile.lock b/Gemfile.lock index 48a91b8f..db901422 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,6 +197,8 @@ GEM hashie (5.0.0) hcaptcha (7.1.0) json + hkdf (0.3.0) + http-2 (0.11.0) http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) @@ -268,6 +270,10 @@ GEM multipart-post (2.1.1) naught (1.1.0) nested_form (0.3.2) + net-http-persistent (4.0.1) + connection_pool (~> 2.2) + net-http2 (0.18.4) + http-2 (~> 0.11) net-imap (0.3.4) date net-protocol @@ -370,6 +376,16 @@ GEM rexml (3.2.5) rolify (6.0.0) rotp (6.2.0) + rpush (7.0.1) + activesupport (>= 5.2) + jwt (>= 1.5.6) + multi_json (~> 1.0) + net-http-persistent + net-http2 (~> 0.18, >= 0.18.3) + railties + rainbow + thor (>= 0.18.1, < 2.0) + webpush (~> 1.0) rqrcode (2.1.2) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -506,6 +522,9 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) + webpush (1.1.0) + hkdf (~> 0.2) + jwt (~> 2.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -567,6 +586,7 @@ DEPENDENCIES redcarpet redis rolify (~> 6.0) + rpush rqrcode rspec-its (~> 1.3) rspec-mocks diff --git a/config/initializers/rpush.rb b/config/initializers/rpush.rb new file mode 100644 index 00000000..e8908d7f --- /dev/null +++ b/config/initializers/rpush.rb @@ -0,0 +1,136 @@ +Rpush.configure do |config| + + # Supported clients are :active_record and :redis + config.client = :active_record + + # Options passed to Redis.new + # config.redis_options = {} + + # Frequency in seconds to check for new notifications. + config.push_poll = 2 + + # The maximum number of notifications to load from the store every `push_poll` seconds. + # If some notifications are still enqueued internally, Rpush will load the batch_size less + # the number enqueued. An exception to this is if the service is able to receive multiple + # notification payloads over the connection with a single write, such as APNs. + config.batch_size = 100 + + # Path to write PID file. Relative to current directory unless absolute. + config.pid_file = 'tmp/rpush.pid' + + # Path to log file. Relative to current directory unless absolute. + config.log_file = 'log/rpush.log' + + config.log_level = (defined?(Rails) && Rails.logger) ? Rails.logger.level : ::Logger::Severity::INFO + + # Define a custom logger. + # config.logger = MyLogger.new + + # By default in foreground mode logs are directed both to the logger and to stdout. + # If the logger goes to stdout, you can disable foreground logging to avoid duplication. + # config.foreground_logging = false + + # config.apns.feedback_receiver.enabled = true + # config.apns.feedback_receiver.frequency = 60 + +end + +Rpush.reflect do |on| + + # Called with a Rpush::Apns::Feedback instance when feedback is received + # from the APNs that a notification has failed to be delivered. + # Further notifications should not be sent to the device. + # on.apns_feedback do |feedback| + # end + + # Called when a notification is queued internally for delivery. + # The internal queue for each app runner can be inspected: + # + # Rpush::Daemon::AppRunner.status + # + # on.notification_enqueued do |notification| + # end + + # Called when a notification is successfully delivered. + # on.notification_delivered do |notification| + # end + + # Called when notification delivery failed. + # Call 'error_code' and 'error_description' on the notification for the cause. + # on.notification_failed do |notification| + # end + + # Called when the notification delivery failed and only the notification ID + # is present in memory. + # on.notification_id_failed do |app, notification_id, error_code, error_description| + # end + + # Called when a notification will be retried at a later date. + # Call 'deliver_after' on the notification for the next delivery date + # and 'retries' for the number of times this notification has been retried. + # on.notification_will_retry do |notification| + # end + + # Called when a notification will be retried and only the notification ID + # is present in memory. + # on.notification_id_will_retry do |app, notification_id, retry_after| + # end + + # Called when a TCP connection is lost and will be reconnected. + # on.tcp_connection_lost do |app, error| + # end + + # Called for each recipient which successfully receives a notification. This + # can occur more than once for the same notification when there are multiple + # recipients. + # on.gcm_delivered_to_recipient do |notification, registration_id| + # end + + # Called for each recipient which fails to receive a notification. This + # can occur more than once for the same notification when there are multiple + # recipients. (do not handle invalid registration IDs here) + # on.gcm_failed_to_recipient do |notification, error, registration_id| + # end + + # Called when the GCM returns a canonical registration ID. + # You will need to replace old_id with canonical_id in your records. + # on.gcm_canonical_id do |old_id, canonical_id| + # end + + # Called when the GCM returns a failure that indicates an invalid registration id. + # You will need to delete the registration_id from your records. + # on.gcm_invalid_registration_id do |app, error, registration_id| + # end + + # Called when an SSL certificate will expire within 1 month. + # Implement on.error to catch errors raised when the certificate expires. + # on.ssl_certificate_will_expire do |app, expiration_time| + # end + + # Called when an SSL certificate has been revoked. + # on.ssl_certificate_revoked do |app, error| + # end + + # Called when the ADM returns a canonical registration ID. + # You will need to replace old_id with canonical_id in your records. + # on.adm_canonical_id do |old_id, canonical_id| + # end + + # Called when Failed to deliver to ADM. Check the 'reason' string for further + # explanations. + # + # If the reason is the string 'Unregistered', you should remove + # this registration id from your records. + # on.adm_failed_to_recipient do |notification, registration_id, reason| + # end + + # Called when Failed to deliver to WNS. Check the 'reason' string for further + # explanations. + # You should remove this uri from your records + # on.wns_invalid_channel do |notification, uri, reason| + # end + + # Called when an exception is raised. + # on.error do |error| + # end +end diff --git a/db/migrate/20220909161542_add_rpush.rb b/db/migrate/20220909161542_add_rpush.rb new file mode 100644 index 00000000..d5ed63a1 --- /dev/null +++ b/db/migrate/20220909161542_add_rpush.rb @@ -0,0 +1,352 @@ +# NOTE TO THE CURIOUS. +# +# Congratulations on being a diligent developer and vetting the migrations +# added to your project! +# +# You're probably thinking "This migration is huge!". It is, but that doesn't +# mean it'll take a long time to run, or that the reason for it being +# this size is because of lousy developers. +# +# Rpush used to be known as Rapns. In an effort to reduce clutter in db/migrate +# for new users of Rpush, what you see below is a concatenation of the +# migrations added to Rapns over its lifetime. +# +# The reason for concatenating old migrations - instead of producing a new +# one that attempts to recreate their accumulative state - is that I don't +# want to introduce any bugs by writing a new migration. +# +# So while this looks like a scary amount of code, it is in fact the safest +# approach. The constituent parts of this migration have been executed +# many times, by many people! + +class AddRpush < ActiveRecord::Migration[5.0] + def self.migrations + [CreateRapnsNotifications, CreateRapnsFeedback, + AddAlertIsJsonToRapnsNotifications, AddAppToRapns, + CreateRapnsApps, AddGcm, AddWpns, AddAdm, RenameRapnsToRpush, + AddFailAfterToRpushNotifications] + end + + def self.up + migrations.map(&:up) + end + + def self.down + migrations.reverse.each do |m| + begin + m.down + rescue ActiveRecord::StatementInvalid => e + p e + end + end + end + + class CreateRapnsNotifications < ActiveRecord::Migration[5.0] + def self.up + create_table :rapns_notifications do |t| + t.integer :badge, null: true + t.string :device_token, null: false, limit: 64 + t.string :sound, null: true, default: "1.aiff" + t.string :alert, null: true + t.text :attributes_for_device, null: true + t.integer :expiry, null: false, default: 1.day.to_i + t.boolean :delivered, null: false, default: false + t.timestamp :delivered_at, null: true + t.boolean :failed, null: false, default: false + t.timestamp :failed_at, null: true + t.integer :error_code, null: true + t.string :error_description, null: true + t.timestamp :deliver_after, null: true + t.timestamps + end + + add_index :rapns_notifications, [:delivered, :failed, :deliver_after], name: 'index_rapns_notifications_multi' + end + + def self.down + if index_name_exists?(:rapns_notifications, 'index_rapns_notifications_multi') + remove_index :rapns_notifications, name: 'index_rapns_notifications_multi' + end + + drop_table :rapns_notifications + end + end + + class CreateRapnsFeedback < ActiveRecord::Migration[5.0] + def self.up + create_table :rapns_feedback do |t| + t.string :device_token, null: false, limit: 64 + t.timestamp :failed_at, null: false + t.timestamps + end + + add_index :rapns_feedback, :device_token + end + + def self.down + if index_name_exists?(:rapns_feedback, :index_rapns_feedback_on_device_token) + remove_index :rapns_feedback, name: :index_rapns_feedback_on_device_token + end + + drop_table :rapns_feedback + end + end + + class AddAlertIsJsonToRapnsNotifications < ActiveRecord::Migration[5.0] + def self.up + add_column :rapns_notifications, :alert_is_json, :boolean, null: true, default: false + end + + def self.down + remove_column :rapns_notifications, :alert_is_json + end + end + + class AddAppToRapns < ActiveRecord::Migration[5.0] + def self.up + add_column :rapns_notifications, :app, :string, null: true + add_column :rapns_feedback, :app, :string, null: true + end + + def self.down + remove_column :rapns_notifications, :app + remove_column :rapns_feedback, :app + end + end + + class CreateRapnsApps < ActiveRecord::Migration[5.0] + def self.up + create_table :rapns_apps do |t| + t.string :key, null: false + t.string :environment, null: false + t.text :certificate, null: false + t.string :password, null: true + t.integer :connections, null: false, default: 1 + t.timestamps + end + end + + def self.down + drop_table :rapns_apps + end + end + + class AddGcm < ActiveRecord::Migration[5.0] + module Rapns + class App < ActiveRecord::Base + self.table_name = 'rapns_apps' + end + + class Notification < ActiveRecord::Base + belongs_to :app + self.table_name = 'rapns_notifications' + end + end + + def self.up + add_column :rapns_notifications, :type, :string, null: true + add_column :rapns_apps, :type, :string, null: true + + AddGcm::Rapns::Notification.update_all type: 'Rapns::Apns::Notification' + AddGcm::Rapns::App.update_all type: 'Rapns::Apns::App' + + change_column :rapns_notifications, :type, :string, null: false + change_column :rapns_apps, :type, :string, null: false + change_column :rapns_notifications, :device_token, :string, null: true, limit: 64 + change_column :rapns_notifications, :expiry, :integer, null: true, default: 1.day.to_i + change_column :rapns_apps, :environment, :string, null: true + change_column :rapns_apps, :certificate, :text, null: true, default: nil + + change_column :rapns_notifications, :error_description, :text, null: true, default: nil + change_column :rapns_notifications, :sound, :string, default: 'default' + + rename_column :rapns_notifications, :attributes_for_device, :data + rename_column :rapns_apps, :key, :name + + add_column :rapns_apps, :auth_key, :string, null: true + + add_column :rapns_notifications, :collapse_key, :string, null: true + add_column :rapns_notifications, :delay_while_idle, :boolean, null: false, default: false + + reg_ids_type = ActiveRecord::Base.connection.adapter_name.include?('Mysql') ? :mediumtext : :text + add_column :rapns_notifications, :registration_ids, reg_ids_type, null: true + add_column :rapns_notifications, :app_id, :integer, null: true + add_column :rapns_notifications, :retries, :integer, null: true, default: 0 + + AddGcm::Rapns::Notification.reset_column_information + AddGcm::Rapns::App.reset_column_information + + AddGcm::Rapns::App.all.each do |app| + AddGcm::Rapns::Notification.where(app: app.name).update_all(app_id: app.id) + end + + change_column :rapns_notifications, :app_id, :integer, null: false + remove_column :rapns_notifications, :app + + if index_name_exists?(:rapns_notifications, "index_rapns_notifications_multi") + remove_index :rapns_notifications, name: "index_rapns_notifications_multi" + elsif index_name_exists?(:rapns_notifications, "index_rapns_notifications_on_delivered_failed_deliver_after") + remove_index :rapns_notifications, name: "index_rapns_notifications_on_delivered_failed_deliver_after" + end + + add_index :rapns_notifications, [:app_id, :delivered, :failed, :deliver_after], name: "index_rapns_notifications_multi" + end + + def self.down + AddGcm::Rapns::Notification.where(type: 'Rapns::Gcm::Notification').delete_all + + remove_column :rapns_notifications, :type + remove_column :rapns_apps, :type + + change_column :rapns_notifications, :device_token, :string, null: false, limit: 64 + change_column :rapns_notifications, :expiry, :integer, null: false, default: 1.day.to_i + change_column :rapns_apps, :environment, :string, null: false + change_column :rapns_apps, :certificate, :text, null: false + + change_column :rapns_notifications, :error_description, :string, null: true, default: nil + change_column :rapns_notifications, :sound, :string, default: '1.aiff' + + rename_column :rapns_notifications, :data, :attributes_for_device + rename_column :rapns_apps, :name, :key + + remove_column :rapns_apps, :auth_key + + remove_column :rapns_notifications, :collapse_key + remove_column :rapns_notifications, :delay_while_idle + remove_column :rapns_notifications, :registration_ids + remove_column :rapns_notifications, :retries + + add_column :rapns_notifications, :app, :string, null: true + + AddGcm::Rapns::Notification.reset_column_information + AddGcm::Rapns::App.reset_column_information + + AddGcm::Rapns::App.all.each do |app| + AddGcm::Rapns::Notification.where(app_id: app.id).update_all(app: app.key) + end + + if index_name_exists?(:rapns_notifications, :index_rapns_notifications_multi) + remove_index :rapns_notifications, name: :index_rapns_notifications_multi + end + + remove_column :rapns_notifications, :app_id + + add_index :rapns_notifications, [:delivered, :failed, :deliver_after], name: :index_rapns_notifications_multi + end + end + + class AddWpns < ActiveRecord::Migration[5.0] + module Rapns + class Notification < ActiveRecord::Base + self.table_name = 'rapns_notifications' + end + end + + def self.up + add_column :rapns_notifications, :uri, :string, null: true + end + + def self.down + AddWpns::Rapns::Notification.where(type: 'Rapns::Wpns::Notification').delete_all + remove_column :rapns_notifications, :uri + end + end + + class AddAdm < ActiveRecord::Migration[5.0] + module Rapns + class Notification < ActiveRecord::Base + self.table_name = 'rapns_notifications' + end + end + + def self.up + add_column :rapns_apps, :client_id, :string, null: true + add_column :rapns_apps, :client_secret, :string, null: true + add_column :rapns_apps, :access_token, :string, null: true + add_column :rapns_apps, :access_token_expiration, :datetime, null: true + end + + def self.down + AddAdm::Rapns::Notification.where(type: 'Rapns::Adm::Notification').delete_all + + remove_column :rapns_apps, :client_id + remove_column :rapns_apps, :client_secret + remove_column :rapns_apps, :access_token + remove_column :rapns_apps, :access_token_expiration + end + end + + class RenameRapnsToRpush < ActiveRecord::Migration[5.0] + module Rpush + class App < ActiveRecord::Base + self.table_name = 'rpush_apps' + end + + class Notification < ActiveRecord::Base + self.table_name = 'rpush_notifications' + end + end + + def self.update_type(model, from, to) + model.where(type: from).update_all(type: to) + end + + def self.up + rename_table :rapns_notifications, :rpush_notifications + rename_table :rapns_apps, :rpush_apps + rename_table :rapns_feedback, :rpush_feedback + + if index_name_exists?(:rpush_notifications, :index_rapns_notifications_multi) + rename_index :rpush_notifications, :index_rapns_notifications_multi, :index_rpush_notifications_multi + end + + if index_name_exists?(:rpush_feedback, :index_rapns_feedback_on_device_token) + rename_index :rpush_feedback, :index_rapns_feedback_on_device_token, :index_rpush_feedback_on_device_token + end + + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Apns::Notification', 'Rpush::Apns::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Gcm::Notification', 'Rpush::Gcm::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Adm::Notification', 'Rpush::Adm::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Wpns::Notification', 'Rpush::Wpns::Notification') + + update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Apns::App', 'Rpush::Apns::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Gcm::App', 'Rpush::Gcm::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Adm::App', 'Rpush::Adm::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Wpns::App', 'Rpush::Wpns::App') + end + + def self.down + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Apns::Notification', 'Rapns::Apns::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Gcm::Notification', 'Rapns::Gcm::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Adm::Notification', 'Rapns::Adm::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Wpns::Notification', 'Rapns::Wpns::Notification') + + update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Apns::App', 'Rapns::Apns::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Gcm::App', 'Rapns::Gcm::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Adm::App', 'Rapns::Adm::App') + update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Wpns::App', 'Rapns::Wpns::App') + + if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) + rename_index :rpush_notifications, :index_rpush_notifications_multi, :index_rapns_notifications_multi + end + + if index_name_exists?(:rpush_feedback, :index_rpush_feedback_on_device_token) + rename_index :rpush_feedback, :index_rpush_feedback_on_device_token, :index_rapns_feedback_on_device_token + end + + rename_table :rpush_notifications, :rapns_notifications + rename_table :rpush_apps, :rapns_apps + rename_table :rpush_feedback, :rapns_feedback + end + end + + class AddFailAfterToRpushNotifications < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :fail_after, :timestamp, null: true + end + + def self.down + remove_column :rpush_notifications, :fail_after + end + end +end diff --git a/db/migrate/20220909161543_rpush_2_0_0_updates.rb b/db/migrate/20220909161543_rpush_2_0_0_updates.rb new file mode 100644 index 00000000..ccf041c8 --- /dev/null +++ b/db/migrate/20220909161543_rpush_2_0_0_updates.rb @@ -0,0 +1,71 @@ +class Rpush200Updates < ActiveRecord::Migration[5.0] + module Rpush + class App < ActiveRecord::Base + self.table_name = 'rpush_apps' + end + + class Notification < ActiveRecord::Base + self.table_name = 'rpush_notifications' + end + end + + def self.update_type(model, from, to) + model.where(type: from).update_all(type: to) + end + + def self.up + add_column :rpush_notifications, :processing, :boolean, null: false, default: false + add_column :rpush_notifications, :priority, :integer, null: true + + if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) + remove_index :rpush_notifications, name: :index_rpush_notifications_multi + end + + add_index :rpush_notifications, [:delivered, :failed], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + + rename_column :rpush_feedback, :app, :app_id + + if postgresql? + execute('ALTER TABLE rpush_feedback ALTER COLUMN app_id TYPE integer USING (trim(app_id)::integer)') + else + change_column :rpush_feedback, :app_id, :integer + end + + [:Apns, :Gcm, :Wpns, :Adm].each do |service| + update_type(Rpush200Updates::Rpush::App, "Rpush::#{service}::App", "Rpush::Client::ActiveRecord::#{service}::App") + update_type(Rpush200Updates::Rpush::Notification, "Rpush::#{service}::Notification", "Rpush::Client::ActiveRecord::#{service}::Notification") + end + end + + def self.down + [:Apns, :Gcm, :Wpns, :Adm].each do |service| + update_type(Rpush200Updates::Rpush::App, "Rpush::Client::ActiveRecord::#{service}::App", "Rpush::#{service}::App") + update_type(Rpush200Updates::Rpush::Notification, "Rpush::Client::ActiveRecord::#{service}::Notification", "Rpush::#{service}::Notification") + end + + change_column :rpush_feedback, :app_id, :string + rename_column :rpush_feedback, :app_id, :app + + if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) + remove_index :rpush_notifications, name: :index_rpush_notifications_multi + end + + add_index :rpush_notifications, [:app_id, :delivered, :failed, :deliver_after], name: 'index_rpush_notifications_multi' + + remove_column :rpush_notifications, :priority + remove_column :rpush_notifications, :processing + end + + def self.adapter_name + env = (defined?(Rails) && Rails.env) ? Rails.env : 'development' + if ActiveRecord::VERSION::MAJOR > 6 + ActiveRecord::Base.configurations.configs_for(env_name: env).first.configuration_hash[:adapter] + else + Hash[ActiveRecord::Base.configurations[env].map { |k,v| [k.to_sym,v] }][:adapter] + end + end + + def self.postgresql? + adapter_name =~ /postgresql|postgis/ + end +end diff --git a/db/migrate/20220909161544_rpush_2_1_0_updates.rb b/db/migrate/20220909161544_rpush_2_1_0_updates.rb new file mode 100644 index 00000000..ab7868eb --- /dev/null +++ b/db/migrate/20220909161544_rpush_2_1_0_updates.rb @@ -0,0 +1,11 @@ +class Rpush210Updates < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :url_args, :text, null: true + add_column :rpush_notifications, :category, :string, null: true + end + + def self.down + remove_column :rpush_notifications, :url_args + remove_column :rpush_notifications, :category + end +end diff --git a/db/migrate/20220909161545_rpush_2_6_0_updates.rb b/db/migrate/20220909161545_rpush_2_6_0_updates.rb new file mode 100644 index 00000000..42e30a89 --- /dev/null +++ b/db/migrate/20220909161545_rpush_2_6_0_updates.rb @@ -0,0 +1,10 @@ +class Rpush260Updates < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :content_available, :boolean, default: false + end + + def self.down + remove_column :rpush_notifications, :content_available + end +end + diff --git a/db/migrate/20220909161546_rpush_2_7_0_updates.rb b/db/migrate/20220909161546_rpush_2_7_0_updates.rb new file mode 100644 index 00000000..2dcba35c --- /dev/null +++ b/db/migrate/20220909161546_rpush_2_7_0_updates.rb @@ -0,0 +1,12 @@ +class Rpush270Updates < ActiveRecord::Migration[5.0] + def self.up + change_column :rpush_notifications, :alert, :text + add_column :rpush_notifications, :notification, :text + end + + def self.down + change_column :rpush_notifications, :alert, :string + remove_column :rpush_notifications, :notification + end +end + diff --git a/db/migrate/20220909161547_rpush_3_0_0_updates.rb b/db/migrate/20220909161547_rpush_3_0_0_updates.rb new file mode 100644 index 00000000..77a3046b --- /dev/null +++ b/db/migrate/20220909161547_rpush_3_0_0_updates.rb @@ -0,0 +1,11 @@ +class Rpush300Updates < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :mutable_content, :boolean, default: false + change_column :rpush_notifications, :sound, :string, default: nil + end + + def self.down + remove_column :rpush_notifications, :mutable_content + change_column :rpush_notifications, :sound, :string, default: 'default' + end +end diff --git a/db/migrate/20220909161548_rpush_3_0_1_updates.rb b/db/migrate/20220909161548_rpush_3_0_1_updates.rb new file mode 100644 index 00000000..38da62a1 --- /dev/null +++ b/db/migrate/20220909161548_rpush_3_0_1_updates.rb @@ -0,0 +1,13 @@ +class Rpush301Updates < ActiveRecord::Migration[5.0] + def self.up + change_column_null :rpush_notifications, :mutable_content, false + change_column_null :rpush_notifications, :content_available, false + change_column_null :rpush_notifications, :alert_is_json, false + end + + def self.down + change_column_null :rpush_notifications, :mutable_content, true + change_column_null :rpush_notifications, :content_available, true + change_column_null :rpush_notifications, :alert_is_json, true + end +end diff --git a/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb b/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb new file mode 100644 index 00000000..7037f2f9 --- /dev/null +++ b/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb @@ -0,0 +1,9 @@ +class Rpush310AddPushy < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :external_device_id, :string, null: true + end + + def self.down + remove_column :rpush_notifications, :external_device_id + end +end diff --git a/db/migrate/20220909161550_rpush_3_1_1_updates.rb b/db/migrate/20220909161550_rpush_3_1_1_updates.rb new file mode 100644 index 00000000..68386461 --- /dev/null +++ b/db/migrate/20220909161550_rpush_3_1_1_updates.rb @@ -0,0 +1,15 @@ +class Rpush311Updates < ActiveRecord::Migration[5.0] + def self.up + change_table :rpush_notifications do |t| + t.remove_index name: 'index_rpush_notifications_multi' + t.index [:delivered, :failed, :processing, :deliver_after, :created_at], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + end + end + + def self.down + change_table :rpush_notifications do |t| + t.remove_index name: 'index_rpush_notifications_multi' + t.index [:delivered, :failed], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + end + end +end diff --git a/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb b/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb new file mode 100644 index 00000000..64561ac3 --- /dev/null +++ b/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb @@ -0,0 +1,15 @@ +class Rpush320AddApnsP8 < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_apps, :apn_key, :string, null: true + add_column :rpush_apps, :apn_key_id, :string, null: true + add_column :rpush_apps, :team_id, :string, null: true + add_column :rpush_apps, :bundle_id, :string, null: true + end + + def self.down + remove_column :rpush_apps, :apn_key + remove_column :rpush_apps, :apn_key_id + remove_column :rpush_apps, :team_id + remove_column :rpush_apps, :bundle_id + end +end diff --git a/db/migrate/20220909161552_rpush_3_2_4_updates.rb b/db/migrate/20220909161552_rpush_3_2_4_updates.rb new file mode 100644 index 00000000..da2f4f61 --- /dev/null +++ b/db/migrate/20220909161552_rpush_3_2_4_updates.rb @@ -0,0 +1,9 @@ +class Rpush324Updates < ActiveRecord::Migration[5.0] + def self.up + change_column :rpush_apps, :apn_key, :text, null: true + end + + def self.down + change_column :rpush_apps, :apn_key, :string, null: true + end +end diff --git a/db/migrate/20220909161553_rpush_3_3_0_updates.rb b/db/migrate/20220909161553_rpush_3_3_0_updates.rb new file mode 100644 index 00000000..edd6efc5 --- /dev/null +++ b/db/migrate/20220909161553_rpush_3_3_0_updates.rb @@ -0,0 +1,9 @@ +class Rpush330Updates < ActiveRecord::Migration[5.0] + def self.up + add_column :rpush_notifications, :thread_id, :string, null: true + end + + def self.down + remove_column :rpush_notifications, :thread_id + end +end diff --git a/db/migrate/20220909161554_rpush_3_3_1_updates.rb b/db/migrate/20220909161554_rpush_3_3_1_updates.rb new file mode 100644 index 00000000..bfc07556 --- /dev/null +++ b/db/migrate/20220909161554_rpush_3_3_1_updates.rb @@ -0,0 +1,11 @@ +class Rpush331Updates < ActiveRecord::Migration[5.0] + def self.up + change_column :rpush_notifications, :device_token, :string, null: true + change_column :rpush_feedback, :device_token, :string, null: true + end + + def self.down + change_column :rpush_notifications, :device_token, :string, null: true, limit: 64 + change_column :rpush_feedback, :device_token, :string, null: true, limit: 64 + end +end diff --git a/db/migrate/20220909161555_rpush_4_1_0_updates.rb b/db/migrate/20220909161555_rpush_4_1_0_updates.rb new file mode 100644 index 00000000..a0fbe52f --- /dev/null +++ b/db/migrate/20220909161555_rpush_4_1_0_updates.rb @@ -0,0 +1,9 @@ +class Rpush410Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] + def self.up + add_column :rpush_notifications, :dry_run, :boolean, null: false, default: false + end + + def self.down + remove_column :rpush_notifications, :dry_run + end +end diff --git a/db/migrate/20220909161556_rpush_4_1_1_updates.rb b/db/migrate/20220909161556_rpush_4_1_1_updates.rb new file mode 100644 index 00000000..bde7366d --- /dev/null +++ b/db/migrate/20220909161556_rpush_4_1_1_updates.rb @@ -0,0 +1,9 @@ +class Rpush411Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] + def self.up + add_column :rpush_apps, :feedback_enabled, :boolean, default: true + end + + def self.down + remove_column :rpush_apps, :feedback_enabled + end +end diff --git a/db/migrate/20220909161557_rpush_4_2_0_updates.rb b/db/migrate/20220909161557_rpush_4_2_0_updates.rb new file mode 100644 index 00000000..df13a117 --- /dev/null +++ b/db/migrate/20220909161557_rpush_4_2_0_updates.rb @@ -0,0 +1,10 @@ +class Rpush420Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] + def self.up + add_column :rpush_notifications, :sound_is_json, :boolean, null: true, default: false + end + + def self.down + remove_column :rpush_notifications, :sound_is_json + end +end + diff --git a/db/schema.rb b/db/schema.rb index 094c992c..8d453624 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -183,6 +183,75 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do t.index ["resource_type", "resource_id"], name: "index_roles_on_resource" end + create_table "rpush_apps", force: :cascade do |t| + t.string "name", null: false + t.string "environment" + t.text "certificate" + t.string "password" + t.integer "connections", default: 1, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "type", null: false + t.string "auth_key" + t.string "client_id" + t.string "client_secret" + t.string "access_token" + t.datetime "access_token_expiration" + t.text "apn_key" + t.string "apn_key_id" + t.string "team_id" + t.string "bundle_id" + t.boolean "feedback_enabled", default: true + end + + create_table "rpush_feedback", force: :cascade do |t| + t.string "device_token" + t.datetime "failed_at", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.integer "app_id" + t.index ["device_token"], name: "index_rpush_feedback_on_device_token" + end + + create_table "rpush_notifications", force: :cascade do |t| + t.integer "badge" + t.string "device_token" + t.string "sound" + t.text "alert" + t.text "data" + t.integer "expiry", default: 86400 + t.boolean "delivered", default: false, null: false + t.datetime "delivered_at" + t.boolean "failed", default: false, null: false + t.datetime "failed_at" + t.integer "error_code" + t.text "error_description" + t.datetime "deliver_after" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.boolean "alert_is_json", default: false, null: false + t.string "type", null: false + t.string "collapse_key" + t.boolean "delay_while_idle", default: false, null: false + t.text "registration_ids" + t.integer "app_id", null: false + t.integer "retries", default: 0 + t.string "uri" + t.datetime "fail_after" + t.boolean "processing", default: false, null: false + t.integer "priority" + t.text "url_args" + t.string "category" + t.boolean "content_available", default: false, null: false + t.text "notification" + t.boolean "mutable_content", default: false, null: false + t.string "external_device_id" + t.string "thread_id" + t.boolean "dry_run", default: false, null: false + t.boolean "sound_is_json", default: false + t.index ["delivered", "failed", "processing", "deliver_after", "created_at"], name: "index_rpush_notifications_multi", where: "((NOT delivered) AND (NOT failed))" + end + create_table "services", id: :serial, force: :cascade do |t| t.string "type", null: false t.bigint "user_id", null: false From 4e08f035d1b1f5fd9577fb8e4469bfc8ab194137 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:10:01 +0200 Subject: [PATCH 02/42] Create migration for adding an rpush app for webpush --- db/migrate/20220909220449_add_webpush_app.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 db/migrate/20220909220449_add_webpush_app.rb diff --git a/db/migrate/20220909220449_add_webpush_app.rb b/db/migrate/20220909220449_add_webpush_app.rb new file mode 100644 index 00000000..e527d64c --- /dev/null +++ b/db/migrate/20220909220449_add_webpush_app.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "webpush" + +class AddWebpushApp < ActiveRecord::Migration[6.1] + def up + vapid_keypair = Webpush.generate_key.to_hash + app = Rpush::Webpush::App.new + app.name = "webpush" + app.certificate = vapid_keypair.merge(subject: "user@example.com").to_json # TODO: put an email address here + app.connections = 1 + app.save! + end + + def down + Rpush::Webpush::App.find_by(name: "webpush").destroy! + end +end From 32ab9267ec2901fa64ab354754df81f2b25fb9aa Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:10:21 +0200 Subject: [PATCH 03/42] Add web push subscription model --- app/models/user.rb | 1 + app/models/web_push_subscription.rb | 5 +++++ .../20220910000514_create_web_push_subscriptions.rb | 11 +++++++++++ db/schema.rb | 8 ++++++++ 4 files changed, 25 insertions(+) create mode 100644 app/models/web_push_subscription.rb create mode 100644 db/migrate/20220910000514_create_web_push_subscriptions.rb diff --git a/app/models/user.rb b/app/models/user.rb index 3ec6b9c7..4b97f7d1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,6 +42,7 @@ class User < ApplicationRecord has_many :subscriptions, dependent: :destroy_async has_many :totp_recovery_codes, dependent: :destroy_async + has_many :web_push_subscriptions, dependent: :destroy_async has_one :profile, dependent: :destroy has_one :theme, dependent: :destroy diff --git a/app/models/web_push_subscription.rb b/app/models/web_push_subscription.rb new file mode 100644 index 00000000..dddf28db --- /dev/null +++ b/app/models/web_push_subscription.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class WebPushSubscription < ApplicationRecord + belongs_to :user +end diff --git a/db/migrate/20220910000514_create_web_push_subscriptions.rb b/db/migrate/20220910000514_create_web_push_subscriptions.rb new file mode 100644 index 00000000..45a2dcf4 --- /dev/null +++ b/db/migrate/20220910000514_create_web_push_subscriptions.rb @@ -0,0 +1,11 @@ +class CreateWebPushSubscriptions < ActiveRecord::Migration[6.1] + def change + create_table :web_push_subscriptions do |t| + t.bigint :user_id, null: false + t.json :subscription + t.timestamps + end + + add_index :web_push_subscriptions, :user_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 8d453624..6c2491d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -382,5 +382,13 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do t.index ["user_id"], name: "index_users_roles_on_user_id" end + create_table "web_push_subscriptions", force: :cascade do |t| + t.bigint "user_id", null: false + t.json "subscription" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id" + end + add_foreign_key "profiles", "users" end From bae227be768ef5791577aa2eb5910d380076e521 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:12:42 +0200 Subject: [PATCH 04/42] Add endpoints for getting webpush public key and subscribing --- app/controllers/ajax/web_push_controller.rb | 19 +++++++++++++++++++ config/routes.rb | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 app/controllers/ajax/web_push_controller.rb diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb new file mode 100644 index 00000000..af4e715f --- /dev/null +++ b/app/controllers/ajax/web_push_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Ajax::WebPushController < AjaxController + def key + certificate = Rpush::Webpush::App.find_by(name: "webpush").certificate + + @response[:key] = JSON.parse(certificate)["public_key"] + end + + def subscribe + WebPushSubscription.create!( + user: current_user, + subscription: params[:subscription] + ) + + @response[:status] = :okay + @response[:success] = true + end +end diff --git a/config/routes.rb b/config/routes.rb index 8a753e5b..fe0ef852 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,6 +132,8 @@ Rails.application.routes.draw do post "/list_membership", to: "list#membership", as: :list_membership post "/subscribe", to: "subscription#subscribe", as: :subscribe_answer post "/unsubscribe", to: "subscription#unsubscribe", as: :unsubscribe_answer + get "/webpush/key", to: "web_push#key", as: :webpush_key + post "/webpush", to: "web_push#subscribe", as: :webpush_subscribe end resource :anonymous_block, controller: :anonymous_block, only: %i[create destroy] From 8b98c278dab860d6fef11cae9e13687400463e54 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:25:11 +0200 Subject: [PATCH 05/42] Send push notifications on question create --- app/workers/question_worker.rb | 9 +++++++++ config/initializers/rpush.rb | 2 ++ lib/use_case/question/create.rb | 9 +++++++++ 3 files changed, 20 insertions(+) diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index 9e7a3e2b..be82dfb4 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -10,6 +10,7 @@ class QuestionWorker def perform(user_id, question_id) user = User.find(user_id) question = Question.find(question_id) + webpush_app = Rpush::App.find_by(name: "webpush") user.followers.each do |f| next if f.inbox_locked? @@ -18,6 +19,14 @@ class QuestionWorker next if user.muting?(question.user) Inbox.create(user_id: f.id, question_id: question_id, new: true) + + f.web_push_subscriptions.each do |s| + n = Rpush::Webpush::Notification.new + n.app = webpush_app + n.registration_ids = [s.subscription.symbolize_keys] + n.data = { message: { title: "New question notif title", body: question.content }.to_json } + n.save! + end end rescue StandardError => e logger.info "failed to ask question: #{e.message}" diff --git a/config/initializers/rpush.rb b/config/initializers/rpush.rb index e8908d7f..0aaf129c 100644 --- a/config/initializers/rpush.rb +++ b/config/initializers/rpush.rb @@ -134,3 +134,5 @@ Rpush.reflect do |on| # on.error do |error| # end end + +Rails.application.config.active_record.yaml_column_permitted_classes = [Symbol, Hash] diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index 36ef0205..4426c680 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -30,6 +30,15 @@ module UseCase inbox = ::Inbox.create!(user: target_user, question: question, new: true) + webpush_app = Rpush::App.find_by(name: "webpush") + target_user.web_push_subscriptions.each do |s| + n = Rpush::Webpush::Notification.new + n.app = webpush_app + n.registration_ids = [s.subscription.symbolize_keys] + n.data = { message: { title: "New question notif title", body: question.content }.to_json } + n.save! + end + { status: 201, resource: question, From 2da4767623f918ec486d446c464d4558556282bf Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:42:40 +0200 Subject: [PATCH 06/42] Add JS for subscribing to and receiving push notifications --- app/javascript/packs/application.ts | 2 + .../retrospring/features/webpush/dismiss.ts | 7 +++ .../retrospring/features/webpush/enable.ts | 59 +++++++++++++++++++ .../retrospring/features/webpush/index.ts | 28 +++++++++ app/views/inbox/_push_settings.haml | 5 ++ app/views/layouts/inbox.html.haml | 1 + config/locales/frontend.en.yml | 9 +++ config/locales/views.en.yml | 2 + public/service_worker.js | 12 ++++ 9 files changed, 125 insertions(+) create mode 100644 app/javascript/retrospring/features/webpush/dismiss.ts create mode 100644 app/javascript/retrospring/features/webpush/enable.ts create mode 100644 app/javascript/retrospring/features/webpush/index.ts create mode 100644 app/views/inbox/_push_settings.haml create mode 100644 public/service_worker.js diff --git a/app/javascript/packs/application.ts b/app/javascript/packs/application.ts index e6a85b3e..4d292a2e 100644 --- a/app/javascript/packs/application.ts +++ b/app/javascript/packs/application.ts @@ -15,6 +15,7 @@ import initModeration from 'retrospring/features/moderation'; import initMemes from 'retrospring/features/memes'; import initLocales from 'retrospring/features/locales'; import initFront from 'retrospring/features/front'; +import initWebpush from 'retrospring/features/webpush'; start(); document.addEventListener('DOMContentLoaded', initAnswerbox); @@ -28,6 +29,7 @@ document.addEventListener('DOMContentLoaded', initModeration); document.addEventListener('DOMContentLoaded', initMemes); document.addEventListener('turbo:load', initLocales); document.addEventListener('turbo:load', initFront); +document.addEventListener('DOMContentLoaded', initWebpush); window['Stimulus'] = Application.start(); const context = require.context('../retrospring/controllers', true, /\.ts$/); diff --git a/app/javascript/retrospring/features/webpush/dismiss.ts b/app/javascript/retrospring/features/webpush/dismiss.ts new file mode 100644 index 00000000..48e17885 --- /dev/null +++ b/app/javascript/retrospring/features/webpush/dismiss.ts @@ -0,0 +1,7 @@ +export function dismissHandler (event: Event): void { + event.preventDefault(); + + const sender: HTMLButtonElement = event.target as HTMLButtonElement; + sender.closest('.push-settings').classList.add('d-none'); + localStorage.setItem('dismiss-push-settings-prompt', 'true'); +} diff --git a/app/javascript/retrospring/features/webpush/enable.ts b/app/javascript/retrospring/features/webpush/enable.ts new file mode 100644 index 00000000..a17b6f2c --- /dev/null +++ b/app/javascript/retrospring/features/webpush/enable.ts @@ -0,0 +1,59 @@ +import { get, post } from '@rails/request.js'; +import I18n from "retrospring/i18n"; +import { showNotification } from "utilities/notifications"; + +export function enableHandler (event: Event): void { + event.preventDefault(); + + try { + installServiceWorker() + .then(subscribe) + .then(async subscription => { + return Notification.requestPermission().then(permission => { + if (permission != "granted") { + return; + } + + post('/ajax/web_push', { + body: { + subscription + }, + contentType: 'application/json' + }).then(async response => { + const data = await response.json; + + if (data.success) { + new Notification(I18n.translate("frontend.push_notifications.subscribe.success.title"), { + body: I18n.translate("frontend.push_notifications.subscribe.success.body") + }); + } else { + new Notification(I18n.translate("frontend.push_notifications.fail.title"), { + body: I18n.translate("frontend.push_notifications.fail.body") + }); + } + }); + }); + }); + } catch (error) { + console.error("Failed to set up push notifications", error); + showNotification(I18n.translate("frontend.push_notifications.setup_fail")); + } +} + +async function installServiceWorker(): Promise { + return navigator.serviceWorker.register("/service_worker.js", { scope: "/" }); +} + +async function getServerKey(): Promise { + const response = await get("/ajax/webpush/key"); + const data = await response.json; + return Buffer.from(data.key, 'base64'); +} + +async function subscribe(registration: ServiceWorkerRegistration): Promise { + const key = await getServerKey(); + return await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key + }); +} diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts new file mode 100644 index 00000000..8b438699 --- /dev/null +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -0,0 +1,28 @@ +import registerEvents from 'retrospring/utilities/registerEvents'; +import { enableHandler } from './enable'; +import { dismissHandler } from "./dismiss"; + +export default (): void => { + const swCapable = 'serviceWorker' in navigator; + if (swCapable) { + document.body.classList.add('cap-service-worker'); + } + + const notificationCapable = 'Notification' in window; + if (notificationCapable) { + document.body.classList.add('cap-notification'); + } + + if (swCapable && notificationCapable) { + navigator.serviceWorker.getRegistration().then(registration => { + if (!registration && localStorage.getItem('dismiss-push-settings-prompt') == null) { + document.querySelector('.push-settings').classList.remove('d-none'); + } + }) + + registerEvents([ + {type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true}, + {type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true}, + ]); + } +} diff --git a/app/views/inbox/_push_settings.haml b/app/views/inbox/_push_settings.haml new file mode 100644 index 00000000..65c9e0db --- /dev/null +++ b/app/views/inbox/_push_settings.haml @@ -0,0 +1,5 @@ +.card.push-settings.d-none + .card-body + = t(".description") + %button.btn.btn-primary{ data: { action: "push-enable" } }= t("voc.y") + %button.btn{ data: { action: "push-dismiss" } }= t("voc.n") diff --git a/app/views/layouts/inbox.html.haml b/app/views/layouts/inbox.html.haml index e87d6cbb..b54b82b5 100644 --- a/app/views/layouts/inbox.html.haml +++ b/app/views/layouts/inbox.html.haml @@ -2,6 +2,7 @@ .row .col-sm-10.col-md-10.col-lg-9.mx-auto = render 'inbox/actions', delete_id: @delete_id, disabled: @disabled, inbox_count: @inbox_count + = render 'inbox/push_settings' = render 'layouts/messages' = yield diff --git a/config/locales/frontend.en.yml b/config/locales/frontend.en.yml index 0e734fd6..3ca04e3c 100644 --- a/config/locales/frontend.en.yml +++ b/config/locales/frontend.en.yml @@ -49,6 +49,15 @@ en: confirm: title: "Are you sure?" text: "This will mute this user for everyone." + push_notifications: + subscribe: + success: + title: Push notifications enabled! + body: You will now receive push notifications for new questions on this device. + fail: + title: Failed to subscribe to push notifications + body: Please try again later + setup_fail: Failed to set up push notifications. Please try again later. report: confirm: title: "Are you sure you want to report this %{type}?" diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 7d427b81..552e7821 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -231,6 +231,8 @@ en: share: heading: "Share" button: "Share on %{service}" + push_settings: + description: "Want to receive push notifications for new questions on your device?" layouts: feedback: heading: "Feedback" diff --git a/public/service_worker.js b/public/service_worker.js new file mode 100644 index 00000000..f9fa4973 --- /dev/null +++ b/public/service_worker.js @@ -0,0 +1,12 @@ +self.addEventListener('push', function (event) { + if (event.data) { + const notification = event.data.json(); + console.log(event.data); + + event.waitUntil(self.registration.showNotification(notification.title, { + body: notification.body + })); + } else { + console.error("Push event received, but it didn't contain any data.", event); + } +}); From 93d4af3f0d4bc04b499a7b8066c79fdf910848ce Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 22:34:18 +0200 Subject: [PATCH 07/42] Deduplicate notification sending logic and replace placeholder string --- app/models/inbox.rb | 13 +++++++++++++ app/models/user.rb | 1 + app/models/user/push_notification_methods.rb | 15 +++++++++++++++ app/workers/question_worker.rb | 11 ++--------- config/locales/frontend.en.yml | 2 ++ lib/use_case/question/create.rb | 8 +------- 6 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 app/models/user/push_notification_methods.rb diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 5c84a055..73dbc433 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Inbox < ApplicationRecord belongs_to :user belongs_to :question @@ -24,4 +26,15 @@ class Inbox < ApplicationRecord self.question.destroy if self.question.can_be_removed? self.destroy end + + def as_push_notification + { + type: :inbox, + title: I18n.t( + "frontend.push_notifications.inbox.title", + user: question.author_is_anonymous ? user.profile.display_name : question.author.profile.safe_name + ), + body: question.content, + } + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4b97f7d1..a5263e0c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ApplicationRecord include User::BanMethods include User::InboxMethods include User::QuestionMethods + include User::PushNotificationMethods include User::ReactionMethods include User::RelationshipMethods include User::TimelineMethods diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb new file mode 100644 index 00000000..ea166b43 --- /dev/null +++ b/app/models/user/push_notification_methods.rb @@ -0,0 +1,15 @@ +module User::PushNotificationMethods + def push_notification(app, resource) + raise ArgumentError("Resource must respond to `as_push_notification`") unless resource.respond_to? :as_push_notification + + web_push_subscriptions.each do |s| + n = Rpush::Webpush::Notification.new + n.app = app + n.registration_ids = [s.subscription.symbolize_keys] + n.data = { + message: resource.as_push_notification + } + n.save! + end + end +end diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index be82dfb4..47a6a5b4 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -18,15 +18,8 @@ class QuestionWorker next if MuteRule.where(user: f).any? { |rule| rule.applies_to? question } next if user.muting?(question.user) - Inbox.create(user_id: f.id, question_id: question_id, new: true) - - f.web_push_subscriptions.each do |s| - n = Rpush::Webpush::Notification.new - n.app = webpush_app - n.registration_ids = [s.subscription.symbolize_keys] - n.data = { message: { title: "New question notif title", body: question.content }.to_json } - n.save! - end + inbox = Inbox.create(user_id: f.id, question_id: question_id, new: true) + f.push_notification(webpush_app, inbox) end rescue StandardError => e logger.info "failed to ask question: #{e.message}" diff --git a/config/locales/frontend.en.yml b/config/locales/frontend.en.yml index 3ca04e3c..19e8ea79 100644 --- a/config/locales/frontend.en.yml +++ b/config/locales/frontend.en.yml @@ -58,6 +58,8 @@ en: title: Failed to subscribe to push notifications body: Please try again later setup_fail: Failed to set up push notifications. Please try again later. + inbox: + title: New question from %{user} report: confirm: title: "Are you sure you want to report this %{type}?" diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index 4426c680..a0a55450 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -31,13 +31,7 @@ module UseCase inbox = ::Inbox.create!(user: target_user, question: question, new: true) webpush_app = Rpush::App.find_by(name: "webpush") - target_user.web_push_subscriptions.each do |s| - n = Rpush::Webpush::Notification.new - n.app = webpush_app - n.registration_ids = [s.subscription.symbolize_keys] - n.data = { message: { title: "New question notif title", body: question.content }.to_json } - n.save! - end + target_user.push_notification(webpush_app, inbox) { status: 201, From a04b29006754da89b774259cc885a72c1e3d82f0 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 23:20:10 +0200 Subject: [PATCH 08/42] Appease the dog overlords --- app/models/inbox.rb | 2 +- app/models/user/push_notification_methods.rb | 2 + app/workers/question_worker.rb | 2 +- db/migrate/20220909161542_add_rpush.rb | 138 ++++++++---------- .../20220909161543_rpush_2_0_0_updates.rb | 32 ++-- .../20220909161544_rpush_2_1_0_updates.rb | 2 + .../20220909161545_rpush_2_6_0_updates.rb | 3 +- .../20220909161546_rpush_2_7_0_updates.rb | 3 +- .../20220909161547_rpush_3_0_0_updates.rb | 4 +- .../20220909161548_rpush_3_0_1_updates.rb | 2 + .../20220909161549_rpush_3_1_0_add_pushy.rb | 2 + .../20220909161550_rpush_3_1_1_updates.rb | 10 +- .../20220909161551_rpush_3_2_0_add_apns_p8.rb | 2 + .../20220909161552_rpush_3_2_4_updates.rb | 2 + .../20220909161553_rpush_3_3_0_updates.rb | 2 + .../20220909161554_rpush_3_3_1_updates.rb | 2 + .../20220909161555_rpush_4_1_0_updates.rb | 2 + .../20220909161556_rpush_4_1_1_updates.rb | 2 + .../20220909161557_rpush_4_2_0_updates.rb | 3 +- ...910000514_create_web_push_subscriptions.rb | 2 + 20 files changed, 116 insertions(+), 103 deletions(-) diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 73dbc433..fde7b26b 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -34,7 +34,7 @@ class Inbox < ApplicationRecord "frontend.push_notifications.inbox.title", user: question.author_is_anonymous ? user.profile.display_name : question.author.profile.safe_name ), - body: question.content, + body: question.content } end end diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb index ea166b43..dcf0fec9 100644 --- a/app/models/user/push_notification_methods.rb +++ b/app/models/user/push_notification_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module User::PushNotificationMethods def push_notification(app, resource) raise ArgumentError("Resource must respond to `as_push_notification`") unless resource.respond_to? :as_push_notification diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index 47a6a5b4..eea5ea27 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -18,7 +18,7 @@ class QuestionWorker next if MuteRule.where(user: f).any? { |rule| rule.applies_to? question } next if user.muting?(question.user) - inbox = Inbox.create(user_id: f.id, question_id: question_id, new: true) + inbox = Inbox.create(user_id: f.id, question_id:, new: true) f.push_notification(webpush_app, inbox) end rescue StandardError => e diff --git a/db/migrate/20220909161542_add_rpush.rb b/db/migrate/20220909161542_add_rpush.rb index d5ed63a1..93c30e2b 100644 --- a/db/migrate/20220909161542_add_rpush.rb +++ b/db/migrate/20220909161542_add_rpush.rb @@ -1,4 +1,6 @@ -# NOTE TO THE CURIOUS. +# frozen_string_literal: true + +# NOTE: TO THE CURIOUS. # # Congratulations on being a diligent developer and vetting the migrations # added to your project! @@ -33,15 +35,13 @@ class AddRpush < ActiveRecord::Migration[5.0] def self.down migrations.reverse.each do |m| - begin - m.down - rescue ActiveRecord::StatementInvalid => e - p e - end + m.down + rescue ActiveRecord::StatementInvalid => e + Rails.logger.debug e end end - class CreateRapnsNotifications < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up create_table :rapns_notifications do |t| t.integer :badge, null: true @@ -60,19 +60,17 @@ class AddRpush < ActiveRecord::Migration[5.0] t.timestamps end - add_index :rapns_notifications, [:delivered, :failed, :deliver_after], name: 'index_rapns_notifications_multi' + add_index :rapns_notifications, %i[delivered failed deliver_after], name: "index_rapns_notifications_multi" end def self.down - if index_name_exists?(:rapns_notifications, 'index_rapns_notifications_multi') - remove_index :rapns_notifications, name: 'index_rapns_notifications_multi' - end + remove_index :rapns_notifications, name: "index_rapns_notifications_multi" if index_name_exists?(:rapns_notifications, "index_rapns_notifications_multi") drop_table :rapns_notifications end end - class CreateRapnsFeedback < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up create_table :rapns_feedback do |t| t.string :device_token, null: false, limit: 64 @@ -84,15 +82,13 @@ class AddRpush < ActiveRecord::Migration[5.0] end def self.down - if index_name_exists?(:rapns_feedback, :index_rapns_feedback_on_device_token) - remove_index :rapns_feedback, name: :index_rapns_feedback_on_device_token - end + remove_index :rapns_feedback, name: :index_rapns_feedback_on_device_token if index_name_exists?(:rapns_feedback, :index_rapns_feedback_on_device_token) drop_table :rapns_feedback end end - class AddAlertIsJsonToRapnsNotifications < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up add_column :rapns_notifications, :alert_is_json, :boolean, null: true, default: false end @@ -102,7 +98,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddAppToRapns < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up add_column :rapns_notifications, :app, :string, null: true add_column :rapns_feedback, :app, :string, null: true @@ -114,7 +110,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class CreateRapnsApps < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up create_table :rapns_apps do |t| t.string :key, null: false @@ -131,15 +127,15 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddGcm < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] module Rapns - class App < ActiveRecord::Base - self.table_name = 'rapns_apps' + class App < ApplicationRecord + self.table_name = "rapns_apps" end - class Notification < ActiveRecord::Base + class Notification < ApplicationRecord belongs_to :app - self.table_name = 'rapns_notifications' + self.table_name = "rapns_notifications" end end @@ -147,8 +143,8 @@ class AddRpush < ActiveRecord::Migration[5.0] add_column :rapns_notifications, :type, :string, null: true add_column :rapns_apps, :type, :string, null: true - AddGcm::Rapns::Notification.update_all type: 'Rapns::Apns::Notification' - AddGcm::Rapns::App.update_all type: 'Rapns::Apns::App' + AddGcm::Rapns::Notification.update_all type: "Rapns::Apns::Notification" + AddGcm::Rapns::App.update_all type: "Rapns::Apns::App" change_column :rapns_notifications, :type, :string, null: false change_column :rapns_apps, :type, :string, null: false @@ -158,7 +154,7 @@ class AddRpush < ActiveRecord::Migration[5.0] change_column :rapns_apps, :certificate, :text, null: true, default: nil change_column :rapns_notifications, :error_description, :text, null: true, default: nil - change_column :rapns_notifications, :sound, :string, default: 'default' + change_column :rapns_notifications, :sound, :string, default: "default" rename_column :rapns_notifications, :attributes_for_device, :data rename_column :rapns_apps, :key, :name @@ -168,7 +164,7 @@ class AddRpush < ActiveRecord::Migration[5.0] add_column :rapns_notifications, :collapse_key, :string, null: true add_column :rapns_notifications, :delay_while_idle, :boolean, null: false, default: false - reg_ids_type = ActiveRecord::Base.connection.adapter_name.include?('Mysql') ? :mediumtext : :text + reg_ids_type = ActiveRecord::Base.connection.adapter_name.include?("Mysql") ? :mediumtext : :text add_column :rapns_notifications, :registration_ids, reg_ids_type, null: true add_column :rapns_notifications, :app_id, :integer, null: true add_column :rapns_notifications, :retries, :integer, null: true, default: 0 @@ -189,11 +185,11 @@ class AddRpush < ActiveRecord::Migration[5.0] remove_index :rapns_notifications, name: "index_rapns_notifications_on_delivered_failed_deliver_after" end - add_index :rapns_notifications, [:app_id, :delivered, :failed, :deliver_after], name: "index_rapns_notifications_multi" + add_index :rapns_notifications, %i[app_id delivered failed deliver_after], name: "index_rapns_notifications_multi" end def self.down - AddGcm::Rapns::Notification.where(type: 'Rapns::Gcm::Notification').delete_all + AddGcm::Rapns::Notification.where(type: "Rapns::Gcm::Notification").delete_all remove_column :rapns_notifications, :type remove_column :rapns_apps, :type @@ -204,7 +200,7 @@ class AddRpush < ActiveRecord::Migration[5.0] change_column :rapns_apps, :certificate, :text, null: false change_column :rapns_notifications, :error_description, :string, null: true, default: nil - change_column :rapns_notifications, :sound, :string, default: '1.aiff' + change_column :rapns_notifications, :sound, :string, default: "1.aiff" rename_column :rapns_notifications, :data, :attributes_for_device rename_column :rapns_apps, :name, :key @@ -225,20 +221,18 @@ class AddRpush < ActiveRecord::Migration[5.0] AddGcm::Rapns::Notification.where(app_id: app.id).update_all(app: app.key) end - if index_name_exists?(:rapns_notifications, :index_rapns_notifications_multi) - remove_index :rapns_notifications, name: :index_rapns_notifications_multi - end + remove_index :rapns_notifications, name: :index_rapns_notifications_multi if index_name_exists?(:rapns_notifications, :index_rapns_notifications_multi) remove_column :rapns_notifications, :app_id - add_index :rapns_notifications, [:delivered, :failed, :deliver_after], name: :index_rapns_notifications_multi + add_index :rapns_notifications, %i[delivered failed deliver_after], name: :index_rapns_notifications_multi end end - class AddWpns < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] module Rapns - class Notification < ActiveRecord::Base - self.table_name = 'rapns_notifications' + class Notification < ApplicationRecord + self.table_name = "rapns_notifications" end end @@ -247,15 +241,15 @@ class AddRpush < ActiveRecord::Migration[5.0] end def self.down - AddWpns::Rapns::Notification.where(type: 'Rapns::Wpns::Notification').delete_all + AddWpns::Rapns::Notification.where(type: "Rapns::Wpns::Notification").delete_all remove_column :rapns_notifications, :uri end end - class AddAdm < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] module Rapns - class Notification < ActiveRecord::Base - self.table_name = 'rapns_notifications' + class Notification < ApplicationRecord + self.table_name = "rapns_notifications" end end @@ -267,7 +261,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end def self.down - AddAdm::Rapns::Notification.where(type: 'Rapns::Adm::Notification').delete_all + AddAdm::Rapns::Notification.where(type: "Rapns::Adm::Notification").delete_all remove_column :rapns_apps, :client_id remove_column :rapns_apps, :client_secret @@ -276,14 +270,14 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class RenameRapnsToRpush < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] module Rpush - class App < ActiveRecord::Base - self.table_name = 'rpush_apps' + class App < ApplicationRecord + self.table_name = "rpush_apps" end - class Notification < ActiveRecord::Base - self.table_name = 'rpush_notifications' + class Notification < ApplicationRecord + self.table_name = "rpush_notifications" end end @@ -296,43 +290,35 @@ class AddRpush < ActiveRecord::Migration[5.0] rename_table :rapns_apps, :rpush_apps rename_table :rapns_feedback, :rpush_feedback - if index_name_exists?(:rpush_notifications, :index_rapns_notifications_multi) - rename_index :rpush_notifications, :index_rapns_notifications_multi, :index_rpush_notifications_multi - end + rename_index :rpush_notifications, :index_rapns_notifications_multi, :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rapns_notifications_multi) - if index_name_exists?(:rpush_feedback, :index_rapns_feedback_on_device_token) - rename_index :rpush_feedback, :index_rapns_feedback_on_device_token, :index_rpush_feedback_on_device_token - end + rename_index :rpush_feedback, :index_rapns_feedback_on_device_token, :index_rpush_feedback_on_device_token if index_name_exists?(:rpush_feedback, :index_rapns_feedback_on_device_token) - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Apns::Notification', 'Rpush::Apns::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Gcm::Notification', 'Rpush::Gcm::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Adm::Notification', 'Rpush::Adm::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rapns::Wpns::Notification', 'Rpush::Wpns::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Apns::Notification", "Rpush::Apns::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Gcm::Notification", "Rpush::Gcm::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Adm::Notification", "Rpush::Adm::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Wpns::Notification", "Rpush::Wpns::Notification") - update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Apns::App', 'Rpush::Apns::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Gcm::App', 'Rpush::Gcm::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Adm::App', 'Rpush::Adm::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rapns::Wpns::App', 'Rpush::Wpns::App') + update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Apns::App", "Rpush::Apns::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Gcm::App", "Rpush::Gcm::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Adm::App", "Rpush::Adm::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Wpns::App", "Rpush::Wpns::App") end def self.down - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Apns::Notification', 'Rapns::Apns::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Gcm::Notification', 'Rapns::Gcm::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Adm::Notification', 'Rapns::Adm::Notification') - update_type(RenameRapnsToRpush::Rpush::Notification, 'Rpush::Wpns::Notification', 'Rapns::Wpns::Notification') + update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Apns::Notification", "Rapns::Apns::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Gcm::Notification", "Rapns::Gcm::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Adm::Notification", "Rapns::Adm::Notification") + update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Wpns::Notification", "Rapns::Wpns::Notification") - update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Apns::App', 'Rapns::Apns::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Gcm::App', 'Rapns::Gcm::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Adm::App', 'Rapns::Adm::App') - update_type(RenameRapnsToRpush::Rpush::App, 'Rpush::Wpns::App', 'Rapns::Wpns::App') + update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Apns::App", "Rapns::Apns::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Gcm::App", "Rapns::Gcm::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Adm::App", "Rapns::Adm::App") + update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Wpns::App", "Rapns::Wpns::App") - if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - rename_index :rpush_notifications, :index_rpush_notifications_multi, :index_rapns_notifications_multi - end + rename_index :rpush_notifications, :index_rpush_notifications_multi, :index_rapns_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - if index_name_exists?(:rpush_feedback, :index_rpush_feedback_on_device_token) - rename_index :rpush_feedback, :index_rpush_feedback_on_device_token, :index_rapns_feedback_on_device_token - end + rename_index :rpush_feedback, :index_rpush_feedback_on_device_token, :index_rapns_feedback_on_device_token if index_name_exists?(:rpush_feedback, :index_rpush_feedback_on_device_token) rename_table :rpush_notifications, :rapns_notifications rename_table :rpush_apps, :rapns_apps @@ -340,7 +326,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddFailAfterToRpushNotifications < ActiveRecord::Migration[5.0] + class AddRpush < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :fail_after, :timestamp, null: true end diff --git a/db/migrate/20220909161543_rpush_2_0_0_updates.rb b/db/migrate/20220909161543_rpush_2_0_0_updates.rb index ccf041c8..102c1337 100644 --- a/db/migrate/20220909161543_rpush_2_0_0_updates.rb +++ b/db/migrate/20220909161543_rpush_2_0_0_updates.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + class Rpush200Updates < ActiveRecord::Migration[5.0] module Rpush - class App < ActiveRecord::Base - self.table_name = 'rpush_apps' + class App < ApplicationRecord + self.table_name = "rpush_apps" end - class Notification < ActiveRecord::Base - self.table_name = 'rpush_notifications' + class Notification < ApplicationRecord + self.table_name = "rpush_notifications" end end @@ -17,28 +19,26 @@ class Rpush200Updates < ActiveRecord::Migration[5.0] add_column :rpush_notifications, :processing, :boolean, null: false, default: false add_column :rpush_notifications, :priority, :integer, null: true - if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - remove_index :rpush_notifications, name: :index_rpush_notifications_multi - end + remove_index :rpush_notifications, name: :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - add_index :rpush_notifications, [:delivered, :failed], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + add_index :rpush_notifications, %i[delivered failed], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed" rename_column :rpush_feedback, :app, :app_id if postgresql? - execute('ALTER TABLE rpush_feedback ALTER COLUMN app_id TYPE integer USING (trim(app_id)::integer)') + execute("ALTER TABLE rpush_feedback ALTER COLUMN app_id TYPE integer USING (trim(app_id)::integer)") else change_column :rpush_feedback, :app_id, :integer end - [:Apns, :Gcm, :Wpns, :Adm].each do |service| + %i[Apns Gcm Wpns Adm].each do |service| update_type(Rpush200Updates::Rpush::App, "Rpush::#{service}::App", "Rpush::Client::ActiveRecord::#{service}::App") update_type(Rpush200Updates::Rpush::Notification, "Rpush::#{service}::Notification", "Rpush::Client::ActiveRecord::#{service}::Notification") end end def self.down - [:Apns, :Gcm, :Wpns, :Adm].each do |service| + %i[Apns Gcm Wpns Adm].each do |service| update_type(Rpush200Updates::Rpush::App, "Rpush::Client::ActiveRecord::#{service}::App", "Rpush::#{service}::App") update_type(Rpush200Updates::Rpush::Notification, "Rpush::Client::ActiveRecord::#{service}::Notification", "Rpush::#{service}::Notification") end @@ -46,22 +46,20 @@ class Rpush200Updates < ActiveRecord::Migration[5.0] change_column :rpush_feedback, :app_id, :string rename_column :rpush_feedback, :app_id, :app - if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - remove_index :rpush_notifications, name: :index_rpush_notifications_multi - end + remove_index :rpush_notifications, name: :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi) - add_index :rpush_notifications, [:app_id, :delivered, :failed, :deliver_after], name: 'index_rpush_notifications_multi' + add_index :rpush_notifications, %i[app_id delivered failed deliver_after], name: "index_rpush_notifications_multi" remove_column :rpush_notifications, :priority remove_column :rpush_notifications, :processing end def self.adapter_name - env = (defined?(Rails) && Rails.env) ? Rails.env : 'development' + env = defined?(Rails) && Rails.env ? Rails.env : "development" if ActiveRecord::VERSION::MAJOR > 6 ActiveRecord::Base.configurations.configs_for(env_name: env).first.configuration_hash[:adapter] else - Hash[ActiveRecord::Base.configurations[env].map { |k,v| [k.to_sym,v] }][:adapter] + ActiveRecord::Base.configurations[env].to_h { |k, v| [k.to_sym, v] }[:adapter] end end diff --git a/db/migrate/20220909161544_rpush_2_1_0_updates.rb b/db/migrate/20220909161544_rpush_2_1_0_updates.rb index ab7868eb..75ee3c48 100644 --- a/db/migrate/20220909161544_rpush_2_1_0_updates.rb +++ b/db/migrate/20220909161544_rpush_2_1_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush210Updates < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :url_args, :text, null: true diff --git a/db/migrate/20220909161545_rpush_2_6_0_updates.rb b/db/migrate/20220909161545_rpush_2_6_0_updates.rb index 42e30a89..3545a88c 100644 --- a/db/migrate/20220909161545_rpush_2_6_0_updates.rb +++ b/db/migrate/20220909161545_rpush_2_6_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush260Updates < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :content_available, :boolean, default: false @@ -7,4 +9,3 @@ class Rpush260Updates < ActiveRecord::Migration[5.0] remove_column :rpush_notifications, :content_available end end - diff --git a/db/migrate/20220909161546_rpush_2_7_0_updates.rb b/db/migrate/20220909161546_rpush_2_7_0_updates.rb index 2dcba35c..bc8dda54 100644 --- a/db/migrate/20220909161546_rpush_2_7_0_updates.rb +++ b/db/migrate/20220909161546_rpush_2_7_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush270Updates < ActiveRecord::Migration[5.0] def self.up change_column :rpush_notifications, :alert, :text @@ -9,4 +11,3 @@ class Rpush270Updates < ActiveRecord::Migration[5.0] remove_column :rpush_notifications, :notification end end - diff --git a/db/migrate/20220909161547_rpush_3_0_0_updates.rb b/db/migrate/20220909161547_rpush_3_0_0_updates.rb index 77a3046b..d1767ab2 100644 --- a/db/migrate/20220909161547_rpush_3_0_0_updates.rb +++ b/db/migrate/20220909161547_rpush_3_0_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush300Updates < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :mutable_content, :boolean, default: false @@ -6,6 +8,6 @@ class Rpush300Updates < ActiveRecord::Migration[5.0] def self.down remove_column :rpush_notifications, :mutable_content - change_column :rpush_notifications, :sound, :string, default: 'default' + change_column :rpush_notifications, :sound, :string, default: "default" end end diff --git a/db/migrate/20220909161548_rpush_3_0_1_updates.rb b/db/migrate/20220909161548_rpush_3_0_1_updates.rb index 38da62a1..501d84ef 100644 --- a/db/migrate/20220909161548_rpush_3_0_1_updates.rb +++ b/db/migrate/20220909161548_rpush_3_0_1_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush301Updates < ActiveRecord::Migration[5.0] def self.up change_column_null :rpush_notifications, :mutable_content, false diff --git a/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb b/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb index 7037f2f9..fc47b223 100644 --- a/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb +++ b/db/migrate/20220909161549_rpush_3_1_0_add_pushy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush310AddPushy < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :external_device_id, :string, null: true diff --git a/db/migrate/20220909161550_rpush_3_1_1_updates.rb b/db/migrate/20220909161550_rpush_3_1_1_updates.rb index 68386461..e5643a64 100644 --- a/db/migrate/20220909161550_rpush_3_1_1_updates.rb +++ b/db/migrate/20220909161550_rpush_3_1_1_updates.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + class Rpush311Updates < ActiveRecord::Migration[5.0] def self.up change_table :rpush_notifications do |t| - t.remove_index name: 'index_rpush_notifications_multi' - t.index [:delivered, :failed, :processing, :deliver_after, :created_at], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + t.remove_index name: "index_rpush_notifications_multi" + t.index %i[delivered failed processing deliver_after created_at], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed" end end def self.down change_table :rpush_notifications do |t| - t.remove_index name: 'index_rpush_notifications_multi' - t.index [:delivered, :failed], name: 'index_rpush_notifications_multi', where: 'NOT delivered AND NOT failed' + t.remove_index name: "index_rpush_notifications_multi" + t.index %i[delivered failed], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed" end end end diff --git a/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb b/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb index 64561ac3..51325e49 100644 --- a/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb +++ b/db/migrate/20220909161551_rpush_3_2_0_add_apns_p8.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush320AddApnsP8 < ActiveRecord::Migration[5.0] def self.up add_column :rpush_apps, :apn_key, :string, null: true diff --git a/db/migrate/20220909161552_rpush_3_2_4_updates.rb b/db/migrate/20220909161552_rpush_3_2_4_updates.rb index da2f4f61..f29319b7 100644 --- a/db/migrate/20220909161552_rpush_3_2_4_updates.rb +++ b/db/migrate/20220909161552_rpush_3_2_4_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush324Updates < ActiveRecord::Migration[5.0] def self.up change_column :rpush_apps, :apn_key, :text, null: true diff --git a/db/migrate/20220909161553_rpush_3_3_0_updates.rb b/db/migrate/20220909161553_rpush_3_3_0_updates.rb index edd6efc5..d5ffce23 100644 --- a/db/migrate/20220909161553_rpush_3_3_0_updates.rb +++ b/db/migrate/20220909161553_rpush_3_3_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush330Updates < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :thread_id, :string, null: true diff --git a/db/migrate/20220909161554_rpush_3_3_1_updates.rb b/db/migrate/20220909161554_rpush_3_3_1_updates.rb index bfc07556..659f6ed5 100644 --- a/db/migrate/20220909161554_rpush_3_3_1_updates.rb +++ b/db/migrate/20220909161554_rpush_3_3_1_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush331Updates < ActiveRecord::Migration[5.0] def self.up change_column :rpush_notifications, :device_token, :string, null: true diff --git a/db/migrate/20220909161555_rpush_4_1_0_updates.rb b/db/migrate/20220909161555_rpush_4_1_0_updates.rb index a0fbe52f..7c3fca19 100644 --- a/db/migrate/20220909161555_rpush_4_1_0_updates.rb +++ b/db/migrate/20220909161555_rpush_4_1_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush410Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] def self.up add_column :rpush_notifications, :dry_run, :boolean, null: false, default: false diff --git a/db/migrate/20220909161556_rpush_4_1_1_updates.rb b/db/migrate/20220909161556_rpush_4_1_1_updates.rb index bde7366d..5db49e66 100644 --- a/db/migrate/20220909161556_rpush_4_1_1_updates.rb +++ b/db/migrate/20220909161556_rpush_4_1_1_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush411Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] def self.up add_column :rpush_apps, :feedback_enabled, :boolean, default: true diff --git a/db/migrate/20220909161557_rpush_4_2_0_updates.rb b/db/migrate/20220909161557_rpush_4_2_0_updates.rb index df13a117..7418ff39 100644 --- a/db/migrate/20220909161557_rpush_4_2_0_updates.rb +++ b/db/migrate/20220909161557_rpush_4_2_0_updates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Rpush420Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"] def self.up add_column :rpush_notifications, :sound_is_json, :boolean, null: true, default: false @@ -7,4 +9,3 @@ class Rpush420Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR} remove_column :rpush_notifications, :sound_is_json end end - diff --git a/db/migrate/20220910000514_create_web_push_subscriptions.rb b/db/migrate/20220910000514_create_web_push_subscriptions.rb index 45a2dcf4..3da83671 100644 --- a/db/migrate/20220910000514_create_web_push_subscriptions.rb +++ b/db/migrate/20220910000514_create_web_push_subscriptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateWebPushSubscriptions < ActiveRecord::Migration[6.1] def change create_table :web_push_subscriptions do |t| From 4e65954a7ade6f8f7296ccc21ae412cd1223b0a2 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 23:55:06 +0200 Subject: [PATCH 09/42] Open inbox on notification click --- public/service_worker.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/public/service_worker.js b/public/service_worker.js index f9fa4973..7b903ecd 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -4,9 +4,19 @@ self.addEventListener('push', function (event) { console.log(event.data); event.waitUntil(self.registration.showNotification(notification.title, { - body: notification.body + body: notification.body, + tag: notification.type })); } else { console.error("Push event received, but it didn't contain any data.", event); } }); + +self.addEventListener('notificationclick', async event => { + if (event.notification.tag === 'inbox') { + event.preventDefault(); + return clients.openWindow("/inbox", "_blank"); + } else { + console.warn(`Unhandled notification tag: ${event.notification.tag}`); + } +}); From 29a3bfea88b5324ef693e64bca712e255a9938e9 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 12 Sep 2022 00:48:04 +0200 Subject: [PATCH 10/42] Fix incorrect internal class names in Rpush migration This was caused by `rubocop -A` --- db/migrate/20220909161542_add_rpush.rb | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/db/migrate/20220909161542_add_rpush.rb b/db/migrate/20220909161542_add_rpush.rb index 93c30e2b..f90a4934 100644 --- a/db/migrate/20220909161542_add_rpush.rb +++ b/db/migrate/20220909161542_add_rpush.rb @@ -41,7 +41,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class CreateRapnsNotifications < ActiveRecord::Migration[5.0] def self.up create_table :rapns_notifications do |t| t.integer :badge, null: true @@ -70,7 +70,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class CreateRapnsFeedback < ActiveRecord::Migration[5.0] def self.up create_table :rapns_feedback do |t| t.string :device_token, null: false, limit: 64 @@ -88,7 +88,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddAlertIsJsonToRapnsNotifications < ActiveRecord::Migration[5.0] def self.up add_column :rapns_notifications, :alert_is_json, :boolean, null: true, default: false end @@ -98,7 +98,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddAppToRapns < ActiveRecord::Migration[5.0] def self.up add_column :rapns_notifications, :app, :string, null: true add_column :rapns_feedback, :app, :string, null: true @@ -110,7 +110,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class CreateRapnsApps < ActiveRecord::Migration[5.0] def self.up create_table :rapns_apps do |t| t.string :key, null: false @@ -127,13 +127,13 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddGcm < ActiveRecord::Migration[5.0] module Rapns - class App < ApplicationRecord - self.table_name = "rapns_apps" + class App < ActiveRecord::Base + self.table_name = 'rapns_apps' end - class Notification < ApplicationRecord + class Notification < ActiveRecord::Base belongs_to :app self.table_name = "rapns_notifications" end @@ -229,10 +229,10 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddWpns < ActiveRecord::Migration[5.0] module Rapns - class Notification < ApplicationRecord - self.table_name = "rapns_notifications" + class Notification < ActiveRecord::Base + self.table_name = 'rapns_notifications' end end @@ -246,10 +246,10 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddAdm < ActiveRecord::Migration[5.0] module Rapns - class Notification < ApplicationRecord - self.table_name = "rapns_notifications" + class Notification < ActiveRecord::Base + self.table_name = 'rapns_notifications' end end @@ -270,14 +270,14 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class RenameRapnsToRpush < ActiveRecord::Migration[5.0] module Rpush - class App < ApplicationRecord - self.table_name = "rpush_apps" + class App < ActiveRecord::Base + self.table_name = 'rpush_apps' end - class Notification < ApplicationRecord - self.table_name = "rpush_notifications" + class Notification < ActiveRecord::Base + self.table_name = 'rpush_notifications' end end @@ -326,7 +326,7 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - class AddRpush < ActiveRecord::Migration[5.0] + class AddFailAfterToRpushNotifications < ActiveRecord::Migration[5.0] def self.up add_column :rpush_notifications, :fail_after, :timestamp, null: true end From c8f5511a38cbf8a196d1aeb52be8f3c29fe06bed Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Thu, 20 Oct 2022 18:49:02 +0200 Subject: [PATCH 11/42] Fix capability classes being removed on page navigation The body tag gets replaced by Turbo on page navigation, removing the classes. --- app/javascript/packs/application.ts | 2 ++ .../retrospring/features/answerbox/index.ts | 4 ---- .../retrospring/features/capabilities/index.ts | 13 +++++++++++++ .../retrospring/features/webpush/index.ts | 11 ++--------- 4 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 app/javascript/retrospring/features/capabilities/index.ts diff --git a/app/javascript/packs/application.ts b/app/javascript/packs/application.ts index 4d292a2e..eb7044a4 100644 --- a/app/javascript/packs/application.ts +++ b/app/javascript/packs/application.ts @@ -5,6 +5,7 @@ import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers'; import start from 'retrospring/common'; import initAnswerbox from 'retrospring/features/answerbox/index'; +import initCapabilities from 'retrospring/features/capabilities'; import initInbox from 'retrospring/features/inbox/index'; import initUser from 'retrospring/features/user'; import initSettings from 'retrospring/features/settings/index'; @@ -18,6 +19,7 @@ import initFront from 'retrospring/features/front'; import initWebpush from 'retrospring/features/webpush'; start(); +document.addEventListener('turbo:load', initCapabilities); document.addEventListener('DOMContentLoaded', initAnswerbox); document.addEventListener('DOMContentLoaded', initInbox); document.addEventListener('DOMContentLoaded', initUser); diff --git a/app/javascript/retrospring/features/answerbox/index.ts b/app/javascript/retrospring/features/answerbox/index.ts index c67de9e7..b0a3a897 100644 --- a/app/javascript/retrospring/features/answerbox/index.ts +++ b/app/javascript/retrospring/features/answerbox/index.ts @@ -7,10 +7,6 @@ import { answerboxSmileHandler } from './smile'; import { answerboxSubscribeHandler } from './subscribe'; export default (): void => { - if ('share' in navigator) { - document.body.classList.add('cap-web-share'); - } - registerEvents([ { type: 'click', target: '[name=ab-share]', handler: shareEventHandler, global: true }, { type: 'click', target: '[data-action=ab-submarine]', handler: answerboxSubscribeHandler, global: true }, diff --git a/app/javascript/retrospring/features/capabilities/index.ts b/app/javascript/retrospring/features/capabilities/index.ts new file mode 100644 index 00000000..bc0c29f8 --- /dev/null +++ b/app/javascript/retrospring/features/capabilities/index.ts @@ -0,0 +1,13 @@ +export default (): void => { + if ('share' in navigator) { + document.body.classList.add('cap-web-share'); + } + + if ('serviceWorker' in navigator) { + document.body.classList.add('cap-service-worker'); + } + + if ('Notification' in window) { + document.body.classList.add('cap-notification'); + } +} diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index 8b438699..2c10566b 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -3,15 +3,8 @@ import { enableHandler } from './enable'; import { dismissHandler } from "./dismiss"; export default (): void => { - const swCapable = 'serviceWorker' in navigator; - if (swCapable) { - document.body.classList.add('cap-service-worker'); - } - - const notificationCapable = 'Notification' in window; - if (notificationCapable) { - document.body.classList.add('cap-notification'); - } + const swCapable = document.body.classList.contains('cap-service-worker'); + const notificationCapable = document.body.classList.contains('cap-notification'); if (swCapable && notificationCapable) { navigator.serviceWorker.getRegistration().then(registration => { From 752cf1506b55dac449cb67070204b0cf4be28405 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Thu, 20 Oct 2022 19:02:12 +0200 Subject: [PATCH 12/42] Add settings page for push notifications --- app/controllers/ajax/web_push_controller.rb | 2 ++ .../settings/push_notifications_controller.rb | 9 +++++++++ .../retrospring/features/webpush/enable.ts | 2 +- app/javascript/styles/application.scss | 1 + .../styles/components/_push-settings.scss | 7 +++++++ app/views/settings/_push_notifications.haml | 17 +++++++++++++++++ .../settings/push_notifications/index.haml | 4 ++++ app/views/tabs/_settings.html.haml | 1 + config/locales/views.en.yml | 14 ++++++++++++++ config/routes.rb | 2 ++ 10 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 app/controllers/settings/push_notifications_controller.rb create mode 100644 app/javascript/styles/components/_push-settings.scss create mode 100644 app/views/settings/_push_notifications.haml create mode 100644 app/views/settings/push_notifications/index.haml diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index af4e715f..49d8a497 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -4,6 +4,8 @@ class Ajax::WebPushController < AjaxController def key certificate = Rpush::Webpush::App.find_by(name: "webpush").certificate + @response[:status] = :okay + @response[:success] = true @response[:key] = JSON.parse(certificate)["public_key"] end diff --git a/app/controllers/settings/push_notifications_controller.rb b/app/controllers/settings/push_notifications_controller.rb new file mode 100644 index 00000000..551ae46e --- /dev/null +++ b/app/controllers/settings/push_notifications_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::PushNotificationsController < ApplicationController + before_action :authenticate_user! + + def index + @subscriptions = current_user.web_push_subscriptions + end +end diff --git a/app/javascript/retrospring/features/webpush/enable.ts b/app/javascript/retrospring/features/webpush/enable.ts index a17b6f2c..c52ca51c 100644 --- a/app/javascript/retrospring/features/webpush/enable.ts +++ b/app/javascript/retrospring/features/webpush/enable.ts @@ -14,7 +14,7 @@ export function enableHandler (event: Event): void { return; } - post('/ajax/web_push', { + post('/ajax/webpush', { body: { subscription }, diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index b5765b62..3500dd56 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -108,6 +108,7 @@ "components/mobile-nav", "components/notifications", "components/profile", +"components/push-settings", "components/question", "components/smiles", "components/themes", diff --git a/app/javascript/styles/components/_push-settings.scss b/app/javascript/styles/components/_push-settings.scss new file mode 100644 index 00000000..c5d38d94 --- /dev/null +++ b/app/javascript/styles/components/_push-settings.scss @@ -0,0 +1,7 @@ +.push-notifications { + &-unavailable { + body.cap-service-worker.cap-notification & { + display: none; + } + } +} diff --git a/app/views/settings/_push_notifications.haml b/app/views/settings/_push_notifications.haml new file mode 100644 index 00000000..05753739 --- /dev/null +++ b/app/views/settings/_push_notifications.haml @@ -0,0 +1,17 @@ +.card.push-notifications-settings + .card-body + %p= t('.description') + %p= t('.subscription_count', count: subscriptions.count) + + .push-notifications-unavailable.text-danger + %i.fa.fa-warning + = t('.unsupported') + + .push-notifications-current-target.d-none.text-success + %i.fa.fa-check + = t('.current_target') + + .button-group{ role: 'group' } + %button.btn.btn-primary{ data: { action: 'push-enable' } }= t('.subscribe') + %button.btn.btn-primary{ data: { action: 'push-disable' } }= t('.unsubscribe_current') + %button.btn.btn-danger{ data: { action: 'push-remove-all' } }= t('.unsubscribe_all') diff --git a/app/views/settings/push_notifications/index.haml b/app/views/settings/push_notifications/index.haml new file mode 100644 index 00000000..9985eeff --- /dev/null +++ b/app/views/settings/push_notifications/index.haml @@ -0,0 +1,4 @@ += render "settings/push_notifications", subscriptions: @subscriptions + +- provide(:title, generate_title(t(".title"))) +- parent_layout "user/settings" diff --git a/app/views/tabs/_settings.html.haml b/app/views/tabs/_settings.html.haml index 49da5ae5..366aafc1 100644 --- a/app/views/tabs/_settings.html.haml +++ b/app/views/tabs/_settings.html.haml @@ -8,6 +8,7 @@ = list_group_item t(".mutes"), settings_muted_path = list_group_item t(".blocks"), settings_blocks_path = list_group_item t(".theme"), edit_settings_theme_path + = list_group_item t(".push_notifications"), settings_push_notifications_path = list_group_item t(".data"), settings_data_path = list_group_item t(".export"), settings_export_path diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 552e7821..0c3aca6c 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -531,6 +531,18 @@ en: body: "Raised content includes all the different boxes and panels you can see across the site." accent: example: "Some text on top of a accented area on a raised element!" + push_notifications: + index: + title: "Push Notifications" + subscription_count: + zero: "You are not currently subscribed to push notifications on any devices." + one: "You are currently receiving push notifications on one device." + other: "You are currently receiving push notifications on %{count} devices." + unsupported: "This browser does not support push notifications." + current_target: "You are currently receiving push notifications on this device." + subscribe: "Enable on this device" + unsubscribe_current: "Disable on this device" + unsubscribe_all: "Disable on all devices" shared: links: about: "About" @@ -594,6 +606,8 @@ en: theme: "Theme" data: "Your Data" export: "Export" + blocks: "Blocks" + push_notifications: "Push Notifications" moderation: inbox: header: diff --git a/config/routes.rb b/config/routes.rb index fe0ef852..c3770c08 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,6 +89,8 @@ Rails.application.routes.draw do get :data, to: "data#index" + resources :push_notifications, only: %i[index] + namespace :two_factor_authentication do get :otp_authentication, to: "otp_authentication#index" patch :otp_authentication, to: "otp_authentication#update" From 8ff213af4ef37f43648ccc6deafcc993c7a0564d Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Fri, 21 Oct 2022 22:37:52 +0200 Subject: [PATCH 13/42] Add the ability to unsubscribe from push notifications --- app/controllers/ajax/web_push_controller.rb | 13 +++++++ .../retrospring/features/webpush/index.ts | 3 ++ .../features/webpush/unsubscribe.ts | 36 +++++++++++++++++++ config/routes.rb | 1 + 4 files changed, 53 insertions(+) create mode 100644 app/javascript/retrospring/features/webpush/unsubscribe.ts diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index 49d8a497..b3a671eb 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -18,4 +18,17 @@ class Ajax::WebPushController < AjaxController @response[:status] = :okay @response[:success] = true end + + def unsubscribe + params.permit(:endpoint) + + if params.key?(:endpoint) + current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy + else + current_user.web_push_subscriptions.destroy_all + end + + @response[:status] = :okay + @response[:success] = true + end end diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index 2c10566b..e9149684 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -1,6 +1,7 @@ import registerEvents from 'retrospring/utilities/registerEvents'; import { enableHandler } from './enable'; import { dismissHandler } from "./dismiss"; +import { unsubscribeHandler } from "retrospring/features/webpush/unsubscribe"; export default (): void => { const swCapable = document.body.classList.contains('cap-service-worker'); @@ -16,6 +17,8 @@ export default (): void => { registerEvents([ {type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true}, {type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true}, + {type: 'click', target: '[data-action="push-disable"]', handler: () => unsubscribeHandler(false), global: true}, + {type: 'click', target: '[data-action="push-remove-all"]', handler: () => unsubscribeHandler(true), global: true}, ]); } } diff --git a/app/javascript/retrospring/features/webpush/unsubscribe.ts b/app/javascript/retrospring/features/webpush/unsubscribe.ts new file mode 100644 index 00000000..c89a8d7e --- /dev/null +++ b/app/javascript/retrospring/features/webpush/unsubscribe.ts @@ -0,0 +1,36 @@ +import { destroy } from '@rails/request.js'; +import { showErrorNotification, showNotification } from "utilities/notifications"; +import I18n from "retrospring/i18n"; + +export function unsubscribeHandler(all = false): void { + getSubscription().then(subscription => { + const body = all ? { endpoint: subscription.endpoint } : undefined; + + destroy('/ajax/webpush', { + body, + contentType: 'application/json', + }).then(async response => { + const data = await response.json; + + if (data.success) { + subscription?.unsubscribe().then(() => { + showNotification(I18n.translate("frontend.push_notifications.unsubscribe.success")); + }).catch(error => { + console.error("Tried to unsubscribe this browser but was unable to.\n" + + "As we've been unsubscribed on the server-side, this should not be an issue.", + error); + }) + } else { + showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.fail")); + } + }).catch(error => { + showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.error")); + console.error(error); + }); + }) +} + +async function getSubscription(): Promise { + const registration = await navigator.serviceWorker.getRegistration('/'); + return await registration.pushManager.getSubscription(); +} diff --git a/config/routes.rb b/config/routes.rb index c3770c08..55300166 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -136,6 +136,7 @@ Rails.application.routes.draw do post "/unsubscribe", to: "subscription#unsubscribe", as: :unsubscribe_answer get "/webpush/key", to: "web_push#key", as: :webpush_key post "/webpush", to: "web_push#subscribe", as: :webpush_subscribe + delete "/webpush", to: "web_push#unsubscribe", as: :webpush_unsubscribe end resource :anonymous_block, controller: :anonymous_block, only: %i[create destroy] From 66b1dac3b975e56f898c9f4172e01f17768d4474 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 23 Oct 2022 16:00:21 +0200 Subject: [PATCH 14/42] Improve UX for push subscription management --- app/controllers/ajax/web_push_controller.rb | 7 +- .../retrospring/features/webpush/enable.ts | 8 +++ .../retrospring/features/webpush/index.ts | 35 +++++++--- .../features/webpush/unsubscribe.ts | 69 ++++++++++++------- app/views/settings/_push_notifications.haml | 8 +-- config/locales/controllers.en.yml | 5 ++ config/locales/views.en.yml | 1 + 7 files changed, 91 insertions(+), 42 deletions(-) diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index b3a671eb..0d00b852 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -17,18 +17,23 @@ class Ajax::WebPushController < AjaxController @response[:status] = :okay @response[:success] = true + @response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count) end def unsubscribe params.permit(:endpoint) if params.key?(:endpoint) - current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy + current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all else current_user.web_push_subscriptions.destroy_all end + count = current_user.web_push_subscriptions.count + @response[:status] = :okay @response[:success] = true + @response[:message] = t(".subscription_count", count:) + @response[:count] = count end end diff --git a/app/javascript/retrospring/features/webpush/enable.ts b/app/javascript/retrospring/features/webpush/enable.ts index c52ca51c..56088c35 100644 --- a/app/javascript/retrospring/features/webpush/enable.ts +++ b/app/javascript/retrospring/features/webpush/enable.ts @@ -4,6 +4,7 @@ import { showNotification } from "utilities/notifications"; export function enableHandler (event: Event): void { event.preventDefault(); + const sender = event.target as HTMLButtonElement; try { installServiceWorker() @@ -26,6 +27,13 @@ export function enableHandler (event: Event): void { new Notification(I18n.translate("frontend.push_notifications.subscribe.success.title"), { body: I18n.translate("frontend.push_notifications.subscribe.success.body") }); + + document.querySelectorAll('button[data-action="push-disable"], button[data-action="push-remove-all"]') + .forEach(button => button.classList.remove('d-none')); + + sender.classList.add('d-none'); + + document.getElementById('subscription-count').textContent = data.message; } else { new Notification(I18n.translate("frontend.push_notifications.fail.title"), { body: I18n.translate("frontend.push_notifications.fail.body") diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index e9149684..dcdbeef1 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -8,17 +8,30 @@ export default (): void => { const notificationCapable = document.body.classList.contains('cap-notification'); if (swCapable && notificationCapable) { - navigator.serviceWorker.getRegistration().then(registration => { - if (!registration && localStorage.getItem('dismiss-push-settings-prompt') == null) { - document.querySelector('.push-settings').classList.remove('d-none'); - } - }) - registerEvents([ - {type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true}, - {type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true}, - {type: 'click', target: '[data-action="push-disable"]', handler: () => unsubscribeHandler(false), global: true}, - {type: 'click', target: '[data-action="push-remove-all"]', handler: () => unsubscribeHandler(true), global: true}, - ]); + navigator.serviceWorker.getRegistration().then(registration => { + return registration.pushManager.getSubscription().then(subscription => { + if (!subscription) { + document.querySelector('button[data-action="push-enable"]').classList.remove('d-none'); + } else { + document.querySelector('[data-action="push-disable"]').classList.remove('d-none'); + if (localStorage.getItem('dismiss-push-settings-prompt') == null) { + document.querySelector('.push-settings')?.classList.remove('d-none'); + } + } + }); + }); } + + registerEvents([ + {type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true}, + {type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true}, + {type: 'click', target: '[data-action="push-disable"]', handler: unsubscribeHandler, global: true}, + { + type: 'click', + target: '[data-action="push-remove-all"]', + handler: unsubscribeHandler, + global: true + }, + ]); } diff --git a/app/javascript/retrospring/features/webpush/unsubscribe.ts b/app/javascript/retrospring/features/webpush/unsubscribe.ts index c89a8d7e..fb4bda8e 100644 --- a/app/javascript/retrospring/features/webpush/unsubscribe.ts +++ b/app/javascript/retrospring/features/webpush/unsubscribe.ts @@ -2,35 +2,52 @@ import { destroy } from '@rails/request.js'; import { showErrorNotification, showNotification } from "utilities/notifications"; import I18n from "retrospring/i18n"; -export function unsubscribeHandler(all = false): void { - getSubscription().then(subscription => { - const body = all ? { endpoint: subscription.endpoint } : undefined; - - destroy('/ajax/webpush', { - body, - contentType: 'application/json', - }).then(async response => { - const data = await response.json; - - if (data.success) { - subscription?.unsubscribe().then(() => { - showNotification(I18n.translate("frontend.push_notifications.unsubscribe.success")); - }).catch(error => { - console.error("Tried to unsubscribe this browser but was unable to.\n" + - "As we've been unsubscribed on the server-side, this should not be an issue.", - error); - }) - } else { - showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.fail")); - } - }).catch(error => { +export function unsubscribeHandler(): void { + navigator.serviceWorker.getRegistration() + .then(registration => registration.pushManager.getSubscription()) + .then(subscription => unsubscribeClient(subscription)) + .then(subscription => unsubscribeServer(subscription)) + .then() + .catch(error => { showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.error")); console.error(error); }); - }) } -async function getSubscription(): Promise { - const registration = await navigator.serviceWorker.getRegistration('/'); - return await registration.pushManager.getSubscription(); +async function unsubscribeClient(subscription?: PushSubscription): Promise { + subscription?.unsubscribe().then(success => { + if (!success) { + throw new Error("Failed to unsubscribe."); + } + }); + + return subscription; +} + +async function unsubscribeServer(subscription?: PushSubscription): Promise { + const body = subscription != null ? { endpoint: subscription.endpoint } : undefined; + + return destroy('/ajax/webpush', { + body, + contentType: 'application/json', + }).then(async response => { + const data = await response.json; + + if (data.success) { + showNotification(I18n.translate("frontend.push_notifications.unsubscribe.success")); + + document.getElementById('subscription-count').textContent = data.message; + + if (data.count == 0) { + document.querySelectorAll('button[data-action="push-disable"], button[data-action="push-remove-all"]') + .forEach(button => button.classList.add('d-none')); + } + + if (document.body.classList.contains('cap-service-worker') && document.body.classList.contains('cap-notification')) { + document.querySelector('button[data-action="push-enable"]')?.classList.remove('d-none'); + } + } else { + showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.fail")); + } + }) } diff --git a/app/views/settings/_push_notifications.haml b/app/views/settings/_push_notifications.haml index 05753739..12438a32 100644 --- a/app/views/settings/_push_notifications.haml +++ b/app/views/settings/_push_notifications.haml @@ -1,7 +1,7 @@ .card.push-notifications-settings .card-body %p= t('.description') - %p= t('.subscription_count', count: subscriptions.count) + %p#subscription-count= t('.subscription_count', count: subscriptions.count) .push-notifications-unavailable.text-danger %i.fa.fa-warning @@ -12,6 +12,6 @@ = t('.current_target') .button-group{ role: 'group' } - %button.btn.btn-primary{ data: { action: 'push-enable' } }= t('.subscribe') - %button.btn.btn-primary{ data: { action: 'push-disable' } }= t('.unsubscribe_current') - %button.btn.btn-danger{ data: { action: 'push-remove-all' } }= t('.unsubscribe_all') + %button.btn.btn-primary{ data: { action: 'push-enable' }, class: 'd-none' }= t('.subscribe') + %button.btn.btn-primary{ data: { action: 'push-disable' }, class: 'd-none' }= t('.unsubscribe_current') + %button.btn.btn-danger{ data: { action: 'push-remove-all' }, class: subscriptions.empty? ? 'd-none' : '' }= t('.unsubscribe_all') diff --git a/config/locales/controllers.en.yml b/config/locales/controllers.en.yml index 32a548be..1373b154 100644 --- a/config/locales/controllers.en.yml +++ b/config/locales/controllers.en.yml @@ -139,6 +139,11 @@ en: destroy_comment: success: "Successfully unsmiled comment." error: "You have not smiled that comment." + web_push: + subscription_count: + zero: "You are not currently subscribed to push notifications on any devices." + one: "You are currently receiving push notifications on one device." + other: "You are currently receiving push notifications on %{count} devices." inbox: author: info: "No questions from @%{author} found, showing entries from all users instead!" diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 0c3aca6c..d4deff76 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -543,6 +543,7 @@ en: subscribe: "Enable on this device" unsubscribe_current: "Disable on this device" unsubscribe_all: "Disable on all devices" + description: "Here you can set up or disable push notifications for new questions in your inbox." shared: links: about: "About" From 3619f463608ae42bd8b0d5ef31fa53aa28d28748 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 23 Oct 2022 16:07:41 +0200 Subject: [PATCH 15/42] Add unsubscribe messages --- config/locales/frontend.en.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/frontend.en.yml b/config/locales/frontend.en.yml index 19e8ea79..96228659 100644 --- a/config/locales/frontend.en.yml +++ b/config/locales/frontend.en.yml @@ -57,6 +57,9 @@ en: fail: title: Failed to subscribe to push notifications body: Please try again later + unsubscribe: + success: Push notifications disabled successfully. + fail: Failed to disable push notifications. setup_fail: Failed to set up push notifications. Please try again later. inbox: title: New question from %{user} From 2f8126d73208dc87b320cf70f6dfa4230145b2c3 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 23 Oct 2022 16:30:05 +0200 Subject: [PATCH 16/42] Supress lint errors in Add RPush migration --- db/migrate/20220909161542_add_rpush.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/db/migrate/20220909161542_add_rpush.rb b/db/migrate/20220909161542_add_rpush.rb index f90a4934..fbc0af97 100644 --- a/db/migrate/20220909161542_add_rpush.rb +++ b/db/migrate/20220909161542_add_rpush.rb @@ -41,6 +41,8 @@ class AddRpush < ActiveRecord::Migration[5.0] end end + # rubocop:disable Rails/MigrationClassName + class CreateRapnsNotifications < ActiveRecord::Migration[5.0] def self.up create_table :rapns_notifications do |t| @@ -335,4 +337,6 @@ class AddRpush < ActiveRecord::Migration[5.0] remove_column :rpush_notifications, :fail_after end end + + # rubocop:enable all end From dc80c1dba38add19ab8250aa0c87947cf06a3dc6 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Thu, 22 Dec 2022 22:48:03 +0000 Subject: [PATCH 17/42] Fix push notification settings not appearing when not subscribed --- .../retrospring/features/webpush/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index dcdbeef1..681284fb 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -8,19 +8,22 @@ export default (): void => { const notificationCapable = document.body.classList.contains('cap-notification'); if (swCapable && notificationCapable) { + const enableBtn = document.querySelector('button[data-action="push-enable"]'); - navigator.serviceWorker.getRegistration().then(registration => { - return registration.pushManager.getSubscription().then(subscription => { - if (!subscription) { - document.querySelector('button[data-action="push-enable"]').classList.remove('d-none'); - } else { + if (!enableBtn) return; + + enableBtn.classList.remove('d-none'); + + navigator.serviceWorker.getRegistration().then(registration => + registration?.pushManager.getSubscription().then(subscription => { + if (subscription) { + document.querySelector('button[data-action="push-enable"]').classList.add('d-none'); document.querySelector('[data-action="push-disable"]').classList.remove('d-none'); if (localStorage.getItem('dismiss-push-settings-prompt') == null) { document.querySelector('.push-settings')?.classList.remove('d-none'); } } - }); - }); + })); } registerEvents([ From 8c2bfcb452637e69e2fbe15914ff0c3d73ed0638 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Thu, 22 Dec 2022 22:48:24 +0000 Subject: [PATCH 18/42] Use JSON for notification payload --- app/models/user/push_notification_methods.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb index dcf0fec9..32b630bd 100644 --- a/app/models/user/push_notification_methods.rb +++ b/app/models/user/push_notification_methods.rb @@ -9,7 +9,7 @@ module User::PushNotificationMethods n.app = app n.registration_ids = [s.subscription.symbolize_keys] n.data = { - message: resource.as_push_notification + message: resource.as_push_notification.to_json } n.save! end From 44112c5449a33e3a46d83b56b8901d8b72d7d79f Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Fri, 23 Dec 2022 13:03:28 +0000 Subject: [PATCH 19/42] Test for sending notifications for new questions --- spec/workers/question_worker_spec.rb | 36 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/spec/workers/question_worker_spec.rb b/spec/workers/question_worker_spec.rb index ab1e2914..584baac7 100644 --- a/spec/workers/question_worker_spec.rb +++ b/spec/workers/question_worker_spec.rb @@ -43,8 +43,8 @@ describe QuestionWorker do it "respects inbox locks" do user.followers.first.update(privacy_lock_inbox: true) - - + + expect { subject } .to( change { Inbox.where(user_id: user.followers.ids, question_id:, new: true).count } @@ -52,7 +52,7 @@ describe QuestionWorker do .to(4) ) end - + it "does not send questions to banned users" do user.followers.first.ban @@ -63,5 +63,35 @@ describe QuestionWorker do .to(4) ) end + + context "receiver has push notifications enabled" do + let(:receiver) { FactoryBot.create(:user) } + + before do + Rpush::Webpush::App.create( + name: "webpush", + certificate: { public_key: "AAAA", private_key: "AAAA", subject: "" }.to_json, + connections: 1, + ) + + WebPushSubscription.create!( + user: receiver, + subscription: { + endpoint: "This will not be used", + keys: {}, + }, + ) + receiver.follow(user) + end + + it "sends notifications" do + expect { subject } + .to( + change { Rpush::Webpush::Notification.count } + .from(0) + .to(1) + ) + end + end end end From 3eafa5e335517e955db14502b6fb65c7d2223861 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sat, 24 Dec 2022 16:41:21 +0000 Subject: [PATCH 20/42] Add tests for subscription management --- app/controllers/ajax/web_push_controller.rb | 8 +- .../ajax/web_push_controller_spec.rb | 200 ++++++++++++++++++ .../push_notifications_controller_spec.rb | 20 ++ 3 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 spec/controllers/ajax/web_push_controller_spec.rb create mode 100644 spec/controllers/settings/push_notifications_controller_spec.rb diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index 0d00b852..8f0408ad 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Ajax::WebPushController < AjaxController + before_action :authenticate_user! + def key certificate = Rpush::Webpush::App.find_by(name: "webpush").certificate @@ -23,7 +25,7 @@ class Ajax::WebPushController < AjaxController def unsubscribe params.permit(:endpoint) - if params.key?(:endpoint) + removed = if params.key?(:endpoint) current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all else current_user.web_push_subscriptions.destroy_all @@ -31,8 +33,8 @@ class Ajax::WebPushController < AjaxController count = current_user.web_push_subscriptions.count - @response[:status] = :okay - @response[:success] = true + @response[:status] = removed.any? ? :okay : :err + @response[:success] = removed.any? @response[:message] = t(".subscription_count", count:) @response[:count] = count end diff --git a/spec/controllers/ajax/web_push_controller_spec.rb b/spec/controllers/ajax/web_push_controller_spec.rb new file mode 100644 index 00000000..dcac726a --- /dev/null +++ b/spec/controllers/ajax/web_push_controller_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Ajax::WebPushController, :ajax_controller, type: :controller do + before do + Rpush::Webpush::App.create( + name: "webpush", + certificate: { public_key: "AAAA", private_key: "BBBB", subject: "" }.to_json, + connections: 1 + ) + end + + describe "#key" do + subject { get :key, format: :json } + + let(:expected_response) do + { + "message" => "", + "status" => "okay", + "success" => true, + "key" => "AAAA" + } + end + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + + before { sign_in user } + + include_examples "returns the expected response" + end + end + + describe "#subscribe" do + subject { post :subscribe, params: } + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + + before { sign_in user } + + context "given a valid subscription" do + let(:params) do + { + subscription: { + endpoint: "https://some.webpush/endpoint", + keys: {} + } + } + end + let(:expected_response) do + { + "message" => I18n.t("settings.push_notifications.subscription_count.one"), + "status" => "okay", + "success" => true + } + end + + it "stores the subscription" do + expect { subject } + .to( + change { WebPushSubscription.count } + .by(1) + ) + end + + include_examples "returns the expected response" + end + end + + context "given no subscription param" do + let(:params) do + {} + end + end + end + + describe "#unsubscribe" do + subject { delete :unsubscribe, params: } + + shared_examples_for "does not remove any subscriptions" do + it "does not remove any subscriptions" do + expect { subject }.not_to(change { WebPushSubscription.count }) + end + end + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + + before { sign_in user } + + context "valid subscription" do + let(:endpoint) { "some endpoint" } + let!(:subscription) do + WebPushSubscription.create( + user:, + subscription: { endpoint:, keys: {} } + ) + end + let!(:other_subscription) do + WebPushSubscription.create( + user:, + subscription: { endpoint: "other endpoint", keys: {} } + ) + end + let(:params) do + { endpoint: } + end + let(:expected_response) do + { + "status" => "okay", + "success" => true, + "message" => I18n.t("ajax.web_push.subscription_count.one"), + "count" => 1 + } + end + + it "removes the subscription" do + subject + expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { other_subscription.reload }.not_to raise_error(ActiveRecord::RecordNotFound) + end + + include_examples "returns the expected response" + end + + context "invalid subscription" do + let!(:subscription) do + WebPushSubscription.create( + user:, + subscription: { endpoint: "other endpoint", keys: {} } + ) + end + let(:params) do + { endpoint: "some endpoint" } + end + let(:expected_response) do + { + "status" => "err", + "success" => false, + "message" => I18n.t("ajax.web_push.subscription_count.one"), + "count" => 1 + } + end + + include_examples "does not remove any subscriptions" + include_examples "returns the expected response" + end + + context "someone else's subscription" do + let(:endpoint) { "some endpoint" } + let(:other_user) { FactoryBot.create(:user) } + let!(:subscription) do + WebPushSubscription.create( + user: other_user, + subscription: { endpoint:, keys: {} } + ) + end + let(:params) do + { endpoint: } + end + let(:expected_response) do + { + "status" => "err", + "success" => false, + "message" => I18n.t("ajax.web_push.subscription_count.zero"), + "count" => 0 + } + end + + include_examples "does not remove any subscriptions" + include_examples "returns the expected response" + end + + context "no subscription provided" do + let(:other_user) { FactoryBot.create(:user) } + let(:params) { {} } + + before do + 4.times do |i| + WebPushSubscription.create( + user: i.zero? ? other_user : user, + subscription: { endpoint: i, keys: {} } + ) + end + end + + it "removes all own subscriptions" do + expect { subject } + .to( + change { WebPushSubscription.count } + .from(4) + .to(1) + ) + end + end + end + end +end diff --git a/spec/controllers/settings/push_notifications_controller_spec.rb b/spec/controllers/settings/push_notifications_controller_spec.rb new file mode 100644 index 00000000..d78b7350 --- /dev/null +++ b/spec/controllers/settings/push_notifications_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Settings::PushNotificationsController, type: :controller do + describe "#index" do + subject { get :index } + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + + before { sign_in user } + + it "renders the index template" do + subject + expect(response).to render_template(:index) + end + end + end +end From d9514a306acce072ea349cf66129aeb71f9e86b8 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sat, 24 Dec 2022 20:10:55 +0000 Subject: [PATCH 21/42] Make push notification settings reinit on navigation --- app/javascript/packs/application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/packs/application.ts b/app/javascript/packs/application.ts index eb7044a4..c071d6d4 100644 --- a/app/javascript/packs/application.ts +++ b/app/javascript/packs/application.ts @@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', initModeration); document.addEventListener('DOMContentLoaded', initMemes); document.addEventListener('turbo:load', initLocales); document.addEventListener('turbo:load', initFront); -document.addEventListener('DOMContentLoaded', initWebpush); +document.addEventListener('turbo:load', initWebpush); window['Stimulus'] = Application.start(); const context = require.context('../retrospring/controllers', true, /\.ts$/); From 062e293607780869333c988d18bf7dee08eb721e Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sat, 24 Dec 2022 20:11:21 +0000 Subject: [PATCH 22/42] Fix missing namespace qualifier for Rpush --- lib/use_case/question/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index a0a55450..abe57516 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -30,7 +30,7 @@ module UseCase inbox = ::Inbox.create!(user: target_user, question: question, new: true) - webpush_app = Rpush::App.find_by(name: "webpush") + webpush_app = ::Rpush::App.find_by(name: "webpush") target_user.push_notification(webpush_app, inbox) { From 16eb27cc2b3df5fe402147a825204e49858b1f5a Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sat, 24 Dec 2022 20:11:30 +0000 Subject: [PATCH 23/42] Add icon to notifications --- public/service_worker.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/service_worker.js b/public/service_worker.js index 7b903ecd..08e1f717 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -5,7 +5,8 @@ self.addEventListener('push', function (event) { event.waitUntil(self.registration.showNotification(notification.title, { body: notification.body, - tag: notification.type + tag: notification.type, + icon: "/icons/maskable_icon_x128.png" })); } else { console.error("Push event received, but it didn't contain any data.", event); From 185c454da0314c5186e3aa0df7a673727ce2629e Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sat, 24 Dec 2022 20:20:13 +0000 Subject: [PATCH 24/42] Fix incorrect author relationship name --- app/models/inbox.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/inbox.rb b/app/models/inbox.rb index fde7b26b..0e7e0327 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -32,7 +32,7 @@ class Inbox < ApplicationRecord type: :inbox, title: I18n.t( "frontend.push_notifications.inbox.title", - user: question.author_is_anonymous ? user.profile.display_name : question.author.profile.safe_name + user: question.author_is_anonymous ? user.profile.display_name : question.user.profile.safe_name ), body: question.content } From 89008364d94084523828cbfdc513f49999d7fe43 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 25 Dec 2022 18:08:55 +0000 Subject: [PATCH 25/42] Handle push notifications in Sidekiq job --- app/models/user/push_notification_methods.rb | 2 ++ app/workers/push_notification_worker.rb | 17 +++++++++++++++++ config/sidekiq.yml | 1 + 3 files changed, 20 insertions(+) create mode 100644 app/workers/push_notification_worker.rb diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb index 32b630bd..f2446805 100644 --- a/app/models/user/push_notification_methods.rb +++ b/app/models/user/push_notification_methods.rb @@ -12,6 +12,8 @@ module User::PushNotificationMethods message: resource.as_push_notification.to_json } n.save! + + PushNotificationWorker.perform_async(n.id) end end end diff --git a/app/workers/push_notification_worker.rb b/app/workers/push_notification_worker.rb new file mode 100644 index 00000000..34902d41 --- /dev/null +++ b/app/workers/push_notification_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "rpush/daemon" + +class PushNotificationWorker + include Sidekiq::Worker + + sidekiq_options queue: :push_notification, retry: 0 + + def perform(notification_id) + Rpush.config.push = true + Rpush::Daemon.common_init + Rpush::Daemon::Synchronizer.sync + Rpush::Daemon::AppRunner.enqueue(Rpush::Client::ActiveRecord::Notification.where(id: notification_id)) + Rpush::Daemon::AppRunner.stop + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 2236bdc5..b9c163be 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -12,4 +12,5 @@ production: - mailers - question - export + - push_notification From 10c224b2febfa7f41b0783fe372e25babad72298 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 25 Dec 2022 23:30:06 +0000 Subject: [PATCH 26/42] Address review comments from @nilsding Co-authored-by: nilsding --- app/controllers/ajax/web_push_controller.rb | 10 +++++----- app/workers/question_worker.rb | 2 +- config/justask.yml.example | 2 ++ db/migrate/20220909220449_add_webpush_app.rb | 2 +- lib/use_case/question/create.rb | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index 8f0408ad..f48d2174 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -22,14 +22,14 @@ class Ajax::WebPushController < AjaxController @response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count) end - def unsubscribe + def unsubscribe # rubocop:disable Metrics/AbcSize params.permit(:endpoint) removed = if params.key?(:endpoint) - current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all - else - current_user.web_push_subscriptions.destroy_all - end + current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all + else + current_user.web_push_subscriptions.destroy_all + end count = current_user.web_push_subscriptions.count diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index eea5ea27..3936f77f 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -19,7 +19,7 @@ class QuestionWorker next if user.muting?(question.user) inbox = Inbox.create(user_id: f.id, question_id:, new: true) - f.push_notification(webpush_app, inbox) + f.push_notification(webpush_app, inbox) if webpush_app end rescue StandardError => e logger.info "failed to ask question: #{e.message}" diff --git a/config/justask.yml.example b/config/justask.yml.example index 023f6b85..1d627249 100644 --- a/config/justask.yml.example +++ b/config/justask.yml.example @@ -8,6 +8,8 @@ hostname: "justask.rrerr.net" https: true email_from: "noreply@justask.rrerr.net" +# Required by WebPush spec in case of problems with notifications +contact_email: "contact@justask.rrerr.net" # Name of the "Anonymous" user. (e.g. "Anonymous Coward", "Arno Nym", "Mr. X", ...) anonymous_name: "Anonymous" diff --git a/db/migrate/20220909220449_add_webpush_app.rb b/db/migrate/20220909220449_add_webpush_app.rb index e527d64c..efa8a205 100644 --- a/db/migrate/20220909220449_add_webpush_app.rb +++ b/db/migrate/20220909220449_add_webpush_app.rb @@ -7,7 +7,7 @@ class AddWebpushApp < ActiveRecord::Migration[6.1] vapid_keypair = Webpush.generate_key.to_hash app = Rpush::Webpush::App.new app.name = "webpush" - app.certificate = vapid_keypair.merge(subject: "user@example.com").to_json # TODO: put an email address here + app.certificate = vapid_keypair.merge(subject: APP_CONFIG["contact_email"]).to_json app.connections = 1 app.save! end diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index abe57516..db92a068 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -31,7 +31,7 @@ module UseCase inbox = ::Inbox.create!(user: target_user, question: question, new: true) webpush_app = ::Rpush::App.find_by(name: "webpush") - target_user.push_notification(webpush_app, inbox) + target_user.push_notification(webpush_app, inbox) if webpush_app { status: 201, From 91d3db4034b40ebe5b0523bb8e88e772e092040c Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 25 Dec 2022 23:34:04 +0000 Subject: [PATCH 27/42] Move rpush init/exit logic into Sidekiq initializer Co-authored-by: nilsding --- app/workers/push_notification_worker.rb | 4 ---- config/initializers/15_sidekiq.rb | 14 +++++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/workers/push_notification_worker.rb b/app/workers/push_notification_worker.rb index 34902d41..7bc08cd4 100644 --- a/app/workers/push_notification_worker.rb +++ b/app/workers/push_notification_worker.rb @@ -8,10 +8,6 @@ class PushNotificationWorker sidekiq_options queue: :push_notification, retry: 0 def perform(notification_id) - Rpush.config.push = true - Rpush::Daemon.common_init - Rpush::Daemon::Synchronizer.sync Rpush::Daemon::AppRunner.enqueue(Rpush::Client::ActiveRecord::Notification.where(id: notification_id)) - Rpush::Daemon::AppRunner.stop end end diff --git a/config/initializers/15_sidekiq.rb b/config/initializers/15_sidekiq.rb index f0985236..d2045cf4 100644 --- a/config/initializers/15_sidekiq.rb +++ b/config/initializers/15_sidekiq.rb @@ -1,9 +1,21 @@ +require "rpush/daemon" +require "rpush/daemon/store/active_record" +require "rpush/client/active_record" + redis_url = ENV.fetch("REDIS_URL") { APP_CONFIG["redis_url"] } Sidekiq.configure_server do |config| config.redis = { url: redis_url } + Rpush.config.push = true + Rpush::Daemon.store = Rpush::Daemon::Store::ActiveRecord.new + Rpush::Daemon.common_init + Rpush::Daemon::Synchronizer.sync + + at_exit do + Rpush::Daemon::AppRunner.stop + end end Sidekiq.configure_client do |config| config.redis = { url: redis_url } -end \ No newline at end of file +end From a67c26d985e15e717142d27b3b2b62a007e479b6 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 25 Dec 2022 23:44:01 +0000 Subject: [PATCH 28/42] Fix missing anon names from notification text --- app/models/inbox.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 0e7e0327..f0ee2cfb 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -32,7 +32,11 @@ class Inbox < ApplicationRecord type: :inbox, title: I18n.t( "frontend.push_notifications.inbox.title", - user: question.author_is_anonymous ? user.profile.display_name : question.user.profile.safe_name + user: if question.author_is_anonymous + user.profile.anon_display_name || APP_CONFIG["anonymous_name"] + else + question.user.profile.safe_name + end ), body: question.content } From 2d6f539dfd924b93a369a2d67ba5d63820dfd400 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 26 Dec 2022 00:19:52 +0000 Subject: [PATCH 29/42] Use author avatar on notification --- app/models/inbox.rb | 1 + public/service_worker.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/inbox.rb b/app/models/inbox.rb index f0ee2cfb..fbc631d6 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -38,6 +38,7 @@ class Inbox < ApplicationRecord question.user.profile.safe_name end ), + icon: question.author_is_anonymous ? "/icons/maskable_icon_x128.png" : question.user.profile_picture.url(:small), body: question.content } end diff --git a/public/service_worker.js b/public/service_worker.js index 08e1f717..383b3d0a 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -6,7 +6,7 @@ self.addEventListener('push', function (event) { event.waitUntil(self.registration.showNotification(notification.title, { body: notification.body, tag: notification.type, - icon: "/icons/maskable_icon_x128.png" + icon: notification.icon, })); } else { console.error("Push event received, but it didn't contain any data.", event); From ee9c48fd06ed9be653142aaf2d5cf161774b5d4c Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 26 Dec 2022 09:59:48 +0000 Subject: [PATCH 30/42] Clean up question create use case --- lib/use_case/question/create.rb | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index db92a068..857e1b14 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -11,27 +11,16 @@ module UseCase option :direct, type: Types::Params::Bool, default: proc { true } def call - check_user - check_lock - check_anonymous_rules - check_blocks + do_checks! - question = ::Question.create!( - content: content, - author_is_anonymous: anonymous, - author_identifier: author_identifier, - user: source_user_id.nil? ? nil : source_user, - direct: direct - ) + question = create_question return if filtered?(question) increment_asked_count - inbox = ::Inbox.create!(user: target_user, question: question, new: true) - - webpush_app = ::Rpush::App.find_by(name: "webpush") - target_user.push_notification(webpush_app, inbox) if webpush_app + inbox = ::Inbox.create!(user: target_user, question:, new: true) + notify { status: 201, @@ -44,6 +33,13 @@ module UseCase private + def do_checks! + check_user + check_lock + check_anonymous_rules + check_blocks + end + def check_lock raise Errors::InboxLocked if target_user.inbox_locked? end @@ -69,6 +65,21 @@ module UseCase raise Errors::NotAuthorized if target_user.privacy_require_user && !source_user_id end + def create_question + ::Question.create!( + content:, + author_is_anonymous: anonymous, + author_identifier:, + user: source_user_id.nil? ? nil : source_user, + direct: + ) + end + + def notify + webpush_app = ::Rpush::App.find_by(name: "webpush") + target_user.push_notification(webpush_app, inbox) if webpush_app + end + def increment_asked_count unless source_user_id && !anonymous && !direct # Only increment the asked count of the source user if the question From 22a84ab818eb3adc506db0bec7738a2506f0b65d Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 26 Dec 2022 09:59:56 +0000 Subject: [PATCH 31/42] Appease the dog overlords --- app/models/user.rb | 2 +- app/workers/question_worker.rb | 8 +++++++- db/migrate/20220909161542_add_rpush.rb | 3 +-- db/migrate/20220909161543_rpush_2_0_0_updates.rb | 2 +- spec/controllers/ajax/web_push_controller_spec.rb | 2 +- spec/workers/question_worker_spec.rb | 7 +++---- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index a5263e0c..a56bb514 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class User < ApplicationRecord +class User < ApplicationRecord # rubocop:disable Metrics/ClassLength include User::Relationship include User::Relationship::Follow include User::Relationship::Block diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index 3936f77f..571bc609 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -15,7 +15,7 @@ class QuestionWorker user.followers.each do |f| next if f.inbox_locked? next if f.banned? - next if MuteRule.where(user: f).any? { |rule| rule.applies_to? question } + next if muted?(f, question) next if user.muting?(question.user) inbox = Inbox.create(user_id: f.id, question_id:, new: true) @@ -25,4 +25,10 @@ class QuestionWorker logger.info "failed to ask question: #{e.message}" Sentry.capture_exception(e) end + + private + + def muted?(user, question) + MuteRule.where(user:).any? { |rule| rule.applies_to? question } + end end diff --git a/db/migrate/20220909161542_add_rpush.rb b/db/migrate/20220909161542_add_rpush.rb index fbc0af97..9b8cba01 100644 --- a/db/migrate/20220909161542_add_rpush.rb +++ b/db/migrate/20220909161542_add_rpush.rb @@ -22,6 +22,7 @@ # many times, by many people! class AddRpush < ActiveRecord::Migration[5.0] + # rubocop:disable all def self.migrations [CreateRapnsNotifications, CreateRapnsFeedback, AddAlertIsJsonToRapnsNotifications, AddAppToRapns, @@ -41,8 +42,6 @@ class AddRpush < ActiveRecord::Migration[5.0] end end - # rubocop:disable Rails/MigrationClassName - class CreateRapnsNotifications < ActiveRecord::Migration[5.0] def self.up create_table :rapns_notifications do |t| diff --git a/db/migrate/20220909161543_rpush_2_0_0_updates.rb b/db/migrate/20220909161543_rpush_2_0_0_updates.rb index 102c1337..505b74a0 100644 --- a/db/migrate/20220909161543_rpush_2_0_0_updates.rb +++ b/db/migrate/20220909161543_rpush_2_0_0_updates.rb @@ -12,7 +12,7 @@ class Rpush200Updates < ActiveRecord::Migration[5.0] end def self.update_type(model, from, to) - model.where(type: from).update_all(type: to) + model.where(type: from).update_all(type: to) # rubocop:disable Rails/SkipsModelValidations end def self.up diff --git a/spec/controllers/ajax/web_push_controller_spec.rb b/spec/controllers/ajax/web_push_controller_spec.rb index dcac726a..ea24d960 100644 --- a/spec/controllers/ajax/web_push_controller_spec.rb +++ b/spec/controllers/ajax/web_push_controller_spec.rb @@ -153,7 +153,7 @@ describe Ajax::WebPushController, :ajax_controller, type: :controller do let(:other_user) { FactoryBot.create(:user) } let!(:subscription) do WebPushSubscription.create( - user: other_user, + user: other_user, subscription: { endpoint:, keys: {} } ) end diff --git a/spec/workers/question_worker_spec.rb b/spec/workers/question_worker_spec.rb index 584baac7..cf022124 100644 --- a/spec/workers/question_worker_spec.rb +++ b/spec/workers/question_worker_spec.rb @@ -44,7 +44,6 @@ describe QuestionWorker do it "respects inbox locks" do user.followers.first.update(privacy_lock_inbox: true) - expect { subject } .to( change { Inbox.where(user_id: user.followers.ids, question_id:, new: true).count } @@ -71,15 +70,15 @@ describe QuestionWorker do Rpush::Webpush::App.create( name: "webpush", certificate: { public_key: "AAAA", private_key: "AAAA", subject: "" }.to_json, - connections: 1, + connections: 1 ) WebPushSubscription.create!( user: receiver, subscription: { endpoint: "This will not be used", - keys: {}, - }, + keys: {} + } ) receiver.follow(user) end From 67423699b6b7bc8c48debbecf97bf40b06466da1 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 26 Dec 2022 11:03:18 +0000 Subject: [PATCH 32/42] Use fetch to get contact_email from config in webpush app migration --- db/migrate/20220909220449_add_webpush_app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20220909220449_add_webpush_app.rb b/db/migrate/20220909220449_add_webpush_app.rb index efa8a205..91b1d32d 100644 --- a/db/migrate/20220909220449_add_webpush_app.rb +++ b/db/migrate/20220909220449_add_webpush_app.rb @@ -7,7 +7,7 @@ class AddWebpushApp < ActiveRecord::Migration[6.1] vapid_keypair = Webpush.generate_key.to_hash app = Rpush::Webpush::App.new app.name = "webpush" - app.certificate = vapid_keypair.merge(subject: APP_CONFIG["contact_email"]).to_json + app.certificate = vapid_keypair.merge(subject: APP_CONFIG.fetch("contact_email")).to_json app.connections = 1 app.save! end From ba3c406bc742f79a97a337ea9db329dde7db01f1 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 26 Dec 2022 11:09:25 +0000 Subject: [PATCH 33/42] Remove notification after click --- public/service_worker.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/service_worker.js b/public/service_worker.js index 383b3d0a..f42dd8ef 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -16,7 +16,10 @@ self.addEventListener('push', function (event) { self.addEventListener('notificationclick', async event => { if (event.notification.tag === 'inbox') { event.preventDefault(); - return clients.openWindow("/inbox", "_blank"); + return clients.openWindow("/inbox", "_blank").then(result => { + event.notification.close(); + return result; + }); } else { console.warn(`Unhandled notification tag: ${event.notification.tag}`); } From 1cfd3250c0a94cf5001b0e09cf9232b7172696c8 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Tue, 27 Dec 2022 23:22:51 +0000 Subject: [PATCH 34/42] Track failures on Web Push subscriptions --- .../settings/push_notifications_controller.rb | 2 +- app/models/user/push_notification_methods.rb | 2 +- app/models/web_push_subscription.rb | 3 +++ config/initializers/rpush.rb | 10 ++++++++-- ...226101907_add_failures_to_web_push_subscriptions.rb | 7 +++++++ db/schema.rb | 1 + 6 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20221226101907_add_failures_to_web_push_subscriptions.rb diff --git a/app/controllers/settings/push_notifications_controller.rb b/app/controllers/settings/push_notifications_controller.rb index 551ae46e..f207cb32 100644 --- a/app/controllers/settings/push_notifications_controller.rb +++ b/app/controllers/settings/push_notifications_controller.rb @@ -4,6 +4,6 @@ class Settings::PushNotificationsController < ApplicationController before_action :authenticate_user! def index - @subscriptions = current_user.web_push_subscriptions + @subscriptions = current_user.web_push_subscriptions.active end end diff --git a/app/models/user/push_notification_methods.rb b/app/models/user/push_notification_methods.rb index f2446805..01138633 100644 --- a/app/models/user/push_notification_methods.rb +++ b/app/models/user/push_notification_methods.rb @@ -4,7 +4,7 @@ module User::PushNotificationMethods def push_notification(app, resource) raise ArgumentError("Resource must respond to `as_push_notification`") unless resource.respond_to? :as_push_notification - web_push_subscriptions.each do |s| + web_push_subscriptions.active.find_each do |s| n = Rpush::Webpush::Notification.new n.app = app n.registration_ids = [s.subscription.symbolize_keys] diff --git a/app/models/web_push_subscription.rb b/app/models/web_push_subscription.rb index dddf28db..6821d10b 100644 --- a/app/models/web_push_subscription.rb +++ b/app/models/web_push_subscription.rb @@ -2,4 +2,7 @@ class WebPushSubscription < ApplicationRecord belongs_to :user + + scope :active, -> { where(failures: ...3) } + scope :failed, -> { where(failures: 3..) } end diff --git a/config/initializers/rpush.rb b/config/initializers/rpush.rb index 0aaf129c..3cb8eb77 100644 --- a/config/initializers/rpush.rb +++ b/config/initializers/rpush.rb @@ -57,8 +57,14 @@ Rpush.reflect do |on| # Called when notification delivery failed. # Call 'error_code' and 'error_description' on the notification for the cause. - # on.notification_failed do |notification| - # end + on.notification_failed do |notification| + # See: https://developer.apple.com/documentation/usernotifications/sending_web_push_notifications_in_safari_and_other_browsers#3994594 + if %i[403 410].include? notification.error_code + subscription = WebPushSubscription::where("subscription ->> 'endpoint' = ?", notification.registration_ids.first[:endpoint]) + subscription.increment :failures + subscription.save + end + end # Called when the notification delivery failed and only the notification ID # is present in memory. diff --git a/db/migrate/20221226101907_add_failures_to_web_push_subscriptions.rb b/db/migrate/20221226101907_add_failures_to_web_push_subscriptions.rb new file mode 100644 index 00000000..e52b50e7 --- /dev/null +++ b/db/migrate/20221226101907_add_failures_to_web_push_subscriptions.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFailuresToWebPushSubscriptions < ActiveRecord::Migration[6.1] + def change + add_column :web_push_subscriptions, :failures, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 6c2491d6..05514255 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -387,6 +387,7 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do t.json "subscription" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.integer "failures", default: 0 t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id" end From fccf35fdab28ca051c6bf1c17d4ca4d3781c1475 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 21:28:48 +0100 Subject: [PATCH 35/42] Restore push notification prompt in inbox --- .../retrospring/features/webpush/index.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index 681284fb..e5c6ecaf 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -4,21 +4,20 @@ import { dismissHandler } from "./dismiss"; import { unsubscribeHandler } from "retrospring/features/webpush/unsubscribe"; export default (): void => { - const swCapable = document.body.classList.contains('cap-service-worker'); - const notificationCapable = document.body.classList.contains('cap-notification'); + const swCapable = 'serviceWorker' in navigator; + const notificationCapable = 'Notification' in window; if (swCapable && notificationCapable) { const enableBtn = document.querySelector('button[data-action="push-enable"]'); - if (!enableBtn) return; - - enableBtn.classList.remove('d-none'); - navigator.serviceWorker.getRegistration().then(registration => registration?.pushManager.getSubscription().then(subscription => { if (subscription) { - document.querySelector('button[data-action="push-enable"]').classList.add('d-none'); - document.querySelector('[data-action="push-disable"]').classList.remove('d-none'); + document.querySelector('button[data-action="push-enable"]')?.classList.add('d-none'); + document.querySelector('[data-action="push-disable"]')?.classList.remove('d-none'); + } else { + enableBtn?.classList.remove('d-none'); + if (localStorage.getItem('dismiss-push-settings-prompt') == null) { document.querySelector('.push-settings')?.classList.remove('d-none'); } From e0195654b5e851ef2adf0a9d6f35e41eb01539e2 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 21:34:31 +0100 Subject: [PATCH 36/42] Send notification on 3 push failures --- app/models/notification/push_subscription_error.rb | 4 ++++ .../notifications/type/_webpushsubscription.html.haml | 10 ++++++++++ config/initializers/rpush.rb | 8 ++++++++ config/locales/views.en.yml | 4 ++++ 4 files changed, 26 insertions(+) create mode 100644 app/models/notification/push_subscription_error.rb create mode 100644 app/views/notifications/type/_webpushsubscription.html.haml diff --git a/app/models/notification/push_subscription_error.rb b/app/models/notification/push_subscription_error.rb new file mode 100644 index 00000000..2e6be2c7 --- /dev/null +++ b/app/models/notification/push_subscription_error.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Notification::PushSubscriptionError < Notification +end diff --git a/app/views/notifications/type/_webpushsubscription.html.haml b/app/views/notifications/type/_webpushsubscription.html.haml new file mode 100644 index 00000000..ce9b4a80 --- /dev/null +++ b/app/views/notifications/type/_webpushsubscription.html.haml @@ -0,0 +1,10 @@ +.media.notification + .notification__icon + %span.fa-stack + %i.fa.fa-2x.fa-fw.fa-bell + %i.fa.fa-stack-1x.fa-fw.fa-exclamation-triangle.text-danger.pl-2 + .media-body + %h6.media-heading.notification__user + = t(".heading") + .notification__text + = t(".text_html", settings_push: link_to(t(".settings_push"), settings_push_notifications_path)) diff --git a/config/initializers/rpush.rb b/config/initializers/rpush.rb index 3cb8eb77..6c1175e6 100644 --- a/config/initializers/rpush.rb +++ b/config/initializers/rpush.rb @@ -63,6 +63,14 @@ Rpush.reflect do |on| subscription = WebPushSubscription::where("subscription ->> 'endpoint' = ?", notification.registration_ids.first[:endpoint]) subscription.increment :failures subscription.save + + if subscription.failures > 3 + Notification::PushSubscriptionError.create( + target: subscription, + recipient: subscription.user, + new: true + ) + end end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index d4deff76..e52b74ff 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -354,6 +354,10 @@ en: link_text: "your comment" follow: heading_html: "followed you %{time} ago" + webpushsubscription: + heading: "Push notifications are failing to send to one of your devices." + text_html: "Please check the %{settings_push} if you still want to be notified." + settings_push: "push notification settings" expiredtwitterserviceconnection: heading: "Twitter connection expired" text_html: "If you would like to continue automatically sharing your answers to Twitter, head to %{settings_sharing} and re-connect your account." From efad76855e0940b7cf247cad8ca471e920d37c09 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 21:34:49 +0100 Subject: [PATCH 37/42] Add endpoint for checking subscription status --- app/controllers/ajax/web_push_controller.rb | 17 +++++++++++++++++ config/routes.rb | 1 + 2 files changed, 18 insertions(+) diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index f48d2174..d6ef3032 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -11,6 +11,23 @@ class Ajax::WebPushController < AjaxController @response[:key] = JSON.parse(certificate)["public_key"] end + def check + params.permit(:endpoint) + + found = WebPushSubscription.where("subscription ->> 'endpoint' = ?", params[:endpoint]).first + + @response[:status] = if found + if found.failures >= 3 + :failed + else + :subscribed + end + else + :unsubscribed + end + @response[:success] = true + end + def subscribe WebPushSubscription.create!( user: current_user, diff --git a/config/routes.rb b/config/routes.rb index 55300166..0929f60c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,7 @@ Rails.application.routes.draw do post "/subscribe", to: "subscription#subscribe", as: :subscribe_answer post "/unsubscribe", to: "subscription#unsubscribe", as: :unsubscribe_answer get "/webpush/key", to: "web_push#key", as: :webpush_key + post "/webpush/check", to: "web_push#check", as: :webpush_check post "/webpush", to: "web_push#subscribe", as: :webpush_subscribe delete "/webpush", to: "web_push#unsubscribe", as: :webpush_unsubscribe end From 2417354b31177ed9ebcfac48599441d341ceefd7 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 22:06:01 +0100 Subject: [PATCH 38/42] Unsubscribe client on too many failures --- .../retrospring/features/webpush/index.ts | 9 ++++++++- .../features/webpush/unsubscribe.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts index e5c6ecaf..bb5a154d 100644 --- a/app/javascript/retrospring/features/webpush/index.ts +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -1,7 +1,9 @@ import registerEvents from 'retrospring/utilities/registerEvents'; import { enableHandler } from './enable'; import { dismissHandler } from "./dismiss"; -import { unsubscribeHandler } from "retrospring/features/webpush/unsubscribe"; +import { unsubscribeHandler, checkSubscription } from "retrospring/features/webpush/unsubscribe"; + +let subscriptionChecked = false; export default (): void => { const swCapable = 'serviceWorker' in navigator; @@ -15,6 +17,11 @@ export default (): void => { if (subscription) { document.querySelector('button[data-action="push-enable"]')?.classList.add('d-none'); document.querySelector('[data-action="push-disable"]')?.classList.remove('d-none'); + + if (!subscriptionChecked) { + checkSubscription(subscription); + subscriptionChecked = true; + } } else { enableBtn?.classList.remove('d-none'); diff --git a/app/javascript/retrospring/features/webpush/unsubscribe.ts b/app/javascript/retrospring/features/webpush/unsubscribe.ts index fb4bda8e..0b388634 100644 --- a/app/javascript/retrospring/features/webpush/unsubscribe.ts +++ b/app/javascript/retrospring/features/webpush/unsubscribe.ts @@ -1,4 +1,4 @@ -import { destroy } from '@rails/request.js'; +import { post, destroy } from '@rails/request.js'; import { showErrorNotification, showNotification } from "utilities/notifications"; import I18n from "retrospring/i18n"; @@ -7,13 +7,27 @@ export function unsubscribeHandler(): void { .then(registration => registration.pushManager.getSubscription()) .then(subscription => unsubscribeClient(subscription)) .then(subscription => unsubscribeServer(subscription)) - .then() .catch(error => { showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.error")); console.error(error); }); } +export function checkSubscription(subscription: PushSubscription): void { + post('/ajax/webpush/check', { + body: { + endpoint: subscription.endpoint + }, + contentType: 'application/json' + }).then(async response => { + const data = await response.json(); + + if (data.status == 'subscribed') return; + if (data.status == 'failed') await unsubscribeServer(subscription); + await unsubscribeClient(subscription); + }) +} + async function unsubscribeClient(subscription?: PushSubscription): Promise { subscription?.unsubscribe().then(success => { if (!success) { From 48c7beb54e2adbec251d8593e644c9aed190c1c3 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 22:07:02 +0100 Subject: [PATCH 39/42] Only allow checking of own subscriptions --- app/controllers/ajax/web_push_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/ajax/web_push_controller.rb b/app/controllers/ajax/web_push_controller.rb index d6ef3032..e9723c54 100644 --- a/app/controllers/ajax/web_push_controller.rb +++ b/app/controllers/ajax/web_push_controller.rb @@ -14,7 +14,7 @@ class Ajax::WebPushController < AjaxController def check params.permit(:endpoint) - found = WebPushSubscription.where("subscription ->> 'endpoint' = ?", params[:endpoint]).first + found = current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).first @response[:status] = if found if found.failures >= 3 From 5a3f2966dd0b817285a2ad6ea6dfaee969db70a5 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 1 Jan 2023 22:27:36 +0100 Subject: [PATCH 40/42] Add tests for subscription check endpoint --- .../ajax/web_push_controller_spec.rb | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/spec/controllers/ajax/web_push_controller_spec.rb b/spec/controllers/ajax/web_push_controller_spec.rb index ea24d960..f82000b1 100644 --- a/spec/controllers/ajax/web_push_controller_spec.rb +++ b/spec/controllers/ajax/web_push_controller_spec.rb @@ -197,4 +197,67 @@ describe Ajax::WebPushController, :ajax_controller, type: :controller do end end end + + describe "#check" do + subject { post :check, params: } + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + let(:endpoint) { "https://some.domain/some/webpush/endpoint" } + let!(:subscription) do + WebPushSubscription.create( + user:, + subscription: { endpoint:, keys: {} }, + failures: + ) + end + let(:expected_response) do + { + "message" => "", + "status" => expected_status, + "success" => true + } + end + + before { sign_in user } + + context "subscription exists" do + let(:params) do + { endpoint: } + end + + context "without failures" do + let(:failures) { 0 } + let(:expected_status) { "subscribed" } + + it_behaves_like "returns the expected response" + end + + context "with 2 failures" do + let(:failures) { 2 } + let(:expected_status) { "subscribed" } + + it_behaves_like "returns the expected response" + end + + context "with 3 failures" do + let(:failures) { 3 } + let(:expected_status) { "failed" } + + it_behaves_like "returns the expected response" + end + end + + context "subscription doesn't exist" do + let(:params) do + { endpoint: "https;//some.domain/some/other/endpoint" } + end + + let(:failures) { 0 } + let(:expected_status) { "unsubscribed" } + + it_behaves_like "returns the expected response" + end + end + end end From c20974d182dfc5be6aa9c8799d42c75be1721bb3 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 2 Jan 2023 12:04:48 +0100 Subject: [PATCH 41/42] Appease the dog overlords Co-authored-by: nilsding --- app/workers/question_worker.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index 571bc609..c30c98d4 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -13,10 +13,7 @@ class QuestionWorker webpush_app = Rpush::App.find_by(name: "webpush") user.followers.each do |f| - next if f.inbox_locked? - next if f.banned? - next if muted?(f, question) - next if user.muting?(question.user) + next if skip_inbox?(f, question, user) inbox = Inbox.create(user_id: f.id, question_id:, new: true) f.push_notification(webpush_app, inbox) if webpush_app @@ -28,6 +25,15 @@ class QuestionWorker private + def skip_inbox?(follower, question, user) + return true if follower.inbox_locked? + return true if follower.banned? + return true if muted?(follower, question) + return true if user.muting?(question.user) + + false + end + def muted?(user, question) MuteRule.where(user:).any? { |rule| rule.applies_to? question } end From 1adf3956ba370ea055e377f1fac967010bc544d2 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Mon, 2 Jan 2023 12:57:14 +0100 Subject: [PATCH 42/42] Remove `console.log` --- public/service_worker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/service_worker.js b/public/service_worker.js index f42dd8ef..1f13f0f8 100644 --- a/public/service_worker.js +++ b/public/service_worker.js @@ -1,7 +1,6 @@ self.addEventListener('push', function (event) { if (event.data) { const notification = event.data.json(); - console.log(event.data); event.waitUntil(self.registration.showNotification(notification.title, { body: notification.body,