create NotificationService: send notifications after receive
This commit is contained in:
parent
97f4b0c2e4
commit
ebfb0aa884
15 changed files with 255 additions and 134 deletions
|
|
@ -3,15 +3,15 @@
|
||||||
# the COPYRIGHT file.
|
# the COPYRIGHT file.
|
||||||
#
|
#
|
||||||
class Notification < ActiveRecord::Base
|
class Notification < ActiveRecord::Base
|
||||||
belongs_to :recipient, :class_name => 'User'
|
belongs_to :recipient, class_name: "User"
|
||||||
has_many :notification_actors, :dependent => :destroy
|
has_many :notification_actors, dependent: :destroy
|
||||||
has_many :actors, :class_name => 'Person', :through => :notification_actors, :source => :person
|
has_many :actors, class_name: "Person", through: :notification_actors, source: :person
|
||||||
belongs_to :target, :polymorphic => true
|
belongs_to :target, polymorphic: true
|
||||||
|
|
||||||
attr_accessor :note_html
|
attr_accessor :note_html
|
||||||
|
|
||||||
def self.for(recipient, opts={})
|
def self.for(recipient, opts={})
|
||||||
self.where(opts.merge!(:recipient_id => recipient.id)).order('updated_at desc')
|
where(opts.merge!(recipient_id: recipient.id)).order("updated_at DESC")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.notify(recipient, target, actor)
|
def self.notify(recipient, target, actor)
|
||||||
|
|
@ -33,11 +33,11 @@ class Notification < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_json(opts={})
|
def as_json(opts={})
|
||||||
super(opts.merge(:methods => :note_html))
|
super(opts.merge(methods: :note_html))
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_the_user(target, actor)
|
def email_the_user(target, actor)
|
||||||
self.recipient.mail(self.mail_job, self.recipient_id, actor.id, target.id)
|
recipient.mail(mail_job, recipient_id, actor.id, target.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_read_state( read_state )
|
def set_read_state( read_state )
|
||||||
|
|
@ -45,14 +45,13 @@ class Notification < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def mail_job
|
def mail_job
|
||||||
raise NotImplementedError.new('Subclass this.')
|
raise NotImplementedError.new("Subclass this.")
|
||||||
end
|
end
|
||||||
|
|
||||||
def effective_target
|
def linked_object
|
||||||
self.popup_translation_key == "notifications.mentioned" ? self.target.post : self.target
|
target
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
def self.concatenate_or_create(recipient, target, actor, notification_type)
|
def self.concatenate_or_create(recipient, target, actor, notification_type)
|
||||||
return nil if suppress_notification?(recipient, target)
|
return nil if suppress_notification?(recipient, target)
|
||||||
|
|
||||||
|
|
@ -76,7 +75,6 @@ private
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def self.make_notification(recipient, target, actor, notification_type)
|
def self.make_notification(recipient, target, actor, notification_type)
|
||||||
return nil if suppress_notification?(recipient, target)
|
return nil if suppress_notification?(recipient, target)
|
||||||
n = notification_type.new(:target => target,
|
n = notification_type.new(:target => target,
|
||||||
|
|
@ -87,9 +85,28 @@ private
|
||||||
n
|
n
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.concatenate_or_create(recipient, target, actor)
|
||||||
|
return nil if suppress_notification?(recipient, target)
|
||||||
|
|
||||||
|
find_or_initialize_by(recipient: recipient, target: target, unread: true).tap do |notification|
|
||||||
|
notification.actors |= [actor]
|
||||||
|
# Explicitly touch the notification to update updated_at whenever new actor is inserted in notification.
|
||||||
|
if notification.new_record? || notification.changed?
|
||||||
|
notification.save!
|
||||||
|
else
|
||||||
|
notification.touch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create_notification(recipient_id, target, actor)
|
||||||
|
create(recipient_id: recipient_id, target: target, actors: [actor])
|
||||||
|
end
|
||||||
|
|
||||||
def self.suppress_notification?(recipient, post)
|
def self.suppress_notification?(recipient, post)
|
||||||
post.is_a?(Post) && recipient.is_shareable_hidden?(post)
|
post.is_a?(Post) && recipient.is_shareable_hidden?(post)
|
||||||
end
|
end
|
||||||
|
private_class_method :suppress_notification?
|
||||||
|
|
||||||
def self.types
|
def self.types
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
class Notifications::AlsoCommented < Notification
|
module Notifications
|
||||||
def mail_job
|
class AlsoCommented < Notification
|
||||||
Workers::Mail::AlsoCommented
|
def mail_job
|
||||||
end
|
Workers::Mail::AlsoCommented
|
||||||
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
def popup_translation_key
|
||||||
'notifications.also_commented'
|
"notifications.also_commented"
|
||||||
end
|
end
|
||||||
|
|
||||||
def deleted_translation_key
|
def deleted_translation_key
|
||||||
'notifications.also_commented_deleted'
|
"notifications.also_commented_deleted"
|
||||||
end
|
end
|
||||||
|
|
||||||
def linked_object
|
def self.notify(comment, _recipient_user_ids)
|
||||||
Post.where(:id => self.target_id).first
|
actor = comment.author
|
||||||
|
commentable = comment.commentable
|
||||||
|
recipient_ids = commentable.participants.local.where.not(id: [commentable.author_id, actor.id]).pluck(:owner_id)
|
||||||
|
|
||||||
|
User.where(id: recipient_ids).find_each do |recipient|
|
||||||
|
concatenate_or_create(recipient, commentable, actor)
|
||||||
|
.try {|notification| notification.email_the_user(comment, actor) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
class Notifications::CommentOnPost < Notification
|
module Notifications
|
||||||
def mail_job
|
class CommentOnPost < Notification
|
||||||
Workers::Mail::CommentOnPost
|
def mail_job
|
||||||
end
|
Workers::Mail::CommentOnPost
|
||||||
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
def popup_translation_key
|
||||||
'notifications.comment_on_post'
|
"notifications.comment_on_post"
|
||||||
end
|
end
|
||||||
|
|
||||||
def deleted_translation_key
|
def deleted_translation_key
|
||||||
'notifications.also_commented_deleted'
|
"notifications.also_commented_deleted"
|
||||||
end
|
end
|
||||||
|
|
||||||
def linked_object
|
def self.notify(comment, _recipient_user_ids)
|
||||||
Post.where(:id => self.target_id).first
|
actor = comment.author
|
||||||
|
commentable_author = comment.commentable.author
|
||||||
|
|
||||||
|
return unless commentable_author.local? && actor != commentable_author
|
||||||
|
|
||||||
|
concatenate_or_create(commentable_author.owner, comment.commentable, actor).email_the_user(comment, actor)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
class Notifications::Liked < Notification
|
module Notifications
|
||||||
def mail_job
|
class Liked < Notification
|
||||||
Workers::Mail::Liked
|
def mail_job
|
||||||
end
|
Workers::Mail::Liked
|
||||||
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
def popup_translation_key
|
||||||
'notifications.liked'
|
"notifications.liked"
|
||||||
end
|
end
|
||||||
|
|
||||||
def deleted_translation_key
|
def deleted_translation_key
|
||||||
'notifications.liked_post_deleted'
|
"notifications.liked_post_deleted"
|
||||||
end
|
end
|
||||||
|
|
||||||
def linked_object
|
def self.notify(like, _recipient_user_ids)
|
||||||
post = self.target
|
actor = like.author
|
||||||
post = post.target if post.is_a? Like
|
target_author = like.target.author
|
||||||
post
|
|
||||||
|
return unless like.target_type == "Post" && target_author.local? && actor != target_author
|
||||||
|
|
||||||
|
concatenate_or_create(target_author.owner, like.target, actor).email_the_user(like, actor)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,31 @@
|
||||||
class Notifications::Mentioned < Notification
|
module Notifications
|
||||||
def mail_job
|
class Mentioned < Notification
|
||||||
Workers::Mail::Mentioned
|
def mail_job
|
||||||
end
|
Workers::Mail::Mentioned
|
||||||
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
def popup_translation_key
|
||||||
'notifications.mentioned'
|
"notifications.mentioned"
|
||||||
end
|
end
|
||||||
|
|
||||||
def deleted_translation_key
|
def deleted_translation_key
|
||||||
'notifications.mentioned_deleted'
|
"notifications.mentioned_deleted"
|
||||||
end
|
end
|
||||||
|
|
||||||
def linked_object
|
def linked_object
|
||||||
Mention.find(self.target_id).post
|
target.post
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.notify(mentionable, recipient_user_ids)
|
||||||
|
actor = mentionable.author
|
||||||
|
|
||||||
|
mentionable.mentions.select {|mention| mention.person.local? }.each do |mention|
|
||||||
|
recipient = mention.person
|
||||||
|
|
||||||
|
next if recipient == actor || !(mentionable.public || recipient_user_ids.include?(recipient.owner_id))
|
||||||
|
|
||||||
|
create_notification(recipient.owner_id, mention, actor).email_the_user(mention, actor)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,30 @@
|
||||||
class Notifications::PrivateMessage < Notification
|
module Notifications
|
||||||
def mail_job
|
class PrivateMessage < Notification
|
||||||
Workers::Mail::PrivateMessage
|
def mail_job
|
||||||
end
|
Workers::Mail::PrivateMessage
|
||||||
def popup_translation_key
|
end
|
||||||
'notifications.private_message'
|
|
||||||
end
|
def popup_translation_key
|
||||||
def self.make_notification(recipient, target, actor, notification_type)
|
"notifications.private_message"
|
||||||
n = notification_type.new(:target => target,
|
end
|
||||||
:recipient_id => recipient.id)
|
|
||||||
target.increase_unread(recipient)
|
def self.notify(object, recipient_user_ids)
|
||||||
n.actors << actor
|
case object
|
||||||
n
|
when Conversation
|
||||||
|
object.messages.each do |message|
|
||||||
|
recipient_ids = recipient_user_ids - [message.author.owner_id]
|
||||||
|
User.where(id: recipient_ids).find_each {|recipient| notify_message(message, recipient) }
|
||||||
|
end
|
||||||
|
when Message
|
||||||
|
recipients = object.conversation.participants.select(&:local?) - [object.author]
|
||||||
|
recipients.each {|recipient| notify_message(object, recipient.owner) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.notify_message(message, recipient)
|
||||||
|
message.increase_unread(recipient)
|
||||||
|
new(recipient: recipient).email_the_user(message, message.author)
|
||||||
|
end
|
||||||
|
private_class_method :notify_message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
class Notifications::RequestAccepted < Notification
|
|
||||||
def mail_job
|
|
||||||
Workers::Mail::RequestAcceptance
|
|
||||||
end
|
|
||||||
def popup_translation_key
|
|
||||||
'notifications.request_accepted'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
class Notifications::Reshared < Notification
|
module Notifications
|
||||||
def mail_job
|
class Reshared < Notification
|
||||||
Workers::Mail::Reshared
|
def mail_job
|
||||||
end
|
Workers::Mail::Reshared
|
||||||
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
def popup_translation_key
|
||||||
'notifications.reshared'
|
"notifications.reshared"
|
||||||
end
|
end
|
||||||
|
|
||||||
def deleted_translation_key
|
def deleted_translation_key
|
||||||
'notifications.reshared_post_deleted'
|
"notifications.reshared_post_deleted"
|
||||||
end
|
end
|
||||||
|
|
||||||
def linked_object
|
def self.notify(reshare, _recipient_user_ids)
|
||||||
self.target
|
return unless reshare.root.present? && reshare.root.author.local?
|
||||||
|
|
||||||
|
actor = reshare.author
|
||||||
|
concatenate_or_create(reshare.root.author.owner, reshare.root, actor).email_the_user(reshare, actor)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
class Notifications::StartedSharing < Notification
|
module Notifications
|
||||||
def mail_job
|
class StartedSharing < Notification
|
||||||
Workers::Mail::StartedSharing
|
def mail_job
|
||||||
|
Workers::Mail::StartedSharing
|
||||||
|
end
|
||||||
|
|
||||||
|
def popup_translation_key
|
||||||
|
"notifications.started_sharing"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.notify(contact, _recipient_user_ids)
|
||||||
|
sender = contact.person
|
||||||
|
create_notification(contact.user_id, sender, sender).email_the_user(sender, sender)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def popup_translation_key
|
|
||||||
'notifications.started_sharing'
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_the_user(target, actor)
|
|
||||||
super(target.sender, actor)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def self.make_notification(recipient, target, actor, notification_type)
|
|
||||||
super(recipient, target.sender, actor, notification_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
21
app/services/notification_service.rb
Normal file
21
app/services/notification_service.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
class NotificationService
|
||||||
|
NOTIFICATION_TYPES = {
|
||||||
|
Comment => [Notifications::CommentOnPost, Notifications::AlsoCommented],
|
||||||
|
Like => [Notifications::Liked],
|
||||||
|
StatusMessage => [Notifications::Mentioned],
|
||||||
|
Conversation => [Notifications::PrivateMessage],
|
||||||
|
Message => [Notifications::PrivateMessage],
|
||||||
|
Reshare => [Notifications::Reshared],
|
||||||
|
Contact => [Notifications::StartedSharing]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def notify(object, recipient_user_ids)
|
||||||
|
notification_types(object).each {|type| type.notify(object, recipient_user_ids) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def notification_types(object)
|
||||||
|
NOTIFICATION_TYPES.fetch(object.class, [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
.media.stream_element{:data=>{:guid => note.id, :type => (Notification.types.key(note.type) || '') }, :class => (note.unread ? 'unread' : 'read')}
|
.media.stream_element{:data=>{:guid => note.id, :type => (Notification.types.key(note.type) || '') }, :class => (note.unread ? 'unread' : 'read')}
|
||||||
.unread-toggle.pull-right
|
.unread-toggle.pull-right
|
||||||
%i.entypo-eye{title: (note.unread ? t("notifications.index.mark_read") : t("notifications.index.mark_unread"))}
|
%i.entypo-eye{title: (note.unread ? t("notifications.index.mark_read") : t("notifications.index.mark_unread"))}
|
||||||
- if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.effective_target)
|
- if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.target)
|
||||||
.pull-right
|
.pull-right
|
||||||
= aspect_membership_dropdown(contact, note.effective_target, "right")
|
= aspect_membership_dropdown(contact, note.target, "right")
|
||||||
|
|
||||||
.media-object.pull-left
|
.media-object.pull-left
|
||||||
= person_image_link note.actors.first, :size => :thumb_small, :class => 'hovercardable'
|
= person_image_link note.actors.first, :size => :thumb_small, :class => 'hovercardable'
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ module Workers
|
||||||
def perform(object_class_string, object_id, recipient_user_ids)
|
def perform(object_class_string, object_id, recipient_user_ids)
|
||||||
object = object_class_string.constantize.find(object_id)
|
object = object_class_string.constantize.find(object_id)
|
||||||
# TODO: create visibilities
|
# TODO: create visibilities
|
||||||
# TODO: send notifications
|
NotificationService.new.notify(object, recipient_user_ids)
|
||||||
rescue ActiveRecord::RecordNotFound # Already deleted before the job could run
|
rescue ActiveRecord::RecordNotFound # Already deleted before the job could run
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,13 @@ describe "Receive federation messages feature" do
|
||||||
expect(new_contact).not_to be_nil
|
expect(new_contact).not_to be_nil
|
||||||
expect(new_contact.sharing).to eq(true)
|
expect(new_contact.sharing).to eq(true)
|
||||||
|
|
||||||
# TODO: handle notifications
|
expect(
|
||||||
# expect(
|
Notifications::StartedSharing.exists?(
|
||||||
# Notifications::StartedSharing.exists?(
|
recipient_id: alice.id,
|
||||||
# recipient_id: alice.id,
|
target_type: "Person",
|
||||||
# target_type: "Person",
|
target_id: sender.person.id
|
||||||
# target_id: sender.person.id
|
)
|
||||||
# )
|
).to be_truthy
|
||||||
# ).to be_truthy
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with sharing" do
|
context "with sharing" do
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ shared_examples_for "messages which are indifferent about sharing fact" do
|
||||||
|
|
||||||
describe "notifications are sent where required" do
|
describe "notifications are sent where required" do
|
||||||
it "for comment on local post" do
|
it "for comment on local post" do
|
||||||
skip("TODO: handle notifications") # TODO
|
|
||||||
|
|
||||||
entity = create_relayable_entity(:comment_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
|
entity = create_relayable_entity(:comment_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
|
||||||
post_message(generate_xml(entity, sender, recipient), recipient)
|
post_message(generate_xml(entity, sender, recipient), recipient)
|
||||||
|
|
||||||
|
|
@ -41,8 +39,6 @@ shared_examples_for "messages which are indifferent about sharing fact" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "for like on local post" do
|
it "for like on local post" do
|
||||||
skip("TODO: handle notifications") # TODO
|
|
||||||
|
|
||||||
entity = create_relayable_entity(:like_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
|
entity = create_relayable_entity(:like_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
|
||||||
post_message(generate_xml(entity, sender, recipient), recipient)
|
post_message(generate_xml(entity, sender, recipient), recipient)
|
||||||
|
|
||||||
|
|
@ -134,8 +130,6 @@ shared_examples_for "messages which can't be send without sharing" do
|
||||||
# this one shouldn't depend on the sharing fact. this must be fixed
|
# this one shouldn't depend on the sharing fact. this must be fixed
|
||||||
describe "notifications are sent where required" do
|
describe "notifications are sent where required" do
|
||||||
it "for comment on remote post where we participate" do
|
it "for comment on remote post where we participate" do
|
||||||
skip("TODO: handle notifications") # TODO
|
|
||||||
|
|
||||||
alice.participate!(remote_parent)
|
alice.participate!(remote_parent)
|
||||||
author_id = remote_user_on_pod_c.diaspora_handle
|
author_id = remote_user_on_pod_c.diaspora_handle
|
||||||
entity = create_relayable_entity(:comment_entity, remote_parent, author_id)
|
entity = create_relayable_entity(:comment_entity, remote_parent, author_id)
|
||||||
|
|
|
||||||
47
spec/models/notifications/also_commented_spec.rb
Normal file
47
spec/models/notifications/also_commented_spec.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Copyright (c) 2010-2011, Diaspora Inc. This file is
|
||||||
|
# licensed under the Affero General Public License version 3 or later. See
|
||||||
|
# the COPYRIGHT file.
|
||||||
|
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
describe Notifications::AlsoCommented, type: :model do
|
||||||
|
let(:sm) { FactoryGirl.build(:status_message, author: alice.person, public: true) }
|
||||||
|
let(:comment) { FactoryGirl.create(:comment, commentable: sm) }
|
||||||
|
let(:notification) { Notifications::AlsoCommented.new(recipient: bob) }
|
||||||
|
|
||||||
|
describe ".notify" do
|
||||||
|
it "does not notify the commentable author" do
|
||||||
|
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
|
||||||
|
|
||||||
|
Notifications::AlsoCommented.notify(comment, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "notifies a local participant" do
|
||||||
|
bob.participate!(sm)
|
||||||
|
|
||||||
|
expect(Notifications::AlsoCommented).to receive(:concatenate_or_create).with(
|
||||||
|
bob, sm, comment.author
|
||||||
|
).and_return(notification)
|
||||||
|
expect(bob).to receive(:mail).with(Workers::Mail::AlsoCommented, bob.id, comment.author.id, comment.id)
|
||||||
|
|
||||||
|
Notifications::AlsoCommented.notify(comment, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not notify the a remote participant" do
|
||||||
|
FactoryGirl.create(:participation, target: sm)
|
||||||
|
|
||||||
|
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
|
||||||
|
|
||||||
|
Notifications::AlsoCommented.notify(comment, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not notify the author of the comment" do
|
||||||
|
bob.participate!(sm)
|
||||||
|
comment = FactoryGirl.create(:comment, commentable: sm, author: bob.person)
|
||||||
|
|
||||||
|
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
|
||||||
|
|
||||||
|
Notifications::AlsoCommented.notify(comment, [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue