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,11 +1,9 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
<div class="span11">
<label>
{{view Ember.Checkbox checkedBinding="enabled" value="true"}}
{{unbound description}}
</label>
</div>
{{/with}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
<div class="span11">
<label>
{{view Ember.Checkbox checkedBinding="enabled" value="true"}}
{{unbound description}}
</label>
</div>

View File

@@ -1,19 +1,17 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
<div class="span11">
{{combobox valueAttribute="value" content=validValues value=value none=allowsNone}}
<div class='desc'>{{unbound description}}</div>
</div>
{{#if dirty}}
<div class='span3'>
<button class='btn ok' {{action save this}}><i class='icon-ok'></i></button>
<button class='btn cancel' {{action cancel this}}><i class='icon-remove'></i></button>
</div>
<div class="span11">
{{combobox valueAttribute="value" content=validValues value=value none=allowsNone}}
<div class='desc'>{{unbound description}}</div>
</div>
{{#if dirty}}
<div class='span3'>
<button class='btn ok' {{action save this}}><i class='icon-ok'></i></button>
<button class='btn cancel' {{action cancel this}}><i class='icon-remove'></i></button>
</div>
{{else}}
{{#if overridden}}
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{else}}
{{#if overridden}}
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{/with}}
{{/if}}

View File

@@ -1,19 +1,17 @@
{{#with view.content}}
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
<div class='span4 offset1'>
<h3>{{unbound setting}}</h3>
</div>
<div class="span11">
{{textField value=value classNames="input-xxlarge"}}
<div class='desc'>{{unbound description}}</div>
</div>
{{#if dirty}}
<div class='span3'>
<button class='btn ok' {{action save this}}><i class='icon-ok'></i></button>
<button class='btn cancel' {{action cancel this}}><i class='icon-remove'></i></button>
</div>
<div class="span11">
{{textField value=value classNames="input-xxlarge"}}
<div class='desc'>{{unbound description}}</div>
</div>
{{#if dirty}}
<div class='span3'>
<button class='btn ok' {{action save this}}><i class='icon-ok'></i></button>
<button class='btn cancel' {{action cancel this}}><i class='icon-remove'></i></button>
</div>
{{else}}
{{#if overridden}}
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{else}}
{{#if overridden}}
<button class='btn' href='#' {{action resetDefault this}}>{{i18n admin.site_settings.reset}}</button>
{{/if}}
{{/with}}
{{/if}}

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;
}
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,20 +1,18 @@
{{#with view.content}}
<div class='row'>
<div class='topic-meta-data span2'>
<div class='contents'>
<div>
<a href='/users/{{unbound username}}'>{{avatar this imageSize="small"}}</a>
</div>
<h5 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h5>
<div class='row'>
<div class='topic-meta-data span2'>
<div class='contents'>
<div>
<a href='/users/{{unbound username}}'>{{avatar this imageSize="small"}}</a>
</div>
</div>
<div class='span11 topic-body'>
<div class="topic-meta-data-inside">
<div class='post-info post-date'>{{unboundAgeWithTooltip created_at}}</div>
{{#if view.previousPost}}<a href='{{unbound url}}' class="post-info arrow" title="{{i18n topic.jump_reply_up}}"><i class='icon icon-arrow-up'></i></a>{{/if}}
</div>
{{{unbound cooked}}}
{{#unless view.previousPost}}<a href='{{unbound url}}' class="arrow" title="{{i18n topic.jump_reply_down}}"><i class='icon icon-arrow-down'></i></a>{{/unless}}
<h5 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h5>
</div>
</div>
{{/with}}
<div class='span11 topic-body'>
<div class="topic-meta-data-inside">
<div class='post-info post-date'>{{unboundAgeWithTooltip created_at}}</div>
{{#if view.previousPost}}<a href='{{unbound url}}' class="post-info arrow" title="{{i18n topic.jump_reply_up}}"><i class='icon icon-arrow-up'></i></a>{{/if}}
</div>
{{{unbound cooked}}}
{{#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>

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}}
<a href='{{unbound url}}'>
<span class='badge-category' style="background-color: #{{unbound color}}; color: #{{unbound text_color}};">{{unbound title}}</span>
</a>

View File

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

View File

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

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>
{{outlet activity}}
<div id='user-activity'>
{{outlet activity}}
</div>

View File

@@ -1,24 +1,21 @@
{{#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>
<span class='time'>{{date path="created_at" leaveAgo="true"}}</span>
<span class="title">
<a href="{{unbound postUrl}}">{{unbound title}}</a>
</span>
<span class="type">{{unbound descriptionHtml}}</span>
</div>
<p class='excerpt'>
{{{unbound excerpt}}}
</p>
{{#each children}}
<div class='child-actions'>
<i class="icon {{unbound icon}}"></i>
{{#each items}}
<a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
{{/each}}
</div>
{{/each}}
<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>
<span class='time'>{{date path="created_at" leaveAgo="true"}}</span>
<span class="title">
<a href="{{unbound postUrl}}">{{unbound title}}</a>
</span>
<span class="type">{{unbound descriptionHtml}}</span>
</div>
{{/with}}
<p class='excerpt'>
{{{unbound excerpt}}}
</p>
{{#each children}}
<div class='child-actions'>
<i class="icon {{unbound icon}}"></i>
{{#each items}}
<a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
{{/each}}
</div>
{{/each}}
</div>

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