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 8139d3972b..14d3da398c 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 ee754a1e75..ad47bb3ac0 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js
@@ -85,6 +85,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 adbe0756a4..17092b642b 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 has_message?(text: nil, id: nil)
if text
has_css?(".chat-message-text", text: text)
diff --git a/plugins/chat/spec/system/transcript_spec.rb b/plugins/chat/spec/system/transcript_spec.rb
new file mode 100644
index 0000000000..90f2ecd9f2
--- /dev/null
+++ b/plugins/chat/spec/system/transcript_spec.rb
@@ -0,0 +1,243 @@
+# 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) }
+ fab!(:chat_message_1) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:chat_message_2) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:chat_message_3) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:chat_message_4) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:chat_message_5) { Fabricate(:chat_message, chat_channel: chat_channel_1) }
+ fab!(:topic) { Fabricate(:with_posts_topic) }
+
+ 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(current_user, [chat_channel_1])
+ sign_in(current_user)
+ end
+
+ def select_message(message, mobile: false)
+ if page.has_css?(".chat-message-container.selecting-messages")
+ chat_channel_page.message_by_id(message.id).find(".chat-message-selector").click
+ else
+ # we long press instead of hover on mobile
+ if mobile
+ chat_channel_page.message_by_id(message.id).click(delay: 0.5)
+ else
+ chat_channel_page.message_by_id(message.id).hover
+ end
+
+ # we also have a different actions menu on mobile
+ if mobile
+ find(".chat-message-action-item[data-id=\"selectMessage\"]").click
+ else
+ 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
+ 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(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! }
+
+ it "quotes a single chat message into a topic " do
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ clip_text = copy_messages_to_clipboard(chat_message_5)
+
+ # post transcript in topic
+ topic_page.visit_topic_and_open_composer(topic)
+ topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
+ expect(page).to have_css(".d-editor-preview .chat-transcript")
+ topic_page.send_reply
+ expect(page).to have_content("This is a new post!")
+ within topic_page.post_by_number(topic.posts.reload.last.post_number) do
+ expect(page).to have_css(".chat-transcript")
+ end
+ end
+
+ it "quotes multiple chat messages into a topic" do
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ messages = [chat_message_5, chat_message_4, chat_message_3, chat_message_2]
+ clip_text = copy_messages_to_clipboard(messages)
+
+ # post transcript in topic
+ topic_page.visit_topic_and_open_composer(topic)
+ topic_page.fill_in_composer("This is a new post!\n\n" + clip_text)
+ expect(page).to have_css(".d-editor-preview .chat-transcript", count: 4)
+ expect(page).to have_content("Originally sent in #{chat_channel_1.name}")
+ topic_page.send_reply
+ expect(page).to have_content("This is a new post!")
+ within topic_page.post_by_number(topic.posts.reload.last.post_number) do
+ expect(page).to have_css(".chat-transcript", count: 4)
+ end
+ end
+
+ it "does not error in preview when quoting a chat message with a onebox" do
+ Oneboxer.stubs(:preview).returns(
+ "",
+ )
+ chat_message_3.update!(message: "http://www.example.com/has-title.html")
+ chat_message_3.rebake!
+
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ clip_text = copy_messages_to_clipboard(chat_message_3)
+
+ # post transcript in topic
+ topic_page.visit_topic_and_open_composer(topic)
+ topic_page.fill_in_composer(clip_text)
+
+ within ".chat-transcript-messages" do
+ expect(page).to have_content("An interesting article")
+ end
+ end
+
+ it "quotes a single chat message into another chat message " do
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ # select message + copy to clipboard
+ clip_text = copy_messages_to_clipboard(chat_message_5)
+ click_selection_button("cancel")
+
+ # send transcript message in chat
+ chat_channel_page.fill_composer(clip_text)
+ chat_channel_page.click_send_message
+ message = nil
+ try_until_success do
+ message = ChatMessage.find_by(user: current_user, message: clip_text.chomp)
+ expect(message).not_to eq(nil)
+ end
+ expect(chat_channel_page).to have_message(id: message.id)
+ within chat_channel_page.message_by_id(message.id) do
+ expect(page).to have_css(".chat-transcript")
+ end
+ end
+ end
+
+ describe "quoting into a topic directly" do
+ it "opens the topic composer with the quote prefilled and the channel category preselected" do
+ topic.update!(category: chat_channel_1.chatable)
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ # select message + prefill in composer
+ select_message(chat_message_5)
+ click_selection_button("quote")
+ expect(topic_page).to have_expanded_composer
+ expect(topic_page).to have_composer_content(generate_transcript(chat_message_5, 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)
+
+ # create the topic with the transcript as the OP
+ topic_page.fill_in_composer_title("Some topic title for testing")
+ topic_page.send_reply
+ expect(page).to have_content("Some topic title for testing")
+ topic = Topic.where(user: current_user).last
+ expect(page).to have_current_path(topic.url)
+ within topic_page.post_by_number(1) do
+ expect(page).to have_css(".chat-transcript")
+ end
+
+ # ensure the transcript date is formatted
+ expect(page).to have_css(".chat-transcript-datetime a[data-date-formatted=\"true\"]")
+ 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
+ topic.update!(category: chat_channel_1.chatable)
+ chat_page.visit_channel(chat_channel_1)
+ expect(chat_channel_page).to have_no_loading_skeleton
+ expect(chat_channel_page).to have_content(chat_message_5.message)
+
+ # select message + prefill in composer
+ select_message(chat_message_5, mobile: true)
+ click_selection_button("quote")
+
+ expect(topic_page).to have_expanded_composer
+ expect(topic_page).to have_composer_content(
+ generate_transcript(chat_message_5, 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/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js b/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js
deleted file mode 100644
index 63569a12ad..0000000000
--- a/plugins/chat/test/javascripts/acceptance/chat-quoting-test.js
+++ /dev/null
@@ -1,183 +0,0 @@
-import { skip, test } from "qunit";
-import {
- click,
- currentURL,
- tap,
- triggerEvent,
- visit,
-} from "@ember/test-helpers";
-import {
- acceptance,
- exists,
- loggedInUser,
- query,
- visible,
-} from "discourse/tests/helpers/qunit-helpers";
-import {
- chatChannels,
- generateChatView,
-} from "discourse/plugins/chat/chat-fixtures";
-import selectKit from "discourse/tests/helpers/select-kit-helper";
-
-const quoteResponse = {
- markdown: `[chat quote="martin-chat;3875498;2022-02-04T01:12:15Z" channel="The Beam Discussions" channelId="1234"]
- an extremely insightful response :)
- [/chat]`,
-};
-
-function setupPretenders(server, helper) {
- server.get("/chat/chat_channels.json", () => helper.response(chatChannels));
- server.post(`/chat/4/quote.json`, () => helper.response(quoteResponse));
- server.post(`/chat/7/quote.json`, () => helper.response(quoteResponse));
- server.get("/chat/:chat_channel_id/messages.json", () =>
- helper.response(generateChatView(loggedInUser()))
- );
- server.post("/uploads/lookup-urls", () => {
- return helper.response([]);
- });
-}
-
-acceptance("Discourse Chat | Copying messages", function (needs) {
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: true,
- has_chat_enabled: true,
- });
-
- needs.settings({
- chat_enabled: true,
- });
-
- needs.pretender((server, helper) => {
- setupPretenders(server, helper);
- });
-
- test("it copies the quote and shows a message", async function (assert) {
- await visit("/chat/channel/7/Bug");
- assert.ok(exists(".chat-message-container"));
-
- const firstMessage = query(".chat-message-container");
- await triggerEvent(firstMessage, "mouseenter");
- const dropdown = selectKit(
- `.chat-message-actions-container[data-id="${firstMessage.dataset.id}"] .more-buttons`
- );
- await dropdown.expand();
- await dropdown.selectRowByValue("selectMessage");
- assert.ok(firstMessage.classList.contains("selecting-messages"));
-
- const copyButton = query(".chat-live-pane #chat-copy-btn");
- assert.equal(
- copyButton.disabled,
- false,
- "button is enabled as a message is selected"
- );
-
- await click(firstMessage.querySelector("input[type='checkbox']"));
- assert.equal(
- copyButton.disabled,
- true,
- "button is disabled when no messages are selected"
- );
-
- await click(firstMessage.querySelector("input[type='checkbox']"));
- await click("#chat-copy-btn");
- assert.ok(exists(".chat-selection-message"), "shows the message");
- });
-});
-
-acceptance("Discourse Chat | Quoting in composer", async function (needs) {
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: true,
- has_chat_enabled: true,
- });
-
- needs.settings({
- chat_enabled: true,
- });
-
- needs.pretender((server, helper) => {
- setupPretenders(server, helper);
- });
-
- skip("it opens the composer for the topic and pastes in the quote", async function (assert) {
- await visit("/t/internationalization-localization/280");
-
- await click(".header-dropdown-toggle.open-chat");
- assert.ok(visible(".chat-drawer-container"), "chat drawer is open");
- assert.ok(exists(".chat-message-container"));
-
- const firstMessage = query(".chat-message-container");
- await triggerEvent(firstMessage, "mouseenter");
- const dropdown = selectKit(".chat-message-container .more-buttons");
- await dropdown.expand();
- await dropdown.selectRowByValue("selectMessage");
- assert.ok(firstMessage.classList.contains("selecting-messages"));
-
- await click("#chat-quote-btn");
- assert.ok(exists("#reply-control.composer-action-reply"));
- assert.strictEqual(
- query(".composer-action-title .action-title").innerText,
- "Internationalization / localization"
- );
- assert.strictEqual(
- query("textarea.d-editor-input").value,
- quoteResponse.markdown
- );
- });
-});
-
-acceptance("Discourse Chat | Quoting on mobile", async function (needs) {
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: true,
- has_chat_enabled: true,
- });
-
- needs.settings({
- chat_enabled: true,
- });
-
- needs.pretender((server, helper) => {
- setupPretenders(server, helper);
- });
-
- needs.mobileView();
-
- skip("it opens the chatable, opens the composer, and pastes the markdown in", async function (assert) {
- await visit("/chat/channel/7/Bug");
- assert.ok(exists(".chat-message-container"));
-
- const firstMessage = query(".chat-message-container");
- await tap(firstMessage);
- await click(".chat-message-action-item[data-id='selectMessage'] button");
- assert.ok(firstMessage.classList.contains("selecting-messages"));
-
- await click("#chat-quote-btn");
-
- assert.equal(currentURL(), "/c/bug/1", "navigates to the chatable url");
- assert.ok(
- exists("#reply-control.composer-action-createTopic"),
- "the composer opens"
- );
- assert.strictEqual(
- query("textarea.d-editor-input").value,
- quoteResponse.markdown,
- "the composer has the markdown"
- );
- assert.strictEqual(
- selectKit(".category-chooser").header().value(),
- "1",
- "it fills category selector with the right category"
- );
- });
-});
diff --git a/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js b/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js
deleted file mode 100644
index 98ff2a6b8b..0000000000
--- a/plugins/chat/test/javascripts/acceptance/chat-transcript-test.js
+++ /dev/null
@@ -1,597 +0,0 @@
-import PrettyText, { buildOptions } from "pretty-text/pretty-text";
-import { emojiUnescape } from "discourse/lib/text";
-import I18n from "I18n";
-import topicFixtures from "discourse/tests/fixtures/topic";
-import { cloneJSON, deepMerge } from "discourse-common/lib/object";
-import QUnit, { test } from "qunit";
-
-import { click, fillIn, visit } from "@ember/test-helpers";
-import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
-
-const rawOpts = {
- siteSettings: {
- enable_emoji: true,
- enable_emoji_shortcuts: true,
- enable_mentions: true,
- emoji_set: "twitter",
- external_emoji_url: "",
- highlighted_languages: "json|ruby|javascript",
- default_code_lang: "auto",
- enable_markdown_linkify: true,
- markdown_linkify_tlds: "com",
- chat_enabled: true,
- },
- getURL: (url) => url,
-};
-
-function cookMarkdown(input, opts) {
- const merged = deepMerge({}, rawOpts, opts);
- return new PrettyText(buildOptions(merged)).cook(input);
-}
-
-QUnit.assert.cookedChatTranscript = function (input, opts, expected, message) {
- const actual = cookMarkdown(input, opts);
- this.pushResult({
- result: actual === expected,
- actual,
- expected,
- message,
- });
-};
-
-function generateTranscriptHTML(messageContent, opts) {
- const channelDataAttr = opts.channel
- ? ` data-channel-name=\"${opts.channel}\"`
- : "";
- const channelIdDataAttr = opts.channelId
- ? ` data-channel-id=\"${opts.channelId}\"`
- : "";
- const reactDataAttr = opts.reactions
- ? ` data-reactions=\"${opts.reactionsAttr}\"`
- : "";
-
- let tabIndexHTML = opts.linkTabIndex ? ' tabindex="-1"' : "";
-
- let transcriptClasses = ["chat-transcript"];
- if (opts.chained) {
- transcriptClasses.push("chat-transcript-chained");
- }
-
- const transcript = [];
- transcript.push(
- `
`
- );
-
- if (opts.channel && opts.multiQuote) {
- let originallySent = I18n.t("chat.quote.original_channel", {
- channel: opts.channel,
- channelLink: `/chat/channel/${opts.channelId}/-`,
- });
- if (opts.linkTabIndex) {
- originallySent = originallySent.replace(">", tabIndexHTML + ">");
- }
- transcript.push(`
-${originallySent}
`);
- }
-
- const dateTimeText = opts.showDateTimeText
- ? moment
- .tz(opts.datetime, opts.timezone)
- .format(I18n.t("dates.long_no_year"))
- : "";
-
- const innerDatetimeEl =
- opts.noLink || !opts.channelId
- ? `
${dateTimeText}`
- : `
${dateTimeText}`;
- transcript.push(`
-
-
-${opts.username}
-
-${innerDatetimeEl}
`);
-
- if (opts.channel && !opts.multiQuote) {
- transcript.push(
- `
-#${opts.channel}`
- );
- } else {
- transcript.push("
");
- }
-
- let messageHtml = `\n${messageContent}`;
-
- if (opts.reactions) {
- let reactionsHtml = [`
\n`];
- opts.reactions.forEach((react) => {
- reactionsHtml.push(
- `
\n${emojiUnescape(
- `:${react.emoji}:`,
- { lazy: true }
- ).replace(/'/g, '"')} ${react.usernames.length}
\n`
- );
- });
- reactionsHtml.push(`
\n`);
- messageHtml += reactionsHtml.join("");
- }
- transcript.push(`${messageHtml}
`);
- transcript.push("");
- return transcript.join("\n");
-}
-
-// these are both set by the plugin with Site.markdown_additional_options which we can't really
-// modify the response for here, source of truth are consts in ChatMessage::MARKDOWN_FEATURES
-// and ChatMessage::MARKDOWN_IT_RULES
-function buildAdditionalOptions() {
- return {
- chat: {
- limited_pretty_text_features: [
- "anchor",
- "bbcode-block",
- "bbcode-inline",
- "code",
- "category-hashtag",
- "censored",
- "discourse-local-dates",
- "emoji",
- "emojiShortcuts",
- "inlineEmoji",
- "html-img",
- "mentions",
- "onebox",
- "text-post-process",
- "upload-protocol",
- "watched-words",
- "table",
- "spoiler-alert",
- ],
- limited_pretty_text_markdown_rules: [
- "autolink",
- "list",
- "backticks",
- "newline",
- "code",
- "fence",
- "table",
- "linkify",
- "link",
- "strikethrough",
- "blockquote",
- "emphasis",
- ],
- hashtag_configurations: {
- "chat-composer": ["channel", "category", "tag"],
- },
- },
- };
-}
-
-acceptance("Discourse Chat | chat-transcript", function (needs) {
- let additionalOptions = buildAdditionalOptions();
-
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: false,
- has_chat_enabled: false,
- timezone: "Australia/Brisbane",
- });
-
- needs.settings({
- emoji_set: "twitter",
- });
-
- test("works with a minimal quote bbcode block", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }),
- "renders the chat message with the required CSS classes and attributes"
- );
- });
-
- test("renders the channel name if provided with multiQuote", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- multiQuote: true,
- timezone: "Australia/Brisbane",
- }),
- "renders the chat transcript with the channel name included above the user and datetime"
- );
- });
-
- test("renders the channel name if provided without multiQuote", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- timezone: "Australia/Brisbane",
- }),
- "renders the chat transcript with the channel name included next to the datetime"
- );
- });
-
- test("renders with the chained attribute for more compact quotes", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" chained="true"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- multiQuote: true,
- chained: true,
- timezone: "Australia/Brisbane",
- }),
- "renders with the chained attribute"
- );
- });
-
- test("renders with the noLink attribute to remove the links to the individual messages from the datetimes", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" noLink="true"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- multiQuote: true,
- noLink: true,
- timezone: "Australia/Brisbane",
- }),
- "renders with the noLink attribute"
- );
- });
-
- test("renders with the reactions attribute", function (assert) {
- const reactionsAttr = "+1:martin;heart:martin,eviltrout";
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" reactions="${reactionsAttr}"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML("This is a chat message.
", {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- timezone: "Australia/Brisbane",
- reactionsAttr,
- reactions: [
- { emoji: "+1", usernames: ["martin"] },
- { emoji: "heart", usernames: ["martin", "eviltrout"] },
- ],
- }),
- "renders with the reaction data attribute and HTML"
- );
- });
-
- test("renders with minimal markdown rules inside the quote bbcode block, same as server-side chat messages", function (assert) {
- assert.cookedChatTranscript(
- `[chat quote="johnsmith;450;2021-04-25T05:40:39Z"]
-[quote="martin, post:3, topic:6215"]
-another cool reply
-[/quote]
-[/chat]`,
- { additionalOptions },
- generateTranscriptHTML(
- `[quote="martin, post:3, topic:6215"]
-another cool reply
-[/quote]
`,
- {
- messageId: "450",
- username: "johnsmith",
- datetime: "2021-04-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "does not render the markdown feature that has been excluded"
- );
-
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis ~~does work~~ with removed _rules_.\n\n* list item 1\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML(
- `This does work with removed rules.
-`,
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "renders correctly when the rule has not been excluded"
- );
-
- additionalOptions.chat.limited_pretty_text_markdown_rules = [
- "autolink",
- // "list",
- "backticks",
- "newline",
- "code",
- "fence",
- "table",
- "linkify",
- "link",
- // "strikethrough",
- "blockquote",
- // "emphasis",
- ];
-
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis ~~does work~~ with removed _rules_.\n\n* list item 1\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML(
- `This ~~does work~~ with removed _rules_.
-* list item 1
`,
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "renders correctly with some obvious rules excluded (list/strikethrough/emphasis)"
- );
-
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nhere is a message :P with category hashtag #test\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML(
- `here is a message
with category hashtag #test
`,
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "renders correctly when the feature has not been excluded"
- );
-
- additionalOptions.chat.limited_pretty_text_features = [
- "anchor",
- "bbcode-block",
- "bbcode-inline",
- "code",
- // "category-hashtag",
- "censored",
- "discourse-local-dates",
- "emoji",
- // "emojiShortcuts",
- "inlineEmoji",
- "html-img",
- "mentions",
- "onebox",
- "text-post-process",
- "upload-protocolrouter.location.setURL",
- "watched-words",
- "table",
- "spoiler-alert",
- ];
-
- assert.cookedChatTranscript(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nhere is a message :P with category hashtag #test\n[/chat]`,
- { additionalOptions },
- generateTranscriptHTML(
- `here is a message :P with category hashtag #test
`,
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "renders correctly with some obvious features excluded (category-hashtag, emojiShortcuts)"
- );
-
- assert.cookedChatTranscript(
- `This ~~does work~~ with removed _rules_.
-
-* list item 1
-
-here is a message :P with category hashtag #test
-
-[chat quote="martin;2321;2022-01-25T05:40:39Z"]
-This ~~does work~~ with removed _rules_.
-
-* list item 1
-
-here is a message :P with category hashtag #test
-[/chat]`,
- { additionalOptions },
- `This does work with removed rules.
-
-here is a message
with category hashtag #test
\n` +
- generateTranscriptHTML(
- `This ~~does work~~ with removed _rules_.
-* list item 1
-here is a message :P with category hashtag #test
`,
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- timezone: "Australia/Brisbane",
- }
- ),
- "the rule changes do not apply outside the BBCode [chat] block"
- );
- });
-});
-
-acceptance(
- "Discourse Chat | chat-transcript date decoration",
- function (needs) {
- let additionalOptions = buildAdditionalOptions();
-
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: true,
- has_chat_enabled: true,
- timezone: "Australia/Brisbane",
- });
- needs.settings({
- chat_enabled: true,
- });
-
- needs.pretender((server, helper) => {
- server.get("/chat/chat_channels.json", () =>
- helper.response({
- public_channels: [],
- direct_message_channels: [],
- message_bus_last_ids: {
- channel_metadata: 0,
- channel_edits: 0,
- channel_status: 0,
- new_channel: 0,
- user_tracking_state: 0,
- },
- })
- );
-
- const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
- const firstPost = topicResponse.post_stream.posts[0];
- const postCooked = cookMarkdown(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions }
- );
- firstPost.cooked += postCooked;
-
- server.get("/t/280.json", () => helper.response(topicResponse));
- });
-
- test("chat transcript datetimes are formatted into the link with decorateCookedElement", async function (assert) {
- await visit("/t/-/280");
-
- assert.strictEqual(
- query(".chat-transcript-datetime span").innerText.trim(),
- moment
- .tz("2022-01-25T05:40:39Z", "Australia/Brisbane")
- .format(I18n.t("dates.long_no_year")),
- "it decorates the chat transcript datetime link with a formatted date"
- );
- });
- }
-);
-
-acceptance(
- "Discourse Chat - chat-transcript - Composer Oneboxes ",
- function (needs) {
- let additionalOptions = buildAdditionalOptions();
- needs.user({
- admin: false,
- moderator: false,
- username: "eviltrout",
- id: 1,
- can_chat: true,
- has_chat_enabled: true,
- timezone: "Australia/Brisbane",
- });
- needs.settings({
- chat_enabled: true,
- enable_markdown_linkify: true,
- max_oneboxes_per_post: 2,
- });
- needs.pretender((server, helper) => {
- server.get("/chat/chat_channels.json", () =>
- helper.response({
- public_channels: [],
- direct_message_channels: [],
- message_bus_last_ids: {
- channel_metadata: 0,
- channel_edits: 0,
- channel_status: 0,
- new_channel: 0,
- user_tracking_state: 0,
- },
- })
- );
-
- const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
- const firstPost = topicResponse.post_stream.posts[0];
- const postCooked = cookMarkdown(
- `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`,
- { additionalOptions }
- );
- firstPost.cooked += postCooked;
-
- server.get("/t/280.json", () => helper.response(topicResponse));
- });
-
- test("Preview should not error for oneboxes within [chat] bbcode", async function (assert) {
- await visit("/t/internationalization-localization/280");
- await click("#topic-footer-buttons .btn.create");
-
- await fillIn(
- ".d-editor-input",
- `
-[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"]
-http://www.example.com/has-title.html
-[/chat]`
- );
-
- const rendered = generateTranscriptHTML(
- '',
- {
- messageId: "2321",
- username: "martin",
- datetime: "2022-01-25T05:40:39Z",
- channel: "Cool Cats Club",
- channelId: "1234",
- multiQuote: true,
- linkTabIndex: true,
- showDateTimeText: true,
- timezone: "Australia/Brisbane",
- }
- );
-
- assert.strictEqual(
- query(".d-editor-preview").innerHTML.trim(),
- rendered.trim(),
- "it renders correctly with the onebox inside the [chat] bbcode"
- );
-
- const textarea = query("#reply-control .d-editor-input");
- await fillIn(".d-editor-input", textarea.value + "\nA");
- assert.ok(
- query(".d-editor-preview").innerHTML.trim().includes("\nA
"),
- "it does not error with a opts.discourse.hoisted error in the markdown pipeline when typing more text"
- );
- });
- }
-);
diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb
index b65cd48d7f..3254d71dab 100644
--- a/spec/fabricators/topic_fabricator.rb
+++ b/spec/fabricators/topic_fabricator.rb
@@ -20,6 +20,14 @@ Fabricator(:banner_topic, from: :topic) do
archetype Archetype.banner
end
+Fabricator(:with_posts_topic, from: :topic) do
+ after_create do |topic|
+ 3.times do
+ Fabricate(:post, topic: topic)
+ end
+ end
+end
+
Fabricator(:private_message_topic, from: :topic) do
transient :recipient
category_id { nil }
diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb
index 5815fb760c..dd8670a5cc 100644
--- a/spec/system/page_objects/pages/topic.rb
+++ b/spec/system/page_objects/pages/topic.rb
@@ -80,18 +80,34 @@ module PageObjects
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.set(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
within("#reply-control") { find(".save-or-cancel .create").click }
end
+ def fill_in_composer_title(title)
+ find("#reply-title").set(title)
+ end
+
private
def topic_footer_button_id(button)