diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 1f9141604e5..dc5df8c2269 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -189,7 +189,8 @@ const User = Discourse.Model.extend({
'enable_quoting',
'disable_jump_reply',
'custom_fields',
- 'user_fields');
+ 'user_fields',
+ 'muted_usernames');
['muted','watched','tracked'].forEach(function(s){
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs
index d7cc7b20c34..5bd61338581 100644
--- a/app/assets/javascripts/discourse/templates/user/preferences.hbs
+++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs
@@ -237,6 +237,14 @@
{{partial 'user/preferences/saveButton'}}
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 6936c8d6055..1ce508b649e 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -22,8 +22,8 @@
}
.user-preferences {
- input.category-group {
- width: 500px;
+ input.category-group, input.user-selector {
+ width: 530px;
}
textarea {
@@ -31,6 +31,8 @@
height: 100px;
}
+ input
+
input[type=text] {
@include small-width {
width: 450px;
diff --git a/app/models/muted_user.rb b/app/models/muted_user.rb
new file mode 100644
index 00000000000..a60b30202b3
--- /dev/null
+++ b/app/models/muted_user.rb
@@ -0,0 +1,4 @@
+class MutedUser < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :muted_user, class_name: 'User'
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index e815e86d255..b53d8c3e93b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -55,6 +55,9 @@ class User < ActiveRecord::Base
has_many :group_managers, dependent: :destroy
has_many :managed_groups, through: :group_managers, source: :group
+ has_many :muted_user_records, class_name: 'MutedUser'
+ has_many :muted_users, through: :muted_user_records
+
has_one :user_search_data, dependent: :destroy
has_one :api_key, dependent: :destroy
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index f589b329fe4..37d42599c88 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -95,7 +95,8 @@ class UserSerializer < BasicUserSerializer
:custom_avatar_upload_id,
:has_title_badges,
:card_image_badge,
- :card_image_badge_id
+ :card_image_badge_id,
+ :muted_usernames
untrusted_attributes :bio_raw,
:bio_cooked,
@@ -252,6 +253,10 @@ class UserSerializer < BasicUserSerializer
CategoryUser.lookup(object, :watching).pluck(:category_id)
end
+ def muted_usernames
+ MutedUser.where(user_id: object.id).joins(:muted_user).pluck(:username)
+ end
+
def include_private_message_stats?
can_edit && !(omit_stats == true)
end
diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb
index ac24e0ff549..e62c06c43b3 100644
--- a/app/services/post_alerter.rb
+++ b/app/services/post_alerter.rb
@@ -86,6 +86,9 @@ class PostAlerter
# Make sure the user can see the post
return unless Guardian.new(user).can_see?(post)
+ # apply muting here
+ return if post.user_id && MutedUser.where(user_id: user.id, muted_user_id: post.user_id).exists?
+
# skip if muted on the topic
return if TopicUser.get(post.topic, user).try(:notification_level) == TopicUser.notification_levels[:muted]
diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb
index 1881f40a782..9dc5ee79482 100644
--- a/app/services/user_updater.rb
+++ b/app/services/user_updater.rb
@@ -66,6 +66,11 @@ class UserUpdater
end
User.transaction do
+
+ if attributes.key?(:muted_usernames)
+ update_muted_users(attributes[:muted_usernames])
+ end
+
user_profile.save && user.save
end
end
@@ -74,6 +79,29 @@ class UserUpdater
attr_reader :user, :guardian
+ def update_muted_users(usernames)
+ usernames ||= ""
+ desired_ids = User.where(username: usernames.split(",")).pluck(:id)
+ if desired_ids.empty?
+ MutedUser.where(user_id: user.id).destroy_all
+ else
+ MutedUser.where('id not in (?)', desired_ids).destroy_all
+
+ # SQL is easier here than figuring out how to do the same in AR
+ MutedUser.exec_sql("INSERT into muted_users(user_id, muted_user_id, created_at, updated_at)
+ SELECT :user_id, id, :now, :now
+ FROM users
+ WHERE
+ id in (:desired_ids) AND
+ id NOT IN (
+ SELECT muted_user_id
+ FROM muted_users
+ WHERE user_id = :user_id
+ )",
+ now: Time.now, user_id: user.id, desired_ids: desired_ids)
+ end
+ end
+
def format_url(website)
if website =~ /^http/
website
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 82934516fc4..d572ba63ec5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -360,6 +360,9 @@ en:
delete_yourself_not_allowed: "You cannot delete your account right now. Contact an admin to do delete your account for you."
unread_message_count: "Messages"
admin_delete: "Delete"
+ users: "Users"
+ muted_users: "Muted"
+ muted_users_instructions: "Suppress all notifications from these users."
staff_counters:
flags_given: "helpful flags"
diff --git a/db/migrate/20150323234856_add_muted_users.rb b/db/migrate/20150323234856_add_muted_users.rb
new file mode 100644
index 00000000000..2e7c5048f68
--- /dev/null
+++ b/db/migrate/20150323234856_add_muted_users.rb
@@ -0,0 +1,12 @@
+class AddMutedUsers < ActiveRecord::Migration
+ def change
+ create_table :muted_users do |t|
+ t.integer :user_id, null: false
+ t.integer :muted_user_id, null: false
+ t.timestamps
+ end
+
+ add_index :muted_users, [:user_id, :muted_user_id], unique: true
+ add_index :muted_users, [:muted_user_id, :user_id], unique: true
+ end
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 8fe685e90f5..cbd2d81659f 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -983,13 +983,32 @@ describe UsersController do
let!(:user) { log_in(:user) }
it 'allows the update' do
- put :update, username: user.username, name: 'Jim Tom', custom_fields: {test: :it}
+
+ user2 = Fabricate(:user)
+ user3 = Fabricate(:user)
+
+ put :update,
+ username: user.username,
+ name: 'Jim Tom',
+ custom_fields: {test: :it},
+ muted_usernames: "#{user2.username},#{user3.username}"
+
expect(response).to be_success
user.reload
expect(user.name).to eq 'Jim Tom'
expect(user.custom_fields['test']).to eq 'it'
+ expect(user.muted_users.pluck(:username).sort).to eq [user2.username,user3.username].sort
+
+ put :update,
+ username: user.username,
+ muted_usernames: ""
+
+ user.reload
+
+ expect(user.muted_users.pluck(:username).sort).to be_empty
+
end
context "with user fields" do
diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb
index 1de486e1951..fb6e8974c65 100644
--- a/spec/services/post_alerter_spec.rb
+++ b/spec/services/post_alerter_spec.rb
@@ -11,6 +11,15 @@ describe PostAlerter do
context 'quotes' do
+ it 'does not notify for muted users' do
+ post = Fabricate(:post, raw: '[quote="EvilTrout, post:1"]whatup[/quote]')
+ MutedUser.create!(user_id: evil_trout.id, muted_user_id: post.user_id)
+
+ lambda {
+ PostAlerter.post_created(post)
+ }.should change(evil_trout.notifications, :count).by(0)
+ end
+
it 'notifies a user by username' do
lambda {
create_post_with_alerts(raw: '[quote="EvilTrout, post:1"]whatup[/quote]')