Radically simplify user activity system

The activity system we were using was built around a separate table.
Whenever the user posted something, or deleted a post, we would sync
the table. The advantage of this was that we could aggregate activity
of all different types very efficiently.

It turns out that it came with a huge disadvantage: there was no
efficient way to enforce permissions on activity. If a user posted
something in a private tag, everyone could still see it on their
activity feed. My stopgap solution was to only sync activity for posts
that are viewable by guests, but that was way too limited.

It also turns out that aggregating activity of different types is
really not that useful, especially considering most of it is the user
making posts. So I've gotten rid of that whole overly-complicated
system, and just made the user profile display separate lists of posts
and discussions, retrieved from those respective APIs. The discussions
page is an actual discussion list too, which is pretty cool.

It's still technically possible to aggregate different activity types
(basically just aggregate API responses together), but we can do that
later if there's a need for it.

This is probably my favourite commit of the day :)
This commit is contained in:
Toby Zerner 2015-08-04 18:44:22 +09:30
parent 4c23a1e274
commit d5296e9aa5
24 changed files with 98 additions and 804 deletions

View File

@ -22,7 +22,7 @@ export default class Activity extends Component {
<div className="Activity-header">
<strong className="Activity-description">{this.description()}</strong>
{humanTime(activity.time())}
{humanTime(this.time())}
</div>
{this.content()}
@ -34,9 +34,18 @@ export default class Activity extends Component {
* Get the user whose avatar should be displayed.
*
* @return {User}
* @abstract
*/
user() {
return this.props.activity.user();
}
/**
* Get the time of the activity.
*
* @return {Date}
* @abstract
*/
time() {
}
/**

View File

@ -1,11 +0,0 @@
import Activity from 'flarum/components/Activity';
/**
* The `JoinedActivity` component displays an activity feed item for when a user
* joined the forum.
*/
export default class JoinedActivity extends Activity {
description() {
return app.trans('core.joined_the_forum');
}
}

View File

@ -12,14 +12,20 @@ import { truncate } from 'flarum/utils/string';
* - All of the props for Activity
*/
export default class PostedActivity extends Activity {
description() {
const post = this.props.activity.subject();
user() {
return this.props.post.user();
}
return app.trans(post.number() === 1 ? 'core.started_a_discussion' : 'core.posted_a_reply');
time() {
return this.props.post.time();
}
description() {
return app.trans(this.props.post.number() === 1 ? 'core.started_a_discussion' : 'core.posted_a_reply');
}
content() {
const post = this.props.activity.subject();
const post = this.props.post;
return (
<a className="Activity-content PostedActivity-preview"
@ -43,7 +49,7 @@ export default class PostedActivity extends Activity {
headerItems() {
const items = new ItemList();
items.add('title', <h3>{this.props.activity.subject().discussion().title()}</h3>);
items.add('title', <h3>{this.props.post.discussion().title()}</h3>);
return items;
}

View File

@ -0,0 +1,26 @@
import UserPage from 'flarum/components/UserPage';
import DiscussionList from 'flarum/components/DiscussionList';
/**
* The `UserDiscussionsPage` component shows a user's activity feed inside of their
* profile.
*/
export default class UserDiscussionsPage extends UserPage {
constructor(...args) {
super(...args);
this.loadUser(m.route.param('username'));
}
content() {
return (
<div className="UserPostsPage">
{DiscussionList.component({
params: {
q: 'author:' + this.user.username()
}
})}
</div>
);
}
}

View File

@ -134,11 +134,11 @@ export default class UserPage extends Component {
const items = new ItemList();
const user = this.user;
items.add('activity',
items.add('posts',
LinkButton.component({
href: app.route('user.activity', {username: user.username()}),
children: app.trans('core.activity'),
icon: 'user'
href: app.route('user.posts', {username: user.username()}),
children: [app.trans('core.posts'), <span className="Button-badge">{user.commentsCount()}</span>],
icon: 'comment-o'
})
);
@ -150,14 +150,6 @@ export default class UserPage extends Component {
})
);
items.add('posts',
LinkButton.component({
href: app.route('user.posts', {username: user.username()}),
children: [app.trans('core.posts'), <span className="Button-badge">{user.commentsCount()}</span>],
icon: 'comment-o'
})
);
if (app.session.user === user) {
items.add('separator', Separator.component());
items.add('settings',

View File

@ -1,12 +1,13 @@
import UserPage from 'flarum/components/UserPage';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Button from 'flarum/components/Button';
import PostedActivity from 'flarum/components/PostedActivity';
/**
* The `ActivityPage` component shows a user's activity feed inside of their
* The `UserPostsPage` component shows a user's activity feed inside of their
* profile.
*/
export default class ActivityPage extends UserPage {
export default class UserPostsPage extends UserPage {
constructor(...args) {
super(...args);
@ -25,10 +26,11 @@ export default class ActivityPage extends UserPage {
this.moreResults = false;
/**
* The Activity models in the feed.
* @type {Activity[]}
* The Post models in the feed.
*
* @type {Post[]}
*/
this.activity = [];
this.posts = [];
/**
* The number of activity items to load per request.
@ -47,7 +49,7 @@ export default class ActivityPage extends UserPage {
footer = LoadingIndicator.component();
} else if (this.moreResults) {
footer = (
<div className="ActivityPage-loadMore">
<div className="UserPostsPage-loadMore">
{Button.component({
children: app.trans('core.load_more'),
className: 'Button',
@ -58,11 +60,10 @@ export default class ActivityPage extends UserPage {
}
return (
<div className="ActivityPage">
<ul className="ActivityPage-list">
{this.activity.map(activity => {
const ActivityComponent = app.activityComponents[activity.contentType()];
return ActivityComponent ? <li>{ActivityComponent.component({activity})}</li> : '';
<div className="UserPostsPage">
<ul className="UserPostsPage-list ActivityList">
{this.posts.map(post => {
return <li>{PostedActivity.component({post})}</li>;
})}
</ul>
{footer}
@ -87,7 +88,7 @@ export default class ActivityPage extends UserPage {
*/
refresh() {
this.loading = true;
this.activity = [];
this.posts = [];
// Redraw, but only if we're not in the middle of a route change.
m.startComputation();
@ -104,12 +105,13 @@ export default class ActivityPage extends UserPage {
* @protected
*/
loadResults(offset) {
return app.store.find('activity', {
return app.store.find('posts', {
filter: {
user: this.user.id(),
type: this.props.filter
type: 'comment'
},
page: {offset, limit: this.loadLimit}
page: {offset, limit: this.loadLimit},
sort: '-time'
});
}
@ -120,19 +122,19 @@ export default class ActivityPage extends UserPage {
*/
loadMore() {
this.loading = true;
this.loadResults(this.activity.length).then(this.parseResults.bind(this));
this.loadResults(this.posts.length).then(this.parseResults.bind(this));
}
/**
* Parse results and append them to the activity feed.
*
* @param {Activity[]} results
* @return {Activity[]}
* @param {Post[]} results
* @return {Post[]}
*/
parseResults(results) {
this.loading = false;
[].push.apply(this.activity, results);
[].push.apply(this.posts, results);
this.moreResults = results.length >= this.loadLimit;
m.redraw();

View File

@ -14,9 +14,5 @@ export default function components(app) {
app.postComponents.comment = CommentPost;
app.postComponents.discussionRenamed = DiscussionRenamedPost;
app.activityComponents.posted = PostedActivity;
app.activityComponents.startedDiscussion = PostedActivity;
app.activityComponents.joined = JoinedActivity;
app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
}

View File

@ -1,6 +1,7 @@
import IndexPage from 'flarum/components/IndexPage';
import DiscussionPage from 'flarum/components/DiscussionPage';
import ActivityPage from 'flarum/components/ActivityPage';
import UserPostsPage from 'flarum/components/UserPostsPage';
import UserDiscussionsPage from 'flarum/components/UserDiscussionsPage';
import SettingsPage from 'flarum/components/SettingsPage';
import NotificationsPage from 'flarum/components/NotificationsPage';
@ -18,10 +19,9 @@ export default function(app) {
'discussion': {path: '/d/:id/:slug', component: DiscussionPage.component()},
'discussion.near': {path: '/d/:id/:slug/:near', component: DiscussionPage.component()},
'user': {path: '/u/:username', component: ActivityPage.component()},
'user.activity': {path: '/u/:username', component: ActivityPage.component()},
'user.discussions': {path: '/u/:username/discussions', component: ActivityPage.component({filter: 'startedDiscussion'})},
'user.posts': {path: '/u/:username/posts', component: ActivityPage.component({filter: 'posted'})},
'user': {path: '/u/:username', component: UserPostsPage.component()},
'user.posts': {path: '/u/:username', component: UserPostsPage.component()},
'user.discussions': {path: '/u/:username/discussions', component: UserDiscussionsPage.component()},
'settings': {path: '/settings', component: SettingsPage.component()},
'notifications': {path: '/notifications', component: NotificationsPage.component()}

View File

@ -1,11 +0,0 @@
import Model from 'flarum/Model';
import mixin from 'flarum/utils/mixin';
export default class Activity extends mixin(Model, {
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
time: Model.attribute('time', Model.transformDate),
user: Model.hasOne('user'),
subject: Model.hasOne('subject')
}) {}

View File

@ -1,11 +1,11 @@
.ActivityPage-loadMore {
.UserPostsPage-loadMore {
text-align: center;
.LoadingIndicator {
height: 46px;
}
}
.ActivityPage-list {
.ActivityList {
border-left: 3px solid @control-bg;
list-style: none;
margin: 0 0 0 16px;

View File

@ -1,94 +0,0 @@
<?php namespace Flarum\Api\Actions\Activity;
use Flarum\Core\Users\UserRepository;
use Flarum\Core\Activity\ActivityRepository;
use Flarum\Api\Actions\SerializeCollectionAction;
use Flarum\Api\JsonApiRequest;
use Tobscure\JsonApi\Document;
class IndexAction extends SerializeCollectionAction
{
/**
* @var UserRepository
*/
protected $users;
/**
* @var ActivityRepository
*/
protected $activity;
/**
* @inheritdoc
*/
public $serializer = 'Flarum\Api\Serializers\ActivitySerializer';
/**
* @inheritdoc
*/
public $include = [
'subject' => true,
'subject.user' => true,
'subject.discussion' => true
];
/**
* @inheritdoc
*/
public $link = ['user'];
/**
* @inheritdoc
*/
public $limitMax = 50;
/**
* @inheritdoc
*/
public $limit = 20;
/**
* @inheritdoc
*/
public $sortFields = [];
/**
* @inheritdoc
*/
public $sort;
/**
* @param UserRepository $users
* @param ActivityRepository $activity
*/
public function __construct(UserRepository $users, ActivityRepository $activity)
{
$this->users = $users;
$this->activity = $activity;
}
/**
* Get the activity results, ready to be serialized and assigned to the
* document response.
*
* @param JsonApiRequest $request
* @param Document $document
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function data(JsonApiRequest $request, Document $document)
{
$userId = $request->get('filter.user');
$actor = $request->actor;
$user = $this->users->findOrFail($userId, $actor);
return $this->activity->findByUser(
$user->id,
$actor,
$request->limit,
$request->offset,
$request->get('filter.type')
)
->load($request->include);
}
}

View File

@ -22,7 +22,7 @@ trait GetsPosts
$offset = $this->posts->getIndexForNumber($where['discussion_id'], $near, $actor);
$offset = max(0, $offset - $request->limit / 2);
} else {
$offset = 0;
$offset = $request->offset;
}
return $this->posts->findWhere(

View File

@ -43,7 +43,7 @@ class IndexAction extends SerializeCollectionAction
/**
* @inheritdoc
*/
public $sortFields = [];
public $sortFields = ['time'];
/**
* @inheritdoc
@ -84,6 +84,9 @@ class IndexAction extends SerializeCollectionAction
if ($userId = $request->get('filter.user')) {
$where['user_id'] = $userId;
}
if ($type = $request->get('filter.type')) {
$where['type'] = $type;
}
$posts = $this->getPosts($request, $where);
}

View File

@ -1,117 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Model;
/**
* Models a user activity record in the database.
*
* Activity records show up in chronological order on a user's activity feed.
* They indicate when the user has done something, like making a new post. They
* can also be used to show any other relevant information on the user's
* activity feed, like if the user has been mentioned in another post.
*
* Each activity record has a *type*. The type determines how the record looks
* in the activity feed, and what *subject* is associated with it. For example,
* the 'posted' activity type represents that a user made a post. Its subject is
* a post, of which the ID is stored in the `subject_id` column.
*
* @todo document database columns with @property
*/
class Activity extends Model
{
/**
* {@inheritdoc}
*/
protected $table = 'activity';
/**
* {@inheritdoc}
*/
protected $dates = ['time'];
/**
* A map of activity types and the model classes to use for their subjects.
* For example, the 'posted' activity type, which represents that a user
* made a post, has the subject model class 'Flarum\Core\Posts\Post'.
*
* @var array
*/
protected static $subjectModels = [];
/**
* When getting the data attribute, unserialize the JSON stored in the
* database into a plain array.
*
* @param string $value
* @return mixed
*/
public function getDataAttribute($value)
{
return json_decode($value, true);
}
/**
* When setting the data attribute, serialize it into JSON for storage in
* the database.
*
* @param mixed $value
*/
public function setDataAttribute($value)
{
$this->attributes['data'] = json_encode($value);
}
/**
* Get the subject model for this activity record by looking up its type in
* our subject model map.
*
* @return string|null
*/
public function getSubjectModelAttribute()
{
return array_get(static::$subjectModels, $this->type);
}
/**
* Define the relationship with the activity's recipient.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
/**
* Define the relationship with the activity's subject.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function subject()
{
return $this->morphTo('subject', 'subjectModel', 'subject_id');
}
/**
* Get the type-to-subject-model map.
*
* @return array
*/
public static function getSubjectModels()
{
return static::$subjectModels;
}
/**
* Set the subject model for the given activity type.
*
* @param string $type The activity type.
* @param string $subjectModel The class name of the subject model for that
* type.
* @return void
*/
public static function setSubjectModel($type, $subjectModel)
{
static::$subjectModels[$type] = $subjectModel;
}
}

View File

@ -1,42 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
class ActivityRepository
{
/**
* Find a user's activity.
*
* @param int $userId
* @param User $actor
* @param null|int $limit
* @param int $offset
* @param null|string $type
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByUser($userId, User $actor, $limit = null, $offset = 0, $type = null)
{
$query = Activity::where('user_id', $userId)
->whereIn('type', $this->getRegisteredTypes())
->latest('time')
->skip($offset)
->take($limit);
if ($type !== null) {
$query->where('type', $type);
}
return $query->get();
}
/**
* Get a list of activity types that have been registered with the activity
* model.
*
* @return array
*/
protected function getRegisteredTypes()
{
return array_keys(Activity::getSubjectModels());
}
}

View File

@ -1,58 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
use Flarum\Events\RegisterActivityTypes;
use Flarum\Support\ServiceProvider;
use Flarum\Extend;
class ActivityServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->registerActivityTypes();
$events = $this->app->make('events');
$events->subscribe('Flarum\Core\Activity\Listeners\UserActivitySyncer');
}
/**
* Register activity types.
*
* @return void
*/
public function registerActivityTypes()
{
$blueprints = [
'Flarum\Core\Activity\PostedBlueprint',
'Flarum\Core\Activity\StartedDiscussionBlueprint',
'Flarum\Core\Activity\JoinedBlueprint'
];
event(new RegisterActivityTypes($blueprints));
foreach ($blueprints as $blueprint) {
Activity::setSubjectModel(
$blueprint::getType(),
$blueprint::getSubjectModel()
);
}
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Flarum\Core\Activity\ActivityRepositoryInterface',
'Flarum\Core\Activity\EloquentActivityRepository'
);
}
}

View File

@ -1,117 +0,0 @@
<?php namespace Flarum\Core\Activity;
/**
* The Activity Syncer commits activity blueprints to the database. Where a
* blueprint represents a single piece of activity, the syncer associates it
* with a particular user(s) and makes it available on their activity feed.
*/
class ActivitySyncer
{
/**
* @var ActivityRepository
*/
protected $activity;
/**
* Create a new instance of the activity syncer.
*
* @param ActivityRepository $activity
*/
public function __construct(ActivityRepository $activity)
{
$this->activity = $activity;
}
/**
* Sync a piece of activity so that it is present for the specified users,
* and not present for anyone else.
*
* @param Blueprint $blueprint
* @param \Flarum\Core\Models\User[] $users
* @return void
*/
public function sync(Blueprint $blueprint, array $users)
{
$attributes = $this->getAttributes($blueprint);
// Find all existing activity records in the database matching this
// blueprint. We will begin by assuming that they all need to be
// deleted in order to match the provided list of users.
$toDelete = Activity::where($attributes)->get();
$toInsert = [];
// For each of the provided users, check to see if they already have
// an activity record in the database. If they do, we can leave it be;
// otherwise, we will need to create a new one for them.
foreach ($users as $user) {
$existing = $toDelete->first(function ($activity) use ($user) {
return $activity->user_id === $user->id;
});
if ($existing) {
$toDelete->forget($toDelete->search($existing));
} else {
$toInsert[] = $attributes + ['user_id' => $user->id];
}
}
// Finally, delete all of the remaining activity records which weren't
// removed from this collection by the above loop. Insert the records
// we need to insert as well.
if (count($toDelete)) {
$this->deleteActivity($toDelete->lists('id'));
}
if (count($toInsert)) {
$this->createActivity($toInsert);
}
}
/**
* Delete a piece of activity for all users.
*
* @param Blueprint $blueprint
* @return void
*/
public function delete(Blueprint $blueprint)
{
Activity::where($this->getAttributes($blueprint))->delete();
}
/**
* Delete a list of activity records.
*
* @param int[] $ids
*/
protected function deleteActivity(array $ids)
{
Activity::whereIn('id', $ids)->delete();
}
/**
* Insert a list of activity record into the database.
*
* @param array[] $records An array containing arrays of activity record
* attributes to insert.
*/
protected function createActivity(array $records)
{
Activity::insert($records);
}
/**
* Construct an array of attributes to be stored in an activity record in
* the database, given an activity blueprint.
*
* @param Blueprint $blueprint
* @return array
*/
protected function getAttributes(Blueprint $blueprint)
{
return [
'type' => $blueprint::getType(),
'subject_id' => $blueprint->getSubject()->id,
'time' => $blueprint->getTime()
];
}
}

View File

@ -1,37 +0,0 @@
<?php namespace Flarum\Core\Activity;
/**
* An activity Blueprint, when instantiated, represents a single piece of
* activity. The blueprint is used by the ActivitySyncer to commit the activity
* to the database.
*/
interface Blueprint
{
/**
* Get the model that is the subject of this activity.
*
* @return \Flarum\Core\Model
*/
public function getSubject();
/**
* Get the time at which the activity occurred.
*
* @return mixed
*/
public function getTime();
/**
* Get the serialized type of this activity.
*
* @return string
*/
public static function getType();
/**
* Get the name of the model class for the subject of this activity.
*
* @return string
*/
public static function getSubjectModel();
}

View File

@ -1,59 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Users\User;
/**
* An activity blueprint for the 'joined' activity type, which represents a user
* joining the forum.
*/
class JoinedBlueprint implements Blueprint
{
/**
* The user who joined the forum.
*
* @var User
*/
protected $user;
/**
* Create a new 'joined' activity blueprint.
*
* @param User $user The user who joined the forum.
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* {@inheritdoc}
*/
public function getSubject()
{
return $this->user;
}
/**
* {@inheritdoc}
*/
public function getTime()
{
return $this->user->join_time;
}
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'joined';
}
/**
* {@inheritdoc}
*/
public static function getSubjectModel()
{
return 'Flarum\Core\Users\User';
}
}

View File

@ -1,129 +0,0 @@
<?php namespace Flarum\Core\Activity\Listeners;
use Flarum\Core\Activity\ActivitySyncer;
use Flarum\Core\Activity\PostedBlueprint;
use Flarum\Core\Activity\StartedDiscussionBlueprint;
use Flarum\Core\Activity\JoinedBlueprint;
use Flarum\Core\Posts\Post;
use Flarum\Core\Users\Guest;
use Flarum\Events\PostWasPosted;
use Flarum\Events\PostWasDeleted;
use Flarum\Events\PostWasHidden;
use Flarum\Events\PostWasRestored;
use Flarum\Events\UserWasRegistered;
use Illuminate\Contracts\Events\Dispatcher;
class UserActivitySyncer
{
/**
* @var \Flarum\Core\Activity\ActivitySyncer
*/
protected $activity;
/**
* @param \Flarum\Core\Activity\ActivitySyncer $activity
*/
public function __construct(ActivitySyncer $activity)
{
$this->activity = $activity;
}
/**
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Events\PostWasPosted', [$this, 'whenPostWasPosted']);
$events->listen('Flarum\Events\PostWasHidden', [$this, 'whenPostWasHidden']);
$events->listen('Flarum\Events\PostWasRestored', [$this, 'whenPostWasRestored']);
$events->listen('Flarum\Events\PostWasDeleted', [$this, 'whenPostWasDeleted']);
$events->listen('Flarum\Events\UserWasRegistered', [$this, 'whenUserWasRegistered']);
}
/**
* @param \Flarum\Events\PostWasPosted $event
* @return void
*/
public function whenPostWasPosted(PostWasPosted $event)
{
$this->postBecameVisible($event->post);
}
/**
* @param \Flarum\Events\PostWasHidden $event
* @return void
*/
public function whenPostWasHidden(PostWasHidden $event)
{
$this->postBecameInvisible($event->post);
}
/**
* @param \Flarum\Events\PostWasRestored $event
* @return void
*/
public function whenPostWasRestored(PostWasRestored $event)
{
$this->postBecameVisible($event->post);
}
/**
* @param \Flarum\Events\PostWasDeleted $event
* @return void
*/
public function whenPostWasDeleted(PostWasDeleted $event)
{
$this->postBecameInvisible($event->post);
}
/**
* @param \Flarum\Events\UserWasRegistered $event
* @return void
*/
public function whenUserWasRegistered(UserWasRegistered $event)
{
$blueprint = new JoinedBlueprint($event->user);
$this->activity->sync($blueprint, [$event->user]);
}
/**
* Sync activity to a post's author when a post becomes visible.
*
* @param \Flarum\Core\Posts\Post $post
* @return void
*/
protected function postBecameVisible(Post $post)
{
if ($post->isVisibleTo(new Guest)) {
$blueprint = $this->postedBlueprint($post);
$this->activity->sync($blueprint, [$post->user]);
}
}
/**
* Delete activity when a post becomes invisible.
*
* @param \Flarum\Core\Posts\Post $post
* @return void
*/
protected function postBecameInvisible(Post $post)
{
$blueprint = $this->postedBlueprint($post);
$this->activity->delete($blueprint);
}
/**
* Create the appropriate activity blueprint for a post.
*
* @param \Flarum\Core\Posts\Post $post
* @return \Flarum\Core\Activity\Blueprint
*/
protected function postedBlueprint(Post $post)
{
return $post->number == 1 ? new StartedDiscussionBlueprint($post) : new PostedBlueprint($post);
}
}

View File

@ -1,59 +0,0 @@
<?php namespace Flarum\Core\Activity;
use Flarum\Core\Posts\Post;
/**
* An activity blueprint for the 'posted' activity type, which represents a user
* posting in a discussion.
*/
class PostedBlueprint implements Blueprint
{
/**
* The user who joined the forum.
*
* @var Post
*/
protected $post;
/**
* Create a new 'posted' activity blueprint.
*
* @param Post $post The post that was made.
*/
public function __construct(Post $post)
{
$this->post = $post;
}
/**
* {@inheritdoc}
*/
public function getSubject()
{
return $this->post;
}
/**
* {@inheritdoc}
*/
public function getTime()
{
return $this->post->time;
}
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'posted';
}
/**
* {@inheritdoc}
*/
public static function getSubjectModel()
{
return 'Flarum\Core\Posts\Post';
}
}

View File

@ -1,16 +0,0 @@
<?php namespace Flarum\Core\Activity;
/**
* An activity blueprint for the 'startedDiscussion' activity type, which
* represents a user starting a discussion.
*/
class StartedDiscussionBlueprint extends PostedBlueprint
{
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'startedDiscussion';
}
}

View File

@ -41,7 +41,6 @@ class CoreServiceProvider extends ServiceProvider
// FIXME: probably use Illuminate's AggregateServiceProvider
// functionality, because it includes the 'provides' stuff.
$this->app->register('Flarum\Core\Activity\ActivityServiceProvider');
$this->app->register('Flarum\Core\Discussions\DiscussionsServiceProvider');
$this->app->register('Flarum\Core\Formatter\FormatterServiceProvider');
$this->app->register('Flarum\Core\Groups\GroupsServiceProvider');

View File

@ -85,9 +85,20 @@ class PostRepository
*/
public function findByIds(array $ids, User $actor = null)
{
$ids = $this->filterDiscussionVisibleTo($ids, $actor);
$visibleIds = $this->filterDiscussionVisibleTo($ids, $actor);
$posts = Post::with('discussion')->whereIn('id', (array) $ids)->get();
$posts = Post::with('discussion')->whereIn('id', $visibleIds)->get();
$posts->sort(function ($a, $b) use ($ids) {
$aPos = array_search($a->id, $ids);
$bPos = array_search($b->id, $ids);
if ($aPos === $bPos) {
return 0;
}
return $aPos < $bPos ? -1 : 1;
});
return $this->filterVisibleTo($posts, $actor);
}