From 585a2e4e77913c9732313f051bada4c121af76cd Mon Sep 17 00:00:00 2001
From: Jan Cernik <66427541+jancernik@users.noreply.github.com>
Date: Mon, 3 Jul 2023 11:09:41 -0300
Subject: [PATCH] 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
---
.../app/components/composer-editor.js | 20 ++++-
.../discourse/app/components/d-tooltip.js | 1 +
.../app/components/sidebar/section-link.hbs | 1 +
.../app/components/sidebar/user/sections.hbs | 4 +
.../app/components/user-status-message.hbs | 48 +++++-----
.../discourse/app/lib/autocomplete.js | 5 ++
.../discourse/app/lib/d-tooltip.js | 22 +++++
.../app/lib/update-user-status-on-mention.js | 34 ++-----
.../discourse/app/lib/user-status-message.js | 62 +++++++++++++
.../app/lib/user-status-on-autocomplete.js | 36 ++++++++
.../templates/user-selector-autocomplete.hbr | 10 +--
.../discourse/app/widgets/post-cooked.js | 11 ++-
.../composer-editor-mentions-test.js | 11 +--
.../acceptance/post-inline-mentions-test.js | 90 +++++++++++++------
.../stylesheets/common/base/compose.scss | 15 +++-
.../discourse/components/chat-composer.js | 11 +++
.../discourse/components/chat-message.js | 11 ++-
.../discourse/initializers/chat-sidebar.js | 22 ++---
plugins/chat/spec/system/chat_channel_spec.rb | 4 +-
.../system/sidebar_navigation_menu_spec.rb | 2 +-
.../spec/system/user_status/sidebar_spec.rb | 10 +--
.../user-status-on-mentions-test.js | 52 +++++++++--
.../components/chat-channel-test.js | 50 +++++++++--
23 files changed, 398 insertions(+), 134 deletions(-)
create mode 100644 app/assets/javascripts/discourse/app/lib/d-tooltip.js
create mode 100644 app/assets/javascripts/discourse/app/lib/user-status-message.js
create mode 100644 app/assets/javascripts/discourse/app/lib/user-status-on-autocomplete.js
diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index 1d46e8194d7..e4750a71d64 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -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,
});
}
diff --git a/app/assets/javascripts/discourse/app/components/d-tooltip.js b/app/assets/javascripts/discourse/app/components/d-tooltip.js
index 1e2e26ed559..c12843eeb43 100644
--- a/app/assets/javascripts/discourse/app/components/d-tooltip.js
+++ b/app/assets/javascripts/discourse/app/components/d-tooltip.js
@@ -19,6 +19,7 @@ export default class DiscourseTooltip extends Component {
}
stopPropagation(instance, event) {
+ event.preventDefault();
event.stopPropagation();
}
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs b/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs
index f432b8e1a4d..4ff6382374b 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs
+++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.hbs
@@ -52,6 +52,7 @@
}}
>
{{@content}}
+ {{@contentComponent}}
{{#if @badgeText}}
diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
index 2f7f7f6bf08..62e3fd261e7 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
+++ b/app/assets/javascripts/discourse/app/components/sidebar/user/sections.hbs
@@ -47,6 +47,10 @@
@didInsert={{link.didInsert}}
@willDestroy={{link.willDestroy}}
@content={{link.text}}
+ @contentComponent={{component
+ link.contentComponent
+ status=link.contentComponentArgs
+ }}
/>
{{/each}}
diff --git a/app/assets/javascripts/discourse/app/components/user-status-message.hbs b/app/assets/javascripts/discourse/app/components/user-status-message.hbs
index 5d3f22581f8..630c42a8bbe 100644
--- a/app/assets/javascripts/discourse/app/components/user-status-message.hbs
+++ b/app/assets/javascripts/discourse/app/components/user-status-message.hbs
@@ -1,23 +1,25 @@
-
- {{emoji @status.emoji skipTitle=true}}
- {{#if @showDescription}}
-
- {{@status.description}}
-
- {{/if}}
- {{#if this.showTooltip}}
-
-
-
- {{/if}}
-
\ No newline at end of file
+{{#if @status}}
+
+ {{emoji @status.emoji skipTitle=true}}
+ {{#if @showDescription}}
+
+ {{@status.description}}
+
+ {{/if}}
+ {{#if this.showTooltip}}
+
+
+
+ {{/if}}
+
+{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/lib/autocomplete.js b/app/assets/javascripts/discourse/app/lib/autocomplete.js
index 7744844c322..32cb925e69f 100644
--- a/app/assets/javascripts/discourse/app/lib/autocomplete.js
+++ b/app/assets/javascripts/discourse/app/lib/autocomplete.js
@@ -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], {
diff --git a/app/assets/javascripts/discourse/app/lib/d-tooltip.js b/app/assets/javascripts/discourse/app/lib/d-tooltip.js
new file mode 100644
index 00000000000..539aa50c01e
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/d-tooltip.js
@@ -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,
+ });
+}
diff --git a/app/assets/javascripts/discourse/app/lib/update-user-status-on-mention.js b/app/assets/javascripts/discourse/app/lib/update-user-status-on-mention.js
index 99227bdb7d7..eb95e52d34a 100644
--- a/app/assets/javascripts/discourse/app/lib/update-user-status-on-mention.js
+++ b/app/assets/javascripts/discourse/app/lib/update-user-status-on-mention.js
@@ -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();
}
diff --git a/app/assets/javascripts/discourse/app/lib/user-status-message.js b/app/assets/javascripts/discourse/app/lib/user-status-message.js
new file mode 100644
index 00000000000..7159b4b2c68
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/user-status-message.js
@@ -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;
+}
diff --git a/app/assets/javascripts/discourse/app/lib/user-status-on-autocomplete.js b/app/assets/javascripts/discourse/app/lib/user-status-on-autocomplete.js
new file mode 100644
index 00000000000..49cdcee1770
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/user-status-on-autocomplete.js
@@ -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 = [];
+}
diff --git a/app/assets/javascripts/discourse/app/templates/user-selector-autocomplete.hbr b/app/assets/javascripts/discourse/app/templates/user-selector-autocomplete.hbr
index 24f7a18cd36..53c1ecbf7c9 100644
--- a/app/assets/javascripts/discourse/app/templates/user-selector-autocomplete.hbr
+++ b/app/assets/javascripts/discourse/app/templates/user-selector-autocomplete.hbr
@@ -2,7 +2,7 @@
{{#each options as |item|}}
{{#if item.isUser}}
- -
+
-
{{avatar item imageSize="tiny"}}
{{format-username item.username}}
@@ -10,13 +10,7 @@
{{item.name}}
{{/if}}
{{#if item.status}}
- {{emoji item.status.emoji}}
-
- {{item.status.description}}
-
- {{#if item.status.ends_at}}
- {{format-age item.status.ends_at}}
- {{/if}}
+
{{/if}}
diff --git a/app/assets/javascripts/discourse/app/widgets/post-cooked.js b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
index 78b697a22c0..3b2accee8c7 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-cooked.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
@@ -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);
});
}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
index 2f474d946e6..20d2e646fce 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js
@@ -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) {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js
index 0913858b91d..cbfed865363 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js
@@ -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"
);
});
diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss
index bc120e625d5..99fa30ad508 100644
--- a/app/assets/stylesheets/common/base/compose.scss
+++ b/app/assets/stylesheets/common/base/compose.scss
@@ -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 {
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
index eff75513848..bbbd868c3fa 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
@@ -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,
});
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
index 84d4aadb07f..b2e6d09abac 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js
@@ -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();
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
index 21bd2528ded..e1297b9b6cf 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js
@@ -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 `${emojiUnescape(emoji, {
- title,
- })}`;
- }
-
_userStatusTitle(status) {
let title = `${escapeExpression(status.description)}`;
diff --git a/plugins/chat/spec/system/chat_channel_spec.rb b/plugins/chat/spec/system/chat_channel_spec.rb
index 87bb7ab4117..483cda9d265 100644
--- a/plugins/chat/spec/system/chat_channel_spec.rb
+++ b/plugins/chat/spec/system/chat_channel_spec.rb
@@ -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
diff --git a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
index 51dba0f0626..45da1efd4c4 100644
--- a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
+++ b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
@@ -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
diff --git a/plugins/chat/spec/system/user_status/sidebar_spec.rb b/plugins/chat/spec/system/user_status/sidebar_spec.rb
index 22b0746ab0d..7b210d04b18 100644
--- a/plugins/chat/spec/system/user_status/sidebar_spec.rb
+++ b/plugins/chat/spec/system/user_status/sidebar_spec.rb
@@ -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
diff --git a/plugins/chat/test/javascripts/acceptance/user-status-on-mentions-test.js b/plugins/chat/test/javascripts/acceptance/user-status-on-mentions-test.js
index a361f2f3f85..3560018c84b 100644
--- a/plugins/chat/test/javascripts/acceptance/user-status-on-mentions-test.js
+++ b/plugins/chat/test/javascripts/acceptance/user-status-on-mentions-test.js
@@ -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`;
}
});
diff --git a/plugins/chat/test/javascripts/components/chat-channel-test.js b/plugins/chat/test/javascripts/components/chat-channel-test.js
index b24898dd636..9bcc2d2f6e2 100644
--- a/plugins/chat/test/javascripts/components/chat-channel-test.js
+++ b/plugins/chat/test/javascripts/components/chat-channel-test.js
@@ -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`;
}
}
);