1
0
mirror of https://github.com/flarum/core.git synced 2025-07-20 08:11:27 +02: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:
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')
}) {}