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:
Jan Cernik 2023-07-03 11:09:41 -03:00 committed by GitHub
parent 5034eda386
commit 585a2e4e77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 398 additions and 134 deletions

View File

@ -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,
});
}

View File

@ -19,6 +19,7 @@ export default class DiscourseTooltip extends Component {
}
stopPropagation(instance, event) {
event.preventDefault();
event.stopPropagation();
}

View File

@ -52,6 +52,7 @@
}}
>
{{@content}}
{{@contentComponent}}
</span>
{{#if @badgeText}}

View File

@ -47,6 +47,10 @@
@didInsert={{link.didInsert}}
@willDestroy={{link.willDestroy}}
@content={{link.text}}
@contentComponent={{component
link.contentComponent
status=link.contentComponentArgs
}}
/>
{{/each}}
</Sidebar::Section>

View File

@ -1,23 +1,25 @@
<span class={{concat-class "user-status-message" @class}}>
{{emoji @status.emoji skipTitle=true}}
{{#if @showDescription}}
<span class="user-status-message-description">
{{@status.description}}
</span>
{{/if}}
{{#if this.showTooltip}}
<DTooltip>
<div class="user-status-message-tooltip">
{{emoji @status.emoji skipTitle=true}}
<span class="user-status-tooltip-description">
{{@status.description}}
</span>
{{#if this.until}}
<div class="user-status-tooltip-until">
{{this.until}}
</div>
{{/if}}
</div>
</DTooltip>
{{/if}}
</span>
{{#if @status}}
<span class={{concat-class "user-status-message" @class}}>
{{emoji @status.emoji skipTitle=true}}
{{#if @showDescription}}
<span class="user-status-message-description">
{{@status.description}}
</span>
{{/if}}
{{#if this.showTooltip}}
<DTooltip>
<div class="user-status-message-tooltip">
{{emoji @status.emoji skipTitle=true}}
<span class="user-status-tooltip-description">
{{@status.description}}
</span>
{{#if this.until}}
<div class="user-status-tooltip-until">
{{this.until}}
</div>
{{/if}}
</div>
</DTooltip>
{{/if}}
</span>
{{/if}}

View File

@ -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], {

View File

@ -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,
});
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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 = [];
}

View File

@ -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>

View File

@ -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);
});
}

View File

@ -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) {

View File

@ -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"
);
});

View File

@ -475,10 +475,17 @@ html.composer-open {
color: var(--primary-high);
}
.status-description {
@include ellipsis;
font-size: var(--font-down-2);
color: var(--primary-high);
.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 {

View File

@ -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,
});
}

View File

@ -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();
}
}

View File

@ -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)}`;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`;
}
});

View File

@ -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`;
}
}
);