diff --git a/plugins/chat/app/controllers/chat/api/channel_threads_controller.rb b/plugins/chat/app/controllers/chat/api/channel_threads_controller.rb index 97a707b7281..0d7669c5e92 100644 --- a/plugins/chat/app/controllers/chat/api/channel_threads_controller.rb +++ b/plugins/chat/app/controllers/chat/api/channel_threads_controller.rb @@ -32,6 +32,8 @@ class Chat::Api::ChannelThreadsController < Chat::ApiController ::Chat::ThreadSerializer, root: "thread", membership: result.membership, + include_preview: true, + participants: result.participants, ) end on_failed_policy(:threaded_discussions_enabled) { raise Discourse::NotFound } diff --git a/plugins/chat/app/jobs/regular/chat/update_thread_reply_count.rb b/plugins/chat/app/jobs/regular/chat/update_thread_reply_count.rb index d367e489846..4173c6ecc10 100644 --- a/plugins/chat/app/jobs/regular/chat/update_thread_reply_count.rb +++ b/plugins/chat/app/jobs/regular/chat/update_thread_reply_count.rb @@ -16,8 +16,6 @@ module Jobs Time.zone.now.to_i, ) thread.set_replies_count_cache(thread.replies.count, update_db: true) - - ::Chat::Publisher.publish_thread_original_message_metadata!(thread) end end end diff --git a/plugins/chat/app/models/chat/thread.rb b/plugins/chat/app/models/chat/thread.rb index 6c9b9b04ac6..0012c427a5c 100644 --- a/plugins/chat/app/models/chat/thread.rb +++ b/plugins/chat/app/models/chat/thread.rb @@ -42,7 +42,7 @@ module Chat end def replies - self.chat_messages.where.not(id: self.original_message_id) + self.chat_messages.where.not(id: self.original_message_id).order("created_at ASC, id ASC") end def url diff --git a/plugins/chat/app/models/chat/view.rb b/plugins/chat/app/models/chat/view.rb index a32f946e855..3b69bdf87a6 100644 --- a/plugins/chat/app/models/chat/view.rb +++ b/plugins/chat/app/models/chat/view.rb @@ -10,7 +10,8 @@ module Chat :unread_thread_ids, :threads, :tracking, - :thread_memberships + :thread_memberships, + :thread_participants def initialize( chat_channel:, @@ -21,7 +22,8 @@ module Chat unread_thread_ids: nil, threads: nil, tracking: nil, - thread_memberships: nil + thread_memberships: nil, + thread_participants: nil ) @chat_channel = chat_channel @chat_messages = chat_messages @@ -32,6 +34,7 @@ module Chat @threads = threads @tracking = tracking @thread_memberships = thread_memberships + @thread_participants = thread_participants end def reviewable_ids diff --git a/plugins/chat/app/models/concerns/chat/thread_cache.rb b/plugins/chat/app/models/concerns/chat/thread_cache.rb index a87a1c7666d..97fbad5a4da 100644 --- a/plugins/chat/app/models/concerns/chat/thread_cache.rb +++ b/plugins/chat/app/models/concerns/chat/thread_cache.rb @@ -76,7 +76,6 @@ module Chat def thread_reply_count_cache_changed Jobs.enqueue_in(5.seconds, Jobs::Chat::UpdateThreadReplyCount, thread_id: self.id) - ::Chat::Publisher.publish_thread_original_message_metadata!(self) end end end diff --git a/plugins/chat/app/queries/chat/thread_participant_query.rb b/plugins/chat/app/queries/chat/thread_participant_query.rb new file mode 100644 index 00000000000..7c1d7d0488a --- /dev/null +++ b/plugins/chat/app/queries/chat/thread_participant_query.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Chat + # Builds a query to find the total count of participants for one + # or more threads (on a per-thread basis), as well as up to 3 + # participants in the thread. The participants will be made up + # of: + # + # - Participant 1 & 2 - The most frequent participants in the thread. + # - Participant 3 - The most recent participant in the thread. + # + # This result should be cached to avoid unnecessary queries, + # since the participants will not often change for a thread, + # and if there is a delay in updating them based on message + # count it is not a big deal. + class ThreadParticipantQuery + # @param thread_ids [Array] The IDs of the threads to query. + # @return [Hash] A hash of thread IDs to participant data. + def self.call(thread_ids:) + return {} if thread_ids.blank? + + # We only want enough data for BasicUserSerializer, since the participants + # are just showing username & avatar. + thread_participant_stats = DB.query(<<~SQL, thread_ids: thread_ids) + SELECT thread_participant_stats.*, users.username, users.name, users.uploaded_avatar_id FROM ( + SELECT chat_messages.thread_id, chat_messages.user_id, COUNT(*) AS message_count, + ROW_NUMBER() OVER (PARTITION BY chat_messages.thread_id ORDER BY COUNT(*) DESC) AS row_number + FROM chat_messages + INNER JOIN chat_threads ON chat_threads.id = chat_messages.thread_id + INNER JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id + AND user_chat_thread_memberships.user_id = chat_messages.user_id + WHERE chat_messages.thread_id IN (:thread_ids) + AND chat_messages.deleted_at IS NULL + GROUP BY chat_messages.thread_id, chat_messages.user_id + ) AS thread_participant_stats + INNER JOIN users ON users.id = thread_participant_stats.user_id + ORDER BY thread_participant_stats.thread_id ASC, thread_participant_stats.message_count DESC, thread_participant_stats.user_id ASC + SQL + + most_recent_participants = DB.query(<<~SQL, thread_ids: thread_ids) + SELECT DISTINCT ON (thread_id) chat_messages.thread_id, chat_messages.user_id, + users.username, users.name, users.uploaded_avatar_id + FROM chat_messages + INNER JOIN chat_threads ON chat_threads.id = chat_messages.thread_id + INNER JOIN user_chat_thread_memberships ON user_chat_thread_memberships.thread_id = chat_threads.id + AND user_chat_thread_memberships.user_id = chat_messages.user_id + INNER JOIN users ON users.id = chat_messages.user_id + WHERE chat_messages.thread_id IN (:thread_ids) + AND chat_messages.deleted_at IS NULL + ORDER BY chat_messages.thread_id ASC, chat_messages.created_at DESC + SQL + most_recent_participants = + most_recent_participants.reduce({}) do |hash, mrm| + hash[mrm.thread_id] = { + id: mrm.user_id, + username: mrm.username, + name: mrm.name, + uploaded_avatar_id: mrm.uploaded_avatar_id, + } + hash + end + + thread_participants = {} + thread_participant_stats.each do |thread_participant_stat| + thread_id = thread_participant_stat.thread_id + thread_participants[thread_id] ||= {} + thread_participants[thread_id][:users] ||= [] + thread_participants[thread_id][:total_count] ||= 0 + + # If we want to return more of the top N users in the thread we + # can just increase the number here. + if thread_participants[thread_id][:users].length < 2 && + thread_participant_stat.user_id != most_recent_participants[thread_id][:id] + thread_participants[thread_id][:users].push( + { + id: thread_participant_stat.user_id, + username: thread_participant_stat.username, + name: thread_participant_stat.name, + uploaded_avatar_id: thread_participant_stat.uploaded_avatar_id, + }, + ) + end + + thread_participants[thread_id][:total_count] += 1 + end + + # Always put the most recent participant at the end of the array. + most_recent_participants.each do |thread_id, user| + thread_participants[thread_id][:users].push(user) + end + + thread_participants + end + end +end diff --git a/plugins/chat/app/serializers/chat/message_serializer.rb b/plugins/chat/app/serializers/chat/message_serializer.rb index eb277054e65..0d9b5d544df 100644 --- a/plugins/chat/app/serializers/chat/message_serializer.rb +++ b/plugins/chat/app/serializers/chat/message_serializer.rb @@ -16,8 +16,6 @@ module Chat :bookmark, :available_flags, :thread_id, - :thread_reply_count, - :thread_title, :chat_channel_id, :mentioned_users @@ -172,17 +170,5 @@ module Chat def include_thread_id? include_threading_data? end - - def include_thread_reply_count? - include_threading_data? && object.thread_id.present? - end - - def thread_reply_count - object.thread&.replies_count_cache || 0 - end - - def thread_title - object.thread&.title - end end end diff --git a/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb b/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb index f484bada942..8500fba6bc6 100644 --- a/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb +++ b/plugins/chat/app/serializers/chat/thread_original_message_serializer.rb @@ -2,6 +2,8 @@ module Chat class ThreadOriginalMessageSerializer < Chat::MessageSerializer + has_one :user, serializer: BasicUserWithStatusSerializer, embed: :objects + def excerpt object.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH) end diff --git a/plugins/chat/app/serializers/chat/thread_preview_serializer.rb b/plugins/chat/app/serializers/chat/thread_preview_serializer.rb index 0b4bac0cfb7..dbf132954e8 100644 --- a/plugins/chat/app/serializers/chat/thread_preview_serializer.rb +++ b/plugins/chat/app/serializers/chat/thread_preview_serializer.rb @@ -2,7 +2,22 @@ module Chat class ThreadPreviewSerializer < ApplicationSerializer - attributes :last_reply_created_at, :last_reply_excerpt, :last_reply_id + attributes :last_reply_created_at, + :last_reply_excerpt, + :last_reply_id, + :participant_count, + :reply_count + has_many :participant_users, serializer: BasicUserSerializer, embed: :objects + has_one :last_reply_user, serializer: BasicUserSerializer, embed: :objects + + def initialize(object, opts) + super(object, opts) + @participants = opts[:participants] + end + + def reply_count + object.replies_count_cache || 0 + end def last_reply_created_at object.last_reply.created_at @@ -13,7 +28,31 @@ module Chat end def last_reply_excerpt - object.last_reply.censored_excerpt + object.last_reply.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH) + end + + def last_reply_user + object.last_reply.user + end + + def include_participant_data? + @participants.present? + end + + def include_participant_users? + include_participant_data? + end + + def include_participant_count? + include_participant_data? + end + + def participant_users + @participant_users ||= @participants[:users].map { |user| User.new(user) } + end + + def participant_count + @participants[:total_count] end end end diff --git a/plugins/chat/app/serializers/chat/thread_serializer.rb b/plugins/chat/app/serializers/chat/thread_serializer.rb index 66c73d315c2..54a98d6b955 100644 --- a/plugins/chat/app/serializers/chat/thread_serializer.rb +++ b/plugins/chat/app/serializers/chat/thread_serializer.rb @@ -2,7 +2,6 @@ module Chat class ThreadSerializer < ApplicationSerializer - has_one :original_message_user, serializer: BasicUserWithStatusSerializer, embed: :objects has_one :original_message, serializer: Chat::ThreadOriginalMessageSerializer, embed: :objects attributes :id, @@ -36,7 +35,12 @@ module Chat end def preview - Chat::ThreadPreviewSerializer.new(object, scope: scope, root: false).as_json + Chat::ThreadPreviewSerializer.new( + object, + scope: scope, + root: false, + participants: @opts[:participants], + ).as_json end def include_current_user_membership? diff --git a/plugins/chat/app/serializers/chat/view_serializer.rb b/plugins/chat/app/serializers/chat/view_serializer.rb index 1d2a5842718..aa4277957b7 100644 --- a/plugins/chat/app/serializers/chat/view_serializer.rb +++ b/plugins/chat/app/serializers/chat/view_serializer.rb @@ -12,6 +12,8 @@ module Chat thread, scope: scope, membership: object.thread_memberships.find { |m| m.thread_id == thread.id }, + participants: object.thread_participants[thread.id], + include_preview: true, root: nil, ) end diff --git a/plugins/chat/app/services/chat/channel_view_builder.rb b/plugins/chat/app/services/chat/channel_view_builder.rb index ce15a4e905f..ea6e5b65b34 100644 --- a/plugins/chat/app/services/chat/channel_view_builder.rb +++ b/plugins/chat/app/services/chat/channel_view_builder.rb @@ -38,6 +38,7 @@ module Chat step :fetch_threads_for_messages step :fetch_tracking step :fetch_thread_memberships + step :fetch_thread_participants step :build_view class Contract @@ -218,6 +219,11 @@ module Chat end end + def fetch_thread_participants(threads:, **) + context.thread_participants = + ::Chat::ThreadParticipantQuery.call(thread_ids: threads.map(&:id)) + end + def build_view( guardian:, channel:, @@ -228,6 +234,7 @@ module Chat can_load_more_past:, can_load_more_future:, thread_memberships:, + thread_participants:, ** ) context.view = @@ -241,6 +248,7 @@ module Chat threads: threads, tracking: tracking, thread_memberships: thread_memberships, + thread_participants: thread_participants, ) end end diff --git a/plugins/chat/app/services/chat/lookup_channel_threads.rb b/plugins/chat/app/services/chat/lookup_channel_threads.rb index 45b9a75f242..0b73bf65d16 100644 --- a/plugins/chat/app/services/chat/lookup_channel_threads.rb +++ b/plugins/chat/app/services/chat/lookup_channel_threads.rb @@ -55,7 +55,7 @@ module Chat .strict_loading .includes( :channel, - last_reply: [:uploads], + last_reply: %i[user uploads], original_message_user: :user_status, original_message: [ :chat_webhook_event, diff --git a/plugins/chat/app/services/chat/lookup_thread.rb b/plugins/chat/app/services/chat/lookup_thread.rb index c04d5e62ddc..c0771297964 100644 --- a/plugins/chat/app/services/chat/lookup_thread.rb +++ b/plugins/chat/app/services/chat/lookup_thread.rb @@ -25,6 +25,7 @@ module Chat policy :invalid_access policy :threading_enabled_for_channel step :fetch_membership + step :fetch_participants # @!visibility private class Contract @@ -43,6 +44,7 @@ module Chat def fetch_thread(contract:, **) Chat::Thread.includes( :channel, + last_reply: :user, original_message_user: :user_status, original_message: :chat_webhook_event, ).find_by(id: contract.thread_id, channel_id: contract.channel_id) @@ -59,5 +61,9 @@ module Chat def fetch_membership(thread:, guardian:, **) context.membership = thread.membership_for(guardian.user) end + + def fetch_participants(thread:, **) + context.participants = ::Chat::ThreadParticipantQuery.call(thread_ids: [thread.id])[thread.id] + end end end diff --git a/plugins/chat/app/services/chat/publisher.rb b/plugins/chat/app/services/chat/publisher.rb index ff2456ed496..13285dd3e09 100644 --- a/plugins/chat/app/services/chat/publisher.rb +++ b/plugins/chat/app/services/chat/publisher.rb @@ -72,23 +72,27 @@ module Chat user_id: chat_message.user.id, username: chat_message.user.username, thread_id: chat_message.thread_id, - created_at: chat_message.created_at, - excerpt: - chat_message.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH), }, permissions(chat_channel), ) + + publish_thread_original_message_metadata!(chat_message.thread) end end def self.publish_thread_original_message_metadata!(thread) + preview = + ::Chat::ThreadPreviewSerializer.new( + thread, + participants: ::Chat::ThreadParticipantQuery.call(thread_ids: [thread.id])[thread.id], + root: false, + ).as_json publish_to_channel!( thread.channel, { type: :update_thread_original_message, original_message_id: thread.original_message_id, - replies_count: thread.replies_count_cache, - title: thread.title, + preview: preview.as_json, }, ) end diff --git a/plugins/chat/app/services/chat/restore_message.rb b/plugins/chat/app/services/chat/restore_message.rb index fa1a6b173b8..210a9ee817a 100644 --- a/plugins/chat/app/services/chat/restore_message.rb +++ b/plugins/chat/app/services/chat/restore_message.rb @@ -58,6 +58,10 @@ module Chat def publish_events(guardian:, message:, **) DiscourseEvent.trigger(:chat_message_restored, message, message.chat_channel, guardian.user) Chat::Publisher.publish_restore!(message.chat_channel, message) + + if message.thread.present? + Chat::Publisher.publish_thread_original_message_metadata!(message.thread) + end end end end diff --git a/plugins/chat/app/services/chat/trash_message.rb b/plugins/chat/app/services/chat/trash_message.rb index 23ba5f0b212..cbfecdcc037 100644 --- a/plugins/chat/app/services/chat/trash_message.rb +++ b/plugins/chat/app/services/chat/trash_message.rb @@ -73,6 +73,10 @@ module Chat def publish_events(guardian:, message:, **) DiscourseEvent.trigger(:chat_message_trashed, message, message.chat_channel, guardian.user) Chat::Publisher.publish_delete!(message.chat_channel, message) + + if message.thread.present? + Chat::Publisher.publish_thread_original_message_metadata!(message.thread) + end end end end diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel.js index a1563581d72..eedf4da63e2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel.js @@ -1,6 +1,5 @@ import { capitalize } from "@ember/string"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; -import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread"; import Component from "@glimmer/component"; import { bind, debounce } from "discourse-common/utils/decorators"; import { action } from "@ember/object"; @@ -207,7 +206,18 @@ export default class ChatLivePane extends Component { if (result.threads) { result.threads.forEach((thread) => { - this.args.channel.threadsManager.store(this.args.channel, thread); + const storedThread = this.args.channel.threadsManager.store( + this.args.channel, + thread, + { replace: true } + ); + const originalMessage = messages.findBy( + "id", + storedThread.originalMessage.id + ); + if (originalMessage) { + originalMessage.thread = storedThread; + } }); } @@ -297,7 +307,18 @@ export default class ChatLivePane extends Component { if (result.threads) { result.threads.forEach((thread) => { - this.args.channel.threadsManager.store(this.args.channel, thread); + const storedThread = this.args.channel.threadsManager.store( + this.args.channel, + thread, + { replace: true } + ); + const originalMessage = messages.findBy( + "id", + storedThread.originalMessage.id + ); + if (originalMessage) { + originalMessage.thread = storedThread; + } }); } @@ -403,13 +424,6 @@ export default class ChatLivePane extends Component { } const message = ChatMessage.create(channel, messageData); - - if (messageData.thread_id) { - message.thread = ChatThread.create(channel, { - id: messageData.thread_id, - }); - } - messages.push(message); }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.hbs index 2e4607e1a77..9a291ad8572 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.hbs @@ -3,11 +3,50 @@ @models={{@message.thread.routeModels}} class="chat-message-thread-indicator" > - - {{i18n "chat.thread.replies" count=@message.threadReplyCount}} - - - {{i18n "chat.thread.view_thread"}}{{#if this.threadTitle}}: - {{replace-emoji this.threadTitle}}{{/if}} - + {{#unless this.chatStateManager.isDrawerActive}} +
+ +
+ {{/unless}} +
+ +
+ {{replace-emoji (html-safe @message.thread.preview.lastReplyExcerpt)}} +
+
+ +
+ +
\ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.js index d2823521ab2..5e7129b9d0c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-thread-indicator.js @@ -1,7 +1,11 @@ import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; import { escapeExpression } from "discourse/lib/utilities"; export default class ChatMessageThreadIndicator extends Component { + @service site; + @service chatStateManager; + get threadTitle() { return escapeExpression(this.args.message.threadTitle); } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js index c4d60ecbbbc..2f0556a3ccb 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js @@ -356,7 +356,7 @@ export default class ChatMessage extends Component { this.args.context !== MESSAGE_CONTEXT_THREAD && this.threadingEnabled && this.args.message?.thread && - this.args.message?.threadReplyCount > 0 + this.args.message?.thread.preview.replyCount > 0 ); } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-user-avatar.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-user-avatar.hbs index 3cad164465b..adbcb644d84 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-user-avatar.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-user-avatar.hbs @@ -1,4 +1,7 @@ -
+
+
+ {{#each @thread.preview.participantUsers as |user|}} + + {{/each}} +
+ {{#if @thread.preview.otherParticipantCount}} +
+ {{i18n + "chat.thread.participants_other_count" + count=@thread.preview.otherParticipantCount + }} +
+ {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-message.js index b5ec5c39662..0d471a6fa2e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/styleguide/chat-message.js @@ -54,12 +54,11 @@ export default class ChatStyleguideChatMessage extends Component { if (this.message.thread) { this.message.channel.threadingEnabled = false; this.message.thread = null; - this.message.threadReplyCount = 0; } else { this.message.thread = fabricators.thread({ channel: this.message.channel, }); - this.message.threadReplyCount = 1; + this.message.thread.preview.replyCount = 1; this.message.channel.threadingEnabled = true; } } diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message.js b/plugins/chat/assets/javascripts/discourse/models/chat-message.js index c96f095a6ae..daf08ec59d6 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-message.js @@ -51,7 +51,6 @@ export default class ChatMessage { @tracked firstOfResults; @tracked message; @tracked thread; - @tracked threadReplyCount; @tracked manager; @tracked threadTitle; @@ -68,8 +67,6 @@ export default class ChatMessage { this.editing = args.editing || false; this.availableFlags = args.availableFlags || args.available_flags; this.hidden = args.hidden || false; - this.threadReplyCount = args.threadReplyCount || args.thread_reply_count; - this.threadTitle = args.threadTitle || args.thread_title; this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event; this.createdAt = args.createdAt || args.created_at; this.deletedAt = args.deletedAt || args.deleted_at; @@ -104,7 +101,6 @@ export default class ChatMessage { edited: this.edited, availableFlags: this.availableFlags, hidden: this.hidden, - threadReplyCount: this.threadReplyCount, chatWebhookEvent: this.chatWebhookEvent, createdAt: this.createdAt, deletedAt: this.deletedAt, diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread-preview.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread-preview.js index e790c156480..c74ce8059a2 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-thread-preview.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread-preview.js @@ -1,18 +1,38 @@ import { tracked } from "@glimmer/tracking"; +import { TrackedArray } from "@ember-compat/tracked-built-ins"; export default class ChatThreadPreview { static create(args = {}) { return new ChatThreadPreview(args); } + @tracked replyCount; @tracked lastReplyId; @tracked lastReplyCreatedAt; @tracked lastReplyExcerpt; + @tracked lastReplyUser; + @tracked participantCount; + @tracked participantUsers; constructor(args = {}) { + if (!args) { + args = {}; + } + + this.replyCount = args.reply_count || args.replyCount || 0; this.lastReplyId = args.last_reply_id || args.lastReplyId; this.lastReplyCreatedAt = args.last_reply_created_at || args.lastReplyCreatedAt; this.lastReplyExcerpt = args.last_reply_excerpt || args.lastReplyExcerpt; + this.lastReplyUser = args.last_reply_user || args.lastReplyUser; + this.participantCount = + args.participant_count || args.participantCount || 0; + this.participantUsers = new TrackedArray( + args.participant_users || args.participantUsers || [] + ); + } + + get otherParticipantCount() { + return this.participantCount - this.participantUsers.length; } } diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js index ef6a0dfbae4..614ee06e727 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js @@ -58,9 +58,7 @@ export default class ChatThread { } this.tracking = new ChatTrackingState(getOwner(this)); - if (args.preview) { - this.preview = ChatThreadPreview.create(args.preview); - } + this.preview = ChatThreadPreview.create(args.preview); } async stageMessage(message) { diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js index cf8e85f336f..a0acf760275 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane-subscriptions-manager.js @@ -1,5 +1,6 @@ import { inject as service } from "@ember/service"; import ChatPaneBaseSubscriptionsManager from "./chat-pane-base-subscriptions-manager"; +import ChatThreadPreview from "../models/chat-thread-preview"; export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager { @service chat; @@ -23,10 +24,7 @@ export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSub handleThreadOriginalMessageUpdate(data) { const message = this.messagesManager.findMessage(data.original_message_id); if (message) { - if (data.replies_count) { - message.threadReplyCount = data.replies_count; - } - message.threadTitle = data.title; + message.thread.preview = ChatThreadPreview.create(data.preview); } } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js index b0a9e8274ff..970dc9cb009 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-pane-base-subscriptions-manager.js @@ -230,7 +230,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service { stagedThread.staged = false; stagedThread.id = data.thread_id; stagedThread.originalMessage.thread = stagedThread; - stagedThread.originalMessage.threadReplyCount ??= 1; + stagedThread.originalMessage.thread.preview.replyCount ??= 1; } else if (data.thread_id) { this.model.threadsManager .find(this.model.id, data.thread_id, { fetchIfNotFound: true }) diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js index b0ed9f1b264..7c45dad6788 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js @@ -3,7 +3,6 @@ import I18n from "I18n"; import { bind } from "discourse-common/utils/decorators"; import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatChannelArchive from "../models/chat-channel-archive"; -import ChatThreadPreview from "../models/chat-thread-preview"; export default class ChatSubscriptionsManager extends Service { @service store; @@ -224,12 +223,6 @@ export default class ChatSubscriptionsManager extends Service { channel.threadsManager .find(busData.channel_id, busData.thread_id) .then((thread) => { - thread.preview = ChatThreadPreview.create({ - lastReplyId: busData.message_id, - lastReplyExcerpt: busData.excerpt, - lastReplyCreatedAt: busData.created_at, - }); - if (busData.user_id === this.currentUser.id) { // Thread should no longer be considered unread. if (thread.currentUserMembership) { diff --git a/plugins/chat/assets/stylesheets/common/chat-message-thread-indicator.scss b/plugins/chat/assets/stylesheets/common/chat-message-thread-indicator.scss index eb4d4d7ab39..eddb3b60183 100644 --- a/plugins/chat/assets/stylesheets/common/chat-message-thread-indicator.scss +++ b/plugins/chat/assets/stylesheets/common/chat-message-thread-indicator.scss @@ -1,36 +1,67 @@ .chat-message-thread-indicator { - align-items: center; - display: grid; - grid: 1fr / auto-flow; display: flex; + align-items: center; + gap: 1rem; cursor: pointer; grid-area: threadindicator; + max-width: 1000px; background-color: var(--primary-very-low); margin: 4px 0 -2px calc(var(--message-left-width) - 0.25rem); - padding-block: 0.25rem; + padding-block: 0.5rem; padding-inline: 0.5rem; - max-width: 500px; - border-radius: 4px; + border-radius: 8px; + color: var(--primary); - &__replies-count { + &:visited, + &:hover { + color: var(--primary); + } + + &:hover { + box-shadow: var(--shadow-dropdown-lite); + } + + &__participants { + flex-shrink: 0; + align-self: flex-start; + } + + &__last-reply-avatar { + align-self: flex-start; + .chat-user-avatar { + width: auto !important; + } + } + + &__last-reply-metadata { + display: flex; + align-items: center; + gap: 0.25rem; color: var(--primary-medium); font-size: var(--font-down-1); } - &__view-thread { - font-size: var(--font-down-1); - flex: 1; - - .chat-message-thread-indicator:hover & { - text-decoration: underline; - } + &__last-reply-label { + margin-right: 0.25rem; } - &__replies-count + &__view-thread { - padding-left: 0.25rem; + &__last-reply-username { + font-weight: bold; + color: var(--secondary-low); + font-size: var(--font-up-1); } - &__separator { - margin: 0 0.5em; + &__last-reply-excerpt { + @include ellipsis; + } + + &__body { + overflow: hidden; + white-space: nowrap; + flex-shrink: 1; + } + + &__replies-count { + color: var(--tertiary); } } diff --git a/plugins/chat/assets/stylesheets/common/chat-message.scss b/plugins/chat/assets/stylesheets/common/chat-message.scss index cc181ba877c..22b21422e6e 100644 --- a/plugins/chat/assets/stylesheets/common/chat-message.scss +++ b/plugins/chat/assets/stylesheets/common/chat-message.scss @@ -64,11 +64,11 @@ &.is-threaded { display: grid; grid-template-columns: var(--message-left-width) 1fr; - grid-template-rows: auto 32px; + grid-template-rows: auto; grid-template-areas: "avatar message" "threadindicator threadindicator"; - + padding: 0.65rem 1rem !important; .chat-user-avatar { grid-area: avatar; } diff --git a/plugins/chat/assets/stylesheets/common/chat-thread-participants.scss b/plugins/chat/assets/stylesheets/common/chat-thread-participants.scss new file mode 100644 index 00000000000..56a3127ccc2 --- /dev/null +++ b/plugins/chat/assets/stylesheets/common/chat-thread-participants.scss @@ -0,0 +1,22 @@ +.chat-thread-participants { + &__other-count { + font-size: var(--font-down-2); + text-align: right; + color: var(--primary-medium); + } + + &__avatar-group { + display: flex; + justify-content: flex-end; + + .chat-user-avatar { + width: auto !important; + + .avatar { + width: 24px; + height: 24px; + padding: 0; + } + } + } +} diff --git a/plugins/chat/assets/stylesheets/common/index.scss b/plugins/chat/assets/stylesheets/common/index.scss index 570cd2fb48d..1873d414de9 100644 --- a/plugins/chat/assets/stylesheets/common/index.scss +++ b/plugins/chat/assets/stylesheets/common/index.scss @@ -56,4 +56,5 @@ @import "chat-thread-header"; @import "chat-thread-list-header"; @import "chat-thread-unread-indicator"; +@import "chat-thread-participants"; @import "channel-summary-modal"; diff --git a/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss b/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss new file mode 100644 index 00000000000..1f469cc46a9 --- /dev/null +++ b/plugins/chat/assets/stylesheets/desktop/chat-message-thread-indicator.scss @@ -0,0 +1,61 @@ +.chat-message-thread-indicator { + width: min-content; + min-width: 400px; + max-width: 600px; + + .chat-drawer & { + align-items: stretch; + flex-wrap: wrap; + width: auto; + max-width: 100%; + min-width: auto; + gap: 0.25rem; + } + + &__last-reply-avatar { + .chat-drawer & { + display: inline-block; + } + } + + &__last-reply-user { + margin-right: 0.25rem; + + .chat-drawer & { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: auto; + } + } + + &__last-reply-metadata { + .chat-drawer & { + flex-wrap: wrap; + } + } + + &__last-reply-excerpt { + .chat-drawer & { + white-space: wrap; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + margin-left: calc(26px + 0.5rem); + } + } + + &__body { + .chat-drawer & { + flex-grow: 1; + } + } + &__participants { + margin-left: auto; + + .chat-drawer & { + align-self: flex-end; + flex-basis: 100%; + } + } +} diff --git a/plugins/chat/assets/stylesheets/desktop/index.scss b/plugins/chat/assets/stylesheets/desktop/index.scss index 03d6fb66517..53952e3807f 100644 --- a/plugins/chat/assets/stylesheets/desktop/index.scss +++ b/plugins/chat/assets/stylesheets/desktop/index.scss @@ -5,4 +5,5 @@ @import "chat-index-full-page"; @import "chat-message-actions"; @import "chat-message"; +@import "chat-message-thread-indicator"; @import "sidebar-extensions"; diff --git a/plugins/chat/assets/stylesheets/mobile/chat-message-thread-indicator.scss b/plugins/chat/assets/stylesheets/mobile/chat-message-thread-indicator.scss new file mode 100644 index 00000000000..e65ee62505d --- /dev/null +++ b/plugins/chat/assets/stylesheets/mobile/chat-message-thread-indicator.scss @@ -0,0 +1,39 @@ +.chat-message-thread-indicator { + &__participants, + &__last-reply-avatar:not(.-mobile) { + display: none; + } + + &__last-reply-metadata { + display: flex; + align-items: center; + gap: 0.25rem; + margin-bottom: 0.25rem; + } + + &__last-reply-user { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: auto; + } + + &__last-reply-avatar { + align-self: center; + .avatar { + width: 18px; + height: 18px; + } + } + + &__last-reply-username { + margin-right: auto; + } + + &__last-reply-excerpt { + white-space: wrap; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } +} diff --git a/plugins/chat/assets/stylesheets/mobile/index.scss b/plugins/chat/assets/stylesheets/mobile/index.scss index 9d15ac78433..e2ff4030a40 100644 --- a/plugins/chat/assets/stylesheets/mobile/index.scss +++ b/plugins/chat/assets/stylesheets/mobile/index.scss @@ -12,3 +12,4 @@ @import "chat-thread"; @import "chat-threads-list"; @import "chat-thread-settings-modal"; +@import "chat-message-thread-indicator"; diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml index 926199872cc..dac54a5c841 100644 --- a/plugins/chat/config/locales/client.en.yml +++ b/plugins/chat/config/locales/client.en.yml @@ -565,6 +565,9 @@ en: started_by: "Started by" settings: "Settings" last_reply: "last reply" + participants_other_count: + one: "+%{count} other" + other: "+%{count} others" threads: open: "Open Thread" list: "Ongoing discussions" diff --git a/plugins/chat/lib/chat/message_creator.rb b/plugins/chat/lib/chat/message_creator.rb index b7cc6f738cd..2ea493f8204 100644 --- a/plugins/chat/lib/chat/message_creator.rb +++ b/plugins/chat/lib/chat/message_creator.rb @@ -62,13 +62,13 @@ module Chat create_thread @chat_message.attach_uploads(uploads) Chat::Draft.where(user_id: @user.id, chat_channel_id: @chat_channel.id).destroy_all + post_process_resolved_thread Chat::Publisher.publish_new!( @chat_channel, @chat_message, @staged_id, staged_thread_id: @staged_thread_id, ) - post_process_resolved_thread Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id }) Chat::Notifier.notify_new(chat_message: @chat_message, timestamp: @chat_message.created_at) @chat_channel.touch(:last_message_sent_at) diff --git a/plugins/chat/lib/chat/message_updater.rb b/plugins/chat/lib/chat/message_updater.rb index 91c1296fc4a..26cd9f9ffeb 100644 --- a/plugins/chat/lib/chat/message_updater.rb +++ b/plugins/chat/lib/chat/message_updater.rb @@ -41,6 +41,10 @@ module Chat Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id }) Chat::Notifier.notify_edit(chat_message: @chat_message, timestamp: revision.created_at) DiscourseEvent.trigger(:chat_message_edited, @chat_message, @chat_channel, @user) + + if @chat_message.thread.present? + Chat::Publisher.publish_thread_original_message_metadata!(@chat_message.thread) + end rescue => error @error = error end diff --git a/plugins/chat/spec/components/chat/message_creator_spec.rb b/plugins/chat/spec/components/chat/message_creator_spec.rb index 15ebed5e4df..6a1d6116a07 100644 --- a/plugins/chat/spec/components/chat/message_creator_spec.rb +++ b/plugins/chat/spec/components/chat/message_creator_spec.rb @@ -769,11 +769,6 @@ describe Chat::MessageCreator do it "does not create a thread membership if one exists" do Fabricate(:user_chat_thread_membership, user: user1, thread: existing_thread) - Fabricate( - :user_chat_thread_membership, - user: existing_thread.original_message_user, - thread: existing_thread, - ) expect { described_class.create( chat_channel: public_chat_channel, diff --git a/plugins/chat/spec/components/chat/message_updater_spec.rb b/plugins/chat/spec/components/chat/message_updater_spec.rb index c6a67af24e4..3398394f49c 100644 --- a/plugins/chat/spec/components/chat/message_updater_spec.rb +++ b/plugins/chat/spec/components/chat/message_updater_spec.rb @@ -610,6 +610,29 @@ describe Chat::MessageUpdater do end end + context "when the message is in a thread" do + fab!(:message) do + Fabricate( + :chat_message, + user: user1, + chat_channel: public_chat_channel, + thread: Fabricate(:chat_thread, channel: public_chat_channel), + ) + end + + it "publishes a MessageBus event to update the original message metadata" do + messages = + MessageBus.track_publish("/chat/#{public_chat_channel.id}") do + Chat::MessageUpdater.update( + guardian: guardian, + chat_message: message, + new_content: "some new updated content", + ) + end + expect(messages.find { |m| m.data["type"] == "update_thread_original_message" }).to be_present + end + end + describe "watched words" do fab!(:watched_word) { Fabricate(:watched_word) } diff --git a/plugins/chat/spec/fabricators/chat_fabricator.rb b/plugins/chat/spec/fabricators/chat_fabricator.rb index 0fa8cc41f67..e9d57af37c2 100644 --- a/plugins/chat/spec/fabricators/chat_fabricator.rb +++ b/plugins/chat/spec/fabricators/chat_fabricator.rb @@ -168,7 +168,10 @@ Fabricator(:chat_thread, class_name: "Chat::Thread") do ) end - after_create { |thread| thread.original_message.update!(thread_id: thread.id) } + after_create do |thread| + thread.original_message.update!(thread_id: thread.id) + thread.add(thread.original_message_user) + end end Fabricator(:user_chat_thread_membership, class_name: "Chat::UserChatThreadMembership") do diff --git a/plugins/chat/spec/jobs/regular/update_thread_reply_count_spec.rb b/plugins/chat/spec/jobs/regular/update_thread_reply_count_spec.rb index f9eacbcdc85..fc2d0082a90 100644 --- a/plugins/chat/spec/jobs/regular/update_thread_reply_count_spec.rb +++ b/plugins/chat/spec/jobs/regular/update_thread_reply_count_spec.rb @@ -37,20 +37,4 @@ RSpec.describe Jobs::Chat::UpdateThreadReplyCount do Time.at(Time.zone.now.to_i, in: Time.zone), ) end - - it "publishes the thread original message metadata" do - messages = - MessageBus.track_publish("/chat/#{thread.channel_id}") do - described_class.new.execute(thread_id: thread.id) - end - - expect(messages.first.data).to eq( - { - "original_message_id" => thread.original_message_id, - "replies_count" => 2, - "type" => "update_thread_original_message", - "title" => thread.title, - }, - ) - end end diff --git a/plugins/chat/spec/queries/chat/thread_participant_query_spec.rb b/plugins/chat/spec/queries/chat/thread_participant_query_spec.rb new file mode 100644 index 00000000000..9aa6d4e3c4e --- /dev/null +++ b/plugins/chat/spec/queries/chat/thread_participant_query_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe Chat::ThreadParticipantQuery do + fab!(:thread_1) { Fabricate(:chat_thread) } + fab!(:thread_2) { Fabricate(:chat_thread) } + + context "when users have messaged in the thread" do + fab!(:user_1) { Fabricate(:user) } + fab!(:user_2) { Fabricate(:user) } + fab!(:user_3) { Fabricate(:user) } + + before do + Fabricate(:chat_message, thread: thread_1, user: user_1) + Fabricate(:chat_message, thread: thread_1, user: user_1) + Fabricate(:chat_message, thread: thread_1, user: user_1) + Fabricate(:chat_message, thread: thread_1, user: user_2) + Fabricate(:chat_message, thread: thread_1, user: user_2) + Fabricate(:chat_message, thread: thread_1, user: user_3) + + thread_1.add(user_1) + thread_1.add(user_2) + thread_1.add(user_3) + end + + it "has all the user details needed for BasicUserSerializer" do + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].first).to eq( + { + id: user_1.id, + username: user_1.username, + name: user_1.name, + uploaded_avatar_id: user_1.uploaded_avatar_id, + }, + ) + end + + it "does not return more than 3 thread participants" do + other_user = Fabricate(:user) + thread_1.add(other_user) + Fabricate(:chat_message, thread: thread_1, user: other_user) + result = described_class.call(thread_ids: [thread_1.id]) + expect(result[thread_1.id][:users].length).to eq(3) + end + + it "calculates the top messagers in a thread as well as the last messager" do + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [user_1.id, user_2.id, user_3.id], + ) + end + + it "does not count deleted messages for last messager" do + thread_1.replies.where(user: user_3).each(&:trash!) + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [user_1.id, thread_1.original_message_user_id, user_2.id], + ) + end + + it "does not count deleted messages for participation" do + thread_1.replies.where(user: user_1).each(&:trash!) + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [user_2.id, thread_1.original_message_user_id, user_3.id], + ) + end + + it "does not count users who are not members of the thread any longer for participation" do + thread_1.remove(user_1) + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [user_2.id, thread_1.original_message_user_id, user_3.id], + ) + end + + it "calculates the total number of thread participants" do + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:total_count]).to eq(4) + end + + it "gets results for both threads" do + thread_2.add(user_2) + Fabricate(:chat_message, thread: thread_2, user: user_2) + Fabricate(:chat_message, thread: thread_2, user: user_2) + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [user_1.id, user_2.id, user_3.id], + ) + expect(result[thread_2.id][:users].map { |u| u[:id] }).to eq( + [thread_2.original_message_user_id, user_2.id], + ) + end + end + + context "when no one has messaged in either thread but the original message user" do + it "only returns that user as a participant" do + result = described_class.call(thread_ids: [thread_1.id, thread_2.id]) + expect(result[thread_1.id][:users].map { |u| u[:id] }).to eq( + [thread_1.original_message.user_id], + ) + expect(result[thread_1.id][:total_count]).to eq(1) + expect(result[thread_2.id][:users].map { |u| u[:id] }).to eq( + [thread_2.original_message.user_id], + ) + expect(result[thread_2.id][:total_count]).to eq(1) + end + end +end diff --git a/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb b/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb index c97e0a4e5c1..9f1218b0618 100644 --- a/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb +++ b/plugins/chat/spec/serializer/chat/chat_message_serializer_spec.rb @@ -238,7 +238,6 @@ describe Chat::MessageSerializer do it "does not include thread data" do serialized = described_class.new(message_1, scope: guardian, root: nil).as_json expect(serialized).not_to have_key(:thread_id) - expect(serialized).not_to have_key(:thread_reply_count) end end @@ -251,7 +250,6 @@ describe Chat::MessageSerializer do it "does not include thread data" do serialized = described_class.new(message_1, scope: guardian, root: nil).as_json expect(serialized).not_to have_key(:thread_id) - expect(serialized).not_to have_key(:thread_reply_count) end end @@ -264,7 +262,6 @@ describe Chat::MessageSerializer do it "does include thread data" do serialized = described_class.new(message_1, scope: guardian, root: nil).as_json expect(serialized).to have_key(:thread_id) - expect(serialized).to have_key(:thread_reply_count) end end end diff --git a/plugins/chat/spec/services/chat/publisher_spec.rb b/plugins/chat/spec/services/chat/publisher_spec.rb index d932107277f..ef59298ffab 100644 --- a/plugins/chat/spec/services/chat/publisher_spec.rb +++ b/plugins/chat/spec/services/chat/publisher_spec.rb @@ -343,9 +343,6 @@ describe Chat::Publisher do message_id: message_1.id, user_id: message_1.user_id, username: message_1.user.username, - excerpt: - message_1.censored_excerpt(rich: true, max_length: Chat::Thread::EXCERPT_LENGTH), - created_at: message_1.created_at, thread_id: thread.id, }, ) diff --git a/plugins/chat/spec/services/chat/update_thread_spec.rb b/plugins/chat/spec/services/chat/update_thread_spec.rb index 2d4238a1df5..ac85592aabd 100644 --- a/plugins/chat/spec/services/chat/update_thread_spec.rb +++ b/plugins/chat/spec/services/chat/update_thread_spec.rb @@ -47,7 +47,6 @@ RSpec.describe Chat::UpdateThread do .first expect(message.data["type"]).to eq("update_thread_original_message") - expect(message.data["title"]).to eq(title) end end diff --git a/plugins/chat/spec/system/message_thread_indicator_spec.rb b/plugins/chat/spec/system/message_thread_indicator_spec.rb index 2f90d473b61..0e8cf6d9dfc 100644 --- a/plugins/chat/spec/system/message_thread_indicator_spec.rb +++ b/plugins/chat/spec/system/message_thread_indicator_spec.rb @@ -66,13 +66,11 @@ describe "Thread indicator for chat messages", type: :system do it "shows the correct reply counts" do chat_page.visit_channel(channel) - expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_css( - ".chat-message-thread-indicator__replies-count", - text: I18n.t("js.chat.thread.replies", count: 3), + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_reply_count( + 3, ) - expect(channel_page.message_thread_indicator(thread_2.original_message)).to have_css( - ".chat-message-thread-indicator__replies-count", - text: I18n.t("js.chat.thread.replies", count: 1), + expect(channel_page.message_thread_indicator(thread_2.original_message)).to have_reply_count( + 1, ) end @@ -103,9 +101,8 @@ describe "Thread indicator for chat messages", type: :system do it "increments the indicator when a new reply is sent in the thread" do chat_page.visit_channel(channel) - expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_css( - ".chat-message-thread-indicator__replies-count", - text: I18n.t("js.chat.thread.replies", count: 3), + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_reply_count( + 3, ) channel_page.message_thread_indicator(thread_1.original_message).click @@ -114,9 +111,54 @@ describe "Thread indicator for chat messages", type: :system do open_thread.send_message - expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_css( - ".chat-message-thread-indicator__replies-count", - text: I18n.t("js.chat.thread.replies", count: 4), + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_reply_count( + 4, + ) + end + + it "shows participants of the thread" do + chat_page.visit_channel(channel) + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_participant( + current_user, + ) + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_participant( + other_user, + ) + end + + it "shows an excerpt of the last reply in the thread" do + chat_page.visit_channel(channel) + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_excerpt( + thread_1.replies.last, + ) + end + + it "updates the last reply excerpt and participants when a new message is added to the thread" do + new_user = Fabricate(:user) + chat_system_user_bootstrap(user: new_user, channel: channel) + original_last_reply = thread_1.replies.last + + chat_page.visit_channel(channel) + + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_excerpt( + original_last_reply, + ) + + using_session(:new_user) do |session| + sign_in(new_user) + chat_page.visit_channel(channel) + channel_page.message_thread_indicator(thread_1.original_message).click + + expect(side_panel).to have_open_thread(thread_1) + + open_thread.send_message("wow i am happy to join this thread!") + end + + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_participant( + new_user, + ) + expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_excerpt( + thread_1.replies.where(user: new_user).first, ) end end diff --git a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb index 29879f6d00d..9514c23c253 100644 --- a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb +++ b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb @@ -209,16 +209,16 @@ module PageObjects end end - def has_thread_indicator?(message, text: nil) - has_css?(message_thread_indicator_selector(message), text: text) + def has_thread_indicator?(message) + message_thread_indicator(message).exists? end - def has_no_thread_indicator?(message, text: nil) - has_no_css?(message_thread_indicator_selector(message), text: text) + def has_no_thread_indicator?(message) + message_thread_indicator(message).does_not_exist? end def message_thread_indicator(message) - find(message_thread_indicator_selector(message)) + PageObjects::Components::Chat::ThreadIndicator.new(message_by_id_selector(message.id)) end def open_thread_list diff --git a/plugins/chat/spec/system/page_objects/chat/components/thread_indicator.rb b/plugins/chat/spec/system/page_objects/chat/components/thread_indicator.rb new file mode 100644 index 00000000000..fa591f8d239 --- /dev/null +++ b/plugins/chat/spec/system/page_objects/chat/components/thread_indicator.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module PageObjects + module Components + module Chat + class ThreadIndicator < PageObjects::Components::Base + attr_reader :context + + SELECTOR = ".chat-message-thread-indicator" + + def initialize(context) + @context = context + end + + def click + find(@context).find(SELECTOR).click + end + + def exists?(**args) + find(@context).has_css?(SELECTOR) + end + + def does_not_exist?(**args) + find(@context).has_no_css?(SELECTOR) + end + + def has_reply_count?(count) + find(@context).has_css?( + "#{SELECTOR}__replies-count", + text: I18n.t("js.chat.thread.replies", count: count), + ) + end + + def has_participant?(user) + find(@context).has_css?( + ".chat-thread-participants__avatar-group .chat-user-avatar .chat-user-avatar-container[data-user-card=\"#{user.username}\"] img", + ) + end + + def has_excerpt?(message) + excerpt_text = + message.censored_excerpt(rich: true, max_length: ::Chat::Thread::EXCERPT_LENGTH).gsub( + "…", + "…", + ) + find(@context).has_css?("#{SELECTOR}__last-reply-excerpt", text: excerpt_text) + end + end + end + end +end diff --git a/plugins/chat/spec/system/reply_to_message/drawer_spec.rb b/plugins/chat/spec/system/reply_to_message/drawer_spec.rb index 4cc9dc6766e..89f4bab3138 100644 --- a/plugins/chat/spec/system/reply_to_message/drawer_spec.rb +++ b/plugins/chat/spec/system/reply_to_message/drawer_spec.rb @@ -57,7 +57,7 @@ RSpec.describe "Reply to message - channel - drawer", type: :system do chat_page.open_from_header drawer_page.open_channel(channel_1) - expect(channel_page).to have_thread_indicator(original_message, text: "1") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(1) channel_page.reply_to(original_message) @@ -70,7 +70,7 @@ RSpec.describe "Reply to message - channel - drawer", type: :system do drawer_page.back - expect(channel_page).to have_thread_indicator(original_message, text: "2") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) expect(channel_page.messages).to have_no_message(text: "reply to message") end end diff --git a/plugins/chat/spec/system/reply_to_message/full_page_spec.rb b/plugins/chat/spec/system/reply_to_message/full_page_spec.rb index a9c0ce6557b..f7ec6a392ab 100644 --- a/plugins/chat/spec/system/reply_to_message/full_page_spec.rb +++ b/plugins/chat/spec/system/reply_to_message/full_page_spec.rb @@ -67,7 +67,7 @@ RSpec.describe "Reply to message - channel - full page", type: :system do it "replies to the existing thread" do chat_page.visit_channel(channel_1) - expect(channel_page).to have_thread_indicator(original_message, text: "1") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(1) channel_page.reply_to(original_message) @@ -78,7 +78,7 @@ RSpec.describe "Reply to message - channel - full page", type: :system do expect(thread_page).to have_message(text: message_1.message) expect(thread_page).to have_message(text: "reply to message") - expect(channel_page).to have_thread_indicator(original_message, text: "2") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) expect(channel_page).to have_no_message(text: "reply to message") end end diff --git a/plugins/chat/spec/system/reply_to_message/mobile_spec.rb b/plugins/chat/spec/system/reply_to_message/mobile_spec.rb index 3e778bb5665..85c7704ff00 100644 --- a/plugins/chat/spec/system/reply_to_message/mobile_spec.rb +++ b/plugins/chat/spec/system/reply_to_message/mobile_spec.rb @@ -59,7 +59,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru it "replies to the existing thread" do chat_page.visit_channel(channel_1) - expect(channel_page).to have_thread_indicator(original_message, text: "1") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(1) channel_page.reply_to(original_message) thread_page.send_message("reply to message") @@ -69,7 +69,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru thread_page.close - expect(channel_page).to have_thread_indicator(original_message, text: "2") + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) expect(channel_page.messages).to have_no_message(text: "reply to message") end end diff --git a/plugins/chat/spec/system/reply_to_message/smoke_spec.rb b/plugins/chat/spec/system/reply_to_message/smoke_spec.rb index d3e302a3a6b..505f94be5a0 100644 --- a/plugins/chat/spec/system/reply_to_message/smoke_spec.rb +++ b/plugins/chat/spec/system/reply_to_message/smoke_spec.rb @@ -36,39 +36,39 @@ RSpec.describe "Reply to message - smoke", type: :system do thread_page.fill_composer("user1reply") thread_page.click_send_message - expect(channel_page).to have_thread_indicator(original_message, text: 1) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(1) expect(thread_page).to have_message(text: "user1reply") end using_session(:user_2) do |session| expect(thread_page).to have_message(text: "user1reply") - expect(channel_page).to have_thread_indicator(original_message, text: 1) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(1) thread_page.fill_composer("user2reply") thread_page.click_send_message expect(thread_page).to have_message(text: "user2reply") - expect(channel_page).to have_thread_indicator(original_message, text: 2) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) refresh expect(thread_page).to have_message(text: "user1reply") expect(thread_page).to have_message(text: "user2reply") - expect(channel_page).to have_thread_indicator(original_message, text: 2) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) session.quit end using_session(:user_1) do |session| expect(thread_page).to have_message(text: "user2reply") - expect(channel_page).to have_thread_indicator(original_message, text: 2) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) refresh expect(thread_page).to have_message(text: "user1reply") expect(thread_page).to have_message(text: "user2reply") - expect(channel_page).to have_thread_indicator(original_message, text: 2) + expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2) session.quit end diff --git a/plugins/chat/test/javascripts/components/chat-channel-test.js b/plugins/chat/test/javascripts/components/chat-channel-test.js index 1e066ec52f2..b24898dd636 100644 --- a/plugins/chat/test/javascripts/components/chat-channel-test.js +++ b/plugins/chat/test/javascripts/components/chat-channel-test.js @@ -182,7 +182,6 @@ module( 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,