mirror of
https://github.com/flarum/framework.git
synced 2025-01-19 16:02:44 +08:00
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:
parent
4c23a1e274
commit
d5296e9aa5
|
@ -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() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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')
|
||||
}) {}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user