mirror of
https://github.com/flarum/framework.git
synced 2024-11-23 22:44:54 +08:00
Implement notifications
This commit is contained in:
parent
1d1025dcd2
commit
4a1550215c
12
ember/app/components/application/notification-item.js
Normal file
12
ember/app/components/application/notification-item.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
import FadeIn from 'flarum/mixins/fade-in';
|
||||
|
||||
export default Ember.Component.extend(FadeIn, {
|
||||
layoutName: 'components/application/notification-item',
|
||||
tagName: 'li',
|
||||
|
||||
componentName: Ember.computed('notification.contentType', function() {
|
||||
return 'application/notification-'+this.get('notification.contentType');
|
||||
})
|
||||
});
|
3
ember/app/components/application/notification-renamed.js
Normal file
3
ember/app/components/application/notification-renamed.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Notification from './notification';
|
||||
|
||||
export default Notification.extend();
|
11
ember/app/components/application/notification.js
Normal file
11
ember/app/components/application/notification.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['notification'],
|
||||
classNameBindings: ['notification.isRead::unread'],
|
||||
|
||||
click: function() {
|
||||
console.log('click')
|
||||
this.get('notification').set('isRead', true).save();
|
||||
}
|
||||
});
|
39
ember/app/components/application/user-notifications.js
Normal file
39
ember/app/components/application/user-notifications.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
import DropdownButton from 'flarum/components/ui/dropdown-button';
|
||||
|
||||
var precompileTemplate = Ember.Handlebars.compile;
|
||||
|
||||
export default DropdownButton.extend({
|
||||
layoutName: 'components/application/user-notifications',
|
||||
classNames: ['notifications'],
|
||||
classNameBindings: ['unread'],
|
||||
|
||||
buttonClass: 'btn btn-default btn-rounded btn-naked btn-icon',
|
||||
menuClass: 'pull-right',
|
||||
|
||||
unread: Ember.computed.bool('user.unreadNotificationsCount'),
|
||||
|
||||
actions: {
|
||||
buttonClick: function() {
|
||||
if (!this.get('notifications')) {
|
||||
var component = this;
|
||||
this.set('notificationsLoading', true);
|
||||
this.get('parentController.store').find('notification').then(function(notifications) {
|
||||
component.set('user.unreadNotificationsCount', 0);
|
||||
component.set('notifications', notifications);
|
||||
component.set('notificationsLoading', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
markAllAsRead: function() {
|
||||
this.get('notifications').forEach(function(notification) {
|
||||
if (!notification.get('isRead')) {
|
||||
notification.set('isRead', true);
|
||||
notification.save();
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
|
@ -2,8 +2,9 @@ import Ember from 'ember';
|
|||
import DS from 'ember-data';
|
||||
|
||||
import HasItemLists from 'flarum/mixins/has-item-lists';
|
||||
import Subject from './subject';
|
||||
|
||||
export default DS.Model.extend(HasItemLists, {
|
||||
export default Subject.extend(HasItemLists, {
|
||||
/**
|
||||
Define a "badges" item list. Example usage:
|
||||
```
|
||||
|
|
21
ember/app/models/notification.js
Normal file
21
ember/app/models/notification.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({
|
||||
contentType: DS.attr('string'),
|
||||
subjectId: DS.attr('number'),
|
||||
content: DS.attr(),
|
||||
time: DS.attr('date'),
|
||||
isRead: DS.attr('boolean'),
|
||||
unreadCount: DS.attr('number'),
|
||||
additionalUnreadCount: Ember.computed('unreadCount', function() {
|
||||
return Math.max(0, this.get('unreadCount') - 1);
|
||||
}),
|
||||
|
||||
decodedContent: Ember.computed('content', function() {
|
||||
return JSON.parse(this.get('content'));
|
||||
}),
|
||||
|
||||
user: DS.belongsTo('user'),
|
||||
sender: DS.belongsTo('user'),
|
||||
subject: DS.belongsTo('subject', {polymorphic: true})
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
import Subject from './subject';
|
||||
|
||||
export default DS.Model.extend({
|
||||
export default Subject.extend({
|
||||
discussion: DS.belongsTo('discussion', {inverse: 'loadedPosts'}),
|
||||
number: DS.attr('number'),
|
||||
|
||||
|
|
5
ember/app/models/subject.js
Normal file
5
ember/app/models/subject.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({
|
||||
notification: DS.belongsTo('notification')
|
||||
});
|
|
@ -18,6 +18,7 @@ export default DS.Model.extend(HasItemLists, {
|
|||
joinTime: DS.attr('date'),
|
||||
lastSeenTime: DS.attr('date'),
|
||||
readTime: DS.attr('date'),
|
||||
unreadNotificationsCount: DS.attr('number'),
|
||||
|
||||
discussionsCount: DS.attr('number'),
|
||||
commentsCount: DS.attr('number'),
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
@import "@{flarum-base}modals.less";
|
||||
@import "@{flarum-base}layout.less";
|
||||
@import "@{flarum-base}composer.less";
|
||||
@import "@{flarum-base}notifications.less";
|
||||
|
||||
@import "@{flarum-base}index.less";
|
||||
@import "@{flarum-base}discussion.less";
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
padding: 8px 0;
|
||||
margin-top: 7px;
|
||||
background: @fl-body-bg;
|
||||
color: @fl-body-color;
|
||||
.box-shadow(0 2px 6px @fl-shadow-color);
|
||||
|
||||
& > li > a {
|
||||
|
|
|
@ -154,7 +154,7 @@ body {
|
|||
color: @fl-drawer-color;
|
||||
}
|
||||
}
|
||||
&, & a, & .btn-link {
|
||||
&:not(.dropdown-menu), & a:not(.dropdown-menu a), & .btn-link:not(.dropdown-menu .btn-link) {
|
||||
color: @fl-drawer-control-color;
|
||||
}
|
||||
& .form-control {
|
||||
|
@ -207,6 +207,10 @@ body {
|
|||
visibility: visible;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
& .dropdown-menu {
|
||||
width: @drawer-width !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,6 +251,11 @@ body {
|
|||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
& .dropdown-menu {
|
||||
& .btn-group, & .btn {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +288,7 @@ body {
|
|||
.header-controls {
|
||||
&, & > li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.header-primary {
|
||||
|
|
114
ember/app/styles/flarum/notifications.less
Normal file
114
ember/app/styles/flarum/notifications.less
Normal file
|
@ -0,0 +1,114 @@
|
|||
.notifications {
|
||||
& .dropdown-menu {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
& .loading-indicator {
|
||||
height: 100px;
|
||||
}
|
||||
& .dropdown-toggle .label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
@media @tablet, @desktop, @desktop-hd {
|
||||
.notifications {
|
||||
& .dropdown-menu {
|
||||
width: 400px;
|
||||
}
|
||||
& .dropdown-toggle .label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-icon {
|
||||
display: inline-block;
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
padding: 2px 0;
|
||||
font-weight: bold;
|
||||
margin: -2px -3px;
|
||||
}
|
||||
&.unread .notifications-icon {
|
||||
background: #e7562e;
|
||||
color: #fff;
|
||||
}
|
||||
.notifications-header {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid @fl-body-secondary-color;
|
||||
|
||||
& h4 {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
& .btn {
|
||||
float: right;
|
||||
margin-top: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
.notifications-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-notifications {
|
||||
color: @fl-body-muted-color;
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.notification {
|
||||
& > a {
|
||||
display: block;
|
||||
padding: 15px 15px 15px 75px;
|
||||
color: @fl-body-muted-color;
|
||||
overflow: hidden;
|
||||
|
||||
.unread& {
|
||||
background: @fl-body-secondary-color;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: @fl-body-secondary-color;
|
||||
}
|
||||
}
|
||||
& .avatar {
|
||||
float: left;
|
||||
margin-left: -60px;
|
||||
}
|
||||
}
|
||||
.notification-title {
|
||||
color: @fl-body-heading-color;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 6px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.notification-info {
|
||||
font-size: 12px;
|
||||
|
||||
& .fa {
|
||||
font-size: 14px;
|
||||
}
|
||||
& .username {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@media @phone {
|
||||
.notification {
|
||||
& > a {
|
||||
padding-left: 60px;
|
||||
}
|
||||
& .avatar {
|
||||
margin-left: -45px;
|
||||
.avatar-size(32px);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{{component componentName notification=notification}}
|
|
@ -0,0 +1,14 @@
|
|||
{{#link-to "discussion" notification.subject (query-params start=notification.content.number)}}
|
||||
{{user-avatar notification.sender}}
|
||||
|
||||
<h3 class="notification-title">{{notification.content.oldTitle}}</h3>
|
||||
|
||||
<div class="notification-info">
|
||||
{{fa-icon "pencil"}}
|
||||
Renamed by {{user-name notification.sender}}
|
||||
{{#if notification.additionalUnreadCount}}
|
||||
and {{notification.additionalUnreadCount}} others
|
||||
{{/if}}
|
||||
{{human-time notification.time}}
|
||||
</div>
|
||||
{{/link-to}}
|
|
@ -0,0 +1,26 @@
|
|||
<a href="#" {{bind-attr class=":dropdown-toggle buttonClass"}} data-toggle="dropdown" {{action "buttonClick"}}>
|
||||
<span class="notifications-icon">
|
||||
{{#if unread}}
|
||||
{{user.unreadNotificationsCount}}
|
||||
{{else}}
|
||||
{{fa-icon "bell" class="icon-glyph"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="label">Notifications</span>
|
||||
</a>
|
||||
<div class="{{dropdownMenuClass}}">
|
||||
<div class="notifications-header">
|
||||
{{ui/action-button class="btn btn-icon btn-link btn-sm" icon="check" title="Mark All as Read" action="markAllAsRead"}}
|
||||
<h4>Notifications</h4>
|
||||
</div>
|
||||
<ul class="notifications-list">
|
||||
{{#each notifications as |notification|}}
|
||||
{{application/notification-item notification=notification}}
|
||||
{{else unless notificationsLoading}}
|
||||
<li class="no-notifications">No Notifications</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if notificationsLoading}}
|
||||
{{ui/loading-indicator}}
|
||||
{{/if}}
|
||||
</div>
|
|
@ -2,6 +2,7 @@ import Ember from 'ember';
|
|||
|
||||
import HasItemLists from 'flarum/mixins/has-item-lists';
|
||||
import SearchInput from 'flarum/components/ui/search-input';
|
||||
import UserNotifications from 'flarum/components/application/user-notifications';
|
||||
import UserDropdown from 'flarum/components/application/user-dropdown';
|
||||
import ForumStatistic from 'flarum/components/application/forum-statistic';
|
||||
import PoweredBy from 'flarum/components/application/powered-by';
|
||||
|
@ -88,6 +89,11 @@ export default Ember.View.extend(HasItemLists, {
|
|||
}), 'search');
|
||||
|
||||
if (this.get('controller.session.isAuthenticated')) {
|
||||
items.pushObjectWithTag(UserNotifications.extend({
|
||||
user: this.get('controller.session.user'),
|
||||
parentController: controller
|
||||
}), 'notifications');
|
||||
|
||||
items.pushObjectWithTag(UserDropdown.extend({
|
||||
user: this.get('controller.session.user'),
|
||||
parentController: controller
|
||||
|
|
39
migrations/2015_02_24_000000_create_notifications_table.php
Normal file
39
migrations/2015_02_24_000000_create_notifications_table.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateNotificationsTable extends Migration {
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('notifications', function(Blueprint $table)
|
||||
{
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('sender_id')->unsigned()->nullable();
|
||||
$table->string('type');
|
||||
$table->string('subject_type')->nullable();
|
||||
$table->integer('subject_id')->unsigned()->nullable();
|
||||
$table->binary('data')->nullable();
|
||||
$table->dateTime('time');
|
||||
$table->boolean('is_read')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('notifications');
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,7 @@ class CreateUsersTable extends Migration {
|
|||
$table->dateTime('join_time')->nullable();
|
||||
$table->dateTime('last_seen_time')->nullable();
|
||||
$table->dateTime('read_time')->nullable();
|
||||
$table->dateTime('notification_read_time')->nullable();
|
||||
$table->integer('discussions_count')->unsigned()->default(0);
|
||||
$table->integer('comments_count')->unsigned()->default(0);
|
||||
});
|
||||
|
|
50
src/Api/Actions/Notifications/IndexAction.php
Normal file
50
src/Api/Actions/Notifications/IndexAction.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php namespace Flarum\Api\Actions\Notifications;
|
||||
|
||||
use Flarum\Core\Repositories\NotificationRepositoryInterface;
|
||||
use Flarum\Core\Support\Actor;
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Flarum\Api\Actions\BaseAction;
|
||||
use Flarum\Api\Actions\ApiParams;
|
||||
use Flarum\Api\Serializers\NotificationSerializer;
|
||||
|
||||
class IndexAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Instantiate the action.
|
||||
*
|
||||
* @param \Flarum\Core\Search\Discussions\UserSearcher $searcher
|
||||
*/
|
||||
public function __construct(Actor $actor, NotificationRepositoryInterface $notifications)
|
||||
{
|
||||
$this->actor = $actor;
|
||||
$this->notifications = $notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a user's notifications feed.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function run(ApiParams $params)
|
||||
{
|
||||
$start = $params->start();
|
||||
$count = $params->count(10, 50);
|
||||
|
||||
if (! $this->actor->isAuthenticated()) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
$user = $this->actor->getUser();
|
||||
|
||||
$notifications = $this->notifications->findByUser($user->id, $count, $start);
|
||||
|
||||
$user->markNotificationsAsRead()->save();
|
||||
|
||||
// Finally, we can set up the notification serializer and use it to create
|
||||
// a collection of notification results.
|
||||
$serializer = new NotificationSerializer(['sender', 'subject', 'subject.discussion']);
|
||||
$document = $this->document()->setData($serializer->collection($notifications));
|
||||
|
||||
return $this->respondWithDocument($document);
|
||||
}
|
||||
}
|
34
src/Api/Actions/Notifications/UpdateAction.php
Normal file
34
src/Api/Actions/Notifications/UpdateAction.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php namespace Flarum\Api\Actions\Notifications;
|
||||
|
||||
use Flarum\Core\Commands\ReadNotificationCommand;
|
||||
use Flarum\Api\Actions\BaseAction;
|
||||
use Flarum\Api\Actions\ApiParams;
|
||||
use Flarum\Api\Serializers\NotificationSerializer;
|
||||
|
||||
class UpdateAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Edit a discussion. Allows renaming the discussion, and updating its read
|
||||
* state with regards to the current user.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function run(ApiParams $params)
|
||||
{
|
||||
$notificationId = $params->get('id');
|
||||
$user = $this->actor->getUser();
|
||||
|
||||
// if ($params->get('notifications.isRead')) {
|
||||
$command = new ReadNotificationCommand($notificationId, $user);
|
||||
$notification = $this->dispatch($command, $params);
|
||||
// }
|
||||
|
||||
// Presumably, the discussion was updated successfully. (One of the command
|
||||
// handlers would have thrown an exception if not.) We set this
|
||||
// discussion as our document's primary element.
|
||||
$serializer = new NotificationSerializer;
|
||||
$document = $this->document()->setData($serializer->resource($notification));
|
||||
|
||||
return $this->respondWithDocument($document);
|
||||
}
|
||||
}
|
48
src/Api/Serializers/NotificationSerializer.php
Normal file
48
src/Api/Serializers/NotificationSerializer.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php namespace Flarum\Api\Serializers;
|
||||
|
||||
class NotificationSerializer extends BaseSerializer
|
||||
{
|
||||
/**
|
||||
* The resource type.
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'notifications';
|
||||
|
||||
/**
|
||||
* Serialize attributes of an notification model for JSON output.
|
||||
*
|
||||
* @param Notification $notification The notification model to serialize.
|
||||
* @return array
|
||||
*/
|
||||
protected function attributes($notification)
|
||||
{
|
||||
$attributes = [
|
||||
'id' => (int) $notification->id,
|
||||
'contentType' => $notification->type,
|
||||
'content' => $notification->data,
|
||||
'time' => $notification->time->toRFC3339String(),
|
||||
'isRead' => (bool) $notification->is_read,
|
||||
'unreadCount' => $notification->unread_count
|
||||
];
|
||||
|
||||
return $this->extendAttributes($notification, $attributes);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->hasOne('Flarum\Api\Serializers\UserBasicSerializer');
|
||||
}
|
||||
|
||||
public function sender()
|
||||
{
|
||||
return $this->hasOne('Flarum\Api\Serializers\UserBasicSerializer');
|
||||
}
|
||||
|
||||
public function subject()
|
||||
{
|
||||
return $this->hasOne([
|
||||
'Flarum\Core\Models\Discussion' => 'Flarum\Api\Serializers\DiscussionSerializer',
|
||||
'Flarum\Core\Models\CommentPost' => 'Flarum\Api\Serializers\PostSerializer'
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,13 @@ $action = function ($class) {
|
|||
|
||||
Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWithHeader'], function () use ($action) {
|
||||
|
||||
// Get forum information
|
||||
Route::get('forum', [
|
||||
'as' => 'flarum.api.forum.show',
|
||||
'uses' => $action('Flarum\Api\Actions\Forum\ShowAction')
|
||||
]);
|
||||
|
||||
// Retrieve authentication token
|
||||
Route::post('token', [
|
||||
'as' => 'flarum.api.token',
|
||||
'uses' => $action('Flarum\Api\Actions\TokenAction')
|
||||
|
@ -70,6 +77,12 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit
|
|||
'uses' => $action('Flarum\Api\Actions\Notifications\IndexAction')
|
||||
]);
|
||||
|
||||
// Mark a single notification as read
|
||||
Route::put('notifications/{id}', [
|
||||
'as' => 'flarum.api.notifications.update',
|
||||
'uses' => $action('Flarum\Api\Actions\Notifications\UpdateAction')
|
||||
]);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Discussions
|
||||
|
|
14
src/Core/Commands/ReadNotificationCommand.php
Normal file
14
src/Core/Commands/ReadNotificationCommand.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php namespace Flarum\Core\Commands;
|
||||
|
||||
class ReadNotificationCommand
|
||||
{
|
||||
public $notificationId;
|
||||
|
||||
public $user;
|
||||
|
||||
public function __construct($notificationId, $user)
|
||||
{
|
||||
$this->notificationId = $notificationId;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ 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;
|
||||
|
||||
class CoreServiceProvider extends ServiceProvider
|
||||
|
@ -25,6 +26,7 @@ class CoreServiceProvider extends ServiceProvider
|
|||
|
||||
$this->registerEventHandlers($events);
|
||||
$this->registerPostTypes();
|
||||
$this->registerNotificationTypes();
|
||||
$this->registerPermissions();
|
||||
$this->registerGambits();
|
||||
$this->setupModels();
|
||||
|
@ -112,11 +114,16 @@ 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\RenamedPostCreator');
|
||||
$events->subscribe('Flarum\Core\Handlers\Events\DiscussionRenamedNotifier');
|
||||
$events->subscribe('Flarum\Core\Handlers\Events\EmailConfirmationMailer');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php namespace Flarum\Core\Handlers\Commands;
|
||||
|
||||
use Flarum\Core\Models\Notification;
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Flarum\Core\Support\DispatchesEvents;
|
||||
|
||||
class ReadNotificationCommandHandler
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
public function handle($command)
|
||||
{
|
||||
$user = $command->user;
|
||||
|
||||
if (! $user->exists) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
$notification = Notification::where('user_id', $user->id)->findOrFail($command->notificationId);
|
||||
|
||||
$notification->read();
|
||||
|
||||
$notification->save();
|
||||
$this->dispatchEventsFor($notification);
|
||||
|
||||
return $notification;
|
||||
}
|
||||
}
|
61
src/Core/Handlers/Events/DiscussionRenamedNotifier.php
Executable file
61
src/Core/Handlers/Events/DiscussionRenamedNotifier.php
Executable file
|
@ -0,0 +1,61 @@
|
|||
<?php namespace Flarum\Core\Handlers\Events;
|
||||
|
||||
use Flarum\Core\Events\DiscussionWasRenamed;
|
||||
use Flarum\Core\Models\RenamedPost;
|
||||
use Flarum\Core\Models\Notification;
|
||||
|
||||
class DiscussionRenamedNotifier
|
||||
{
|
||||
/**
|
||||
* Register the listeners for the subscriber.
|
||||
*
|
||||
* @param Illuminate\Events\Dispatcher $events
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen('Flarum\Core\Events\DiscussionWasRenamed', __CLASS__.'@whenDiscussionWasRenamed');
|
||||
}
|
||||
|
||||
public function whenDiscussionWasRenamed(DiscussionWasRenamed $event)
|
||||
{
|
||||
$post = $this->createRenamedPost($event);
|
||||
|
||||
$event->discussion->postWasAdded($post);
|
||||
|
||||
$this->createRenamedNotification($event, $post);
|
||||
}
|
||||
|
||||
protected function createRenamedPost(DiscussionWasRenamed $event)
|
||||
{
|
||||
$post = RenamedPost::reply(
|
||||
$event->discussion->id,
|
||||
$event->user->id,
|
||||
$event->oldTitle,
|
||||
$event->discussion->title
|
||||
);
|
||||
|
||||
$post->save();
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
protected function createRenamedNotification(DiscussionWasRenamed $event, RenamedPost $post)
|
||||
{
|
||||
if ($event->discussion->start_user_id === $event->user->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$notification = Notification::notify(
|
||||
$event->discussion->start_user_id,
|
||||
'renamed',
|
||||
$event->user->id,
|
||||
$event->discussion->id,
|
||||
['number' => $post->number, 'oldTitle' => $event->oldTitle]
|
||||
);
|
||||
|
||||
$notification->save();
|
||||
|
||||
return $notification;
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php namespace Flarum\Core\Handlers\Events;
|
||||
|
||||
use Flarum\Core\Events\DiscussionWasRenamed;
|
||||
use Flarum\Core\Models\RenamedPost;
|
||||
|
||||
class RenamedPostCreator
|
||||
{
|
||||
/**
|
||||
* Register the listeners for the subscriber.
|
||||
*
|
||||
* @param Illuminate\Events\Dispatcher $events
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe($events)
|
||||
{
|
||||
$events->listen('Flarum\Core\Events\DiscussionWasRenamed', __CLASS__.'@whenDiscussionWasRenamed');
|
||||
}
|
||||
|
||||
public function whenDiscussionWasRenamed(DiscussionWasRenamed $event)
|
||||
{
|
||||
$post = RenamedPost::reply(
|
||||
$event->discussion->id,
|
||||
$event->user->id,
|
||||
$event->oldTitle,
|
||||
$event->discussion->title
|
||||
);
|
||||
|
||||
$post->save();
|
||||
|
||||
$event->discussion->postWasAdded($post);
|
||||
}
|
||||
}
|
135
src/Core/Models/Notification.php
Normal file
135
src/Core/Models/Notification.php
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
use Flarum\Core\Support\MappedMorphTo;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'notifications';
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['time'];
|
||||
|
||||
/**
|
||||
* A map of notification types, as specified in the `type` column, to
|
||||
* their subject classes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $types = [];
|
||||
|
||||
public static function notify($userId, $type, $senderId, $subjectId, $data)
|
||||
{
|
||||
$notification = new static;
|
||||
|
||||
$notification->user_id = $userId;
|
||||
$notification->sender_id = $senderId;
|
||||
$notification->type = $type;
|
||||
$notification->subject_id = $subjectId;
|
||||
$notification->data = $data;
|
||||
$notification->time = time();
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
public function read()
|
||||
{
|
||||
$this->is_read = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize the data attribute.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function getDataAttribute($value)
|
||||
{
|
||||
return json_decode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the data attribute.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function setDataAttribute($value)
|
||||
{
|
||||
$this->attributes['data'] = json_encode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the notification's recipient.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('Flarum\Core\Models\User', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the notification's sender.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function sender()
|
||||
{
|
||||
return $this->belongsTo('Flarum\Core\Models\User', 'sender_id');
|
||||
}
|
||||
|
||||
public function subject()
|
||||
{
|
||||
$name = 'subject';
|
||||
$typeColumn = 'type';
|
||||
$idColumn = 'subject_id';
|
||||
|
||||
// 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))
|
||||
{
|
||||
return new MappedMorphTo(
|
||||
$this->newQuery(), $this, $idColumn, null, $typeColumn, $name, static::$types
|
||||
);
|
||||
}
|
||||
|
||||
// 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];
|
||||
$instance = new $class;
|
||||
|
||||
return new MappedMorphTo(
|
||||
$instance->newQuery(), $this, $idColumn, $instance->getKeyName(), $typeColumn, $name, static::$types
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTypes()
|
||||
{
|
||||
return static::$types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a notification type and its subject class.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $class
|
||||
* @return void
|
||||
*/
|
||||
public static function addType($type, $class)
|
||||
{
|
||||
static::$types[$type] = $class;
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ class User extends Model
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['join_time', 'last_seen_time', 'read_time'];
|
||||
protected $dates = ['join_time', 'last_seen_time', 'read_time', 'notification_read_time'];
|
||||
|
||||
/**
|
||||
* The hasher with which to hash passwords.
|
||||
|
@ -198,6 +198,18 @@ class User extends Model
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read by setting the user's notification_read_time.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function markNotificationsAsRead()
|
||||
{
|
||||
$this->notification_read_time = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given password matches the user's password.
|
||||
*
|
||||
|
@ -303,6 +315,11 @@ class User extends Model
|
|||
return (bool) $count;
|
||||
}
|
||||
|
||||
public function getUnreadNotificationsCount()
|
||||
{
|
||||
return $this->notifications()->where('time', '>', $this->notification_read_time ?: 0)->where('is_read', 0)->count(\DB::raw('DISTINCT type, subject_id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the user is an administrator.
|
||||
*
|
||||
|
@ -343,6 +360,16 @@ class User extends Model
|
|||
return $this->belongsToMany('Flarum\Core\Models\Group', 'users_groups');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the user's notifications.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function notifications()
|
||||
{
|
||||
return $this->hasMany('Flarum\Core\Models\Notification');
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the user's permissions.
|
||||
*
|
||||
|
|
25
src/Core/Repositories/EloquentNotificationRepository.php
Normal file
25
src/Core/Repositories/EloquentNotificationRepository.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php namespace Flarum\Core\Repositories;
|
||||
|
||||
use Flarum\Core\Models\Notification;
|
||||
use DB;
|
||||
|
||||
class EloquentNotificationRepository implements NotificationRepositoryInterface
|
||||
{
|
||||
public function findByUser($userId, $count = null, $start = 0)
|
||||
{
|
||||
$primaries = Notification::select(DB::raw('MAX(id) AS id'), DB::raw('SUM(is_read = 0) AS unread_count'))
|
||||
->where('user_id', $userId)
|
||||
->whereIn('type', array_keys(Notification::getTypes()))
|
||||
->groupBy('type', 'subject_id')
|
||||
->orderBy('time', 'desc')
|
||||
->skip($start)
|
||||
->take($count);
|
||||
|
||||
return Notification::with('subject')
|
||||
->select('notifications.*', 'p.unread_count')
|
||||
->mergeBindings($primaries->getQuery())
|
||||
->join(DB::raw('('.$primaries->toSql().') p'), 'notifications.id', '=', 'p.id')
|
||||
->orderBy('time', 'desc')
|
||||
->get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php namespace Flarum\Core\Repositories;
|
||||
|
||||
interface NotificationRepositoryInterface
|
||||
{
|
||||
public function findByUser($userId, $count = null, $start = 0);
|
||||
}
|
|
@ -15,4 +15,9 @@ class Actor
|
|||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function isAuthenticated()
|
||||
{
|
||||
return (bool) $this->user;
|
||||
}
|
||||
}
|
||||
|
|
43
src/Core/Support/MappedMorphTo.php
Normal file
43
src/Core/Support/MappedMorphTo.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php namespace Flarum\Core\Support;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MappedMorphTo extends MorphTo {
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $types;
|
||||
|
||||
/**
|
||||
* Create a new morph to relationship instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param string $foreignKey
|
||||
* @param string $otherKey
|
||||
* @param string $type
|
||||
* @param string $relation
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relation, $types)
|
||||
{
|
||||
$this->types = $types;
|
||||
|
||||
parent::__construct($query, $parent, $foreignKey, $otherKey, $type, $relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new model instance by type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function createModelByType($type)
|
||||
{
|
||||
return new $this->types[$type];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user