FEATURE: auto remove user status after predefined period (#17236)
This commit is contained in:
parent
4acf2394e6
commit
c59f1729a6
|
@ -238,7 +238,9 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
|
|
||||||
this.appEvents.on("dom:clean", this, "_cleanDom");
|
this.appEvents.on("dom:clean", this, "_cleanDom");
|
||||||
|
|
||||||
this.appEvents.on("user-status:changed", () => this.queueRerender());
|
if (this.currentUser) {
|
||||||
|
this.currentUser.on("status-changed", this, "queueRerender");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
|
@ -310,6 +312,10 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
this.appEvents.off("header:hide-topic", this, "setTopic");
|
this.appEvents.off("header:hide-topic", this, "setTopic");
|
||||||
this.appEvents.off("dom:clean", this, "_cleanDom");
|
this.appEvents.off("dom:clean", this, "_cleanDom");
|
||||||
|
|
||||||
|
if (this.currentUser) {
|
||||||
|
this.currentUser.off("status-changed", this, "queueRerender");
|
||||||
|
}
|
||||||
|
|
||||||
cancel(this._scheduledRemoveAnimate);
|
cancel(this._scheduledRemoveAnimate);
|
||||||
|
|
||||||
this._itsatrap?.destroy();
|
this._itsatrap?.destroy();
|
||||||
|
|
|
@ -196,6 +196,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setProperties({ user });
|
this.setProperties({ user });
|
||||||
|
this.user.trackStatus();
|
||||||
return user;
|
return user;
|
||||||
})
|
})
|
||||||
.catch(() => this._close())
|
.catch(() => this._close())
|
||||||
|
@ -203,6 +204,10 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||||
},
|
},
|
||||||
|
|
||||||
_close() {
|
_close() {
|
||||||
|
if (this.user) {
|
||||||
|
this.user.stopTrackingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
user: null,
|
user: null,
|
||||||
topicPostCount: null,
|
topicPostCount: null,
|
||||||
|
|
|
@ -5,21 +5,42 @@ import { inject as service } from "@ember/service";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import bootbox from "bootbox";
|
import bootbox from "bootbox";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
import ItsATrap from "@discourse/itsatrap";
|
||||||
|
import {
|
||||||
|
TIME_SHORTCUT_TYPES,
|
||||||
|
timeShortcuts,
|
||||||
|
} from "discourse/lib/time-shortcut";
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
export default Controller.extend(ModalFunctionality, {
|
||||||
userStatusService: service("user-status"),
|
userStatusService: service("user-status"),
|
||||||
|
|
||||||
emoji: null,
|
emoji: null,
|
||||||
description: null,
|
description: null,
|
||||||
|
endsAt: null,
|
||||||
|
|
||||||
showDeleteButton: false,
|
showDeleteButton: false,
|
||||||
|
prefilledDateTime: null,
|
||||||
|
timeShortcuts: null,
|
||||||
|
_itsatrap: null,
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
const status = this.currentUser.status;
|
const status = this.currentUser.status;
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
emoji: status?.emoji,
|
emoji: status?.emoji,
|
||||||
description: status?.description,
|
description: status?.description,
|
||||||
|
endsAt: status?.ends_at,
|
||||||
showDeleteButton: !!status,
|
showDeleteButton: !!status,
|
||||||
|
timeShortcuts: this._buildTimeShortcuts(),
|
||||||
|
prefilledDateTime: status?.ends_at,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.set("_itsatrap", new ItsATrap());
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this._itsatrap.destroy();
|
||||||
|
this.set("_itsatrap", null);
|
||||||
|
this.set("timeShortcuts", null);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("emoji", "description")
|
@discourseComputed("emoji", "description")
|
||||||
|
@ -27,6 +48,18 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
return !!emoji && !!description;
|
return !!emoji && !!description;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
customTimeShortcutLabels() {
|
||||||
|
const labels = {};
|
||||||
|
labels[TIME_SHORTCUT_TYPES.NONE] = "time_shortcut.never";
|
||||||
|
return labels;
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
hiddenTimeShortcutOptions() {
|
||||||
|
return [TIME_SHORTCUT_TYPES.LAST_CUSTOM];
|
||||||
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
delete() {
|
delete() {
|
||||||
this.userStatusService
|
this.userStatusService
|
||||||
|
@ -35,9 +68,18 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
.catch((e) => this._handleError(e));
|
.catch((e) => this._handleError(e));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
onTimeSelected(_, time) {
|
||||||
|
this.set("endsAt", time);
|
||||||
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
saveAndClose() {
|
saveAndClose() {
|
||||||
const status = { description: this.description, emoji: this.emoji };
|
const status = {
|
||||||
|
description: this.description,
|
||||||
|
emoji: this.emoji,
|
||||||
|
ends_at: this.endsAt?.toISOString(),
|
||||||
|
};
|
||||||
this.userStatusService
|
this.userStatusService
|
||||||
.set(status)
|
.set(status)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -53,4 +95,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
popupAjaxError(e);
|
popupAjaxError(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_buildTimeShortcuts() {
|
||||||
|
const timezone = this.currentUser.timezone;
|
||||||
|
const shortcuts = timeShortcuts(timezone);
|
||||||
|
return [shortcuts.oneHour(), shortcuts.twoHours(), shortcuts.tomorrow()];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -117,8 +117,7 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
bus.subscribe(`/user-status/${user.id}`, (data) => {
|
bus.subscribe(`/user-status/${user.id}`, (data) => {
|
||||||
user.set("status", data);
|
appEvents.trigger("current-user-status:changed", data);
|
||||||
appEvents.trigger("user-status:changed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const site = container.lookup("site:main");
|
const site = container.lookup("site:main");
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
nextBusinessWeekStart,
|
nextBusinessWeekStart,
|
||||||
nextMonth,
|
nextMonth,
|
||||||
now,
|
now,
|
||||||
|
oneHour,
|
||||||
oneYear,
|
oneYear,
|
||||||
sixMonths,
|
sixMonths,
|
||||||
thisWeekend,
|
thisWeekend,
|
||||||
|
@ -18,12 +19,15 @@ import {
|
||||||
threeMonths,
|
threeMonths,
|
||||||
tomorrow,
|
tomorrow,
|
||||||
twoDays,
|
twoDays,
|
||||||
|
twoHours,
|
||||||
twoMonths,
|
twoMonths,
|
||||||
twoWeeks,
|
twoWeeks,
|
||||||
} from "discourse/lib/time-utils";
|
} from "discourse/lib/time-utils";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
export const TIME_SHORTCUT_TYPES = {
|
export const TIME_SHORTCUT_TYPES = {
|
||||||
|
ONE_HOUR: "one_hour",
|
||||||
|
TWO_HOURS: "two_hours",
|
||||||
LATER_TODAY: "later_today",
|
LATER_TODAY: "later_today",
|
||||||
TOMORROW: "tomorrow",
|
TOMORROW: "tomorrow",
|
||||||
THIS_WEEKEND: "this_weekend",
|
THIS_WEEKEND: "this_weekend",
|
||||||
|
@ -77,6 +81,24 @@ export function specialShortcutOptions() {
|
||||||
|
|
||||||
export function timeShortcuts(timezone) {
|
export function timeShortcuts(timezone) {
|
||||||
return {
|
return {
|
||||||
|
oneHour() {
|
||||||
|
return {
|
||||||
|
id: TIME_SHORTCUT_TYPES.ONE_HOUR,
|
||||||
|
icon: "angle-right",
|
||||||
|
label: "time_shortcut.in_one_hour",
|
||||||
|
time: oneHour(timezone),
|
||||||
|
timeFormatKey: "dates.time",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
twoHours() {
|
||||||
|
return {
|
||||||
|
id: TIME_SHORTCUT_TYPES.TWO_HOURS,
|
||||||
|
icon: "angle-right",
|
||||||
|
label: "time_shortcut.in_two_hours",
|
||||||
|
time: twoHours(timezone),
|
||||||
|
timeFormatKey: "dates.time",
|
||||||
|
};
|
||||||
|
},
|
||||||
laterToday() {
|
laterToday() {
|
||||||
return {
|
return {
|
||||||
id: TIME_SHORTCUT_TYPES.LATER_TODAY,
|
id: TIME_SHORTCUT_TYPES.LATER_TODAY,
|
||||||
|
|
|
@ -19,6 +19,14 @@ export function startOfDay(momentDate, startOfDayHour = START_OF_DAY_HOUR) {
|
||||||
return momentDate.hour(startOfDayHour).startOf("hour");
|
return momentDate.hour(startOfDayHour).startOf("hour");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function oneHour(timezone) {
|
||||||
|
return now(timezone).add(1, "hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function twoHours(timezone) {
|
||||||
|
return now(timezone).add(2, "hours");
|
||||||
|
}
|
||||||
|
|
||||||
export function tomorrow(timezone) {
|
export function tomorrow(timezone) {
|
||||||
return startOfDay(now(timezone).add(1, "day"));
|
return startOfDay(now(timezone).add(1, "day"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,9 @@ RestModel.reopenClass({
|
||||||
if (!args.siteSettings) {
|
if (!args.siteSettings) {
|
||||||
args.siteSettings = owner.lookup("site-settings:main");
|
args.siteSettings = owner.lookup("site-settings:main");
|
||||||
}
|
}
|
||||||
|
if (!args.appEvents) {
|
||||||
|
args.appEvents = owner.lookup("service:appEvents");
|
||||||
|
}
|
||||||
|
|
||||||
args.__munge = this.munge;
|
args.__munge = this.munge;
|
||||||
return this._super(this.munge(args, args.store));
|
return this._super(this.munge(args, args.store));
|
||||||
|
|
|
@ -31,6 +31,9 @@ import { longDate } from "discourse/lib/formatter";
|
||||||
import { url } from "discourse/lib/computed";
|
import { url } from "discourse/lib/computed";
|
||||||
import { userPath } from "discourse/lib/url";
|
import { userPath } from "discourse/lib/url";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import Evented from "@ember/object/evented";
|
||||||
|
import { cancel, later } from "@ember/runloop";
|
||||||
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
|
||||||
export const SECOND_FACTOR_METHODS = {
|
export const SECOND_FACTOR_METHODS = {
|
||||||
TOTP: 1,
|
TOTP: 1,
|
||||||
|
@ -1072,6 +1075,8 @@ User.reopenClass(Singleton, {
|
||||||
createCurrent() {
|
createCurrent() {
|
||||||
const userJson = PreloadStore.get("currentUser");
|
const userJson = PreloadStore.get("currentUser");
|
||||||
if (userJson) {
|
if (userJson) {
|
||||||
|
userJson.isCurrent = true;
|
||||||
|
|
||||||
if (userJson.primary_group_id) {
|
if (userJson.primary_group_id) {
|
||||||
const primaryGroup = userJson.groups.find(
|
const primaryGroup = userJson.groups.find(
|
||||||
(group) => group.id === userJson.primary_group_id
|
(group) => group.id === userJson.primary_group_id
|
||||||
|
@ -1087,7 +1092,9 @@ User.reopenClass(Singleton, {
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = getOwner(this).lookup("service:store");
|
const store = getOwner(this).lookup("service:store");
|
||||||
return store.createRecord("user", userJson);
|
const currentUser = store.createRecord("user", userJson);
|
||||||
|
currentUser.trackStatus();
|
||||||
|
return currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -1170,6 +1177,78 @@ User.reopenClass(Singleton, {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// user status tracking
|
||||||
|
User.reopen(Evented, {
|
||||||
|
_clearStatusTimerId: null,
|
||||||
|
|
||||||
|
// always call stopTrackingStatus() when done with a user
|
||||||
|
trackStatus() {
|
||||||
|
this.addObserver("status", this, "_statusChanged");
|
||||||
|
|
||||||
|
if (this.isCurrent) {
|
||||||
|
this.appEvents.on(
|
||||||
|
"current-user-status:changed",
|
||||||
|
this,
|
||||||
|
this._updateStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status && this.status.ends_at) {
|
||||||
|
this._scheduleStatusClearing(this.status.ends_at);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopTrackingStatus() {
|
||||||
|
this.removeObserver("status", this, "_statusChanged");
|
||||||
|
if (this.isCurrent) {
|
||||||
|
this.appEvents.off(
|
||||||
|
"current-user-status:changed",
|
||||||
|
this,
|
||||||
|
this._updateStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._unscheduleStatusClearing();
|
||||||
|
},
|
||||||
|
|
||||||
|
_statusChanged(sender, key) {
|
||||||
|
this.trigger("status-changed");
|
||||||
|
|
||||||
|
const status = this.get(key);
|
||||||
|
if (status && status.ends_at) {
|
||||||
|
this._scheduleStatusClearing(status.ends_at);
|
||||||
|
} else {
|
||||||
|
this._unscheduleStatusClearing();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_scheduleStatusClearing(endsAt) {
|
||||||
|
if (isTesting()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._clearStatusTimerId) {
|
||||||
|
this._unscheduleStatusClearing();
|
||||||
|
}
|
||||||
|
|
||||||
|
const utcNow = moment.utc();
|
||||||
|
const remaining = moment.utc(endsAt).diff(utcNow, "milliseconds");
|
||||||
|
this._clearStatusTimerId = later(this, "_autoClearStatus", remaining);
|
||||||
|
},
|
||||||
|
|
||||||
|
_unscheduleStatusClearing() {
|
||||||
|
cancel(this._clearStatusTimerId);
|
||||||
|
this._clearStatusTimerId = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_autoClearStatus() {
|
||||||
|
this.set("status", null);
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateStatus(status) {
|
||||||
|
this.set("status", status);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof Discourse !== "undefined") {
|
if (typeof Discourse !== "undefined") {
|
||||||
let warned = false;
|
let warned = false;
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
|
|
|
@ -63,7 +63,11 @@
|
||||||
<h2 class="staged">{{i18n "user.staged"}}</h2>
|
<h2 class="staged">{{i18n "user.staged"}}</h2>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if this.hasStatus}}
|
{{#if this.hasStatus}}
|
||||||
<h3 class="user-status">{{html-safe this.userStatusEmoji}} {{this.user.status.description}}</h3>
|
<h3 class="user-status">
|
||||||
|
{{html-safe this.userStatusEmoji}}
|
||||||
|
{{this.user.status.description}}
|
||||||
|
{{format-date this.user.status.ends_at format="tiny"}}
|
||||||
|
</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<PluginOutlet @name="user-card-post-names" @connectorTagName="div" @args={{hash user=this.user}} @tagName="div" />
|
<PluginOutlet @name="user-card-post-names" @connectorTagName="div" @args={{hash user=this.user}} @tagName="div" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,24 @@
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<UserStatusPicker @emoji={{emoji}} @description={{description}} />
|
<UserStatusPicker @emoji={{emoji}} @description={{description}} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group control-group-remove-status">
|
||||||
|
<label class="control-label">
|
||||||
|
{{i18n "user_status.remove_status"}}
|
||||||
|
</label>
|
||||||
|
<TimeShortcutPicker
|
||||||
|
@timeShortcuts={{timeShortcuts}}
|
||||||
|
@hiddenOptions={{hiddenTimeShortcutOptions}}
|
||||||
|
@customLabels={{customTimeShortcutLabels}}
|
||||||
|
@prefilledDatetime={{prefilledDateTime}}
|
||||||
|
@onTimeSelected={{action "onTimeSelected"}}
|
||||||
|
@_itsatrap={{_itsatrap}} />
|
||||||
|
</div>
|
||||||
<div class="modal-footer control-group">
|
<div class="modal-footer control-group">
|
||||||
<DButton @label="user_status.save" @class="btn-primary" @disabled={{not statusIsSet}} @action={{action "saveAndClose"}} />
|
<DButton
|
||||||
|
@label="user_status.save"
|
||||||
|
@class="btn-primary"
|
||||||
|
@disabled={{not statusIsSet}}
|
||||||
|
@action={{action "saveAndClose"}} />
|
||||||
<DModalCancel @close={{action "closeModal"}} />
|
<DModalCancel @close={{action "closeModal"}} />
|
||||||
{{#if showDeleteButton}}
|
{{#if showDeleteButton}}
|
||||||
<DButton @icon="trash-alt" @class="delete-status btn-danger" @action={{action "delete"}} />
|
<DButton @icon="trash-alt" @class="delete-status btn-danger" @action={{action "delete"}} />
|
||||||
|
|
|
@ -4,6 +4,7 @@ import QuickAccessItem from "discourse/widgets/quick-access-item";
|
||||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import { dateNode } from "discourse/helpers/node";
|
||||||
|
|
||||||
const _extraItems = [];
|
const _extraItems = [];
|
||||||
|
|
||||||
|
@ -27,20 +28,11 @@ createWidgetFrom(QuickAccessItem, "user-status-item", {
|
||||||
tagName: "li.user-status",
|
tagName: "li.user-status",
|
||||||
|
|
||||||
html() {
|
html() {
|
||||||
const action = "hideMenuAndSetStatus";
|
const status = this.currentUser.status;
|
||||||
const userStatus = this.currentUser.status;
|
if (status) {
|
||||||
if (userStatus) {
|
return this._editStatusButton(status);
|
||||||
return this.attach("flat-button", {
|
|
||||||
action,
|
|
||||||
emoji: userStatus.emoji,
|
|
||||||
translatedLabel: userStatus.description,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return this.attach("flat-button", {
|
return this._setStatusButton();
|
||||||
action,
|
|
||||||
icon: "plus-circle",
|
|
||||||
label: "user_status.set_custom_status",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -51,6 +43,28 @@ createWidgetFrom(QuickAccessItem, "user-status-item", {
|
||||||
modalClass: "user-status",
|
modalClass: "user-status",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_setStatusButton() {
|
||||||
|
return this.attach("flat-button", {
|
||||||
|
action: "hideMenuAndSetStatus",
|
||||||
|
icon: "plus-circle",
|
||||||
|
label: "user_status.set_custom_status",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_editStatusButton(status) {
|
||||||
|
const menuButton = {
|
||||||
|
action: "hideMenuAndSetStatus",
|
||||||
|
emoji: status.emoji,
|
||||||
|
translatedLabel: status.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status.ends_at) {
|
||||||
|
menuButton.contents = dateNode(status.ends_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.attach("flat-button", menuButton);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
||||||
|
|
|
@ -24,8 +24,9 @@ acceptance("User Status", function (needs) {
|
||||||
const userStatus = "off to dentist";
|
const userStatus = "off to dentist";
|
||||||
const userStatusEmoji = "tooth";
|
const userStatusEmoji = "tooth";
|
||||||
const userId = 1;
|
const userId = 1;
|
||||||
|
const userTimezone = "UTC";
|
||||||
|
|
||||||
needs.user({ id: userId });
|
needs.user({ id: userId, timezone: userTimezone });
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
needs.pretender((server, helper) => {
|
||||||
server.put("/user-status.json", () => {
|
server.put("/user-status.json", () => {
|
||||||
|
@ -102,7 +103,11 @@ acceptance("User Status", function (needs) {
|
||||||
this.siteSettings.enable_user_status = true;
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
updateCurrentUser({
|
updateCurrentUser({
|
||||||
status: { description: userStatus, emoji: userStatusEmoji },
|
status: {
|
||||||
|
description: userStatus,
|
||||||
|
emoji: userStatusEmoji,
|
||||||
|
ends_at: "2100-02-01T09:35:00.000Z",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await visit("/");
|
await visit("/");
|
||||||
|
@ -118,6 +123,16 @@ acceptance("User Status", function (needs) {
|
||||||
userStatus,
|
userStatus,
|
||||||
"status description is shown"
|
"status description is shown"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
query(".date-picker").value,
|
||||||
|
"2100-02-01",
|
||||||
|
"date of auto removing of status is shown"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
query(".time-input").value,
|
||||||
|
"09:35",
|
||||||
|
"time of auto removing of status is shown"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("emoji picking", async function (assert) {
|
test("emoji picking", async function (assert) {
|
||||||
|
@ -213,6 +228,28 @@ acceptance("User Status", function (needs) {
|
||||||
assert.notOk(exists(".header-dropdown-toggle .user-status-background"));
|
assert.notOk(exists(".header-dropdown-toggle .user-status-background"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("setting user status with auto removing timer", 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 click(".btn-primary"); // save
|
||||||
|
|
||||||
|
await click(".header-dropdown-toggle.current-user");
|
||||||
|
await click(".menu-links-row .user-preferences-link");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
query("div.quick-access-panel li.user-status span.relative-date")
|
||||||
|
.innerText,
|
||||||
|
"1h",
|
||||||
|
"shows user status timer on the menu"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("it's impossible to set status without description", async function (assert) {
|
test("it's impossible to set status without description", async function (assert) {
|
||||||
this.siteSettings.enable_user_status = true;
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
|
@ -237,7 +274,7 @@ acceptance("User Status", function (needs) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows actual status on the modal after canceling the modal", async function (assert) {
|
test("shows actual status on the modal after canceling the modal and opening it again", async function (assert) {
|
||||||
this.siteSettings.enable_user_status = true;
|
this.siteSettings.enable_user_status = true;
|
||||||
|
|
||||||
updateCurrentUser({
|
updateCurrentUser({
|
||||||
|
|
|
@ -289,6 +289,9 @@ export function acceptance(name, optionsOrCallback) {
|
||||||
if (userChanges) {
|
if (userChanges) {
|
||||||
updateCurrentUser(userChanges);
|
updateCurrentUser(userChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User.current().appEvents = getOwner(this).lookup("service:appEvents");
|
||||||
|
User.current().trackStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingChanges) {
|
if (settingChanges) {
|
||||||
|
@ -313,6 +316,9 @@ export function acceptance(name, optionsOrCallback) {
|
||||||
resetMobile();
|
resetMobile();
|
||||||
let app = getApplication();
|
let app = getApplication();
|
||||||
options?.afterEach?.call(this);
|
options?.afterEach?.call(this);
|
||||||
|
if (loggedIn) {
|
||||||
|
User.current().stopTrackingStatus();
|
||||||
|
}
|
||||||
testCleanup(this.container, app);
|
testCleanup(this.container, app);
|
||||||
|
|
||||||
// We do this after reset so that the willClearRender will have already fired
|
// We do this after reset so that the willClearRender will have already fired
|
||||||
|
|
|
@ -5,7 +5,13 @@ import PreloadStore from "discourse/lib/preload-store";
|
||||||
import sinon from "sinon";
|
import sinon from "sinon";
|
||||||
import { settled } from "@ember/test-helpers";
|
import { settled } from "@ember/test-helpers";
|
||||||
|
|
||||||
module("Unit | Model | user", function () {
|
module("Unit | Model | user", function (hooks) {
|
||||||
|
hooks.afterEach(function () {
|
||||||
|
if (this.clock) {
|
||||||
|
this.clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("staff", function (assert) {
|
test("staff", function (assert) {
|
||||||
let user = User.create({ id: 1, username: "eviltrout" });
|
let user = User.create({ id: 1, username: "eviltrout" });
|
||||||
|
|
||||||
|
|
|
@ -446,6 +446,14 @@ table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-menu .quick-access-panel li.user-status .relative-date {
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--font-down-3);
|
||||||
|
padding-top: 0.45em;
|
||||||
|
margin-left: 0.75em;
|
||||||
|
color: var(--primary-medium);
|
||||||
|
}
|
||||||
|
|
||||||
.user-menu .quick-access-panel li.do-not-disturb {
|
.user-menu .quick-access-panel li.do-not-disturb {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
|
|
|
@ -302,7 +302,18 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
|
||||||
}
|
}
|
||||||
|
|
||||||
h3.user-status {
|
h3.user-status {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
img.emoji {
|
img.emoji {
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative-date {
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--font-down-3);
|
||||||
|
padding-top: 0.5em;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
color: var(--primary-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,5 +22,13 @@
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-group-remove-status {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ class UserStatusController < ApplicationController
|
||||||
description = params.require(:description)
|
description = params.require(:description)
|
||||||
emoji = params.require(:emoji)
|
emoji = params.require(:emoji)
|
||||||
|
|
||||||
current_user.set_status!(description, emoji)
|
current_user.set_status!(description, emoji, params[:ends_at])
|
||||||
render json: success_json
|
render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -666,9 +666,15 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_user_status(status)
|
def publish_user_status(status)
|
||||||
payload = status ?
|
if status
|
||||||
{ description: status.description, emoji: status.emoji } :
|
payload = {
|
||||||
nil
|
description: status.description,
|
||||||
|
emoji: status.emoji,
|
||||||
|
ends_at: status.ends_at&.iso8601
|
||||||
|
}
|
||||||
|
else
|
||||||
|
payload = nil
|
||||||
|
end
|
||||||
|
|
||||||
MessageBus.publish("/user-status/#{id}", payload, user_ids: [id])
|
MessageBus.publish("/user-status/#{id}", payload, user_ids: [id])
|
||||||
end
|
end
|
||||||
|
@ -1526,25 +1532,27 @@ class User < ActiveRecord::Base
|
||||||
publish_user_status(nil)
|
publish_user_status(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_status!(description, emoji)
|
def set_status!(description, emoji, ends_at)
|
||||||
now = Time.zone.now
|
status = {
|
||||||
|
description: description,
|
||||||
|
emoji: emoji,
|
||||||
|
set_at: Time.zone.now,
|
||||||
|
ends_at: ends_at
|
||||||
|
}
|
||||||
|
|
||||||
if user_status
|
if user_status
|
||||||
user_status.update!(
|
user_status.update!(status)
|
||||||
description: description,
|
|
||||||
emoji: emoji,
|
|
||||||
set_at: now)
|
|
||||||
else
|
else
|
||||||
self.user_status = UserStatus.create!(
|
self.user_status = UserStatus.create!(status)
|
||||||
user_id: id,
|
|
||||||
description: description,
|
|
||||||
emoji: emoji,
|
|
||||||
set_at: now
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
publish_user_status(user_status)
|
publish_user_status(user_status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_status?
|
||||||
|
user_status && !user_status.expired?
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def badge_grant
|
def badge_grant
|
||||||
|
|
|
@ -6,6 +6,10 @@ class UserStatus < ActiveRecord::Base
|
||||||
validate :ends_at_greater_than_set_at,
|
validate :ends_at_greater_than_set_at,
|
||||||
if: Proc.new { |t| t.will_save_change_to_set_at? || t.will_save_change_to_ends_at? }
|
if: Proc.new { |t| t.will_save_change_to_set_at? || t.will_save_change_to_ends_at? }
|
||||||
|
|
||||||
|
def expired?
|
||||||
|
ends_at && ends_at < Time.zone.now
|
||||||
|
end
|
||||||
|
|
||||||
def ends_at_greater_than_set_at
|
def ends_at_greater_than_set_at
|
||||||
if ends_at && set_at > ends_at
|
if ends_at && set_at > ends_at
|
||||||
errors.add(:ends_at, I18n.t("user_status.errors.ends_at_should_be_greater_than_set_at"))
|
errors.add(:ends_at, I18n.t("user_status.errors.ends_at_should_be_greater_than_set_at"))
|
||||||
|
|
|
@ -332,7 +332,7 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_status?
|
def include_status?
|
||||||
SiteSetting.enable_user_status
|
SiteSetting.enable_user_status && object.has_status?
|
||||||
end
|
end
|
||||||
|
|
||||||
def status
|
def status
|
||||||
|
|
|
@ -224,7 +224,7 @@ class UserCardSerializer < BasicUserSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_status?
|
def include_status?
|
||||||
SiteSetting.enable_user_status
|
SiteSetting.enable_user_status && user.has_status?
|
||||||
end
|
end
|
||||||
|
|
||||||
def status
|
def status
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UserStatusSerializer < ApplicationSerializer
|
class UserStatusSerializer < ApplicationSerializer
|
||||||
attributes :description, :emoji
|
attributes :description, :emoji, :ends_at
|
||||||
end
|
end
|
||||||
|
|
|
@ -644,6 +644,8 @@ en:
|
||||||
|
|
||||||
time_shortcut:
|
time_shortcut:
|
||||||
now: "Now"
|
now: "Now"
|
||||||
|
in_one_hour: "In one hour"
|
||||||
|
in_two_hours: "In two hours"
|
||||||
later_today: "Later today"
|
later_today: "Later today"
|
||||||
two_days: "Two days"
|
two_days: "Two days"
|
||||||
next_business_day: "Next business day"
|
next_business_day: "Next business day"
|
||||||
|
@ -664,6 +666,7 @@ en:
|
||||||
forever: "Forever"
|
forever: "Forever"
|
||||||
relative: "Relative time"
|
relative: "Relative time"
|
||||||
none: "None needed"
|
none: "None needed"
|
||||||
|
never: "Never"
|
||||||
last_custom: "Last custom datetime"
|
last_custom: "Last custom datetime"
|
||||||
custom: "Custom date and time"
|
custom: "Custom date and time"
|
||||||
select_timeframe: "Select a timeframe"
|
select_timeframe: "Select a timeframe"
|
||||||
|
@ -1796,6 +1799,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?"
|
||||||
|
remove_status: "Remove status"
|
||||||
|
|
||||||
loading: "Loading..."
|
loading: "Loading..."
|
||||||
errors:
|
errors:
|
||||||
|
|
|
@ -38,38 +38,72 @@ describe UserStatusController do
|
||||||
it "sets user status" do
|
it "sets user status" do
|
||||||
status = "off to dentist"
|
status = "off to dentist"
|
||||||
status_emoji = "tooth"
|
status_emoji = "tooth"
|
||||||
put "/user-status.json", params: { description: status, emoji: status_emoji }
|
ends_at = DateTime.parse("2100-01-01 18:00")
|
||||||
|
|
||||||
|
put "/user-status.json", params: {
|
||||||
|
description: status,
|
||||||
|
emoji: status_emoji,
|
||||||
|
ends_at: ends_at
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
expect(user.user_status.description).to eq(status)
|
expect(user.user_status.description).to eq(status)
|
||||||
expect(user.user_status.emoji).to eq(status_emoji)
|
expect(user.user_status.emoji).to eq(status_emoji)
|
||||||
|
expect(user.user_status.ends_at).to eq_time(ends_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "following calls update status" do
|
it "following calls update status" do
|
||||||
status = "off to dentist"
|
status = "off to dentist"
|
||||||
status_emoji = "tooth"
|
status_emoji = "tooth"
|
||||||
put "/user-status.json", params: { description: status, emoji: status_emoji }
|
ends_at = DateTime.parse("2100-01-01 18:00")
|
||||||
|
put "/user-status.json", params: {
|
||||||
|
description: status,
|
||||||
|
emoji: status_emoji,
|
||||||
|
ends_at: ends_at
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.user_status.description).to eq(status)
|
expect(user.user_status.description).to eq(status)
|
||||||
expect(user.user_status.emoji).to eq(status_emoji)
|
expect(user.user_status.emoji).to eq(status_emoji)
|
||||||
|
expect(user.user_status.ends_at).to eq_time(ends_at)
|
||||||
|
|
||||||
new_status = "surfing"
|
new_status = "surfing"
|
||||||
new_status_emoji = "surfing_man"
|
new_status_emoji = "surfing_man"
|
||||||
put "/user-status.json", params: { description: new_status, emoji: new_status_emoji }
|
new_ends_at = DateTime.parse("2100-01-01 18:59")
|
||||||
|
put "/user-status.json", params: {
|
||||||
|
description: new_status,
|
||||||
|
emoji: new_status_emoji,
|
||||||
|
ends_at: new_ends_at
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.user_status.description).to eq(new_status)
|
expect(user.user_status.description).to eq(new_status)
|
||||||
expect(user.user_status.emoji).to eq(new_status_emoji)
|
expect(user.user_status.emoji).to eq(new_status_emoji)
|
||||||
|
expect(user.user_status.ends_at).to eq_time(new_ends_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "publishes to message bus" do
|
it "publishes to message bus" do
|
||||||
status = "off to dentist"
|
status = "off to dentist"
|
||||||
emoji = "tooth"
|
emoji = "tooth"
|
||||||
|
ends_at = "2100-01-01T18:00:00Z"
|
||||||
|
|
||||||
messages = MessageBus.track_publish do
|
messages = MessageBus.track_publish do
|
||||||
put "/user-status.json", params: { description: status, emoji: emoji }
|
put "/user-status.json", params: {
|
||||||
|
description: status,
|
||||||
|
emoji: emoji,
|
||||||
|
ends_at: ends_at
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(messages.size).to eq(1)
|
expect(messages.size).to eq(1)
|
||||||
expect(messages[0].channel).to eq("/user-status/#{user.id}")
|
expect(messages[0].channel).to eq("/user-status/#{user.id}")
|
||||||
expect(messages[0].data[:description]).to eq(status)
|
|
||||||
expect(messages[0].user_ids).to eq([user.id])
|
expect(messages[0].user_ids).to eq([user.id])
|
||||||
|
|
||||||
|
expect(messages[0].data[:description]).to eq(status)
|
||||||
|
expect(messages[0].data[:emoji]).to eq(emoji)
|
||||||
|
expect(messages[0].data[:ends_at]).to eq(ends_at)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -101,6 +135,7 @@ describe UserStatusController do
|
||||||
|
|
||||||
it "clears user status" do
|
it "clears user status" do
|
||||||
delete "/user-status.json"
|
delete "/user-status.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.user_status).to be_nil
|
expect(user.user_status).to be_nil
|
||||||
|
|
|
@ -182,7 +182,7 @@ RSpec.describe CurrentUserSerializer do
|
||||||
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
||||||
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
||||||
|
|
||||||
it "serializes when enabled" do
|
it "adds user status when enabled" do
|
||||||
SiteSetting.enable_user_status = true
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
json = serializer.as_json
|
json = serializer.as_json
|
||||||
|
@ -193,11 +193,31 @@ RSpec.describe CurrentUserSerializer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't serialize when disabled" do
|
it "doesn't add user status when disabled" do
|
||||||
SiteSetting.enable_user_status = false
|
SiteSetting.enable_user_status = false
|
||||||
json = serializer.as_json
|
json = serializer.as_json
|
||||||
expect(json.keys).not_to include :status
|
expect(json.keys).not_to include :status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't add expired user status" do
|
||||||
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
|
user.user_status.ends_at = 1.minutes.ago
|
||||||
|
serializer = described_class.new(user, scope: Guardian.new(user), root: false)
|
||||||
|
json = serializer.as_json
|
||||||
|
|
||||||
|
expect(json.keys).not_to include :status
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't return status if user doesn't have it set" do
|
||||||
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
|
user.clear_status!
|
||||||
|
user.reload
|
||||||
|
json = serializer.as_json
|
||||||
|
|
||||||
|
expect(json.keys).not_to include :status
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#sidebar_tag_names' do
|
describe '#sidebar_tag_names' do
|
||||||
|
|
|
@ -76,7 +76,7 @@ describe UserCardSerializer do
|
||||||
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
||||||
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
||||||
|
|
||||||
it "serializes when enabled" do
|
it "adds user status when enabled" do
|
||||||
SiteSetting.enable_user_status = true
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
json = serializer.as_json
|
json = serializer.as_json
|
||||||
|
@ -87,10 +87,30 @@ describe UserCardSerializer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't serialize when disabled" do
|
it "doesn't add user status when disabled" do
|
||||||
SiteSetting.enable_user_status = false
|
SiteSetting.enable_user_status = false
|
||||||
json = serializer.as_json
|
json = serializer.as_json
|
||||||
expect(json.keys).not_to include :status
|
expect(json.keys).not_to include :status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't add expired user status" do
|
||||||
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
|
user.user_status.ends_at = 1.minutes.ago
|
||||||
|
serializer = described_class.new(user, scope: Guardian.new(user), root: false)
|
||||||
|
json = serializer.as_json
|
||||||
|
|
||||||
|
expect(json.keys).not_to include :status
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't return status if user doesn't have it set" do
|
||||||
|
SiteSetting.enable_user_status = true
|
||||||
|
|
||||||
|
user.clear_status!
|
||||||
|
user.reload
|
||||||
|
json = serializer.as_json
|
||||||
|
|
||||||
|
expect(json.keys).not_to include :status
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue