FEATURE: Initial chat thread indicator and disabling echo mode in channels (#21047)

This commit introduces a new thread indicator for channels with `threading_enabled`
set to true and the `enable_exp` site setting set to true. In addition, in the main channel
stream we now hide all messages that are linked to threads except for the original message,
disabling the concept of an "echo mode" for now, we may revisit this in future. We also
remove the jigsaw puzzle "Open Thread" button for message actions, since the thread
indicator can just be used instead.

This also stops the `Chat::Publisher` from sending any messages related to chat
messages that are linked to a thread, unless that chat message is the OM of the
thread. A subsequent PR will link up all MessageBus events within the thread panel,
and for the message indicators.

Another subsequent PR will add the excerpt of the latest message in each thread,
as well as the avatars of the users messaging in the thread.

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
Martin Brennan 2023-04-12 11:09:06 +10:00 committed by GitHub
parent f0435844df
commit 584a17c948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 506 additions and 79 deletions

View File

@ -176,6 +176,7 @@ module Chat
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
messages = messages.where(thread_id: params[:thread_id]) if params[:thread_id]
messages = exclude_thread_messages(messages) if !include_thread_messages?
if message_id.present?
condition = direction == PAST ? "<" : ">"
@ -259,6 +260,7 @@ module Chat
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
messages = messages.where(thread_id: params[:thread_id]) if params[:thread_id]
messages = exclude_thread_messages(messages) if !include_thread_messages?
past_messages =
messages
@ -274,7 +276,11 @@ module Chat
can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT
can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT
messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat)
messages = [
past_messages.reverse,
(!include_thread_messages? && @message.in_thread?) ? [] : [@message],
future_messages,
].reduce([], :concat)
chat_view =
Chat::View.new(
chat_channel: @chat_channel,
@ -412,12 +418,18 @@ module Chat
.includes(:bookmarks)
.includes(:uploads)
.includes(chat_channel: :chatable)
.includes(:thread)
query = query.includes(user: :user_status) if SiteSetting.enable_user_status
query
end
def include_thread_messages?
params[:thread_id].present? || !SiteSetting.enable_experimental_chat_threaded_discussions ||
!@chat_channel.threading_enabled
end
def find_chatable
@chatable = Category.find_by(id: params[:chatable_id])
guardian.ensure_can_moderate_chat!(@chatable)
@ -431,5 +443,15 @@ module Chat
@message = @message.find_by(id: params[:message_id])
raise Discourse::NotFound unless @message
end
def exclude_thread_messages(messages)
messages.where(<<~SQL, channel_id: @chat_channel.id)
chat_messages.thread_id IS NULL OR chat_messages.id IN (
SELECT original_message_id
FROM chat_threads
WHERE chat_threads.channel_id = :channel_id
)
SQL
end
end
end

View File

@ -307,6 +307,18 @@ module Chat
create_mentions(mentioned_user_ids_to_add)
end
def in_thread?
self.thread_id.present?
end
def thread_reply?
in_thread? && !is_thread_om?
end
def is_thread_om?
self.thread.original_message_id == self.id
end
private
def delete_mentions(user_ids)

View File

@ -16,6 +16,7 @@ module Chat
:bookmark,
:available_flags,
:thread_id,
:thread_reply_count,
:chat_channel_id
has_one :user, serializer: Chat::MessageUserSerializer, embed: :objects
@ -151,5 +152,13 @@ module Chat
sym
end
end
def include_thread_reply_count?
object.thread_id.present?
end
def thread_reply_count
object.thread.replies_count
end
end
end

View File

@ -11,6 +11,8 @@ module Chat
end
def self.publish_new!(chat_channel, chat_message, staged_id)
return if chat_message.thread_reply?
content =
Chat::MessageSerializer.new(
chat_message,
@ -35,7 +37,21 @@ module Chat
)
end
def self.publish_thread_created!(chat_channel, chat_message)
content =
Chat::MessageSerializer.new(
chat_message,
{ scope: anonymous_guardian, root: :chat_message },
).as_json
content[:type] = :thread_created
permissions = permissions(chat_channel)
MessageBus.publish(root_message_bus_channel(chat_channel.id), content.as_json, permissions)
end
def self.publish_processed!(chat_message)
return if chat_message.thread_reply?
chat_channel = chat_message.chat_channel
content = {
type: :processed,
@ -52,6 +68,8 @@ module Chat
end
def self.publish_edit!(chat_channel, chat_message)
return if chat_message.thread_reply?
content =
Chat::MessageSerializer.new(
chat_message,
@ -66,6 +84,8 @@ module Chat
end
def self.publish_refresh!(chat_channel, chat_message)
return if chat_message.thread_reply?
content =
Chat::MessageSerializer.new(
chat_message,
@ -80,6 +100,8 @@ module Chat
end
def self.publish_reaction!(chat_channel, chat_message, action, user, emoji)
return if chat_message.thread_reply?
content = {
action: action,
user: BasicUserSerializer.new(user, root: false).as_json,
@ -99,6 +121,8 @@ module Chat
end
def self.publish_delete!(chat_channel, chat_message)
return if chat_message.thread_reply?
MessageBus.publish(
root_message_bus_channel(chat_channel.id),
{ type: "delete", deleted_id: chat_message.id, deleted_at: chat_message.deleted_at },
@ -115,6 +139,8 @@ module Chat
end
def self.publish_restore!(chat_channel, chat_message)
return if chat_message.thread_reply?
content =
Chat::MessageSerializer.new(
chat_message,
@ -129,6 +155,8 @@ module Chat
end
def self.publish_flag!(chat_message, user, reviewable, score)
return if chat_message.thread_reply?
# Publish to user who created flag
MessageBus.publish(
"/chat/#{chat_message.chat_channel_id}",

View File

@ -35,7 +35,10 @@
@value={{readonly this.value}}
@input={{action "onTextareaInput" value="target.value"}}
@type="text"
@class="chat-composer-input"
@class={{concat-class
"chat-composer-input"
(concat "chat-composer-input--" @context)
}}
@disabled={{this.disableComposer}}
@autocorrect="on"
@autocapitalize="sentences"

View File

@ -550,6 +550,19 @@ export default class ChatLivePane extends Component {
case "flag":
this.handleFlaggedMessage(data);
break;
case "thread_created":
this.handleThreadCreated(data);
break;
}
}
handleThreadCreated(data) {
const message = this.args.channel.messagesManager.findMessage(
data.chat_message.id
);
if (message) {
message.threadId = data.chat_message.thread_id;
message.threadReplyCount = 1;
}
}
@ -773,7 +786,14 @@ export default class ChatLivePane extends Component {
stagedMessage.inReplyTo = this.chatChannelComposer.replyToMsg;
}
this.args.channel.messagesManager.addMessages([stagedMessage]);
if (stagedMessage.inReplyTo) {
if (!this.args.channel.threadingEnabled) {
this.args.channel.messagesManager.addMessages([stagedMessage]);
}
} else {
this.args.channel.messagesManager.addMessages([stagedMessage]);
}
if (!this.args.channel.messagesManager.canLoadMoreFuture) {
this.scrollToLatestMessage();
}

View File

@ -50,15 +50,6 @@
/>
{{/if}}
{{#if this.messageInteractor.canOpenThread}}
<DButton
@class="btn-flat chat-message-thread-btn"
@action={{this.messageInteractor.openThread}}
@icon="puzzle-piece"
@title="chat.threads.open"
/>
{{/if}}
{{#if this.messageInteractor.secondaryButtons.length}}
<DropdownSelectBox
@class="more-buttons"

View File

@ -0,0 +1,12 @@
<LinkTo
@route="chat.channel.thread"
@models={{@message.threadRouteModels}}
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">
{{i18n "chat.thread.view_thread"}}
</span>
</LinkTo>

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class ChatMessageThreadIndicator extends Component {}

View File

@ -21,6 +21,7 @@
(if @message.highlighted "highlighted")
}}
data-id={{@message.id}}
data-thread-id={{@message.threadId}}
{{chat/track-message
(hash
didEnterViewport=(fn @messageDidEnterViewport @message)
@ -61,6 +62,7 @@
(if @message.staged "chat-message-staged")
(if @message.deletedAt "deleted")
(if (and @message.inReplyTo (not this.hideReplyToInfo)) "is-reply")
(if this.showThreadIndicator "is-threaded")
(if this.hideUserInfo "user-info-hidden")
(if @message.error "errored")
(if @message.bookmark "chat-message-bookmarked")
@ -189,6 +191,10 @@
</div>
{{/if}}
</div>
{{#if this.showThreadIndicator}}
<ChatMessageThreadIndicator @message={{@message}} />
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@ -1,8 +1,8 @@
import { isTesting } from "discourse-common/config/environment";
import { action } from "@ember/object";
import Component from "@glimmer/component";
import I18n from "I18n";
import optionalService from "discourse/lib/optional-service";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { cancel, schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
@ -260,7 +260,21 @@ export default class ChatMessage extends Component {
return (
this.args.context === MESSAGE_CONTEXT_THREAD ||
this.args.message?.inReplyTo?.id ===
this.args.message?.previousMessage?.id
this.args.message?.previousMessage?.id ||
this.threadingEnabled
);
}
get threadingEnabled() {
return this.args.channel?.threadingEnabled && this.args.message?.threadId;
}
get showThreadIndicator() {
return (
this.args.context !== MESSAGE_CONTEXT_THREAD &&
this.threadingEnabled &&
this.args.message?.threadId !==
this.args.message?.previousMessage?.threadId
);
}

View File

@ -120,14 +120,6 @@ export default class ChatMessageInteractor {
);
}
get canOpenThread() {
return (
this.context !== MESSAGE_CONTEXT_THREAD &&
this.message.channel?.threadingEnabled &&
this.message?.threadId
);
}
get canRebakeMessage() {
return (
this.currentUser?.staff &&
@ -212,14 +204,6 @@ export default class ChatMessageInteractor {
});
}
if (this.canOpenThread) {
buttons.push({
id: "openThread",
name: I18n.t("chat.threads.open"),
icon: "puzzle-piece",
});
}
return buttons;
}
@ -364,15 +348,6 @@ export default class ChatMessageInteractor {
this.composer.editButtonClicked(this.message.id);
}
@action
openThread() {
this.router.transitionTo(
"chat.channel.thread",
...this.message.channel.routeModels,
this.message.threadId
);
}
@action
openEmojiPicker(_, { target }) {
const pickerState = {

View File

@ -31,6 +31,7 @@ export default class ChatMessage {
@tracked excerpt;
@tracked message;
@tracked threadId;
@tracked threadReplyCount;
@tracked reactions;
@tracked reviewableId;
@tracked user;
@ -59,6 +60,7 @@ export default class ChatMessage {
this.availableFlags = args.availableFlags || args.available_flags;
this.hidden = args.hidden;
this.threadId = args.threadId || args.thread_id;
this.threadReplyCount = args.threadReplyCount || args.thread_reply_count;
this.channelId = args.channelId || args.chat_channel_id;
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
this.createdAt = args.createdAt || args.created_at;
@ -81,6 +83,10 @@ export default class ChatMessage {
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
}
get threadRouteModels() {
return [...this.channel.routeModels, this.threadId];
}
get read() {
return this.channel.currentUserMembership?.last_read_message_id >= this.id;
}

View File

@ -0,0 +1,40 @@
.chat-message-thread-indicator {
align-items: center;
display: grid;
grid: 1fr / auto-flow;
display: flex;
cursor: pointer;
grid-area: threadindicator;
border: 1px solid transparent;
margin: 4px 0 -2px calc(var(--message-left-width) - 5px);
padding: 4px;
&:hover {
.chat-message:hover & {
border-color: var(--primary-low);
background-color: var(--secondary);
}
}
&__replies-count {
color: var(--primary-medium);
font-size: var(--font-down-2);
}
&__view-thread {
font-size: var(--font-down-2);
.chat-message-thread-indicator:hover & {
text-decoration: underline;
}
}
&__replies-count + &__view-thread {
padding-left: 0.25rem;
}
&__separator {
margin: 0 0.5em;
}
}

View File

@ -58,6 +58,23 @@
}
}
&.is-threaded {
display: grid;
grid-template-columns: var(--message-left-width) 1fr;
grid-template-rows: auto 32px;
grid-template-areas:
"avatar message"
"threadindicator threadindicator";
.chat-user-avatar {
grid-area: avatar;
}
.chat-message-content {
grid-area: message;
}
}
.chat-message-content {
display: flex;
flex-direction: column;

View File

@ -24,6 +24,7 @@
@import "chat-message-info";
@import "chat-message-left-gutter";
@import "chat-message-separator";
@import "chat-message-thread-indicator";
@import "chat-message";
@import "chat-onebox";
@import "chat-reply";

View File

@ -534,6 +534,10 @@ en:
no_results: "No results"
thread:
view_thread: View thread
replies:
one: "%{count} reply"
other: "%{count} replies"
label: Thread
threads:
started_by: "Started by"

View File

@ -164,39 +164,47 @@ module Chat
def create_thread
return if @in_reply_to_id.blank?
return if @chat_message.thread_id.present?
return if @chat_message.in_thread?
thread =
@original_message.thread ||
if @original_message.thread
thread = @original_message.thread
else
thread =
Chat::Thread.create!(
original_message: @chat_message.in_reply_to,
original_message_user: @chat_message.in_reply_to.user,
channel: @chat_message.chat_channel,
)
@chat_message.in_reply_to.thread_id = thread.id
Chat::Publisher.publish_thread_created!(
@chat_message.chat_channel,
@chat_message.in_reply_to,
)
end
@chat_message.thread_id = thread.id
# NOTE: We intentionally do not try to correct thread IDs within the chain
# if they are incorrect, and only set the thread ID of messages where the
# thread ID is NULL. In future we may want some sync/background job to correct
# any inconsistencies.
DB.exec(<<~SQL)
WITH RECURSIVE thread_updater AS (
SELECT cm.id, cm.in_reply_to_id
FROM chat_messages cm
WHERE cm.in_reply_to_id IS NULL AND cm.id = #{@original_message_id}
WITH RECURSIVE thread_updater AS (
SELECT cm.id, cm.in_reply_to_id
FROM chat_messages cm
WHERE cm.in_reply_to_id IS NULL AND cm.id = #{@original_message_id}
UNION ALL
UNION ALL
SELECT cm.id, cm.in_reply_to_id
FROM chat_messages cm
JOIN thread_updater ON cm.in_reply_to_id = thread_updater.id
)
UPDATE chat_messages
SET thread_id = #{thread.id}
FROM thread_updater
WHERE thread_id IS NULL AND chat_messages.id = thread_updater.id
SQL
@chat_message.thread_id = thread.id
SELECT cm.id, cm.in_reply_to_id
FROM chat_messages cm
JOIN thread_updater ON cm.in_reply_to_id = thread_updater.id
)
UPDATE chat_messages
SET thread_id = #{thread.id}
FROM thread_updater
WHERE thread_id IS NULL AND chat_messages.id = thread_updater.id
SQL
end
end
end

View File

@ -209,7 +209,7 @@ module Chat
def update_thread_references
threads_to_update = []
@source_messages
.select { |message| message.thread_id.present? }
.select { |message| message.in_thread? }
.each do |message_with_thread|
# If one of the messages we are moving is the original message in a thread,
# then all the remaining messages for that thread must be moved to a new one,

View File

@ -436,6 +436,21 @@ describe Chat::MessageCreator do
expect(message.thread.original_message_user).to eq(reply_message.user)
end
it "publishes the new thread" do
messages =
MessageBus.track_publish do
described_class.create(
chat_channel: public_chat_channel,
user: user1,
content: "this is a message",
in_reply_to_id: reply_message.id,
).chat_message
end
thread_created_message = messages.find { |m| m.data["type"] == "thread_created" }
expect(thread_created_message.channel).to eq("/chat/#{public_chat_channel.id}")
end
context "when the thread_id is provided" do
fab!(:existing_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }

View File

@ -42,6 +42,7 @@ module ChatSystemHelpers
last_message = creator.chat_message
end
last_message.thread.update!(replies_count: messages_count - 1)
last_message.thread
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
describe "Channel thread message echoing", type: :system, js: true do
fab!(:current_user) { Fabricate(:user) }
fab!(:other_user) { Fabricate(:user) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:side_panel) { PageObjects::Pages::ChatSidePanel.new }
let(:open_thread) { PageObjects::Pages::ChatThread.new }
let(:chat_drawer_page) { PageObjects::Pages::ChatDrawer.new }
before do
chat_system_bootstrap(current_user, [channel])
sign_in(current_user)
end
context "when enable_experimental_chat_threaded_discussions is disabled" do
fab!(:channel) { Fabricate(:chat_channel) }
before { SiteSetting.enable_experimental_chat_threaded_discussions = false }
it "echoes the thread messages into the main channel stream" do
thread = chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
chat_page.visit_channel(channel)
thread.chat_messages.each do |thread_message|
expect(channel_page).to have_css(channel_page.message_by_id_selector(thread_message.id))
end
end
end
context "when threading_enabled is false for the channel" do
fab!(:channel) { Fabricate(:chat_channel) }
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
channel.update!(threading_enabled: false)
end
it "echoes the thread messages into the main channel stream" do
thread = chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
chat_page.visit_channel(channel)
thread.chat_messages.each do |thread_message|
expect(channel_page).to have_css(channel_page.message_by_id_selector(thread_message.id))
end
end
end
context "when enable_experimental_chat_threaded_discussions is true and threading is enabled for the channel" do
fab!(:channel) { Fabricate(:chat_channel) }
fab!(:thread) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
channel.update!(threading_enabled: true)
end
it "does not echo the thread messages except for the original message into the channel stream" do
chat_page.visit_channel(channel)
expect(channel_page).to have_css(
channel_page.message_by_id_selector(thread.original_message.id),
)
thread.replies.each do |thread_message|
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(thread_message.id))
end
end
it "does not echo new thread messages into the channel stream" do
chat_page.visit_channel(channel)
channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel).to have_open_thread(thread)
open_thread.send_message(thread.id, "new thread message")
expect(open_thread).to have_message(thread.id, text: "new thread message")
new_message = thread.reload.replies.last
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(new_message.id))
end
it "does not echo the looked up message into the channel stream if it is in a thread" do
current_user
.user_chat_channel_memberships
.find_by(chat_channel: channel)
.update!(last_read_message_id: thread.replies.last.id)
chat_page.visit_channel(channel)
expect(channel_page).not_to have_css(
channel_page.message_by_id_selector(thread.replies.last.id),
)
end
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
describe "Thread indicator for chat messages", type: :system, js: true do
fab!(:current_user) { Fabricate(:user) }
fab!(:other_user) { Fabricate(:user) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:side_panel) { PageObjects::Pages::ChatSidePanel.new }
let(:open_thread) { PageObjects::Pages::ChatThread.new }
let(:chat_drawer_page) { PageObjects::Pages::ChatDrawer.new }
before do
chat_system_bootstrap(current_user, [channel])
sign_in(current_user)
end
context "when enable_experimental_chat_threaded_discussions is disabled" do
fab!(:channel) { Fabricate(:chat_channel) }
before { SiteSetting.enable_experimental_chat_threaded_discussions = false }
it "shows no thread indicators in the channel" do
thread = chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
chat_page.visit_channel(channel)
expect(channel_page).not_to have_thread_indicator(thread.original_message)
end
end
context "when threading_enabled is false for the channel" do
fab!(:channel) { Fabricate(:chat_channel) }
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
channel.update!(threading_enabled: false)
end
it "shows no thread inidcators in the channel" do
thread = chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
chat_page.visit_channel(channel)
expect(channel_page).not_to have_thread_indicator(thread.original_message)
end
end
context "when enable_experimental_chat_threaded_discussions is true and threading is enabled for the channel" do
fab!(:channel) { Fabricate(:chat_channel) }
fab!(:thread_1) do
chat_thread_chain_bootstrap(channel: channel, users: [current_user, other_user])
end
fab!(:thread_2) do
chat_thread_chain_bootstrap(
channel: channel,
users: [current_user, other_user],
messages_count: 2,
)
end
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
channel.update!(threading_enabled: true)
end
it "throws thread indicators on all original messages" do
chat_page.visit_channel(channel)
expect(channel_page).to have_thread_indicator(thread_1.original_message)
expect(channel_page).to have_thread_indicator(thread_2.original_message)
end
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_2.original_message)).to have_css(
".chat-message-thread-indicator__replies-count",
text: I18n.t("js.chat.thread.replies", count: 1),
)
end
it "clicking a thread indicator opens the thread panel" do
chat_page.visit_channel(channel)
channel_page.message_thread_indicator(thread_1.original_message).click
expect(side_panel).to have_open_thread(thread_1)
end
it "shows the thread indicator and hides the sent message when a user first replies to a message without a thread" do
message_without_thread = Fabricate(:chat_message, chat_channel: channel, user: other_user)
chat_page.visit_channel(channel)
channel_page.reply_to(message_without_thread)
channel_page.fill_composer("this is a reply to make a new thread")
channel_page.click_send_message
expect(channel_page).to have_thread_indicator(message_without_thread)
new_thread = message_without_thread.reload.thread
expect(page).not_to have_css(channel_page.message_by_id_selector(new_thread.replies.first))
end
end
end

View File

@ -4,19 +4,29 @@ module PageObjects
module Pages
class ChatChannel < PageObjects::Pages::Base
def type_in_composer(input)
find(".chat-composer-input").send_keys(input)
find(".chat-composer-input--channel").click # makes helper more reliable by ensuring focus is not lost
find(".chat-composer-input--channel").send_keys(input)
end
def fill_composer(input)
find(".chat-composer-input").fill_in(with: input)
find(".chat-composer-input--channel").click # makes helper more reliable by ensuring focus is not lost
find(".chat-composer-input--channel").fill_in(with: input)
end
def click_composer
find(".chat-composer-input--channel").click # ensures autocomplete is closed and not masking anything
end
def click_send_message
find(".chat-composer .send-btn:enabled").click
end
def message_by_id_selector(id)
".chat-message-container[data-id=\"#{id}\"]"
end
def message_by_id(id)
find(".chat-message-container[data-id=\"#{id}\"]")
find(message_by_id_selector(id))
end
def has_no_loading_skeleton?
@ -67,11 +77,6 @@ module PageObjects
find("[data-value='flag']").click
end
def open_message_thread(message)
hover_message(message)
find(".chat-message-thread-btn").click
end
def select_message(message)
hover_message(message)
click_more_button
@ -97,10 +102,9 @@ module PageObjects
def send_message(text = nil)
text = text.chomp if text.present? # having \n on the end of the string counts as an Enter keypress
find(".chat-composer-input").click # makes helper more reliable by ensuring focus is not lost
find(".chat-composer-input").fill_in(with: text)
fill_composer(text)
click_send_message
find(".chat-composer-input").click # ensures autocomplete is closed and not masking anything
click_composer
end
def reply_to(message)
@ -153,6 +157,14 @@ module PageObjects
has_css?(".chat-message-container[data-id=\"#{id}\"]", wait: 10)
end
end
def has_thread_indicator?(message)
has_css?("#{message_by_id_selector(message.id)} .chat-message-thread-indicator")
end
def message_thread_indicator(message)
find("#{message_by_id_selector(message.id)} .chat-message-thread-indicator")
end
end
end
end

View File

@ -14,6 +14,50 @@ module PageObjects
def has_header_content?(content)
header.has_content?(content)
end
def thread_selector_by_id(id)
".chat-thread[data-id=\"#{id}\"]"
end
def has_no_loading_skeleton?
has_no_css?(".chat-thread__messages .chat-skeleton")
end
def type_in_composer(input)
find(".chat-composer-input--thread").click # makes helper more reliable by ensuring focus is not lost
find(".chat-composer-input--thread").send_keys(input)
end
def fill_composer(input)
find(".chat-composer-input--thread").click # makes helper more reliable by ensuring focus is not lost
find(".chat-composer-input--thread").fill_in(with: input)
end
def click_composer
find(".chat-composer-input--thread").click # ensures autocomplete is closed and not masking anything
end
def send_message(id, text = nil)
text = text.chomp if text.present? # having \n on the end of the string counts as an Enter keypress
fill_composer(text)
click_send_message(id)
click_composer
end
def click_send_message(id)
find(thread_selector_by_id(id)).find(".chat-composer .send-btn:enabled").click
end
def has_message?(thread_id, text: nil, id: nil)
if text
find(thread_selector_by_id(thread_id)).has_css?(".chat-message-text", text: text)
elsif id
find(thread_selector_by_id(thread_id)).has_css?(
".chat-message-container[data-id=\"#{id}\"]",
wait: 10,
)
end
end
end
end
end

View File

@ -50,38 +50,37 @@ describe "Single thread in side panel", type: :system, js: true do
before { SiteSetting.enable_experimental_chat_threaded_discussions = true }
it "opens the single thread in the drawer from the message actions menu" do
it "opens the single thread in the drawer using the indicator" do
visit("/latest")
chat_page.open_from_header
chat_drawer_page.open_channel(channel)
channel_page.open_message_thread(thread.chat_messages.order(:created_at).last)
channel_page.message_thread_indicator(thread.original_message).click
expect(chat_drawer_page).to have_open_thread(thread)
end
it "opens the side panel for a single thread from the message actions menu" do
it "opens the side panel for a single thread from the indicator" do
chat_page.visit_channel(channel)
channel_page.open_message_thread(thread.original_message)
channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel).to have_open_thread(thread)
end
xit "shows the excerpt of the thread original message" do
chat_page.visit_channel(channel)
channel_page.open_message_thread(thread.original_message)
channel_page.message_thread_indicator(thread.original_message).click
expect(open_thread).to have_header_content(thread.excerpt)
end
xit "shows the avatar and username of the original message user" do
chat_page.visit_channel(channel)
channel_page.open_message_thread(thread.original_message)
channel_page.message_thread_indicator(thread.original_message).click
expect(open_thread.omu).to have_css(".chat-user-avatar img.avatar")
expect(open_thread.omu).to have_content(thread.original_message_user.username)
end
context "when using mobile" do
it "opens the side panel for a single thread from the mobile message actions menu",
mobile: true do
it "opens the side panel for a single thread using the indicator", mobile: true do
chat_page.visit_channel(channel)
channel_page.click_message_action_mobile(thread.chat_messages.last, "openThread")
channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel).to have_open_thread(thread)
end
end