DEV: allows a context when creating a message (#25647)

The service `Chat::CreateMessage` will now accept `context_post_ids` and `context_topic_id` as params. These values represent the topic which might be visible when sending a message (for now, this is only possible when using the drawer).

The `DiscourseEvent` `chat_message_created` will now have the following signature:

```ruby
on(:chat_message_created) do | message, channel, user, meta|
  p meta[:context][:post_ids]
end
```
This commit is contained in:
Joffrey JAFFEUX 2024-02-13 11:37:15 +01:00 committed by GitHub
parent 2bd0a8f432
commit 06bbed69f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 164 additions and 10 deletions

View File

@ -16,6 +16,8 @@ module Chat
# @param in_reply_to_id [Integer] ID of a message to reply to
# @param thread_id [Integer] ID of a thread to reply to
# @param upload_ids [Array<Integer>] IDs of uploaded documents
# @param context_topic_id [Integer] ID of the currently visible topic in drawer mode
# @param context_post_ids [Array<Integer>] IDs of the currently visible posts in drawer mode
# @param staged_id [String] arbitrary string that will be sent back to the client
# @param incoming_chat_webhook [Chat::IncomingWebhook]
@ -50,6 +52,8 @@ module Chat
class Contract
attribute :chat_channel_id, :string
attribute :in_reply_to_id, :string
attribute :context_topic_id, :integer
attribute :context_post_ids, :array
attribute :message, :string
attribute :staged_id, :string
attribute :upload_ids, :array
@ -185,11 +189,13 @@ module Chat
def process(channel:, message_instance:, contract:, **)
::Chat::Publisher.publish_new!(channel, message_instance, contract.staged_id)
DiscourseEvent.trigger(
:chat_message_created,
message_instance,
channel,
message_instance.user,
{ context: { post_ids: contract.context_post_ids, topic_id: contract.context_topic_id } },
)
if contract.process_inline

View File

@ -35,6 +35,7 @@ import {
checkMessageTopVisibility,
} from "discourse/plugins/chat/discourse/lib/check-message-visibility";
import DatesSeparatorsPositioner from "discourse/plugins/chat/discourse/lib/dates-separators-positioner";
import { extractCurrentTopicInfo } from "discourse/plugins/chat/discourse/lib/extract-current-topic-info";
import {
scrollListToBottom,
scrollListToMessage,
@ -560,12 +561,17 @@ export default class ChatChannel extends Component {
}
try {
await this.chatApi.sendMessage(this.args.channel.id, {
const params = {
message: message.message,
in_reply_to_id: message.inReplyTo?.id,
staged_id: message.id,
upload_ids: message.uploads.map((upload) => upload.id),
});
};
await this.chatApi.sendMessage(
this.args.channel.id,
Object.assign({}, params, extractCurrentTopicInfo(this))
);
if (!this.capabilities.isIOS) {
this.scrollToLatestMessage();

View File

@ -25,6 +25,7 @@ import {
} from "discourse/plugins/chat/discourse/lib/chat-ios-hacks";
import ChatMessagesLoader from "discourse/plugins/chat/discourse/lib/chat-messages-loader";
import DatesSeparatorsPositioner from "discourse/plugins/chat/discourse/lib/dates-separators-positioner";
import { extractCurrentTopicInfo } from "discourse/plugins/chat/discourse/lib/extract-current-topic-info";
import {
scrollListToBottom,
scrollListToMessage,
@ -429,15 +430,17 @@ export default class ChatThread extends Component {
}
try {
const params = {
message: message.message,
in_reply_to_id: null,
staged_id: message.id,
upload_ids: message.uploads.map((upload) => upload.id),
thread_id: message.thread.id,
};
const response = await this.chatApi.sendMessage(
this.args.thread.channel.id,
{
message: message.message,
in_reply_to_id: null,
staged_id: message.id,
upload_ids: message.uploads.map((upload) => upload.id),
thread_id: message.thread.id,
}
Object.assign({}, params, extractCurrentTopicInfo(this))
);
this.args.thread.currentUserMembership ??=

View File

@ -0,0 +1,29 @@
import { getOwner } from "@ember/application";
export function extractCurrentTopicInfo(context) {
const topic = getOwner(context).lookup("controller:topic")?.get("model");
if (!topic) {
return;
}
const info = { topic_id: topic.id };
const currentPostNumber = parseInt(topic.current_post_number, 10);
const posts = topic.postStream.posts;
const currentPost = posts.find(
(post) => post.post_number === currentPostNumber
);
const previousPost = posts.findLast(
(post) =>
!post.hidden && !post.deleted_at && post.post_number < currentPostNumber
);
const nextPost = posts.find(
(post) =>
!post.hidden && !post.deleted_at && post.post_number > currentPostNumber
);
info.post_ids = [previousPost?.id, currentPost?.id, nextPost?.id];
return info;
}

View File

@ -163,6 +163,8 @@ export default {
.lookup("service:chat-api")
.sendMessage(channelId, {
thread_id: options.threadId,
post_ids: options.postIds,
topic_id: options.topicId,
message: options.message,
uploads: options.uploads,
});

View File

@ -180,6 +180,8 @@ export default class ChatApi extends Service {
* @param {number} [data.in_reply_to_id] - The ID of the replied-to message.
* @param {number} [data.staged_id] - The staged ID of the message before it was persisted.
* @param {number} [data.thread_id] - The ID of the thread where this message should be posted.
* @param {number} [data.topic_id] - The ID of the currently visible topic in drawer mode.
* @param {number} [data.post_ids] - The ID of the currently visible posts in drawer mode.
* @param {Array.<number>} [data.upload_ids] - Array of upload ids linked to the message.
* @returns {Promise}
*/

View File

@ -11,10 +11,20 @@ module Chat
case value
when String
value.split(",")
when ::Array
value.map { |item| convert_to_integer(item) }
else
::Array.wrap(value)
end
end
private
def convert_to_integer(item)
Integer(item)
rescue ArgumentError
item
end
end
end
end

View File

@ -24,6 +24,22 @@ RSpec.describe Chat::Types::Array do
end
end
context "when 'value' is an array of numbers as string" do
let(:value) { %w[1 2] }
it "returns it with string casted as integer" do
expect(casted_value).to eq([1, 2])
end
end
context "when 'value' is an array of numbers" do
let(:value) { [1, 2] }
it "returns it" do
expect(casted_value).to eq([1, 2])
end
end
context "when 'value' is something else" do
let(:value) { Time.current }

View File

@ -31,8 +31,17 @@ RSpec.describe Chat::CreateMessage do
let(:guardian) { user.guardian }
let(:content) { "A new message @#{other_user.username_lower}" }
let(:context_topic_id) { nil }
let(:context_post_ids) { nil }
let(:params) do
{ guardian: guardian, chat_channel_id: channel.id, message: content, upload_ids: [upload.id] }
{
guardian: guardian,
chat_channel_id: channel.id,
message: content,
upload_ids: [upload.id],
context_topic_id: context_topic_id,
context_post_ids: context_post_ids,
}
end
let(:message) { result[:message_instance].reload }
@ -91,10 +100,29 @@ RSpec.describe Chat::CreateMessage do
instance_of(Chat::Message),
channel,
user,
anything,
)
result
end
context "when context given" do
let(:context_post_ids) { [1, 2] }
let(:context_topic_id) { 3 }
it "triggers a Discourse event with context if given" do
DiscourseEvent.expects(:trigger).with(
:chat_message_created,
instance_of(Chat::Message),
channel,
user,
{ context: { post_ids: context_post_ids, topic_id: context_topic_id } },
)
result
end
end
it "processes the direct message channel" do
Chat::Action::PublishAndFollowDirectMessageChannel.expects(:call).with(
channel_membership: membership,

View File

@ -151,4 +151,56 @@ RSpec.describe "Drawer", type: :system do
expect(drawer_page).to have_open_channel(channel)
end
end
context "when sending a message from topic" do
fab!(:topic)
fab!(:posts) { Fabricate.times(5, :post, topic: topic) }
fab!(:channel) { Fabricate(:chat_channel) }
fab!(:membership) do
Fabricate(:user_chat_channel_membership, user: current_user, chat_channel: channel)
end
let(:topic_page) { PageObjects::Pages::Topic.new }
context "when on a channel" do
it "has context" do
::Chat::CreateMessage
.expects(:call)
.with do |value|
value["topic_id"] === topic.id.to_s &&
value["post_ids"] === [posts[1].id.to_s, posts[2].id.to_s, posts[3].id.to_s]
end
topic_page.visit_topic(topic, post_number: 3)
chat_page.open_from_header
drawer_page.open_channel(channel)
channel_page.send_message
end
end
context "when on a thread" do
before { channel.update!(threading_enabled: true) }
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel) }
let(:thread_list_page) { PageObjects::Components::Chat::ThreadList.new }
let(:thread_page) { PageObjects::Pages::ChatThread.new }
it "has context" do
::Chat::CreateMessage
.expects(:call)
.with do |value|
value["topic_id"] === topic.id.to_s &&
value["post_ids"] === [posts[1].id.to_s, posts[2].id.to_s, posts[3].id.to_s]
end
topic_page.visit_topic(topic, post_number: 3)
chat_page.open_from_header
drawer_page.open_channel(channel)
drawer_page.open_thread_list
thread_list_page.open_thread(thread_1)
thread_page.send_message
end
end
end
end