diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js
index d225239173e..b7c433ba580 100644
--- a/app/assets/javascripts/discourse/app/models/user.js
+++ b/app/assets/javascripts/discourse/app/models/user.js
@@ -1436,6 +1436,10 @@ User.reopen(Evented, {
this._subscribersCount--;
},
+ isTrackingStatus() {
+ return this._subscribersCount > 0;
+ },
+
_statusChanged(sender, key) {
this.trigger("status-changed");
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
index db659b90902..a443bd9596d 100644
--- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
+++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
@@ -39,6 +39,10 @@ export function success() {
return response({ success: true });
}
+export function OK(resp = {}, headers = {}) {
+ return [200, headers, resp];
+}
+
const loggedIn = () => !!User.current();
const helpers = { response, success, parsePostData };
diff --git a/plugins/chat/app/controllers/chat/chat_controller.rb b/plugins/chat/app/controllers/chat/chat_controller.rb
index bda98f5e541..8ef0d2e37f6 100644
--- a/plugins/chat/app/controllers/chat/chat_controller.rb
+++ b/plugins/chat/app/controllers/chat/chat_controller.rb
@@ -416,6 +416,7 @@ module Chat
.includes(:uploads)
.includes(chat_channel: :chatable)
.includes(:thread)
+ .includes(:chat_mentions)
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
diff --git a/plugins/chat/app/queries/chat/messages_query.rb b/plugins/chat/app/queries/chat/messages_query.rb
index 07acf7427f2..7db1760f88c 100644
--- a/plugins/chat/app/queries/chat/messages_query.rb
+++ b/plugins/chat/app/queries/chat/messages_query.rb
@@ -76,6 +76,7 @@ module Chat
.includes(:uploads)
.includes(chat_channel: :chatable)
.includes(:thread)
+ .includes(:chat_mentions)
.where(chat_channel_id: channel.id)
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
diff --git a/plugins/chat/app/serializers/chat/message_serializer.rb b/plugins/chat/app/serializers/chat/message_serializer.rb
index b1c13073fa3..829b5133549 100644
--- a/plugins/chat/app/serializers/chat/message_serializer.rb
+++ b/plugins/chat/app/serializers/chat/message_serializer.rb
@@ -18,13 +18,21 @@ module Chat
:thread_id,
:thread_reply_count,
:thread_title,
- :chat_channel_id
+ :chat_channel_id,
+ :mentioned_users
has_one :user, serializer: Chat::MessageUserSerializer, embed: :objects
has_one :chat_webhook_event, serializer: Chat::WebhookEventSerializer, embed: :objects
has_one :in_reply_to, serializer: Chat::InReplyToSerializer, embed: :objects
has_many :uploads, serializer: ::UploadSerializer, embed: :objects
+ def mentioned_users
+ User
+ .where(id: object.chat_mentions.pluck(:user_id))
+ .map { |user| BasicUserWithStatusSerializer.new(user, root: false) }
+ .as_json
+ end
+
def channel
@channel ||= @options.dig(:chat_channel) || object.chat_channel
end
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
index e1a25c7d292..bcd9cab45dc 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
@@ -19,6 +19,7 @@ import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
import { isEmpty, isPresent } from "@ember/utils";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import { Promise } from "rsvp";
+import User from "discourse/models/user";
export default class ChatComposer extends Component {
@service capabilities;
@@ -419,7 +420,11 @@ export default class ChatComposer extends Component {
width: "100%",
treatAsTextarea: true,
autoSelectFirstSuggestion: true,
- transformComplete: (v) => v.username || v.name,
+ transformComplete: (userData) => {
+ const user = User.create(userData);
+ this.currentMessage.mentionedUsers.set(user.id, user);
+ return user.username || user.name;
+ },
dataSource: (term) => {
return userSearch({ term, includeGroups: true }).then((result) => {
if (result?.users?.length > 0) {
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
index 44b8f34b218..10d522dd615 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs
@@ -60,6 +60,9 @@
{{else}}
{
+ if (!this.messageContainer) {
+ return;
+ }
+
+ this.args.message.mentionedUsers.forEach((user) => {
+ const href = `/u/${user.username.toLowerCase()}`;
+ const mentions = this.messageContainer.querySelectorAll(
+ `a.mention[href="${href}"]`
+ );
+
+ mentions.forEach((mention) => {
+ updateUserStatusOnMention(mention, user.status, this.currentUser);
+ });
+ });
+ });
}
@action
@@ -135,6 +162,18 @@ export default class ChatMessage extends Component {
});
}
+ @action
+ initMentionedUsers() {
+ this.args.message.mentionedUsers.forEach((user) => {
+ if (user.isTrackingStatus()) {
+ return;
+ }
+
+ user.trackStatus();
+ user.on("status-changed", this, "refreshStatusOnMentions");
+ });
+ }
+
get messageContainer() {
const id = this.args.message?.id;
if (id) {
@@ -406,4 +445,11 @@ export default class ChatMessage extends Component {
dismissMentionWarning() {
this.args.message.mentionWarning = null;
}
+
+ #teardownMentionedUsers() {
+ this.args.message.mentionedUsers.forEach((user) => {
+ user.stopTrackingStatus();
+ user.off("status-changed", this, "refreshStatusOnMentions");
+ });
+ }
}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message.js b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
index 7f92f9c5bda..f462688f679 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
@@ -88,6 +88,7 @@ export default class ChatMessage {
this.uploads = new TrackedArray(args.uploads || []);
this.user = this.#initUserModel(args.user);
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
+ this.mentionedUsers = this.#initMentionedUsers(args.mentioned_users);
}
duplicate() {
@@ -311,6 +312,17 @@ export default class ChatMessage {
return reactions.map((reaction) => ChatMessageReaction.create(reaction));
}
+ #initMentionedUsers(mentionedUsers) {
+ const map = new Map();
+ if (mentionedUsers) {
+ mentionedUsers.forEach((userData) => {
+ const user = User.create(userData);
+ map.set(user.id, user);
+ });
+ }
+ return map;
+ }
+
#initUserModel(user) {
if (!user || user instanceof User) {
return user;
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat.js b/plugins/chat/assets/javascripts/discourse/routes/chat.js
index 07f6bcdda65..a02baa97692 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat.js
@@ -66,8 +66,10 @@ export default class ChatRoute extends DiscourseRoute {
}
deactivate(transition) {
- const url = this.router.urlFor(transition.from.name);
- this.chatStateManager.storeChatURL(url);
+ if (transition) {
+ const url = this.router.urlFor(transition.from.name);
+ this.chatStateManager.storeChatURL(url);
+ }
this.chat.activeChannel = null;
this.chat.updatePresence();
diff --git a/plugins/chat/spec/components/chat/message_creator_spec.rb b/plugins/chat/spec/components/chat/message_creator_spec.rb
index 4ce81e0a837..d21ab7356de 100644
--- a/plugins/chat/spec/components/chat/message_creator_spec.rb
+++ b/plugins/chat/spec/components/chat/message_creator_spec.rb
@@ -136,6 +136,19 @@ describe Chat::MessageCreator do
expect(events.map { _1[:event_name] }).to include(:chat_message_created)
end
+ it "publishes created message to message bus" do
+ content = "a test chat message"
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.create(chat_channel: public_chat_channel, user: user1, content: content)
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["message"]).to eq(content)
+ expect(message["chat_message"]["user"]["id"]).to eq(user1.id)
+ end
+
context "with mentions" do
it "creates mentions and mention notifications for public chat" do
message =
@@ -405,6 +418,52 @@ describe Chat::MessageCreator do
mention = user2.chat_mentions.where(chat_message: message).first
expect(mention.notification).to be_nil
end
+
+ it "adds mentioned user and their status to the message bus message" do
+ SiteSetting.enable_user_status = true
+ status = { description: "dentist", emoji: "tooth" }
+ user2.set_status!(status[:description], status[:emoji])
+
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.create(
+ chat_channel: public_chat_channel,
+ user: user1,
+ content: "Hey @#{user2.username}",
+ )
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["mentioned_users"].count).to be(1)
+ mentioned_user = message["chat_message"]["mentioned_users"][0]
+
+ expect(mentioned_user["id"]).to eq(user2.id)
+ expect(mentioned_user["username"]).to eq(user2.username)
+ expect(mentioned_user["status"]).to be_present
+ expect(mentioned_user["status"].slice(:description, :emoji)).to eq(status)
+ end
+
+ it "doesn't add mentioned user's status to the message bus message when status is disabled" do
+ SiteSetting.enable_user_status = false
+ user2.set_status!("dentist", "tooth")
+
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.create(
+ chat_channel: public_chat_channel,
+ user: user1,
+ content: "Hey @#{user2.username}",
+ )
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["mentioned_users"].count).to be(1)
+ mentioned_user = message["chat_message"]["mentioned_users"][0]
+
+ expect(mentioned_user["status"]).to be_blank
+ end
end
it "creates a chat_mention record without notification when self mentioning" do
diff --git a/plugins/chat/spec/components/chat/message_updater_spec.rb b/plugins/chat/spec/components/chat/message_updater_spec.rb
index cb2682bf604..b42f067c974 100644
--- a/plugins/chat/spec/components/chat/message_updater_spec.rb
+++ b/plugins/chat/spec/components/chat/message_updater_spec.rb
@@ -124,6 +124,23 @@ describe Chat::MessageUpdater do
expect(events.map { _1[:event_name] }).to include(:chat_message_edited)
end
+ it "publishes updated message to message bus" do
+ chat_message = create_chat_message(user1, "This will be changed", public_chat_channel)
+ new_content = "New content"
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.update(
+ guardian: guardian,
+ chat_message: chat_message,
+ new_content: new_content,
+ )
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["message"]).to eq(new_content)
+ end
+
context "with mentions" do
it "sends notifications if a message was updated with new mentions" do
message = create_chat_message(user1, "Mentioning @#{user2.username}", public_chat_channel)
@@ -228,6 +245,56 @@ describe Chat::MessageUpdater do
expect(mention.notification).to be_nil
end
+ it "adds mentioned user and their status to the message bus message" do
+ SiteSetting.enable_user_status = true
+ status = { description: "dentist", emoji: "tooth" }
+ user2.set_status!(status[:description], status[:emoji])
+ chat_message = create_chat_message(user1, "This will be updated", public_chat_channel)
+ new_content = "Hey @#{user2.username}"
+
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.update(
+ guardian: guardian,
+ chat_message: chat_message,
+ new_content: new_content,
+ )
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["mentioned_users"].count).to be(1)
+ mentioned_user = message["chat_message"]["mentioned_users"][0]
+
+ expect(mentioned_user["id"]).to eq(user2.id)
+ expect(mentioned_user["username"]).to eq(user2.username)
+ expect(mentioned_user["status"]).to be_present
+ expect(mentioned_user["status"].slice(:description, :emoji)).to eq(status)
+ end
+
+ it "doesn't add mentioned user's status to the message bus message when status is disabled" do
+ SiteSetting.enable_user_status = false
+ user2.set_status!("dentist", "tooth")
+ chat_message = create_chat_message(user1, "This will be updated", public_chat_channel)
+ new_content = "Hey @#{user2.username}"
+
+ messages =
+ MessageBus.track_publish("/chat/#{public_chat_channel.id}") do
+ described_class.update(
+ guardian: guardian,
+ chat_message: chat_message,
+ new_content: new_content,
+ )
+ end
+
+ expect(messages.count).to be(1)
+ message = messages[0].data
+ expect(message["chat_message"]["mentioned_users"].count).to be(1)
+ mentioned_user = message["chat_message"]["mentioned_users"][0]
+
+ expect(mentioned_user["status"]).to be_blank
+ end
+
context "when updating a mentioned user" do
it "updates the mention record" do
chat_message = create_chat_message(user1, "ping @#{user2.username}", public_chat_channel)
diff --git a/plugins/chat/spec/requests/chat_controller_spec.rb b/plugins/chat/spec/requests/chat_controller_spec.rb
index 4959c78c361..5489b13d8f8 100644
--- a/plugins/chat/spec/requests/chat_controller_spec.rb
+++ b/plugins/chat/spec/requests/chat_controller_spec.rb
@@ -145,6 +145,60 @@ RSpec.describe Chat::ChatController do
expect(response.parsed_body["meta"]["channel_message_bus_last_id"]).not_to eq(nil)
end
+ context "with mentions" do
+ it "returns mentioned users" do
+ last_message = chat_channel.chat_messages.last
+ user1 = Fabricate(:user)
+ user2 = Fabricate(:user)
+ Fabricate(:chat_mention, user: user1, chat_message: last_message)
+ Fabricate(:chat_mention, user: user2, chat_message: last_message)
+
+ get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
+
+ mentioned_users = response.parsed_body["chat_messages"].last["mentioned_users"]
+ expect(mentioned_users[0]["id"]).to eq(user1.id)
+ expect(mentioned_users[0]["username"]).to eq(user1.username)
+ expect(mentioned_users[1]["id"]).to eq(user2.id)
+ expect(mentioned_users[1]["username"]).to eq(user2.username)
+ end
+
+ it "returns an empty list if no one was mentioned" do
+ get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
+
+ last_message = response.parsed_body["chat_messages"].last
+ expect(last_message).to have_key("mentioned_users")
+ expect(last_message["mentioned_users"]).to be_empty
+ end
+
+ context "with user status" do
+ fab!(:status) { Fabricate(:user_status) }
+ fab!(:user1) { Fabricate(:user, user_status: status) }
+ fab!(:chat_mention) do
+ Fabricate(:chat_mention, user: user1, chat_message: chat_channel.chat_messages.last)
+ end
+
+ it "returns status if enabled in settings" do
+ SiteSetting.enable_user_status = true
+
+ get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
+
+ mentioned_user = response.parsed_body["chat_messages"].last["mentioned_users"][0]
+ expect(mentioned_user).to have_key("status")
+ expect(mentioned_user["status"]["emoji"]).to eq(status.emoji)
+ expect(mentioned_user["status"]["description"]).to eq(status.description)
+ end
+
+ it "doesn't return status if disabled in settings" do
+ SiteSetting.enable_user_status = false
+
+ get "/chat/#{chat_channel.id}/messages.json", params: { page_size: page_size }
+
+ mentioned_user = response.parsed_body["chat_messages"].last["mentioned_users"][0]
+ expect(mentioned_user).not_to have_key("status")
+ end
+ end
+ end
+
describe "scrolling to the past" do
it "returns the correct messages in created_at, id order" do
get "/chat/#{chat_channel.id}/messages.json",
diff --git a/plugins/chat/spec/system/chat_channel_spec.rb b/plugins/chat/spec/system/chat_channel_spec.rb
index fdb902c4aad..33238a8eb3d 100644
--- a/plugins/chat/spec/system/chat_channel_spec.rb
+++ b/plugins/chat/spec/system/chat_channel_spec.rb
@@ -133,16 +133,18 @@ RSpec.describe "Chat channel", type: :system, js: true do
context "when a message contains mentions" do
fab!(:other_user) { Fabricate(:user) }
-
- before do
- channel_1.add(other_user)
- channel_1.add(current_user)
+ fab!(:message) do
Fabricate(
:chat_message,
chat_channel: channel_1,
message: "hello @here @all @#{current_user.username} @#{other_user.username} @unexisting",
user: other_user,
)
+ end
+
+ before do
+ channel_1.add(other_user)
+ channel_1.add(current_user)
sign_in(current_user)
end
@@ -158,6 +160,23 @@ RSpec.describe "Chat channel", type: :system, js: true do
expect(page).to have_selector(".mention", text: "@#{other_user.username}")
expect(page).to have_selector(".mention", text: "@unexisting")
end
+
+ it "renders user status on mentions" do
+ SiteSetting.enable_user_status = true
+ current_user.set_status!("off to dentist", "tooth")
+ other_user.set_status!("surfing", "surfing_man")
+ Fabricate(:chat_mention, user: current_user, chat_message: message)
+ Fabricate(:chat_mention, user: other_user, chat_message: message)
+
+ chat.visit_channel(channel_1)
+
+ expect(page).to have_selector(
+ ".mention .user-status[title='#{current_user.user_status.description}']",
+ )
+ expect(page).to have_selector(
+ ".mention .user-status[title='#{other_user.user_status.description}']",
+ )
+ end
end
context "when reply is right under" do
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
new file mode 100644
index 00000000000..619267e59e9
--- /dev/null
+++ b/plugins/chat/test/javascripts/acceptance/user-status-on-mentions-test.js
@@ -0,0 +1,351 @@
+import {
+ acceptance,
+ emulateAutocomplete,
+ loggedInUser,
+ publishToMessageBus,
+ query,
+} from "discourse/tests/helpers/qunit-helpers";
+import { test } from "qunit";
+import {
+ click,
+ triggerEvent,
+ triggerKeyEvent,
+ visit,
+ waitFor,
+} from "@ember/test-helpers";
+import pretender, { OK } from "discourse/tests/helpers/create-pretender";
+
+acceptance("Chat | User status on mentions", function (needs) {
+ const channelId = 1;
+ const messageId = 1;
+ const actingUser = {
+ id: 1,
+ username: "acting_user",
+ };
+ const mentionedUser1 = {
+ id: 1000,
+ username: "user1",
+ status: {
+ description: "surfing",
+ emoji: "surfing_man",
+ },
+ };
+ const mentionedUser2 = {
+ id: 2000,
+ username: "user2",
+ status: {
+ description: "vacation",
+ emoji: "desert_island",
+ },
+ };
+ const mentionedUser3 = {
+ id: 3000,
+ username: "user3",
+ status: {
+ description: "off to dentist",
+ emoji: "tooth",
+ },
+ };
+ const message = {
+ id: messageId,
+ message: `Hey @${mentionedUser1.username}`,
+ cooked: `
Hey @${mentionedUser1.username}
`,
+ mentioned_users: [mentionedUser1],
+ user: actingUser,
+ };
+ const newStatus = {
+ description: "working remotely",
+ emoji: "house",
+ };
+ const channel = {
+ id: channelId,
+ chatable_id: 1,
+ chatable_type: "Category",
+ meta: { message_bus_last_ids: {} },
+ current_user_membership: { following: true },
+ chatable: { id: 1 },
+ };
+
+ needs.settings({ chat_enabled: true });
+
+ needs.user({
+ ...actingUser,
+ has_chat_enabled: true,
+ chat_channels: {
+ public_channels: [channel],
+ direct_message_channels: [],
+ meta: { message_bus_last_ids: {} },
+ tracking: {},
+ },
+ });
+
+ needs.hooks.beforeEach(function () {
+ pretender.post(`/chat/1`, () => OK());
+ pretender.put(`/chat/1/edit/${messageId}`, () => OK());
+ pretender.post(`/chat/drafts`, () => OK());
+ pretender.put(`/chat/api/channels/1/read/1`, () => OK());
+ pretender.delete(`/chat/api/channels/1/messages/${messageId}`, () => OK());
+ pretender.put(`/chat/api/channels/1/messages/${messageId}/restore`, () =>
+ OK()
+ );
+
+ pretender.get(`/chat/api/channels/1`, () =>
+ OK({
+ channel,
+ chat_messages: [message],
+ meta: { can_delete_self: true },
+ })
+ );
+
+ setupAutocompleteResponses([mentionedUser2, mentionedUser3]);
+ });
+
+ test("just posted messages | it shows status on mentions ", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+ await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser2.username),
+ mentionedUser2.status
+ );
+ });
+
+ test("just posted messages | it updates status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+ await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser2.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser2.username);
+ await waitFor(selector);
+ assertStatusIsRendered(assert, selector, newStatus);
+ });
+
+ test("just posted messages | it deletes status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await typeWithAutocompleteAndSend(`mentioning @${mentionedUser2.username}`);
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser2.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser2.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ test("edited messages | it shows status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await editMessage(
+ ".chat-message-content",
+ `mentioning @${mentionedUser3.username}`
+ );
+
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser3.username),
+ mentionedUser3.status
+ );
+ });
+
+ test("edited messages | it updates status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+ await editMessage(
+ ".chat-message-content",
+ `mentioning @${mentionedUser3.username}`
+ );
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser3.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser3.username);
+ await waitFor(selector);
+ assertStatusIsRendered(assert, selector, newStatus);
+ });
+
+ test("edited messages | it deletes status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await editMessage(
+ ".chat-message-content",
+ `mentioning @${mentionedUser3.username}`
+ );
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser3.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser3.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ test("deleted messages | it shows status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await click(".chat-message-expand");
+
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser1.username),
+ mentionedUser1.status
+ );
+ });
+
+ test("deleted messages | it updates status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await click(".chat-message-expand");
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser1.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser1.username);
+ await waitFor(selector);
+ assertStatusIsRendered(assert, selector, newStatus);
+ });
+
+ test("deleted messages | it deletes status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await click(".chat-message-expand");
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser1.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser1.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ test("restored messages | it shows status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await restoreMessage(".chat-message-deleted");
+
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser1.username),
+ mentionedUser1.status
+ );
+ });
+
+ test("restored messages | it updates status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await restoreMessage(".chat-message-deleted");
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser1.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser1.username);
+ await waitFor(selector);
+ assertStatusIsRendered(assert, selector, newStatus);
+ });
+
+ test("restored messages | it deletes status on mentions", async function (assert) {
+ await visit(`/chat/c/-/${channelId}`);
+
+ await deleteMessage(".chat-message-content");
+ await restoreMessage(".chat-message-deleted");
+
+ loggedInUser().appEvents.trigger("user-status:changed", {
+ [mentionedUser1.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser1.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ function assertStatusIsRendered(assert, selector, status) {
+ assert
+ .dom(selector)
+ .exists("status is rendered")
+ .hasAttribute(
+ "title",
+ status.description,
+ "status description is updated"
+ )
+ .hasAttribute(
+ "src",
+ new RegExp(`${status.emoji}.png`),
+ "status emoji is updated"
+ );
+ }
+
+ async function deleteMessage(messageSelector) {
+ await triggerEvent(query(messageSelector), "mouseenter");
+ await click(".more-buttons .select-kit-header-wrapper");
+ await click(".select-kit-collection .select-kit-row[data-value='delete']");
+ await publishToMessageBus(`/chat/${channelId}`, {
+ type: "delete",
+ deleted_id: messageId,
+ deleted_at: "2022-01-01T08:00:00.000Z",
+ });
+ }
+
+ async function editMessage(messageSelector, text) {
+ await triggerEvent(query(messageSelector), "mouseenter");
+ await click(".more-buttons .select-kit-header-wrapper");
+ await click(".select-kit-collection .select-kit-row[data-value='edit']");
+ await typeWithAutocompleteAndSend(text);
+ }
+
+ async function restoreMessage(messageSelector) {
+ await triggerEvent(query(messageSelector), "mouseenter");
+ await click(".more-buttons .select-kit-header-wrapper");
+ await click(".select-kit-collection .select-kit-row[data-value='restore']");
+ await publishToMessageBus(`/chat/${channelId}`, {
+ type: "restore",
+ chat_message: message,
+ });
+ }
+
+ async function typeWithAutocompleteAndSend(text) {
+ await emulateAutocomplete(".chat-composer__input", text);
+ await click(".autocomplete.ac-user .selected");
+ await triggerKeyEvent(".chat-composer__input", "keydown", "Enter");
+ }
+
+ function setupAutocompleteResponses(results) {
+ pretender.get("/u/search/users", () => {
+ return [
+ 200,
+ {},
+ {
+ users: results,
+ },
+ ];
+ });
+
+ pretender.get("/chat/api/mentions/groups.json", () => {
+ return [
+ 200,
+ {},
+ {
+ unreachable: [],
+ over_members_limit: [],
+ invalid: ["and"],
+ },
+ ];
+ });
+ }
+
+ function statusSelector(username) {
+ return `.mention[href='/u/${username}'] .user-status`;
+ }
+});
diff --git a/plugins/chat/test/javascripts/components/chat-channel-test.js b/plugins/chat/test/javascripts/components/chat-channel-test.js
new file mode 100644
index 00000000000..1e066ec52f2
--- /dev/null
+++ b/plugins/chat/test/javascripts/components/chat-channel-test.js
@@ -0,0 +1,200 @@
+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 { module, test } from "qunit";
+import pretender, { OK } from "discourse/tests/helpers/create-pretender";
+import { publishToMessageBus } from "discourse/tests/helpers/qunit-helpers";
+
+module(
+ "Discourse Chat | Component | chat-channel | status on mentions",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ const channelId = 1;
+ const channel = {
+ id: channelId,
+ chatable_id: 1,
+ chatable_type: "Category",
+ meta: { message_bus_last_ids: {} },
+ current_user_membership: { following: true },
+ chatable: { id: 1 },
+ };
+ const actingUser = {
+ id: 1,
+ username: "acting_user",
+ };
+ const mentionedUser = {
+ id: 1000,
+ username: "user1",
+ status: {
+ description: "surfing",
+ emoji: "surfing_man",
+ },
+ };
+ const mentionedUser2 = {
+ id: 2000,
+ username: "user2",
+ status: {
+ description: "vacation",
+ emoji: "desert_island",
+ },
+ };
+ const message = {
+ id: 1891,
+ message: `Hey @${mentionedUser.username}`,
+ cooked: `
Hey @${mentionedUser.username}
`,
+ mentioned_users: [mentionedUser],
+ user: {
+ id: 1,
+ username: "jesse",
+ },
+ };
+
+ hooks.beforeEach(function () {
+ pretender.get(`/chat/api/channels/1`, () =>
+ OK({
+ channel,
+ chat_messages: [message],
+ meta: { can_delete_self: true },
+ })
+ );
+
+ this.channel = fabricators.channel({
+ id: channelId,
+ currentUserMembership: { following: true },
+ meta: { can_join_chat_channel: false },
+ });
+ this.appEvents = this.container.lookup("service:appEvents");
+ });
+
+ test("it shows status on mentions", async function (assert) {
+ await render(hbs`
`);
+
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser.username),
+ mentionedUser.status
+ );
+ });
+
+ test("it updates status on mentions", async function (assert) {
+ await render(hbs`
`);
+
+ const newStatus = {
+ description: "off to dentist",
+ emoji: "tooth",
+ };
+
+ this.appEvents.trigger("user-status:changed", {
+ [mentionedUser.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser.username);
+ await waitFor(selector);
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser.username),
+ newStatus
+ );
+ });
+
+ test("it deletes status on mentions", async function (assert) {
+ await render(hbs`
`);
+
+ this.appEvents.trigger("user-status:changed", {
+ [mentionedUser.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ test("it shows status on mentions on messages that came from Message Bus", async function (assert) {
+ await render(hbs`
`);
+
+ await receiveChatMessageViaMessageBus();
+
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser2.username),
+ mentionedUser2.status
+ );
+ });
+
+ test("it updates status on mentions on messages that came from Message Bus", async function (assert) {
+ await render(hbs`
`);
+ await receiveChatMessageViaMessageBus();
+
+ const newStatus = {
+ description: "off to meeting",
+ emoji: "calendar",
+ };
+ this.appEvents.trigger("user-status:changed", {
+ [mentionedUser2.id]: newStatus,
+ });
+
+ const selector = statusSelector(mentionedUser2.username);
+ await waitFor(selector);
+ assertStatusIsRendered(
+ assert,
+ statusSelector(mentionedUser2.username),
+ newStatus
+ );
+ });
+
+ test("it deletes status on mentions on messages that came from Message Bus", async function (assert) {
+ await render(hbs`
`);
+ await receiveChatMessageViaMessageBus();
+
+ this.appEvents.trigger("user-status:changed", {
+ [mentionedUser2.id]: null,
+ });
+
+ const selector = statusSelector(mentionedUser2.username);
+ await waitFor(selector, { count: 0 });
+ assert.dom(selector).doesNotExist("status is deleted");
+ });
+
+ function assertStatusIsRendered(assert, selector, status) {
+ assert
+ .dom(selector)
+ .exists("status is rendered")
+ .hasAttribute(
+ "title",
+ status.description,
+ "status description is updated"
+ )
+ .hasAttribute(
+ "src",
+ new RegExp(`${status.emoji}.png`),
+ "status emoji is updated"
+ );
+ }
+
+ async function receiveChatMessageViaMessageBus() {
+ await publishToMessageBus(`/chat/${channelId}`, {
+ chat_message: {
+ id: 2138,
+ message: `Hey @${mentionedUser2.username}`,
+ cooked: `
Hey @${mentionedUser2.username}
`,
+ created_at: "2023-05-18T16:07:59.588Z",
+ excerpt: `Hey @${mentionedUser2.username}`,
+ available_flags: [],
+ thread_title: null,
+ chat_channel_id: 7,
+ mentioned_users: [mentionedUser2],
+ user: actingUser,
+ chat_webhook_event: null,
+ uploads: [],
+ },
+ type: "sent",
+ });
+ }
+
+ function statusSelector(username) {
+ return `.mention[href='/u/${username}'] .user-status`;
+ }
+ }
+);