mirror of
https://github.com/discourse/discourse.git
synced 2025-05-25 00:32:52 +08:00
FEATURE: Introduce ignore duration selection (#7266)
* FEATURE: Introducing new UI for tracking User's ignored or muted states
This commit is contained in:
@ -10,8 +10,7 @@ export default DatePicker.extend({
|
|||||||
moment()
|
moment()
|
||||||
.add(1, "day")
|
.add(1, "day")
|
||||||
.toDate(),
|
.toDate(),
|
||||||
setDefaultDate: !!this.get("defaultDate"),
|
setDefaultDate: !!this.get("defaultDate")
|
||||||
minDate: new Date()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -10,11 +10,13 @@ export default Ember.Component.extend({
|
|||||||
selection: null,
|
selection: null,
|
||||||
date: null,
|
date: null,
|
||||||
time: null,
|
time: null,
|
||||||
|
includeDateTime: true,
|
||||||
isCustom: Ember.computed.equal("selection", "pick_date_and_time"),
|
isCustom: Ember.computed.equal("selection", "pick_date_and_time"),
|
||||||
isBasedOnLastPost: Ember.computed.equal(
|
isBasedOnLastPost: Ember.computed.equal(
|
||||||
"selection",
|
"selection",
|
||||||
"set_based_on_last_post"
|
"set_based_on_last_post"
|
||||||
),
|
),
|
||||||
|
displayDateAndTimePicker: Ember.computed.and("includeDateTime", "isCustom"),
|
||||||
displayLabel: null,
|
displayLabel: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
|
loading: false,
|
||||||
|
ignoredUntil: null,
|
||||||
|
actions: {
|
||||||
|
ignore() {
|
||||||
|
if (!this.get("ignoredUntil")) {
|
||||||
|
this.flash(
|
||||||
|
I18n.t("user.user_notifications.ignore_duration_time_frame_required"),
|
||||||
|
"alert-error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.set("loading", true);
|
||||||
|
this.get("model")
|
||||||
|
.updateNotificationLevel("ignore", this.get("ignoredUntil"))
|
||||||
|
.then(() => {
|
||||||
|
this.set("model.ignored", true);
|
||||||
|
this.set("model.muted", false);
|
||||||
|
if (this.get("onSuccess")) {
|
||||||
|
this.get("onSuccess")();
|
||||||
|
}
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => this.set("loading", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -615,10 +615,10 @@ const User = RestModel.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNotificationLevel(level) {
|
updateNotificationLevel(level, expiringAt) {
|
||||||
return ajax(`${userPath(this.get("username"))}/notification_level.json`, {
|
return ajax(`${userPath(this.get("username"))}/notification_level.json`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: { notification_level: level }
|
data: { notification_level: level, expiring_at: expiringAt }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -6,13 +6,15 @@
|
|||||||
statusType=statusType
|
statusType=statusType
|
||||||
value=selection
|
value=selection
|
||||||
input=input
|
input=input
|
||||||
|
includeDateTime=includeDateTime
|
||||||
includeWeekend=includeWeekend
|
includeWeekend=includeWeekend
|
||||||
includeFarFuture=includeFarFuture
|
includeFarFuture=includeFarFuture
|
||||||
|
includeMidFuture=includeMidFuture
|
||||||
clearable=clearable
|
clearable=clearable
|
||||||
none="topic.auto_update_input.none"}}
|
none="topic.auto_update_input.none"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if isCustom}}
|
{{#if displayDateAndTimePicker}}
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
{{d-icon "calendar-alt"}} {{date-picker-future value=date defaultDate=date}}
|
{{d-icon "calendar-alt"}} {{date-picker-future value=date defaultDate=date}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
{{#d-modal-body title="user.user_notifications.ignore_duration_title" autoFocus="false"}}
|
||||||
|
{{future-date-input
|
||||||
|
label="user.user_notifications.ignore_duration_when"
|
||||||
|
input=ignoredUntil
|
||||||
|
includeWeekend=true
|
||||||
|
includeDateTime=false
|
||||||
|
includeMidFuture=true
|
||||||
|
includeFarFuture=false}}
|
||||||
|
<p>{{i18n "user.user_notifications.ignore_duration_note"}}</p>
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button class="btn-primary"
|
||||||
|
disabled=saveDisabled
|
||||||
|
label="user.user_notifications.ignore_duration_save"
|
||||||
|
action=(action "ignore")}}
|
||||||
|
|
||||||
|
{{conditional-loading-spinner size="small" condition=loading}}
|
||||||
|
</div>
|
@ -90,10 +90,22 @@ export const TIMEFRAMES = [
|
|||||||
.minute(0),
|
.minute(0),
|
||||||
icon: "briefcase"
|
icon: "briefcase"
|
||||||
}),
|
}),
|
||||||
|
buildTimeframe({
|
||||||
|
id: "two_months",
|
||||||
|
format: "MMM D",
|
||||||
|
enabled: opts => opts.includeMidFuture,
|
||||||
|
when: (time, timeOfDay) =>
|
||||||
|
time
|
||||||
|
.add(2, "month")
|
||||||
|
.startOf("month")
|
||||||
|
.hour(timeOfDay)
|
||||||
|
.minute(0),
|
||||||
|
icon: "briefcase"
|
||||||
|
}),
|
||||||
buildTimeframe({
|
buildTimeframe({
|
||||||
id: "three_months",
|
id: "three_months",
|
||||||
format: "MMM D",
|
format: "MMM D",
|
||||||
enabled: opts => opts.includeFarFuture,
|
enabled: opts => opts.includeMidFuture,
|
||||||
when: (time, timeOfDay) =>
|
when: (time, timeOfDay) =>
|
||||||
time
|
time
|
||||||
.add(3, "month")
|
.add(3, "month")
|
||||||
@ -102,6 +114,18 @@ export const TIMEFRAMES = [
|
|||||||
.minute(0),
|
.minute(0),
|
||||||
icon: "briefcase"
|
icon: "briefcase"
|
||||||
}),
|
}),
|
||||||
|
buildTimeframe({
|
||||||
|
id: "four_months",
|
||||||
|
format: "MMM D",
|
||||||
|
enabled: opts => opts.includeMidFuture,
|
||||||
|
when: (time, timeOfDay) =>
|
||||||
|
time
|
||||||
|
.add(4, "month")
|
||||||
|
.startOf("month")
|
||||||
|
.hour(timeOfDay)
|
||||||
|
.minute(0),
|
||||||
|
icon: "briefcase"
|
||||||
|
}),
|
||||||
buildTimeframe({
|
buildTimeframe({
|
||||||
id: "six_months",
|
id: "six_months",
|
||||||
format: "MMM D",
|
format: "MMM D",
|
||||||
@ -139,6 +163,7 @@ export const TIMEFRAMES = [
|
|||||||
}),
|
}),
|
||||||
buildTimeframe({
|
buildTimeframe({
|
||||||
id: "pick_date_and_time",
|
id: "pick_date_and_time",
|
||||||
|
enabled: opts => opts.includeDateTime,
|
||||||
icon: "far-calendar-plus"
|
icon: "far-calendar-plus"
|
||||||
}),
|
}),
|
||||||
buildTimeframe({
|
buildTimeframe({
|
||||||
@ -192,7 +217,9 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
|
|||||||
now,
|
now,
|
||||||
day: now.day(),
|
day: now.day(),
|
||||||
includeWeekend: this.get("includeWeekend"),
|
includeWeekend: this.get("includeWeekend"),
|
||||||
|
includeMidFuture: this.get("includeMidFuture") || true,
|
||||||
includeFarFuture: this.get("includeFarFuture"),
|
includeFarFuture: this.get("includeFarFuture"),
|
||||||
|
includeDateTime: this.get("includeDateTime"),
|
||||||
includeBasedOnLastPost: this.get("statusType") === CLOSE_STATUS_TYPE,
|
includeBasedOnLastPost: this.get("statusType") === CLOSE_STATUS_TYPE,
|
||||||
canScheduleToday: 24 - now.hour() > 6
|
canScheduleToday: 24 - now.hour() > 6
|
||||||
};
|
};
|
||||||
|
@ -1,85 +1,99 @@
|
|||||||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
export default DropdownSelectBox.extend({
|
export default DropdownSelectBox.extend({
|
||||||
classNames: ["user-notifications", "user-notifications-dropdown"],
|
classNames: ["user-notifications", "user-notifications-dropdown"],
|
||||||
nameProperty: "label",
|
nameProperty: "label",
|
||||||
allowInitialValueMutation: false,
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
computeHeaderContent() {
|
|
||||||
let content = this._super(...arguments);
|
|
||||||
if (this.get("user.ignored")) {
|
if (this.get("user.ignored")) {
|
||||||
this.set("headerIcon", "eye-slash");
|
this.set("headerIcon", "eye-slash");
|
||||||
content.name = `${I18n.t("user.user_notifications_ignore_option")}`;
|
this.set("value", "changeToIgnored");
|
||||||
} else if (this.get("user.muted")) {
|
} else if (this.get("user.muted")) {
|
||||||
this.set("headerIcon", "times-circle");
|
this.set("headerIcon", "times-circle");
|
||||||
content.name = `${I18n.t("user.user_notifications_mute_option")}`;
|
this.set("value", "changeToMuted");
|
||||||
} else {
|
} else {
|
||||||
this.set("headerIcon", "user");
|
this.set("headerIcon", "user");
|
||||||
content.name = `${I18n.t("user.user_notifications_normal_option")}`;
|
this.set("value", "changeToNormal");
|
||||||
}
|
}
|
||||||
return content;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computeContent() {
|
computeContent() {
|
||||||
const content = [];
|
const content = [];
|
||||||
|
|
||||||
content.push({
|
content.push({
|
||||||
icon: "user",
|
icon: "user",
|
||||||
id: "change-to-normal",
|
id: "changeToNormal",
|
||||||
description: I18n.t("user.user_notifications_normal_option_title"),
|
description: I18n.t("user.user_notifications.normal_option_title"),
|
||||||
action: () => this.send("reset"),
|
label: I18n.t("user.user_notifications.normal_option")
|
||||||
label: I18n.t("user.user_notifications_normal_option")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
content.push({
|
content.push({
|
||||||
icon: "times-circle",
|
icon: "times-circle",
|
||||||
id: "change-to-muted",
|
id: "changeToMuted",
|
||||||
description: I18n.t("user.user_notifications_mute_option_title"),
|
description: I18n.t("user.user_notifications.mute_option_title"),
|
||||||
action: () => this.send("mute"),
|
label: I18n.t("user.user_notifications.mute_option")
|
||||||
label: I18n.t("user.user_notifications_mute_option")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.get("user.can_ignore_user")) {
|
if (this.get("user.can_ignore_user")) {
|
||||||
content.push({
|
content.push({
|
||||||
icon: "eye-slash",
|
icon: "eye-slash",
|
||||||
id: "change-to-ignored",
|
id: "changeToIgnored",
|
||||||
description: I18n.t("user.user_notifications_ignore_option_title"),
|
description: I18n.t("user.user_notifications.ignore_option_title"),
|
||||||
action: () => this.send("ignore"),
|
label: I18n.t("user.user_notifications.ignore_option")
|
||||||
label: I18n.t("user.user_notifications_ignore_option")
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
changeToNormal() {
|
||||||
reset() {
|
|
||||||
this.get("updateNotificationLevel")("normal")
|
this.get("updateNotificationLevel")("normal")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.set("user.ignored", false);
|
this.set("user.ignored", false);
|
||||||
this.set("user.muted", false);
|
this.set("user.muted", false);
|
||||||
this.computeHeaderContent();
|
this.set("headerIcon", "user");
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
mute() {
|
changeToMuted() {
|
||||||
this.get("updateNotificationLevel")("mute")
|
this.get("updateNotificationLevel")("mute")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.set("user.ignored", false);
|
this.set("user.ignored", false);
|
||||||
this.set("user.muted", true);
|
this.set("user.muted", true);
|
||||||
this.computeHeaderContent();
|
this.set("headerIcon", "times-circle");
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
ignore() {
|
changeToIgnored() {
|
||||||
this.get("updateNotificationLevel")("ignore")
|
const controller = showModal("ignore-duration", {
|
||||||
.then(() => {
|
model: this.get("user")
|
||||||
this.set("user.ignored", true);
|
});
|
||||||
this.set("user.muted", false);
|
controller.setProperties({
|
||||||
this.computeHeaderContent();
|
onSuccess: () => {
|
||||||
})
|
this.set("headerIcon", "eye-slash");
|
||||||
.catch(popupAjaxError);
|
},
|
||||||
|
onClose: () => {
|
||||||
|
if (this.get("user.muted")) {
|
||||||
|
this.set("headerIcon", "times-circle");
|
||||||
|
this._select("changeToMuted");
|
||||||
|
} else if (!this.get("user.muted") && !this.get("user.ignored")) {
|
||||||
|
this.set("headerIcon", "user");
|
||||||
|
this._select("changeToNormal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_select(id) {
|
||||||
|
this.select(
|
||||||
|
this.collectionComputedContent.find(c => c.originalContent.id === id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
onSelect(level) {
|
||||||
|
this[level]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -999,7 +999,12 @@ class UsersController < ApplicationController
|
|||||||
if params[:notification_level] == "ignore"
|
if params[:notification_level] == "ignore"
|
||||||
guardian.ensure_can_ignore_user!(user.id)
|
guardian.ensure_can_ignore_user!(user.id)
|
||||||
MutedUser.where(user: current_user, muted_user: user).delete_all
|
MutedUser.where(user: current_user, muted_user: user).delete_all
|
||||||
IgnoredUser.find_or_create_by!(user: current_user, ignored_user: user)
|
ignored_user = IgnoredUser.find_by(user: current_user, ignored_user: user)
|
||||||
|
if ignored_user.present?
|
||||||
|
ignored_user.update(expiring_at: DateTime.parse(params[:expiring_at]))
|
||||||
|
else
|
||||||
|
IgnoredUser.create!(user: current_user, ignored_user: user, expiring_at: Time.parse(params[:expiring_at]))
|
||||||
|
end
|
||||||
elsif params[:notification_level] == "mute"
|
elsif params[:notification_level] == "mute"
|
||||||
guardian.ensure_can_mute_user!(user.id)
|
guardian.ensure_can_mute_user!(user.id)
|
||||||
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
|
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
|
||||||
|
@ -3,7 +3,7 @@ module Jobs
|
|||||||
every 1.day
|
every 1.day
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
IgnoredUser.where("created_at <= ?", 4.months.ago).delete_all
|
IgnoredUser.where("created_at <= ? OR expiring_at <= ?", 4.months.ago, Time.zone.now).delete_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -702,12 +702,18 @@ en:
|
|||||||
new_private_message: "New Message"
|
new_private_message: "New Message"
|
||||||
private_message: "Message"
|
private_message: "Message"
|
||||||
private_messages: "Messages"
|
private_messages: "Messages"
|
||||||
user_notifications_ignore_option: "Ignored"
|
user_notifications:
|
||||||
user_notifications_ignore_option_title: "You will not receive notifications related to this user and all of their topics and replies will be hidden."
|
ignore_duration_title: "Ignore Timer"
|
||||||
user_notifications_mute_option: "Muted"
|
ignore_duration_when: "Duration:"
|
||||||
user_notifications_mute_option_title: "You will not receive any notifications related to this user."
|
ignore_duration_save: "Ignore"
|
||||||
user_notifications_normal_option: "Normal"
|
ignore_duration_note: "Please note that all ignores are automatically removed after the ignore duration expires."
|
||||||
user_notifications_normal_option_title: "You will be notified if this user replies to you, quotes you, or mentions you."
|
ignore_duration_time_frame_required: "Please select a time frame"
|
||||||
|
ignore_option: "Ignored"
|
||||||
|
ignore_option_title: "You will not receive notifications related to this user and all of their topics and replies will be hidden."
|
||||||
|
mute_option: "Muted"
|
||||||
|
mute_option_title: "You will not receive any notifications related to this user."
|
||||||
|
normal_option: "Normal"
|
||||||
|
normal_option_title: "You will be notified if this user replies to you, quotes you, or mentions you."
|
||||||
activity_stream: "Activity"
|
activity_stream: "Activity"
|
||||||
preferences: "Preferences"
|
preferences: "Preferences"
|
||||||
profile_hidden: "This user's public profile is hidden."
|
profile_hidden: "This user's public profile is hidden."
|
||||||
@ -1895,7 +1901,9 @@ en:
|
|||||||
next_week: "Next week"
|
next_week: "Next week"
|
||||||
two_weeks: "Two Weeks"
|
two_weeks: "Two Weeks"
|
||||||
next_month: "Next month"
|
next_month: "Next month"
|
||||||
|
two_months: "Two Months"
|
||||||
three_months: "Three Months"
|
three_months: "Three Months"
|
||||||
|
four_months: "Four Months"
|
||||||
six_months: "Six Months"
|
six_months: "Six Months"
|
||||||
one_year: "One Year"
|
one_year: "One Year"
|
||||||
forever: "Forever"
|
forever: "Forever"
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddExpiringAtColumnToIgnoredUsersTable < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :ignored_users, :expiring_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
@ -39,5 +39,18 @@ describe Jobs::PurgeExpiredIgnoredUsers do
|
|||||||
expect(IgnoredUser.find_by(ignored_user: fred)).to be_nil
|
expect(IgnoredUser.find_by(ignored_user: fred)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when there are expired ignored users by expiring_at" do
|
||||||
|
let(:fred) { Fabricate(:user, username: "fred") }
|
||||||
|
|
||||||
|
it "purges expired ignored users" do
|
||||||
|
Fabricate(:ignored_user, user: tarek, ignored_user: fred, expiring_at: 1.month.from_now)
|
||||||
|
|
||||||
|
freeze_time(2.months.from_now) do
|
||||||
|
subject
|
||||||
|
expect(IgnoredUser.find_by(ignored_user: fred)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2065,11 +2065,25 @@ describe UsersController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when changing notification level to ignore' do
|
context 'when changing notification level to ignore' do
|
||||||
it 'changes notification level to mute' do
|
it 'changes notification level to ignore' do
|
||||||
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore" }
|
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore" }
|
||||||
expect(MutedUser.count).to eq(0)
|
expect(MutedUser.count).to eq(0)
|
||||||
expect(IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)).to be_present
|
expect(IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when expiring_at param is set' do
|
||||||
|
it 'changes notification level to ignore' do
|
||||||
|
freeze_time(Time.now) do
|
||||||
|
expiring_at = 3.days.from_now
|
||||||
|
put "/u/#{another_user.username}/notification_level.json", params: { notification_level: "ignore", expiring_at: expiring_at }
|
||||||
|
|
||||||
|
ignored_user = IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)
|
||||||
|
expect(ignored_user).to be_present
|
||||||
|
expect(ignored_user.expiring_at.to_i).to eq(expiring_at.to_i)
|
||||||
|
expect(MutedUser.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user