FEATURE: Add threads support to chat archives (#24325)

This PR introduces thread support for channel archives. Now, threaded messages are rendered inside a `details` HTML tag in posts.

The transcript markdown rules now support two new attributes: `threadId` and `threadTitle`.

- If `threadId` is present, all nested `chat` tags are rendered inside the first one.
- `threadTitle` (optional) defines the summary content.

```
[chat threadId=19 ... ]
thread OM

  [chat ... ]
  thread reply
  [/chat]

[/chat]
```

If threads are split across multiple posts when archiving, the range of messages in each part will be displayed alongside the thread title. For example: `(message 1 to 16 of 20)` and `(message 17 to 20 of 20)`.
This commit is contained in:
Jan Cernik 2023-11-27 11:47:35 -03:00 committed by GitHub
parent d506721eee
commit ac9e804dbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 673 additions and 41 deletions

View File

@ -20,6 +20,8 @@ const chatTranscriptRule = {
const noLink = !!tagInfo.attrs.noLink;
const channelName = tagInfo.attrs.channel;
const channelId = tagInfo.attrs.channelId;
const threadId = tagInfo.attrs.threadId;
const threadTitle = tagInfo.attrs.threadTitle;
const channelLink = channelId
? options.getURL(`/chat/c/-/${channelId}`)
: null;
@ -29,6 +31,78 @@ const chatTranscriptRule = {
}
let wrapperDivToken = state.push("div_chat_transcript_wrap", "div", 1);
if (channelName && multiQuote) {
let metaDivToken = state.push("div_chat_transcript_meta", "div", 1);
metaDivToken.attrs = [["class", "chat-transcript-meta"]];
const channelToken = state.push("html_inline", "", 0);
const unescapedChannelName = performEmojiUnescape(channelName, {
getURL: options.getURL,
emojiSet: options.emojiSet,
emojiCDNUrl: options.emojiCDNUrl,
enableEmojiShortcuts: options.enableEmojiShortcuts,
inlineEmoji: options.inlineEmoji,
lazy: true,
});
channelToken.content = I18n.t("chat.quote.original_channel", {
channel: unescapedChannelName,
channelLink,
});
state.push("div_chat_transcript_meta", "div", -1);
}
if (threadId) {
state.push("details_chat_transcript_wrap_open", "details", 1);
state.push("summary_chat_transcript_open", "summary", 1);
const threadToken = state.push("div_thread_open", "div", 1);
threadToken.attrs = [["class", "chat-transcript-thread"]];
const threadHeaderToken = state.push("div_thread_header_open", "div", 1);
threadHeaderToken.attrs = [["class", "chat-transcript-thread-header"]];
const thread_svg = state.push("svg_thread_header_open", "svg", 1);
thread_svg.block = false;
thread_svg.attrs = [
["class", "fa d-icon d-icon-discourse-threads svg-icon svg-node"],
];
state.push(thread_svg);
let thread_use = state.push("use_svg_thread_open", "use", 1);
thread_use.block = false;
thread_use.attrs = [["href", "#discourse-threads"]];
state.push(thread_use);
state.push(state.push("use_svg_thread_close", "use", -1));
state.push(state.push("svg_thread_header_close", "svg", -1));
const threadTitleContainerToken = state.push(
"span_thread_title_open",
"span",
1
);
threadTitleContainerToken.attrs = [
["class", "chat-transcript-thread-header__title"],
];
const threadTitleToken = state.push("html_inline", "", 0);
const unescapedThreadTitle = performEmojiUnescape(threadTitle, {
getURL: options.getURL,
emojiSet: options.emojiSet,
emojiCDNUrl: options.emojiCDNUrl,
enableEmojiShortcuts: options.enableEmojiShortcuts,
inlineEmoji: options.inlineEmoji,
lazy: true,
});
threadTitleToken.content = unescapedThreadTitle
? unescapedThreadTitle
: I18n.t("chat.quote.default_thread_title");
state.push("span_thread_title_close", "span", -1);
state.push("div_thread_header_close", "div", -1);
}
let wrapperClasses = ["chat-transcript"];
if (!!tagInfo.attrs.chained) {
@ -46,17 +120,6 @@ const chatTranscriptRule = {
if (channelName) {
wrapperDivToken.attrs.push(["data-channel-name", channelName]);
if (multiQuote) {
let metaDivToken = state.push("div_chat_transcript_meta", "div", 1);
metaDivToken.attrs = [["class", "chat-transcript-meta"]];
const channelToken = state.push("html_inline", "", 0);
channelToken.content = I18n.t("chat.quote.original_channel", {
channel: channelName,
channelLink,
});
state.push("div_chat_transcript_meta", "div", -1);
}
}
if (channelId) {
@ -117,6 +180,17 @@ const chatTranscriptRule = {
spanToken.attrs = [["title", messageTimeStart]];
spanToken.block = false;
if (channelName && !multiQuote) {
let channelLinkToken = state.push("link_open", "a", 1);
channelLinkToken.attrs = [
["class", "chat-transcript-channel"],
["href", channelLink],
];
let inlineTextToken = state.push("html_inline", "", 0);
inlineTextToken.content = `#${channelName}`;
channelLinkToken = state.push("link_close", "a", -1);
channelLinkToken.block = false;
}
spanToken = state.push("span_close", "span", -1);
spanToken.block = false;
} else {
@ -153,11 +227,32 @@ const chatTranscriptRule = {
let messagesToken = state.push("div_chat_transcript_messages", "div", 1);
messagesToken.attrs = [["class", "chat-transcript-messages"]];
// rendering chat message content with limited markdown rule subset
const token = state.push("html_raw", "", 1);
if (threadId) {
const regex = /\[chat/i;
const match = regex.exec(content);
token.content = customMarkdownCookFn(content);
state.push("html_raw", "", -1);
if (match) {
const threadToken = state.push("html_raw", "", 1);
threadToken.content = customMarkdownCookFn(
content.substring(0, match.index)
);
state.push("html_raw", "", -1);
state.push("div_thread_close", "div", -1);
state.push("summary_chat_transcript_close", "summary", -1);
const token = state.push("html_raw", "", 1);
token.content = customMarkdownCookFn(content.substring(match.index));
state.push("html_raw", "", -1);
state.push("details_chat_transcript_wrap_close", "details", -1);
}
} else {
// rendering chat message content with limited markdown rule subset
const token = state.push("html_raw", "", 1);
token.content = customMarkdownCookFn(content);
state.push("html_raw", "", -1);
}
if (reactions) {
let emojiHtmlCache = {};
@ -202,8 +297,12 @@ const chatTranscriptRule = {
export function setup(helper) {
helper.allowList([
"svg[class=fa d-icon d-icon-discourse-threads svg-icon svg-node]",
"use[href=#discourse-threads]",
"div[class=chat-transcript]",
"details[class=chat-transcript]",
"div[class=chat-transcript chat-transcript-chained]",
"details[class=chat-transcript chat-transcript-chained]",
"div.chat-transcript-meta",
"div.chat-transcript-user",
"div.chat-transcript-username",
@ -219,6 +318,9 @@ export function setup(helper) {
"div[data-username]",
"div[data-datetime]",
"a.chat-transcript-channel",
"div.chat-transcript-thread",
"div.chat-transcript-thread-header",
"span.chat-transcript-thread-header__title",
]);
helper.registerOptions((opts, siteSettings) => {

View File

@ -17,17 +17,21 @@
border-bottom: 0;
}
details > .chat-transcript-chained:first-of-type {
margin-top: 0.5rem;
}
.chat-transcript-meta {
color: var(--primary-high);
font-size: var(--font-down-2-rem);
border-bottom: 1px solid var(--primary-low);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
}
.chat-transcript-channel,
.chat-transcript-thread {
font-size: var(--font-down-1-rem);
img.emoji {
height: 1.1em;
width: 1.1em;
}
}
.chat-transcript-separator {
@ -61,6 +65,26 @@
}
}
.topic-body .cooked & {
> details {
padding: 0.5rem;
> summary {
display: flex;
&::before {
line-height: 1;
}
.chat-transcript-thread {
&-header {
margin-bottom: 0.5rem;
}
}
}
}
}
.chat-transcript-user-avatar .avatar {
aspect-ratio: 20 / 20;
}
@ -81,6 +105,8 @@
.chat-transcript-reaction {
@include chat-reaction;
pointer-events: none;
}
}

View File

@ -445,6 +445,7 @@ en:
quote:
original_channel: 'Originally sent in <a href="%{channelLink}">%{channel}</a>'
copy_success: "Chat quote copied to clipboard"
default_thread_title: "Thread"
notification_levels:
never: "Never"

View File

@ -197,6 +197,10 @@ en:
summaries:
no_targets: "There were no messages during the selected period."
transcript:
default_thread_title: "Thread"
split_thread_range: "messages %{start} to %{end} of %{total}"
discourse_push_notifications:
popup:
chat_mention:

View File

@ -85,6 +85,7 @@ module Chat
@chat_channel_archive = chat_channel_archive
@chat_channel = chat_channel_archive.chat_channel
@chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
@archived_messages_ids = []
end
def execute
@ -107,22 +108,88 @@ module Chat
# Another future improvement is to send a MessageBus message for each
# completed batch, so the UI can receive updates and show a progress
# bar or something similar.
buffer = []
batch_thread_ranges = {}
chat_channel
.chat_messages
.find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |chat_messages|
create_post(
Chat::TranscriptService.new(
chat_channel,
chat_channel_archive.archived_by,
messages_or_ids: chat_messages,
opts: {
no_link: true,
include_reactions: true,
},
).generate_markdown,
) { delete_message_batch(chat_messages.map(&:id)) }
.order("created_at ASC")
.find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |message_batch|
thread_ids = message_batch.map(&:thread_id).compact.uniq
threads =
chat_channel
.chat_messages
.where(
thread_id:
Chat::Message
.select(:thread_id)
.where(thread_id: thread_ids)
.group(:thread_id)
.having("count(*) > 1"),
)
.order("created_at ASC")
.to_a
full_batch = (buffer + message_batch + threads).uniq { |msg| msg.id }
message_chunk = full_batch.group_by { |msg| msg.thread_id || msg.id }.values.flatten
buffer.clear
if message_chunk.size > ARCHIVED_MESSAGES_PER_POST
post_last_message = message_chunk[ARCHIVED_MESSAGES_PER_POST - 1]
thread = threads.select { |msg| msg.thread_id == post_last_message.thread_id }
thread_om = thread.first
if !thread_om.nil?
thread_ranges =
calculate_thread_ranges(message_chunk, thread, thread_om, post_last_message)
end
end
batch = []
batch_thread_added = false
message_chunk.each do |message|
# When a thread spans across multiple posts and the first message is part of a thread in
# a previous post, we need to duplicate the original message to give context to the user.
if thread_om.present?
if batch.empty? && message_chunk.size > ARCHIVED_MESSAGES_PER_POST &&
message&.thread_id == thread_om&.thread_id && message != thread_om
batch << thread_om
# We determine the correct range for the current part of the thread.
batch_thread_ranges[thread_om.id] = thread_ranges[message.thread_id].first
thread_ranges[message.thread_id].slice!(0)
elsif thread_ranges.has_key?(message.thread_id) &&
thread_ranges[message.thread_id].present? && batch_thread_added == false
# We determine the correct range for the current part of the thread.
batch_thread_ranges[thread_om.id] = thread_ranges[message.thread_id].first
thread_ranges[message.thread_id].slice!(0)
batch_thread_added = true
end
end
if message == thread_om && batch.size + 1 >= ARCHIVED_MESSAGES_PER_POST
batch_size = batch.size + 1
else
batch << message
batch_size = batch.size
end
if batch_size >= ARCHIVED_MESSAGES_PER_POST
create_post_from_batch(batch, batch_thread_ranges)
batch.clear
end
end
buffer += batch
end
create_post_from_batch(buffer, batch_thread_ranges) unless buffer.empty?
kick_all_users
complete_archive
rescue => err
@ -133,6 +200,63 @@ module Chat
private
# It's used to call the TranscriptService, which will
# generate the markdown for a given set of messages.
def create_post_from_batch(chat_messages, batch_thread_ranges)
create_post(
Chat::TranscriptService.new(
chat_channel,
chat_channel_archive.archived_by,
messages_or_ids: chat_messages,
thread_ranges: batch_thread_ranges,
opts: {
no_link: true,
include_reactions: true,
},
).generate_markdown,
) { delete_message_batch(chat_messages.map(&:id)) }
end
# Message batches can be greater than the maximum number of messages
# per post if we also include threads. This is used to calculate all
# the ranges when we split the threads that are included in the batch.
def calculate_thread_ranges(message_chunk, thread, thread_om, post_last_message)
ranges = {}
thread_size = thread.size - 1
last_thread_index = 0
iterations = (message_chunk.size.to_f / (ARCHIVED_MESSAGES_PER_POST - 1)).ceil
iterations.times do |index|
if last_thread_index != thread_size
if index == 0
thread_index = thread.index(post_last_message)
else
next_post_last_message =
message_chunk[(ARCHIVED_MESSAGES_PER_POST * (index + 1)) - index]
if next_post_last_message&.thread_id == post_last_message&.thread_id
thread_index = last_thread_index + ARCHIVED_MESSAGES_PER_POST - 1
else
thread_index = thread_size
end
end
range =
I18n.t(
"chat.transcript.split_thread_range",
start: last_thread_index + 1,
end: thread_index,
total: thread_size,
)
ranges[thread_om.thread_id] ||= []
ranges[thread_om.thread_id] << range
last_thread_index = thread_index
end
end
ranges
end
def create_post(raw)
pc = nil
Post.transaction do
@ -228,9 +352,8 @@ module Chat
deleted_by_id: chat_channel_archive.archived_by.id,
)
chat_channel_archive.update!(
archived_messages: chat_channel_archive.archived_messages + message_ids.length,
)
@archived_messages_ids = (@archived_messages_ids + message_ids).uniq
chat_channel_archive.update!(archived_messages: @archived_messages_ids.length)
end
Rails.logger.info(

View File

@ -21,7 +21,13 @@ module Chat
NO_LINK_ATTR = "noLink=\"true\""
class TranscriptBBCode
attr_reader :channel, :multiquote, :chained, :no_link, :include_reactions
attr_reader :channel,
:multiquote,
:chained,
:no_link,
:include_reactions,
:thread_id,
:thread_ranges
def initialize(
channel: nil,
@ -29,7 +35,9 @@ module Chat
multiquote: false,
chained: false,
no_link: false,
include_reactions: false
include_reactions: false,
thread_id: nil,
thread_ranges: {}
)
@channel = channel
@acting_user = acting_user
@ -37,13 +45,20 @@ module Chat
@chained = chained
@no_link = no_link
@include_reactions = include_reactions
@thread_ranges = thread_ranges
@message_data = []
@threads_markdown = {}
@thread_id = thread_id
end
def add(message:, reactions: nil)
@message_data << { message: message, reactions: reactions }
end
def add_thread_markdown(thread_id:, markdown:)
@threads_markdown[thread_id] = markdown
end
def render
attrs = [quote_attr(@message_data.first[:message])]
@ -57,15 +72,40 @@ module Chat
attrs << NO_LINK_ATTR if no_link
attrs << reactions_attr if include_reactions
if thread_id
attrs << thread_id_attr
attrs << thread_title_attr(@message_data.first[:message])
end
<<~MARKDOWN
[chat #{attrs.compact.join(" ")}]
#{@message_data.map { |msg| msg[:message].to_markdown }.join("\n\n")}
#{render_messages}
[/chat]
MARKDOWN
end
private
def render_messages
@message_data
.map do |msg_data|
rendered_message = msg_data[:message].to_markdown
if msg_data[:message].thread_id.present?
thread_data = @threads_markdown[msg_data[:message].thread_id]
if thread_data.present?
rendered_message + "\n\n" + thread_data
else
rendered_message
end
else
rendered_message
end
end
.join("\n\n")
end
def reactions_attr
reaction_data =
@message_data.reduce([]) do |array, msg_data|
@ -89,9 +129,23 @@ module Chat
def channel_id_attr
"channelId=\"#{channel.id}\""
end
def thread_id_attr
"threadId=\"#{thread_id}\""
end
def thread_title_attr(message)
thread = Chat::Thread.find(thread_id)
range = thread_ranges[message.id] if thread_ranges.has_key?(message.id)
thread_title =
thread.title.present? ? thread.title : I18n.t("chat.transcript.default_thread_title")
thread_title += " (#{range})" if range.present?
"threadTitle=\"#{thread_title}\""
end
end
def initialize(channel, acting_user, messages_or_ids: [], opts: {})
def initialize(channel, acting_user, messages_or_ids: [], thread_ranges: {}, opts: {})
@channel = channel
@acting_user = acting_user
@ -101,12 +155,15 @@ module Chat
@messages = messages_or_ids
end
@opts = opts
@thread_ranges = thread_ranges
end
def generate_markdown
previous_message = nil
rendered_markdown = []
rendered_thread_markdown = []
all_messages_same_user = messages.count(:user_id) == 1
open_bbcode_tag =
TranscriptBBCode.new(
channel: @channel,
@ -114,11 +171,19 @@ module Chat
multiquote: messages.length > 1,
chained: !all_messages_same_user,
no_link: @opts[:no_link],
thread_id: messages.first.thread_id,
thread_ranges: @thread_ranges,
include_reactions: @opts[:include_reactions],
)
messages.each.with_index do |message, idx|
if previous_message.present? && previous_message.user_id != message.user_id
group_messages(messages).each do |id, message_group|
message = message_group.first
if previous_message.present? &&
(
previous_message.user_id != message.user_id ||
previous_message.thread_id != message.thread_id
)
rendered_markdown << open_bbcode_tag.render
open_bbcode_tag =
@ -126,6 +191,8 @@ module Chat
acting_user: @acting_user,
chained: !all_messages_same_user,
no_link: @opts[:no_link],
thread_id: message.thread_id,
thread_ranges: @thread_ranges,
include_reactions: @opts[:include_reactions],
)
end
@ -135,7 +202,51 @@ module Chat
else
open_bbcode_tag.add(message: message)
end
previous_message = message
if message_group.length > 1
previous_thread_message = nil
rendered_thread_markdown.clear
thread_bbcode_tag =
TranscriptBBCode.new(
acting_user: @acting_user,
chained: !all_messages_same_user,
no_link: @opts[:no_link],
include_reactions: @opts[:include_reactions],
)
message_group[1..].each do |thread_message|
if previous_thread_message.present? &&
previous_thread_message.user_id != thread_message.user_id
rendered_thread_markdown << thread_bbcode_tag.render
thread_bbcode_tag =
TranscriptBBCode.new(
acting_user: @acting_user,
chained: !all_messages_same_user,
no_link: @opts[:no_link],
include_reactions: @opts[:include_reactions],
)
end
if @opts[:include_reactions]
thread_bbcode_tag.add(
message: thread_message,
reactions: reactions_for_message(thread_message),
)
else
thread_bbcode_tag.add(message: thread_message)
end
previous_thread_message = thread_message
end
rendered_thread_markdown << thread_bbcode_tag.render
end
open_bbcode_tag.add_thread_markdown(
thread_id: message_group.first.thread_id,
markdown: rendered_thread_markdown.join("\n"),
)
end
# tie off the last open bbcode + render
@ -145,6 +256,10 @@ module Chat
private
def group_messages(messages)
messages.group_by { |msg| msg.thread_id || msg.id }
end
def messages
@messages ||=
Chat::Message

View File

@ -90,6 +90,13 @@ describe Chat::ChannelArchiveService do
num.times { Fabricate(:chat_message, chat_channel: channel) }
end
def create_threaded_messages(num, title: nil)
original_message = Fabricate(:chat_message, chat_channel: channel)
thread =
Fabricate(:chat_thread, channel: channel, title: title, original_message: original_message)
(num - 1).times { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
end
def start_archive
@channel_archive =
described_class.create_archive_process(
@ -143,6 +150,61 @@ describe Chat::ChannelArchiveService do
expect(@channel_archive.chat_channel.chat_messages.count).to eq(0)
end
it "creates the correct posts for a channel with messages and threads" do
create_messages(2)
create_threaded_messages(6, title: "a new thread")
create_messages(7)
create_threaded_messages(3)
create_threaded_messages(27, title: "another long thread")
create_messages(10)
start_archive
stub_const(Chat::ChannelArchiveService, "ARCHIVED_MESSAGES_PER_POST", 5) do
described_class.new(@channel_archive).execute
end
@channel_archive.reload
topic = @channel_archive.destination_topic
expect(topic.posts.count).to eq(14)
topic
.posts
.where.not(post_number: 1)
.each do |post|
case post.post_number
when 2
expect(post.raw).to include("a new thread")
expect(post.raw).to include(
I18n.t("chat.transcript.split_thread_range", start: 1, end: 2, total: 5),
)
when 3
expect(post.raw).to include("a new thread")
expect(post.raw).to include(
I18n.t("chat.transcript.split_thread_range", start: 3, end: 5, total: 5),
)
when 5
expect(post.raw).to include(
"threadTitle=\"#{I18n.t("chat.transcript.default_thread_title")}\"",
)
when 10
expect(post.raw).to include("another long thread")
expect(post.raw).to include(
I18n.t("chat.transcript.split_thread_range", start: 17, end: 20, total: 26),
)
end
expect(post.raw).to include("[chat")
expect(post.raw).to include("noLink=\"true\"")
expect(post.user).to eq(Discourse.system_user)
end
expect(topic.archived).to eq(true)
expect(@channel_archive.archived_messages).to eq(55)
expect(@channel_archive.chat_channel.status).to eq("archived")
expect(@channel_archive.chat_channel.chat_messages.count).to eq(0)
end
it "does not stop the process if the post length is too high (validations disabled)" do
create_messages(50) && start_archive
SiteSetting.max_post_length = 1

View File

@ -6,7 +6,9 @@ describe Chat::TranscriptService do
let(:acting_user) { Fabricate(:user) }
let(:user1) { Fabricate(:user, username: "martinchat") }
let(:user2) { Fabricate(:user, username: "brucechat") }
let(:channel) { Fabricate(:category_channel, name: "The Beam Discussions") }
let(:channel) do
Fabricate(:category_channel, name: "The Beam Discussions", threading_enabled: true)
end
def service(message_ids, opts: {})
described_class.new(channel, acting_user, messages_or_ids: Array.wrap(message_ids), opts: opts)
@ -254,4 +256,201 @@ describe Chat::TranscriptService do
[/chat]
MARKDOWN
end
it "generates reaction data for threaded messages" do
thread = Fabricate(:chat_thread, channel: channel)
thread_om =
Fabricate(
:chat_message,
user: user1,
chat_channel: channel,
thread: thread,
message: "an extremely insightful response :)",
)
thread_reply_1 =
Fabricate(
:chat_message,
chat_channel: channel,
user: user2,
thread: thread,
message: "wow so tru",
)
thread_reply_2 =
Fabricate(
:chat_message,
chat_channel: channel,
user: user1,
thread: thread,
message: "a new perspective",
)
Chat::MessageReaction.create!(
chat_message: thread_om,
user: Fabricate(:user, username: "bjorn"),
emoji: "heart",
)
Chat::MessageReaction.create!(
chat_message: thread_reply_1,
user: Fabricate(:user, username: "sigurd"),
emoji: "heart",
)
Chat::MessageReaction.create!(
chat_message: thread_reply_1,
user: Fabricate(:user, username: "hvitserk"),
emoji: "+1",
)
Chat::MessageReaction.create!(
chat_message: thread_reply_2,
user: Fabricate(:user, username: "ubbe"),
emoji: "money_mouth_face",
)
rendered =
service(
[thread_om.id, thread_reply_1.id, thread_reply_2.id],
opts: {
include_reactions: true,
},
).generate_markdown
expect(rendered).to eq(<<~MARKDOWN)
[chat quote="martinchat;#{thread_om.id};#{thread_om.created_at.iso8601}" channel="The Beam Discussions" channelId="#{channel.id}" multiQuote="true" chained="true" reactions="heart:bjorn" threadId="#{thread.id}" threadTitle="#{I18n.t("chat.transcript.default_thread_title")}"]
an extremely insightful response :)
[chat quote="brucechat;#{thread_reply_1.id};#{thread_reply_1.created_at.iso8601}" chained="true" reactions="+1:hvitserk;heart:sigurd"]
wow so tru
[/chat]
[chat quote="martinchat;#{thread_reply_2.id};#{thread_reply_2.created_at.iso8601}" chained="true" reactions="money_mouth_face:ubbe"]
a new perspective
[/chat]
[/chat]
MARKDOWN
end
it "generates a chat transcript for threaded messages" do
thread = Fabricate(:chat_thread, channel: channel)
thread_om =
Fabricate(
:chat_message,
chat_channel: channel,
user: user1,
thread: thread,
message: "reply to me!",
)
thread_reply_1 =
Fabricate(:chat_message, chat_channel: channel, user: user2, thread: thread, message: "done")
thread_reply_2 =
Fabricate(
:chat_message,
chat_channel: channel,
user: user1,
thread: thread,
message: "thanks",
)
rendered = service([thread_om.id, thread_reply_1.id, thread_reply_2.id]).generate_markdown
expect(rendered).to eq(<<~MARKDOWN)
[chat quote="martinchat;#{thread_om.id};#{thread_om.created_at.iso8601}" channel="The Beam Discussions" channelId="#{channel.id}" multiQuote="true" chained="true" threadId="#{thread.id}" threadTitle="#{I18n.t("chat.transcript.default_thread_title")}"]
reply to me!
[chat quote="brucechat;#{thread_reply_1.id};#{thread_reply_1.created_at.iso8601}" chained="true"]
done
[/chat]
[chat quote="martinchat;#{thread_reply_2.id};#{thread_reply_2.created_at.iso8601}" chained="true"]
thanks
[/chat]
[/chat]
MARKDOWN
end
it "generates the correct markdown for multiple threads" do
channel_message_1 =
Fabricate(:chat_message, user: user1, chat_channel: channel, message: "I need ideas")
thread_1 = Fabricate(:chat_thread, channel: channel)
thread_1_om =
Fabricate(
:chat_message,
chat_channel: channel,
user: user2,
thread: thread_1,
message: "this is my idea",
)
thread_1_message =
Fabricate(
:chat_message,
chat_channel: channel,
user: user1,
thread: thread_1,
message: "cool",
)
channel_message_2 =
Fabricate(:chat_message, user: user2, chat_channel: channel, message: "more?")
thread_2 = Fabricate(:chat_thread, channel: channel, title: "the second idea")
thread_2_om =
Fabricate(
:chat_message,
chat_channel: channel,
user: user2,
thread: thread_2,
message: "another one",
)
thread_2_message_1 =
Fabricate(
:chat_message,
chat_channel: channel,
user: user1,
thread: thread_2,
message: "thanks",
)
thread_2_message_2 =
Fabricate(:chat_message, chat_channel: channel, user: user2, thread: thread_2, message: "np")
rendered =
service(
[
channel_message_1.id,
thread_1_om.id,
thread_1_message.id,
channel_message_2.id,
thread_2_om.id,
thread_2_message_1.id,
thread_2_message_2.id,
],
).generate_markdown
expect(rendered).to eq(<<~MARKDOWN)
[chat quote="martinchat;#{channel_message_1.id};#{channel_message_1.created_at.iso8601}" channel="The Beam Discussions" channelId="#{channel.id}" multiQuote="true" chained="true"]
I need ideas
[/chat]
[chat quote="brucechat;#{thread_1_om.id};#{thread_1_om.created_at.iso8601}" chained="true" threadId="#{thread_1.id}" threadTitle="#{I18n.t("chat.transcript.default_thread_title")}"]
this is my idea
[chat quote="martinchat;#{thread_1_message.id};#{thread_1_message.created_at.iso8601}" chained="true"]
cool
[/chat]
[/chat]
[chat quote="brucechat;#{channel_message_2.id};#{channel_message_2.created_at.iso8601}" chained="true"]
more?
[/chat]
[chat quote="brucechat;#{thread_2_om.id};#{thread_2_om.created_at.iso8601}" chained="true" threadId="#{thread_2.id}" threadTitle="the second idea"]
another one
[chat quote="martinchat;#{thread_2_message_1.id};#{thread_2_message_1.created_at.iso8601}" chained="true"]
thanks
[/chat]
[chat quote="brucechat;#{thread_2_message_2.id};#{thread_2_message_2.created_at.iso8601}" chained="true"]
np
[/chat]
[/chat]
MARKDOWN
end
end