FEATURE: User page refactor

Re-organise user page so it is easier to find interesting info
split it into tabs

- Introduce notifications and messages tabs
- Stop couting stuff for the user page to speed up rendering
- Suppress more information when viewing your own profile
This commit is contained in:
Sam 2015-12-17 18:06:04 +11:00
parent c3f08145b8
commit a8b5192efd
38 changed files with 371 additions and 194 deletions

View File

@ -0,0 +1,25 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
pmView: false,
privateMessagesActive: Em.computed.equal('pmView', 'index'),
privateMessagesMineActive: Em.computed.equal('pmView', 'mine'),
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'),
isGroup: Em.computed.equal('pmView', 'groups'),
@computed('model.groups', 'groupFilter', 'pmView')
groupPMStats(groups, filter, pmView) {
if (groups) {
return groups.filter(group => group.has_messages)
.map(g => {
return {
name: g.name,
active: (g.name === filter && pmView === "groups")
};
});
}
}
});

View File

@ -6,7 +6,6 @@ import User from 'discourse/models/user';
export default Ember.Controller.extend(CanCheckEmails, { export default Ember.Controller.extend(CanCheckEmails, {
indexStream: false, indexStream: false,
pmView: false,
userActionType: null, userActionType: null,
needs: ['user-notifications', 'user-topics-list'], needs: ['user-notifications', 'user-topics-list'],
@ -28,11 +27,14 @@ export default Ember.Controller.extend(CanCheckEmails, {
}, },
@computed('viewingSelf', 'currentUser.admin') @computed('viewingSelf', 'currentUser.admin')
canSeePrivateMessages(viewingSelf, isAdmin) { showPrivateMessages(viewingSelf, isAdmin) {
return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin); return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin);
}, },
canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'), @computed('viewingSelf', 'currentUser.admin')
canSeeNotificationHistory(viewingSelf, isAdmin) {
return viewingSelf || isAdmin;
},
@computed("content.badge_count") @computed("content.badge_count")
showBadges(badgeCount) { showBadges(badgeCount) {
@ -45,6 +47,12 @@ export default Ember.Controller.extend(CanCheckEmails, {
(userActionType === UserAction.TYPES.messages_received); (userActionType === UserAction.TYPES.messages_received);
}, },
@computed("indexStream", "userActionType")
showActionTypeSummary(indexStream,userActionType, showPMs) {
return (indexStream || userActionType) && !showPMs;
},
@computed() @computed()
canInviteToForum() { canInviteToForum() {
return User.currentProp('can_invite_to_forum'); return User.currentProp('can_invite_to_forum');
@ -64,23 +72,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
} }
}, },
privateMessagesActive: Em.computed.equal('pmView', 'index'),
privateMessagesMineActive: Em.computed.equal('pmView', 'mine'),
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'),
@computed('model.private_messages_stats.groups', 'groupFilter', 'pmView')
groupPMStats(stats,filter,pmView) {
if (stats) {
return stats.map(g => {
return {
name: g.name,
count: g.count,
active: (g.name === filter && pmView === 'groups')
};
});
}
},
actions: { actions: {
expandProfile() { expandProfile() {
this.set('forceExpand', true); this.set('forceExpand', true);

View File

@ -3,4 +3,5 @@ export default {
this.controllerFor('user').set('userActionType', userActionType); this.controllerFor('user').set('userActionType', userActionType);
this.controllerFor('user-activity').set('userActionType', userActionType); this.controllerFor('user-activity').set('userActionType', userActionType);
} }
}; };

View File

@ -199,6 +199,15 @@ const User = RestModel.extend({
ua.action_type === UserAction.TYPES.topics; ua.action_type === UserAction.TYPES.topics;
}, },
@computed("groups.@each")
displayGroups() {
const groups = this.get('groups');
const filtered = groups.filter(group => {
return !group.automatic || group.name === "moderators";
});
return filtered.length === 0 ? null : filtered;
},
// The user's stat count, excluding PMs. // The user's stat count, excluding PMs.
@computed("statsExcludingPms.@each.count") @computed("statsExcludingPms.@each.count")
statsCountNonPM() { statsCountNonPM() {
@ -233,8 +242,8 @@ const User = RestModel.extend({
})); }));
} }
if (!Em.isEmpty(json.user.custom_groups)) { if (!Em.isEmpty(json.user.groups)) {
json.user.custom_groups = json.user.custom_groups.map(g => Group.create(g)); json.user.groups = json.user.groups.map(g => Group.create(g));
} }
if (json.user.invited_by) { if (json.user.invited_by) {

View File

@ -58,13 +58,20 @@ export default function() {
this.resource('users'); this.resource('users');
this.resource('user', { path: '/users/:username' }, function() { this.resource('user', { path: '/users/:username' }, function() {
this.resource('userActivity', { path: '/activity' }, function() { this.resource('userActivity', { path: '/activity' }, function() {
_.map(Discourse.UserAction.TYPES, (id, userAction) => { this.route('topics');
this.route(userAction, { path: userAction.replace('_', '-') }); this.route('replies');
}); this.route('likesGiven', {path: 'likes-given'});
this.route('bookmarks');
});
this.resource('userNotifications', {path: '/notifications'}, function(){
this.route('responses');
this.route('likesReceived', { path: 'likes-received'});
this.route('mentions');
this.route('edits');
}); });
this.route('badges'); this.route('badges');
this.route('notifications');
this.route('flaggedPosts', { path: '/flagged-posts' }); this.route('flaggedPosts', { path: '/flagged-posts' });
this.route('deletedPosts', { path: '/deleted-posts' }); this.route('deletedPosts', { path: '/deleted-posts' });
@ -85,6 +92,7 @@ export default function() {
this.resource('userInvited', { path: '/invited' }, function() { this.resource('userInvited', { path: '/invited' }, function() {
this.route('show', { path: '/:filter' }); this.route('show', { path: '/:filter' });
}); });
}); });
this.route('signup', {path: '/signup'}); this.route('signup', {path: '/signup'});

View File

@ -24,7 +24,7 @@ export default (viewName, path) => {
showParticipants: true showParticipants: true
}); });
this.controllerFor("user").set("pmView", viewName); this.controllerFor("userPrivateMessages").set("pmView", viewName);
this.searchService.set('contextType', 'private_messages'); this.searchService.set('contextType', 'private_messages');
}, },

View File

@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
import UserAction from "discourse/models/user-action"; import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({ export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["replies"] userActionType: UserAction.TYPES["posts"]
}); });

View File

@ -1,5 +1,3 @@
import Draft from 'discourse/models/draft';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
model() { model() {
return this.modelFor("user"); return this.modelFor("user");
@ -7,21 +5,5 @@ export default Discourse.Route.extend({
setupController(controller, user) { setupController(controller, user) {
this.controllerFor("user-activity").set("model", user); this.controllerFor("user-activity").set("model", user);
// Bring up a draft
const composerController = this.controllerFor("composer");
controller.set("model", user);
if (this.currentUser) {
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
});
}
});
}
} }
}); });

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
renderTemplate() {
this.render("user/notifications-index");
}
});

View File

@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
import UserAction from "discourse/models/user-action"; import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({ export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["likes_received"] userActionType: UserAction.TYPES["likes_received"],
}); });

View File

@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
import UserAction from "discourse/models/user-action"; import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({ export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["posts"] userActionType: UserAction.TYPES["replies"]
}); });

View File

@ -1,6 +1,11 @@
import ViewingActionType from "discourse/mixins/viewing-action-type"; import ViewingActionType from "discourse/mixins/viewing-action-type";
export default Discourse.Route.extend(ViewingActionType, { export default Discourse.Route.extend(ViewingActionType, {
renderTemplate() {
this.render('user/notifications');
},
actions: { actions: {
didTransition() { didTransition() {
this.controllerFor("user-notifications")._showFooter(); this.controllerFor("user-notifications")._showFooter();

View File

@ -1,5 +1,5 @@
import Group from 'discourse/models/group'; import Group from 'discourse/models/group';
import createPMRoute from "discourse/routes/build-user-topic-list-route"; import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('groups', 'private-messages-groups').extend({ export default createPMRoute('groups', 'private-messages-groups').extend({
model(params) { model(params) {
@ -13,13 +13,13 @@ export default createPMRoute('groups', 'private-messages-groups').extend({
const groupName = _.last(model.get("filter").split('/')); const groupName = _.last(model.get("filter").split('/'));
Group.findAll().then(groups => { Group.findAll().then(groups => {
const group = _.first(groups.filterBy("name", groupName)); const group = _.first(groups.filterBy("name", groupName));
this.controllerFor("user-topics-list").set("group", group); this.controllerFor("user-private-messages").set("group", group);
}); });
}, },
setupController(controller, model) { setupController(controller, model) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
const group = _.last(model.get("filter").split('/')); const group = _.last(model.get("filter").split('/'));
this.controllerFor("user").set("groupFilter", group); this.controllerFor("userPrivateMessages").set("groupFilter", group);
} }
}); });

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-user-topic-list-route"; import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('index', 'private-messages'); export default createPMRoute('index', 'private-messages');

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-user-topic-list-route"; import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('mine', 'private-messages-sent'); export default createPMRoute('mine', 'private-messages-sent');

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-user-topic-list-route"; import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('unread', 'private-messages-unread'); export default createPMRoute('unread', 'private-messages-unread');

View File

@ -1,6 +1,35 @@
import UserActivityRoute from 'discourse/routes/user-activity'; import Draft from 'discourse/models/draft';
export default Discourse.Route.extend({
renderTemplate() {
this.render('user/messages');
},
model() {
return this.modelFor("user");
},
setupController(controller, user) {
this._super();
// Bring up a draft
const composerController = this.controllerFor("composer");
controller.set("model", user);
if (this.currentUser) {
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
});
}
});
}
},
export default UserActivityRoute.extend({
actions: { actions: {
willTransition: function() { willTransition: function() {
this._super(); this._super();

View File

@ -0,0 +1,2 @@
export default Discourse.Route.extend({
});

View File

@ -1,11 +1,3 @@
<div class="clearfix">
{{#if group}}
{{group-notifications-button group=group}}
{{/if}}
{{#if showNewPM}}
{{d-button class="btn-primary pull-right new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
{{/if}}
</div>
{{basic-topic-list topicList=model {{basic-topic-list topicList=model
hideCategory=hideCategory hideCategory=hideCategory

View File

@ -1 +1,42 @@
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
<li class='no-glyph'>
{{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
</li>
<li class='no-glyph'>
{{#link-to 'userActivity.topics'}}{{i18n 'user_action_groups.4'}}{{/link-to}}
</li>
<li>
{{#link-to 'userActivity.replies'}}
<i class="glyph fa fa-reply"></i>{{i18n 'user_action_groups.5'}}
{{/link-to}}
</li>
<li>
{{#link-to 'userActivity.likesGiven'}}
<i class="glyph fa fa-heart"></i>{{i18n 'user_action_groups.1'}}
{{/link-to}}
</li>
<li>
{{#link-to 'userActivity.bookmarks'}}
<i class="glyph fa fa-bookmark"></i>{{i18n 'user_action_groups.3'}}
{{/link-to}}
</li>
</ul>
{{#if viewingSelf}}
<div class='user-archive'>
{{d-button action="exportUserArchive" label="user.download_archive" icon="download"}}
</div>
{{/if}}
</section>
<section class='user-right'>
{{outlet}} {{outlet}}
</section>

View File

@ -0,0 +1,43 @@
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
<li {{bind-attr class=":noGlyph privateMessagesActive:active"}}>
{{#link-to 'userPrivateMessages.index' model}}
{{i18n 'user.messages.all'}}
{{#if model.hasPMs}}<span class='count'>({{model.private_messages_stats.all}})</span>{{/if}}
{{/link-to}}
</li>
<li {{bind-attr class=":noGlyph privateMessagesMineActive:active"}}>
{{#link-to 'userPrivateMessages.mine' model}}
{{i18n 'user.messages.mine'}}
{{#if model.hasStartedPMs}}<span class='count'>({{model.private_messages_stats.mine}})</span>{{/if}}
{{/link-to}}
</li>
<li {{bind-attr class=":noGlyph privateMessagesUnreadActive:active"}}>
{{#link-to 'userPrivateMessages.unread' model}}
{{i18n 'user.messages.unread'}}
{{#if model.hasUnreadPMs}}<span class='badge-notification unread-private-messages'>{{model.private_messages_stats.unread}}</span>{{/if}}
{{/link-to}}
</li>
{{#each groupPMStats as |group|}}
<li class="{{if group.active "active"}}">
{{#link-to 'userPrivateMessages.group' group.name}}
<i class='glyph fa fa-group'></i>
{{group.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
</section>
<section class='user-right messages'>
{{#if isGroup}}
<div class="clearfix">
{{group-notifications-button group=group}}
</div>
{{/if}}
{{outlet}}
</section>

View File

@ -0,0 +1,34 @@
{{#if model.error}}
<div class="item error">
{{#if model.forbidden}}
{{i18n 'errors.reasons.forbidden'}}
{{else}}
{{i18n 'errors.desc.unknown'}}
{{/if}}
</div>
{{/if}}
{{#if showDismissButton}}
<div class='notification-buttons'>
<button title="{{i18n 'user.dismiss_notifications_tooltip'}}" id='dismiss-notifications-top' class='btn notifications-read' {{action "resetNew"}}>{{i18n 'user.dismiss_notifications'}}</button>
</div>
{{/if}}
{{#each n in model}}
<div {{bind-attr class=":item :notification n.read::unread"}}>
{{notification-item notification=n}}
<span class="time">
{{format-date n.created_at leaveAgo="true"}}
</span>
</div>
{{/each}}
{{#conditional-loading-spinner condition=loading}}
{{#unless model.canLoadMore}}
{{#if showDismissButton}}
<div class='notification-buttons'>
<button title="{{i18n 'user.dismiss_notifications_tooltip'}}" id='dismiss-notifications' class='btn notifications-read' {{action "resetNew"}}>{{i18n 'user.dismiss_notifications'}}</button>
</div>
{{/if}}
{{/unless}}
{{/conditional-loading-spinner}}

View File

@ -1,34 +1,25 @@
{{#if model.error}}
<div class="item error">
{{#if model.forbidden}}
{{i18n 'errors.reasons.forbidden'}}
{{else}}
{{i18n 'errors.desc.unknown'}}
{{/if}}
</div>
{{/if}}
{{#if showDismissButton}} <section class='user-navigation'>
<div class='notification-buttons'> <ul class='action-list nav-stacked'>
<button title="{{i18n 'user.dismiss_notifications_tooltip'}}" id='dismiss-notifications-top' class='btn notifications-read' {{action "resetNew"}}>{{i18n 'user.dismiss_notifications'}}</button> <li class='no-glyph'>
</div> {{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
{{/if}} </li>
<li>
{{#link-to 'userNotifications.responses'}}
<i class="glyph fa fa-reply"></i>
{{i18n 'user_action_groups.6'}}
{{/link-to}}
</li>
<li>
{{#link-to 'userNotifications.likesReceived'}}
<i class="glyph fa fa-heart"></i>{{i18n 'user_action_groups.2'}}
{{/link-to}}
</li>
<li>{{#link-to 'userNotifications.mentions'}}<i class="glyph fa fa-at"></i>{{i18n 'user_action_groups.7'}}{{/link-to}}</li>
<li>{{#link-to 'userNotifications.edits'}}<i class="glyph fa fa-pencil"></i>{{i18n 'user_action_groups.11'}}{{/link-to}}</li>
</ul>
</section>
{{#each n in model}} <section class='user-right'>
<div {{bind-attr class=":item :notification n.read::unread"}}> {{outlet}}
{{notification-item notification=n}} </section>
<span class="time">
{{format-date n.created_at leaveAgo="true"}}
</span>
</div>
{{/each}}
{{#conditional-loading-spinner condition=loading}}
{{#unless model.canLoadMore}}
{{#if showDismissButton}}
<div class='notification-buttons'>
<button title="{{i18n 'user.dismiss_notifications_tooltip'}}" id='dismiss-notifications' class='btn notifications-read' {{action "resetNew"}}>{{i18n 'user.dismiss_notifications'}}</button>
</div>
{{/if}}
{{/unless}}
{{/conditional-loading-spinner}}

View File

@ -237,15 +237,8 @@
{{category-group categories=model.mutedCategories blacklist=selectedCategories}} {{category-group categories=model.mutedCategories blacklist=selectedCategories}}
</div> </div>
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div> <div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
</div> <div class="controls category-controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
<div class="control-group topics">
<label class="control-label">{{i18n 'categories.topics'}}</label>
{{#if siteSettings.automatically_unpin_topics}}
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.automatically_unpin_topics}}
{{/if}}
<div class="controls topic-controls">
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
</div> </div>
</div> </div>
@ -258,6 +251,13 @@
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div> <div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
</div> </div>
{{#if siteSettings.automatically_unpin_topics}}
<div class="control-group topics">
<label class="control-label">{{i18n 'categories.topics'}}</label>
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.automatically_unpin_topics}}
</div>
{{/if}}
{{plugin-outlet "user-custom-controls"}} {{plugin-outlet "user-custom-controls"}}
<div class="control-group"> <div class="control-group">

View File

@ -1,6 +1,7 @@
<div class="container{{if viewingSelf ' viewing-self'}}"> <div class="container{{if viewingSelf ' viewing-self'}}">
<section class='user-main'> <section class='user-main'>
<section {{bind-attr class="collapsedInfo :about model.profileBackground:has-background:no-background"}} style={{model.profileBackground}}> <section {{bind-attr class="collapsedInfo :about model.profileBackground:has-background:no-background"}} style={{model.profileBackground}}>
{{#unless collapsedInfo}}
<div class='staff-counters'> <div class='staff-counters'>
{{#if model.number_of_flags_given}} {{#if model.number_of_flags_given}}
<div><span class="helpful-flags">{{model.number_of_flags_given}}</span>&nbsp;{{i18n 'user.staff_counters.flags_given'}}</div> <div><span class="helpful-flags">{{model.number_of_flags_given}}</span>&nbsp;{{i18n 'user.staff_counters.flags_given'}}</div>
@ -26,6 +27,8 @@
<div><span class="warnings-received">{{model.number_of_warnings}}</span>&nbsp;{{i18n 'user.staff_counters.warnings_received'}}</div> <div><span class="warnings-received">{{model.number_of_warnings}}</span>&nbsp;{{i18n 'user.staff_counters.warnings_received'}}</div>
{{/if}} {{/if}}
</div> </div>
{{/unless}}
<div class='profile-image'></div> <div class='profile-image'></div>
<div class='details'> <div class='details'>
<div class='primary'> <div class='primary'>
@ -40,18 +43,9 @@
</a> </a>
</li> </li>
{{/if}} {{/if}}
{{#if viewingSelf}}
<li><a {{action "logout"}} href class='btn btn-danger'>{{fa-icon "sign-out"}}{{i18n 'user.log_out'}}</a></li>
{{/if}}
{{#if currentUser.staff}} {{#if currentUser.staff}}
<li><a href={{model.adminPath}} class="btn">{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li> <li><a href={{model.adminPath}} class="btn">{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
{{/if}} {{/if}}
{{#if model.can_edit}}
<li>{{#link-to 'preferences' class="btn"}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
{{#if canInviteToForum}}
<li>{{#link-to 'userInvited' class="btn"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
{{/if}}
{{#if collapsedInfo}} {{#if collapsedInfo}}
{{#if viewingSelf}} {{#if viewingSelf}}
<li><a {{action "expandProfile"}} href class="btn">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li> <li><a {{action "expandProfile"}} href class="btn">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li>
@ -109,6 +103,8 @@
<div style='clear: both'></div> <div style='clear: both'></div>
</div> </div>
{{#unless collapsedInfo}}
<div class='secondary'> <div class='secondary'>
<dl> <dl>
{{#if model.created_at}} {{#if model.created_at}}
@ -135,10 +131,10 @@
{{/if}} {{/if}}
</dd> </dd>
{{/if}} {{/if}}
{{#if model.custom_groups}} {{#if model.displayGroups}}
<dt>{{i18n 'groups.title' count=model.custom_groups.length}}</dt> <dt>{{i18n 'groups.title' count=model.displayGroups.length}}</dt>
<dd class='groups'> <dd class='groups'>
{{#each group in model.custom_groups}} {{#each group in model.displayGroups}}
<span>{{#link-to 'group' group class="group-link"}}{{group.name}}{{/link-to}}</span> <span>{{#link-to 'group' group class="group-link"}}{{group.name}}{{/link-to}}</span>
{{/each}} {{/each}}
</dd> </dd>
@ -149,76 +145,34 @@
</dl> </dl>
{{plugin-outlet "user-profile-secondary"}} {{plugin-outlet "user-profile-secondary"}}
</div> </div>
{{/unless}}
</section> </section>
<section class='user-navigation'> <ul class="user-nav">
<ul class='action-list nav-stacked'> <li class='selected'>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
{{activity-filter count=model.statsCountNonPM user=model userActionType=userActionType indexStream=indexStream}} {{#if canSeeNotificationHistory}}
{{#each stat in model.statsExcludingPms}} <li>
{{activity-filter content=stat user=model userActionType=userActionType indexStream=indexStream}} {{#link-to 'userNotifications'}}
{{/each}} {{fa-icon "comment" class="glyph"}}
{{#if showBadges}} {{i18n 'user.notifications'}}
{{#link-to 'user.badges' tagName="li"}}
{{#link-to 'user.badges'}}
<i class='glyph fa fa-certificate'></i>
{{i18n 'badges.title'}}
<span class='count'>({{model.badge_count}})</span>
{{/link-to}}
{{/link-to}} {{/link-to}}
{{/if}} </li>
{{#if canSeeNotificationHistory}}
{{#link-to 'user.notifications' tagName="li"}}
{{#link-to 'user.notifications'}}
{{fa-icon "comment" class="glyph"}}
{{i18n 'user.notifications'}}
{{/link-to}}
{{/link-to}}
{{/if}}
</ul>
{{#if canSeePrivateMessages}}
<h3>{{fa-icon "envelope"}} {{i18n 'user.private_messages'}}</h3>
<ul class='action-list nav-stacked'>
<li {{bind-attr class=":noGlyph privateMessagesActive:active"}}>
{{#link-to 'userPrivateMessages.index' model}}
{{i18n 'user.messages.all'}}
{{#if model.hasPMs}}<span class='count'>({{model.private_messages_stats.all}})</span>{{/if}}
{{/link-to}}
</li>
<li {{bind-attr class=":noGlyph privateMessagesMineActive:active"}}>
{{#link-to 'userPrivateMessages.mine' model}}
{{i18n 'user.messages.mine'}}
{{#if model.hasStartedPMs}}<span class='count'>({{model.private_messages_stats.mine}})</span>{{/if}}
{{/link-to}}
</li>
<li {{bind-attr class=":noGlyph privateMessagesUnreadActive:active"}}>
{{#link-to 'userPrivateMessages.unread' model}}
{{i18n 'user.messages.unread'}}
{{#if model.hasUnreadPMs}}<span class='badge-notification unread-private-messages'>{{model.private_messages_stats.unread}}</span>{{/if}}
{{/link-to}}
</li>
{{#each groupPMStats as |group|}}
<li class="{{if group.active "active"}}">
{{#link-to 'userPrivateMessages.group' group.name}}
<i class='glyph fa fa-group'></i>
{{group.name}}
<span class='count'>({{group.count}})</span>
{{/link-to}}
</li>
{{/each}}
</ul>
{{/if}} {{/if}}
{{#if showPrivateMessages}}
{{#if viewingSelf}} <li>{{#link-to 'userPrivateMessages'}}{{fa-icon "envelope-o"}}{{i18n 'user.private_messages'}}{{/link-to}}</li>
<div class='user-archive'>
{{d-button action="exportUserArchive" label="user.download_archive" icon="download"}}
</div>
{{/if}} {{/if}}
</section> {{#if canInviteToForum}}
<li>{{#link-to 'userInvited'}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
{{/if}}
{{#if showBadges}}
<li>{{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}</li>
{{/if}}
{{#if model.can_edit}}
<li>{{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
</ul>
<section class='user-right'> {{outlet}}
{{outlet}}
</section>
</section> </section>
</div> </div>

View File

@ -131,3 +131,34 @@
} }
} }
.user-nav {
width: 100%;
padding: 0;
margin: 0;
li {
padding: 0;
a {
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 40%));
padding: 5px;
min-width: 90px;
display: inline-block;
text-align: center;
border-bottom: 3px solid transparent;
}
a.active, a:hover {
color: $primary;
border-bottom: 3px solid dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 20%));
}
display: inline-block;
text-decoration: none;
margin: 5px 0px;
.fa {
margin-right: 5px;
}
.fa.fa-comment {
margin-right: 2px;
}
}
}

View File

@ -65,13 +65,13 @@
color: $primary; color: $primary;
} }
} }
.active > a, .active > a, & li > a.active
{ {
color: $secondary; color: $secondary;
background-color: $quaternary; background-color: $quaternary;
} }
.active > a::after, .active > a::after, & li > a.active::after
{ {
left: 90%; left: 90%;
top: 33%; top: 33%;

View File

@ -231,7 +231,6 @@
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
width: 100%; width: 100%;
margin-bottom: 10px;
overflow: hidden; overflow: hidden;
&.group { &.group {
@ -642,3 +641,15 @@
margin-top: 0; margin-top: 0;
} }
} }
.user-right .group-notification-menu {
float: right;
margin-bottom: 5px;
}
.user-right.messages .topic-list {
thead, th.views, td.views {
display: none;
}
}

View File

@ -33,9 +33,10 @@ class UsersController < ApplicationController
@user = fetch_user_from_params(include_inactive: current_user.try(:staff?)) @user = fetch_user_from_params(include_inactive: current_user.try(:staff?))
user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user') user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
if params[:stats].to_s == "false"
user_serializer.omit_stats = true # TODO remove this options from serializer
end user_serializer.omit_stats = true
topic_id = params[:include_post_count_for].to_i topic_id = params[:include_post_count_for].to_i
if topic_id != 0 if topic_id != 0
user_serializer.topic_post_count = {topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count } user_serializer.topic_post_count = {topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count }

View File

@ -141,10 +141,6 @@ class User < ActiveRecord::Base
SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i
end end
def custom_groups
groups.where(automatic: false, visible: true)
end
def self.username_available?(username) def self.username_available?(username)
lower = username.downcase lower = username.downcase
User.where(username_lower: lower).blank? && !SiteSetting.reserved_usernames.split("|").include?(username) User.where(username_lower: lower).blank? && !SiteSetting.reserved_usernames.split("|").include?(username)

View File

@ -11,7 +11,8 @@ class BasicGroupSerializer < ApplicationSerializer
:title, :title,
:grant_trust_level, :grant_trust_level,
:incoming_email, :incoming_email,
:notification_level :notification_level,
:has_messages
def include_incoming_email? def include_incoming_email?
scope.is_staff? scope.is_staff?

View File

@ -70,7 +70,7 @@ class UserSerializer < BasicUserSerializer
:automatically_unpin_topics :automatically_unpin_topics
has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_one :invited_by, embed: :object, serializer: BasicUserSerializer
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer
has_many :featured_user_badges, embed: :ids, serializer: UserBadgeSerializer, root: :user_badges has_many :featured_user_badges, embed: :ids, serializer: UserBadgeSerializer, root: :user_badges
has_one :card_badge, embed: :object, serializer: BadgeSerializer has_one :card_badge, embed: :object, serializer: BadgeSerializer
@ -118,6 +118,14 @@ class UserSerializer < BasicUserSerializer
### ATTRIBUTES ### ATTRIBUTES
### ###
def groups
if scope.is_admin? || object.id == scope.user.try(:id)
object.groups
else
object.groups.where(visible: true)
end
end
def include_email? def include_email?
object.id && object.id == scope.user.try(:id) object.id && object.id == scope.user.try(:id)
end end

View File

@ -378,7 +378,6 @@ en:
"6": "Responses" "6": "Responses"
"7": "Mentions" "7": "Mentions"
"9": "Quotes" "9": "Quotes"
"10": "Starred"
"11": "Edits" "11": "Edits"
"12": "Sent Items" "12": "Sent Items"
"13": "Inbox" "13": "Inbox"
@ -448,6 +447,7 @@ en:
invited_by: "Invited By" invited_by: "Invited By"
trust_level: "Trust Level" trust_level: "Trust Level"
notifications: "Notifications" notifications: "Notifications"
statistics: "Stats"
desktop_notifications: desktop_notifications:
label: "Desktop Notifications" label: "Desktop Notifications"
not_supported: "Notifications are not supported on this browser. Sorry." not_supported: "Notifications are not supported on this browser. Sorry."

View File

@ -305,6 +305,7 @@ Discourse::Application.routes.draw do
get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/notifications" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/notifications" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/notifications/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/pending" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/pending" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
delete "users/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT} delete "users/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT}
# The external_id constraint is to allow periods to be used in the value without becoming part of the format. ie: foo.bar.json # The external_id constraint is to allow periods to be used in the value without becoming part of the format. ie: foo.bar.json

View File

@ -0,0 +1,15 @@
class AddHasMessagesToGroups < ActiveRecord::Migration
def up
add_column :groups, :has_messages, :boolean, default: false, null: false
execute <<SQL
UPDATE groups g SET has_messages = true
WHERE exists(SELECT group_id FROM topic_allowed_groups WHERE group_id = g.id)
SQL
end
def down
remove_column :groups, :has_messages
end
end

View File

@ -186,6 +186,7 @@ class TopicCreator
check_can_send_permission!(topic, group) check_can_send_permission!(topic, group)
topic.topic_allowed_groups.build(group_id: group.id) topic.topic_allowed_groups.build(group_id: group.id)
len += 1 len += 1
group.update_columns(:has_messages, true) unless group.has_messages
end end
rollback_with!(topic, :target_group_not_found) unless len == names.length rollback_with!(topic, :target_group_not_found) unless len == names.length