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:
parent
675d950eab
commit
ef2362a30f
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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";
|
||||
|
|
|
@ -97,7 +97,6 @@
|
|||
|
||||
.user-main {
|
||||
.about {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
Loading…
Reference in New Issue