mirror of
https://github.com/discourse/discourse.git
synced 2025-04-01 17:05:43 +08:00
FEATURE: integrate DnD with user status (#19410)
This PR adds a new "Pause notifications" checkbox to the user status modal. This checkbox allows enabling the Do-Not-Disturb mode together with user status. Note that we don't remove and don't rename the existing DnD menu item in this PR, so the old way of entering the DnD mode is still available. Also, we're not making DnD mode a part of user status on backend and in database. The reason is that the DnD mode should still be available on sites with disabled user status, having them separated helps keep the implementation simple.
This commit is contained in:
parent
c358151a6c
commit
4908a669e0
@ -77,7 +77,9 @@
|
|||||||
<span class="item-label">
|
<span class="item-label">
|
||||||
{{#if this.isInDoNotDisturb}}
|
{{#if this.isInDoNotDisturb}}
|
||||||
<span>{{i18n "do_not_disturb.label"}}</span>
|
<span>{{i18n "do_not_disturb.label"}}</span>
|
||||||
{{format-age this.doNotDisturbDateTime}}
|
{{#if this.showDoNotDisturbEndDate}}
|
||||||
|
{{format-age this.doNotDisturbDateTime}}
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{i18n "do_not_disturb.label"}}
|
{{i18n "do_not_disturb.label"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import DoNotDisturb from "discourse/lib/do-not-disturb";
|
||||||
|
|
||||||
export default class UserMenuProfileTabContent extends Component {
|
export default class UserMenuProfileTabContent extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@ -27,6 +28,12 @@ export default class UserMenuProfileTabContent extends Component {
|
|||||||
return this.#doNotDisturbUntilDate.getTime();
|
return this.#doNotDisturbUntilDate.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showDoNotDisturbEndDate() {
|
||||||
|
return !DoNotDisturb.isEternal(
|
||||||
|
this.currentUser.get("do_not_disturb_until")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get #doNotDisturbUntilDate() {
|
get #doNotDisturbUntilDate() {
|
||||||
if (!this.currentUser.get("do_not_disturb_until")) {
|
if (!this.currentUser.get("do_not_disturb_until")) {
|
||||||
return;
|
return;
|
||||||
@ -63,7 +70,9 @@ export default class UserMenuProfileTabContent extends Component {
|
|||||||
modalClass: "user-status",
|
modalClass: "user-status",
|
||||||
model: {
|
model: {
|
||||||
status: this.currentUser.status,
|
status: this.currentUser.status,
|
||||||
saveAction: (status) => this.userStatus.set(status),
|
pauseNotifications: this.currentUser.isInDoNotDisturb(),
|
||||||
|
saveAction: (status, pauseNotifications) =>
|
||||||
|
this.userStatus.set(status, pauseNotifications),
|
||||||
deleteAction: () => this.userStatus.clear(),
|
deleteAction: () => this.userStatus.clear(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -157,6 +157,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
modalClass: "user-status",
|
modalClass: "user-status",
|
||||||
model: {
|
model: {
|
||||||
status,
|
status,
|
||||||
|
hidePauseNotifications: true,
|
||||||
saveAction: (s) => this.set("newStatus", s),
|
saveAction: (s) => this.set("newStatus", s),
|
||||||
deleteAction: () => this.set("newStatus", null),
|
deleteAction: () => this.set("newStatus", null),
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,8 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
const currentStatus = { ...this.model.status };
|
const currentStatus = { ...this.model.status };
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
status: currentStatus,
|
status: currentStatus,
|
||||||
|
hidePauseNotifications: this.model.hidePauseNotifications,
|
||||||
|
pauseNotifications: this.model.pauseNotifications,
|
||||||
showDeleteButton: !!this.model.status,
|
showDeleteButton: !!this.model.status,
|
||||||
timeShortcuts: this._buildTimeShortcuts(),
|
timeShortcuts: this._buildTimeShortcuts(),
|
||||||
prefilledDateTime: currentStatus?.ends_at,
|
prefilledDateTime: currentStatus?.ends_at,
|
||||||
@ -70,7 +72,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
ends_at: this.status.endsAt?.toISOString(),
|
ends_at: this.status.endsAt?.toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.resolve(this.model.saveAction(newStatus))
|
Promise.resolve(this.model.saveAction(newStatus, this.pauseNotifications))
|
||||||
.then(() => this.send("closeModal"))
|
.then(() => this.send("closeModal"))
|
||||||
.catch((e) => this._handleError(e));
|
.catch((e) => this._handleError(e));
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export default class DoNotDisturb {
|
||||||
|
static forever = "3000-01-01T00:00:00.000Z";
|
||||||
|
|
||||||
|
static isEternal(until) {
|
||||||
|
return moment.utc(until).isSame(DoNotDisturb.forever, "day");
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import Service, { inject as service } from "@ember/service";
|
import Service, { inject as service } from "@ember/service";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import DoNotDisturb from "discourse/lib/do-not-disturb";
|
||||||
|
|
||||||
export default class UserStatusService extends Service {
|
export default class UserStatusService extends Service {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
|
||||||
async set(status) {
|
async set(status, pauseNotifications) {
|
||||||
await ajax({
|
await ajax({
|
||||||
url: "/user-status.json",
|
url: "/user-status.json",
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
@ -12,6 +13,11 @@ export default class UserStatusService extends Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.currentUser.set("status", status);
|
this.currentUser.set("status", status);
|
||||||
|
if (pauseNotifications) {
|
||||||
|
this.#enterDoNotDisturb(status.ends_at);
|
||||||
|
} else {
|
||||||
|
this.#leaveDoNotDisturb();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
@ -21,5 +27,23 @@ export default class UserStatusService extends Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.currentUser.set("status", null);
|
this.currentUser.set("status", null);
|
||||||
|
this.#leaveDoNotDisturb();
|
||||||
|
}
|
||||||
|
|
||||||
|
#enterDoNotDisturb(endsAt) {
|
||||||
|
const duration = this.#duration(endsAt ?? DoNotDisturb.forever);
|
||||||
|
this.currentUser.enterDoNotDisturbFor(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaveDoNotDisturb() {
|
||||||
|
if (!this.currentUser.isInDoNotDisturb()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentUser.leaveDoNotDisturb();
|
||||||
|
}
|
||||||
|
|
||||||
|
#duration(endsAt) {
|
||||||
|
return moment.utc(endsAt).diff(moment.utc(), "minutes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<UserStatusPicker @status={{this.status}} />
|
<UserStatusPicker @status={{this.status}} />
|
||||||
</div>
|
</div>
|
||||||
|
{{#unless this.hidePauseNotifications}}
|
||||||
|
<div class="control-group pause-notifications">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<Input @type="checkbox" @checked={{this.pauseNotifications}} />
|
||||||
|
{{i18n "user_status.pause_notifications"}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
<div class="control-group control-group-remove-status">
|
<div class="control-group control-group-remove-status">
|
||||||
<label class="control-label">
|
<label class="control-label">
|
||||||
{{i18n "user_status.remove_status"}}
|
{{i18n "user_status.remove_status"}}
|
||||||
|
@ -4,6 +4,7 @@ import { dateNode } from "discourse/helpers/node";
|
|||||||
import { h } from "virtual-dom";
|
import { h } from "virtual-dom";
|
||||||
import { iconNode } from "discourse-common/lib/icon-library";
|
import { iconNode } from "discourse-common/lib/icon-library";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import DoNotDisturb from "discourse/lib/do-not-disturb";
|
||||||
|
|
||||||
export default createWidget("do-not-disturb", {
|
export default createWidget("do-not-disturb", {
|
||||||
tagName: "li.do-not-disturb",
|
tagName: "li.do-not-disturb",
|
||||||
@ -14,10 +15,7 @@ export default createWidget("do-not-disturb", {
|
|||||||
return [
|
return [
|
||||||
h("button.btn-flat.do-not-disturb-inner-container", [
|
h("button.btn-flat.do-not-disturb-inner-container", [
|
||||||
iconNode("toggle-on"),
|
iconNode("toggle-on"),
|
||||||
h("span.do-not-disturb-label", [
|
this.label(),
|
||||||
h("span", I18n.t("do_not_disturb.label")),
|
|
||||||
dateNode(this.currentUser.do_not_disturb_until),
|
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
@ -45,4 +43,15 @@ export default createWidget("do-not-disturb", {
|
|||||||
return showModal("do-not-disturb");
|
return showModal("do-not-disturb");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
label() {
|
||||||
|
const content = [h("span", I18n.t("do_not_disturb.label"))];
|
||||||
|
|
||||||
|
const until = this.currentUser.do_not_disturb_until;
|
||||||
|
if (!DoNotDisturb.isEternal(until)) {
|
||||||
|
content.push(dateNode(until));
|
||||||
|
}
|
||||||
|
|
||||||
|
return h("span.do-not-disturb-label", content);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -44,7 +44,9 @@ createWidgetFrom(QuickAccessItem, "user-status-item", {
|
|||||||
modalClass: "user-status",
|
modalClass: "user-status",
|
||||||
model: {
|
model: {
|
||||||
status: this.currentUser.status,
|
status: this.currentUser.status,
|
||||||
saveAction: (status) => this.userStatus.set(status),
|
pauseNotifications: this.currentUser.isInDoNotDisturb(),
|
||||||
|
saveAction: (status, pauseNotifications) =>
|
||||||
|
this.userStatus.set(status, pauseNotifications),
|
||||||
deleteAction: () => this.userStatus.clear(),
|
deleteAction: () => this.userStatus.clear(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import { click, triggerKeyEvent, visit } from "@ember/test-helpers";
|
import { click, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
|
import DoNotDisturb from "discourse/lib/do-not-disturb";
|
||||||
|
|
||||||
acceptance("Do not disturb", function (needs) {
|
acceptance("Do not disturb", function (needs) {
|
||||||
needs.user();
|
needs.user();
|
||||||
@ -99,6 +100,16 @@ acceptance("Do not disturb", function (needs) {
|
|||||||
"The active moon icons are removed"
|
"The active moon icons are removed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doesn't show the end date for eternal DnD", async function (assert) {
|
||||||
|
updateCurrentUser({ do_not_disturb_until: DoNotDisturb.forever });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await click(".header-dropdown-toggle.current-user");
|
||||||
|
await click(".menu-links-row .user-preferences-link");
|
||||||
|
|
||||||
|
assert.dom(".do-not-disturb .relative-date").doesNotExist();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
acceptance("Do not disturb - new user menu", function (needs) {
|
acceptance("Do not disturb - new user menu", function (needs) {
|
||||||
@ -220,4 +231,14 @@ acceptance("Do not disturb - new user menu", function (needs) {
|
|||||||
|
|
||||||
assert.notOk(exists(".user-menu"));
|
assert.notOk(exists(".user-menu"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doesn't show the end date for eternal DnD", async function (assert) {
|
||||||
|
updateCurrentUser({ do_not_disturb_until: DoNotDisturb.forever });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await click(".header-dropdown-toggle.current-user");
|
||||||
|
await click("#user-menu-button-profile");
|
||||||
|
|
||||||
|
assert.dom(".do-not-disturb .relative-date").doesNotExist();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -61,6 +61,15 @@ acceptance("User Profile - Account - User Status", function (needs) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doesn't show the pause notifications control group on the user status modal", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
|
await visit(`/u/${username}/preferences/account`);
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
assert.dom(".pause-notifications").doesNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
test("the status modal sets status", async function (assert) {
|
test("the status modal sets status", async function (assert) {
|
||||||
this.siteSettings.enable_user_status = true;
|
this.siteSettings.enable_user_status = true;
|
||||||
updateCurrentUser({ status: null });
|
updateCurrentUser({ status: null });
|
||||||
|
@ -20,6 +20,10 @@ async function pickEmoji(emoji) {
|
|||||||
await click(".results .emoji");
|
await click(".results .emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setDoNotDisturbMode() {
|
||||||
|
await click(".pause-notifications input[type=checkbox]");
|
||||||
|
}
|
||||||
|
|
||||||
acceptance("User Status", function (needs) {
|
acceptance("User Status", function (needs) {
|
||||||
const userStatus = "off to dentist";
|
const userStatus = "off to dentist";
|
||||||
const userStatusEmoji = "tooth";
|
const userStatusEmoji = "tooth";
|
||||||
@ -40,6 +44,9 @@ acceptance("User Status", function (needs) {
|
|||||||
publishToMessageBus(`/user-status/${userId}`, null);
|
publishToMessageBus(`/user-status/${userId}`, null);
|
||||||
return helper.response({ success: true });
|
return helper.response({ success: true });
|
||||||
});
|
});
|
||||||
|
server.delete("/do-not-disturb.json", () =>
|
||||||
|
helper.response({ success: true })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("doesn't show the user status button on the menu by default", async function (assert) {
|
test("doesn't show the user status button on the menu by default", async function (assert) {
|
||||||
@ -345,6 +352,128 @@ acceptance("User Status", function (needs) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance(
|
||||||
|
"User Status - pause notifications (do not disturb mode)",
|
||||||
|
function (needs) {
|
||||||
|
const userStatus = "off to dentist";
|
||||||
|
const userStatusEmoji = "tooth";
|
||||||
|
const userId = 1;
|
||||||
|
const userTimezone = "UTC";
|
||||||
|
|
||||||
|
needs.user({ id: userId, "user_option.timezone": userTimezone });
|
||||||
|
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.put("/user-status.json", () => {
|
||||||
|
return helper.response({ success: true });
|
||||||
|
});
|
||||||
|
server.delete("/user-status.json", () => {
|
||||||
|
return helper.response({ success: true });
|
||||||
|
});
|
||||||
|
server.post("/do-not-disturb.json", (request) => {
|
||||||
|
const duration = request.requestBody.match(/(?<=duration=)\d+/g)[0]; // body is something like "duration=134"
|
||||||
|
const endsAt = moment.utc().add(duration, "minutes").toISOString();
|
||||||
|
return helper.response({ ends_at: endsAt });
|
||||||
|
});
|
||||||
|
server.delete("/do-not-disturb.json", () =>
|
||||||
|
helper.response({ success: true })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the pause notifications control group", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
assert.dom(".pause-notifications").exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets do-not-disturb mode", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
await fillIn(".user-status-description", userStatus);
|
||||||
|
await pickEmoji(userStatusEmoji);
|
||||||
|
await click("#tap_tile_one_hour");
|
||||||
|
await setDoNotDisturbMode();
|
||||||
|
await click(".btn-primary"); // save
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".do-not-disturb-background .d-icon-moon")
|
||||||
|
.exists("the DnD mode indicator on the menu is shown");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets do-not-disturb mode even if ends at time wasn't chosen", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
await fillIn(".user-status-description", userStatus);
|
||||||
|
await pickEmoji(userStatusEmoji);
|
||||||
|
await setDoNotDisturbMode();
|
||||||
|
await click(".btn-primary"); // save
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".do-not-disturb-background .d-icon-moon")
|
||||||
|
.exists("the DnD mode indicator on the menu is shown");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unsets do-not-disturb mode when removing status", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
updateCurrentUser({ status: { description: userStatus } });
|
||||||
|
updateCurrentUser({ do_not_disturb_until: "2100-01-01T08:00:00.000Z" });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
await click(".btn.delete-status");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".do-not-disturb-background .d-icon-moon")
|
||||||
|
.doesNotExist("there is no DnD mode indicator on the menu");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unsets do-not-disturb mode when updating status", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
updateCurrentUser({
|
||||||
|
status: { emoji: userStatusEmoji, description: userStatus },
|
||||||
|
});
|
||||||
|
updateCurrentUser({ do_not_disturb_until: "2100-01-01T08:00:00.000Z" });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
await click(".pause-notifications input[type=checkbox]");
|
||||||
|
await click(".btn-primary"); // save
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".do-not-disturb-background .d-icon-moon")
|
||||||
|
.doesNotExist("there is no DnD mode indicator on the menu");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if user isn't in DnD mode the user status modal shows it", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
updateCurrentUser({ do_not_disturb_until: null });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
assert.dom(".pause-notifications input").isNotChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if user is in DnD mode the user status modal shows it", async function (assert) {
|
||||||
|
this.siteSettings.enable_user_status = true;
|
||||||
|
updateCurrentUser({ do_not_disturb_until: "2100-01-01T08:00:00.000Z" });
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await openUserStatusModal();
|
||||||
|
|
||||||
|
assert.dom(".pause-notifications input").isChecked();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
acceptance("User Status - new user menu", function (needs) {
|
acceptance("User Status - new user menu", function (needs) {
|
||||||
const userStatus = "off to dentist";
|
const userStatus = "off to dentist";
|
||||||
const userStatusEmoji = "tooth";
|
const userStatusEmoji = "tooth";
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pause-notifications {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
@ -1868,6 +1868,7 @@ en:
|
|||||||
save: "Save"
|
save: "Save"
|
||||||
set_custom_status: "Set custom status"
|
set_custom_status: "Set custom status"
|
||||||
what_are_you_doing: "What are you doing?"
|
what_are_you_doing: "What are you doing?"
|
||||||
|
pause_notifications: "Pause notifications"
|
||||||
remove_status: "Remove status"
|
remove_status: "Remove status"
|
||||||
|
|
||||||
user_tips:
|
user_tips:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user