From a8b5192efd43bcbf64ffe7ae44e5c5b7f3be0b51 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 17 Dec 2015 18:06:04 +1100
Subject: [PATCH] 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
---
 .../controllers/user-private-messages.js.es6  |  25 +++++
 .../discourse/controllers/user.js.es6         |  31 ++---
 .../mixins/viewing-action-type.js.es6         |   1 +
 .../javascripts/discourse/models/user.js.es6  |  13 ++-
 .../discourse/routes/app-route-map.js.es6     |  16 ++-
 ...s6 => build-private-messages-route.js.es6} |   2 +-
 .../routes/user-activity-replies.js.es6       |   2 +-
 .../discourse/routes/user-activity.js.es6     |  18 ---
 ...js.es6 => user-notifications-edits.js.es6} |   0
 .../routes/user-notifications-index.js.es6    |   5 +
 ... user-notifications-likes-received.js.es6} |   2 +-
 ...es6 => user-notifications-mentions.js.es6} |   0
 ...s6 => user-notifications-responses.js.es6} |   2 +-
 .../routes/user-notifications.js.es6          |   5 +
 .../routes/user-private-messages-group.js.es6 |   6 +-
 .../routes/user-private-messages-index.js.es6 |   2 +-
 .../routes/user-private-messages-mine.js.es6  |   2 +-
 .../user-private-messages-unread.js.es6       |   2 +-
 .../routes/user-private-messages.js.es6       |  33 +++++-
 .../discourse/routes/user-statistics.js.es6   |   2 +
 .../discourse/templates/user-topics-list.hbs  |   8 --
 .../discourse/templates/user/activity.hbs     |  41 +++++++
 .../discourse/templates/user/messages.hbs     |  43 +++++++
 .../templates/user/notifications-index.hbs    |  34 ++++++
 .../templates/user/notifications.hbs          |  55 ++++-----
 .../discourse/templates/user/preferences.hbs  |  18 +--
 .../discourse/templates/user/user.hbs         | 106 +++++-------------
 app/assets/stylesheets/common/base/user.scss  |  31 +++++
 .../common/components/navs.css.scss           |   4 +-
 app/assets/stylesheets/desktop/user.scss      |  13 ++-
 app/controllers/users_controller.rb           |   7 +-
 app/models/user.rb                            |   4 -
 app/serializers/basic_group_serializer.rb     |   3 +-
 app/serializers/user_serializer.rb            |  10 +-
 config/locales/client.en.yml                  |   2 +-
 config/routes.rb                              |   1 +
 ...151219045559_add_has_messages_to_groups.rb |  15 +++
 lib/topic_creator.rb                          |   1 +
 38 files changed, 371 insertions(+), 194 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
 rename app/assets/javascripts/discourse/routes/{build-user-topic-list-route.js.es6 => build-private-messages-route.js.es6} (92%)
 rename app/assets/javascripts/discourse/routes/{user-activity-edits.js.es6 => user-notifications-edits.js.es6} (100%)
 create mode 100644 app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
 rename app/assets/javascripts/discourse/routes/{user-activity-likes-received.js.es6 => user-notifications-likes-received.js.es6} (77%)
 rename app/assets/javascripts/discourse/routes/{user-activity-mentions.js.es6 => user-notifications-mentions.js.es6} (100%)
 rename app/assets/javascripts/discourse/routes/{user-activity-posts.js.es6 => user-notifications-responses.js.es6} (80%)
 create mode 100644 app/assets/javascripts/discourse/routes/user-statistics.js.es6
 create mode 100644 app/assets/javascripts/discourse/templates/user/messages.hbs
 create mode 100644 app/assets/javascripts/discourse/templates/user/notifications-index.hbs
 create mode 100644 db/migrate/20151219045559_add_has_messages_to_groups.rb

diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
new file mode 100644
index 000000000..7b7664e75
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6
@@ -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")
+                        };
+                   });
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index 49889e5c0..298592061 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -6,7 +6,6 @@ import User from 'discourse/models/user';
 
 export default Ember.Controller.extend(CanCheckEmails, {
   indexStream: false,
-  pmView: false,
   userActionType: null,
   needs: ['user-notifications', 'user-topics-list'],
 
@@ -28,11 +27,14 @@ export default Ember.Controller.extend(CanCheckEmails, {
   },
 
   @computed('viewingSelf', 'currentUser.admin')
-  canSeePrivateMessages(viewingSelf, isAdmin) {
+  showPrivateMessages(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")
   showBadges(badgeCount) {
@@ -45,6 +47,12 @@ export default Ember.Controller.extend(CanCheckEmails, {
            (userActionType === UserAction.TYPES.messages_received);
   },
 
+  @computed("indexStream", "userActionType")
+  showActionTypeSummary(indexStream,userActionType, showPMs) {
+    return (indexStream || userActionType) && !showPMs;
+  },
+
+
   @computed()
   canInviteToForum() {
     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: {
     expandProfile() {
       this.set('forceExpand', true);
diff --git a/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6 b/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6
index 35084dcf1..1da5529cd 100644
--- a/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6
+++ b/app/assets/javascripts/discourse/mixins/viewing-action-type.js.es6
@@ -3,4 +3,5 @@ export default {
     this.controllerFor('user').set('userActionType', userActionType);
     this.controllerFor('user-activity').set('userActionType', userActionType);
   }
+
 };
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 97c611ab8..2dbeea576 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -199,6 +199,15 @@ const User = RestModel.extend({
            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.
   @computed("statsExcludingPms.@each.count")
   statsCountNonPM() {
@@ -233,8 +242,8 @@ const User = RestModel.extend({
         }));
       }
 
-      if (!Em.isEmpty(json.user.custom_groups)) {
-        json.user.custom_groups = json.user.custom_groups.map(g => Group.create(g));
+      if (!Em.isEmpty(json.user.groups)) {
+        json.user.groups = json.user.groups.map(g => Group.create(g));
       }
 
       if (json.user.invited_by) {
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index d67e0a786..39bee4912 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -58,13 +58,20 @@ export default function() {
   this.resource('users');
   this.resource('user', { path: '/users/:username' }, function() {
     this.resource('userActivity', { path: '/activity' }, function() {
-      _.map(Discourse.UserAction.TYPES, (id, userAction) => {
-        this.route(userAction, { path: userAction.replace('_', '-') });
-      });
+      this.route('topics');
+      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('notifications');
     this.route('flaggedPosts', { path: '/flagged-posts' });
     this.route('deletedPosts', { path: '/deleted-posts' });
 
@@ -85,6 +92,7 @@ export default function() {
     this.resource('userInvited', { path: '/invited' }, function() {
       this.route('show', { path: '/:filter' });
     });
+
   });
 
   this.route('signup', {path: '/signup'});
diff --git a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
similarity index 92%
rename from app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6
rename to app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
index 5704cd96d..cef72ed15 100644
--- a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
@@ -24,7 +24,7 @@ export default (viewName, path) => {
         showParticipants: true
       });
 
-      this.controllerFor("user").set("pmView", viewName);
+      this.controllerFor("userPrivateMessages").set("pmView", viewName);
       this.searchService.set('contextType', 'private_messages');
     },
 
diff --git a/app/assets/javascripts/discourse/routes/user-activity-replies.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-replies.js.es6
index 28e11587f..62607c9a1 100644
--- a/app/assets/javascripts/discourse/routes/user-activity-replies.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity-replies.js.es6
@@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
 import UserAction from "discourse/models/user-action";
 
 export default UserActivityStreamRoute.extend({
-  userActionType: UserAction.TYPES["replies"]
+  userActionType: UserAction.TYPES["posts"]
 });
diff --git a/app/assets/javascripts/discourse/routes/user-activity.js.es6 b/app/assets/javascripts/discourse/routes/user-activity.js.es6
index 5f2173032..546e135cb 100644
--- a/app/assets/javascripts/discourse/routes/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity.js.es6
@@ -1,5 +1,3 @@
-import Draft from 'discourse/models/draft';
-
 export default Discourse.Route.extend({
   model() {
     return this.modelFor("user");
@@ -7,21 +5,5 @@ export default Discourse.Route.extend({
 
   setupController(controller, 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
-          });
-        }
-      });
-    }
   }
 });
diff --git a/app/assets/javascripts/discourse/routes/user-activity-edits.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-edits.js.es6
similarity index 100%
rename from app/assets/javascripts/discourse/routes/user-activity-edits.js.es6
rename to app/assets/javascripts/discourse/routes/user-notifications-edits.js.es6
diff --git a/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
new file mode 100644
index 000000000..7fb891d6b
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user-notifications-index.js.es6
@@ -0,0 +1,5 @@
+export default Discourse.Route.extend({
+  renderTemplate() {
+    this.render("user/notifications-index");
+  }
+});
diff --git a/app/assets/javascripts/discourse/routes/user-activity-likes-received.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-likes-received.js.es6
similarity index 77%
rename from app/assets/javascripts/discourse/routes/user-activity-likes-received.js.es6
rename to app/assets/javascripts/discourse/routes/user-notifications-likes-received.js.es6
index a53300564..7206e7705 100644
--- a/app/assets/javascripts/discourse/routes/user-activity-likes-received.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-notifications-likes-received.js.es6
@@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
 import UserAction from "discourse/models/user-action";
 
 export default UserActivityStreamRoute.extend({
-  userActionType: UserAction.TYPES["likes_received"]
+  userActionType: UserAction.TYPES["likes_received"],
 });
diff --git a/app/assets/javascripts/discourse/routes/user-activity-mentions.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-mentions.js.es6
similarity index 100%
rename from app/assets/javascripts/discourse/routes/user-activity-mentions.js.es6
rename to app/assets/javascripts/discourse/routes/user-notifications-mentions.js.es6
diff --git a/app/assets/javascripts/discourse/routes/user-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications-responses.js.es6
similarity index 80%
rename from app/assets/javascripts/discourse/routes/user-activity-posts.js.es6
rename to app/assets/javascripts/discourse/routes/user-notifications-responses.js.es6
index 62607c9a1..28e11587f 100644
--- a/app/assets/javascripts/discourse/routes/user-activity-posts.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-notifications-responses.js.es6
@@ -2,5 +2,5 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
 import UserAction from "discourse/models/user-action";
 
 export default UserActivityStreamRoute.extend({
-  userActionType: UserAction.TYPES["posts"]
+  userActionType: UserAction.TYPES["replies"]
 });
diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
index 7c2384db0..cfb91acdb 100644
--- a/app/assets/javascripts/discourse/routes/user-notifications.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
@@ -1,6 +1,11 @@
 import ViewingActionType from "discourse/mixins/viewing-action-type";
 
 export default Discourse.Route.extend(ViewingActionType, {
+
+  renderTemplate() {
+    this.render('user/notifications');
+  },
+
   actions: {
     didTransition() {
       this.controllerFor("user-notifications")._showFooter();
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
index 173641b1d..7e962ee86 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6
@@ -1,5 +1,5 @@
 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({
     model(params) {
@@ -13,13 +13,13 @@ export default createPMRoute('groups', 'private-messages-groups').extend({
       const groupName = _.last(model.get("filter").split('/'));
       Group.findAll().then(groups => {
         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) {
       this._super.apply(this, arguments);
       const group = _.last(model.get("filter").split('/'));
-      this.controllerFor("user").set("groupFilter", group);
+      this.controllerFor("userPrivateMessages").set("groupFilter", group);
     }
 });
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6
index 5265bfb67..d1e2bf3c2 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6
@@ -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');
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-mine.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-mine.js.es6
index 47db69992..b2cf029e5 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-mine.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-mine.js.es6
@@ -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');
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-unread.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-unread.js.es6
index 19e2a5167..062ef7328 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages-unread.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages-unread.js.es6
@@ -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');
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
index 328a304ca..b1074f80a 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
@@ -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: {
     willTransition: function() {
       this._super();
diff --git a/app/assets/javascripts/discourse/routes/user-statistics.js.es6 b/app/assets/javascripts/discourse/routes/user-statistics.js.es6
new file mode 100644
index 000000000..610a10489
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user-statistics.js.es6
@@ -0,0 +1,2 @@
+export default Discourse.Route.extend({
+});
diff --git a/app/assets/javascripts/discourse/templates/user-topics-list.hbs b/app/assets/javascripts/discourse/templates/user-topics-list.hbs
index a2aae2de1..c94625730 100644
--- a/app/assets/javascripts/discourse/templates/user-topics-list.hbs
+++ b/app/assets/javascripts/discourse/templates/user-topics-list.hbs
@@ -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
                    hideCategory=hideCategory
diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs
index c24cd6895..2c3d03ab5 100644
--- a/app/assets/javascripts/discourse/templates/user/activity.hbs
+++ b/app/assets/javascripts/discourse/templates/user/activity.hbs
@@ -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}}
+</section>
+
diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs
new file mode 100644
index 000000000..5e404cb17
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/user/messages.hbs
@@ -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>
diff --git a/app/assets/javascripts/discourse/templates/user/notifications-index.hbs b/app/assets/javascripts/discourse/templates/user/notifications-index.hbs
new file mode 100644
index 000000000..9ca0b0b99
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/user/notifications-index.hbs
@@ -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}}
diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs
index 9ca0b0b99..ac8c6353e 100644
--- a/app/assets/javascripts/discourse/templates/user/notifications.hbs
+++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs
@@ -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}}
-  <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}}
+<section class='user-navigation'>
+  <ul class='action-list nav-stacked'>
+    <li class='no-glyph'>
+      {{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}}
+    </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}}
-  <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}}
+<section class='user-right'>
+  {{outlet}}
+</section>
diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs
index 643757fc7..c18a9afe5 100644
--- a/app/assets/javascripts/discourse/templates/user/preferences.hbs
+++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs
@@ -237,15 +237,8 @@
         {{category-group categories=model.mutedCategories blacklist=selectedCategories}}
       </div>
       <div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
-    </div>
-
-    <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 class="controls category-controls">
+      <a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
       </div>
     </div>
 
@@ -258,6 +251,13 @@
       <div class="instructions">{{i18n 'user.muted_users_instructions'}}</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"}}
 
     <div class="control-group">
diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs
index ae0d50b06..db3678945 100644
--- a/app/assets/javascripts/discourse/templates/user/user.hbs
+++ b/app/assets/javascripts/discourse/templates/user/user.hbs
@@ -1,6 +1,7 @@
 <div class="container{{if viewingSelf ' viewing-self'}}">
   <section class='user-main'>
     <section {{bind-attr class="collapsedInfo :about model.profileBackground:has-background:no-background"}}  style={{model.profileBackground}}>
+    {{#unless collapsedInfo}}
     <div class='staff-counters'>
       {{#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>
@@ -26,6 +27,8 @@
         <div><span class="warnings-received">{{model.number_of_warnings}}</span>&nbsp;{{i18n 'user.staff_counters.warnings_received'}}</div>
       {{/if}}
     </div>
+    {{/unless}}
+
       <div class='profile-image'></div>
       <div class='details'>
         <div class='primary'>
@@ -40,18 +43,9 @@
                 </a>
               </li>
               {{/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}}
                 <li><a href={{model.adminPath}} class="btn">{{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
               {{/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 viewingSelf}}
               <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>
 
+
+      {{#unless collapsedInfo}}
       <div class='secondary'>
         <dl>
           {{#if model.created_at}}
@@ -135,10 +131,10 @@
               {{/if}}
             </dd>
           {{/if}}
-          {{#if model.custom_groups}}
-            <dt>{{i18n 'groups.title' count=model.custom_groups.length}}</dt>
+          {{#if model.displayGroups}}
+            <dt>{{i18n 'groups.title' count=model.displayGroups.length}}</dt>
             <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>
               {{/each}}
             </dd>
@@ -149,76 +145,34 @@
         </dl>
         {{plugin-outlet "user-profile-secondary"}}
       </div>
+      {{/unless}}
     </section>
 
-    <section class='user-navigation'>
-      <ul class='action-list nav-stacked'>
-        {{activity-filter count=model.statsCountNonPM user=model userActionType=userActionType indexStream=indexStream}}
-        {{#each stat in model.statsExcludingPms}}
-          {{activity-filter content=stat user=model userActionType=userActionType indexStream=indexStream}}
-        {{/each}}
-        {{#if showBadges}}
-          {{#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}}
+    <ul class="user-nav">
+      <li class='selected'>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
+      {{#if canSeeNotificationHistory}}
+        <li>
+          {{#link-to 'userNotifications'}}
+            {{fa-icon "comment" class="glyph"}}
+            {{i18n 'user.notifications'}}
           {{/link-to}}
-        {{/if}}
-        {{#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>
+        </li>
       {{/if}}
-
-      {{#if viewingSelf}}
-        <div class='user-archive'>
-          {{d-button action="exportUserArchive" label="user.download_archive" icon="download"}}
-        </div>
+      {{#if showPrivateMessages}}
+        <li>{{#link-to 'userPrivateMessages'}}{{fa-icon "envelope-o"}}{{i18n 'user.private_messages'}}{{/link-to}}</li>
       {{/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}}
-    </section>
+    {{outlet}}
 
   </section>
 </div>
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss
index 3eeb787e9..d532078bf 100644
--- a/app/assets/stylesheets/common/base/user.scss
+++ b/app/assets/stylesheets/common/base/user.scss
@@ -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;
+    }
+  }
+}
+
diff --git a/app/assets/stylesheets/common/components/navs.css.scss b/app/assets/stylesheets/common/components/navs.css.scss
index d29d05a3d..c10d68430 100644
--- a/app/assets/stylesheets/common/components/navs.css.scss
+++ b/app/assets/stylesheets/common/components/navs.css.scss
@@ -65,13 +65,13 @@
       color: $primary;
     }
   }
-  .active > a,
+  .active > a, & li > a.active
   {
     color: $secondary;
     background-color: $quaternary;
   }
 
-  .active > a::after,
+  .active > a::after, & li > a.active::after
   {
     left: 90%;
     top: 33%;
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 1cfdfab7e..1ae8d69e6 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -231,7 +231,6 @@
     background-position: center center;
     background-size: cover;
     width: 100%;
-    margin-bottom: 10px;
     overflow: hidden;
 
     &.group {
@@ -642,3 +641,15 @@
     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;
+  }
+}
+
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 213442d11..3e510bb25 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -33,9 +33,10 @@ class UsersController < ApplicationController
 
     @user = fetch_user_from_params(include_inactive: current_user.try(:staff?))
     user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
-    if params[:stats].to_s == "false"
-      user_serializer.omit_stats = true
-    end
+
+    # TODO remove this options from serializer
+    user_serializer.omit_stats = true
+
     topic_id = params[:include_post_count_for].to_i
     if topic_id != 0
       user_serializer.topic_post_count = {topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count }
diff --git a/app/models/user.rb b/app/models/user.rb
index 1a3dbd47e..a9784dd6f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -141,10 +141,6 @@ class User < ActiveRecord::Base
     SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i
   end
 
-  def custom_groups
-    groups.where(automatic: false, visible: true)
-  end
-
   def self.username_available?(username)
     lower = username.downcase
     User.where(username_lower: lower).blank? && !SiteSetting.reserved_usernames.split("|").include?(username)
diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb
index 925ac2365..d9a50ef12 100644
--- a/app/serializers/basic_group_serializer.rb
+++ b/app/serializers/basic_group_serializer.rb
@@ -11,7 +11,8 @@ class BasicGroupSerializer < ApplicationSerializer
              :title,
              :grant_trust_level,
              :incoming_email,
-             :notification_level
+             :notification_level,
+             :has_messages
 
   def include_incoming_email?
     scope.is_staff?
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 476601b0f..06abd889a 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -70,7 +70,7 @@ class UserSerializer < BasicUserSerializer
              :automatically_unpin_topics
 
   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_one  :card_badge, embed: :object, serializer: BadgeSerializer
 
@@ -118,6 +118,14 @@ class UserSerializer < BasicUserSerializer
   ### 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?
     object.id && object.id == scope.user.try(:id)
   end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b44e809b7..0eb1e2fa7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -378,7 +378,6 @@ en:
       "6": "Responses"
       "7": "Mentions"
       "9": "Quotes"
-      "10": "Starred"
       "11": "Edits"
       "12": "Sent Items"
       "13": "Inbox"
@@ -448,6 +447,7 @@ en:
       invited_by: "Invited By"
       trust_level: "Trust Level"
       notifications: "Notifications"
+      statistics: "Stats"
       desktop_notifications:
         label: "Desktop Notifications"
         not_supported: "Notifications are not supported on this browser. Sorry."
diff --git a/config/routes.rb b/config/routes.rb
index c74d77404..3f7f8c8f2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -305,6 +305,7 @@ Discourse::Application.routes.draw do
   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/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}
   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
diff --git a/db/migrate/20151219045559_add_has_messages_to_groups.rb b/db/migrate/20151219045559_add_has_messages_to_groups.rb
new file mode 100644
index 000000000..e4f4fcbef
--- /dev/null
+++ b/db/migrate/20151219045559_add_has_messages_to_groups.rb
@@ -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
diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb
index 79a2a92ef..7c6a8854b 100644
--- a/lib/topic_creator.rb
+++ b/lib/topic_creator.rb
@@ -186,6 +186,7 @@ class TopicCreator
       check_can_send_permission!(topic, group)
       topic.topic_allowed_groups.build(group_id: group.id)
       len += 1
+      group.update_columns(:has_messages, true) unless group.has_messages
     end
 
     rollback_with!(topic, :target_group_not_found) unless len == names.length