FEATURE: user status (#16875)
This commit is contained in:
parent
ac59168dde
commit
5c596273a0
|
@ -238,6 +238,8 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
|
||||
this.appEvents.on("dom:clean", this, "_cleanDom");
|
||||
|
||||
this.appEvents.on("user-status:changed", () => this.queueRerender());
|
||||
|
||||
if (
|
||||
this.currentUser &&
|
||||
!this.get("currentUser.read_first_notification")
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getURLWithCDN } from "discourse-common/lib/get-url";
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
import { dasherize } from "@ember/string";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
|
||||
export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||
elementId: "user-card",
|
||||
|
@ -49,6 +50,17 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
|||
return user.location || user.website_name || this.userTimezone;
|
||||
},
|
||||
|
||||
@discourseComputed("user.status")
|
||||
hasStatus() {
|
||||
return this.siteSettings.enable_user_status && this.user.status;
|
||||
},
|
||||
|
||||
@discourseComputed("user.status")
|
||||
userStatusEmoji() {
|
||||
const emoji = this.user.status.emoji ?? "mega";
|
||||
return emojiUnescape(`:${emoji}:`);
|
||||
},
|
||||
|
||||
isSuspendedOrHasBio: or("user.suspend_reason", "user.bio_excerpt"),
|
||||
showCheckEmail: and("user.staged", "canCheckEmails"),
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import Controller from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { action } from "@ember/object";
|
||||
import { notEmpty } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
userStatusService: service("user-status"),
|
||||
|
||||
description: null,
|
||||
statusIsSet: notEmpty("description"),
|
||||
showDeleteButton: false,
|
||||
|
||||
onShow() {
|
||||
if (this.currentUser.status) {
|
||||
this.setProperties({
|
||||
description: this.currentUser.status.description,
|
||||
showDeleteButton: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
delete() {
|
||||
this.userStatusService
|
||||
.clear()
|
||||
.then(() => {
|
||||
this._resetModal();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((e) => this._handleError(e));
|
||||
},
|
||||
|
||||
@action
|
||||
saveAndClose() {
|
||||
if (this.description) {
|
||||
const status = { description: this.description };
|
||||
this.userStatusService
|
||||
.set(status)
|
||||
.then(() => {
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((e) => this._handleError(e));
|
||||
}
|
||||
},
|
||||
|
||||
_handleError(e) {
|
||||
if (typeof e === "string") {
|
||||
bootbox.alert(e);
|
||||
} else {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
},
|
||||
|
||||
_resetModal() {
|
||||
this.set("description", null);
|
||||
this.set("showDeleteButton", false);
|
||||
},
|
||||
});
|
|
@ -252,6 +252,13 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
|
|||
hasGroups,
|
||||
});
|
||||
},
|
||||
|
||||
setUserStatus() {
|
||||
showModal("user-status", {
|
||||
title: "user_status.set_custom_status",
|
||||
modalClass: "user-status",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default class UserStatusService extends Service {
|
||||
@service appEvents;
|
||||
|
||||
async set(status) {
|
||||
await ajax({
|
||||
url: "/user-status.json",
|
||||
type: "PUT",
|
||||
data: { description: status.description },
|
||||
});
|
||||
|
||||
this.currentUser.set("status", status);
|
||||
this.appEvents.trigger("do-not-disturb:changed");
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await ajax({
|
||||
url: "/user-status.json",
|
||||
type: "DELETE",
|
||||
});
|
||||
|
||||
this.currentUser.set("status", null);
|
||||
this.appEvents.trigger("do-not-disturb:changed");
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
toggleAnonymous=(route-action "toggleAnonymous")
|
||||
logout=(route-action "logout")
|
||||
toggleSidebar=(route-action "toggleSidebar")
|
||||
setUserStatus=(route-action "setUserStatus")
|
||||
}}
|
||||
{{software-update-prompt}}
|
||||
|
||||
|
|
|
@ -62,6 +62,9 @@
|
|||
{{#if this.user.staged}}
|
||||
<h2 class="staged">{{i18n "user.staged"}}</h2>
|
||||
{{/if}}
|
||||
{{#if this.hasStatus}}
|
||||
<h3 class="user-status">{{html-safe this.userStatusEmoji}} {{this.user.status.description}}</h3>
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-card-post-names" connectorTagName="div" args=(hash user=this.user) tagName="div"}}
|
||||
</div>
|
||||
<ul class="usercard-controls">
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{{#d-modal-body}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<div class="control-group user-status-description-wrap">
|
||||
{{input
|
||||
class="user-status-description"
|
||||
placeholder=(i18n "user_status.what_are_you_doing")
|
||||
value=description}}
|
||||
</div>
|
||||
<div class="modal-footer control-group">
|
||||
{{d-button
|
||||
label="user_status.save"
|
||||
class="btn-primary"
|
||||
disabled=(not statusIsSet)
|
||||
action=(action "saveAndClose")}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
{{#if showDeleteButton}}
|
||||
{{d-button
|
||||
icon="trash-alt"
|
||||
class="delete-status btn-danger"
|
||||
action=(action "delete")}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/d-modal-body}}
|
|
@ -86,9 +86,13 @@ export const ButtonClass = {
|
|||
html(attrs) {
|
||||
const contents = [];
|
||||
const left = !attrs.iconRight;
|
||||
|
||||
if (attrs.icon && left) {
|
||||
contents.push(this._buildIcon(attrs));
|
||||
}
|
||||
if (attrs.emoji && left) {
|
||||
contents.push(this.attach("emoji", { name: attrs.emoji }));
|
||||
}
|
||||
if (attrs.label) {
|
||||
contents.push(
|
||||
h("span.d-button-label", I18n.t(attrs.label, attrs.labelOptions))
|
||||
|
@ -106,6 +110,9 @@ export const ButtonClass = {
|
|||
if (attrs.contents) {
|
||||
contents.push(attrs.contents);
|
||||
}
|
||||
if (attrs.emoji && !left) {
|
||||
contents.push(this.attach("emoji", { name: attrs.emoji }));
|
||||
}
|
||||
if (attrs.icon && !left) {
|
||||
contents.push(this._buildIcon(attrs));
|
||||
}
|
||||
|
|
|
@ -68,6 +68,14 @@ createWidget("header-notifications", {
|
|||
),
|
||||
];
|
||||
|
||||
if (this.currentUser.status) {
|
||||
contents.push(
|
||||
this.attach("user-status-bubble", {
|
||||
emoji: this.currentUser.status.emoji,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (user.isInDoNotDisturb()) {
|
||||
contents.push(h("div.do-not-disturb-background", iconNode("moon")));
|
||||
} else {
|
||||
|
|
|
@ -16,13 +16,40 @@ createWidgetFrom(QuickAccessItem, "logout-item", {
|
|||
html() {
|
||||
return this.attach("flat-button", {
|
||||
action: "logout",
|
||||
content: I18n.t("user.log_out"),
|
||||
icon: "sign-out-alt",
|
||||
label: "user.log_out",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
createWidgetFrom(QuickAccessItem, "user-status-item", {
|
||||
tagName: "li.user-status",
|
||||
|
||||
html() {
|
||||
const action = "hideMenuAndSetStatus";
|
||||
const userStatus = this.currentUser.status;
|
||||
if (userStatus) {
|
||||
const emoji = userStatus.emoji ?? "mega";
|
||||
return this.attach("flat-button", {
|
||||
action,
|
||||
emoji,
|
||||
translatedLabel: userStatus.description,
|
||||
});
|
||||
} else {
|
||||
return this.attach("flat-button", {
|
||||
action,
|
||||
icon: "plus-circle",
|
||||
label: "user_status.set_custom_status",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
hideMenuAndSetStatus() {
|
||||
this.sendWidgetAction("toggleUserMenu");
|
||||
this.sendWidgetAction("setUserStatus");
|
||||
},
|
||||
});
|
||||
|
||||
createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
||||
tagName: "div.quick-access-panel.quick-access-profile",
|
||||
|
||||
|
@ -43,11 +70,16 @@ createWidgetFrom(QuickAccessPanel, "quick-access-profile", {
|
|||
},
|
||||
|
||||
_getItems() {
|
||||
let items = this._getDefaultItems();
|
||||
const items = [];
|
||||
|
||||
if (this.siteSettings.enable_user_status) {
|
||||
items.push({ widget: "user-status-item" });
|
||||
}
|
||||
items.push(...this._getDefaultItems());
|
||||
if (this._showToggleAnonymousButton()) {
|
||||
items.push(this._toggleAnonymousButton());
|
||||
}
|
||||
items = items.concat(_extraItems);
|
||||
items.push(..._extraItems);
|
||||
|
||||
if (this.attrs.showLogoutButton) {
|
||||
items.push({ widget: "logout-item" });
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { createWidget } from "discourse/widgets/widget";
|
||||
|
||||
export default createWidget("user-status-bubble", {
|
||||
tagName: "div.user-status-background",
|
||||
|
||||
html(attrs) {
|
||||
const emoji = attrs.emoji ?? "mega";
|
||||
return this.attach("emoji", { name: emoji });
|
||||
},
|
||||
});
|
|
@ -76,3 +76,30 @@ acceptance(
|
|||
});
|
||||
}
|
||||
);
|
||||
|
||||
acceptance("User Card - User Status", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
const response = cloneJSON(userFixtures["/u/charlie/card.json"]);
|
||||
response.user.status = { description: "off to dentist" };
|
||||
server.get("/u/charlie/card.json", () => helper.response(response));
|
||||
});
|
||||
|
||||
test("shows user status if enabled", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert.ok(exists(".user-card h3.user-status"));
|
||||
});
|
||||
|
||||
test("doesn't show user status if disabled", async function (assert) {
|
||||
this.siteSettings.enable_user_status = false;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click('a[data-user-card="charlie"]');
|
||||
|
||||
assert.notOk(exists(".user-card h3.user-status"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("User Status", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.put("/user-status.json", () => helper.response({ success: true }));
|
||||
server.delete("/user-status.json", () =>
|
||||
helper.response({ success: true })
|
||||
);
|
||||
});
|
||||
|
||||
const userStatusFallbackEmoji = "mega";
|
||||
const userStatus = "off to dentist";
|
||||
|
||||
test("doesn't show the user status button on the menu by default", async function (assert) {
|
||||
this.siteSettings.enable_user_status = false;
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
|
||||
assert.notOk(exists("div.quick-access-panel li.user-status"));
|
||||
});
|
||||
|
||||
test("shows the user status button on the menu when disabled in settings", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
|
||||
assert.ok(
|
||||
exists("div.quick-access-panel li.user-status"),
|
||||
"shows the button"
|
||||
);
|
||||
assert.ok(
|
||||
exists("div.quick-access-panel li.user-status svg.d-icon-plus-circle"),
|
||||
"shows the icon on the button"
|
||||
);
|
||||
});
|
||||
|
||||
test("shows user status on loaded page", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
updateCurrentUser({ status: { description: userStatus } });
|
||||
|
||||
await visit("/");
|
||||
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.d-button-label")
|
||||
.innerText,
|
||||
userStatus,
|
||||
"shows user status description on the menu"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query("div.quick-access-panel li.user-status img.emoji").alt,
|
||||
`:${userStatusFallbackEmoji}:`,
|
||||
"shows user status emoji on the menu"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(".header-dropdown-toggle .user-status-background img.emoji").alt,
|
||||
`:${userStatusFallbackEmoji}:`,
|
||||
"shows user status emoji on the user avatar in the header"
|
||||
);
|
||||
});
|
||||
|
||||
test("setting user status", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
await click(".user-status button");
|
||||
await fillIn(".user-status-description", userStatus);
|
||||
await click(".btn-primary");
|
||||
|
||||
assert.equal(
|
||||
query(".header-dropdown-toggle .user-status-background img.emoji").alt,
|
||||
`:${userStatusFallbackEmoji}:`,
|
||||
"shows user status emoji on the user avatar in the header"
|
||||
);
|
||||
|
||||
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.d-button-label")
|
||||
.innerText,
|
||||
userStatus,
|
||||
"shows user status description on the menu"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query("div.quick-access-panel li.user-status img.emoji").alt,
|
||||
`:${userStatusFallbackEmoji}:`,
|
||||
"shows user status emoji on the menu"
|
||||
);
|
||||
});
|
||||
|
||||
test("updating user status", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
updateCurrentUser({ status: { description: userStatus } });
|
||||
const updatedStatus = "off to dentist the second time";
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
await click(".user-status button");
|
||||
await fillIn(".user-status-description", updatedStatus);
|
||||
await click(".btn-primary");
|
||||
|
||||
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.d-button-label")
|
||||
.innerText,
|
||||
updatedStatus,
|
||||
"shows user status description on the menu"
|
||||
);
|
||||
});
|
||||
|
||||
test("clearing user status", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
updateCurrentUser({ status: { description: userStatus } });
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
await click(".user-status button");
|
||||
await click(".btn.delete-status");
|
||||
|
||||
assert.notOk(exists(".header-dropdown-toggle .user-status-background"));
|
||||
});
|
||||
|
||||
test("shows the trash button when editing status that was set before", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
updateCurrentUser({ status: { description: userStatus } });
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
await click(".user-status button");
|
||||
|
||||
assert.ok(exists(".btn.delete-status"));
|
||||
});
|
||||
|
||||
test("doesn't show the trash button when status wasn't set before", async function (assert) {
|
||||
this.siteSettings.enable_user_status = true;
|
||||
updateCurrentUser({ status: null });
|
||||
|
||||
await visit("/");
|
||||
await click(".header-dropdown-toggle.current-user");
|
||||
await click(".menu-links-row .user-preferences-link");
|
||||
await click(".user-status button");
|
||||
|
||||
assert.notOk(exists(".btn.delete-status"));
|
||||
});
|
||||
});
|
|
@ -41,6 +41,20 @@ discourseModule("Integration | Component | Widget | button", function (hooks) {
|
|||
},
|
||||
});
|
||||
|
||||
componentTest("emoji and text button", {
|
||||
template: hbs`{{mount-widget widget="button" args=args}}`,
|
||||
|
||||
beforeEach() {
|
||||
this.set("args", { emoji: "mega", label: "topic.create" });
|
||||
},
|
||||
|
||||
test(assert) {
|
||||
assert.ok(exists("button.widget-button"), "renders the widget");
|
||||
assert.ok(exists("button img.emoji"), "it renders the emoji");
|
||||
assert.ok(exists("button span.d-button-label"), "it renders the label");
|
||||
},
|
||||
});
|
||||
|
||||
componentTest("text only button", {
|
||||
template: hbs`{{mount-widget widget="button" args=args}}`,
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
@import "common/foundation/base";
|
||||
@import "common/select-kit/_index";
|
||||
@import "common/components/_index";
|
||||
@import "common/modal/_index";
|
||||
@import "common/input_tip";
|
||||
@import "common/topic-entrance";
|
||||
@import "common/printer-friendly";
|
||||
|
|
|
@ -401,7 +401,7 @@ table {
|
|||
|
||||
.d-header .header-dropdown-toggle .do-not-disturb-background {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
left: 0;
|
||||
bottom: -1px;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
@ -424,6 +424,29 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
.d-header .header-dropdown-toggle .user-status-background {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
bottom: -1px;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.user-status-background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
background-color: var(--tertiary-low);
|
||||
border-radius: 50%;
|
||||
|
||||
.emoji {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu .quick-access-panel li.do-not-disturb {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
|
|
|
@ -338,6 +338,13 @@
|
|||
padding-top: 0.2em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
padding-top: 0.2em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
.is-warning {
|
||||
.d-icon-far-envelope {
|
||||
|
|
|
@ -300,3 +300,9 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3.user-status {
|
||||
img.emoji {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import "user-status";
|
|
@ -0,0 +1,38 @@
|
|||
.user-status.modal {
|
||||
.modal-inner-container {
|
||||
box-sizing: border-box;
|
||||
min-width: 310px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin: 0;
|
||||
border-top: 0;
|
||||
padding: 10px 0;
|
||||
|
||||
.delete-status {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
width: 375px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ember-text-field.user-status-description {
|
||||
min-width: 220px;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.user-status-description-wrap {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserStatusController < ApplicationController
|
||||
requires_login
|
||||
|
||||
def set
|
||||
ensure_feature_enabled
|
||||
raise Discourse::InvalidParameters.new(:description) if params[:description].blank?
|
||||
|
||||
current_user.set_status!(params[:description])
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def clear
|
||||
ensure_feature_enabled
|
||||
current_user.clear_status!
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_feature_enabled
|
||||
raise ActionController::RoutingError.new("Not Found") if !SiteSetting.enable_user_status
|
||||
end
|
||||
end
|
|
@ -119,7 +119,8 @@ class UsersController < ApplicationController
|
|||
:card_background_upload,
|
||||
:primary_group,
|
||||
:flair_group,
|
||||
:primary_email
|
||||
:primary_email,
|
||||
:user_status
|
||||
)
|
||||
|
||||
users = users.filter { |u| guardian.can_see_profile?(u) }
|
||||
|
|
|
@ -63,6 +63,7 @@ class User < ActiveRecord::Base
|
|||
has_many :muted_user_records, class_name: 'MutedUser', dependent: :delete_all
|
||||
has_many :ignored_user_records, class_name: 'IgnoredUser', dependent: :delete_all
|
||||
has_many :do_not_disturb_timings, dependent: :delete_all
|
||||
has_one :user_status, dependent: :destroy
|
||||
|
||||
# dependent deleting handled via before_destroy (special cases)
|
||||
has_many :user_actions
|
||||
|
@ -1500,6 +1501,23 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def clear_status!
|
||||
user_status.destroy! if user_status
|
||||
end
|
||||
|
||||
def set_status!(description)
|
||||
now = Time.zone.now
|
||||
if user_status
|
||||
user_status.update!(description: description, set_at: now)
|
||||
else
|
||||
self.user_status = UserStatus.create!(
|
||||
user_id: id,
|
||||
description: description,
|
||||
set_at: now
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def badge_grant
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserStatus < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
|
||||
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? }
|
||||
|
||||
def ends_at_greater_than_set_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"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_statuses
|
||||
#
|
||||
# user_id :integer not null, primary key
|
||||
# emoji :string
|
||||
# description :string not null
|
||||
# set_at :datetime not null
|
||||
# ends_at :datetime
|
||||
#
|
|
@ -70,7 +70,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:default_calendar,
|
||||
:bookmark_auto_delete_preference,
|
||||
:pending_posts_count,
|
||||
:experimental_sidebar_enabled
|
||||
:experimental_sidebar_enabled,
|
||||
:status
|
||||
|
||||
delegate :user_stat, to: :object, private: true
|
||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||
|
@ -336,4 +337,12 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
def include_experimental_sidebar_enabled?
|
||||
SiteSetting.enable_experimental_sidebar
|
||||
end
|
||||
|
||||
def include_status?
|
||||
SiteSetting.enable_user_status
|
||||
end
|
||||
|
||||
def status
|
||||
UserStatusSerializer.new(object.user_status, root: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,7 +66,8 @@ class UserCardSerializer < BasicUserSerializer
|
|||
:flair_color,
|
||||
:featured_topic,
|
||||
:timezone,
|
||||
:pending_posts_count
|
||||
:pending_posts_count,
|
||||
:status
|
||||
|
||||
untrusted_attributes :bio_excerpt,
|
||||
:website,
|
||||
|
@ -222,6 +223,14 @@ class UserCardSerializer < BasicUserSerializer
|
|||
object.card_background_upload&.url
|
||||
end
|
||||
|
||||
def include_status?
|
||||
SiteSetting.enable_user_status
|
||||
end
|
||||
|
||||
def status
|
||||
UserStatusSerializer.new(user.user_status, root: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def custom_field_keys
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserStatusSerializer < ApplicationSerializer
|
||||
attributes :description, :emoji
|
||||
end
|
|
@ -37,6 +37,7 @@ class WebHookUserSerializer < UserSerializer
|
|||
user_auth_token_logs
|
||||
use_logo_small_as_avatar
|
||||
pending_posts_count
|
||||
status
|
||||
}.each do |attr|
|
||||
define_method("include_#{attr}?") do
|
||||
false
|
||||
|
|
|
@ -1785,6 +1785,11 @@ en:
|
|||
private_message: "message"
|
||||
the_topic: "the topic"
|
||||
|
||||
user_status:
|
||||
save: "Save"
|
||||
set_custom_status: "Set custom status"
|
||||
what_are_you_doing: "What are you doing?"
|
||||
|
||||
loading: "Loading..."
|
||||
errors:
|
||||
prev_page: "while trying to load"
|
||||
|
|
|
@ -5206,3 +5206,7 @@ en:
|
|||
small_action_post_raw: "Continue discussion at %{new_title}."
|
||||
|
||||
fallback_username: "user"
|
||||
|
||||
user_status:
|
||||
errors:
|
||||
ends_at_should_be_greater_than_set_at: "ends_at should be greater than set_at"
|
||||
|
|
|
@ -1020,6 +1020,9 @@ Discourse::Application.routes.draw do
|
|||
post "/presence/update" => "presence#update"
|
||||
get "/presence/get" => "presence#get"
|
||||
|
||||
put "user-status" => "user_status#set"
|
||||
delete "user-status" => "user_status#clear"
|
||||
|
||||
get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -361,6 +361,10 @@ basic:
|
|||
default: true
|
||||
sitemap_page_size:
|
||||
default: 10000
|
||||
enable_user_status:
|
||||
hidden: true
|
||||
client: true
|
||||
default: false
|
||||
|
||||
login:
|
||||
invite_only:
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateUserStatuses < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
create_table :user_statuses, id: false do |t|
|
||||
t.integer :user_id, primary_key: true, null: false
|
||||
t.string :emoji, null: true
|
||||
t.string :description, null: false
|
||||
t.datetime :set_at, null: false
|
||||
t.datetime :ends_at, null: true
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :user_statuses
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:user_status) do
|
||||
user
|
||||
set_at { Time.zone.now }
|
||||
|
||||
description { "off to dentists" }
|
||||
emoji { "tooth" }
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe UserStatus do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
describe "validations" do
|
||||
it 'is invalid when ends_at is before set_at' do
|
||||
freeze_time
|
||||
user_status = UserStatus.new(user: user, set_at: Time.zone.now, ends_at: 1.hour.ago)
|
||||
user_status.valid?
|
||||
expect(user_status.errors[:ends_at]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe UserStatusController do
|
||||
describe '#set' do
|
||||
it 'requires user to be logged in' do
|
||||
put "/user-status.json", params: { description: "off to dentist" }
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "returns 404 if the feature is disabled" do
|
||||
user = Fabricate(:user)
|
||||
sign_in(user)
|
||||
SiteSetting.enable_user_status = false
|
||||
|
||||
put "/user-status.json", params: { description: "off" }
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
describe 'feature is enabled and user is logged in' do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
SiteSetting.enable_user_status = true
|
||||
end
|
||||
|
||||
it "sets user status" do
|
||||
status = "off to dentist"
|
||||
put "/user-status.json", params: { description: status }
|
||||
expect(user.user_status.description).to eq(status)
|
||||
end
|
||||
|
||||
it 'the description parameter is mandatory' do
|
||||
put "/user-status.json", params: {}
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it "following calls update status" do
|
||||
status = "off to dentist"
|
||||
put "/user-status.json", params: { description: status }
|
||||
user.reload
|
||||
expect(user.user_status.description).to eq(status)
|
||||
|
||||
new_status = "working"
|
||||
put "/user-status.json", params: { description: new_status }
|
||||
user.reload
|
||||
expect(user.user_status.description).to eq(new_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clear' do
|
||||
it 'requires you to be logged in' do
|
||||
delete "/user-status.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "returns 404 if the feature is disabled" do
|
||||
user = Fabricate(:user)
|
||||
sign_in(user)
|
||||
SiteSetting.enable_user_status = false
|
||||
|
||||
delete "/user-status.json"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
describe 'feature is enabled and user is logged in' do
|
||||
fab!(:user_status) { Fabricate(:user_status, description: "off to dentist") }
|
||||
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
SiteSetting.enable_user_status = true
|
||||
end
|
||||
|
||||
it "clears user status" do
|
||||
delete "/user-status.json"
|
||||
|
||||
user.reload
|
||||
expect(user.user_status).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -182,4 +182,27 @@ RSpec.describe CurrentUserSerializer do
|
|||
expect(pending_posts_count).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
describe "#status" do
|
||||
fab!(:user_status) { Fabricate(:user_status) }
|
||||
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
||||
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
||||
|
||||
it "serializes when enabled" do
|
||||
SiteSetting.enable_user_status = true
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:status]).to_not be_nil do |status|
|
||||
expect(status.description).to eq(user_status.description)
|
||||
expect(status.emoji).to eq(user_status.emoji)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't serialize when disabled" do
|
||||
SiteSetting.enable_user_status = false
|
||||
json = serializer.as_json
|
||||
expect(json.keys).not_to include :status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,4 +70,27 @@ describe UserCardSerializer do
|
|||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#status" do
|
||||
fab!(:user_status) { Fabricate(:user_status) }
|
||||
fab!(:user) { Fabricate(:user, user_status: user_status) }
|
||||
let(:serializer) { described_class.new(user, scope: Guardian.new(user), root: false) }
|
||||
|
||||
it "serializes when enabled" do
|
||||
SiteSetting.enable_user_status = true
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:status]).to_not be_nil do |status|
|
||||
expect(status.description).to eq(user_status.description)
|
||||
expect(status.emoji).to eq(user_status.emoji)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't serialize when disabled" do
|
||||
SiteSetting.enable_user_status = false
|
||||
json = serializer.as_json
|
||||
expect(json.keys).not_to include :status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue