Skip to content

Commit

Permalink
Backport 'Implement push notifications for conversations' messages' t…
Browse files Browse the repository at this point in the history
…o v0.27 (#12511)

* Implement push notifications for conversations' messages

* Refactor variables definitions in SendPushNotification spec

* Add spec for ConversationMailer

It also adds the factories to ease-up the specs

* Extract ConversationMailer methods to HasConversation concern

* Add PushNotificationMessage model

* Implement push notifications for conversations' messages

* Fix spec

* Simplify PushNotificationMessage model by removing useless attributes

* Fix url and icon methods in PushNotificationMessage

As a bonus, there are more examples in the spec.

* Add missing class in yardoc

* Clarify  signature

* Use more idiomatic way of checking for presence

Co-authored-by: Alexandru Emil Lupu <contact@alecslupu.ro>

* Move module inclusion to the top for consistency

Co-authored-by: Alexandru Emil Lupu <contact@alecslupu.ro>

* Add skip_injection on factory definition

Co-authored-by: Alexandru Emil Lupu <contact@alecslupu.ro>

* Use alias_method for :user

* Fix rubocop offenses

---------

Co-authored-by: Alexandru Emil Lupu <contact@alecslupu.ro>

* Running linters

* Add missing methods

* Fix failing specs

* Run Linter

* Fix failing specs

* Apply suggestions from code review

Co-authored-by: Andrés Pereira de Lucena <andreslucena@users.noreply.github.com>

* Update decidim-core/lib/decidim/core/test/factories.rb

Co-authored-by: Andrés Pereira de Lucena <andreslucena@users.noreply.github.com>

---------

Co-authored-by: Andrés Pereira de Lucena <andreslucena@users.noreply.github.com>
  • Loading branch information
alecslupu and andreslucena committed Feb 27, 2024
1 parent 013bfd3 commit c0fac7e
Show file tree
Hide file tree
Showing 13 changed files with 652 additions and 215 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ def notify_interlocutors
notify(manager) do
ConversationMailer.new_group_message(sender, manager, conversation, message, recipient).deliver_later
end
Decidim::PushNotificationMessageSender.new.new_group_message(sender, manager, conversation, message, recipient).deliver
end
else
notify(recipient) do
ConversationMailer.new_message(sender, recipient, conversation, message).deliver_later
end
Decidim::PushNotificationMessageSender.new.new_message(sender, recipient, conversation, message).deliver
end
end
end
Expand All @@ -68,6 +70,7 @@ def notify_comanagers
notify(recipient) do
ConversationMailer.comanagers_new_message(sender, recipient, conversation, message, form.context.current_user).deliver_later
end
Decidim::PushNotificationMessageSender.new.comanagers_new_message(sender, recipient, conversation, message, form.context.current_user).deliver
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ def notify_interlocutors
notify(manager) do
ConversationMailer.new_group_conversation(originator, manager, conversation, recipient).deliver_later
end
Decidim::PushNotificationMessageSender.new.new_group_conversation(originator, manager, conversation, recipient).deliver
end
else
notify(recipient) do
ConversationMailer.new_conversation(originator, recipient, conversation).deliver_later
end
Decidim::PushNotificationMessageSender.new.new_conversation(originator, recipient, conversation).deliver
end
end
end
Expand All @@ -68,6 +70,7 @@ def notify_comanagers
notify(recipient) do
ConversationMailer.comanagers_new_conversation(originator, recipient, conversation, form.context.current_user).deliver_later
end
Decidim::PushNotificationMessageSender.new.comanagers_new_conversation(originator, recipient, conversation, form.context.current_user).deliver
end
end

Expand Down
75 changes: 3 additions & 72 deletions decidim-core/app/mailers/decidim/messaging/conversation_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,12 @@ module Messaging
# A custom mailer for sending notifications to users when they receive
# private messages
class ConversationMailer < Decidim::ApplicationMailer
def new_conversation(originator, user, conversation)
notification_mail(
from: originator,
to: user,
conversation: conversation,
message: conversation.messages.first.body,
action: "new_conversation"
)
end

def new_group_conversation(originator, manager, conversation, group)
notification_mail(
from: originator,
to: manager,
conversation: conversation,
message: conversation.messages.first.body,
action: "new_group_conversation",
third_party: group
)
end

def comanagers_new_conversation(group, user, conversation, manager)
notification_mail(
from: group,
to: user,
conversation: conversation,
message: conversation.messages.first.body,
action: "comanagers_new_conversation",
third_party: manager
)
end

def new_message(sender, user, conversation, message)
notification_mail(
from: sender,
to: user,
conversation: conversation,
message: message.body,
action: "new_message"
)
end

def new_group_message(sender, user, conversation, message, group)
notification_mail(
from: sender,
to: user,
conversation: conversation,
message: message.body,
action: "new_group_message",
third_party: group
)
end

def comanagers_new_message(sender, user, conversation, message, manager)
notification_mail(
from: sender,
to: user,
conversation: conversation,
message: message.body,
action: "comanagers_new_message",
third_party: manager
)
end
include HasConversations

private

# rubocop:disable Metrics/ParameterLists
def notification_mail(from:, to:, conversation:, action:, message: nil, third_party: nil)
def send_notification(from:, to:, conversation:, action:, message: nil, third_party: nil)
with_user(to) do
@organization = to.organization
@conversation = conversation
Expand All @@ -81,14 +19,7 @@ def notification_mail(from:, to:, conversation:, action:, message: nil, third_pa
@third_party = third_party
@message = message
@host = @organization.host

subject = I18n.t(
"conversation_mailer.#{action}.subject",
scope: "decidim.messaging",
sender: @sender.name,
manager: @third_party&.name,
group: @third_party&.name
)
subject = get_subject(action: action, sender: @sender, third_party: @third_party)

mail(to: to.email, subject: subject)
end
Expand Down
39 changes: 39 additions & 0 deletions decidim-core/app/models/decidim/push_notification_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Decidim
# A messsage from a conversation that will be sent as a push notification
class PushNotificationMessage
class InvalidActionError < StandardError; end

include SanitizeHelper
include Decidim::TranslatableAttributes

def initialize(recipient:, conversation:, message:)
@recipient = recipient
@conversation = conversation
@message = message
end

attr_reader :recipient, :conversation, :message

alias user recipient

def body
decidim_html_escape(translated_attribute(message))
end

def icon
organization.attached_uploader(:favicon).variant_url(:big, host: organization.host)
end

def url
EngineRouter.new("decidim", {}).public_send(:conversation_path, host: organization.host, id: @conversation)
end

private

def organization
@organization ||= recipient.organization
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Decidim
# A wrapper for preparing push notifications messages from conversations
# It respects the same contract as the Decidim::Messaging::ConversationMailer
class PushNotificationMessageSender
include HasConversations

def deliver
SendPushNotification.new.perform(@notification, title)
end

private

# rubocop:disable Metrics/ParameterLists
# rubocop:disable Lint/UnusedMethodArgument
#
# There are some parameters thar are not used in the method, but they are needed to
# keep the same contract as the Decidim::Messaging::ConversationMailer
def send_notification(from:, to:, conversation:, action:, message: nil, third_party: nil)
@action = action
@sender = to
@third_party = third_party

@notification = PushNotificationMessage.new(recipient: to, conversation: conversation, message: message)

self
end
# rubocop:enable Lint/UnusedMethodArgument
# rubocop:enable Metrics/ParameterLists

def title
get_subject(action: @action, sender: @sender, third_party: @third_party)
end
end
end
30 changes: 22 additions & 8 deletions decidim-core/app/services/decidim/send_push_notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@ class SendPushNotification
# Send the push notification. Returns `nil` if the user didn't allowed push notifications
# or if the subscription to push notifications doesn't exist
#
# Returns the result of the dispatch or nil if user or subscription are empty
def perform(notification)
# @param notification [Decidim::Notification, Decidim::PushNotificationMessage] the notification to be sent
# @param title [String] the title of the notification. Optional.
#
# @return [Array<Net::HTTPCreated>, nil] the result of the dispatch or nil if user or subscription are empty
def perform(notification, title = nil)
return unless Rails.application.secrets.dig(:vapid, :enabled)
raise ArgumentError, "Need to provide a title if the notification is a PushNotificationMessage" if notification.is_a?(Decidim::PushNotificationMessage) && title.nil?

user = notification.user

I18n.with_locale(notification.user.locale || notification.user.organization.default_locale) do
notification.user.notifications_subscriptions.values.map do |subscription|
message_params = notification_params(Decidim::PushNotificationPresenter.new(notification))
payload = build_payload(message_params, subscription)
I18n.with_locale(user.locale || user.organization.default_locale) do
user.notifications_subscriptions.values.map do |subscription|
payload = build_payload(message_params(notification, title), subscription)
# Capture webpush exceptions in order to avoid this call to be repeated by the background job runner
# Webpush::Error class is the parent class of all defined errors
begin
Expand All @@ -36,9 +41,18 @@ def perform(notification)

private

def notification_params(notification)
def message_params(notification, title = nil)
case notification
when Decidim::PushNotificationMessage
notification_params(notification, title)
else # when Decidim::Notification
notification_params(Decidim::PushNotificationPresenter.new(notification))
end
end

def notification_params(notification, title = nil)
{
title: notification.title,
title: title.presence || notification.title,
body: notification.body,
icon: notification.icon,
data: { url: notification.url }
Expand Down
1 change: 1 addition & 0 deletions decidim-core/lib/decidim/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ module Decidim
autoload :ControllerHelpers, "decidim/controller_helpers"
autoload :ProcessesFileLocally, "decidim/processes_file_locally"
autoload :DependencyResolver, "decidim/dependency_resolver"
autoload :HasConversations, "decidim/has_conversations"

include ActiveSupport::Configurable
# Loads seeds from all engines.
Expand Down
78 changes: 76 additions & 2 deletions decidim-core/lib/decidim/core/test/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,39 @@
require "decidim/assemblies/test/factories"
require "decidim/comments/test/factories"

def generate_localized_title
Decidim::Faker::Localized.localized { generate(:title) }
def generate_component_name(locales, manifest_name, skip_injection: false)
prepend = skip_injection ? "" : "<script>alert(\"#{manifest_name}\");</script>"

Decidim::Components::Namer.new(locales, manifest_name).i18n_name.transform_values { |v| [prepend, v].compact_blank.join(" ") }
end

def generate_localized_description(field = nil, skip_injection: false, before: "<p>", after: "</p>")
Decidim::Faker::Localized.wrapped(before, after) do
generate_localized_title(field, skip_injection: skip_injection)
end
end

def generate_localized_word(field = nil, skip_injection: false)
skip_injection = true if field.nil?
Decidim::Faker::Localized.localized do
if skip_injection
Faker::Lorem.word
else
"<script>alert(\"#{field}\");</script> #{Faker::Lorem.word}"
end
end
end

def generate_localized_title(field = nil, skip_injection: false)
skip_injection = true if field.nil?

Decidim::Faker::Localized.localized do
if skip_injection
generate(:title)
else
"<script>alert(\"#{field}\");</script> #{generate(:title)}"
end
end
end

FactoryBot.define do
Expand Down Expand Up @@ -629,6 +660,49 @@ def generate_localized_title
end
end

factory :conversation, class: "Decidim::Messaging::Conversation" do
transient do
skip_injection { false }
end

originator { build(:user) }
interlocutors { [build(:user)] }
body { Faker::Lorem.sentence }
user

after(:create) do |object|
object.participants ||= [originator + interlocutors].flatten
end

initialize_with { Decidim::Messaging::Conversation.start(originator: originator, interlocutors: interlocutors, body: body, user: user) }
end

factory :message, class: "Decidim::Messaging::Message" do
transient do
skip_injection { false }
end

body { generate_localized_description(:message_body) }
conversation

before(:create) do |object|
object.sender ||= object.conversation.participants.take
end
end

factory :push_notification_message, class: "Decidim::PushNotificationMessage" do
transient do
skip_injection { false }
end

recipient { build(:user) }
conversation { create(:conversation) }
message { generate_localized_description(:push_notification_message_message) }

skip_create
initialize_with { Decidim::PushNotificationMessage.new(recipient: recipient, conversation: conversation, message: message) }
end

factory :action_log, class: "Decidim::ActionLog" do
transient do
extra_data { {} }
Expand Down

0 comments on commit c0fac7e

Please sign in to comment.