mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Allow users to DM groups in chat (#25189)
Allows users to create DMs by selecting groups as a target. It also allows adding user groups to an existing chat - When creating the channel, it expands the user group and adds all its members with chat enabled to the channel. - After creation, there's no difference between adding a group or adding its members individually. - Users can add multiple groups and users simultaneously. - There are UI validations; the member count preview updates according to the member count of added groups, and it does not allow users to add more members than SiteSetting.chat_max_direct_message_users."
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class UsersFromUsernamesAndGroupsQuery
|
||||
def self.call(usernames:, groups:, excluded_user_ids: [])
|
||||
User
|
||||
.joins(:user_option)
|
||||
.left_outer_joins(:groups)
|
||||
.where(user_options: { chat_enabled: true })
|
||||
.where(
|
||||
"username IN (?) OR (groups.name IN (?) AND group_users.user_id IS NOT NULL)",
|
||||
usernames,
|
||||
groups,
|
||||
)
|
||||
.where.not(id: excluded_user_ids)
|
||||
.distinct
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class ChatableGroupSerializer < BasicGroupSerializer
|
||||
attributes :chat_enabled, :chat_enabled_user_count, :can_chat
|
||||
|
||||
def chat_enabled
|
||||
SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def chat_enabled_user_count
|
||||
object.users.count { |user| user.user_option&.chat_enabled }
|
||||
end
|
||||
|
||||
def can_chat
|
||||
# + 1 for current user
|
||||
chat_enabled && chat_enabled_user_count + 1 <= SiteSetting.chat_max_direct_message_users
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
module Chat
|
||||
class ChatablesSerializer < ::ApplicationSerializer
|
||||
attributes :users
|
||||
attributes :groups
|
||||
attributes :direct_message_channels
|
||||
attributes :category_channels
|
||||
|
||||
@@ -18,6 +19,18 @@ module Chat
|
||||
.as_json
|
||||
end
|
||||
|
||||
def groups
|
||||
(object.groups || [])
|
||||
.map do |group|
|
||||
{
|
||||
identifier: "g-#{group.id}",
|
||||
model: ::Chat::ChatableGroupSerializer.new(group, scope: scope, root: false),
|
||||
type: "group",
|
||||
}
|
||||
end
|
||||
.as_json
|
||||
end
|
||||
|
||||
def direct_message_channels
|
||||
(object.direct_message_channels || [])
|
||||
.map do |channel|
|
||||
|
||||
@@ -20,6 +20,7 @@ module Chat
|
||||
# @param [Integer] id of the channel
|
||||
# @param [Hash] params_to_create
|
||||
# @option params_to_create [Array<String>] usernames
|
||||
# @option params_to_create [Array<String>] groups
|
||||
# @return [Service::Base::Context]
|
||||
contract
|
||||
model :channel
|
||||
@@ -27,6 +28,7 @@ module Chat
|
||||
model :users, optional: true
|
||||
|
||||
transaction do
|
||||
step :validate_user_count
|
||||
step :upsert_memberships
|
||||
step :recompute_users_count
|
||||
step :notice_channel
|
||||
@@ -35,20 +37,15 @@ module Chat
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :usernames, :array
|
||||
validates :usernames, presence: true
|
||||
attribute :groups, :array
|
||||
|
||||
attribute :channel_id, :integer
|
||||
validates :channel_id, presence: true
|
||||
|
||||
validate :usernames_length
|
||||
validate :target_presence
|
||||
|
||||
def usernames_length
|
||||
if usernames && usernames.length > SiteSetting.chat_max_direct_message_users + 1 # 1 for current user
|
||||
errors.add(
|
||||
:usernames,
|
||||
"should have less than #{SiteSetting.chat_max_direct_message_users} elements",
|
||||
)
|
||||
end
|
||||
def target_presence
|
||||
usernames.present? || groups.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,17 +57,23 @@ module Chat
|
||||
end
|
||||
|
||||
def fetch_users(contract:, channel:, **)
|
||||
::User.where(
|
||||
"username IN (?) AND id NOT IN (?)",
|
||||
[*contract.usernames],
|
||||
channel.chatable.direct_message_users.select(:user_id),
|
||||
).to_a
|
||||
::Chat::UsersFromUsernamesAndGroupsQuery.call(
|
||||
usernames: contract.usernames,
|
||||
groups: contract.groups,
|
||||
excluded_user_ids: channel.chatable.direct_message_users.pluck(:user_id),
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
::Chat::Channel.includes(:chatable).find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def validate_user_count(channel:, users:, **)
|
||||
if channel.user_count + users.length > SiteSetting.chat_max_direct_message_users
|
||||
fail!("should have less than #{SiteSetting.chat_max_direct_message_users} elements")
|
||||
end
|
||||
end
|
||||
|
||||
def upsert_memberships(channel:, users:, **)
|
||||
always_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ module Chat
|
||||
# @param [Guardian] guardian
|
||||
# @param [Hash] params_to_create
|
||||
# @option params_to_create [Array<String>] target_usernames
|
||||
# @option params_to_create [Array<String>] target_groups
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
policy :can_create_direct_message
|
||||
@@ -32,6 +33,7 @@ module Chat
|
||||
class_name: Chat::DirectMessageChannel::CanCommunicateAllPartiesPolicy
|
||||
model :direct_message, :fetch_or_create_direct_message
|
||||
model :channel, :fetch_or_create_channel
|
||||
step :validate_user_count
|
||||
step :set_optional_name
|
||||
step :update_memberships
|
||||
step :recompute_users_count
|
||||
@@ -40,7 +42,13 @@ module Chat
|
||||
class Contract
|
||||
attribute :name, :string
|
||||
attribute :target_usernames, :array
|
||||
validates :target_usernames, presence: true
|
||||
attribute :target_groups, :array
|
||||
|
||||
validate :target_presence
|
||||
|
||||
def target_presence
|
||||
target_usernames.present? || target_groups.present?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -50,13 +58,22 @@ module Chat
|
||||
end
|
||||
|
||||
def fetch_target_users(guardian:, contract:, **)
|
||||
User.where(username: [guardian.user.username, *contract.target_usernames]).to_a
|
||||
::Chat::UsersFromUsernamesAndGroupsQuery.call(
|
||||
usernames: [*contract.target_usernames, guardian.user.username],
|
||||
groups: contract.target_groups,
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_user_comm_screener(target_users:, guardian:, **)
|
||||
UserCommScreener.new(acting_user: guardian.user, target_user_ids: target_users.map(&:id))
|
||||
end
|
||||
|
||||
def validate_user_count(target_users:, **)
|
||||
if target_users.length > SiteSetting.chat_max_direct_message_users
|
||||
fail!("should have less than #{SiteSetting.chat_max_direct_message_users} elements")
|
||||
end
|
||||
end
|
||||
|
||||
def actor_allows_dms(user_comm_screener:, **)
|
||||
!user_comm_screener.actor_disallowing_all_pms?
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Returns a list of chatables (users, category channels, direct message channels) that can be chatted with.
|
||||
# Returns a list of chatables (users, groups ,category channels, direct message channels) that can be chatted with.
|
||||
#
|
||||
# @example
|
||||
# Chat::SearchChatable.call(term: "@bob", guardian: guardian)
|
||||
@@ -18,6 +18,7 @@ module Chat
|
||||
step :clean_term
|
||||
model :memberships, optional: true
|
||||
model :users, optional: true
|
||||
model :groups, optional: true
|
||||
model :category_channels, optional: true
|
||||
model :direct_message_channels, optional: true
|
||||
|
||||
@@ -25,6 +26,7 @@ module Chat
|
||||
class Contract
|
||||
attribute :term, :string, default: ""
|
||||
attribute :include_users, :boolean, default: true
|
||||
attribute :include_groups, :boolean, default: true
|
||||
attribute :include_category_channels, :boolean, default: true
|
||||
attribute :include_direct_message_channels, :boolean, default: true
|
||||
attribute :excluded_memberships_channel_id, :integer
|
||||
@@ -46,6 +48,12 @@ module Chat
|
||||
search_users(context, guardian, contract)
|
||||
end
|
||||
|
||||
def fetch_groups(guardian:, contract:, **)
|
||||
return unless contract.include_groups
|
||||
return unless guardian.can_create_direct_message?
|
||||
search_groups(context, guardian, contract)
|
||||
end
|
||||
|
||||
def fetch_category_channels(guardian:, contract:, **)
|
||||
return unless contract.include_category_channels
|
||||
return if !SiteSetting.enable_public_channels
|
||||
@@ -109,5 +117,15 @@ module Chat
|
||||
|
||||
user_search
|
||||
end
|
||||
|
||||
def search_groups(context, guardian, contract)
|
||||
Group
|
||||
.visible_groups(guardian.user)
|
||||
.includes(users: :user_option)
|
||||
.where(
|
||||
"groups.name ILIKE :term_like OR groups.full_name ILIKE :term_like",
|
||||
term_like: "%#{context.term}%",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user