From cf91bca0cd79f4589f1787e2c0a72e37606e471e Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Fri, 31 Jul 2015 14:22:28 -0400
Subject: [PATCH] FIX: Small actions should show descriptions on the user
 stream

---
 .../discourse/components/small-action.js.es6  | 29 +++++++---------
 .../discourse/components/stream-item.js.es6   |  7 ++++
 .../controllers/user-activity.js.es6          |  2 +-
 .../templates/components/small-action.hbs     |  5 ++-
 .../templates/components/stream-item.hbs      | 32 +++++++++++++++++
 .../discourse/templates/user/stream.hbs       | 30 ++--------------
 .../views/user-activity-stream.js.es6         | 27 +++++++++++++++
 .../discourse/views/user-stream.js.es6        |  6 ++--
 app/controllers/user_actions_controller.rb    |  2 +-
 app/models/user_action.rb                     |  1 +
 app/models/user_action_observer.rb            | 34 +++++++++----------
 app/serializers/user_action_serializer.rb     |  9 ++---
 12 files changed, 110 insertions(+), 74 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/components/stream-item.js.es6
 create mode 100644 app/assets/javascripts/discourse/templates/components/stream-item.hbs
 create mode 100644 app/assets/javascripts/discourse/views/user-activity-stream.js.es6

diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6
index 50e42f19ab5..977b408c807 100644
--- a/app/assets/javascripts/discourse/components/small-action.js.es6
+++ b/app/assets/javascripts/discourse/components/small-action.js.es6
@@ -13,27 +13,22 @@ const icons = {
   'visible.disabled': 'eye-slash'
 };
 
+export function actionDescription(actionCode, createdAt) {
+  return function() {
+    const ac = this.get(actionCode);
+    if (actionCode) {
+      const dt = new Date(this.get(createdAt));
+      const when =  Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'});
+      return I18n.t(`action_codes.${ac}`, {when}).htmlSafe();
+    }
+  }.property(actionCode, createdAt);
+}
+
 export default Ember.Component.extend({
   layoutName: 'components/small-action', // needed because `time-gap` inherits from this
   classNames: ['small-action'],
 
-  description: function() {
-    const actionCode = this.get('actionCode');
-    if (actionCode) {
-      const dt = new Date(this.get('post.created_at'));
-      const when =  Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'});
-      var result = I18n.t(`action_codes.${actionCode}`, {when});
-      var cooked = this.get('post.cooked');
-
-      result = "<p>" + result + "</p>";
-
-      if (!Em.isEmpty(cooked)) {
-        result += "<div class='custom-message'>" + cooked + "</div>";
-      }
-
-      return result;
-    }
-  }.property('actionCode', 'post.created_at', 'post.cooked'),
+  description: actionDescription('actionCode', 'post.created_at'),
 
   icon: function() {
     return icons[this.get('actionCode')] || 'exclamation';
diff --git a/app/assets/javascripts/discourse/components/stream-item.js.es6 b/app/assets/javascripts/discourse/components/stream-item.js.es6
new file mode 100644
index 00000000000..ef7b03ad5c3
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/stream-item.js.es6
@@ -0,0 +1,7 @@
+import { actionDescription } from 'discourse/components/small-action';
+
+export default Ember.Component.extend({
+  classNameBindings: [':item', 'item.hidden', 'item.deleted', 'moderatorAction'],
+  moderatorAction: Discourse.computed.propertyEqual('item.post_type', 'site.post_types.moderator_action'),
+  actionDescription: actionDescription('item.action_code', 'item.created_at')
+});
diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
index 6113cedff3d..7e4ec2db92f 100644
--- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
@@ -5,7 +5,7 @@ export default Ember.ObjectController.extend({
   _showFooter: function() {
     var showFooter;
     if (this.get("userActionType")) {
-      var stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") });
+      const stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") });
       showFooter = stat && stat.count <= this.get("model.stream.itemsLoaded");
     } else {
       showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded");
diff --git a/app/assets/javascripts/discourse/templates/components/small-action.hbs b/app/assets/javascripts/discourse/templates/components/small-action.hbs
index b05c6580884..091f0e07df2 100644
--- a/app/assets/javascripts/discourse/templates/components/small-action.hbs
+++ b/app/assets/javascripts/discourse/templates/components/small-action.hbs
@@ -11,5 +11,8 @@
       {{avatar post imageSize="small"}}
     </a>
   {{/if}}
-  {{{description}}}
+  <p>{{description}}</p>
+  {{#if post.cooked}}
+    <div class='custom-message'>{{{post.cooked}}}</div>
+  {{/if}}
 </div>
diff --git a/app/assets/javascripts/discourse/templates/components/stream-item.hbs b/app/assets/javascripts/discourse/templates/components/stream-item.hbs
new file mode 100644
index 00000000000..8dea1ccb131
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/stream-item.hbs
@@ -0,0 +1,32 @@
+<div class='clearfix info'>
+  <a href={{item.userUrl}} data-user-card={{item.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
+  <span class='time'>{{format-date item.created_at}}</span>
+  {{topic-status topic=item disableActions=true}}
+  <span class="title">
+    <a href={{item.postUrl}}>{{{item.title}}}</a>
+  </span>
+  <div class="category">{{category-link item.category}}</div>
+</div>
+
+{{#if actionDescription}}
+  <p class='excerpt'>{{actionDescription}}</p>
+{{/if}}
+
+<p class='excerpt'>{{{item.excerpt}}}</p>
+
+
+{{#each item.children as |child|}}
+  <div class='child-actions'>
+    <i class="icon {{child.icon}}"></i>
+    {{#each child.items as |grandChild|}}
+      {{#if grandChild.removableBookmark}}
+        <button class="btn btn-default remove-bookmark" {{action "removeBookmark" grandChild}}>
+          {{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
+        </button>
+      {{else}}
+          <a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
+          {{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
+      {{/if}}
+    {{/each}}
+  </div>
+{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/user/stream.hbs b/app/assets/javascripts/discourse/templates/user/stream.hbs
index 96e96c83768..4f03bef896c 100644
--- a/app/assets/javascripts/discourse/templates/user/stream.hbs
+++ b/app/assets/javascripts/discourse/templates/user/stream.hbs
@@ -1,29 +1,3 @@
-{{#each item in model.content}}
-  <div {{bind-attr class=":item item.hidden item.deleted item.moderator_action"}}>
-    <div class='clearfix info'>
-      <a href="{{unbound item.userUrl}}" data-user-card="{{unbound item.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
-      <span class='time'>{{format-date item.created_at}}</span>
-      {{topic-status topic=item disableActions=true}}
-      <span class="title">
-        <a href="{{unbound item.postUrl}}">{{{unbound item.title}}}</a>
-      </span>
-      <div class="category">{{category-link item.category}}</div>
-    </div>
-    <p class='excerpt'>{{{unbound item.excerpt}}}</p>
-    {{#each child in item.children}}
-      <div class='child-actions'>
-        <i class="icon {{unbound child.icon}}"></i>
-        {{#each grandChild in child.items}}
-          {{#if grandChild.removableBookmark}}
-            <button class="btn btn-default remove-bookmark" {{action "removeBookmark" grandChild}}>
-              {{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
-            </button>
-          {{else}}
-              <a href="{{unbound grandChild.userUrl}}" data-user-card="{{unbound grandChild.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
-              {{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{unbound grandChild.edit_reason}}</span>{{/if}}
-          {{/if}}
-        {{/each}}
-      </div>
-    {{/each}}
-  </div>
+{{#each model.content as |item|}}
+  {{stream-item item=item}}
 {{/each}}
diff --git a/app/assets/javascripts/discourse/views/user-activity-stream.js.es6 b/app/assets/javascripts/discourse/views/user-activity-stream.js.es6
new file mode 100644
index 00000000000..86fbaefd8f2
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/user-activity-stream.js.es6
@@ -0,0 +1,27 @@
+import LoadMore from "discourse/mixins/load-more";
+
+export default Ember.View.extend(LoadMore, {
+  loading: false,
+  eyelineSelector: '.user-stream .item',
+  classNames: ['user-stream'],
+
+  _scrollTopOnModelChange: function() {
+    Em.run.schedule('afterRender', function() {
+      $(document).scrollTop(0);
+    });
+  }.observes('controller.model.user.id'),
+
+  actions: {
+    loadMore() {
+      const self = this;
+      if (this.get('loading')) { return; }
+
+      this.set('loading', true);
+      const stream = this.get('controller.model');
+      stream.findItems().then(function() {
+        self.set('loading', false);
+        self.get('eyeline').flushRest();
+      });
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/views/user-stream.js.es6 b/app/assets/javascripts/discourse/views/user-stream.js.es6
index 367c089ed03..86fbaefd8f2 100644
--- a/app/assets/javascripts/discourse/views/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/views/user-stream.js.es6
@@ -12,12 +12,12 @@ export default Ember.View.extend(LoadMore, {
   }.observes('controller.model.user.id'),
 
   actions: {
-    loadMore: function() {
-      var self = this;
+    loadMore() {
+      const self = this;
       if (this.get('loading')) { return; }
 
       this.set('loading', true);
-      var stream = this.get('controller.model');
+      const stream = this.get('controller.model');
       stream.findItems().then(function() {
         self.set('loading', false);
         self.get('eyeline').flushRest();
diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb
index e4d4079fec0..c20f9c8d23d 100644
--- a/app/controllers/user_actions_controller.rb
+++ b/app/controllers/user_actions_controller.rb
@@ -24,7 +24,7 @@ class UserActionsController < ApplicationController
       UserAction.stream(opts)
     end
 
-    render_serialized(stream, UserActionSerializer, root: "user_actions")
+    render_serialized(stream, UserActionSerializer, root: 'user_actions')
   end
 
   def show
diff --git a/app/models/user_action.rb b/app/models/user_action.rb
index 0d4c661d100..9b72a755dea 100644
--- a/app/models/user_action.rb
+++ b/app/models/user_action.rb
@@ -154,6 +154,7 @@ SQL
         CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
         p.hidden,
         p.post_type,
+        p.action_code,
         p.edit_reason,
         t.category_id
       FROM user_actions as a
diff --git a/app/models/user_action_observer.rb b/app/models/user_action_observer.rb
index 665a1efdfce..34e6fcabedc 100644
--- a/app/models/user_action_observer.rb
+++ b/app/models/user_action_observer.rb
@@ -29,11 +29,11 @@ class UserActionObserver < ActiveRecord::Observer
     return unless action && post && user && post.id
 
     row = {
-        action_type: action,
-        user_id: user.id,
-        acting_user_id: acting_user_id || post.user_id,
-        target_topic_id: post.topic_id,
-        target_post_id: post.id
+      action_type: action,
+      user_id: user.id,
+      acting_user_id: acting_user_id || post.user_id,
+      target_topic_id: post.topic_id,
+      target_post_id: post.id
     }
 
     if post.deleted_at.nil?
@@ -48,12 +48,12 @@ class UserActionObserver < ActiveRecord::Observer
     return if model.is_first_post?
 
     row = {
-        action_type: UserAction::REPLY,
-        user_id: model.user_id,
-        acting_user_id: model.user_id,
-        target_post_id: model.id,
-        target_topic_id: model.topic_id,
-        created_at: model.created_at
+      action_type: UserAction::REPLY,
+      user_id: model.user_id,
+      acting_user_id: model.user_id,
+      target_post_id: model.id,
+      target_topic_id: model.topic_id,
+      created_at: model.created_at
     }
 
     rows = [row]
@@ -79,12 +79,12 @@ class UserActionObserver < ActiveRecord::Observer
 
   def log_topic(model)
     row = {
-        action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC,
-        user_id: model.user_id,
-        acting_user_id: model.user_id,
-        target_topic_id: model.id,
-        target_post_id: -1,
-        created_at: model.created_at
+      action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC,
+      user_id: model.user_id,
+      acting_user_id: model.user_id,
+      target_topic_id: model.id,
+      target_post_id: -1,
+      created_at: model.created_at
     }
 
     rows = [row]
diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb
index f6dcf2b9fb3..8b393996392 100644
--- a/app/serializers/user_action_serializer.rb
+++ b/app/serializers/user_action_serializer.rb
@@ -22,7 +22,8 @@ class UserActionSerializer < ApplicationSerializer
              :title,
              :deleted,
              :hidden,
-             :moderator_action,
+             :post_type,
+             :action_code,
              :edit_reason,
              :category_id,
              :uploaded_avatar_id,
@@ -32,7 +33,7 @@ class UserActionSerializer < ApplicationSerializer
 
   def excerpt
     cooked = object.cooked || PrettyText.cook(object.raw)
-    PrettyText.excerpt(cooked, 300, { keep_emojis: true }) if cooked
+    PrettyText.excerpt(cooked, 300, keep_emojis: true) if cooked
   end
 
   def avatar_template
@@ -67,10 +68,6 @@ class UserActionSerializer < ApplicationSerializer
     object.title.present?
   end
 
-  def moderator_action
-    object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action]
-  end
-
   def include_reply_to_post_number?
     object.action_type == UserAction::REPLY
   end