Show topics as a list of topics on the User Stream.

This commit is contained in:
Robin Ward 2013-07-24 17:15:21 -04:00
parent 3f5ea1ef79
commit 0317cf9608
57 changed files with 743 additions and 650 deletions

View File

@ -13,5 +13,3 @@
{{collection contentBinding="filteredContent" classNames="form-horizontal settings" itemViewClass="Discourse.SiteSettingView"}}
<!-- will remove as soon as I figure out what is going on -->
<p><small>Diagnostics: last_message_processed {{diags.last_message_processed}}</small></p>

View File

@ -1,4 +1,3 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
@ -8,4 +7,3 @@
{{unbound description}}
</label>
</div>
{{/with}}

View File

@ -1,4 +1,3 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
@ -16,4 +15,3 @@
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{/if}}
{{/with}}

View File

@ -1,4 +1,3 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
@ -16,4 +15,3 @@
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{/if}}
{{/with}}

View File

@ -64,6 +64,9 @@ Discourse.URL = Em.Object.createWithMixins({
if (this.navigatedToListMore(oldPath, path)) { return; }
if (this.navigatedToHome(oldPath, path)) { return; }
if (path.match(/^\/?users\/[^\/]+$/)) {
path += "/activity";
}
// Be wary of looking up the router. In this case, we have links in our
// HTML, say form compiled markdown posts, that need to be routed.
var router = this.get('router');

View File

@ -87,7 +87,7 @@ Discourse.Utilities = {
},
userUrl: function(username) {
return Discourse.getURL("/users/" + username);
return Discourse.getURL("/users/" + username.toLowerCase());
},
emailValid: function(email) {

View File

@ -24,6 +24,16 @@ Discourse.ListController = Discourse.Controller.extend({
});
}.property(),
createTopicText: function() {
if (this.get('category.name')) {
return I18n.t("topic.create_in", {
categoryName: this.get('category.name')
});
} else {
return I18n.t("topic.create");
}
}.property('category.name'),
/**
Refresh our current topic list

View File

@ -81,11 +81,11 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({
}.property('allLoaded', 'topics.length'),
loadMore: function() {
this.set('loadingMore', true);
var listTopicsController = this;
return this.get('model').loadMoreTopics().then(function(hasMoreTopics) {
listTopicsController.set('loadingMore', false);
return hasMoreTopics;
var topicList = this.get('model');
return topicList.loadMoreTopics().then(function(moreUrl) {
if (!Em.isEmpty(moreUrl)) {
Discourse.URL.replaceState(Discourse.getURL("/") + topicList.get('filter') + "/more");
}
});
}

View File

@ -1,51 +1,3 @@
/**
The route for editing a user's "About Me" bio.
@class PreferencesAboutRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAboutRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newBio: model.get('bio_raw') });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
events: {
changeAbout: function() {
var route = this;
var controller = route.controllerFor('preferencesAbout');
controller.setProperties({ saving: true });
return controller.get('model').save().then(function() {
controller.set('saving', false);
route.transitionTo('user.index');
}, function() {
// model failed to save
controller.set('saving', false);
alert(I18n.t('generic_error'));
});
}
}
});
/**
This controller supports actions related to updating your "About Me" bio

View File

@ -1,21 +1,3 @@
/**
The common route stuff for a user's preference
@class PreferencesRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});
/**
This controller supports actions related to updating one's preferences

View File

@ -1,32 +1,3 @@
/**
The route for editing a user's email
@class PreferencesEmailRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newEmail: model.get('email') });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});
/**
This controller supports actions related to updating one's email address

View File

@ -1,32 +1,3 @@
/**
The route for updating a user's username
@class PreferencesUsernameRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
return this.render({ into: 'user', outlet: 'userOutlet' });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
setupController: function(controller, user) {
controller.setProperties({ model: user, newUsername: user.get('username') });
}
});
/**
This controller supports actions related to updating one's username

View File

@ -1,82 +1,3 @@
/**
The base route for showing an activity stream.
@class UserActivityRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserActivityRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('user_activity', {into: 'user', outlet: 'userOutlet' });
},
model: function() {
return this.modelFor('user');
},
setupController: function(controller, user) {
this.controllerFor('userActivity').set('model', user);
var composerController = this.controllerFor('composer');
controller.set('model', user);
if (Discourse.User.current()) {
Discourse.Draft.get('new_private_message').then(function(data) {
if (data.draft) {
composerController.open({
draft: data.draft,
draftKey: 'new_private_message',
ignoreIfChanged: true,
draftSequence: data.draft_sequence
});
}
});
}
}
});
Discourse.UserActivityIndexRoute = Discourse.Route.extend({
model: function() {
return this.modelFor('user').findStream(this.get('userActionType'));
},
renderTemplate: function() {
this.render('user_stream', {into: 'user_activity', outlet: 'activity'});
},
setupController: function() {
this.controllerFor('user_activity').set('userActionType', this.get('userActionType'));
}
});
Discourse.UserIndexRoute = Discourse.UserActivityRoute.extend({
renderTemplate: function() {
this._super();
this.render('user_stream', {into: 'user_activity', outlet: 'activity'});
},
model: function() {
return this.modelFor('user').findStream();
},
setupController: function(controller, model) {
this.controllerFor('userActivity').set('model', this.modelFor('user'));
this.set('model', model);
}
});
// Build all the filter routes
Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) {
Discourse["UserActivity" + userAction.classify() + "Route"] = Discourse.UserActivityIndexRoute.extend({
userActionType: Discourse.UserAction.TYPES[userAction]
});
});
// // Build the private message routes
Discourse.UserPrivateMessagesRoute = Discourse.UserActivityRoute.extend({});
Discourse.UserPrivateMessagesIndexRoute = Discourse.UserActivityMessagesReceivedRoute;
Discourse.UserPrivateMessagesSentRoute = Discourse.UserActivityMessagesSentRoute;
/**
This controller supports all actions on a user's activity stream

View File

@ -0,0 +1,39 @@
/**
This mixin provides the ability to load more items for a view which is
scrolled to the bottom.
@class Discourse.LoadMore
@extends Ember.Mixin
@uses Discourse.Scrolling
@namespace Discourse
@module Discourse
**/
Discourse.LoadMore = Em.Mixin.create(Discourse.Scrolling, {
scrolled: function(e) {
var eyeline = this.get('eyeline');
if (eyeline) { eyeline.update(); }
},
loadMore: function() {
console.error('loadMore() not defined');
},
didInsertElement: function() {
this._super();
var eyeline = new Discourse.Eyeline(this.get('eyelineSelector'));
this.set('eyeline', eyeline);
var paginatedTopicListView = this;
eyeline.on('sawBottom', function() {
paginatedTopicListView.loadMore();
});
this.bindScrolling();
},
willRemoveElement: function() {
this._super();
this.unbindScrolling();
}
});

View File

@ -23,32 +23,37 @@ Discourse.TopicList = Discourse.Model.extend({
},
loadMoreTopics: function() {
var moreUrl, _this = this;
if (moreUrl = this.get('more_topics_url')) {
Discourse.URL.replaceState(Discourse.getURL("/") + (this.get('filter')) + "/more");
if (this.get('loadingMore')) { return Ember.RSVP.reject(); }
var moreUrl = this.get('more_topics_url');
if (moreUrl) {
var topicList = this;
this.set('loadingMore', true);
return Discourse.ajax({url: moreUrl}).then(function (result) {
var newTopics, topics, topicsAdded = 0;
var topicsAdded = 0;
if (result) {
// the new topics loaded from the server
newTopics = Discourse.TopicList.topicsFrom(result);
topics = _this.get("topics");
var newTopics = Discourse.TopicList.topicsFrom(result);
var topics = topicList.get("topics");
_this.forEachNew(newTopics, function(t) {
topicList.forEachNew(newTopics, function(t) {
t.set('highlight', topicsAdded++ === 0);
topics.pushObject(t);
});
_this.set('more_topics_url', result.topic_list.more_topics_url);
Discourse.set('transient.topicsList', _this);
}
topicList.set('more_topics_url', result.topic_list.more_topics_url);
Discourse.set('transient.topicsList', topicList);
topicList.set('loadingMore', false);
return result.topic_list.more_topics_url;
}
});
} else {
// Return a promise indicating no more results
return Ember.Deferred.promise(function (p) {
p.resolve(false);
});
return Ember.RSVP.reject();
}
},
@ -109,6 +114,9 @@ Discourse.TopicList.reopenClass({
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topics = Em.A();
console.log(result.topic_list);
_.each(result.topic_list.topics,function(ft) {
ft.category = categories[ft.category_id];
_.each(ft.posters,function(p) {

View File

@ -264,6 +264,7 @@ Discourse.User = Discourse.Model.extend({
json.user.invited_by = Discourse.User.create(json.user.invited_by);
}
user.setProperties(json.user);
return user;
});

View File

@ -99,7 +99,11 @@ Discourse.UserAction = Discourse.Model.extend({
}.property('target_username'),
targetUserUrl: Discourse.computed.url('target_username', '/users/%@'),
userUrl: Discourse.computed.url('username', '/users/%@'),
usernameLower: function() {
return this.get('username').toLowerCase();
}.property('username'),
userUrl: Discourse.computed.url('usernameLower', '/users/%@'),
postUrl: function() {
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));

View File

@ -18,7 +18,7 @@ Discourse.UserStream = Discourse.Model.extend({
findItems: function() {
var me = this;
if(this.get("loading")) { return; }
if(this.get("loading")) { return Ember.RSVP.reject(); }
this.set("loading",true);
var url = Discourse.getURL("/user_actions.json?offset=") + this.get('itemsLoaded') + "&username=" + (this.get('user.username_lower'));

View File

@ -47,5 +47,3 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
Discourse.ListController.filters.forEach(function(filter) {
Discourse["List" + (filter.capitalize()) + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter });
});

View File

@ -0,0 +1,119 @@
/**
The common route stuff for a user's preference
@class PreferencesRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});
/**
The route for editing a user's "About Me" bio.
@class PreferencesAboutRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAboutRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newBio: model.get('bio_raw') });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
events: {
changeAbout: function() {
var route = this;
var controller = route.controllerFor('preferencesAbout');
controller.setProperties({ saving: true });
return controller.get('model').save().then(function() {
controller.set('saving', false);
route.transitionTo('user.index');
}, function() {
// model failed to save
controller.set('saving', false);
alert(I18n.t('generic_error'));
});
}
}
});
/**
The route for editing a user's email
@class PreferencesEmailRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newEmail: model.get('email') });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});
/**
The route for updating a user's username
@class PreferencesUsernameRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
return this.render({ into: 'user', outlet: 'userOutlet' });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
exit: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
setupController: function(controller, user) {
controller.setProperties({ model: user, newUsername: user.get('username') });
}
});

View File

@ -1,21 +0,0 @@
/**
This route shows who a user has invited
@class UserInvitedRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserInvitedRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
model: function() {
return Discourse.InviteList.findInvitedBy(this.modelFor('user'));
}
});

View File

@ -1,56 +0,0 @@
/**
Handles routes related to users.
@class UserRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserRoute = Discourse.Route.extend({
model: function(params) {
// If we're viewing the currently logged in user, return that object
// instead.
var currentUser = Discourse.User.current();
if (currentUser && (params.username.toLowerCase() === currentUser.get('username_lower'))) {
return currentUser;
}
return Discourse.User.create({username: params.username});
},
afterModel: function() {
return this.modelFor('user').findDetails();
},
serialize: function(params) {
if (!params) return {};
return { username: Em.get(params, 'username').toLowerCase() };
},
setupController: function(controller, user) {
controller.set('model', user);
// Add a search context
this.controllerFor('search').set('searchContext', user.get('searchContext'));
},
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'));
// Remove the search context
this.controllerFor('search').set('searchContext', null);
}
});

View File

@ -0,0 +1,192 @@
/**
Handles routes related to users.
@class UserRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserRoute = Discourse.Route.extend({
model: function(params) {
// If we're viewing the currently logged in user, return that object
// instead.
var currentUser = Discourse.User.current();
if (currentUser && (params.username.toLowerCase() === currentUser.get('username_lower'))) {
return currentUser;
}
return Discourse.User.create({username: params.username});
},
afterModel: function() {
return this.modelFor('user').findDetails();
},
serialize: function(params) {
if (!params) return {};
return { username: Em.get(params, 'username').toLowerCase() };
},
setupController: function(controller, user) {
controller.set('model', user);
// Add a search context
this.controllerFor('search').set('searchContext', user.get('searchContext'));
},
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'));
// Remove the search context
this.controllerFor('search').set('searchContext', null);
}
});
/**
This route shows who a user has invited
@class UserInvitedRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserInvitedRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
model: function() {
return Discourse.InviteList.findInvitedBy(this.modelFor('user'));
}
});
/**
The base route for showing a user's activity
@class UserActivityRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserActivityRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('user_activity', {into: 'user', outlet: 'userOutlet' });
},
model: function() {
return this.modelFor('user');
},
setupController: function(controller, user) {
this.controllerFor('userActivity').set('model', user);
var composerController = this.controllerFor('composer');
controller.set('model', user);
if (Discourse.User.current()) {
Discourse.Draft.get('new_private_message').then(function(data) {
if (data.draft) {
composerController.open({
draft: data.draft,
draftKey: 'new_private_message',
ignoreIfChanged: true,
draftSequence: data.draft_sequence
});
}
});
}
}
});
Discourse.UserPrivateMessagesRoute = Discourse.UserActivityRoute.extend({});
/**
If we request /user/eviltrout without a sub route.
@class UserIndexRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserIndexRoute = Discourse.UserActivityRoute.extend({
redirect: function() {
this.transitionTo('userActivity', this.modelFor('user'));
}
});
/**
The base route for showing an activity stream.
@class UserActivityStreamRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserActivityStreamRoute = Discourse.Route.extend({
model: function() {
return this.modelFor('user').findStream(this.get('userActionType'));
},
renderTemplate: function() {
this.render('user_stream', {into: 'user_activity', outlet: 'activity'});
},
setupController: function(controller, model) {
controller.set('model', model);
this.controllerFor('user_activity').set('userActionType', this.get('userActionType'));
}
});
// Build all activity stream routes
['bookmarks', 'edits', 'likes_given', 'likes_received', 'replies', 'posts', 'index'].forEach(function (userAction) {
Discourse["UserActivity" + userAction.classify() + "Route"] = Discourse.UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES[userAction]
});
});
Discourse.UserPrivateMessagesIndexRoute = Discourse.UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_received
});
Discourse.UserPrivateMessagesSentRoute = Discourse.UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_sent
});
//Discourse.UserTopicsListView = Em.View.extend({ templateName: 'user/topics_list' });
Discourse.UserTopicListRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('paginated_topic_list', {into: 'user_activity', outlet: 'activity'});
},
setupController: function(controller, model) {
this.controllerFor('user_activity').set('userActionType', this.get('userActionType'));
controller.set('model', model);
}
});
Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.topics,
model: function() {
return Discourse.TopicList.find('topics/created-by/' + this.modelFor('user').get('username_lower'));
}
});
Discourse.UserActivityFavoritesRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.favorites,
model: function() {
return Discourse.TopicList.find('favorited');
}
});

View File

@ -1,4 +1,3 @@
{{#with view.content}}
<div class='row'>
<div class='topic-meta-data span2'>
<div class='contents'>
@ -17,4 +16,3 @@
{{#unless view.previousPost}}<a href='{{unbound url}}' class="arrow" title="{{i18n topic.jump_reply_down}}"><i class='icon icon-arrow-down'></i></a>{{/unless}}
</div>
</div>
{{/with}}

View File

@ -0,0 +1,44 @@
<div id='list-controls'>
<div class="container">
<ul class="nav nav-pills" id='category-filter'>
{{each availableNavItems itemViewClass="Discourse.NavItemView"}}
</ul>
{{#if canCreateTopic}}
<button class='btn btn-default' {{action createTopic}}><i class='icon icon-plus'></i>{{createTopicText}}</button>
{{/if}}
{{#if canEditCategory}}
<button class='btn btn-default' {{action editCategory category}}>{{i18n category.edit_long}}</button>
{{/if}}
{{#if canCreateCategory}}
<button class='btn btn-default' {{action createCategory}}><i class='icon icon-plus'></i>{{i18n category.create}}</button>
{{/if}}
</div>
</div>
<div class="container">
<div class="row">
<div class="full-width">
<div id='list-area'>
{{#if loading}}
<div class='contents loading'>
<table id='topic-list'>
<tr>
<td colspan='8'>
<div class='spinner'>{{i18n loading}}</div>
</td>
</tr>
</table>
</div>
{{/if}}
{{outlet listView}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,55 @@
<table id="topic-list">
<tr>
<th>
{{i18n topic.title}}
</th>
<th>{{i18n category_title}}</th>
<th class='num posts'>{{i18n posts}}</th>
<th class='num likes'>{{i18n likes}}</th>
<th class='num views'>{{i18n views}}</th>
<th class='num activity' colspan='2'>{{i18n activity}}</th>
</tr>
{{#group}}
{{#collection contentBinding="view.topics" tagName="tbody" itemTagName="tr"}}
<td class='main-link'>
<a class='title' href="{{unbound lastReadUrl}}">{{{unbound fancy_title}}}</a>
{{#if unread}}
<a href="{{unbound lastReadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound unread}}</a>
{{/if}}
{{#if new_posts}}
<a href="{{unbound lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound new_posts}}</a>
{{/if}}
{{#if unseen}}
<a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}}
</td>
<td class='category'>
{{categoryLink category}}
</td>
<td class='num posts'><a href="{{lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'>
{{#if like_count}}
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}'>{{like_count}} <i class='icon-heart'></i></a>
{{/if}}
</td>
<td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td>
{{#if bumped}}
<td class='num activity'>
<a href="{{url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a>
</td>
<td class='num activity last'>
<a href="{{lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a>
</td>
{{else}}
<td class='num activity'>
<a href="{{url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a>
</td>
<td class="activity"></td>
{{/if}}
{{/collection}}
{{/group}}
</table>

View File

@ -5,7 +5,7 @@
</ul>
{{#if canCreateTopic}}
<button class='btn btn-default' {{action createTopic}}><i class='icon icon-plus'></i>{{view.createTopicText}}</button>
<button class='btn btn-default' {{action createTopic}}><i class='icon icon-plus'></i>{{createTopicText}}</button>
{{/if}}
{{#if canEditCategory}}
@ -21,7 +21,6 @@
<div class="container">
<div class="row">
<div class="full-width">
<div id='list-area'>
{{#if loading}}
@ -39,7 +38,6 @@
{{outlet listView}}
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,3 @@
{{#with view.content}}
<a href='{{unbound url}}'>
<span class='badge-category' style="background-color: #{{unbound color}}; color: #{{unbound text_color}};">{{unbound title}}</span>
</a>
{{/with}}

View File

@ -1,6 +1,3 @@
{{#with view.content}}
<a href='{{unbound url}}'>
{{unbound title}}
</a>
{{/with}}

View File

@ -1,7 +1,4 @@
{{#with view.content}}
<a href='{{unbound url}}'>
{{avatar this usernamePath="title" imageSize="small"}}
{{unbound title}}
</a>
{{/with}}

View File

@ -1,43 +0,0 @@
{{#with view.content}}
{{#group}}
<td class='main-link'>
<a class='title' href="{{unbound lastReadUrl}}">{{{unbound fancy_title}}}</a>
{{#if unread}}
<a href="{{unbound lastReadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound unread}}</a>
{{/if}}
{{#if new_posts}}
<a href="{{unbound lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new_posts count="new_posts"}}'>{{unbound new_posts}}</a>
{{/if}}
{{#if unseen}}
<a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}}
</td>
<td class='category'>
{{categoryLink category}}
</td>
<td class='num posts'><a href="{{lastReadUrl}}" class='badge-posts'>{{number posts_count numberKey="posts_long"}}</a></td>
<td class='num likes'>
{{#if like_count}}
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}'>{{like_count}} <i class='icon-heart'></i></a>
{{/if}}
</td>
<td {{bindAttr class=":num :views viewsHeat"}}>{{number views numberKey="views_long"}}</td>
{{#if bumped}}
<td class='num activity'>
<a href="{{url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a>
</td>
<td class='num activity last'>
<a href="{{lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a>
</td>
{{else}}
<td class='num activity'>
<a href="{{url}}" class='age' title='{{i18n first_post}}: {{{unboundDate created_at}}}'>{{unboundAge created_at}}</a>
</td>
<td class="activity"></td>
{{/if}}
{{/group}}
{{/with}}

View File

@ -74,24 +74,9 @@
{{#if details.suggested_topics.length}}
<div id='suggested-topics'>
<h3>{{i18n suggested_topics.title}}</h3>
<div class='topics'>
<table id="topic-list">
<tr>
<th>
{{i18n topic.title}}
</th>
<th>{{i18n category_title}}</th>
<th class='num posts'>{{i18n posts}}</th>
<th class='num likes'>{{i18n likes}}</th>
<th class='num views'>{{i18n views}}</th>
<th class='num activity' colspan='2'>{{i18n activity}}</th>
</tr>
{{each details.suggested_topics itemTagName="tr" itemViewClass="Discourse.SuggestedTopicView"}}
</table>
{{basicTopicList topics=details.suggested_topics}}
</div>
<br/>
<h3>{{{view.browseMoreMessage}}}</h3>

View File

@ -57,6 +57,7 @@
{{/if}}
</div>
<div id='user-activity'>
{{outlet activity}}
</div>

View File

@ -1,4 +1,3 @@
{{#with view.content}}
<div {{bindAttr class=":item hidden deleted moderator_action"}}>
<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>
@ -20,5 +19,3 @@
</div>
{{/each}}
</div>
{{/with}}

View File

@ -10,6 +10,11 @@ Discourse.EmbeddedPostView = Discourse.View.extend({
templateName: 'embedded_post',
classNames: ['reply'],
init: function() {
this._super();
this.set('context', this.get('content'));
},
didInsertElement: function() {
Discourse.ScreenTrack.instance().track(this.get('elementId'), this.get('post.post_number'));
},

View File

@ -0,0 +1,12 @@
/**
This view is used for rendering a basic list of topics.
@class BasicTopicListView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.BasicTopicListView = Discourse.View.extend({
templateName: 'list/basic_topic_list'
});
Discourse.View.registerHelper('basicTopicList', Discourse.BasicTopicListView);

View File

@ -4,34 +4,24 @@
@class ListTopicsView
@extends Discourse.View
@namespace Discourse
@uses Discourse.Scrolling
@uses Discourse.LoadMore
@module Discourse
**/
Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
Discourse.ListTopicsView = Discourse.View.extend(Discourse.LoadMore, {
templateName: 'list/topics',
categoryBinding: 'controller.controllers.list.category',
canCreateTopicBinding: 'controller.controllers.list.canCreateTopic',
listBinding: 'controller.model',
loadedMore: false,
currentTopicId: null,
eyelineSelector: '.topic-list-item',
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),
willDestroyElement: function() {
this.unbindScrolling();
},
didInsertElement: function() {
this.bindScrolling();
var eyeline = new Discourse.Eyeline('.topic-list-item');
var listTopicsView = this;
eyeline.on('sawBottom', function() {
listTopicsView.loadMore();
});
this._super();
var scrollPos = Discourse.get('transient.topicListScrollPos');
if (scrollPos) {
Em.run.schedule('afterRender', function() {
@ -42,15 +32,10 @@ Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
$('html, body').scrollTop(0);
});
}
this.set('eyeline', eyeline);
},
showTable: function() {
var topics = this.get('list.topics');
if(topics) {
return this.get('list.topics').length > 0 || this.get('topicTrackingState.hasIncoming');
}
}.property('list.topics.@each','topicTrackingState.hasIncoming'),
hasTopics: Em.computed.gt('list.topics.length', 0),
showTable: Em.computed.or('hasTopics', 'topicTrackingState.hasIncoming'),
updateTitle: function(){
Discourse.notifyTitle(this.get('topicTrackingState.incomingCount'));
@ -76,9 +61,8 @@ Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
// When the topic list is scrolled
scrolled: function(e) {
this._super();
this.saveScrollPos();
var eyeline = this.get('eyeline');
if (eyeline) { eyeline.update(); }
}

View File

@ -1,32 +0,0 @@
/**
This view handles the rendering of a list
@class ListView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.ListView = Discourse.View.extend({
templateName: 'list/list',
composeViewBinding: Ember.Binding.oneWay('Discourse.composeView'),
// The window has been scrolled
scrolled: function(e) {
var currentView;
currentView = this.get('container.currentView');
return currentView ? typeof currentView.scrolled === "function" ? currentView.scrolled(e) : void 0 : void 0;
},
createTopicText: function() {
if (this.get('controller.category.name')) {
return I18n.t("topic.create_in", {
categoryName: this.get('controller.category.name')
});
} else {
return I18n.t("topic.create");
}
}.property('controller.category.name')
});

View File

@ -0,0 +1,20 @@
/**
This view is used for rendering a basic list of topics.
@class PaginatedTopicListView
@extends Discourse.View
@namespace Discourse
@uses Discourse.LoadMore
@module Discourse
**/
Discourse.PaginatedTopicListView = Discourse.BasicTopicListView.extend(Discourse.LoadMore, {
topics: Em.computed.alias('controller.model.topics'),
classNames: ['paginated-topics-list'],
eyelineSelector: '.paginated-topics-list #topic-list tr',
loadMore: function() {
this.get('controller.model').loadMoreTopics();
}
});

View File

@ -12,9 +12,7 @@ Discourse.TopicListItemView = Discourse.View.extend({
classNameBindings: ['content.archived', ':topic-list-item', 'content.hasExcerpt:has-excerpt'],
attributeBindings: ['data-topic-id'],
'data-topic-id': function() {
return this.get('content.id');
}.property('content.id'),
'data-topic-id': Em.computed.alias('content.id'),
init: function() {
this._super();

View File

@ -10,17 +10,14 @@ Discourse.SearchResultsTypeView = Ember.CollectionView.extend({
tagName: 'ul',
itemViewClass: Ember.View.extend({
tagName: 'li',
classNameBindings: ['selectedClass'],
classNameBindings: ['selected'],
templateName: Discourse.computed.fmt('parentView.type', "search/%@_result"),
selected: Discourse.computed.propertyEqual('content.index', 'controller.selectedIndex'),
templateName: function() {
return "search/" + (this.get('parentView.type')) + "_result";
}.property('parentView.type'),
// Is this row currently selected by the keyboard?
selectedClass: function() {
if (this.get('content.index') === this.get('controller.selectedIndex')) return 'selected';
return null;
}.property('controller.selectedIndex')
init: function() {
this._super();
this.set('context', this.get('content'));
}
})
});

View File

@ -1,13 +0,0 @@
/**
This view is used for rendering a suggested topic
@class SuggestedTopicView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.SuggestedTopicView = Discourse.View.extend({
templateName: 'suggested_topic'
});

View File

@ -42,7 +42,7 @@ Discourse.ActivityFilterView = Discourse.View.extend({
url: function() {
var section = this.get('content.isPM') ? "/private-messages" : "/activity";
return "/users/" + this.get('user.username_lower') + section + this.get('typeKey');
}.property('typeKey'),
}.property('typeKey', 'user.username_lower'),
description: function() {
return this.get('content.description') || I18n.t("user.filters.all");

View File

@ -4,45 +4,32 @@
@class UserStreamView
@extends Discourse.View
@namespace Discourse
@uses Discourse.Scrolling
@uses Discourse.LoadMore
@module Discourse
**/
Discourse.UserStreamView = Ember.CollectionView.extend(Discourse.Scrolling, {
Discourse.UserStreamView = Ember.CollectionView.extend(Discourse.LoadMore, {
loading: false,
elementId: 'user-stream',
content: Em.computed.alias('controller.model.content'),
itemViewClass: Ember.View.extend({ templateName: 'user/stream_item' }),
eyelineSelector: '#user-activity .user-stream .item',
classNames: ['user-stream'],
scrolled: function(e) {
var eyeline = this.get('eyeline');
if (eyeline) { eyeline.update(); }
},
itemViewClass: Ember.View.extend({
templateName: 'user/stream_item',
init: function() {
this._super();
this.set('context', this.get('content'));
}
}),
loadMore: function() {
var userStreamView = this;
if (userStreamView.get('loading')) { return; }
var stream = this.get('stream');
var stream = this.get('controller.model');
stream.findItems().then(function() {
userStreamView.set('loading', false);
userStreamView.get('eyeline').flushRest();
});
},
willDestroyElement: function() {
this.unbindScrolling();
},
didInsertElement: function() {
this.bindScrolling();
var eyeline = new Discourse.Eyeline('#user-stream .item');
this.set('eyeline', eyeline);
var userStreamView = this;
eyeline.on('sawBottom', function() {
userStreamView.loadMore();
});
}
});

View File

@ -8,6 +8,7 @@
//= require ./pagedown_custom.js
// Stuff we need to load first
//= require ./discourse/mixins/scrolling
//= require_tree ./discourse/mixins
//= require ./discourse/components/computed
//= require ./discourse/views/view

View File

@ -282,6 +282,11 @@
height: 20px;
}
#topic-list {
th {
background-color: $topic-list-th-background-color;
}
}
#suggested-topics {
margin: 40px 0 40px 20px;
@ -294,9 +299,6 @@
color: darken($darkish_gray, 20%);
margin-bottom: 10px;
}
th {
background-color: $topic-list-th-background-color;
}
}
#topic-footer-buttons {

View File

@ -237,11 +237,12 @@
}
}
#user-stream {
#user-activity {
width: 840px;
float: left;
margin-bottom: 50px;
.user-stream {
.excerpt {
margin: 5px 0px;
font-size: 13px;
@ -290,9 +291,10 @@
font-size: 14px;
}
}
}
// styling of bottom section
#user-stream .child-actions {
.user-stream .child-actions {
margin-top: 8px;
.avatar-link {
float: none;
@ -312,12 +314,12 @@
}
@include medium-width {
#user-stream {
#user-activity {
width: 725px;
}
}
@include small-width {
#user-stream {
#user-activity {
width: 680px;
}
}

View File

@ -1,6 +1,6 @@
class ListController < ApplicationController
before_filter :ensure_logged_in, except: [:latest, :hot, :category, :category_feed, :latest_feed, :hot_feed]
before_filter :ensure_logged_in, except: [:latest, :hot, :category, :category_feed, :latest_feed, :hot_feed, :topics_by]
before_filter :set_category, only: [:category, :category_feed]
skip_before_filter :check_xhr
@ -28,6 +28,14 @@ class ListController < ApplicationController
end
end
def topics_by
list_opts = build_topic_list_options
list = TopicQuery.new(current_user, list_opts).list_topics_by(fetch_user_from_params)
list.more_topics_url = url_for(topics_by_path(list_opts.merge(format: 'json', page: next_page)))
respond(list)
end
def category
query = TopicQuery.new(current_user, page: params[:page])

View File

@ -201,6 +201,7 @@ Discourse::Application.routes.draw do
post 't' => 'topics#create'
post 'topics/timings'
get 'topics/similar_to'
get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT}
# Legacy route for old avatars
get 'threads/:topic_id/:post_number/avatar' => 'topics#avatar', constraints: {topic_id: /\d+/, post_number: /\d+/}

View File

@ -8,7 +8,7 @@ task 'integration:create_fixtures' => :environment do
topic: ["/t/280.json"],
user: ["/users/eviltrout.json",
"/user_actions.json?offset=0&username=eviltrout",
"/user_actions.json?offset=0&username=eviltrout&filter=4",
"/topics/created-by/eviltrout.json",
"/user_actions.json?offset=0&username=eviltrout&filter=5",
"/user_actions.json?offset=0&username=eviltrout&filter=6,7,9",
"/user_actions.json?offset=0&username=eviltrout&filter=1",

View File

@ -129,6 +129,14 @@ class TopicQuery
create_list(:posted) {|l| l.where('tu.user_id IS NOT NULL') }
end
def list_topics_by(user)
Rails.logger.info ">>> #{user.id}"
create_list(:user_topics) do |topics|
topics.where(user_id: user.id)
end
end
def list_uncategorized
create_list(:uncategorized, unordered: true) do |list|
list = list.where(category_id: nil)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ test("Activity Streams", function() {
var streamTest = function(url) {
visit(url).then(function() {
ok(exists(".user-heading"), "The heading is rendered");
ok(exists("#user-stream"), "The stream is rendered");
ok(exists("#user-activity"), "The activity is rendered");
});
};