FEATURE: Introduce Ignore user (#7072)

FEATURE: Introduce Ignore user (#7072)

diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
index d13fcd1..4849a91 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -195,6 +195,16 @@ export default Ember.Component.extend(
         this._close();
       },
 
+      ignoreUser() {
+        this.get("user").ignore();
+        this._close();
+      },
+
+      watchUser() {
+        this.get("user").watch();
+        this._close();
+      },
+
       showUser() {
         this.showUser(this.get("user"));
         this._close();
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index e0b31bd..d6f577f 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -145,6 +145,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
 
     adminDelete() {
       this.get("adminTools").deleteUser(this.get("model.id"));
+    },
+
+    ignoreUser() {
+      const user = this.get("model");
+      user.ignore().then(() => user.set("ignored", true));
+    },
+
+    watchUser() {
+      const user = this.get("model");
+      user.watch().then(() => user.set("ignored", false));
     }
   }
 });
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 428f08b..ab7f315 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -615,6 +615,20 @@ const User = RestModel.extend({
     }
   },
 
+  ignore() {
+    return ajax(`${userPath(this.get("username"))}/ignore.json`, {
+      type: "PUT",
+      data: { ignored_user_id: this.get("id") }
+    });
+  },
+
+  watch() {
+    return ajax(`${userPath(this.get("username"))}/ignore.json`, {
+      type: "DELETE",
+      data: { ignored_user_id: this.get("id") }
+    });
+  },
+
   dismissBanner(bannerKey) {
     this.set("dismissed_banner_key", bannerKey);
     ajax(userPath(this.get("username") + ".json"), {
diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
index a7fc8ca..e40a5a2 100644
--- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
+++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
@@ -48,6 +48,21 @@
             icon="envelope"
             label="user.private_message"}}
         </li>
+        {{#if user.can_ignore_user}}
+          <li>
+            {{#if user.ignored}}
+              {{d-button class="btn-default"
+                         action=(action "watchUser")
+                         icon="eye"
+                         label="user.watch"}}
+            {{else}}
+              {{d-button class="btn-danger"
+                         action=(action "ignoreUser")
+                         icon="eye-slash"
+                         label="user.ignore"}}
+            {{/if}}
+          </li>
+        {{/if}}
       {{/if}}
 
       {{#if showFilter}}
diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs
index 0376714..755d909 100644
--- a/app/assets/javascripts/discourse/templates/user.hbs
+++ b/app/assets/javascripts/discourse/templates/user.hbs
@@ -43,12 +43,27 @@
           <section class='controls'>
             <ul>
               {{#if model.can_send_private_message_to_user}}
-              <li>
-                {{d-button class="btn btn-primary compose-pm"
-                  action=(route-action "composePrivateMessage" model)
-                  icon="envelope"
-                  label="user.private_message"}}
-              </li>
+                <li>
+                  {{d-button class="btn-primary compose-pm"
+                    action=(route-action "composePrivateMessage" model)
+                    icon="envelope"
+                    label="user.private_message"}}
+                </li>
+                {{#if model.can_ignore_user}}
+                  <li>
+                  {{#if model.ignored}}
+                    {{d-button class="btn-default"
+                               action=(action "watchUser")
+                               icon="eye"
+                               label="user.watch"}}
+                  {{else}}
+                    {{d-button class="btn-danger"
+                               action=(action "ignoreUser")
+                               icon="eye-slash"
+                               label="user.ignore"}}
+                  {{/if}}
+                  </li>
+                {{/if}}
               {{/if}}
               {{#if currentUser.staff}}
                 <li><a href={{model.adminPath}} class="btn btn-default">{{d-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}</a></li>
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index be2ba57..81d6ea8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -14,7 +14,7 @@ class UsersController < ApplicationController
     :pick_avatar, :destroy_user_image, :destroy, :check_emails,
     :topic_tracking_state, :preferences, :create_second_factor,
     :update_second_factor, :create_second_factor_backup, :select_avatar,
-    :revoke_auth_token
+    :ignore, :watch, :revoke_auth_token
   ]
 
   skip_before_action :check_xhr, only: [
@@ -995,6 +995,22 @@ class UsersController < ApplicationController
     render json: success_json
   end
 
+  def ignore
+    raise Discourse::NotFound unless SiteSetting.ignore_user_enabled
+
+    ::IgnoredUser.find_or_create_by!(
+      user: current_user,
+      ignored_user_id: params[:ignored_user_id])
+    render json: success_json
+  end
+
+  def watch
+    raise Discourse::NotFound unless SiteSetting.ignore_user_enabled
+
+    IgnoredUser.where(user: current_user, ignored_user_id: params[:ignored_user_id]).delete_all
+    render json: success_json
+  end
+
   def read_faq
     if user = current_user
       user.user_stat.read_faq = 1.second.ago
diff --git a/app/models/ignored_user.rb b/app/models/ignored_user.rb
new file mode 100644
index 0000000..34c6bdc
--- /dev/null
+++ b/app/models/ignored_user.rb
@@ -0,0 +1,20 @@
+class IgnoredUser < ActiveRecord::Base
+  belongs_to :user
+  belongs_to :ignored_user, class_name: "User"
+end
+
+# == Schema Information
+#
+# Table name: ignored_users
+#
+#  id            :integer          not null, primary key
+#  user_id       :integer          not null
+#  ignored_user_id :integer          not null
+#  created_at    :datetime         not null
+#  updated_at    :datetime         not null
+#
+# Indexes
+#
+#  index_ignored_users_on_ignored_user_id_and_user_id  (ignored_user_id,user_id) UNIQUE
+#  index_ignored_users_on_user_id_and_ignored_user_id  (user_id,ignored_user_id) UNIQUE
+#
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 325f4dd..7f30ead 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -49,6 +49,8 @@ class UserSerializer < BasicUserSerializer
              :can_edit_email,
              :can_edit_name,
              :stats,
+             :ignored,
+             :can_ignore_user,
              :can_send_private_messages,
              :can_send_private_message_to_user,
              :bio_excerpt,
@@ -274,6 +276,14 @@ class UserSerializer < BasicUserSerializer
     UserAction.stats(object.id, scope)
   end
 
+  def ignored
+    IgnoredUser.where(user_id: scope.user&.id, ignored_user_id: object.id).exists?
+  end
+
+  def can_ignore_user
+    SiteSetting.ignore_user_enabled
+  end
+
   # Needed because 'send_private_message_to_user' will always return false
   # when the current user is being serialized

[... diff too long, it was truncated ...]

GitHub sha: 986cc8a0