diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs
index 8139d3972b7..14d3da398c5 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-selection-manager.hbs
@@ -10,7 +10,7 @@
{{/if}}
-
+
{{#if this.showChatQuoteSuccess}}
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
index c892853691c..69ee6ab84ee 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
@@ -83,6 +83,8 @@ export default {
I18n.t("dates.long_no_year")
);
}
+
+ dateTimeEl.dataset.dateFormatted = true;
});
},
{ id: "chat-transcript-datetime" }
diff --git a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
index 8919b1ee46e..5b53086adc5 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
@@ -23,6 +23,10 @@ module PageObjects
has_no_css?(".chat-skeleton")
end
+ def has_selection_management?
+ has_css?(".chat-selection-management")
+ end
+
def expand_message_actions(message)
hover_message(message)
click_more_buttons(message)
diff --git a/plugins/chat/spec/system/transcript_spec.rb b/plugins/chat/spec/system/transcript_spec.rb
new file mode 100644
index 00000000000..4c7723dbddb
--- /dev/null
+++ b/plugins/chat/spec/system/transcript_spec.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
+ fab!(:current_user) { Fabricate(:user) }
+ fab!(:chat_channel_1) { Fabricate(:chat_channel) }
+
+ let(:chat_page) { PageObjects::Pages::Chat.new }
+ let(:chat_channel_page) { PageObjects::Pages::ChatChannel.new }
+ let(:topic_page) { PageObjects::Pages::Topic.new }
+
+ before do
+ chat_system_bootstrap(Fabricate(:admin), [chat_channel_1])
+ chat_channel_1.add(current_user)
+ sign_in(current_user)
+ end
+
+ def select_message_desktop(message)
+ if page.has_css?(".chat-message-container.selecting-messages")
+ chat_channel_page.message_by_id(message.id).find(".chat-message-selector").click
+ else
+ chat_channel_page.message_by_id(message.id).hover
+ expect(page).to have_css(".chat-message-actions .more-buttons")
+ find(".chat-message-actions .more-buttons").click
+ find(".select-kit-row[data-value=\"selectMessage\"]").click
+ end
+ end
+
+ def select_message_mobile(message)
+ if page.has_css?(".chat-message-container.selecting-messages")
+ chat_channel_page.message_by_id(message.id).find(".chat-message-selector").click
+ else
+ chat_channel_page.message_by_id(message.id).click(delay: 0.5)
+ find(".chat-message-action-item[data-id=\"selectMessage\"]").click
+ end
+ end
+
+ def cdp_allow_clipboard_access!
+ cdp_params = {
+ origin: page.server_url,
+ permission: {
+ name: "clipboard-read",
+ },
+ setting: "granted",
+ }
+ page.driver.browser.execute_cdp("Browser.setPermission", **cdp_params)
+
+ cdp_params = {
+ origin: page.server_url,
+ permission: {
+ name: "clipboard-write",
+ },
+ setting: "granted",
+ }
+ page.driver.browser.execute_cdp("Browser.setPermission", **cdp_params)
+ end
+
+ def read_clipboard
+ page.evaluate_async_script("navigator.clipboard.readText().then(arguments[0])")
+ end
+
+ def click_selection_button(button)
+ selector =
+ case button
+ when "quote"
+ "#chat-quote-btn"
+ when "copy"
+ "#chat-copy-btn"
+ when "cancel"
+ "#chat-cancel-selection-btn"
+ when "move"
+ "#chat-move-to-channel-btn"
+ end
+ within(".chat-selection-management-buttons") { find(selector).click }
+ end
+
+ def copy_messages_to_clipboard(messages)
+ messages = Array.wrap(messages)
+ messages.each { |message| select_message_desktop(message) }
+ expect(chat_channel_page).to have_selection_management
+ click_selection_button("copy")
+ expect(page).to have_content("Chat quote copied to clipboard")
+ clip_text = read_clipboard
+ expect(clip_text.chomp).to eq(generate_transcript(messages, current_user))
+ clip_text
+ end
+
+ def generate_transcript(messages, acting_user)
+ messages = Array.wrap(messages)
+ ChatTranscriptService
+ .new(messages.first.chat_channel, acting_user, messages_or_ids: messages.map(&:id))
+ .generate_markdown
+ .chomp
+ end
+
+ describe "copying quote transcripts with the clipboard" do
+ before { cdp_allow_clipboard_access! }
+
+ context "when quoting a single message into a topic" do
+ fab!(:post_1) { Fabricate(:post) }
+ fab!(:message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+
+ it "quotes the message" do
+ chat_page.visit_channel(chat_channel_1)
+
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ clip_text = copy_messages_to_clipboard(message_1)
+ topic_page.visit_topic_and_open_composer(post_1.topic)
+ topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
+
+ within(".d-editor-preview") { expect(page).to have_css(".chat-transcript") }
+
+ topic_page.send_reply
+ selector = topic_page.post_by_number_selector(2)
+
+ expect(page).to have_css(selector)
+ within(selector) { expect(page).to have_css(".chat-transcript") }
+ end
+ end
+
+ context "when quoting multiple messages into a topic" do
+ fab!(:post_1) { Fabricate(:post) }
+ fab!(:message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:message_2) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+
+ it "quotes the messages" do
+ chat_page.visit_channel(chat_channel_1)
+
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ clip_text = copy_messages_to_clipboard([message_1, message_2])
+ topic_page.visit_topic_and_open_composer(post_1.topic)
+ topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
+
+ within(".d-editor-preview") { expect(page).to have_css(".chat-transcript", count: 2) }
+ expect(page).to have_content("Originally sent in #{chat_channel_1.name}")
+
+ topic_page.send_reply
+
+ selector = topic_page.post_by_number_selector(2)
+ expect(page).to have_css(selector)
+ within(selector) { expect(page).to have_css(".chat-transcript", count: 2) }
+ end
+ end
+
+ context "when quoting a message containing a onebox" do
+ fab!(:post_1) { Fabricate(:post) }
+ fab!(:message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+
+ before do
+ Oneboxer.stubs(:preview).returns(
+ "",
+ )
+ message_1.update!(message: "http://www.example.com/has-title.html")
+ message_1.rebake!
+ end
+
+ it "works" do
+ chat_page.visit_channel(chat_channel_1)
+
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ clip_text = copy_messages_to_clipboard(message_1)
+ topic_page.visit_topic_and_open_composer(post_1.topic)
+ topic_page.fill_in_composer(clip_text)
+
+ within(".chat-transcript-messages") do
+ expect(page).to have_content("An interesting article")
+ end
+ end
+ end
+
+ context "when quoting a message in another message" do
+ fab!(:message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+
+ it "quotes the message" do
+ chat_page.visit_channel(chat_channel_1)
+
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ clip_text = copy_messages_to_clipboard(message_1)
+ click_selection_button("cancel")
+ chat_channel_page.fill_composer(clip_text)
+ chat_channel_page.click_send_message
+
+ expect(page).to have_selector(".chat-message", count: 2)
+
+ message = ChatMessage.find_by(user: current_user, message: clip_text.chomp)
+
+ within(chat_channel_page.message_by_id(message.id)) do
+ expect(page).to have_css(".chat-transcript")
+ end
+ end
+ end
+ end
+
+ context "when quoting into a topic directly" do
+ fab!(:message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ let(:topic_title) { "Some topic title for testing" }
+
+ it "opens the topic composer with correct state" do
+ chat_page.visit_channel(chat_channel_1)
+
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ select_message_desktop(message_1)
+ click_selection_button("quote")
+
+ expect(topic_page).to have_expanded_composer
+ expect(topic_page).to have_composer_content(generate_transcript(message_1, current_user))
+ expect(page).to have_css(
+ ".category-input .select-kit-header[data-value='#{chat_channel_1.chatable.id}']",
+ )
+ expect(page).not_to have_current_path(chat_channel_1.chatable.url)
+
+ topic_page.fill_in_composer_title(topic_title)
+ topic_page.send_reply
+
+ selector = topic_page.post_by_number_selector(1)
+ expect(page).to have_css(selector)
+ within(selector) { expect(page).to have_css(".chat-transcript") }
+
+ topic = Topic.find_by(user: current_user, title: topic_title)
+ expect(page).to have_current_path(topic.url)
+ end
+
+ context "when on mobile" do
+ it "first navigates to the channel's category before opening the topic composer with the quote prefilled",
+ mobile: true do
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+
+ select_message_mobile(message_1)
+ click_selection_button("quote")
+
+ expect(topic_page).to have_expanded_composer
+ expect(topic_page).to have_composer_content(generate_transcript(message_1, current_user))
+ expect(page).to have_current_path(chat_channel_1.chatable.url)
+ expect(page).to have_css(
+ ".category-input .select-kit-header[data-value='#{chat_channel_1.chatable.id}']",
+ )
+ end
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index daa3ac9b868..006b888eeef 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -245,6 +245,8 @@ RSpec.configure do |config|
allow: [Webdrivers::Chromedriver.base_url]
)
+ Capybara.disable_animation = true
+
Capybara.configure do |capybara_config|
capybara_config.server_host = "localhost"
capybara_config.server_port = 31337
diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb
index 9d5054a75d5..ce954dfa7b5 100644
--- a/spec/support/system_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -25,6 +25,7 @@ module SystemHelpers
SiteSetting.force_hostname = Capybara.server_host
SiteSetting.port = Capybara.server_port
SiteSetting.external_system_avatars_enabled = false
+ SiteSetting.disable_avatar_education_message = true
end
def try_until_success(timeout: 2, frequency: 0.01)
diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb
index 20a9675aad0..efe278d05cc 100644
--- a/spec/system/page_objects/pages/topic.rb
+++ b/spec/system/page_objects/pages/topic.rb
@@ -34,7 +34,11 @@ module PageObjects
def post_by_number(post_or_number)
post_or_number = post_or_number.is_a?(Post) ? post_or_number.post_number : post_or_number
- find("#post_#{post_or_number}")
+ find(".topic-post:not(.staged) #post_#{post_or_number}")
+ end
+
+ def post_by_number_selector(post_number)
+ ".topic-post:not(.staged) #post_#{post_number}"
end
def has_post_more_actions?(post)
@@ -74,24 +78,41 @@ module PageObjects
def click_reply_button
find(".topic-footer-main-buttons > .create").click
+ has_expanded_composer?
end
def has_expanded_composer?
has_css?("#reply-control.open")
end
+ def find_composer
+ find("#reply-control .d-editor .d-editor-input")
+ end
+
def type_in_composer(input)
- find("#reply-control .d-editor .d-editor-input").send_keys(input)
+ find_composer.send_keys(input)
+ end
+
+ def fill_in_composer(input)
+ find_composer.fill_in(with: input)
end
def clear_composer
- find("#reply-control .d-editor .d-editor-input").set("")
+ fill_in_composer("")
+ end
+
+ def has_composer_content?(content)
+ find_composer.value == content
end
def send_reply
find("#reply-control .save-or-cancel .create").click
end
+ def fill_in_composer_title(title)
+ find("#reply-title").fill_in(with: title)
+ end
+
private
def topic_footer_button_id(button)