FEATURE: Do not disturb (#11484)

This commit is contained in:
Mark VanLandingham 2020-12-18 09:03:51 -06:00 committed by GitHub
parent 806f05f851
commit 649ed24bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 635 additions and 67 deletions

View File

@ -6,6 +6,7 @@ import PanEvents, {
import { cancel, later, schedule } from "@ember/runloop";
import Docking from "discourse/mixins/docking";
import MountWidget from "discourse/components/mount-widget";
import { isTesting } from "discourse-common/config/environment";
import { observes } from "discourse-common/utils/decorators";
import { topicTitleDecorators } from "discourse/components/topic-title";
@ -13,6 +14,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
widget: "header",
docAt: null,
dockedHeader: null,
_listenToDoNotDisturbLoop: null,
_animate: false,
_isPanning: false,
_panMenuOrigin: "right",
@ -194,12 +196,32 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
}
},
listenForDoNotDisturbChanges() {
if (this.currentUser && !this.currentUser.isInDoNotDisturb()) {
this.queueRerender();
} else {
cancel(this._listenToDoNotDisturbLoop);
this._listenToDoNotDisturbLoop = later(
this,
() => {
this.listenForDoNotDisturbChanges();
},
10000
);
}
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
this.appEvents.on("header:show-topic", this, "setTopic");
this.appEvents.on("header:hide-topic", this, "setTopic");
this.appEvents.on("do-not-disturb:changed", () => this.queueRerender());
if (!isTesting()) {
this.listenForDoNotDisturbChanges();
}
this.dispatch("notifications:changed", "user-notifications");
this.dispatch("header:keyboard-trigger", "header");
@ -250,6 +272,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
this.appEvents.off("dom:clean", this, "_cleanDom");
cancel(this._scheduledRemoveAnimate);
cancel(this._listenToDoNotDisturbLoop);
window.cancelAnimationFrame(this._scheduledMovingAnimation);
document.removeEventListener("click", this._dismissFirstNotification);

View File

@ -0,0 +1,36 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, {
duration: null,
saving: false,
@discourseComputed("saving", "duration")
saveDisabled(saving, duration) {
return saving || !duration;
},
@action
setDuration(duration) {
this.set("duration", duration);
},
@action
save() {
this.set("saving", true);
this.currentUser
.enterDoNotDisturbFor(this.duration)
.then(() => {
this.send("closeModal");
})
.catch((e) => {
this.flash(extractError(e), "error");
})
.finally(() => {
this.set("saving", false);
});
},
});

View File

@ -113,6 +113,10 @@ export default {
user.notification_channel_position
);
bus.subscribe(`/do-not-disturb/${user.get("id")}`, (data) => {
user.updateDoNotDisturbStatus(data.ends_at);
});
const site = container.lookup("site:main");
const siteSettings = container.lookup("site-settings:main");
const router = container.lookup("router:main");
@ -130,7 +134,7 @@ export default {
if (!isTesting()) {
bus.subscribe(alertChannel(user), (data) =>
onNotification(data, siteSettings)
onNotification(data, siteSettings, user)
);
initDesktopNotifications(bus, appEvents);

View File

@ -136,7 +136,7 @@ function isIdle() {
}
// Call-in point from message bus
function onNotification(data, siteSettings) {
function onNotification(data, siteSettings, user) {
if (!liveEnabled) {
return;
}
@ -146,6 +146,9 @@ function onNotification(data, siteSettings) {
if (!isIdle()) {
return;
}
if (user.isInDoNotDisturb()) {
return;
}
if (keyValueStore.getItem("notifications-disabled")) {
return;
}

View File

@ -75,6 +75,7 @@ export function isPushNotificationsSupported(mobileView) {
export function isPushNotificationsEnabled(user, mobileView) {
return (
user &&
!user.isInDoNotDisturb() &&
isPushNotificationsSupported(mobileView) &&
keyValueStore.getItem(userSubscriptionKey(user))
);

View File

@ -958,6 +958,37 @@ const User = RestModel.extend({
return muted_ids.filter((existing_id) => existing_id !== id);
}
},
enterDoNotDisturbFor(duration) {
return ajax({
url: "/do-not-disturb.json",
type: "POST",
data: { duration },
}).then((response) => {
return this.updateDoNotDisturbStatus(response.ends_at);
});
},
leaveDoNotDisturb() {
return ajax({
url: "/do-not-disturb.json",
type: "DELETE",
}).then(() => {
this.updateDoNotDisturbStatus(null);
});
},
updateDoNotDisturbStatus(ends_at) {
this.set("do_not_disturb_until", ends_at);
this.appEvents.trigger("do-not-disturb:changed", this.do_not_disturb_until);
},
isInDoNotDisturb() {
return (
this.do_not_disturb_until &&
new Date(this.do_not_disturb_until) >= new Date()
);
},
});
User.reopenClass(Singleton, {

View File

@ -1,2 +1,4 @@
{{d-icon icon}}
{{#if icon}}
{{d-icon icon}}
{{/if}}
{{ yield }}

View File

@ -38,7 +38,7 @@
{{#if userHasTimezoneSet}}
{{#tap-tile-grid activeTile=selectedReminderType as |grid|}}
{{#if showLaterToday}}
{{#tap-tile icon="far-moon" tileId=reminderTypes.LATER_TODAY activeTile=grid.activeTile onChange=(action "selectReminderType")}}
{{#tap-tile icon="angle-right" tileId=reminderTypes.LATER_TODAY activeTile=grid.activeTile onChange=(action "selectReminderType")}}
<div class="tap-tile-title">{{i18n "bookmarks.reminders.later_today"}}</div>
<div class="tap-tile-date">{{laterTodayFormatted}}</div>
{{/tap-tile}}

View File

@ -0,0 +1,24 @@
{{#d-modal-body title="do_not_disturb.title"}}
{{#tap-tile-grid activeTile=duration as |grid|}}
{{#tap-tile class="do-not-disturb-tile" tileId="30" activeTile=grid.activeTile onChange=(action "setDuration")}}
{{i18n "do_not_disturb.options.half_hour"}}
{{/tap-tile}}
{{#tap-tile class="do-not-disturb-tile" tileId="60" activeTile=grid.activeTile onChange=(action "setDuration")}}
{{i18n "do_not_disturb.options.one_hour"}}
{{/tap-tile}}
{{#tap-tile class="do-not-disturb-tile" tileId="120" activeTile=grid.activeTile onChange=(action "setDuration")}}
{{i18n "do_not_disturb.options.two_hours"}}
{{/tap-tile}}
{{#tap-tile class="do-not-disturb-tile" tileId="tomorrow" activeTile=grid.activeTile onChange=(action "setDuration")}}
{{i18n "do_not_disturb.options.tomorrow"}}
{{/tap-tile}}
{{/tap-tile-grid}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button
label="do_not_disturb.save"
action=(action "save")
class="btn-primary"
disabled=saveDisabled
}}
</div>

View File

@ -0,0 +1,54 @@
import I18n from "I18n";
import { createWidget } from "discourse/widgets/widget";
import { h } from "virtual-dom";
import { iconNode } from "discourse-common/lib/icon-library";
import showModal from "discourse/lib/show-modal";
export default createWidget("do-not-disturb", {
tagName: "div.btn.do-not-disturb-btn",
saving: false,
html() {
if (this.currentUser.isInDoNotDisturb()) {
let remainingTime = moment()
.to(moment(this.currentUser.do_not_disturb_until))
.split(" ")
.slice(1)
.join(" "); // The first word is "in" and we don't want that.
return [
h("div.do-not-disturb-inner-container", [
h("div.do-not-disturb-background", iconNode("moon")),
h("span.do-not-disturb-label", [
h("span", I18n.t("do_not_disturb.label")),
h(
"span.time-remaining",
I18n.t("do_not_disturb.remaining", { remaining: remainingTime })
),
]),
]),
];
} else {
return [
iconNode("far-moon"),
h("span.do-not-disturb-label", I18n.t("do_not_disturb.label")),
];
}
},
click() {
if (this.saving) {
return;
}
this.saving = true;
if (this.currentUser.do_not_disturb_until) {
return this.currentUser.leaveDoNotDisturb().then(() => {
this.saving = false;
});
} else {
this.saving = false;
return showModal("do-not-disturb");
}
},
});

View File

@ -64,66 +64,69 @@ createWidget("header-notifications", {
),
];
const unreadNotifications = user.get("unread_notifications");
if (!!unreadNotifications) {
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-notifications",
rawLabel: unreadNotifications,
omitSpan: true,
title: "notifications.tooltip.regular",
titleOptions: { count: unreadNotifications },
})
);
}
const unreadHighPriority = user.get("unread_high_priority_notifications");
if (!!unreadHighPriority) {
// highlight the avatar if the first ever PM is not read
if (
!user.get("read_first_notification") &&
!user.get("enforcedSecondFactor")
) {
if (!attrs.active && attrs.ringBackdrop) {
contents.push(h("span.ring"));
contents.push(h("span.ring-backdrop-spotlight"));
contents.push(
h(
"span.ring-backdrop",
{},
h("h1.ring-first-notification", {}, [
h("span", {}, I18n.t("user.first_notification")),
h("span", {}, [
I18n.t("user.skip_new_user_tips.not_first_time"),
" ",
this.attach("link", {
action: "skipNewUserTips",
className: "skip-new-user-tips",
label: "user.skip_new_user_tips.skip_link",
title: "user.skip_new_user_tips.description",
omitSpan: true,
}),
]),
])
)
);
}
if (user.isInDoNotDisturb()) {
contents.push(h("div.do-not-disturb-background", iconNode("moon")));
} else {
const unreadNotifications = user.get("unread_notifications");
if (!!unreadNotifications) {
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-notifications",
rawLabel: unreadNotifications,
omitSpan: true,
title: "notifications.tooltip.regular",
titleOptions: { count: unreadNotifications },
})
);
}
// add the counter for the unread high priority
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-high-priority-notifications",
rawLabel: unreadHighPriority,
omitSpan: true,
title: "notifications.tooltip.high_priority",
titleOptions: { count: unreadHighPriority },
})
);
}
const unreadHighPriority = user.get("unread_high_priority_notifications");
if (!!unreadHighPriority) {
// highlight the avatar if the first ever PM is not read
if (
!user.get("read_first_notification") &&
!user.get("enforcedSecondFactor")
) {
if (!attrs.active && attrs.ringBackdrop) {
contents.push(h("span.ring"));
contents.push(h("span.ring-backdrop-spotlight"));
contents.push(
h(
"span.ring-backdrop",
{},
h("h1.ring-first-notification", {}, [
h("span", {}, I18n.t("user.first_notification")),
h("span", {}, [
I18n.t("user.skip_new_user_tips.not_first_time"),
" ",
this.attach("link", {
action: "skipNewUserTips",
className: "skip-new-user-tips",
label: "user.skip_new_user_tips.skip_link",
title: "user.skip_new_user_tips.description",
omitSpan: true,
}),
]),
])
)
);
}
}
// add the counter for the unread high priority
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-high-priority-notifications",
rawLabel: unreadHighPriority,
omitSpan: true,
title: "notifications.tooltip.high_priority",
titleOptions: { count: unreadHighPriority },
})
);
}
}
return contents;
},
});

View File

@ -5,6 +5,7 @@ import { createWidgetFrom } from "discourse/widgets/widget";
createWidgetFrom(QuickAccessPanel, "quick-access-notifications", {
buildKey: () => "quick-access-notifications",
emptyStatePlaceholderItemKey: "notifications.empty",
showDoNotDisturb: true,
markReadRequest() {
return ajax("/notifications/mark-read", { type: "PUT" });

View File

@ -95,12 +95,17 @@ export default createWidget("quick-access-panel", {
return [h("div.spinner-container", h("div.spinner"))];
}
let bottomItems = [];
const items = this.getItems().length
? this.getItems().map((item) => this.itemHtml(item))
: [this.emptyStatePlaceholderItem()];
let bottomItems = [];
if (!this.hideBottomItems()) {
if (this.showDoNotDisturb) {
bottomItems.push(this.attach("do-not-disturb"));
}
bottomItems.push(
// intentionally a link so it can be ctrl clicked
this.attach("link", {

View File

@ -0,0 +1,71 @@
import {
acceptance,
exists,
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
acceptance("Do not disturb", function (needs) {
needs.user();
needs.pretender((server, helper) => {
server.post("/do-not-disturb.json", () => {
let now = new Date();
now.setHours(now.getHours() + 1);
return helper.response({ ends_at: now });
});
server.delete("/do-not-disturb.json", () =>
helper.response({ success: true })
);
});
test("when turned off, it is turned on from modal", async function (assert) {
updateCurrentUser({ do_not_disturb_until: null });
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".do-not-disturb-btn");
assert.ok(exists(".do-not-disturb-modal"), "modal to choose time appears");
let tiles = queryAll(".do-not-disturb-tile");
assert.ok(tiles.length === 4, "There are 4 duration choices");
await click(tiles[0]);
await click(".modal-footer .btn.btn-primary");
assert.ok(
queryAll(".do-not-disturb-modal")[0].style.display === "none",
"modal is hidden"
);
assert.ok(
exists(".header-dropdown-toggle .do-not-disturb-background"),
"moon icon is present in header"
);
});
test("when turned on, it can be turned off", async function (assert) {
let now = new Date();
now.setHours(now.getHours() + 1);
updateCurrentUser({ do_not_disturb_until: now });
await visit("/");
await click(".header-dropdown-toggle.current-user");
assert.ok(
queryAll(".do-not-disturb-btn .time-remaining")[0].textContent ===
"an hour remaining",
"The remaining time is displayed"
);
await click(".do-not-disturb-btn");
assert.ok(
queryAll(".do-not-disturb-background").length === 0,
"The active moon icons are removed"
);
});
});

View File

@ -205,6 +205,11 @@ export function acceptance(name, optionsOrCallback) {
getApplication().reset();
this.container = getOwner(this);
if (loggedIn) {
updateCurrentUser({
appEvents: this.container.lookup("service:app-events"),
});
}
setURLContainer(this.container);
setDefaultOwner(this.container);

View File

@ -416,6 +416,85 @@ table {
}
}
.d-header .header-dropdown-toggle .do-not-disturb-background {
position: absolute;
left: 2px;
bottom: -1px;
z-index: 1002;
}
.do-not-disturb-background {
display: flex;
align-items: center;
justify-content: center;
width: 1.25em;
background-color: var(--tertiary-med-or-tertiary);
border-radius: 50%;
height: 1.25em;
.d-icon.d-icon-moon {
color: var(--tertiary-or-white) !important;
line-height: unset;
font-size: 0.875em;
margin: 0;
}
}
.do-not-disturb-btn {
display: flex;
flex: 0 0 100%;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin-bottom: 0.5em;
.do-not-disturb-inner-container {
display: flex;
align-items: center;
.do-not-disturb-label {
display: flex;
flex-direction: column;
padding-top: 2px;
margin-left: 0.6em;
}
.time-remaining {
text-align: left;
font-size: $font-down-3;
margin-top: 0.3em;
color: var(--primary-medium);
}
.do-not-disturb-background {
width: 1.75em;
height: 1.75em;
.d-icon-far-moon {
margin-right: 0;
}
}
}
}
.do-not-disturb-modal {
.do-not-disturb-choice {
display: grid;
grid-template-columns: 2em 1fr auto;
grid-template-rows: auto auto;
align-items: center;
cursor: pointer;
padding: 0.5em 0;
&:hover {
background-color: var(--tertiary-low);
}
label {
margin-bottom: 0;
}
}
}
.ring-backdrop-spotlight {
position: absolute;
width: 80px;

View File

@ -65,6 +65,8 @@
display: flex;
flex: 1 0 0%; // safari height fix
margin-top: 0.5em;
flex-wrap: wrap;
.show-all {
flex: 1 1 auto;
button {
@ -236,7 +238,7 @@
color: var(--primary);
}
span.double-user,
span.double-user,
// e.g., "username, username2"
span.multi-user
// e.g., "username, username2, and n others"
@ -247,7 +249,7 @@
white-space: nowrap;
}
span.multi-user
span.multi-user
// e.g., "username, username2, and n others"
{
span.multi-username:nth-of-type(2) {

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class DoNotDisturbController < ApplicationController
requires_login
def create
raise Discourse::InvalidParameters.new(:duration) if params[:duration].blank?
duration_minutes = (Integer(params[:duration]) rescue false)
ends_at = duration_minutes ?
ends_at_from_minutes(duration_minutes) :
ends_at_from_string(params[:duration])
new_timing = current_user.do_not_disturb_timings.new(starts_at: Time.zone.now, ends_at: ends_at)
if new_timing.save
current_user.publish_do_not_disturb(ends_at: ends_at)
render json: { ends_at: ends_at }
else
render_json_error(new_timing)
end
end
def destroy
current_user.active_do_not_disturb_timings.destroy_all
current_user.publish_do_not_disturb(ends_at: nil)
render json: success_json
end
private
def ends_at_from_minutes(duration)
duration.minutes.from_now
end
def ends_at_from_string(string)
if string == 'tomorrow'
Time.now.end_of_day.utc
else
raise Discourse::InvalidParameters.new(:duration)
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class DoNotDisturbTiming < ActiveRecord::Base
belongs_to :user
validate :ends_at_greater_thans_starts_at
def ends_at_greater_thans_starts_at
if starts_at > ends_at
errors.add(:ends_at, :invalid)
end
end
end

View File

@ -282,7 +282,8 @@ class Notification < ActiveRecord::Base
end
def send_email
NotificationEmailer.process_notification(self) if !skip_send_email
return if skip_send_email || user.do_not_disturb? # TODO: 'shelve' emails rather than skipping them entirely
NotificationEmailer.process_notification(self)
end
end

View File

@ -59,6 +59,7 @@ class User < ActiveRecord::Base
has_many :group_requests, dependent: :delete_all
has_many :muted_user_records, class_name: 'MutedUser', dependent: :delete_all
has_many :ignored_user_records, class_name: 'IgnoredUser', dependent: :delete_all
has_many :do_not_disturb_timings, dependent: :delete_all
# dependent deleting handled via before_destroy (special cases)
has_many :user_actions
@ -635,6 +636,10 @@ class User < ActiveRecord::Base
MessageBus.publish("/notification/#{id}", payload, user_ids: [id])
end
def publish_do_not_disturb(ends_at: nil)
MessageBus.publish("/do-not-disturb/#{id}", { ends_at: ends_at }, user_ids: [id])
end
def password=(password)
# special case for passwordless accounts
unless password.blank?
@ -1365,6 +1370,15 @@ class User < ActiveRecord::Base
UrlHelper.encode_component(lower ? username_lower : username)
end
def do_not_disturb?
active_do_not_disturb_timings.exists?
end
def active_do_not_disturb_timings
now = Time.zone.now
do_not_disturb_timings.where('starts_at <= ? AND ends_at > ?', now, now)
end
protected
def badge_grant

View File

@ -49,7 +49,8 @@ class CurrentUserSerializer < BasicUserSerializer
:title_count_mode,
:timezone,
:featured_topic,
:skip_new_user_tips
:skip_new_user_tips,
:do_not_disturb_until,
def groups
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
@ -237,4 +238,8 @@ class CurrentUserSerializer < BasicUserSerializer
def featured_topic
object.user_profile.featured_topic
end
def do_not_disturb_until
object.active_do_not_disturb_timings.maximum(:ends_at)
end
end

View File

@ -464,6 +464,8 @@ class PostAlerter
end
def push_notification(user, payload)
return if user.do_not_disturb?
if user.push_subscriptions.exists?
Jobs.enqueue(:send_push_notification, user_id: user.id, payload: payload)
end

View File

@ -932,7 +932,7 @@ en:
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
disable: "Disable Notifications"
enable: "Enable Notifications"
each_browser_note: "Note: You have to change this setting on every browser you use."
each_browser_note: 'Note: You have to change this setting on every browser you use. All notifications will be disabled when in "do not disturb", regardless of this setting.'
consent_prompt: "Do you want live notifications when people reply to your posts?"
dismiss: "Dismiss"
dismiss_notifications: "Dismiss All"
@ -3538,6 +3538,19 @@ en:
image_removed: "(image removed)"
do_not_disturb:
title: "Do not disturb for..."
save: "Save"
label: "Do not disturb"
remaining: "%{remaining} remaining"
options:
half_hour: "30 minutes"
one_hour: "1 hour"
two_hours: "2 hours"
tomorrow: "Until tomorrow"
custom: "Custom"
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "type to filter..."

View File

@ -960,6 +960,9 @@ Discourse::Application.routes.draw do
get "/permalink-check", to: 'permalinks#check'
post "/do-not-disturb" => "do_not_disturb#create"
delete "/do-not-disturb" => "do_not_disturb#destroy"
get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateDoNotDisturbTimings < ActiveRecord::Migration[6.0]
def change
create_table :do_not_disturb_timings do |t|
t.integer :user_id, null: false
t.datetime :starts_at, null: false
t.datetime :ends_at, null: false
end
add_index :do_not_disturb_timings, [:user_id], unique: false
add_index :do_not_disturb_timings, [:starts_at], unique: false
add_index :do_not_disturb_timings, [:ends_at], unique: false
end
end

View File

@ -148,6 +148,7 @@ module SvgSprite
"minus",
"minus-circle",
"mobile-alt",
"moon",
"paint-brush",
"paper-plane",
"pencil-alt",

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:do_not_disturb_timing) do
user
starts_at { Time.zone.now }
ends_at { 1.hour.from_now }
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'rails_helper'
describe DoNotDisturbTiming do
fab!(:user) { Fabricate(:user) }
describe "validations" do
it 'is invalid when ends_at is before starts_at' do
freeze_time
timing = DoNotDisturbTiming.new(user: user, starts_at: Time.zone.now, ends_at: 1.hour.ago)
timing.valid?
expect(timing.errors[:ends_at]).to be_present
end
end
end

View File

@ -373,6 +373,24 @@ describe Notification do
end
end
describe "do not disturb" do
it "calls NotificationEmailer.process_notification when user is not in 'do not disturb'" do
user = Fabricate(:user)
notification = Notification.new(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1)
NotificationEmailer.expects(:process_notification).with(notification)
notification.save!
end
it "doesn't call NotificationEmailer.process_notification when user is in 'do not disturb'" do
freeze_time
user = Fabricate(:user)
Fabricate(:do_not_disturb_timing, user: user, starts_at: Time.zone.now, ends_at: 1.day.from_now)
notification = Notification.new(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1)
NotificationEmailer.expects(:process_notification).with(notification).never
notification.save!
end
end
end
# pulling this out cause I don't want an observer

View File

@ -2481,4 +2481,16 @@ describe User do
end
end
end
describe "#do_not_disturb?" do
it "is true when a dnd timing is present for the current time" do
Fabricate(:do_not_disturb_timing, user: user, starts_at: Time.zone.now, ends_at: 1.day.from_now)
expect(user.do_not_disturb?).to eq(true)
end
it "is false when no dnd timing is present for the current time" do
Fabricate(:do_not_disturb_timing, user: user, starts_at: Time.zone.now - 2.day, ends_at: 1.minute.ago)
expect(user.do_not_disturb?).to eq(false)
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'rails_helper'
describe DoNotDisturbController do
it 'requires you to be logged in' do
post "/do-not-disturb.json", params: { duration: 30 }
expect(response.status).to eq(403)
end
describe 'logged in' do
fab!(:user) { Fabricate(:user) }
before do
sign_in(user)
end
it 'returns a 400 when a duration is not passed in' do
post "/do-not-disturb.json"
expect(response.status).to eq(400)
end
it 'works properly with integer minute durations' do
freeze_time
post "/do-not-disturb.json", params: { duration: 30 }
expect(response.status).to eq(200)
expect(user.do_not_disturb_timings.last.ends_at).to eq_time(30.minutes.from_now)
end
it 'works properly with integer minute durations' do
post "/do-not-disturb.json", params: { duration: -30 }
expect(response.status).to eq(422)
expect(response.parsed_body).to eq({ "errors" => ["Ends at is invalid"] })
end
include ActiveSupport::Testing::TimeHelpers
it "works properly with duration of 'tomorrow'" do
travel_to Time.new(2020, 11, 24, 01, 04, 44) do
post "/do-not-disturb.json", params: { duration: 'tomorrow' }
expect(response.status).to eq(200)
expect(user.do_not_disturb_timings.last.ends_at.to_i).to eq(Time.new(2020, 11, 24, 23, 59, 59).utc.to_i)
end
end
end
end

View File

@ -731,6 +731,21 @@ describe PostAlerter do
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
end
it "pushes nothing when the user is in 'do not disturb'" do
SiteSetting.allowed_user_api_push_urls = "https://site.com/push|https://site2.com/push"
2.times do |i|
UserApiKey.create!(user_id: evil_trout.id,
client_id: "xxx#{i}",
application_name: "iPhone#{i}",
scopes: ['notifications'].map { |name| UserApiKeyScope.new(name: name) },
push_url: "https://site2.com/push")
end
Fabricate(:do_not_disturb_timing, user: evil_trout, starts_at: Time.zone.now, ends_at: 1.day.from_now)
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
end
it "correctly pushes notifications if configured correctly" do
Jobs.run_immediately!
SiteSetting.allowed_user_api_push_urls = "https://site.com/push|https://site2.com/push"