From bc9be30a02d0480ca967ef96134241cc5f45fda9 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sat, 28 Mar 2015 15:43:31 +1030 Subject: [PATCH] More powerful/extensible notifications - Notifications can be delivered in multiple ways (alert, email) - Different notification types can implement interfaces to allow themselves to be delivered in these various ways - User preferences for each notification type/method combination are automatically registered --- ....js => notification-discussion-renamed.js} | 0 ...bs => notification-discussion-renamed.hbs} | 0 src/Core/CoreServiceProvider.php | 18 +----- .../Events/DiscussionRenamedNotifier.php | 24 ++++---- src/Core/Models/Notification.php | 29 +++++---- src/Core/Models/User.php | 12 +++- .../NotificationServiceProvider.php | 35 +++++++++++ src/Core/Notifications/Notifier.php | 59 +++++++++++++++++++ .../Senders/NotificationAlerter.php | 25 ++++++++ .../Senders/NotificationEmailer.php | 29 +++++++++ .../Senders/NotificationSender.php | 10 ++++ .../Types/AlertableNotification.php | 8 +++ .../Types/DiscussionRenamedNotification.php | 42 +++++++++++++ .../Types/EmailableNotification.php | 8 +++ src/Core/Notifications/Types/Notification.php | 36 +++++++++++ 15 files changed, 292 insertions(+), 43 deletions(-) rename ember/app/components/application/{notification-renamed.js => notification-discussion-renamed.js} (100%) rename ember/app/templates/components/application/{notification-renamed.hbs => notification-discussion-renamed.hbs} (100%) create mode 100644 src/Core/Notifications/NotificationServiceProvider.php create mode 100644 src/Core/Notifications/Notifier.php create mode 100644 src/Core/Notifications/Senders/NotificationAlerter.php create mode 100644 src/Core/Notifications/Senders/NotificationEmailer.php create mode 100644 src/Core/Notifications/Senders/NotificationSender.php create mode 100644 src/Core/Notifications/Types/AlertableNotification.php create mode 100644 src/Core/Notifications/Types/DiscussionRenamedNotification.php create mode 100644 src/Core/Notifications/Types/EmailableNotification.php create mode 100644 src/Core/Notifications/Types/Notification.php diff --git a/ember/app/components/application/notification-renamed.js b/ember/app/components/application/notification-discussion-renamed.js similarity index 100% rename from ember/app/components/application/notification-renamed.js rename to ember/app/components/application/notification-discussion-renamed.js diff --git a/ember/app/templates/components/application/notification-renamed.hbs b/ember/app/templates/components/application/notification-discussion-renamed.hbs similarity index 100% rename from ember/app/templates/components/application/notification-renamed.hbs rename to ember/app/templates/components/application/notification-discussion-renamed.hbs diff --git a/src/Core/CoreServiceProvider.php b/src/Core/CoreServiceProvider.php index 09e63e3e6..80b9fbe69 100644 --- a/src/Core/CoreServiceProvider.php +++ b/src/Core/CoreServiceProvider.php @@ -11,7 +11,6 @@ use Flarum\Core\Models\Model; use Flarum\Core\Models\Forum; use Flarum\Core\Models\User; use Flarum\Core\Models\Discussion; -use Flarum\Core\Models\Notification; use Flarum\Core\Search\GambitManager; use League\Flysystem\Adapter\Local; @@ -28,7 +27,6 @@ class CoreServiceProvider extends ServiceProvider $this->registerEventHandlers($events); $this->registerPostTypes(); - $this->registerNotificationTypes(); $this->registerPermissions(); $this->registerGambits(); $this->setupModels(); @@ -47,6 +45,8 @@ class CoreServiceProvider extends ServiceProvider */ public function register() { + $this->app->register('Flarum\Core\Notifications\NotificationServiceProvider'); + // Register a singleton entity that represents this forum. This entity // will be used to check for global forum permissions (like viewing the // forum, registering, and starting discussions.) @@ -81,14 +81,6 @@ class CoreServiceProvider extends ServiceProvider 'Flarum\Core\Repositories\ActivityRepositoryInterface', 'Flarum\Core\Repositories\EloquentActivityRepository' ); - $this->app->bind( - 'Flarum\Core\Repositories\NotificationRepositoryInterface', - 'Flarum\Core\Repositories\EloquentNotificationRepository' - ); - - $this->app->singleton('flarum.avatars.storage', function () { - return new Local(__DIR__.'/../../ember/public/avatars'); - }); $this->app->when('Flarum\Core\Handlers\Commands\UploadAvatarCommandHandler') ->needs('League\Flysystem\FilesystemInterface') @@ -126,16 +118,10 @@ class CoreServiceProvider extends ServiceProvider CommentPost::setFormatter($this->app['flarum.formatter']); } - public function registerNotificationTypes() - { - Notification::addType('renamed', 'Flarum\Core\Models\Discussion'); - } - public function registerEventHandlers($events) { $events->subscribe('Flarum\Core\Handlers\Events\DiscussionMetadataUpdater'); $events->subscribe('Flarum\Core\Handlers\Events\UserMetadataUpdater'); - $events->subscribe('Flarum\Core\Handlers\Events\DiscussionRenamedNotifier'); $events->subscribe('Flarum\Core\Handlers\Events\EmailConfirmationMailer'); } diff --git a/src/Core/Handlers/Events/DiscussionRenamedNotifier.php b/src/Core/Handlers/Events/DiscussionRenamedNotifier.php index 260616f0b..f989cc07c 100755 --- a/src/Core/Handlers/Events/DiscussionRenamedNotifier.php +++ b/src/Core/Handlers/Events/DiscussionRenamedNotifier.php @@ -1,11 +1,17 @@ notifier = $notifier; + } + /** * Register the listeners for the subscriber. * @@ -44,17 +50,13 @@ class DiscussionRenamedNotifier protected function sendNotification(DiscussionWasRenamed $event, DiscussionRenamedPost $post) { - - $notification = Notification::notify( - $event->discussion->start_user_id, - 'renamed', - $event->user->id, - $event->discussion->id, - ['number' => $post->number, 'oldTitle' => $event->oldTitle] + $notification = new DiscussionRenamedNotification( + $event->discussion->startUser, + $event->user, + $post, + $event->oldTitle ); - $notification->save(); - - return $notification; + $this->notifier->send($notification); } } diff --git a/src/Core/Models/Notification.php b/src/Core/Models/Notification.php index 63a7dd832..3d3962048 100644 --- a/src/Core/Models/Notification.php +++ b/src/Core/Models/Notification.php @@ -19,14 +19,13 @@ class Notification extends Model protected $dates = ['time']; /** - * A map of notification types, as specified in the `type` column, to - * their subject classes. + * * * @var array */ - protected static $types = []; + protected static $subjects = []; - public static function notify($userId, $type, $senderId, $subjectId, $data) + public static function alert($userId, $type, $senderId, $subjectId, $data) { $notification = new static; @@ -95,41 +94,41 @@ class Notification extends Model // If the type value is null it is probably safe to assume we're eager loading // the relationship. When that is the case we will pass in a dummy query as // there are multiple types in the morph and we can't use single queries. - if (is_null($type = $this->$typeColumn)) - { + if (is_null($type = $this->$typeColumn)) { return new MappedMorphTo( - $this->newQuery(), $this, $idColumn, null, $typeColumn, $name, static::$types + $this->newQuery(), $this, $idColumn, null, $typeColumn, $name, static::$subjects ); } // If we are not eager loading the relationship we will essentially treat this // as a belongs-to style relationship since morph-to extends that class and // we will pass in the appropriate values so that it behaves as expected. - else - { - $class = static::$types[$type]; + else { + $class = static::$subjects[$type]; $instance = new $class; return new MappedMorphTo( - $instance->newQuery(), $this, $idColumn, $instance->getKeyName(), $typeColumn, $name, static::$types + $instance->newQuery(), $this, $idColumn, $instance->getKeyName(), $typeColumn, $name, static::$subjects ); } } public static function getTypes() { - return static::$types; + return static::$subjects; } /** - * Register a notification type and its subject class. + * Register a notification type. * * @param string $type * @param string $class * @return void */ - public static function addType($type, $class) + public static function registerType($class) { - static::$types[$type] = $class; + if ($subject = $class::getSubjectModel()) { + static::$subjects[$class::getType()] = $subject; + } } } diff --git a/src/Core/Models/User.php b/src/Core/Models/User.php index 1ff3700dc..eccbe28e2 100755 --- a/src/Core/Models/User.php +++ b/src/Core/Models/User.php @@ -356,7 +356,7 @@ class User extends Model $defaults[$k] = $v['default']; } - return array_merge($defaults, (array) json_decode($value, true)); + return array_merge($defaults, array_only((array) json_decode($value, true), array_keys(static::$preferences))); } public function setPreferencesAttribute($value) @@ -372,6 +372,16 @@ class User extends Model ]; } + public static function notificationPreferenceKey($type, $sender) + { + return 'notify_'.$type.'_'.$sender; + } + + public function shouldNotify($type, $method) + { + return $this->preference(static::notificationPreferenceKey($type, $method)); + } + public function preference($key, $default = null) { return array_get($this->preferences, $key, $default); diff --git a/src/Core/Notifications/NotificationServiceProvider.php b/src/Core/Notifications/NotificationServiceProvider.php new file mode 100644 index 000000000..d4417ec7c --- /dev/null +++ b/src/Core/Notifications/NotificationServiceProvider.php @@ -0,0 +1,35 @@ +registerMethod('alert', 'Flarum\Core\Notifications\Senders\NotificationAlerter'); + $notifier->registerMethod('email', 'Flarum\Core\Notifications\Senders\NotificationEmailer'); + + $notifier->registerType('Flarum\Core\Notifications\Types\DiscussionRenamedNotification', ['alert' => true]); + + $events->subscribe('Flarum\Core\Handlers\Events\DiscussionRenamedNotifier'); + } + + public function register() + { + $this->app->bind( + 'Flarum\Core\Repositories\NotificationRepositoryInterface', + 'Flarum\Core\Repositories\EloquentNotificationRepository' + ); + + $this->app->singleton('Flarum\Core\Notifications\Notifier'); + } +} diff --git a/src/Core/Notifications/Notifier.php b/src/Core/Notifications/Notifier.php new file mode 100644 index 000000000..c0cc0dbd3 --- /dev/null +++ b/src/Core/Notifications/Notifier.php @@ -0,0 +1,59 @@ +container = $container; + } + + public function send(Notification $notification) + { + foreach ($this->methods as $method => $sender) { + $sender = $this->container->make($sender); + if ($notification->getRecipient()->shouldNotify($notification::getType(), $method) && $sender->compatibleWith($notification)) { + $sender->send($notification); + } + } + } + + public function registerMethod($name, $class) + { + $this->methods[$name] = $class; + } + + public function registerType($class, $defaultPreferences = []) + { + $this->types[] = $class; + + NotificationModel::registerType($class); + + foreach ($this->methods as $method => $sender) { + $sender = $this->container->make($sender); + if ($sender->compatibleWith($class)) { + User::registerPreference(User::notificationPreferenceKey($class::getType(), $method), 'boolval', array_get($defaultPreferences, $method, false)); + } + } + } + + public function getMethods() + { + return $this->methods; + } + + public function getTypes() + { + return $this->types; + } +} diff --git a/src/Core/Notifications/Senders/NotificationAlerter.php b/src/Core/Notifications/Senders/NotificationAlerter.php new file mode 100644 index 000000000..580950570 --- /dev/null +++ b/src/Core/Notifications/Senders/NotificationAlerter.php @@ -0,0 +1,25 @@ +getRecipient()->id, + $notification::getType(), + $notification->getSender()->id, + $notification->getSubject()->id, + $notification->getAlertData() + ); + $model->save(); + } + + public function compatibleWith($className) + { + return (new ReflectionClass($className))->implementsInterface('Flarum\Core\Notifications\Types\AlertableNotification'); + } +} diff --git a/src/Core/Notifications/Senders/NotificationEmailer.php b/src/Core/Notifications/Senders/NotificationEmailer.php new file mode 100644 index 000000000..2ef68693a --- /dev/null +++ b/src/Core/Notifications/Senders/NotificationEmailer.php @@ -0,0 +1,29 @@ +mailer = $mailer; + $this->forum = $forum; + } + + public function send(Notification $notification) + { + $this->mailer->send($notification->getEmailView(), ['notification' => $notification], function ($message) use ($notification) { + $recipient = $notification->getRecipient(); + $message->to($recipient->email, $recipient->username) + ->subject('['.$this->forum->title.'] '.$notification->getEmailSubject()); + }); + } + + public function compatibleWith($class) + { + return (new ReflectionClass($class))->implementsInterface('Flarum\Core\Notifications\Types\EmailableNotification'); + } +} diff --git a/src/Core/Notifications/Senders/NotificationSender.php b/src/Core/Notifications/Senders/NotificationSender.php new file mode 100644 index 000000000..892af9d9e --- /dev/null +++ b/src/Core/Notifications/Senders/NotificationSender.php @@ -0,0 +1,10 @@ +post = $post; + $this->oldTitle = $oldTitle; + + parent::__construct($recipient, $sender); + } + + public function getSubject() + { + return $this->post->discussion; + } + + public function getAlertData() + { + return [ + 'number' => $this->post->number, + 'oldTitle' => $this->oldTitle + ]; + } + + public static function getType() + { + return 'discussionRenamed'; + } + + public static function getSubjectModel() + { + return 'Flarum\Core\Models\Discussion'; + } +} diff --git a/src/Core/Notifications/Types/EmailableNotification.php b/src/Core/Notifications/Types/EmailableNotification.php new file mode 100644 index 000000000..17691c24d --- /dev/null +++ b/src/Core/Notifications/Types/EmailableNotification.php @@ -0,0 +1,8 @@ +recipient = $recipient; + $this->sender = $sender; + } + + public function getRecipient() + { + return $this->recipient; + } + + public function getSender() + { + return $this->sender; + } + + public static function getType() + { + return null; + } + + public static function getSubjectModel() + { + return null; + } +}