mirror of
https://github.com/discourse/discourse.git
synced 2025-02-07 00:05:00 +08:00
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:
parent
d506721eee
commit
ac9e804dbe
|
@ -20,6 +20,8 @@ const chatTranscriptRule = {
|
||||||
const noLink = !!tagInfo.attrs.noLink;
|
const noLink = !!tagInfo.attrs.noLink;
|
||||||
const channelName = tagInfo.attrs.channel;
|
const channelName = tagInfo.attrs.channel;
|
||||||
const channelId = tagInfo.attrs.channelId;
|
const channelId = tagInfo.attrs.channelId;
|
||||||
|
const threadId = tagInfo.attrs.threadId;
|
||||||
|
const threadTitle = tagInfo.attrs.threadTitle;
|
||||||
const channelLink = channelId
|
const channelLink = channelId
|
||||||
? options.getURL(`/chat/c/-/${channelId}`)
|
? options.getURL(`/chat/c/-/${channelId}`)
|
||||||
: null;
|
: null;
|
||||||
|
@ -29,6 +31,78 @@ const chatTranscriptRule = {
|
||||||
}
|
}
|
||||||
|
|
||||||
let wrapperDivToken = state.push("div_chat_transcript_wrap", "div", 1);
|
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"];
|
let wrapperClasses = ["chat-transcript"];
|
||||||
|
|
||||||
if (!!tagInfo.attrs.chained) {
|
if (!!tagInfo.attrs.chained) {
|
||||||
|
@ -46,17 +120,6 @@ const chatTranscriptRule = {
|
||||||
|
|
||||||
if (channelName) {
|
if (channelName) {
|
||||||
wrapperDivToken.attrs.push(["data-channel-name", 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) {
|
if (channelId) {
|
||||||
|
@ -117,6 +180,17 @@ const chatTranscriptRule = {
|
||||||
spanToken.attrs = [["title", messageTimeStart]];
|
spanToken.attrs = [["title", messageTimeStart]];
|
||||||
|
|
||||||
spanToken.block = false;
|
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 = state.push("span_close", "span", -1);
|
||||||
spanToken.block = false;
|
spanToken.block = false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -153,11 +227,32 @@ const chatTranscriptRule = {
|
||||||
let messagesToken = state.push("div_chat_transcript_messages", "div", 1);
|
let messagesToken = state.push("div_chat_transcript_messages", "div", 1);
|
||||||
messagesToken.attrs = [["class", "chat-transcript-messages"]];
|
messagesToken.attrs = [["class", "chat-transcript-messages"]];
|
||||||
|
|
||||||
// rendering chat message content with limited markdown rule subset
|
if (threadId) {
|
||||||
const token = state.push("html_raw", "", 1);
|
const regex = /\[chat/i;
|
||||||
|
const match = regex.exec(content);
|
||||||
|
|
||||||
token.content = customMarkdownCookFn(content);
|
if (match) {
|
||||||
state.push("html_raw", "", -1);
|
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) {
|
if (reactions) {
|
||||||
let emojiHtmlCache = {};
|
let emojiHtmlCache = {};
|
||||||
|
@ -202,8 +297,12 @@ const chatTranscriptRule = {
|
||||||
|
|
||||||
export function setup(helper) {
|
export function setup(helper) {
|
||||||
helper.allowList([
|
helper.allowList([
|
||||||
|
"svg[class=fa d-icon d-icon-discourse-threads svg-icon svg-node]",
|
||||||
|
"use[href=#discourse-threads]",
|
||||||
"div[class=chat-transcript]",
|
"div[class=chat-transcript]",
|
||||||
|
"details[class=chat-transcript]",
|
||||||
"div[class=chat-transcript chat-transcript-chained]",
|
"div[class=chat-transcript chat-transcript-chained]",
|
||||||
|
"details[class=chat-transcript chat-transcript-chained]",
|
||||||
"div.chat-transcript-meta",
|
"div.chat-transcript-meta",
|
||||||
"div.chat-transcript-user",
|
"div.chat-transcript-user",
|
||||||
"div.chat-transcript-username",
|
"div.chat-transcript-username",
|
||||||
|
@ -219,6 +318,9 @@ export function setup(helper) {
|
||||||
"div[data-username]",
|
"div[data-username]",
|
||||||
"div[data-datetime]",
|
"div[data-datetime]",
|
||||||
"a.chat-transcript-channel",
|
"a.chat-transcript-channel",
|
||||||
|
"div.chat-transcript-thread",
|
||||||
|
"div.chat-transcript-thread-header",
|
||||||
|
"span.chat-transcript-thread-header__title",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
helper.registerOptions((opts, siteSettings) => {
|
helper.registerOptions((opts, siteSettings) => {
|
||||||
|
|
|
@ -17,17 +17,21 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details > .chat-transcript-chained:first-of-type {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-transcript-meta {
|
.chat-transcript-meta {
|
||||||
color: var(--primary-high);
|
color: var(--primary-high);
|
||||||
font-size: var(--font-down-2-rem);
|
font-size: var(--font-down-2-rem);
|
||||||
border-bottom: 1px solid var(--primary-low);
|
border-bottom: 1px solid var(--primary-low);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.chat-transcript-channel,
|
img.emoji {
|
||||||
.chat-transcript-thread {
|
height: 1.1em;
|
||||||
font-size: var(--font-down-1-rem);
|
width: 1.1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-transcript-separator {
|
.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 {
|
.chat-transcript-user-avatar .avatar {
|
||||||
aspect-ratio: 20 / 20;
|
aspect-ratio: 20 / 20;
|
||||||
}
|
}
|
||||||
|
@ -81,6 +105,8 @@
|
||||||
|
|
||||||
.chat-transcript-reaction {
|
.chat-transcript-reaction {
|
||||||
@include chat-reaction;
|
@include chat-reaction;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -445,6 +445,7 @@ en:
|
||||||
quote:
|
quote:
|
||||||
original_channel: 'Originally sent in <a href="%{channelLink}">%{channel}</a>'
|
original_channel: 'Originally sent in <a href="%{channelLink}">%{channel}</a>'
|
||||||
copy_success: "Chat quote copied to clipboard"
|
copy_success: "Chat quote copied to clipboard"
|
||||||
|
default_thread_title: "Thread"
|
||||||
|
|
||||||
notification_levels:
|
notification_levels:
|
||||||
never: "Never"
|
never: "Never"
|
||||||
|
|
|
@ -197,6 +197,10 @@ en:
|
||||||
summaries:
|
summaries:
|
||||||
no_targets: "There were no messages during the selected period."
|
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:
|
discourse_push_notifications:
|
||||||
popup:
|
popup:
|
||||||
chat_mention:
|
chat_mention:
|
||||||
|
|
|
@ -85,6 +85,7 @@ module Chat
|
||||||
@chat_channel_archive = chat_channel_archive
|
@chat_channel_archive = chat_channel_archive
|
||||||
@chat_channel = chat_channel_archive.chat_channel
|
@chat_channel = chat_channel_archive.chat_channel
|
||||||
@chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
|
@chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
|
||||||
|
@archived_messages_ids = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
|
@ -107,22 +108,88 @@ module Chat
|
||||||
# Another future improvement is to send a MessageBus message for each
|
# Another future improvement is to send a MessageBus message for each
|
||||||
# completed batch, so the UI can receive updates and show a progress
|
# completed batch, so the UI can receive updates and show a progress
|
||||||
# bar or something similar.
|
# bar or something similar.
|
||||||
|
|
||||||
|
buffer = []
|
||||||
|
batch_thread_ranges = {}
|
||||||
|
|
||||||
chat_channel
|
chat_channel
|
||||||
.chat_messages
|
.chat_messages
|
||||||
.find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |chat_messages|
|
.order("created_at ASC")
|
||||||
create_post(
|
.find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |message_batch|
|
||||||
Chat::TranscriptService.new(
|
thread_ids = message_batch.map(&:thread_id).compact.uniq
|
||||||
chat_channel,
|
threads =
|
||||||
chat_channel_archive.archived_by,
|
chat_channel
|
||||||
messages_or_ids: chat_messages,
|
.chat_messages
|
||||||
opts: {
|
.where(
|
||||||
no_link: true,
|
thread_id:
|
||||||
include_reactions: true,
|
Chat::Message
|
||||||
},
|
.select(:thread_id)
|
||||||
).generate_markdown,
|
.where(thread_id: thread_ids)
|
||||||
) { delete_message_batch(chat_messages.map(&:id)) }
|
.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
|
end
|
||||||
|
|
||||||
|
create_post_from_batch(buffer, batch_thread_ranges) unless buffer.empty?
|
||||||
|
|
||||||
kick_all_users
|
kick_all_users
|
||||||
complete_archive
|
complete_archive
|
||||||
rescue => err
|
rescue => err
|
||||||
|
@ -133,6 +200,63 @@ module Chat
|
||||||
|
|
||||||
private
|
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)
|
def create_post(raw)
|
||||||
pc = nil
|
pc = nil
|
||||||
Post.transaction do
|
Post.transaction do
|
||||||
|
@ -228,9 +352,8 @@ module Chat
|
||||||
deleted_by_id: chat_channel_archive.archived_by.id,
|
deleted_by_id: chat_channel_archive.archived_by.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_channel_archive.update!(
|
@archived_messages_ids = (@archived_messages_ids + message_ids).uniq
|
||||||
archived_messages: chat_channel_archive.archived_messages + message_ids.length,
|
chat_channel_archive.update!(archived_messages: @archived_messages_ids.length)
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info(
|
Rails.logger.info(
|
||||||
|
|
|
@ -21,7 +21,13 @@ module Chat
|
||||||
NO_LINK_ATTR = "noLink=\"true\""
|
NO_LINK_ATTR = "noLink=\"true\""
|
||||||
|
|
||||||
class TranscriptBBCode
|
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(
|
def initialize(
|
||||||
channel: nil,
|
channel: nil,
|
||||||
|
@ -29,7 +35,9 @@ module Chat
|
||||||
multiquote: false,
|
multiquote: false,
|
||||||
chained: false,
|
chained: false,
|
||||||
no_link: false,
|
no_link: false,
|
||||||
include_reactions: false
|
include_reactions: false,
|
||||||
|
thread_id: nil,
|
||||||
|
thread_ranges: {}
|
||||||
)
|
)
|
||||||
@channel = channel
|
@channel = channel
|
||||||
@acting_user = acting_user
|
@acting_user = acting_user
|
||||||
|
@ -37,13 +45,20 @@ module Chat
|
||||||
@chained = chained
|
@chained = chained
|
||||||
@no_link = no_link
|
@no_link = no_link
|
||||||
@include_reactions = include_reactions
|
@include_reactions = include_reactions
|
||||||
|
@thread_ranges = thread_ranges
|
||||||
@message_data = []
|
@message_data = []
|
||||||
|
@threads_markdown = {}
|
||||||
|
@thread_id = thread_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def add(message:, reactions: nil)
|
def add(message:, reactions: nil)
|
||||||
@message_data << { message: message, reactions: reactions }
|
@message_data << { message: message, reactions: reactions }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_thread_markdown(thread_id:, markdown:)
|
||||||
|
@threads_markdown[thread_id] = markdown
|
||||||
|
end
|
||||||
|
|
||||||
def render
|
def render
|
||||||
attrs = [quote_attr(@message_data.first[:message])]
|
attrs = [quote_attr(@message_data.first[:message])]
|
||||||
|
|
||||||
|
@ -57,15 +72,40 @@ module Chat
|
||||||
attrs << NO_LINK_ATTR if no_link
|
attrs << NO_LINK_ATTR if no_link
|
||||||
attrs << reactions_attr if include_reactions
|
attrs << reactions_attr if include_reactions
|
||||||
|
|
||||||
|
if thread_id
|
||||||
|
attrs << thread_id_attr
|
||||||
|
attrs << thread_title_attr(@message_data.first[:message])
|
||||||
|
end
|
||||||
|
|
||||||
<<~MARKDOWN
|
<<~MARKDOWN
|
||||||
[chat #{attrs.compact.join(" ")}]
|
[chat #{attrs.compact.join(" ")}]
|
||||||
#{@message_data.map { |msg| msg[:message].to_markdown }.join("\n\n")}
|
#{render_messages}
|
||||||
[/chat]
|
[/chat]
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
def reactions_attr
|
||||||
reaction_data =
|
reaction_data =
|
||||||
@message_data.reduce([]) do |array, msg_data|
|
@message_data.reduce([]) do |array, msg_data|
|
||||||
|
@ -89,9 +129,23 @@ module Chat
|
||||||
def channel_id_attr
|
def channel_id_attr
|
||||||
"channelId=\"#{channel.id}\""
|
"channelId=\"#{channel.id}\""
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def initialize(channel, acting_user, messages_or_ids: [], opts: {})
|
def initialize(channel, acting_user, messages_or_ids: [], thread_ranges: {}, opts: {})
|
||||||
@channel = channel
|
@channel = channel
|
||||||
@acting_user = acting_user
|
@acting_user = acting_user
|
||||||
|
|
||||||
|
@ -101,12 +155,15 @@ module Chat
|
||||||
@messages = messages_or_ids
|
@messages = messages_or_ids
|
||||||
end
|
end
|
||||||
@opts = opts
|
@opts = opts
|
||||||
|
@thread_ranges = thread_ranges
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_markdown
|
def generate_markdown
|
||||||
previous_message = nil
|
previous_message = nil
|
||||||
rendered_markdown = []
|
rendered_markdown = []
|
||||||
|
rendered_thread_markdown = []
|
||||||
all_messages_same_user = messages.count(:user_id) == 1
|
all_messages_same_user = messages.count(:user_id) == 1
|
||||||
|
|
||||||
open_bbcode_tag =
|
open_bbcode_tag =
|
||||||
TranscriptBBCode.new(
|
TranscriptBBCode.new(
|
||||||
channel: @channel,
|
channel: @channel,
|
||||||
|
@ -114,11 +171,19 @@ module Chat
|
||||||
multiquote: messages.length > 1,
|
multiquote: messages.length > 1,
|
||||||
chained: !all_messages_same_user,
|
chained: !all_messages_same_user,
|
||||||
no_link: @opts[:no_link],
|
no_link: @opts[:no_link],
|
||||||
|
thread_id: messages.first.thread_id,
|
||||||
|
thread_ranges: @thread_ranges,
|
||||||
include_reactions: @opts[:include_reactions],
|
include_reactions: @opts[:include_reactions],
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.each.with_index do |message, idx|
|
group_messages(messages).each do |id, message_group|
|
||||||
if previous_message.present? && previous_message.user_id != message.user_id
|
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
|
rendered_markdown << open_bbcode_tag.render
|
||||||
|
|
||||||
open_bbcode_tag =
|
open_bbcode_tag =
|
||||||
|
@ -126,6 +191,8 @@ module Chat
|
||||||
acting_user: @acting_user,
|
acting_user: @acting_user,
|
||||||
chained: !all_messages_same_user,
|
chained: !all_messages_same_user,
|
||||||
no_link: @opts[:no_link],
|
no_link: @opts[:no_link],
|
||||||
|
thread_id: message.thread_id,
|
||||||
|
thread_ranges: @thread_ranges,
|
||||||
include_reactions: @opts[:include_reactions],
|
include_reactions: @opts[:include_reactions],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -135,7 +202,51 @@ module Chat
|
||||||
else
|
else
|
||||||
open_bbcode_tag.add(message: message)
|
open_bbcode_tag.add(message: message)
|
||||||
end
|
end
|
||||||
|
|
||||||
previous_message = message
|
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
|
end
|
||||||
|
|
||||||
# tie off the last open bbcode + render
|
# tie off the last open bbcode + render
|
||||||
|
@ -145,6 +256,10 @@ module Chat
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def group_messages(messages)
|
||||||
|
messages.group_by { |msg| msg.thread_id || msg.id }
|
||||||
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
@messages ||=
|
@messages ||=
|
||||||
Chat::Message
|
Chat::Message
|
||||||
|
|
|
@ -90,6 +90,13 @@ describe Chat::ChannelArchiveService do
|
||||||
num.times { Fabricate(:chat_message, chat_channel: channel) }
|
num.times { Fabricate(:chat_message, chat_channel: channel) }
|
||||||
end
|
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
|
def start_archive
|
||||||
@channel_archive =
|
@channel_archive =
|
||||||
described_class.create_archive_process(
|
described_class.create_archive_process(
|
||||||
|
@ -143,6 +150,61 @@ describe Chat::ChannelArchiveService do
|
||||||
expect(@channel_archive.chat_channel.chat_messages.count).to eq(0)
|
expect(@channel_archive.chat_channel.chat_messages.count).to eq(0)
|
||||||
end
|
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
|
it "does not stop the process if the post length is too high (validations disabled)" do
|
||||||
create_messages(50) && start_archive
|
create_messages(50) && start_archive
|
||||||
SiteSetting.max_post_length = 1
|
SiteSetting.max_post_length = 1
|
||||||
|
|
|
@ -6,7 +6,9 @@ describe Chat::TranscriptService do
|
||||||
let(:acting_user) { Fabricate(:user) }
|
let(:acting_user) { Fabricate(:user) }
|
||||||
let(:user1) { Fabricate(:user, username: "martinchat") }
|
let(:user1) { Fabricate(:user, username: "martinchat") }
|
||||||
let(:user2) { Fabricate(:user, username: "brucechat") }
|
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: {})
|
def service(message_ids, opts: {})
|
||||||
described_class.new(channel, acting_user, messages_or_ids: Array.wrap(message_ids), opts: 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]
|
[/chat]
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user