mirror of
https://github.com/discourse/discourse.git
synced 2025-01-30 06:28:03 +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 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) => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user