diff --git a/framework/core/js/forum/src/components/join-activity.js b/framework/core/js/forum/src/components/joined-activity.js similarity index 88% rename from framework/core/js/forum/src/components/join-activity.js rename to framework/core/js/forum/src/components/joined-activity.js index bce3ee5bf..d5bef94cf 100644 --- a/framework/core/js/forum/src/components/join-activity.js +++ b/framework/core/js/forum/src/components/joined-activity.js @@ -2,7 +2,7 @@ import Component from 'flarum/component'; import humanTime from 'flarum/helpers/human-time'; import avatar from 'flarum/helpers/avatar'; -export default class JoinActivity extends Component { +export default class JoinedActivity extends Component { view() { var activity = this.props.activity; var user = activity.user(); diff --git a/framework/core/js/forum/src/components/post-activity.js b/framework/core/js/forum/src/components/posted-activity.js similarity index 80% rename from framework/core/js/forum/src/components/post-activity.js rename to framework/core/js/forum/src/components/posted-activity.js index 8cbf66263..9b62d620a 100644 --- a/framework/core/js/forum/src/components/post-activity.js +++ b/framework/core/js/forum/src/components/posted-activity.js @@ -4,11 +4,11 @@ import avatar from 'flarum/helpers/avatar'; import listItems from 'flarum/helpers/list-items'; import ItemList from 'flarum/utils/item-list'; -export default class PostActivity extends Component { +export default class PostedActivity extends Component { view() { var activity = this.props.activity; var user = activity.user(); - var post = activity.post(); + var post = activity.subject(); var discussion = post.discussion(); return m('div', [ @@ -23,7 +23,7 @@ export default class PostActivity extends Component { near: post.number() }), config: m.route}, [ m('ul.list-inline', listItems(this.headerItems().toArray())), - m('div.body', m.trust(post.contentHtml())) + m('div.body', m.trust(post.excerpt())) ]) ]); } @@ -31,7 +31,7 @@ export default class PostActivity extends Component { headerItems() { var items = new ItemList(); - items.add('title', m('h3.title', this.props.activity.post().discussion().title())); + items.add('title', m('h3.title', this.props.activity.subject().discussion().title())); return items; } diff --git a/framework/core/js/forum/src/initializers/components.js b/framework/core/js/forum/src/initializers/components.js index 92cb63ab8..f71a69909 100644 --- a/framework/core/js/forum/src/initializers/components.js +++ b/framework/core/js/forum/src/initializers/components.js @@ -1,21 +1,22 @@ import CommentPost from 'flarum/components/comment-post'; import DiscussionRenamedPost from 'flarum/components/discussion-renamed-post'; -import PostActivity from 'flarum/components/post-activity'; -import JoinActivity from 'flarum/components/join-activity'; +import PostedActivity from 'flarum/components/posted-activity'; +import JoinedActivity from 'flarum/components/joined-activity'; import DiscussionRenamedNotification from 'flarum/components/discussion-renamed-notification'; export default function(app) { app.postComponentRegistry = { - comment: CommentPost, - discussionRenamed: DiscussionRenamedPost + 'comment': CommentPost, + 'discussionRenamed': DiscussionRenamedPost }; app.activityComponentRegistry = { - post: PostActivity, - join: JoinActivity + 'posted': PostedActivity, + 'startedDiscussion': PostedActivity, + 'joined': JoinedActivity }; app.notificationComponentRegistry = { - discussionRenamed: DiscussionRenamedNotification + 'discussionRenamed': DiscussionRenamedNotification }; } diff --git a/framework/core/js/forum/src/initializers/routes.js b/framework/core/js/forum/src/initializers/routes.js index 97c8aa031..0ad18b168 100644 --- a/framework/core/js/forum/src/initializers/routes.js +++ b/framework/core/js/forum/src/initializers/routes.js @@ -13,8 +13,8 @@ export default function(app) { 'user': ['/u/:username', ActivityPage.component()], 'user.activity': ['/u/:username', ActivityPage.component()], - 'user.discussions': ['/u/:username/discussions', ActivityPage.component({filter: 'discussion'})], - 'user.posts': ['/u/:username/posts', ActivityPage.component({filter: 'post'})], + 'user.discussions': ['/u/:username/discussions', ActivityPage.component({filter: 'startedDiscussion'})], + 'user.posts': ['/u/:username/posts', ActivityPage.component({filter: 'posted'})], 'settings': ['/settings', SettingsPage.component()] }; diff --git a/framework/core/js/lib/models/activity.js b/framework/core/js/lib/models/activity.js index 0d620b3bb..e2ddab39b 100644 --- a/framework/core/js/lib/models/activity.js +++ b/framework/core/js/lib/models/activity.js @@ -8,7 +8,6 @@ Activity.prototype.content = Model.prop('content'); Activity.prototype.time = Model.prop('time', Model.date); Activity.prototype.user = Model.one('user'); -Activity.prototype.sender = Model.one('sender'); -Activity.prototype.post = Model.one('post'); +Activity.prototype.subject = Model.one('subject'); export default Activity; diff --git a/framework/core/migrations/2015_02_24_000000_create_activity_table.php b/framework/core/migrations/2015_02_24_000000_create_activity_table.php index 6b5e96a6d..a0a601641 100644 --- a/framework/core/migrations/2015_02_24_000000_create_activity_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_activity_table.php @@ -14,11 +14,10 @@ class CreateActivityTable extends Migration public function up() { Schema::create('activity', function (Blueprint $table) { - $table->increments('id'); $table->integer('user_id')->unsigned(); - $table->integer('sender_id')->unsigned()->nullable(); $table->string('type', 100); + $table->integer('subject_id')->unsigned()->nullable(); $table->binary('data')->nullable(); $table->dateTime('time'); }); diff --git a/framework/core/src/Api/Actions/Activity/IndexAction.php b/framework/core/src/Api/Actions/Activity/IndexAction.php index 42c89331a..dc9a42a70 100644 --- a/framework/core/src/Api/Actions/Activity/IndexAction.php +++ b/framework/core/src/Api/Actions/Activity/IndexAction.php @@ -32,12 +32,9 @@ class IndexAction extends SerializeCollectionAction * @var array */ public static $include = [ - 'sender' => true, - 'post' => true, - 'post.user' => true, - 'post.discussion' => true, - 'post.discussion.startUser' => true, - 'post.discussion.lastUser' => true + 'subject' => true, + 'subject.user' => true, + 'subject.discussion' => true ]; /** @@ -73,6 +70,7 @@ class IndexAction extends SerializeCollectionAction $user = $this->users->findOrFail($request->get('users'), $actor); - return $this->activity->findByUser($user->id, $actor, $request->limit, $request->offset, $request->get('type')); + return $this->activity->findByUser($user->id, $actor, $request->limit, $request->offset, $request->get('type')) + ->load($request->include); } } diff --git a/framework/core/src/Api/Serializers/ActivitySerializer.php b/framework/core/src/Api/Serializers/ActivitySerializer.php index 2348598e4..4dd10b8c9 100644 --- a/framework/core/src/Api/Serializers/ActivitySerializer.php +++ b/framework/core/src/Api/Serializers/ActivitySerializer.php @@ -9,6 +9,17 @@ class ActivitySerializer extends BaseSerializer */ protected $type = 'activity'; + /** + * A map of activity types (key) to the serializer that should be used to + * output the activity's subject (value). + * + * @var array + */ + public static $subjects = [ + 'posted' => 'Flarum\Api\Serializers\PostBasicSerializer', + 'joined' => 'Flarum\Api\Serializers\UserBasicSerializer' + ]; + /** * Serialize attributes of an Activity model for JSON output. * @@ -18,9 +29,7 @@ class ActivitySerializer extends BaseSerializer protected function attributes($activity) { $attributes = [ - 'id' => ((int) $activity->id) ?: str_random(5), 'contentType' => $activity->type, - 'content' => json_encode($activity->data), 'time' => $activity->time->toRFC3339String() ]; @@ -37,8 +46,10 @@ class ActivitySerializer extends BaseSerializer return $this->hasOne('Flarum\Api\Serializers\UserBasicSerializer'); } - public function post() + public function subject() { - return $this->hasOne('Flarum\Api\Serializers\PostSerializer'); + return $this->hasOne(function ($activity) { + return static::$subjects[$activity->type]; + }); } } diff --git a/framework/core/src/Core/Activity/ActivityAbstract.php b/framework/core/src/Core/Activity/ActivityAbstract.php new file mode 100644 index 000000000..d39e67b42 --- /dev/null +++ b/framework/core/src/Core/Activity/ActivityAbstract.php @@ -0,0 +1,5 @@ +activity = $activity; + } + + /** + * Sync a piece of activity so that it is present for the specified users, + * and not present for anyone else. + * + * @param \Flarum\Core\Activity\ActivityInterface $activity + * @param \Flarum\Core\Models\User[] $users + * @return void + */ + public function sync(ActivityInterface $activity, array $users) + { + Activity::unguard(); + + $attributes = [ + 'type' => $activity::getType(), + 'subject_id' => $activity->getSubject()->id, + 'time' => $activity->getTime() + ]; + + $toDelete = Activity::where($attributes)->get(); + $toInsert = []; + + foreach ($users as $user) { + $existing = $toDelete->where('user_id', $user->id)->first(); + + if ($k = $toDelete->search($existing)) { + $toDelete->pull($k); + } else { + $toInsert[] = $attributes + ['user_id' => $user->id]; + } + } + + if (count($toDelete)) { + Activity::whereIn('id', $toDelete->lists('id'))->delete(); + } + if (count($toInsert)) { + Activity::insert($toInsert); + } + } +} diff --git a/framework/core/src/Core/Activity/JoinedActivity.php b/framework/core/src/Core/Activity/JoinedActivity.php new file mode 100644 index 000000000..38b6bf4ce --- /dev/null +++ b/framework/core/src/Core/Activity/JoinedActivity.php @@ -0,0 +1,33 @@ +user = $user; + } + + public function getSubject() + { + return $this->user; + } + + public function getTime() + { + return $this->user->join_time; + } + + public static function getType() + { + return 'joined'; + } + + public static function getSubjectModel() + { + return 'Flarum\Core\Models\User'; + } +} diff --git a/framework/core/src/Core/Activity/PostedActivity.php b/framework/core/src/Core/Activity/PostedActivity.php new file mode 100644 index 000000000..8c306c663 --- /dev/null +++ b/framework/core/src/Core/Activity/PostedActivity.php @@ -0,0 +1,33 @@ +post = $post; + } + + public function getSubject() + { + return $this->post; + } + + public function getTime() + { + return $this->post->time; + } + + public static function getType() + { + return 'posted'; + } + + public static function getSubjectModel() + { + return 'Flarum\Core\Models\Post'; + } +} diff --git a/framework/core/src/Core/Activity/StartedDiscussionActivity.php b/framework/core/src/Core/Activity/StartedDiscussionActivity.php new file mode 100644 index 000000000..9b7b3a550 --- /dev/null +++ b/framework/core/src/Core/Activity/StartedDiscussionActivity.php @@ -0,0 +1,9 @@ +subscribe('Flarum\Core\Handlers\Events\DiscussionRenamedNotifier'); + $events->subscribe('Flarum\Core\Handlers\Events\UserActivitySyncer'); + $this->extend( (new NotificationType('Flarum\Core\Notifications\DiscussionRenamedNotification', 'Flarum\Api\Serializers\DiscussionBasicSerializer')) ->enableByDefault('alert'), + + (new ActivityType('Flarum\Core\Activity\PostedActivity', 'Flarum\Api\Serializers\PostBasicSerializer')), + (new ActivityType('Flarum\Core\Activity\StartedDiscussionActivity', 'Flarum\Api\Serializers\PostBasicSerializer')), + + (new ActivityType('Flarum\Core\Activity\JoinedActivity', 'Flarum\Api\Serializers\UserBasicSerializer')) ); } diff --git a/framework/core/src/Core/Handlers/Events/UserActivitySyncer.php b/framework/core/src/Core/Handlers/Events/UserActivitySyncer.php new file mode 100755 index 000000000..369222b90 --- /dev/null +++ b/framework/core/src/Core/Handlers/Events/UserActivitySyncer.php @@ -0,0 +1,76 @@ +activity = $activity; + } + + public function subscribe(Dispatcher $events) + { + $events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted'); + $events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden'); + $events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored'); + $events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted'); + $events->listen('Flarum\Core\Events\UserWasRegistered', __CLASS__.'@whenUserWasRegistered'); + } + + public function whenPostWasPosted(PostWasPosted $event) + { + $this->postBecameVisible($event->post); + } + + public function whenPostWasHidden(PostWasHidden $event) + { + $this->postBecameInvisible($event->post); + } + + public function whenPostWasRestored(PostWasRestored $event) + { + $this->postBecameVisible($event->post); + } + + public function whenPostWasDeleted(PostWasDeleted $event) + { + $this->postBecameInvisible($event->post); + } + + public function whenUserWasRegistered(UserWasRegistered $event) + { + $this->activity->sync(new JoinedActivity($event->user), [$event->user]); + } + + protected function postBecameVisible(Post $post) + { + $activity = $this->postedActivity($post); + + $this->activity->sync($activity, [$post->user]); + } + + protected function postBecameInvisible(Post $post) + { + $activity = $this->postedActivity($post); + + $this->activity->sync($activity, []); + } + + protected function postedActivity(Post $post) + { + return $post->number === 1 ? new StartedDiscussionActivity($post) : new PostedActivity($post); + } +} diff --git a/framework/core/src/Core/Models/Activity.php b/framework/core/src/Core/Models/Activity.php index e75860b71..d12c6434e 100644 --- a/framework/core/src/Core/Models/Activity.php +++ b/framework/core/src/Core/Models/Activity.php @@ -16,6 +16,13 @@ class Activity extends Model */ protected $dates = ['time']; + /** + * + * + * @var array + */ + protected static $subjects = []; + /** * Unserialize the data attribute. * @@ -47,23 +54,27 @@ class Activity extends Model return $this->belongsTo('Flarum\Core\Models\User', 'user_id'); } - /** - * Define the relationship with the activity's sender. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function sender() + public function subject() { - return $this->belongsTo('Flarum\Core\Models\User', 'sender_id'); + return $this->mappedMorphTo(static::$subjects, 'subject', 'type', 'subject_id'); + } + + public static function getTypes() + { + return static::$subjects; } /** - * Define the relationship with the activity's sender. + * Register a notification type. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @param string $type + * @param string $class + * @return void */ - public function post() + public static function registerType($class) { - return $this->belongsTo('Flarum\Core\Models\Post', 'post_id'); + if ($subject = $class::getSubjectModel()) { + static::$subjects[$class::getType()] = $subject; + } } } diff --git a/framework/core/src/Core/Notifications/Types/AlertableNotification.php b/framework/core/src/Core/Notifications/Types/AlertableNotification.php deleted file mode 100644 index 25130c430..000000000 --- a/framework/core/src/Core/Notifications/Types/AlertableNotification.php +++ /dev/null @@ -1,32 +0,0 @@ -whereIn('type', array_keys(Activity::getTypes())) + ->orderBy('time', 'desc') + ->skip($offset) + ->take($limit); - $null = \DB::raw('NULL'); - $query = Activity::with('sender')->select('id', 'user_id', 'sender_id', 'type', 'data', 'time', \DB::raw('NULL as post_id'))->where('user_id', $userId); - - if ($type) { + if ($type !== null) { $query->where('type', $type); } - $posts = Post::whereCan($viewer, 'view')->with('post', 'post.discussion', 'post.user', 'post.discussion.startUser', 'post.discussion.lastUser')->select(\DB::raw("CONCAT('post', id)"), 'user_id', $null, \DB::raw("'post'"), $null, 'time', 'id')->where('user_id', $userId)->where('type', 'comment')->whereNull('hide_time'); - - if ($type === 'post') { - $posts->where('number', '>', 1); - } elseif ($type === 'discussion') { - $posts->where('number', 1); - } - - if (!$type) { - $join = User::select(\DB::raw("CONCAT('join', id)"), 'id', 'id', \DB::raw("'join'"), $null, 'join_time', $null)->where('id', $userId); - $query->union($join->getQuery()); - } - - return $query->union($posts->getQuery()) - ->orderBy('time', 'desc') - ->skip($start) - ->take($count) - ->get(); + return $query->get(); } } diff --git a/framework/core/src/Extend/ActivityType.php b/framework/core/src/Extend/ActivityType.php new file mode 100644 index 000000000..92a86c079 --- /dev/null +++ b/framework/core/src/Extend/ActivityType.php @@ -0,0 +1,27 @@ +class = $class; + $this->serializer = $serializer; + } + + public function extend(Application $app) + { + $class = $this->class; + + Activity::registerType($class); + + ActivitySerializer::$subjects[$class::getType()] = $this->serializer; + } +}