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
8 changed files with 673 additions and 41 deletions

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