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