From 92e371f0b38de9df495aa3f074177dc4251a205b Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 24 Mar 2015 11:55:22 +1100 Subject: [PATCH] FEATURE: civilized mute Allow user to mute all notifications generated by specific users --- .../javascripts/discourse/models/user.js.es6 | 3 +- .../discourse/templates/user/preferences.hbs | 8 ++++++ app/assets/stylesheets/desktop/user.scss | 6 ++-- app/models/muted_user.rb | 4 +++ app/models/user.rb | 3 ++ app/serializers/user_serializer.rb | 7 ++++- app/services/post_alerter.rb | 3 ++ app/services/user_updater.rb | 28 +++++++++++++++++++ config/locales/client.en.yml | 3 ++ db/migrate/20150323234856_add_muted_users.rb | 12 ++++++++ spec/controllers/users_controller_spec.rb | 21 +++++++++++++- spec/services/post_alerter_spec.rb | 9 ++++++ 12 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 app/models/muted_user.rb create mode 100644 db/migrate/20150323234856_add_muted_users.rb diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 1f9141604..dc5df8c22 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 d7cc7b20c..5bd613385 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -237,6 +237,14 @@
{{i18n 'user.muted_categories_instructions'}}
+
+ +
+ + {{user-selector excludeCurrentUser=true usernames=muted_usernames class="user-selector"}} +
+
{{i18n 'user.muted_users_instructions'}}
+
{{partial 'user/preferences/saveButton'}} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 6936c8d60..1ce508b64 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 000000000..a60b30202 --- /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 e815e86d2..b53d8c3e9 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 f589b329f..37d42599c 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 ac24e0ff5..e62c06c43 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 1881f40a7..9dc5ee794 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 82934516f..d572ba63e 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 000000000..2e7c5048f --- /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 8fe685e90..cbd2d8165 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 1de486e19..fb6e8974c 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]')