diff --git a/plugins/chat/app/queries/chat/users_from_usernames_and_groups_query.rb b/plugins/chat/app/queries/chat/users_from_usernames_and_groups_query.rb new file mode 100644 index 00000000000..3160f9a2b3a --- /dev/null +++ b/plugins/chat/app/queries/chat/users_from_usernames_and_groups_query.rb @@ -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 diff --git a/plugins/chat/app/serializers/chat/chatable_group_serializer.rb b/plugins/chat/app/serializers/chat/chatable_group_serializer.rb new file mode 100644 index 00000000000..d2ada8ea0ca --- /dev/null +++ b/plugins/chat/app/serializers/chat/chatable_group_serializer.rb @@ -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 diff --git a/plugins/chat/app/serializers/chat/chatables_serializer.rb b/plugins/chat/app/serializers/chat/chatables_serializer.rb index eada1f105cf..dd35af1f6b6 100644 --- a/plugins/chat/app/serializers/chat/chatables_serializer.rb +++ b/plugins/chat/app/serializers/chat/chatables_serializer.rb @@ -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| diff --git a/plugins/chat/app/services/chat/add_users_to_channel.rb b/plugins/chat/app/services/chat/add_users_to_channel.rb index 6303fbc8bd4..dc6645b1889 100644 --- a/plugins/chat/app/services/chat/add_users_to_channel.rb +++ b/plugins/chat/app/services/chat/add_users_to_channel.rb @@ -20,6 +20,7 @@ module Chat # @param [Integer] id of the channel # @param [Hash] params_to_create # @option params_to_create [Array] usernames + # @option params_to_create [Array] 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] diff --git a/plugins/chat/app/services/chat/create_direct_message_channel.rb b/plugins/chat/app/services/chat/create_direct_message_channel.rb index 8ce61377ed2..d211904067b 100644 --- a/plugins/chat/app/services/chat/create_direct_message_channel.rb +++ b/plugins/chat/app/services/chat/create_direct_message_channel.rb @@ -19,6 +19,7 @@ module Chat # @param [Guardian] guardian # @param [Hash] params_to_create # @option params_to_create [Array] target_usernames + # @option params_to_create [Array] 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 diff --git a/plugins/chat/app/services/chat/search_chatable.rb b/plugins/chat/app/services/chat/search_chatable.rb index 30e45541d64..e8b12f72cdc 100644 --- a/plugins/chat/app/services/chat/search_chatable.rb +++ b/plugins/chat/app/services/chat/search_chatable.rb @@ -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 diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/add-members.gjs b/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/add-members.gjs index e274f1280e9..0a7904c1e5f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/add-members.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/add-members.gjs @@ -17,9 +17,14 @@ export default class AddMembers extends Component { @service loadingSlider; get membersCount() { - return ( - this.args.members?.length + (this.args.channel?.membershipsCount ?? 0) - ); + const userCount = this.args.members?.reduce((acc, member) => { + if (member.type === "group") { + return acc + member.model.chat_enabled_user_count; + } else { + return acc + 1; + } + }, 0); + return userCount + (this.args.channel?.membershipsCount ?? 0); } @action @@ -27,10 +32,18 @@ export default class AddMembers extends Component { try { this.loadingSlider.transitionStarted(); - await this.chatApi.addMembersToChannel( - this.args.channel.id, - this.args.members.mapBy("model.username") - ); + const usernames = this.args.members + .filter((member) => member.type === "user") + .mapBy("model.username"); + + const groups = this.args.members + .filter((member) => member.type === "group") + .mapBy("model.name"); + + await this.chatApi.addMembersToChannel(this.args.channel.id, { + usernames, + groups, + }); this.toasts.success({ data: { message: I18n.t("saved") } }); this.router.transitionTo( @@ -58,6 +71,7 @@ export default class AddMembers extends Component { @onChange={{@onChangeMembers}} @close={{@close}} @cancel={{@cancel}} + @membersCount={{this.membersCount}} @maxReached={{gte this.membersCount this.siteSettings.chat_max_direct_message_users diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel.gjs b/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel.gjs index 60c8b881b7f..15faaf7a4e7 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel.gjs @@ -2,6 +2,7 @@ import Component from "@glimmer/component"; import { inject as service } from "@ember/service"; import concatClass from "discourse/helpers/concat-class"; import gt from "truth-helpers/helpers/gt"; +import not from "truth-helpers/helpers/not"; import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title"; export default class Channel extends Component { @@ -16,7 +17,10 @@ export default class Channel extends Component { }