mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Do not disturb (#11484)
This commit is contained in:
parent
806f05f851
commit
649ed24bb4
@ -6,6 +6,7 @@ import PanEvents, {
|
|||||||
import { cancel, later, schedule } from "@ember/runloop";
|
import { cancel, later, schedule } from "@ember/runloop";
|
||||||
import Docking from "discourse/mixins/docking";
|
import Docking from "discourse/mixins/docking";
|
||||||
import MountWidget from "discourse/components/mount-widget";
|
import MountWidget from "discourse/components/mount-widget";
|
||||||
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import { observes } from "discourse-common/utils/decorators";
|
import { observes } from "discourse-common/utils/decorators";
|
||||||
import { topicTitleDecorators } from "discourse/components/topic-title";
|
import { topicTitleDecorators } from "discourse/components/topic-title";
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
|||||||
widget: "header",
|
widget: "header",
|
||||||
docAt: null,
|
docAt: null,
|
||||||
dockedHeader: null,
|
dockedHeader: null,
|
||||||
|
_listenToDoNotDisturbLoop: null,
|
||||||
_animate: false,
|
_animate: false,
|
||||||
_isPanning: false,
|
_isPanning: false,
|
||||||
_panMenuOrigin: "right",
|
_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() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
|
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
|
||||||
|
|
||||||
this.appEvents.on("header:show-topic", this, "setTopic");
|
this.appEvents.on("header:show-topic", this, "setTopic");
|
||||||
this.appEvents.on("header:hide-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("notifications:changed", "user-notifications");
|
||||||
this.dispatch("header:keyboard-trigger", "header");
|
this.dispatch("header:keyboard-trigger", "header");
|
||||||
@ -250,6 +272,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
|
|||||||
this.appEvents.off("dom:clean", this, "_cleanDom");
|
this.appEvents.off("dom:clean", this, "_cleanDom");
|
||||||
|
|
||||||
cancel(this._scheduledRemoveAnimate);
|
cancel(this._scheduledRemoveAnimate);
|
||||||
|
cancel(this._listenToDoNotDisturbLoop);
|
||||||
window.cancelAnimationFrame(this._scheduledMovingAnimation);
|
window.cancelAnimationFrame(this._scheduledMovingAnimation);
|
||||||
|
|
||||||
document.removeEventListener("click", this._dismissFirstNotification);
|
document.removeEventListener("click", this._dismissFirstNotification);
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -113,6 +113,10 @@ export default {
|
|||||||
user.notification_channel_position
|
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 site = container.lookup("site:main");
|
||||||
const siteSettings = container.lookup("site-settings:main");
|
const siteSettings = container.lookup("site-settings:main");
|
||||||
const router = container.lookup("router:main");
|
const router = container.lookup("router:main");
|
||||||
@ -130,7 +134,7 @@ export default {
|
|||||||
|
|
||||||
if (!isTesting()) {
|
if (!isTesting()) {
|
||||||
bus.subscribe(alertChannel(user), (data) =>
|
bus.subscribe(alertChannel(user), (data) =>
|
||||||
onNotification(data, siteSettings)
|
onNotification(data, siteSettings, user)
|
||||||
);
|
);
|
||||||
initDesktopNotifications(bus, appEvents);
|
initDesktopNotifications(bus, appEvents);
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ function isIdle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call-in point from message bus
|
// Call-in point from message bus
|
||||||
function onNotification(data, siteSettings) {
|
function onNotification(data, siteSettings, user) {
|
||||||
if (!liveEnabled) {
|
if (!liveEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -146,6 +146,9 @@ function onNotification(data, siteSettings) {
|
|||||||
if (!isIdle()) {
|
if (!isIdle()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (user.isInDoNotDisturb()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (keyValueStore.getItem("notifications-disabled")) {
|
if (keyValueStore.getItem("notifications-disabled")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ export function isPushNotificationsSupported(mobileView) {
|
|||||||
export function isPushNotificationsEnabled(user, mobileView) {
|
export function isPushNotificationsEnabled(user, mobileView) {
|
||||||
return (
|
return (
|
||||||
user &&
|
user &&
|
||||||
|
!user.isInDoNotDisturb() &&
|
||||||
isPushNotificationsSupported(mobileView) &&
|
isPushNotificationsSupported(mobileView) &&
|
||||||
keyValueStore.getItem(userSubscriptionKey(user))
|
keyValueStore.getItem(userSubscriptionKey(user))
|
||||||
);
|
);
|
||||||
|
@ -958,6 +958,37 @@ const User = RestModel.extend({
|
|||||||
return muted_ids.filter((existing_id) => existing_id !== id);
|
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, {
|
User.reopenClass(Singleton, {
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
{{d-icon icon}}
|
{{#if icon}}
|
||||||
|
{{d-icon icon}}
|
||||||
|
{{/if}}
|
||||||
{{ yield }}
|
{{ yield }}
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
{{#if userHasTimezoneSet}}
|
{{#if userHasTimezoneSet}}
|
||||||
{{#tap-tile-grid activeTile=selectedReminderType as |grid|}}
|
{{#tap-tile-grid activeTile=selectedReminderType as |grid|}}
|
||||||
{{#if showLaterToday}}
|
{{#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-title">{{i18n "bookmarks.reminders.later_today"}}</div>
|
||||||
<div class="tap-tile-date">{{laterTodayFormatted}}</div>
|
<div class="tap-tile-date">{{laterTodayFormatted}}</div>
|
||||||
{{/tap-tile}}
|
{{/tap-tile}}
|
||||||
|
@ -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>
|
@ -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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@ -64,66 +64,69 @@ createWidget("header-notifications", {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const unreadNotifications = user.get("unread_notifications");
|
if (user.isInDoNotDisturb()) {
|
||||||
if (!!unreadNotifications) {
|
contents.push(h("div.do-not-disturb-background", iconNode("moon")));
|
||||||
contents.push(
|
} else {
|
||||||
this.attach("link", {
|
const unreadNotifications = user.get("unread_notifications");
|
||||||
action: attrs.action,
|
if (!!unreadNotifications) {
|
||||||
className: "badge-notification unread-notifications",
|
contents.push(
|
||||||
rawLabel: unreadNotifications,
|
this.attach("link", {
|
||||||
omitSpan: true,
|
action: attrs.action,
|
||||||
title: "notifications.tooltip.regular",
|
className: "badge-notification unread-notifications",
|
||||||
titleOptions: { count: unreadNotifications },
|
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,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the counter for the unread high priority
|
const unreadHighPriority = user.get("unread_high_priority_notifications");
|
||||||
contents.push(
|
if (!!unreadHighPriority) {
|
||||||
this.attach("link", {
|
// highlight the avatar if the first ever PM is not read
|
||||||
action: attrs.action,
|
if (
|
||||||
className: "badge-notification unread-high-priority-notifications",
|
!user.get("read_first_notification") &&
|
||||||
rawLabel: unreadHighPriority,
|
!user.get("enforcedSecondFactor")
|
||||||
omitSpan: true,
|
) {
|
||||||
title: "notifications.tooltip.high_priority",
|
if (!attrs.active && attrs.ringBackdrop) {
|
||||||
titleOptions: { count: unreadHighPriority },
|
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;
|
return contents;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import { createWidgetFrom } from "discourse/widgets/widget";
|
|||||||
createWidgetFrom(QuickAccessPanel, "quick-access-notifications", {
|
createWidgetFrom(QuickAccessPanel, "quick-access-notifications", {
|
||||||
buildKey: () => "quick-access-notifications",
|
buildKey: () => "quick-access-notifications",
|
||||||
emptyStatePlaceholderItemKey: "notifications.empty",
|
emptyStatePlaceholderItemKey: "notifications.empty",
|
||||||
|
showDoNotDisturb: true,
|
||||||
|
|
||||||
markReadRequest() {
|
markReadRequest() {
|
||||||
return ajax("/notifications/mark-read", { type: "PUT" });
|
return ajax("/notifications/mark-read", { type: "PUT" });
|
||||||
|
@ -95,12 +95,17 @@ export default createWidget("quick-access-panel", {
|
|||||||
return [h("div.spinner-container", h("div.spinner"))];
|
return [h("div.spinner-container", h("div.spinner"))];
|
||||||
}
|
}
|
||||||
|
|
||||||
let bottomItems = [];
|
|
||||||
const items = this.getItems().length
|
const items = this.getItems().length
|
||||||
? this.getItems().map((item) => this.itemHtml(item))
|
? this.getItems().map((item) => this.itemHtml(item))
|
||||||
: [this.emptyStatePlaceholderItem()];
|
: [this.emptyStatePlaceholderItem()];
|
||||||
|
|
||||||
|
let bottomItems = [];
|
||||||
|
|
||||||
if (!this.hideBottomItems()) {
|
if (!this.hideBottomItems()) {
|
||||||
|
if (this.showDoNotDisturb) {
|
||||||
|
bottomItems.push(this.attach("do-not-disturb"));
|
||||||
|
}
|
||||||
|
|
||||||
bottomItems.push(
|
bottomItems.push(
|
||||||
// intentionally a link so it can be ctrl clicked
|
// intentionally a link so it can be ctrl clicked
|
||||||
this.attach("link", {
|
this.attach("link", {
|
||||||
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -205,6 +205,11 @@ export function acceptance(name, optionsOrCallback) {
|
|||||||
|
|
||||||
getApplication().reset();
|
getApplication().reset();
|
||||||
this.container = getOwner(this);
|
this.container = getOwner(this);
|
||||||
|
if (loggedIn) {
|
||||||
|
updateCurrentUser({
|
||||||
|
appEvents: this.container.lookup("service:app-events"),
|
||||||
|
});
|
||||||
|
}
|
||||||
setURLContainer(this.container);
|
setURLContainer(this.container);
|
||||||
setDefaultOwner(this.container);
|
setDefaultOwner(this.container);
|
||||||
|
|
||||||
|
@ -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 {
|
.ring-backdrop-spotlight {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
@ -65,6 +65,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 0%; // safari height fix
|
flex: 1 0 0%; // safari height fix
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.show-all {
|
.show-all {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
button {
|
button {
|
||||||
|
44
app/controllers/do_not_disturb_controller.rb
Normal file
44
app/controllers/do_not_disturb_controller.rb
Normal 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
|
13
app/models/do_not_disturb_timing.rb
Normal file
13
app/models/do_not_disturb_timing.rb
Normal 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
|
@ -282,7 +282,8 @@ class Notification < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_email
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -59,6 +59,7 @@ class User < ActiveRecord::Base
|
|||||||
has_many :group_requests, dependent: :delete_all
|
has_many :group_requests, dependent: :delete_all
|
||||||
has_many :muted_user_records, class_name: 'MutedUser', 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 :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)
|
# dependent deleting handled via before_destroy (special cases)
|
||||||
has_many :user_actions
|
has_many :user_actions
|
||||||
@ -635,6 +636,10 @@ class User < ActiveRecord::Base
|
|||||||
MessageBus.publish("/notification/#{id}", payload, user_ids: [id])
|
MessageBus.publish("/notification/#{id}", payload, user_ids: [id])
|
||||||
end
|
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)
|
def password=(password)
|
||||||
# special case for passwordless accounts
|
# special case for passwordless accounts
|
||||||
unless password.blank?
|
unless password.blank?
|
||||||
@ -1365,6 +1370,15 @@ class User < ActiveRecord::Base
|
|||||||
UrlHelper.encode_component(lower ? username_lower : username)
|
UrlHelper.encode_component(lower ? username_lower : username)
|
||||||
end
|
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
|
protected
|
||||||
|
|
||||||
def badge_grant
|
def badge_grant
|
||||||
|
@ -49,7 +49,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||||||
:title_count_mode,
|
:title_count_mode,
|
||||||
:timezone,
|
:timezone,
|
||||||
:featured_topic,
|
:featured_topic,
|
||||||
:skip_new_user_tips
|
:skip_new_user_tips,
|
||||||
|
:do_not_disturb_until,
|
||||||
|
|
||||||
def groups
|
def groups
|
||||||
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
|
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
|
||||||
@ -237,4 +238,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||||||
def featured_topic
|
def featured_topic
|
||||||
object.user_profile.featured_topic
|
object.user_profile.featured_topic
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def do_not_disturb_until
|
||||||
|
object.active_do_not_disturb_timings.maximum(:ends_at)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -464,6 +464,8 @@ class PostAlerter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def push_notification(user, payload)
|
def push_notification(user, payload)
|
||||||
|
return if user.do_not_disturb?
|
||||||
|
|
||||||
if user.push_subscriptions.exists?
|
if user.push_subscriptions.exists?
|
||||||
Jobs.enqueue(:send_push_notification, user_id: user.id, payload: payload)
|
Jobs.enqueue(:send_push_notification, user_id: user.id, payload: payload)
|
||||||
end
|
end
|
||||||
|
@ -932,7 +932,7 @@ en:
|
|||||||
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
|
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
|
||||||
disable: "Disable Notifications"
|
disable: "Disable Notifications"
|
||||||
enable: "Enable 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?"
|
consent_prompt: "Do you want live notifications when people reply to your posts?"
|
||||||
dismiss: "Dismiss"
|
dismiss: "Dismiss"
|
||||||
dismiss_notifications: "Dismiss All"
|
dismiss_notifications: "Dismiss All"
|
||||||
@ -3538,6 +3538,19 @@ en:
|
|||||||
|
|
||||||
image_removed: "(image removed)"
|
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
|
# This section is exported to the javascript for i18n in the admin section
|
||||||
admin_js:
|
admin_js:
|
||||||
type_to_filter: "type to filter..."
|
type_to_filter: "type to filter..."
|
||||||
|
@ -960,6 +960,9 @@ Discourse::Application.routes.draw do
|
|||||||
|
|
||||||
get "/permalink-check", to: 'permalinks#check'
|
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
|
get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
14
db/migrate/20201210151635_create_do_not_disturb_timings.rb
Normal file
14
db/migrate/20201210151635_create_do_not_disturb_timings.rb
Normal 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
|
@ -148,6 +148,7 @@ module SvgSprite
|
|||||||
"minus",
|
"minus",
|
||||||
"minus-circle",
|
"minus-circle",
|
||||||
"mobile-alt",
|
"mobile-alt",
|
||||||
|
"moon",
|
||||||
"paint-brush",
|
"paint-brush",
|
||||||
"paper-plane",
|
"paper-plane",
|
||||||
"pencil-alt",
|
"pencil-alt",
|
||||||
|
7
spec/fabricators/do_not_disturb_fabricator.rb
Normal file
7
spec/fabricators/do_not_disturb_fabricator.rb
Normal 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
|
16
spec/models/do_not_disturb_timing_spec.rb
Normal file
16
spec/models/do_not_disturb_timing_spec.rb
Normal 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
|
@ -373,6 +373,24 @@ describe Notification do
|
|||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
# pulling this out cause I don't want an observer
|
# pulling this out cause I don't want an observer
|
||||||
|
@ -2481,4 +2481,16 @@ describe User do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
46
spec/requests/do_not_disturb_controller_spec.rb
Normal file
46
spec/requests/do_not_disturb_controller_spec.rb
Normal 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
|
@ -731,6 +731,21 @@ describe PostAlerter do
|
|||||||
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
|
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
|
||||||
end
|
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
|
it "correctly pushes notifications if configured correctly" do
|
||||||
Jobs.run_immediately!
|
Jobs.run_immediately!
|
||||||
SiteSetting.allowed_user_api_push_urls = "https://site.com/push|https://site2.com/push"
|
SiteSetting.allowed_user_api_push_urls = "https://site.com/push|https://site2.com/push"
|
||||||
|
Loading…
Reference in New Issue
Block a user