DEV: chat streaming (#25736)

This commit introduces the possibility to stream messages. To allow plugins to use streaming this commit also ships a `ChatSDK` library to allow to interact with few parts of discourse chat.

```ruby
ChatSDK::Message.create_with_stream(raw: "test") do |helper|
  5.times do |i|
    is_streaming = helper.stream(raw: "more #{i}")
    next if !is_streaming
    sleep 2
  end
end
```

This commit also introduces all the frontend parts:
- messages can now be marked as streaming
- when streaming their content will be updated when a new content is appended
- a special UI will be showing (a blinking indicator)
- a cancel button allows the user to stop the streaming, when cancelled `helper.stream(...)` will return `false`, and the plugin can decide exit early
This commit is contained in:
Joffrey JAFFEUX
2024-02-20 09:49:19 +01:00
committed by GitHub
parent b057f1b2b4
commit d8d756cd2f
30 changed files with 815 additions and 24 deletions

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require "rails_helper"
describe ChatSDK::Channel do
describe ".messages" do
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1) }
let(:params) { { channel_id: channel_1.id, guardian: Discourse.system_user.guardian } }
it "loads the messages" do
messages = described_class.messages(**params)
expect(messages).to eq([message_1, message_2])
end
it "accepts page_size" do
messages = described_class.messages(**params, page_size: 1)
expect(messages).to eq([message_1])
end
context "when guardian can't see the channel" do
fab!(:channel_1) { Fabricate(:private_category_channel) }
it "fails" do
params[:guardian] = Fabricate(:user).guardian
expect { described_class.messages(**params) }.to raise_error("Guardian can't view channel")
end
end
context "when target_message doesn’t exist" do
it "fails" do
expect { described_class.messages(**params, target_message_id: -999) }.to raise_error(
"Target message doesn't exist",
)
end
end
end
end

View File

@ -0,0 +1,100 @@
# frozen_string_literal: true
require "rails_helper"
describe ChatSDK::Message do
describe ".create" do
fab!(:channel_1) { Fabricate(:chat_channel) }
let(:guardian) { Discourse.system_user.guardian }
let(:params) do
{ enforce_membership: false, raw: "something", channel_id: channel_1.id, guardian: guardian }
end
it "creates the message" do
message = described_class.create(**params)
expect(message.message).to eq("something")
end
context "when thread_id is present" do
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel_1) }
it "creates the message in a thread" do
message = described_class.create(**params, thread_id: thread_1.id)
expect(message.thread_id).to eq(thread_1.id)
end
end
context "when channel doesn’t exist" do
it "fails" do
expect { described_class.create(**params, channel_id: -999) }.to raise_error(
"Couldn't find channel with id: `-999`",
)
end
end
context "when user can't join channel" do
it "fails" do
params[:guardian] = Fabricate(:user).guardian
expect { described_class.create(**params) }.to raise_error(
"User with id: `#{params[:guardian].user.id}` can't join this channel",
)
end
end
context "when membership is enforced" do
it "works" do
params[:enforce_membership] = true
params[:guardian] = Fabricate(:user).guardian
SiteSetting.chat_allowed_groups = [Group::AUTO_GROUPS[:everyone]]
message = described_class.create(**params)
expect(message.message).to eq("something")
end
end
context "when thread doesn't exist" do
it "fails" do
expect { described_class.create(**params, thread_id: -999) }.to raise_error(
"Couldn't find thread with id: `-999`",
)
end
end
context "when params are invalid" do
it "fails" do
expect { described_class.create(**params, raw: nil, channel_id: nil) }.to raise_error(
"Chat channel can't be blank, Message can't be blank",
)
end
end
end
describe ".create_with_stream" do
fab!(:channel_1) { Fabricate(:chat_channel) }
let(:guardian) { Discourse.system_user.guardian }
let(:params) { { raw: "something", channel_id: channel_1.id, guardian: guardian } }
it "allows streaming" do
created_message =
described_class.create_with_stream(**params) do |helper, message|
expect(message.streaming).to eq(true)
edit =
MessageBus
.track_publish("/chat/#{channel_1.id}") { helper.stream(raw: "test") }
.find { |m| m.data["type"] == "edit" }
expect(edit.data["chat_message"]["message"]).to eq("something test")
end
expect(created_message.streaming).to eq(false)
expect(created_message.message).to eq("something test")
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
require "rails_helper"
describe ChatSDK::Thread do
describe ".update_title" do
fab!(:thread_1) { Fabricate(:chat_thread) }
let(:params) do
{
title: "New Title",
channel_id: thread_1.channel_id,
thread_id: thread_1.id,
guardian: Discourse.system_user.guardian,
}
end
it "changes the title" do
expect { described_class.update_title(**params) }.to change { thread_1.reload.title }.from(
thread_1.title,
).to(params[:title])
end
context "when missing param" do
it "fails" do
params.delete(:thread_id)
expect { described_class.update_title(**params) }.to raise_error("Thread can't be blank")
end
end
context "when guardian can't see the channel" do
fab!(:thread_1) { Fabricate(:chat_thread, channel: Fabricate(:private_category_channel)) }
it "fails" do
params[:guardian] = Fabricate(:user).guardian
expect { described_class.update_title(**params) }.to raise_error(
"Guardian can't view channel",
)
end
end
context "when guardian can't edit the thread" do
it "fails" do
params[:guardian] = Fabricate(:user).guardian
expect { described_class.update_title(**params) }.to raise_error(
"Guardian can't edit thread",
)
end
end
context "when the threadind is not enabled" do
before { thread_1.channel.update!(threading_enabled: false) }
it "fails" do
expect { described_class.update_title(**params) }.to raise_error(
"Threading is not enabled for this channel",
)
end
end
context "when the thread doesn't exist" do
it "fails" do
params[:thread_id] = -999
expect { described_class.update_title(**params) }.to raise_error(
"Couldn’t find thread with id: `-999`",
)
end
end
context "when target_message doesn’t exist" do
it "fails" do
expect { described_class.messages(**params, target_message_id: -999) }.to raise_error(
"Target message doesn't exist",
)
end
end
end
end