FEATURE: Use rich user status tooltip everywhere (#21125)
- Inline mentions on posts - Inline mentions on chat messages - The user autocomplete for the composer - The user autocomplete for chat - The chat section of the sidebar
This commit is contained in:
parent
5034eda386
commit
585a2e4e77
|
@ -42,6 +42,11 @@ import { isTesting } from "discourse-common/config/environment";
|
|||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
import {
|
||||
destroyTippyInstances,
|
||||
initUserStatusHtml,
|
||||
renderUserStatusHtml,
|
||||
} from "discourse/lib/user-status-on-autocomplete";
|
||||
|
||||
// original string `![image|foo=bar|690x220, 50%|bar=baz](upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title")`
|
||||
// group 1 `image|foo=bar`
|
||||
|
@ -220,18 +225,27 @@ export default Component.extend(
|
|||
if (this.siteSettings.enable_mentions) {
|
||||
$input.autocomplete({
|
||||
template: findRawTemplate("user-selector-autocomplete"),
|
||||
dataSource: (term) =>
|
||||
userSearch({
|
||||
dataSource: (term) => {
|
||||
destroyTippyInstances();
|
||||
return userSearch({
|
||||
term,
|
||||
topicId: this.topic?.id,
|
||||
categoryId: this.topic?.category_id || this.composer?.categoryId,
|
||||
includeGroups: true,
|
||||
}),
|
||||
}).then((result) => {
|
||||
initUserStatusHtml(result.users);
|
||||
return result;
|
||||
});
|
||||
},
|
||||
onRender: (options) => {
|
||||
renderUserStatusHtml(options);
|
||||
},
|
||||
key: "@",
|
||||
transformComplete: (v) => v.username || v.name,
|
||||
afterComplete: this._afterMentionComplete,
|
||||
triggerRule: (textarea) =>
|
||||
!inCodeBlock(textarea.value, caretPosition(textarea)),
|
||||
onClose: destroyTippyInstances,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ export default class DiscourseTooltip extends Component {
|
|||
}
|
||||
|
||||
stopPropagation(instance, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
}}
|
||||
>
|
||||
{{@content}}
|
||||
{{@contentComponent}}
|
||||
</span>
|
||||
|
||||
{{#if @badgeText}}
|
||||
|
|
|
@ -47,6 +47,10 @@
|
|||
@didInsert={{link.didInsert}}
|
||||
@willDestroy={{link.willDestroy}}
|
||||
@content={{link.text}}
|
||||
@contentComponent={{component
|
||||
link.contentComponent
|
||||
status=link.contentComponentArgs
|
||||
}}
|
||||
/>
|
||||
{{/each}}
|
||||
</Sidebar::Section>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{#if @status}}
|
||||
<span class={{concat-class "user-status-message" @class}}>
|
||||
{{emoji @status.emoji skipTitle=true}}
|
||||
{{#if @showDescription}}
|
||||
|
@ -21,3 +22,4 @@
|
|||
</DTooltip>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
|
@ -148,6 +148,7 @@ export default function (options) {
|
|||
|
||||
function closeAutocomplete() {
|
||||
_autoCompletePopper?.destroy();
|
||||
options.onClose && options.onClose();
|
||||
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
|
@ -404,6 +405,10 @@ export default function (options) {
|
|||
scrollElement = div.find(options.scrollElementSelector);
|
||||
}
|
||||
|
||||
if (options.onRender) {
|
||||
options.onRender(autocompleteOptions);
|
||||
}
|
||||
|
||||
if (isInput || options.treatAsTextarea) {
|
||||
_autoCompletePopper && _autoCompletePopper.destroy();
|
||||
_autoCompletePopper = createPopper(me[0], div[0], {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import tippy from "tippy.js";
|
||||
|
||||
function stopPropagation(instance, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
function hasTouchCapabilities() {
|
||||
return navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
export default function createDTooltip(target, content) {
|
||||
return tippy(target, {
|
||||
interactive: false,
|
||||
content,
|
||||
trigger: hasTouchCapabilities() ? "click" : "mouseenter",
|
||||
theme: "d-tooltip",
|
||||
arrow: false,
|
||||
placement: "bottom-start",
|
||||
onTrigger: stopPropagation,
|
||||
onUntrigger: stopPropagation,
|
||||
});
|
||||
}
|
|
@ -1,36 +1,14 @@
|
|||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import { until } from "discourse/lib/formatter";
|
||||
import createUserStatusMessage from "discourse/lib/user-status-message";
|
||||
|
||||
export function updateUserStatusOnMention(mention, status, currentUser) {
|
||||
export function updateUserStatusOnMention(mention, status, tippyInstances) {
|
||||
removeStatus(mention);
|
||||
if (status) {
|
||||
const html = statusHtml(status, currentUser);
|
||||
mention.insertAdjacentHTML("beforeend", html);
|
||||
const statusHtml = createUserStatusMessage(status, { showTooltip: true });
|
||||
tippyInstances.push(statusHtml._tippy);
|
||||
mention.appendChild(statusHtml);
|
||||
}
|
||||
}
|
||||
|
||||
function removeStatus(mention) {
|
||||
mention.querySelector("img.user-status")?.remove();
|
||||
}
|
||||
|
||||
function statusHtml(status, currentUser) {
|
||||
const emoji = escapeExpression(`:${status.emoji}:`);
|
||||
return emojiUnescape(emoji, {
|
||||
class: "user-status",
|
||||
title: statusTitle(status, currentUser),
|
||||
});
|
||||
}
|
||||
|
||||
function statusTitle(status, currentUser) {
|
||||
if (!status.ends_at) {
|
||||
return status.description;
|
||||
}
|
||||
|
||||
const timezone = currentUser
|
||||
? currentUser.user_option?.timezone
|
||||
: moment.tz.guess();
|
||||
|
||||
const until_ = until(status.ends_at, timezone, currentUser?.locale);
|
||||
return escapeExpression(`${status.description} ${until_}`);
|
||||
mention.querySelector("span.user-status-message")?.remove();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import createDTooltip from "discourse/lib/d-tooltip";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { until } from "discourse/lib/formatter";
|
||||
import User from "discourse/models/user";
|
||||
|
||||
function getUntil(endsAt) {
|
||||
const currentUser = User.current();
|
||||
|
||||
const timezone = currentUser
|
||||
? currentUser.user_option?.timezone
|
||||
: moment.tz.guess();
|
||||
|
||||
return until(endsAt, timezone, currentUser?.locale);
|
||||
}
|
||||
|
||||
function getEmoji(emojiName) {
|
||||
const emoji = escapeExpression(`:${emojiName}:`);
|
||||
return emojiUnescape(emoji, {
|
||||
skipTitle: true,
|
||||
});
|
||||
}
|
||||
|
||||
function attachTooltip(target, status) {
|
||||
const content = document.createElement("div");
|
||||
content.classList.add("user-status-message-tooltip");
|
||||
content.innerHTML = getEmoji(status.emoji);
|
||||
|
||||
const tooltipDescription = document.createElement("span");
|
||||
tooltipDescription.classList.add("user-status-tooltip-description");
|
||||
tooltipDescription.innerText = status.description;
|
||||
content.appendChild(tooltipDescription);
|
||||
|
||||
if (status.ends_at) {
|
||||
const untilElement = document.createElement("div");
|
||||
untilElement.classList.add("user-status-tooltip-until");
|
||||
untilElement.innerText = getUntil(status.ends_at);
|
||||
content.appendChild(untilElement);
|
||||
}
|
||||
createDTooltip(target, content);
|
||||
}
|
||||
|
||||
export default function createUserStatusMessage(status, opts) {
|
||||
const userStatusMessage = document.createElement("span");
|
||||
userStatusMessage.classList.add("user-status-message");
|
||||
if (opts?.class) {
|
||||
userStatusMessage.classList.add(opts.class);
|
||||
}
|
||||
userStatusMessage.innerHTML = getEmoji(status.emoji);
|
||||
|
||||
if (opts?.showDescription) {
|
||||
const messageDescription = document.createElement("span");
|
||||
messageDescription.classList.add("user-status-message-description");
|
||||
messageDescription.innerText = status.description;
|
||||
userStatusMessage.appendChild(messageDescription);
|
||||
}
|
||||
|
||||
if (opts?.showTooltip) {
|
||||
attachTooltip(userStatusMessage, status);
|
||||
}
|
||||
return userStatusMessage;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import createUserStatusMessage from "discourse/lib/user-status-message";
|
||||
|
||||
let tippyInstances = [];
|
||||
|
||||
export function initUserStatusHtml(users) {
|
||||
(users || []).forEach((user, index) => {
|
||||
if (user.status) {
|
||||
user.index = index;
|
||||
user.statusHtml = createUserStatusMessage(user.status, {
|
||||
showTooltip: true,
|
||||
showDescription: true,
|
||||
});
|
||||
tippyInstances.push(user.statusHtml._tippy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function renderUserStatusHtml(options) {
|
||||
const users = document.querySelectorAll(".autocomplete.ac-user li");
|
||||
users.forEach((user) => {
|
||||
const index = user.dataset.index;
|
||||
const statusHtml = options.find(function (el) {
|
||||
return el.index === parseInt(index, 10);
|
||||
})?.statusHtml;
|
||||
if (statusHtml) {
|
||||
user.querySelector(".user-status").replaceWith(statusHtml);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyTippyInstances() {
|
||||
tippyInstances.forEach((instance) => {
|
||||
instance.destroy();
|
||||
});
|
||||
tippyInstances = [];
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<ul>
|
||||
{{#each options as |item|}}
|
||||
{{#if item.isUser}}
|
||||
<li>
|
||||
<li data-index={{item.index}}>
|
||||
<a href title="{{item.name}}" class="{{item.cssClasses}}">
|
||||
{{avatar item imageSize="tiny"}}
|
||||
<span class='username'>{{format-username item.username}}</span>
|
||||
|
@ -10,13 +10,7 @@
|
|||
<span class='name'>{{item.name}}</span>
|
||||
{{/if}}
|
||||
{{#if item.status}}
|
||||
{{emoji item.status.emoji}}
|
||||
<span class='status-description' title='{{item.status.description}}'>
|
||||
{{item.status.description}}
|
||||
</span>
|
||||
{{#if item.status.ends_at}}
|
||||
{{format-age item.status.ends_at}}
|
||||
{{/if}}
|
||||
<span class='user-status'></span>
|
||||
{{/if}}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -36,6 +36,7 @@ function createDetachedElement(nodeName) {
|
|||
|
||||
export default class PostCooked {
|
||||
originalQuoteContents = null;
|
||||
tippyInstances = [];
|
||||
|
||||
constructor(attrs, decoratorHelper, currentUser) {
|
||||
this.attrs = attrs;
|
||||
|
@ -76,6 +77,7 @@ export default class PostCooked {
|
|||
|
||||
destroy() {
|
||||
this._stopTrackingMentionedUsersStatus();
|
||||
this._destroyTippyInstances();
|
||||
}
|
||||
|
||||
_decorateAndAdopt(cooked) {
|
||||
|
@ -380,7 +382,14 @@ export default class PostCooked {
|
|||
}
|
||||
}
|
||||
|
||||
_destroyTippyInstances() {
|
||||
this.tippyInstances.forEach((instance) => {
|
||||
instance.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
_rerenderUserStatusOnMentions() {
|
||||
this._destroyTippyInstances();
|
||||
this._post()?.mentioned_users?.forEach((user) =>
|
||||
this._rerenderUserStatusOnMention(this.cookedDiv, user)
|
||||
);
|
||||
|
@ -391,7 +400,7 @@ export default class PostCooked {
|
|||
const mentions = postElement.querySelectorAll(`a.mention[href="${href}"]`);
|
||||
|
||||
mentions.forEach((mention) => {
|
||||
updateUserStatusOnMention(mention, user.status, this.currentUser);
|
||||
updateUserStatusOnMention(mention, user.status, this.tippyInstances);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -137,19 +137,16 @@ acceptance("Composer - editor mentions", function (needs) {
|
|||
await emulateAutocomplete(".d-editor-input", "@u");
|
||||
|
||||
assert.ok(
|
||||
exists(`.autocomplete .emoji[title='${status.emoji}']`),
|
||||
exists(`.autocomplete .emoji[alt='${status.emoji}']`),
|
||||
"status emoji is shown"
|
||||
);
|
||||
assert.equal(
|
||||
query(".autocomplete .status-description").textContent.trim(),
|
||||
query(
|
||||
".autocomplete .user-status-message-description"
|
||||
).textContent.trim(),
|
||||
status.description,
|
||||
"status description is shown"
|
||||
);
|
||||
assert.equal(
|
||||
query(".autocomplete .relative-date").textContent.trim(),
|
||||
"1h",
|
||||
"status expiration time is shown"
|
||||
);
|
||||
});
|
||||
|
||||
test("metadata matches are moved to the end", async function (assert) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
publishToMessageBus,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { triggerEvent, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import topicFixtures from "../fixtures/topic";
|
||||
|
@ -32,6 +32,14 @@ function topicWithUserStatus(topicId, mentionedUserId, status) {
|
|||
return topic;
|
||||
}
|
||||
|
||||
async function mouseenter() {
|
||||
await triggerEvent(query(".user-status-message"), "mouseenter");
|
||||
}
|
||||
|
||||
async function mouseleave() {
|
||||
await triggerEvent(query(".user-status-message"), "mouseleave");
|
||||
}
|
||||
|
||||
acceptance("Post inline mentions", function (needs) {
|
||||
needs.user();
|
||||
|
||||
|
@ -51,19 +59,28 @@ acceptance("Post inline mentions", function (needs) {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"user status is shown"
|
||||
);
|
||||
const statusElement = query(".topic-post .cooked .mention .user-status");
|
||||
assert.equal(
|
||||
statusElement.title,
|
||||
status.description,
|
||||
"status description is correct"
|
||||
|
||||
const statusElement = query(
|
||||
".topic-post .cooked .mention .user-status-message img"
|
||||
);
|
||||
assert.ok(
|
||||
statusElement.src.includes(status.emoji),
|
||||
"status emoji is correct"
|
||||
);
|
||||
|
||||
await mouseenter();
|
||||
const statusTooltipDescription = document.querySelector(
|
||||
".user-status-message-tooltip .user-status-tooltip-description"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
statusTooltipDescription.innerText,
|
||||
status.description,
|
||||
"status description is correct"
|
||||
);
|
||||
});
|
||||
|
||||
test("inserts user status on message bus message", async function (assert) {
|
||||
|
@ -73,7 +90,7 @@ acceptance("Post inline mentions", function (needs) {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.notOk(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"user status isn't shown"
|
||||
);
|
||||
|
||||
|
@ -85,19 +102,30 @@ acceptance("Post inline mentions", function (needs) {
|
|||
});
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"user status is shown"
|
||||
);
|
||||
const statusElement = query(".topic-post .cooked .mention .user-status");
|
||||
assert.equal(
|
||||
statusElement.title,
|
||||
status.description,
|
||||
"status description is correct"
|
||||
|
||||
const statusElement = query(
|
||||
".topic-post .cooked .mention .user-status-message img"
|
||||
);
|
||||
assert.ok(
|
||||
statusElement.src.includes(status.emoji),
|
||||
"status emoji is correct"
|
||||
);
|
||||
|
||||
await mouseenter();
|
||||
const statusTooltipDescription = document.querySelector(
|
||||
".user-status-message-tooltip .user-status-tooltip-description"
|
||||
);
|
||||
assert.equal(
|
||||
statusTooltipDescription.innerText,
|
||||
status.description,
|
||||
"status description is correct"
|
||||
);
|
||||
|
||||
// Needed to remove the tooltip in between tests
|
||||
await mouseleave();
|
||||
});
|
||||
|
||||
test("updates user status on message bus message", async function (assert) {
|
||||
|
@ -107,7 +135,7 @@ acceptance("Post inline mentions", function (needs) {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"initial user status is shown"
|
||||
);
|
||||
|
||||
|
@ -122,20 +150,32 @@ acceptance("Post inline mentions", function (needs) {
|
|||
},
|
||||
});
|
||||
|
||||
await mouseenter();
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"updated user status is shown"
|
||||
);
|
||||
const statusElement = query(".topic-post .cooked .mention .user-status");
|
||||
assert.equal(
|
||||
statusElement.title,
|
||||
newStatus.description,
|
||||
"updated status description is correct"
|
||||
|
||||
const statusElement = query(
|
||||
".topic-post .cooked .mention .user-status-message img"
|
||||
);
|
||||
assert.ok(
|
||||
statusElement.src.includes(newStatus.emoji),
|
||||
"updated status emoji is correct"
|
||||
);
|
||||
|
||||
const statusTooltipDescription = document.querySelector(
|
||||
".user-status-message-tooltip .user-status-tooltip-description"
|
||||
);
|
||||
assert.equal(
|
||||
statusTooltipDescription.innerText,
|
||||
newStatus.description,
|
||||
"updated status description is correct"
|
||||
);
|
||||
|
||||
// Needed to remove the tooltip in between tests
|
||||
await mouseleave();
|
||||
});
|
||||
|
||||
test("removes user status on message bus message", async function (assert) {
|
||||
|
@ -145,7 +185,7 @@ acceptance("Post inline mentions", function (needs) {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"initial user status is shown"
|
||||
);
|
||||
|
||||
|
@ -154,7 +194,7 @@ acceptance("Post inline mentions", function (needs) {
|
|||
});
|
||||
|
||||
assert.notOk(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"updated user has disappeared"
|
||||
);
|
||||
});
|
||||
|
@ -177,7 +217,7 @@ acceptance("Post inline mentions as an anonymous user", function () {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"user status is shown"
|
||||
);
|
||||
});
|
||||
|
@ -197,7 +237,7 @@ acceptance("Post inline mentions as an anonymous user", function () {
|
|||
await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`);
|
||||
|
||||
assert.ok(
|
||||
exists(".topic-post .cooked .mention .user-status"),
|
||||
exists(".topic-post .cooked .mention .user-status-message"),
|
||||
"user status is shown"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -475,10 +475,17 @@ html.composer-open {
|
|||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.status-description {
|
||||
.user-status-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
|
||||
.user-status-message-description {
|
||||
@include ellipsis;
|
||||
font-size: var(--font-down-2);
|
||||
color: var(--primary-high);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.relative-date {
|
||||
|
|
|
@ -20,6 +20,11 @@ import { isPresent } from "@ember/utils";
|
|||
import { Promise } from "rsvp";
|
||||
import User from "discourse/models/user";
|
||||
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
|
||||
import {
|
||||
destroyTippyInstances,
|
||||
initUserStatusHtml,
|
||||
renderUserStatusHtml,
|
||||
} from "discourse/lib/user-status-on-autocomplete";
|
||||
|
||||
export default class ChatComposer extends Component {
|
||||
@service capabilities;
|
||||
|
@ -409,6 +414,7 @@ export default class ChatComposer extends Component {
|
|||
return obj.username || obj.name;
|
||||
},
|
||||
dataSource: (term) => {
|
||||
destroyTippyInstances();
|
||||
return userSearch({ term, includeGroups: true }).then((result) => {
|
||||
if (result?.users?.length > 0) {
|
||||
const presentUserNames =
|
||||
|
@ -418,16 +424,21 @@ export default class ChatComposer extends Component {
|
|||
user.cssClasses = "is-online";
|
||||
}
|
||||
});
|
||||
initUserStatusHtml(result.users);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
onRender: (options) => {
|
||||
renderUserStatusHtml(options);
|
||||
},
|
||||
afterComplete: (text, event) => {
|
||||
event.preventDefault();
|
||||
this.composer.textarea.value = text;
|
||||
this.composer.focus();
|
||||
this.captureMentions();
|
||||
},
|
||||
onClose: destroyTippyInstances,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-m
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
let _chatMessageDecorators = [];
|
||||
let _tippyInstances = [];
|
||||
|
||||
export function addChatMessageDecorator(decorator) {
|
||||
_chatMessageDecorators.push(decorator);
|
||||
|
@ -136,6 +137,13 @@ export default class ChatMessage extends Component {
|
|||
this.#teardownMentionedUsers();
|
||||
}
|
||||
|
||||
#destroyTippyInstances() {
|
||||
_tippyInstances.forEach((instance) => {
|
||||
instance.destroy();
|
||||
});
|
||||
_tippyInstances = [];
|
||||
}
|
||||
|
||||
@action
|
||||
refreshStatusOnMentions() {
|
||||
schedule("afterRender", () => {
|
||||
|
@ -146,7 +154,7 @@ export default class ChatMessage extends Component {
|
|||
);
|
||||
|
||||
mentions.forEach((mention) => {
|
||||
updateUserStatusOnMention(mention, user.status, this.currentUser);
|
||||
updateUserStatusOnMention(mention, user.status, _tippyInstances);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -402,5 +410,6 @@ export default class ChatMessage extends Component {
|
|||
user.stopTrackingStatus();
|
||||
user.off("status-changed", this, "refreshStatusOnMentions");
|
||||
});
|
||||
this.#destroyTippyInstances();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,15 +227,19 @@ export default {
|
|||
return this.channel.chatable.users.length === 1;
|
||||
}
|
||||
|
||||
get contentComponentArgs() {
|
||||
return this.channel.chatable.users[0].get("status");
|
||||
}
|
||||
|
||||
get contentComponent() {
|
||||
return "user-status-message";
|
||||
}
|
||||
|
||||
get text() {
|
||||
const username = this.channel.escapedTitle.replaceAll("@", "");
|
||||
if (this.oneOnOneMessage) {
|
||||
const status = this.channel.chatable.users[0].get("status");
|
||||
const statusHtml = status ? this._userStatusHtml(status) : "";
|
||||
return htmlSafe(
|
||||
`${escapeExpression(
|
||||
username
|
||||
)}${statusHtml} ${decorateUsername(
|
||||
`${escapeExpression(username)}${decorateUsername(
|
||||
escapeExpression(username)
|
||||
)}`
|
||||
);
|
||||
|
@ -307,14 +311,6 @@ export default {
|
|||
return I18n.t("chat.direct_messages.leave");
|
||||
}
|
||||
|
||||
_userStatusHtml(status) {
|
||||
const emoji = escapeExpression(`:${status.emoji}:`);
|
||||
const title = this._userStatusTitle(status);
|
||||
return `<span class="user-status">${emojiUnescape(emoji, {
|
||||
title,
|
||||
})}</span>`;
|
||||
}
|
||||
|
||||
_userStatusTitle(status) {
|
||||
let title = `${escapeExpression(status.description)}`;
|
||||
|
||||
|
|
|
@ -181,10 +181,10 @@ RSpec.describe "Chat channel", type: :system do
|
|||
chat.visit_channel(channel_1)
|
||||
|
||||
expect(page).to have_selector(
|
||||
".mention .user-status[title='#{current_user.user_status.description}']",
|
||||
".mention .user-status-message img[alt='#{current_user.user_status.emoji}']",
|
||||
)
|
||||
expect(page).to have_selector(
|
||||
".mention .user-status[title='#{other_user.user_status.description}']",
|
||||
".mention .user-status-message img[alt='#{other_user.user_status.emoji}']",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -146,7 +146,7 @@ RSpec.describe "Sidebar navigation menu", type: :system do
|
|||
visit("/")
|
||||
|
||||
expect(sidebar_page.dms_section.find("a.sidebar-section-link:nth-child(1)")).to have_css(
|
||||
".user-status",
|
||||
".user-status-message",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,8 +20,8 @@ RSpec.describe "User status | sidebar", type: :system do
|
|||
|
||||
visit("/")
|
||||
|
||||
expect(find(".user-status .emoji")["title"]).to eq("online")
|
||||
expect(find(".user-status .emoji")["src"]).to include("heart")
|
||||
expect(find(".user-status-message .emoji")["alt"]).to eq("heart")
|
||||
expect(find(".user-status-message .emoji")["src"]).to include("heart")
|
||||
end
|
||||
|
||||
context "when changing status" do
|
||||
|
@ -31,8 +31,8 @@ RSpec.describe "User status | sidebar", type: :system do
|
|||
visit("/")
|
||||
current_user.set_status!("offline", "tooth")
|
||||
|
||||
expect(page).to have_css('.user-status .emoji[title="offline"]')
|
||||
expect(find(".user-status .emoji")["src"]).to include("tooth")
|
||||
expect(page).to have_css('.user-status-message .emoji[alt="tooth"]')
|
||||
expect(find(".user-status-message .emoji")["src"]).to include("tooth")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,7 +43,7 @@ RSpec.describe "User status | sidebar", type: :system do
|
|||
visit("/")
|
||||
current_user.clear_status!
|
||||
|
||||
expect(page).to have_no_css(".user-status")
|
||||
expect(page).to have_no_css(".user-status-message")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,6 +102,11 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
statusSelector(mentionedUser2.username),
|
||||
mentionedUser2.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser2.username),
|
||||
mentionedUser2.status
|
||||
);
|
||||
});
|
||||
|
||||
skip("just posted messages | it updates status on mentions", async function (assert) {
|
||||
|
@ -115,6 +120,7 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
const selector = statusSelector(mentionedUser2.username);
|
||||
await waitFor(selector);
|
||||
assertStatusIsRendered(assert, selector, newStatus);
|
||||
await assertStatusTooltipIsRendered(assert, selector, newStatus);
|
||||
});
|
||||
|
||||
skip("just posted messages | it deletes status on mentions", async function (assert) {
|
||||
|
@ -144,6 +150,11 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
statusSelector(mentionedUser3.username),
|
||||
mentionedUser3.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser3.username),
|
||||
mentionedUser3.status
|
||||
);
|
||||
});
|
||||
|
||||
skip("edited messages | it updates status on mentions", async function (assert) {
|
||||
|
@ -160,6 +171,7 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
const selector = statusSelector(mentionedUser3.username);
|
||||
await waitFor(selector);
|
||||
assertStatusIsRendered(assert, selector, newStatus);
|
||||
await assertStatusTooltipIsRendered(assert, selector, newStatus);
|
||||
});
|
||||
|
||||
skip("edited messages | it deletes status on mentions", async function (assert) {
|
||||
|
@ -190,6 +202,11 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
statusSelector(mentionedUser1.username),
|
||||
mentionedUser1.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser1.username),
|
||||
mentionedUser1.status
|
||||
);
|
||||
});
|
||||
|
||||
test("deleted messages | it updates status on mentions", async function (assert) {
|
||||
|
@ -205,6 +222,7 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
const selector = statusSelector(mentionedUser1.username);
|
||||
await waitFor(selector);
|
||||
assertStatusIsRendered(assert, selector, newStatus);
|
||||
await assertStatusTooltipIsRendered(assert, selector, newStatus);
|
||||
});
|
||||
|
||||
test("deleted messages | it deletes status on mentions", async function (assert) {
|
||||
|
@ -233,6 +251,11 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
statusSelector(mentionedUser1.username),
|
||||
mentionedUser1.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser1.username),
|
||||
mentionedUser1.status
|
||||
);
|
||||
});
|
||||
|
||||
test("restored messages | it updates status on mentions", async function (assert) {
|
||||
|
@ -248,6 +271,7 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
const selector = statusSelector(mentionedUser1.username);
|
||||
await waitFor(selector);
|
||||
assertStatusIsRendered(assert, selector, newStatus);
|
||||
await assertStatusTooltipIsRendered(assert, selector, newStatus);
|
||||
});
|
||||
|
||||
test("restored messages | it deletes status on mentions", async function (assert) {
|
||||
|
@ -269,11 +293,6 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
assert
|
||||
.dom(selector)
|
||||
.exists("status is rendered")
|
||||
.hasAttribute(
|
||||
"title",
|
||||
status.description,
|
||||
"status description is updated"
|
||||
)
|
||||
.hasAttribute(
|
||||
"src",
|
||||
new RegExp(`${status.emoji}.png`),
|
||||
|
@ -281,6 +300,27 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
);
|
||||
}
|
||||
|
||||
async function assertStatusTooltipIsRendered(assert, selector, status) {
|
||||
await triggerEvent(selector, "mouseenter");
|
||||
|
||||
assert.equal(
|
||||
document
|
||||
.querySelector(".user-status-tooltip-description")
|
||||
.textContent.trim(),
|
||||
status.description,
|
||||
"status description is correct"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(
|
||||
`.user-status-message-tooltip img[alt='${status.emoji}']`
|
||||
),
|
||||
"status emoji is correct"
|
||||
);
|
||||
|
||||
await triggerEvent(selector, "mouseleave");
|
||||
}
|
||||
|
||||
async function deleteMessage(messageSelector) {
|
||||
await triggerEvent(query(messageSelector), "mouseenter");
|
||||
await click(".more-buttons .select-kit-header-wrapper");
|
||||
|
@ -340,6 +380,6 @@ acceptance("Chat | User status on mentions", function (needs) {
|
|||
}
|
||||
|
||||
function statusSelector(username) {
|
||||
return `.mention[href='/u/${username}'] .user-status`;
|
||||
return `.mention[href='/u/${username}'] .user-status-message img`;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
|
||||
import { render, waitFor } from "@ember/test-helpers";
|
||||
import { render, triggerEvent, waitFor } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import pretender, { OK } from "discourse/tests/helpers/create-pretender";
|
||||
import { publishToMessageBus } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
@ -76,6 +76,11 @@ module(
|
|||
statusSelector(mentionedUser.username),
|
||||
mentionedUser.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser.username),
|
||||
mentionedUser.status
|
||||
);
|
||||
});
|
||||
|
||||
test("it updates status on mentions", async function (assert) {
|
||||
|
@ -97,6 +102,11 @@ module(
|
|||
statusSelector(mentionedUser.username),
|
||||
newStatus
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser.username),
|
||||
newStatus
|
||||
);
|
||||
});
|
||||
|
||||
test("it deletes status on mentions", async function (assert) {
|
||||
|
@ -121,6 +131,11 @@ module(
|
|||
statusSelector(mentionedUser2.username),
|
||||
mentionedUser2.status
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser2.username),
|
||||
mentionedUser2.status
|
||||
);
|
||||
});
|
||||
|
||||
test("it updates status on mentions on messages that came from Message Bus", async function (assert) {
|
||||
|
@ -142,6 +157,11 @@ module(
|
|||
statusSelector(mentionedUser2.username),
|
||||
newStatus
|
||||
);
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser2.username),
|
||||
newStatus
|
||||
);
|
||||
});
|
||||
|
||||
test("it deletes status on mentions on messages that came from Message Bus", async function (assert) {
|
||||
|
@ -161,11 +181,6 @@ module(
|
|||
assert
|
||||
.dom(selector)
|
||||
.exists("status is rendered")
|
||||
.hasAttribute(
|
||||
"title",
|
||||
status.description,
|
||||
"status description is updated"
|
||||
)
|
||||
.hasAttribute(
|
||||
"src",
|
||||
new RegExp(`${status.emoji}.png`),
|
||||
|
@ -173,6 +188,27 @@ module(
|
|||
);
|
||||
}
|
||||
|
||||
async function assertStatusTooltipIsRendered(assert, selector, status) {
|
||||
await triggerEvent(selector, "mouseenter");
|
||||
|
||||
assert.equal(
|
||||
document
|
||||
.querySelector(".user-status-tooltip-description")
|
||||
.textContent.trim(),
|
||||
status.description,
|
||||
"status description is correct"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(
|
||||
`.user-status-message-tooltip img[alt='${status.emoji}']`
|
||||
),
|
||||
"status emoji is correct"
|
||||
);
|
||||
|
||||
await triggerEvent(selector, "mouseleave");
|
||||
}
|
||||
|
||||
async function receiveChatMessageViaMessageBus() {
|
||||
await publishToMessageBus(`/chat/${channelId}`, {
|
||||
chat_message: {
|
||||
|
@ -193,7 +229,7 @@ module(
|
|||
}
|
||||
|
||||
function statusSelector(username) {
|
||||
return `.mention[href='/u/${username}'] .user-status`;
|
||||
return `.mention[href='/u/${username}'] .user-status-message img`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue