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}} - -
- {{emoji @status.emoji skipTitle=true}} - - {{@status.description}} - - {{#if this.until}} -
- {{this.until}} -
- {{/if}} -
-
- {{/if}} -
\ No newline at end of file +{{#if @status}} + + {{emoji @status.emoji skipTitle=true}} + {{#if @showDescription}} + + {{@status.description}} + + {{/if}} + {{#if this.showTooltip}} + +
+ {{emoji @status.emoji skipTitle=true}} + + {{@status.description}} + + {{#if this.until}} +
+ {{this.until}} +
+ {{/if}} +
+
+ {{/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 @@