FEATURE: Thread indicator improvements and participants (#21909)
This commit adds the initial part of thread indicator improvements: * Show the reply count, last reply date and excerpt, and the participants of the thread's avatars and count of additional participants * Add a participants component for the thread that can be reused for the list * Add a query class to get the thread participants * Live update the thread indicator more consistently with the last reply and participant details image image In subsequent PRs we will cache the participants since they do not change often, and improve the thread list further with participants. This commit also adds a showPresence boolean (default true) to ChatUserAvatar, since we don't want to show the online indicator for thread participants. --------- Co-authored-by: chapoi <charlie@discourse.org>
This commit is contained in:
parent
897b6d86c7
commit
f75ac9da30
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Integer>] The IDs of the threads to query.
|
||||
# @return [Hash<Integer, 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -3,11 +3,50 @@
|
|||
@models={{@message.thread.routeModels}}
|
||||
class="chat-message-thread-indicator"
|
||||
>
|
||||
<span class="chat-message-thread-indicator__replies-count">
|
||||
{{i18n "chat.thread.replies" count=@message.threadReplyCount}}
|
||||
</span>
|
||||
<span class="chat-message-thread-indicator__view-thread overflow-ellipsis">
|
||||
{{i18n "chat.thread.view_thread"}}{{#if this.threadTitle}}:
|
||||
{{replace-emoji this.threadTitle}}{{/if}}
|
||||
</span>
|
||||
{{#unless this.chatStateManager.isDrawerActive}}
|
||||
<div class="chat-message-thread-indicator__last-reply-avatar">
|
||||
<ChatUserAvatar
|
||||
@user={{@message.thread.preview.lastReplyUser}}
|
||||
@avatarSize="small"
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="chat-message-thread-indicator__body">
|
||||
<div class="chat-message-thread-indicator__last-reply-metadata">
|
||||
<div class="chat-message-thread-indicator__last-reply-user">
|
||||
{{#if (or this.site.mobileView this.chatStateManager.isDrawerActive)}}
|
||||
<span
|
||||
class="chat-message-thread-indicator__last-reply-avatar -mobile"
|
||||
>
|
||||
<ChatUserAvatar
|
||||
@user={{@message.thread.preview.lastReplyUser}}
|
||||
@avatarSize="tiny"
|
||||
/>
|
||||
</span>
|
||||
{{/if}}
|
||||
<span class="chat-message-thread-indicator__last-reply-username">
|
||||
{{@message.thread.preview.lastReplyUser.username}}
|
||||
</span>
|
||||
</div>
|
||||
<span><span class="chat-message-thread-indicator__last-reply-label">{{i18n
|
||||
"chat.thread.last_reply"
|
||||
}}</span>{{format-date
|
||||
@message.thread.preview.lastReplyCreatedAt
|
||||
leaveAgo="true"
|
||||
}}</span>
|
||||
|
||||
|
|
||||
<span class="chat-message-thread-indicator__replies-count">
|
||||
{{i18n "chat.thread.replies" count=@message.thread.preview.replyCount}}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div class="chat-message-thread-indicator__last-reply-excerpt">
|
||||
{{replace-emoji (html-safe @message.thread.preview.lastReplyExcerpt)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-message-thread-indicator__participants">
|
||||
<Chat::Thread::Participants @thread={{@message.thread}} />
|
||||
</div>
|
||||
</LinkTo>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<div class="chat-user-avatar {{if this.isOnline 'is-online'}}">
|
||||
<div
|
||||
class="chat-user-avatar
|
||||
{{if (and this.isOnline this.showPresence) 'is-online'}}"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
class="chat-user-avatar-container clickable"
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class ChatUserAvatar extends Component {
|
|||
user = null;
|
||||
|
||||
avatarSize = "tiny";
|
||||
showPresence = true;
|
||||
|
||||
@computed("chat.presenceChannel.users.[]", "user.{id,username}")
|
||||
get isOnline() {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{{#if (gt @thread.preview.participantUsers.length 1)}}
|
||||
<div class="chat-thread-participants">
|
||||
<div class="chat-thread-participants__avatar-group">
|
||||
{{#each @thread.preview.participantUsers as |user|}}
|
||||
<ChatUserAvatar
|
||||
@user={{user}}
|
||||
@avatarSize="tiny"
|
||||
@showPresence={{false}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if @thread.preview.otherParticipantCount}}
|
||||
<div class="chat-thread-participants__other-count">
|
||||
{{i18n
|
||||
"chat.thread.participants_other_count"
|
||||
count=@thread.preview.otherParticipantCount
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,4 +5,5 @@
|
|||
@import "chat-index-full-page";
|
||||
@import "chat-message-actions";
|
||||
@import "chat-message";
|
||||
@import "chat-message-thread-indicator";
|
||||
@import "sidebar-extensions";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -12,3 +12,4 @@
|
|||
@import "chat-thread";
|
||||
@import "chat-threads-list";
|
||||
@import "chat-thread-settings-modal";
|
||||
@import "chat-message-thread-indicator";
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue