FEATURE: Introduce ignore duration selection (#7266)
* FEATURE: Introducing new UI for tracking User's ignored or muted states
This commit is contained in:
parent
961fb2c70e
commit
b1cb95fc23
|
@ -10,8 +10,7 @@ export default DatePicker.extend({
|
|||
moment()
|
||||
.add(1, "day")
|
||||
.toDate(),
|
||||
setDefaultDate: !!this.get("defaultDate"),
|
||||
minDate: new Date()
|
||||
setDefaultDate: !!this.get("defaultDate")
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,11 +10,13 @@ export default Ember.Component.extend({
|
|||
selection: null,
|
||||
date: null,
|
||||
time: null,
|
||||
includeDateTime: true,
|
||||
isCustom: Ember.computed.equal("selection", "pick_date_and_time"),
|
||||
isBasedOnLastPost: Ember.computed.equal(
|
||||
"selection",
|
||||
"set_based_on_last_post"
|
||||
),
|
||||
displayDateAndTimePicker: Ember.computed.and("includeDateTime", "isCustom"),
|
||||
displayLabel: null,
|
||||
|
||||
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`, {
|
||||
type: "PUT",
|
||||
data: { notification_level: level }
|
||||
data: { notification_level: level, expiring_at: expiringAt }
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
statusType=statusType
|
||||
value=selection
|
||||
input=input
|
||||
includeDateTime=includeDateTime
|
||||
includeWeekend=includeWeekend
|
||||
includeFarFuture=includeFarFuture
|
||||
includeMidFuture=includeMidFuture
|
||||
clearable=clearable
|
||||
none="topic.auto_update_input.none"}}
|
||||
</div>
|
||||
|
||||
{{#if isCustom}}
|
||||
{{#if displayDateAndTimePicker}}
|
||||
<div class="control-group">
|
||||
{{d-icon "calendar-alt"}} {{date-picker-future value=date defaultDate=date}}
|
||||
</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),
|
||||
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({
|
||||
id: "three_months",
|
||||
format: "MMM D",
|
||||
enabled: opts => opts.includeFarFuture,
|
||||
enabled: opts => opts.includeMidFuture,
|
||||
when: (time, timeOfDay) =>
|
||||
time
|
||||
.add(3, "month")
|
||||
|
@ -102,6 +114,18 @@ export const TIMEFRAMES = [
|
|||
.minute(0),
|
||||
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({
|
||||
id: "six_months",
|
||||
format: "MMM D",
|
||||
|
@ -139,6 +163,7 @@ export const TIMEFRAMES = [
|
|||
}),
|
||||
buildTimeframe({
|
||||
id: "pick_date_and_time",
|
||||
enabled: opts => opts.includeDateTime,
|
||||
icon: "far-calendar-plus"
|
||||
}),
|
||||
buildTimeframe({
|
||||
|
@ -192,7 +217,9 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
|
|||
now,
|
||||
day: now.day(),
|
||||
includeWeekend: this.get("includeWeekend"),
|
||||
includeMidFuture: this.get("includeMidFuture") || true,
|
||||
includeFarFuture: this.get("includeFarFuture"),
|
||||
includeDateTime: this.get("includeDateTime"),
|
||||
includeBasedOnLastPost: this.get("statusType") === CLOSE_STATUS_TYPE,
|
||||
canScheduleToday: 24 - now.hour() > 6
|
||||
};
|
||||
|
|
|
@ -1,85 +1,99 @@
|
|||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default DropdownSelectBox.extend({
|
||||
classNames: ["user-notifications", "user-notifications-dropdown"],
|
||||
nameProperty: "label",
|
||||
allowInitialValueMutation: false,
|
||||
|
||||
computeHeaderContent() {
|
||||
let content = this._super(...arguments);
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (this.get("user.ignored")) {
|
||||
this.set("headerIcon", "eye-slash");
|
||||
content.name = `${I18n.t("user.user_notifications_ignore_option")}`;
|
||||
this.set("value", "changeToIgnored");
|
||||
} else if (this.get("user.muted")) {
|
||||
this.set("headerIcon", "times-circle");
|
||||
content.name = `${I18n.t("user.user_notifications_mute_option")}`;
|
||||
this.set("value", "changeToMuted");
|
||||
} else {
|
||||
this.set("headerIcon", "user");
|
||||
content.name = `${I18n.t("user.user_notifications_normal_option")}`;
|
||||
this.set("value", "changeToNormal");
|
||||
}
|
||||
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")
|
||||
id: "changeToNormal",
|
||||
description: I18n.t("user.user_notifications.normal_option_title"),
|
||||
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")
|
||||
id: "changeToMuted",
|
||||
description: I18n.t("user.user_notifications.mute_option_title"),
|
||||
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")
|
||||
id: "changeToIgnored",
|
||||
description: I18n.t("user.user_notifications.ignore_option_title"),
|
||||
label: I18n.t("user.user_notifications.ignore_option")
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
changeToNormal() {
|
||||
this.get("updateNotificationLevel")("normal")
|
||||
.then(() => {
|
||||
this.set("user.ignored", false);
|
||||
this.set("user.muted", false);
|
||||
this.set("headerIcon", "user");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
changeToMuted() {
|
||||
this.get("updateNotificationLevel")("mute")
|
||||
.then(() => {
|
||||
this.set("user.ignored", false);
|
||||
this.set("user.muted", true);
|
||||
this.set("headerIcon", "times-circle");
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
changeToIgnored() {
|
||||
const controller = showModal("ignore-duration", {
|
||||
model: this.get("user")
|
||||
});
|
||||
controller.setProperties({
|
||||
onSuccess: () => {
|
||||
this.set("headerIcon", "eye-slash");
|
||||
},
|
||||
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: {
|
||||
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);
|
||||
onSelect(level) {
|
||||
this[level]();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -999,7 +999,12 @@ class UsersController < ApplicationController
|
|||
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)
|
||||
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"
|
||||
guardian.ensure_can_mute_user!(user.id)
|
||||
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
|
||||
|
|
|
@ -3,7 +3,7 @@ module Jobs
|
|||
every 1.day
|
||||
|
||||
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
|
||||
|
|
|
@ -702,12 +702,18 @@ en:
|
|||
new_private_message: "New Message"
|
||||
private_message: "Message"
|
||||
private_messages: "Messages"
|
||||
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."
|
||||
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, quotes you, or mentions you."
|
||||
user_notifications:
|
||||
ignore_duration_title: "Ignore Timer"
|
||||
ignore_duration_when: "Duration:"
|
||||
ignore_duration_save: "Ignore"
|
||||
ignore_duration_note: "Please note that all ignores are automatically removed after the ignore duration expires."
|
||||
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"
|
||||
preferences: "Preferences"
|
||||
profile_hidden: "This user's public profile is hidden."
|
||||
|
@ -1895,7 +1901,9 @@ en:
|
|||
next_week: "Next week"
|
||||
two_weeks: "Two Weeks"
|
||||
next_month: "Next month"
|
||||
two_months: "Two Months"
|
||||
three_months: "Three Months"
|
||||
four_months: "Four Months"
|
||||
six_months: "Six Months"
|
||||
one_year: "One Year"
|
||||
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
|
||||
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
|
||||
|
|
|
@ -2065,11 +2065,25 @@ describe UsersController do
|
|||
end
|
||||
|
||||
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" }
|
||||
expect(MutedUser.count).to eq(0)
|
||||
expect(IgnoredUser.find_by(user_id: user.id, ignored_user_id: another_user.id)).to be_present
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue