mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 02:19:27 +08:00
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:
parent
f0435844df
commit
584a17c948
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatMessageThreadIndicator extends Component {}
|
|
@ -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}}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
96
plugins/chat/spec/system/message_thread_indicator_spec.rb
Normal file
96
plugins/chat/spec/system/message_thread_indicator_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user