Refactor + Fix: Wasn't correctly loading activity streams. Code is a lot more Ember-y now.

This commit is contained in:
Robin Ward 2013-05-22 11:20:16 -04:00
parent 89a617f0c6
commit 0f296cd42b
15 changed files with 198 additions and 187 deletions

View File

@ -177,23 +177,6 @@ Discourse.User = Discourse.Model.extend({
}); });
}, },
/**
Filters out this user's stream of user actions by a given filter
@method filterStream
@param {String} filter
**/
filterStream: function(filter) {
if (Discourse.UserAction.statGroups[filter]) {
filter = Discourse.UserAction.statGroups[filter].join(",");
}
this.set('streamFilter', filter);
this.set('stream', Em.A());
this.set('totalItems', 0);
return this.loadMoreUserActions();
},
/** /**
Loads a single user action by id. Loads a single user action by id.
@ -207,44 +190,9 @@ Discourse.User = Discourse.Model.extend({
return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) { return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) {
if (result) { if (result) {
if ((user.get('streamFilter') || result.action_type) !== result.action_type) return; if ((user.get('streamFilter') || result.action_type) !== result.action_type) return;
var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(result)]);
var action = Em.A(); stream.set('itemsLoaded', user.get('itemsLoaded') + 1);
action.pushObject(Discourse.UserAction.create(result)); stream.insertAt(0, action[0]);
action = Discourse.UserAction.collapseStream(action);
user.set('totalItems', user.get('totalItems') + 1);
return stream.insertAt(0, action[0]);
}
});
},
/**
Loads more user actions, and then calls a callback if defined.
@method loadMoreUserActions
@returns {Promise} the content of the user actions
**/
loadMoreUserActions: function() {
var user = this;
var stream = user.get('stream');
if (!stream) return;
var url = Discourse.getURL("/user_actions?offset=") + this.get('totalItems') + "&user_id=" + (this.get("id"));
if (this.get('streamFilter')) {
url += "&filter=" + (this.get('streamFilter'));
}
return Discourse.ajax(url, { cache: 'false' }).then( function(result) {
if (result && result.user_actions && result.user_actions.each) {
var copy = Em.A();
result.user_actions.each(function(i) {
return copy.pushObject(Discourse.UserAction.create(i));
});
copy = Discourse.UserAction.collapseStream(copy);
stream.pushObjects(copy);
user.set('stream', stream);
user.set('totalItems', user.get('totalItems') + result.user_actions.length);
} }
}); });
}, },
@ -284,33 +232,36 @@ Discourse.User = Discourse.Model.extend({
return this.get('stats').filterProperty('isPM'); return this.get('stats').filterProperty('isPM');
}.property('stats.@each.isPM'), }.property('stats.@each.isPM'),
/**
Load extra details for the user
@method loadDetails findDetails: function() {
**/
loadDetails: function() {
this.set('loading', true);
// Check the preload store first
var user = this; var user = this;
var username = user.get('username'); return PreloadStore.getAndRemove("user_" + user.get('username'), function() {
return Discourse.ajax("/users/" + user.get('username') + '.json');
return PreloadStore.getAndRemove("user_" + username, function() {
return Discourse.ajax("/users/" + username + '.json');
}).then(function (json) { }).then(function (json) {
// Create a user from the resulting JSON
json.user.stats = Discourse.User.groupStats(json.user.stats.map(function(s) { json.user.stats = Discourse.User.groupStats(json.user.stats.map(function(s) {
if (s.count) s.count = parseInt(s.count, 10); if (s.count) s.count = parseInt(s.count, 10);
return Discourse.UserActionStat.create(s); return Discourse.UserActionStat.create(s);
})); }));
user.setProperties(json.user); user.setProperties(json.user);
user.set('loading', false);
return user; return user;
}); });
},
findStream: function(filter) {
if (Discourse.UserAction.statGroups[filter]) {
filter = Discourse.UserAction.statGroups[filter].join(",");
}
var stream = Discourse.UserStream.create({
totalItems: 0,
content: [],
filter: filter,
user: this
});
stream.findItems();
return stream;
} }
}); });

View File

@ -0,0 +1,39 @@
/**
Represents a user's stream
@class UserStream
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.UserStream = Discourse.Model.extend({
filterChanged: function() {
this.setProperties({
content: Em.A(),
itemsLoaded: 0
});
this.findItems();
}.observes('filter'),
findItems: function() {
var url = Discourse.getURL("/user_actions?offset=") + this.get('itemsLoaded') + "&username=" + (this.get('user.username_lower'));
if (this.get('filter')) {
url += "&filter=" + (this.get('filter'));
}
var stream = this;
return Discourse.ajax(url, {cache: 'false'}).then( function(result) {
if (result && result.user_actions && result.user_actions.each) {
var copy = Em.A();
result.user_actions.each(function(i) {
return copy.pushObject(Discourse.UserAction.create(i));
});
copy = Discourse.UserAction.collapseStream(copy);
stream.get('content').pushObjects(copy);
stream.set('itemsLoaded', stream.get('itemsLoaded') + result.user_actions.length);
}
});
}
});

View File

@ -11,9 +11,11 @@ Discourse.Route = Em.Route.extend({
/** /**
Called every time we enter a route on Discourse. Called every time we enter a route on Discourse.
@method enter @method activate
**/ **/
enter: function(router, context) { activate: function(router, context) {
this._super();
// Close mini profiler // Close mini profiler
$('.profiler-results .profiler-result').remove(); $('.profiler-results .profiler-result').remove();

View File

@ -9,15 +9,11 @@
Discourse.UserActivityRoute = Discourse.Route.extend({ Discourse.UserActivityRoute = Discourse.Route.extend({
model: function() { model: function() {
return this.modelFor('user'); return this.modelFor('user').findStream();
}, },
renderTemplate: function() { renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' }); this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, user) {
user.filterStream(null);
} }
}); });

View File

@ -9,16 +9,14 @@
Discourse.UserPrivateMessagesRoute = Discourse.RestrictedUserRoute.extend({ Discourse.UserPrivateMessagesRoute = Discourse.RestrictedUserRoute.extend({
model: function() { model: function() {
return this.modelFor('user'); return this.modelFor('user').findStream(Discourse.UserAction.GOT_PRIVATE_MESSAGE);
}, },
renderTemplate: function() { renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' }); this.render({ into: 'user', outlet: 'userOutlet' });
}, },
setupController: function(controller, user) { setupController: function(controller, stream) {
user.filterStream(Discourse.UserAction.GOT_PRIVATE_MESSAGE);
var composerController = this.controllerFor('composer'); var composerController = this.controllerFor('composer');
Discourse.Draft.get('new_private_message').then(function(data) { Discourse.Draft.get('new_private_message').then(function(data) {
if (data.draft) { if (data.draft) {
@ -32,6 +30,7 @@ Discourse.UserPrivateMessagesRoute = Discourse.RestrictedUserRoute.extend({
}); });
} }
}); });

View File

@ -9,11 +9,29 @@
Discourse.UserRoute = Discourse.Route.extend({ Discourse.UserRoute = Discourse.Route.extend({
model: function(params) { model: function(params) {
return Discourse.User.create({username: params.username}).loadDetails(); return Discourse.User.create({username: params.username});
}, },
serialize: function(params) { serialize: function(params) {
return { username: Em.get(params, 'username').toLowerCase() }; return { username: Em.get(params, 'username').toLowerCase() };
},
setupController: function(controller, user) {
user.findDetails();
},
activate: function() {
this._super();
var user = this.modelFor('user');
Discourse.MessageBus.subscribe("/users/" + user.get('username_lower'), function(data) {
user.loadUserAction(data);
});
},
deactivate: function() {
this._super();
Discourse.MessageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));
} }
}); });

View File

@ -1,53 +1,54 @@
<div id='user-info'> {{#with user}}
<nav class='buttons'> <div id='user-info'>
{{#if can_edit}} <nav class='buttons'>
{{#linkTo "preferences" class="btn"}}{{i18n user.edit}}{{/linkTo}} {{#if can_edit}}
{{/if}} {{#linkTo "preferences" class="btn"}}{{i18n user.edit}}{{/linkTo}}
<br/> {{/if}}
{{#if can_send_private_message_to_user}} <br/>
<button class='btn create' {{action composePrivateMessage}}> {{#if can_send_private_message_to_user}}
<i class='icon icon-envelope-alt'></i> <button class='btn create' {{action composePrivateMessage}}>
{{i18n user.private_message}} <i class='icon icon-envelope-alt'></i>
</button> {{i18n user.private_message}}
{{/if}} </button>
</nav> {{/if}}
<div class='clearfix'></div> </nav>
<div class='clearfix'></div>
<ul class='action-list nav-stacked side-nav'> <ul class='action-list nav-stacked side-nav'>
{{view Discourse.ActivityFilterView countBinding="statsCountNonPM"}} {{view Discourse.ActivityFilterView countBinding="statsCountNonPM"}}
{{#each statsExcludingPms}} {{#each statsExcludingPms}}
{{view Discourse.ActivityFilterView contentBinding="this"}} {{view Discourse.ActivityFilterView contentBinding="this"}}
{{/each}} {{/each}}
</ul> </ul>
<div class='show'> <div class='show'>
<dl> <dl>
{{#if hasWebsite}} {{#if hasWebsite}}
<dt>{{i18n user.website}}:</dt><dd><a {{bindAttr href="website"}} target="_blank">{{websiteName}}</a></dd> <dt>{{i18n user.website}}:</dt><dd><a {{bindAttr href="website"}} target="_blank">{{websiteName}}</a></dd>
{{/if}} {{/if}}
<dt>{{i18n user.created}}:</dt><dd>{{date created_at}}</dd> <dt>{{i18n user.created}}:</dt><dd>{{date created_at}}</dd>
{{#if last_posted_at}} {{#if last_posted_at}}
<dt>{{i18n user.last_posted}}:</dt><dd>{{date last_posted_at}}</dd> <dt>{{i18n user.last_posted}}:</dt><dd>{{date last_posted_at}}</dd>
{{/if}} {{/if}}
{{#if last_seen_at}} {{#if last_seen_at}}
<dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd> <dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd>
{{/if}} {{/if}}
{{#if invited_by}} {{#if invited_by}}
<dt>{{i18n user.invited_by}}:</dt><dd>{{#linkTo user.activity invited_by}}{{invited_by.username}}{{/linkTo}}</dd> <dt>{{i18n user.invited_by}}:</dt><dd>{{#linkTo user.activity invited_by}}{{invited_by.username}}{{/linkTo}}</dd>
{{/if}} {{/if}}
{{#if email}} {{#if email}}
<dt>{{i18n user.email.title}}:</dt><dd {{bindAttr title="email"}}>{{email}}</dd> <dt>{{i18n user.email.title}}:</dt><dd {{bindAttr title="email"}}>{{email}}</dd>
{{/if}} {{/if}}
<dt>{{i18n user.trust_level}}:</dt><dd>{{trustLevel.name}}</dd> <dt>{{i18n user.trust_level}}:</dt><dd>{{trustLevel.name}}</dd>
</dl> </dl>
</div>
{{#if can_edit}}
<div style='margin-top: 10px'>
<button class='btn' data-not-implemented='true' disabled title="{{i18n not_implemented}}">{{i18n user.download_archive}}</button>
</div> </div>
{{/if}}
{{#if can_edit}}
<div style='margin-top: 10px'>
<button class='btn' data-not-implemented='true' disabled title="{{i18n not_implemented}}">{{i18n user.download_archive}}</button>
</div>
{{/if}}
</div> </div>
{{/with}}
{{view Discourse.UserStreamView streamBinding="stream"}} {{view Discourse.UserStreamView streamBinding="model"}}

View File

@ -1,23 +1,24 @@
<div id='user-info'> <div id='user-info'>
<nav class='buttons'> {{#with user}}
{{#if can_edit}} <nav class='buttons'>
{{#linkTo "preferences" class="btn"}}{{i18n user.edit}}{{/linkTo}} {{#if can_edit}}
{{/if}} {{#linkTo "preferences" class="btn"}}{{i18n user.edit}}{{/linkTo}}
<br/> {{/if}}
{{#if can_send_private_message_to_user}} <br/>
<button class='btn create' {{action composePrivateMessage}}> {{#if can_send_private_message_to_user}}
<i class='icon icon-plus'></i> <button class='btn create' {{action composePrivateMessage}}>
{{i18n user.private_message}} <i class='icon icon-plus'></i>
</button> {{i18n user.private_message}}
{{/if}} </button>
</nav> {{/if}}
<div class='clearfix'></div> </nav>
<div class='clearfix'></div>
<ul class='action-list nav-stacked side-nav'>
{{#each statsPmsOnly}}
{{view Discourse.ActivityFilterView contentBinding="this"}}
{{/each}}
</ul>
<ul class='action-list nav-stacked side-nav'>
{{#each statsPmsOnly}}
{{view Discourse.ActivityFilterView contentBinding="this"}}
{{/each}}
</ul>
{{/with}}
</div> </div>
{{view Discourse.UserStreamView streamBinding="stream"}} {{view Discourse.UserStreamView streamBinding="model"}}

View File

@ -1,6 +1,6 @@
<div id='user-stream'> <div id='user-stream'>
{{#collection contentBinding="stream" itemClass="item"}} {{#each view.stream.content}}
{{#with view.content}} <div class='item'>
<div class='clearfix info'> <div class='clearfix info'>
<a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a> <a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{date path="created_at" leaveAgo="true"}}</span> <span class='time'>{{date path="created_at" leaveAgo="true"}}</span>
@ -20,7 +20,8 @@
{{/each}} {{/each}}
</div> </div>
{{/each}} {{/each}}
{{/with}} </div>
{{/collection}} {{/each}}
</div> </div>
<div id="user-stream-bottom"></div> <div id="user-stream-bottom"></div>

View File

@ -10,6 +10,8 @@ Discourse.ActivityFilterView = Discourse.View.extend({
tagName: 'li', tagName: 'li',
classNameBindings: ['active'], classNameBindings: ['active'],
stream: Em.computed.alias('controller.content'),
countChanged: function(){ countChanged: function(){
this.rerender(); this.rerender();
}.observes('count'), }.observes('count'),
@ -17,11 +19,11 @@ Discourse.ActivityFilterView = Discourse.View.extend({
active: function() { active: function() {
var content = this.get('content'); var content = this.get('content');
if (content) { if (content) {
return parseInt(this.get('controller.content.streamFilter'), 10) === parseInt(Em.get(content, 'action_type'), 10); return parseInt(this.get('stream.filter'), 10) === parseInt(Em.get(content, 'action_type'), 10);
} else { } else {
return this.blank('controller.content.streamFilter'); return this.blank('stream.filter');
} }
}.property('controller.content.streamFilter', 'content.action_type'), }.property('stream.filter', 'content.action_type'),
render: function(buffer) { render: function(buffer) {
var content = this.get('content'); var content = this.get('content');
@ -40,7 +42,7 @@ Discourse.ActivityFilterView = Discourse.View.extend({
}, },
click: function() { click: function() {
this.get('controller.content').filterStream(this.get('content.action_type')); this.set('stream.filter', this.get('content.action_type'));
return false; return false;
} }
}); });

View File

@ -9,8 +9,6 @@
**/ **/
Discourse.UserStreamView = Discourse.View.extend(Discourse.Scrolling, { Discourse.UserStreamView = Discourse.View.extend(Discourse.Scrolling, {
templateName: 'user/stream', templateName: 'user/stream',
currentUserBinding: 'Discourse.currentUser',
userBinding: 'controller.content',
scrolled: function(e) { scrolled: function(e) {
@ -23,13 +21,16 @@ Discourse.UserStreamView = Discourse.View.extend(Discourse.Scrolling, {
var docViewTop = $(window).scrollTop(); var docViewTop = $(window).scrollTop();
var windowHeight = $(window).height(); var windowHeight = $(window).height();
var docViewBottom = docViewTop + windowHeight; var docViewBottom = docViewTop + windowHeight;
this.set('loading', true);
if (position.top < docViewBottom) { if (position.top < docViewBottom) {
$userStreamBottom.data('loading', true); $userStreamBottom.data('loading', true);
this.set('loading', true); this.set('loading', true);
var userStreamView = this; var userStreamView = this;
this.get('controller.content').loadMoreUserActions().then(function() { var user = this.get('stream.user');
var stream = this.get('stream');
stream.findItems().then(function() {
userStreamView.set('loading', false); userStreamView.set('loading', false);
Em.run.schedule('afterRender', function() { Em.run.schedule('afterRender', function() {
$userStreamBottom.data('loading', null); $userStreamBottom.data('loading', null);
@ -39,15 +40,10 @@ Discourse.UserStreamView = Discourse.View.extend(Discourse.Scrolling, {
}, },
willDestroyElement: function() { willDestroyElement: function() {
Discourse.MessageBus.unsubscribe("/users/" + (this.get('user.username').toLowerCase()));
this.unbindScrolling(); this.unbindScrolling();
}, },
didInsertElement: function() { didInsertElement: function() {
var userSteamView = this;
Discourse.MessageBus.subscribe("/users/" + (this.get('user.username').toLowerCase()), function(data) {
userSteamView.get('user').loadUserAction(data);
});
this.bindScrolling(); this.bindScrolling();
} }

View File

@ -175,6 +175,19 @@ class ApplicationController < ActionController::Base
end end
end end
def fetch_user_from_params
username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '')
user = User.where(username_lower: username_lower).first
raise Discourse::NotFound.new if user.blank?
guardian.ensure_can_see!(user)
user
end
private private
def render_json_error(obj) def render_json_error(obj)

View File

@ -1,11 +1,13 @@
class UserActionsController < ApplicationController class UserActionsController < ApplicationController
def index def index
requires_parameters(:user_id) requires_parameters(:username)
per_chunk = 60 per_chunk = 60
user = fetch_user_from_params
opts = { opts = {
user_id: params[:user_id].to_i, user_id: user.id,
offset: params[:offset], offset: params[:offset].to_i,
limit: per_chunk, limit: per_chunk,
action_types: (params[:filter] || "").split(",").map(&:to_i), action_types: (params[:filter] || "").split(",").map(&:to_i),
guardian: guardian, guardian: guardian,
@ -29,4 +31,5 @@ class UserActionsController < ApplicationController
# todo # todo
end end
end end

View File

@ -8,7 +8,7 @@ class UsersController < ApplicationController
before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect] before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect]
# we need to allow account creation with bad CSRF tokens, if people are caching, the CSRF token on the # we need to allow account creation with bad CSRF tokens, if people are caching, the CSRF token on the
# page is going to be empty, this means that server will see an invalid CSRF and blow the session # page is going to be empty, this means that server will see an invalid CSRF and blow the session
# once that happens you can't log in with social # once that happens you can't log in with social
skip_before_filter :verify_authenticity_token, only: [:create] skip_before_filter :verify_authenticity_token, only: [:create]
@ -348,17 +348,6 @@ class UsersController < ApplicationController
'3019774c067cc2b' '3019774c067cc2b'
end end
def fetch_user_from_params
username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '')
user = User.where(username_lower: username_lower).first
raise Discourse::NotFound.new if user.blank?
guardian.ensure_can_see!(user)
user
end
def honeypot_or_challenge_fails?(params) def honeypot_or_challenge_fails?(params)
params[:password_confirmation] != honeypot_value || params[:password_confirmation] != honeypot_value ||
params[:challenge] != challenge_value.try(:reverse) params[:challenge] != challenge_value.try(:reverse)

View File

@ -62,12 +62,12 @@ class UserSerializer < BasicUserSerializer
scope.can_send_private_message?(object) scope.can_send_private_message?(object)
end end
def stats
UserAction.stats(object.id, scope)
end
def can_edit def can_edit
scope.can_edit?(object) scope.can_edit?(object)
end end
def stats
UserAction.stats(object.id, scope)
end
end end