DEV: Create and update chat message mentions earlier (#21388)

We need to create and update `chat_mentions` records for messages earlier. They should be created or updated before we  call `Chat::Publisher.publish_new!` `Chat::Publisher.publish_edit!` to send the message to message bus subscribers).

This logic is covered with tests in `message_creator_spec.rb`, `message_updater_spec.rb`, `notifier_spec.rb` and `notify_mentioned_spec.rb`.

See the commits history for steps of refactoring.
This commit is contained in:
Andrei Prigorshnev 2023-05-05 15:47:07 +04:00 committed by GitHub
parent 78616404ce
commit 35a414bb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 104 additions and 65 deletions

View File

@ -157,6 +157,8 @@ module Chat
self.cooked = self.class.cook(self.message, user_id: self.last_editor_id)
self.cooked_version = BAKED_VERSION
invalidate_parsed_mentions
end
def rebake!(invalidate_oneboxes: false, priority: nil)
@ -258,7 +260,49 @@ module Chat
"/chat/c/-/#{self.chat_channel_id}/#{self.id}"
end
def create_mentions(user_ids)
def create_mentions
insert_mentions(parsed_mentions.all_mentioned_users_ids)
end
def update_mentions
mentioned_user_ids = parsed_mentions.all_mentioned_users_ids
old_mentions = chat_mentions.pluck(:user_id)
updated_mentions = mentioned_user_ids
mentioned_user_ids_to_drop = old_mentions - updated_mentions
mentioned_user_ids_to_add = updated_mentions - old_mentions
delete_mentions(mentioned_user_ids_to_drop)
insert_mentions(mentioned_user_ids_to_add)
end
def in_thread?
self.thread_id.present?
end
def thread_reply?
in_thread? && !thread_om?
end
def thread_om?
in_thread? && self.thread.original_message_id == self.id
end
def parsed_mentions
@parsed_mentions ||= Chat::ParsedMentions.new(self)
end
def invalidate_parsed_mentions
@parsed_mentions = nil
end
private
def delete_mentions(user_ids)
chat_mentions.where(user_id: user_ids).destroy_all
end
def insert_mentions(user_ids)
return if user_ids.empty?
now = Time.zone.now
@ -277,34 +321,6 @@ module Chat
Chat::Mention.insert_all(mentions)
end
def update_mentions(mentioned_user_ids)
old_mentions = chat_mentions.pluck(:user_id)
updated_mentions = mentioned_user_ids
mentioned_user_ids_to_drop = old_mentions - updated_mentions
mentioned_user_ids_to_add = updated_mentions - old_mentions
delete_mentions(mentioned_user_ids_to_drop)
create_mentions(mentioned_user_ids_to_add)
end
def in_thread?
self.thread_id.present?
end
def thread_reply?
in_thread? && !thread_om?
end
def thread_om?
in_thread? && self.thread.original_message_id == self.id
end
private
def delete_mentions(user_ids)
chat_mentions.where(user_id: user_ids).destroy_all
end
def message_too_short?
message.length < SiteSetting.chat_minimum_message_length
end

View File

@ -52,9 +52,12 @@ module Chat
validate_message!(has_uploads: uploads.any?)
validate_reply_chain!
validate_existing_thread!
@chat_message.thread_id = @existing_thread&.id
@chat_message.cook
@chat_message.save!
@chat_message.create_mentions
create_chat_webhook_event
create_thread
@chat_message.attach_uploads(uploads)

View File

@ -31,8 +31,11 @@ module Chat
validate_message!(has_uploads: upload_info[:uploads].any?)
@chat_message.cook
@chat_message.save!
@chat_message.update_mentions
update_uploads(upload_info)
revision = save_revision!
@chat_message.reload
Chat::Publisher.publish_edit!(@chat_channel, @chat_message)
Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id })

View File

@ -61,16 +61,11 @@ module Chat
@timestamp = timestamp
@chat_channel = @chat_message.chat_channel
@user = @chat_message.user
@mentions = Chat::MessageMentions.new(chat_message)
end
### Public API
def notify_new
if @mentions.all_mentioned_users_ids.present?
@chat_message.create_mentions(@mentions.all_mentioned_users_ids)
end
to_notify = list_users_to_notify
mentioned_user_ids = to_notify.extract!(:all_mentioned_user_ids)[:all_mentioned_user_ids]
@ -87,8 +82,6 @@ module Chat
end
def notify_edit
@chat_message.update_mentions(@mentions.all_mentioned_users_ids)
already_notified_user_ids =
Chat::Mention
.where(chat_message: @chat_message)
@ -108,7 +101,8 @@ module Chat
private
def list_users_to_notify
skip_notifications = @mentions.count > SiteSetting.max_mentions_per_chat_message
skip_notifications =
@chat_message.parsed_mentions.count > SiteSetting.max_mentions_per_chat_message
{}.tap do |to_notify|
# The order of these methods is the precedence
@ -130,10 +124,11 @@ module Chat
end
def expand_global_mention(to_notify, already_covered_ids)
has_all_mention = @mentions.has_global_mention
has_all_mention = @chat_message.parsed_mentions.has_global_mention
if has_all_mention && @chat_channel.allow_channel_wide_mentions
to_notify[:global_mentions] = @mentions
to_notify[:global_mentions] = @chat_message
.parsed_mentions
.global_mentions
.not_suspended
.where(user_options: { ignore_channel_wide_mention: [false, nil] })
@ -147,10 +142,11 @@ module Chat
end
def expand_here_mention(to_notify, already_covered_ids)
has_here_mention = @mentions.has_here_mention
has_here_mention = @chat_message.parsed_mentions.has_here_mention
if has_here_mention && @chat_channel.allow_channel_wide_mentions
to_notify[:here_mentions] = @mentions
to_notify[:here_mentions] = @chat_message
.parsed_mentions
.here_mentions
.not_suspended
.where(user_options: { ignore_channel_wide_mention: [false, nil] })
@ -190,7 +186,10 @@ module Chat
if skip
direct_mentions = []
else
direct_mentions = @mentions.direct_mentions.not_suspended.where.not(id: already_covered_ids)
direct_mentions =
@chat_message.parsed_mentions.direct_mentions.not_suspended.where.not(
id: already_covered_ids,
)
end
grouped = group_users_to_notify(direct_mentions)
@ -202,21 +201,24 @@ module Chat
end
def expand_group_mentions(to_notify, already_covered_ids)
return if @mentions.visible_groups.empty?
return if @chat_message.parsed_mentions.visible_groups.empty?
reached_by_group =
@mentions
@chat_message
.parsed_mentions
.group_mentions
.not_suspended
.where("user_count <= ?", SiteSetting.max_users_notified_per_group_mention)
.where.not(id: already_covered_ids)
too_many_members, mentionable =
@mentions.mentionable_groups.partition do |group|
@chat_message.parsed_mentions.mentionable_groups.partition do |group|
group.user_count > SiteSetting.max_users_notified_per_group_mention
end
mentions_disabled = @mentions.visible_groups - @mentions.mentionable_groups
mentions_disabled =
@chat_message.parsed_mentions.visible_groups -
@chat_message.parsed_mentions.mentionable_groups
to_notify[:group_mentions_disabled] = mentions_disabled
to_notify[:too_many_members] = too_many_members
mentionable.each { |g| to_notify[g.name.downcase] = [] }
@ -226,7 +228,8 @@ module Chat
# When a user is a member of multiple mentioned groups,
# the most far to the left should take precedence.
ordered_group_names =
@mentions.parsed_group_mentions & mentionable.map { |mg| mg.name.downcase }
@chat_message.parsed_mentions.parsed_group_mentions &
mentionable.map { |mg| mg.name.downcase }
user_group_names = user.groups.map { |ug| ug.name.downcase }
group_name = ordered_group_names.detect { |gn| user_group_names.include?(gn) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Chat
class MessageMentions
class ParsedMentions
def initialize(message)
@message = message

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe Chat::MessageMentions do
RSpec.describe Chat::ParsedMentions do
fab!(:channel_member_1) { Fabricate(:user) }
fab!(:channel_member_2) { Fabricate(:user) }
fab!(:channel_member_3) { Fabricate(:user) }

View File

@ -552,17 +552,22 @@ describe Chat::Message do
fab!(:user1) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) }
it "creates mentions for passed user ids" do
mentioned_user_ids = [user1.id, user2.id]
message.create_mentions(mentioned_user_ids)
it "creates mentions for mentioned usernames" do
message.message = "Mentioning @#{user1.username} and @#{user2.username}"
message.cook
message.create_mentions
message.reload
expect(message.chat_mentions.pluck(:user_id)).to match_array(mentioned_user_ids)
expect(message.chat_mentions.pluck(:user_id)).to match_array([user1.id, user2.id])
end
it "ignores duplicates in passed user ids" do
mentioned_user_ids = [user1.id, user1.id, user1.id, user1.id, user1.id]
message.create_mentions(mentioned_user_ids)
it "ignores duplicated mentions" do
message.message =
"Mentioning @#{user1.username} @#{user1.username} @#{user1.username} @#{user1.username}"
message.cook
message.create_mentions
message.reload
expect(message.chat_mentions.pluck(:user_id)).to contain_exactly(user1.id)
@ -570,28 +575,36 @@ describe Chat::Message do
end
describe "#update_mentions" do
fab!(:message) { Fabricate(:chat_message) }
fab!(:user1) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) }
fab!(:user3) { Fabricate(:user) }
fab!(:user4) { Fabricate(:user) }
fab!(:message) do
Fabricate(:chat_message, message: "Hey @#{user1.username} and @#{user2.username}")
end
let(:already_mentioned) { [user1.id, user2.id] }
before { message.create_mentions(already_mentioned) }
before { message.create_mentions }
it "creates newly added mentions" do
existing_mention_ids = message.chat_mentions.pluck(:id)
message.message = message.message + " @#{user3.username} @#{user4.username} "
message.cook
mentioned_user_ids = [*already_mentioned, user3.id, user4.id]
message.update_mentions(mentioned_user_ids)
message.update_mentions
message.reload
expect(message.chat_mentions.pluck(:user_id)).to match_array(mentioned_user_ids)
expect(message.chat_mentions.pluck(:user_id)).to match_array(
[user1.id, user2.id, user3.id, user4.id],
)
expect(message.chat_mentions.pluck(:id)).to include(*existing_mention_ids) # existing mentions weren't recreated
end
it "drops removed mentions" do
message.update_mentions([user1.id]) # user 2 is not mentioned anymore
message.message = "Hey @#{user1.username}" # user 2 is not mentioned anymore
message.cook
message.update_mentions
message.reload
expect(message.chat_mentions.pluck(:user_id)).to contain_exactly(user1.id)
@ -599,12 +612,13 @@ describe Chat::Message do
it "changes nothing if passed mentions are identical to existing mentions" do
existing_mention_ids = message.chat_mentions.pluck(:id)
message.message = message.message
message.cook
mentioned_user_ids = [*already_mentioned]
message.update_mentions(mentioned_user_ids)
message.update_mentions
message.reload
expect(message.chat_mentions.pluck(:user_id)).to match_array(mentioned_user_ids)
expect(message.chat_mentions.pluck(:user_id)).to match_array(already_mentioned)
expect(message.chat_mentions.pluck(:id)).to include(*existing_mention_ids) # the mentions weren't recreated
end
end