FEATURE: Introduce ignore duration selection (#7266)

* FEATURE: Introducing new UI for tracking User's ignored or muted states
This commit is contained in:
Tarek Khalil 2019-03-29 10:14:53 +00:00 committed by GitHub
parent 961fb2c70e
commit b1cb95fc23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 201 additions and 62 deletions

View File

@ -10,8 +10,7 @@ export default DatePicker.extend({
moment()
.add(1, "day")
.toDate(),
setDefaultDate: !!this.get("defaultDate"),
minDate: new Date()
setDefaultDate: !!this.get("defaultDate")
};
}
});

View File

@ -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() {

View File

@ -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));
}
}
});

View File

@ -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 }
});
},

View File

@ -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>

View File

@ -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>

View File

@ -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
};

View File

@ -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]();
}
}
});

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,5 @@
class AddExpiringAtColumnToIgnoredUsersTable < ActiveRecord::Migration[5.2]
def change
add_column :ignored_users, :expiring_at, :datetime
end
end

View File

@ -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

View File

@ -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