FEATURE: Introducing new UI for changing User's notification levels (#7248)

* FEATURE: Introducing new UI for tracking User's ignored or muted states
This commit is contained in:
Tarek Khalil 2019-03-27 09:41:50 +00:00 committed by GitHub
parent 675d950eab
commit ef2362a30f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 251 additions and 92 deletions

View File

@ -33,7 +33,10 @@ export default Ember.Controller.extend(CanCheckEmails, {
}
return (!indexStream || viewingSelf) && !forceExpand;
},
canMuteOrIgnoreUser: Ember.computed.or(
"model.can_ignore_user",
"model.can_mute_user"
),
hasGivenFlags: Ember.computed.gt("model.number_of_flags_given", 0),
hasFlaggedPosts: Ember.computed.gt("model.number_of_flagged_posts", 0),
hasDeletedPosts: Ember.computed.gt("model.number_of_deleted_posts", 0),
@ -147,14 +150,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
this.get("adminTools").deleteUser(this.get("model.id"));
},
ignoreUser() {
updateNotificationLevel(level) {
const user = this.get("model");
user.ignore().then(() => user.set("ignored", true));
},
unignoreUser() {
const user = this.get("model");
user.unignore().then(() => user.set("ignored", false));
return user.updateNotificationLevel(level);
}
}
});

View File

@ -615,17 +615,10 @@ const User = RestModel.extend({
}
},
ignore() {
return ajax(`${userPath(this.get("username"))}/ignore.json`, {
updateNotificationLevel(level) {
return ajax(`${userPath(this.get("username"))}/notification_level.json`, {
type: "PUT",
data: { ignored_user_id: this.get("id") }
});
},
unignore() {
return ajax(`${userPath(this.get("username"))}/ignore.json`, {
type: "DELETE",
data: { ignored_user_id: this.get("id") }
data: { notification_level: level }
});
},

View File

@ -48,19 +48,9 @@
icon="envelope"
label="user.private_message"}}
</li>
{{#if model.can_ignore_user}}
{{#if canMuteOrIgnoreUser}}
<li>
{{#if model.ignored}}
{{d-button class="btn-default"
action=(action "unignoreUser")
icon="far-eye"
label="user.unignore"}}
{{else}}
{{d-button class="btn-default"
action=(action "ignoreUser")
icon="eye-slash"
label="user.ignore"}}
{{/if}}
{{user-notifications-dropdown user=model updateNotificationLevel=(action "updateNotificationLevel")}}
</li>
{{/if}}
{{/if}}

View File

@ -0,0 +1,85 @@
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default DropdownSelectBox.extend({
classNames: ["user-notifications", "user-notifications-dropdown"],
nameProperty: "label",
allowInitialValueMutation: false,
computeHeaderContent() {
let content = this._super(...arguments);
if (this.get("user.ignored")) {
this.set("headerIcon", "eye-slash");
content.name = `${I18n.t("user.user_notifications_ignore_option")}`;
} else if (this.get("user.muted")) {
this.set("headerIcon", "times-circle");
content.name = `${I18n.t("user.user_notifications_mute_option")}`;
} else {
this.set("headerIcon", "user");
content.name = `${I18n.t("user.user_notifications_normal_option")}`;
}
return content;
},
computeContent() {
const content = [];
content.push({
icon: "user",
id: "change-to-normal",
description: I18n.t("user.user_notifications_normal_option_title"),
action: () => this.send("reset"),
label: I18n.t("user.user_notifications_normal_option")
});
content.push({
icon: "times-circle",
id: "change-to-muted",
description: I18n.t("user.user_notifications_mute_option_title"),
action: () => this.send("mute"),
label: I18n.t("user.user_notifications_mute_option")
});
if (this.get("user.can_ignore_user")) {
content.push({
icon: "eye-slash",
id: "change-to-ignored",
description: I18n.t("user.user_notifications_ignore_option_title"),
action: () => this.send("ignore"),
label: I18n.t("user.user_notifications_ignore_option")
});
}
return content;
},
actions: {
reset() {
this.get("updateNotificationLevel")("normal")
.then(() => {
this.set("user.ignored", false);
this.set("user.muted", false);
this.computeHeaderContent();
})
.catch(popupAjaxError);
},
mute() {
this.get("updateNotificationLevel")("mute")
.then(() => {
this.set("user.ignored", false);
this.set("user.muted", true);
this.computeHeaderContent();
})
.catch(popupAjaxError);
},
ignore() {
this.get("updateNotificationLevel")("ignore")
.then(() => {
this.set("user.ignored", true);
this.set("user.muted", false);
this.computeHeaderContent();
})
.catch(popupAjaxError);
}
}
});

View File

@ -27,6 +27,7 @@
@import "common/select-kit/tag-drop";
@import "common/select-kit/toolbar-popup-menu-options";
@import "common/select-kit/topic-notifications-button";
@import "common/select-kit/user-notifications-dropdown";
@import "common/select-kit/color-palettes";
@import "common/components/*";
@import "common/input_tip";

View File

@ -97,7 +97,6 @@
.user-main {
.about {
overflow: hidden;
width: 100%;
margin-bottom: 15px;

View File

@ -0,0 +1,20 @@
.select-kit {
&.dropdown-select-box {
&.user-notifications-dropdown {
text-align: left;
.select-kit-body {
width: 485px;
max-width: 485px;
}
.select-kit-header {
justify-content: center;
}
.dropdown-select-box-header {
align-items: center;
}
}
}
}

View File

@ -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,
:ignore, :unignore, :revoke_auth_token
:notification_level, :revoke_auth_token
]
skip_before_action :check_xhr, only: [
@ -995,18 +995,23 @@ class UsersController < ApplicationController
render json: success_json
end
def ignore
def notification_level
raise Discourse::NotFound unless SiteSetting.ignore_user_enabled
guardian.ensure_can_ignore_user!(params[:ignored_user_id])
user = fetch_user_from_params
IgnoredUser.find_or_create_by!(user: current_user, ignored_user_id: params[:ignored_user_id])
render json: success_json
end
if params[:notification_level] == "ignore"
guardian.ensure_can_ignore_user!(user.id)
MutedUser.where(user: current_user, muted_user: user).delete_all
IgnoredUser.find_or_create_by!(user: current_user, ignored_user: user)
elsif params[:notification_level] == "mute"
guardian.ensure_can_mute_user!(user.id)
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
MutedUser.find_or_create_by!(user: current_user, muted_user: user)
elsif params[:notification_level] == "normal"
MutedUser.where(user: current_user, muted_user: user).delete_all
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
end
def unignore
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

View File

@ -50,7 +50,9 @@ class UserSerializer < BasicUserSerializer
:can_edit_name,
:stats,
:ignored,
:muted,
:can_ignore_user,
:can_mute_user,
:can_send_private_messages,
:can_send_private_message_to_user,
:bio_excerpt,
@ -281,6 +283,14 @@ class UserSerializer < BasicUserSerializer
IgnoredUser.where(user_id: scope.user&.id, ignored_user_id: object.id).exists?
end
def muted
MutedUser.where(user_id: scope.user&.id, muted_user_id: object.id).exists?
end
def can_mute_user
scope.can_mute_user?(object.id)
end
def can_ignore_user
scope.can_ignore_user?(object.id)
end

View File

@ -12,6 +12,9 @@ class WebHookUserSerializer < UserSerializer
can_edit_name
can_send_private_messages
can_send_private_message_to_user
can_ignore_user
can_mute_user
ignored
uploaded_avatar_id
has_title_badges
bio_cooked

View File

@ -637,8 +637,12 @@ en:
new_private_message: "New Message"
private_message: "Message"
private_messages: "Messages"
ignore: "Ignore"
unignore: "Unignore"
user_notifications_ignore_option: "Ignored"
user_notifications_ignore_option_title: "You will not receive notifications related to this user and all of their topics and replies will be hidden from you."
user_notifications_mute_option: "Muted"
user_notifications_mute_option_title: "You will not receive any notifications related to this user."
user_notifications_normal_option: "Normal"
user_notifications_normal_option_title: "You will be notified if this user replies to you or mentions you, and you will see their topics."
activity_stream: "Activity"
preferences: "Preferences"
profile_hidden: "This user's public profile is hidden."

View File

@ -426,8 +426,7 @@ Discourse::Application.routes.draw do
post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/ignore" => "users#ignore", constraints: { username: RouteFormat.username }
delete "#{root_path}/:username/ignore" => "users#unignore", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/notification_level" => "users#notification_level", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormat.username }

View File

@ -390,6 +390,17 @@ class Guardian
UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0
end
def can_mute_user?(user_id)
can_mute_users? &&
@user.id != user_id &&
User.where(id: user_id, admin: false, moderator: false).exists?
end
def can_mute_users?
return false if anonymous?
@user.staff? || @user.trust_level >= TrustLevel.levels[:basic]
end
def can_ignore_user?(user_id)
can_ignore_users? && @user.id != user_id && User.where(id: user_id, admin: false, moderator: false).exists?
end

View File

@ -9,6 +9,7 @@ describe Guardian do
let(:user) { Fabricate(:user) }
let(:moderator) { Fabricate(:moderator) }
let(:admin) { Fabricate(:admin) }
let(:trust_level_1) { build(:user, trust_level: 1) }
let(:trust_level_2) { build(:user, trust_level: 2) }
let(:trust_level_3) { build(:user, trust_level: 3) }
let(:trust_level_4) { build(:user, trust_level: 4) }
@ -2698,6 +2699,61 @@ describe Guardian do
end
end
describe '#can_mute_user?' do
let(:guardian) { Guardian.new(trust_level_1) }
context "when muted user is the same as guardian user" do
it 'does not allow muting user' do
expect(guardian.can_mute_user?(trust_level_1.id)).to eq(false)
end
end
context "when muted user is a staff user" do
let!(:admin) { Fabricate(:user, admin: true) }
it 'does not allow muting user' do
expect(guardian.can_mute_user?(admin.id)).to eq(false)
end
end
context "when muted user is a normal user" do
let!(:another_user) { Fabricate(:user) }
it 'allows muting user' do
expect(guardian.can_mute_user?(another_user.id)).to eq(true)
end
end
context "when muter's trust level is below tl1" do
let(:guardian) { Guardian.new(trust_level_0) }
let!(:another_user) { Fabricate(:user) }
let!(:trust_level_0) { build(:user, trust_level: 0) }
it 'does not allow muting user' do
expect(guardian.can_mute_user?(another_user.id)).to eq(false)
end
end
context "when muter is staff" do
let(:guardian) { Guardian.new(admin) }
let!(:another_user) { Fabricate(:user) }
it 'allows muting user' do
expect(guardian.can_mute_user?(another_user.id)).to eq(true)
end
end
context "when muters's trust level is tl1" do
let(:guardian) { Guardian.new(trust_level_1) }
let!(:another_user) { Fabricate(:user) }
it 'allows muting user' do
expect(guardian.can_mute_user?(another_user.id)).to eq(true)
end
end
end
describe "#allow_themes?" do
let(:theme) { Fabricate(:theme) }
let(:theme2) { Fabricate(:theme) }

View File

@ -2022,7 +2022,7 @@ describe UsersController do
describe '#ignore' do
it 'raises an error when not logged in' do
put "/u/#{user.username}/ignore.json", params: { ignored_user_id: "" }
put "/u/#{user.username}/notification_level.json", params: { notification_level: "" }
expect(response.status).to eq(403)
end
@ -2033,58 +2033,43 @@ describe UsersController do
sign_in(user)
end
describe 'when SiteSetting.ignore_user_enabled is false' do
it 'raises an error' do
SiteSetting.ignore_user_enabled = false
put "/u/#{user.username}/ignore.json"
context 'when ignore_user_enable is OFF' do
it 'raises an error when not logged in' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "" }
expect(response.status).to eq(404)
end
end
describe 'when SiteSetting.ignore_user_enabled is true' do
it 'creates IgnoredUser record' do
SiteSetting.ignore_user_enabled = true
put "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
expect(response.status).to eq(200)
expect(IgnoredUser.find_by(user_id: user.id,
ignored_user_id: another_user.id)).to be_present
end
end
end
end
describe '#watch' do
it 'raises an error when not logged in' do
delete "/u/#{user.username}/ignore.json"
expect(response.status).to eq(403)
end
context 'while logged in' do
let(:user) { Fabricate(:user) }
let(:another_user) { Fabricate(:user) }
before do
sign_in(user)
end
describe 'when SiteSetting.ignore_user_enabled is false' do
it 'raises an error' do
SiteSetting.ignore_user_enabled = false
delete "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
expect(response.status).to eq(404)
end
end
describe 'when SiteSetting.ignore_user_enabled is true' do
context 'when ignore_user_enable is ON' do
before do
Fabricate(:ignored_user, user_id: user.id, ignored_user_id: another_user.id)
SiteSetting.ignore_user_enabled = true
end
it 'destroys IgnoredUser record' do
SiteSetting.ignore_user_enabled = true
delete "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
expect(response.status).to eq(200)
expect(IgnoredUser.find_by(user_id: user.id,
ignored_user_id: another_user.id)).to be_blank
let!(:ignored_user) { Fabricate(:ignored_user, user: user, ignored_user: another_user) }
let!(:muted_user) { Fabricate(:muted_user, user: user, muted_user: another_user) }
context 'when changing notification level to normal' do
it 'changes notification level to normal' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "normal" }
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.count).to eq(0)
end
end
context 'when changing notification level to mute' do
it 'changes notification level to mute' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "mute" }
expect(IgnoredUser.count).to eq(0)
expect(MutedUser.find_by(user_id: user.id, muted_user_id: another_user.id)).to be_present
end
end
context 'when changing notification level to ignore' do
it 'changes notification level to mute' do
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore" }
expect(MutedUser.count).to eq(0)
expect(IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)).to be_present
end
end
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe WebHookUserSerializer do
it 'should only include the required keys' do
count = serializer.as_json.keys.count
difference = count - 46
difference = count - 45
expect(difference).to eq(0), lambda {
message = ""