mirror of
https://github.com/discourse/discourse.git
synced 2025-04-25 23:54:30 +08:00
FEATURE: introduces group channels
This commit is contained in:
parent
184f038cbf
commit
7a4671c6d1
@ -28,4 +28,12 @@ class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
with_service(Chat::AddUsersToChannel) do
|
||||
on_failed_policy(:can_add_users_to_channel) do
|
||||
render_json_error("Users can't be added to this channel")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,21 +27,5 @@ module Chat
|
||||
self.slug = Slug.for(self.title.strip, "")
|
||||
self.slug = "" if duplicate_slug?
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
if self.slug.present?
|
||||
# if we don't unescape it first we strip the % from the encoded version
|
||||
slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug
|
||||
self.slug = Slug.for(slug, "", method: :encoded)
|
||||
|
||||
if self.slug.blank?
|
||||
errors.add(:slug, :invalid)
|
||||
elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.slug_contains_non_ascii_chars"))
|
||||
elsif duplicate_slug?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.is_already_in_use"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,6 +27,7 @@ module Chat
|
||||
class_name: "Chat::Message",
|
||||
foreign_key: :last_message_id,
|
||||
optional: true
|
||||
|
||||
def last_message
|
||||
super || NullMessage.new
|
||||
end
|
||||
@ -109,9 +110,29 @@ module Chat
|
||||
|
||||
%i[allowed_user_ids allowed_group_ids chatable_url].each { |name| define_method(name) { nil } }
|
||||
|
||||
def ensure_slug_ok
|
||||
if self.slug.present?
|
||||
# if we don't unescape it first we strip the % from the encoded version
|
||||
slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug
|
||||
self.slug = Slug.for(slug, "", method: :encoded)
|
||||
|
||||
if self.slug.blank?
|
||||
errors.add(:slug, :invalid)
|
||||
elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.slug_contains_non_ascii_chars"))
|
||||
elsif duplicate_slug?
|
||||
errors.add(:slug, I18n.t("chat.category_channel.errors.is_already_in_use"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def membership_for(user)
|
||||
if user_chat_channel_memberships.loaded?
|
||||
user_chat_channel_memberships.detect { |m| m.user_id == user.id }
|
||||
else
|
||||
user_chat_channel_memberships.find_by(user: user)
|
||||
end
|
||||
end
|
||||
|
||||
def add(user)
|
||||
Chat::ChannelMembershipManager.new(self).follow(user)
|
||||
@ -177,6 +198,7 @@ module Chat
|
||||
AND (users.suspended_till IS NULL OR users.suspended_till <= CURRENT_TIMESTAMP)
|
||||
AND NOT users.staged
|
||||
AND user_chat_channel_memberships.following
|
||||
and users.id > 0
|
||||
GROUP BY user_chat_channel_memberships.chat_channel_id
|
||||
) subquery
|
||||
WHERE channels.id = subquery.chat_channel_id
|
||||
|
@ -66,6 +66,7 @@ end
|
||||
# Table name: direct_message_channels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# group :boolean
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
@ -20,12 +20,8 @@ module Chat
|
||||
direct_message.chat_channel_title_for_user(self, user)
|
||||
end
|
||||
|
||||
def ensure_slug_ok
|
||||
true
|
||||
end
|
||||
|
||||
def generate_auto_slug
|
||||
self.slug = nil
|
||||
return if !self.slug.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -114,7 +114,7 @@ module Chat
|
||||
return uploads.first.original_filename if cooked.blank? && uploads.present?
|
||||
|
||||
# this may return blank for some complex things like quotes, that is acceptable
|
||||
PrettyText.excerpt(cooked, max_length, strip_links: true)
|
||||
PrettyText.excerpt(cooked, max_length, strip_links: true, keep_mentions: true)
|
||||
end
|
||||
|
||||
def censored_excerpt(max_length: 50)
|
||||
|
@ -7,7 +7,7 @@ module Chat
|
||||
Chat::UserChatChannelMembership
|
||||
.joins(:user)
|
||||
.includes(:user)
|
||||
.where(user: User.activated.not_suspended.not_staged)
|
||||
.where(user: User.human_users.activated.not_suspended.not_staged)
|
||||
.where(chat_channel: channel, following: true)
|
||||
|
||||
return query.count if count_only
|
||||
|
15
plugins/chat/app/serializers/chat/basic_user_serializer.rb
Normal file
15
plugins/chat/app/serializers/chat/basic_user_serializer.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
class BasicUserSerializer < BasicUserSerializer
|
||||
attributes :can_chat, :has_chat_enabled
|
||||
|
||||
def can_chat
|
||||
SiteSetting.chat_enabled && scope&.can_chat?
|
||||
end
|
||||
|
||||
def has_chat_enabled
|
||||
can_chat && object.user_option&.chat_enabled
|
||||
end
|
||||
end
|
||||
end
|
@ -2,9 +2,9 @@
|
||||
|
||||
module Chat
|
||||
class DirectMessageSerializer < ApplicationSerializer
|
||||
attributes :id
|
||||
attribute :group
|
||||
|
||||
has_many :users, serializer: Chat::ChatableUserSerializer, embed: :objects
|
||||
has_many :users, serializer: ::Chat::ChatableUserSerializer, embed: :objects
|
||||
|
||||
def users
|
||||
users = object.direct_message_users.map(&:user).map { |u| u || Chat::NullUser.new }
|
||||
|
@ -37,7 +37,7 @@ module Chat
|
||||
def mentioned_users
|
||||
object
|
||||
.chat_mentions
|
||||
.includes(:user)
|
||||
.includes(user: :user_status)
|
||||
.map(&:user)
|
||||
.compact
|
||||
.sort_by(&:id)
|
||||
|
@ -7,8 +7,8 @@ module Chat
|
||||
:last_reply_id,
|
||||
:participant_count,
|
||||
:reply_count
|
||||
has_many :participant_users, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :last_reply_user, serializer: BasicUserSerializer, embed: :objects
|
||||
has_many :participant_users, serializer: ::BasicUserSerializer, embed: :objects
|
||||
has_one :last_reply_user, serializer: ::BasicUserSerializer, embed: :objects
|
||||
|
||||
def initialize(object, opts)
|
||||
super(object, opts)
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
module Chat
|
||||
class UserChannelMembershipSerializer < BaseChannelMembershipSerializer
|
||||
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :user, serializer: ::Chat::BasicUserSerializer, embed: :objects
|
||||
|
||||
def user
|
||||
object.user
|
||||
object.user || Chat::NullUser.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
139
plugins/chat/app/services/chat/add_users_to_channel.rb
Normal file
139
plugins/chat/app/services/chat/add_users_to_channel.rb
Normal file
@ -0,0 +1,139 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Service responsible to add users to a channel.
|
||||
# The guardian passed in is the "acting user" when adding users.
|
||||
# The service is essentially creating memberships for the users.
|
||||
#
|
||||
# @example
|
||||
# ::Chat::AddUsersToChannel.call(
|
||||
# guardian: guardian,
|
||||
# channel_id: 1,
|
||||
# usernames: ["bob", "alice"]
|
||||
# )
|
||||
#
|
||||
class AddUsersToChannel
|
||||
include Service::Base
|
||||
|
||||
# @!method call(guardian:, **params_to_create)
|
||||
# @param [Guardian] guardian
|
||||
# @param [Integer] id of the channel
|
||||
# @param [Hash] params_to_create
|
||||
# @option params_to_create [Array<String>] usernames
|
||||
# @return [Service::Base::Context]
|
||||
contract
|
||||
model :channel
|
||||
policy :can_add_users_to_channel
|
||||
model :users
|
||||
|
||||
transaction do
|
||||
step :upsert_memberships
|
||||
step :recompute_users_count
|
||||
step :notice_channel
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :usernames, :array
|
||||
validates :usernames, presence: true
|
||||
|
||||
attribute :channel_id, :integer
|
||||
validates :channel_id, presence: true
|
||||
|
||||
validate :usernames_length
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_add_users_to_channel(guardian:, channel:, **)
|
||||
(guardian.user.admin? || channel.joined_by?(guardian.user)) &&
|
||||
channel.direct_message_channel? && channel.chatable.group
|
||||
end
|
||||
|
||||
def fetch_users(contract:, channel:, **)
|
||||
::User.where(
|
||||
"username IN (?) AND id NOT IN (?)",
|
||||
[*contract.usernames],
|
||||
channel.allowed_user_ids,
|
||||
).to_a
|
||||
end
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
::Chat::Channel.includes(:chatable).find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def upsert_memberships(channel:, users:, **)
|
||||
always_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
memberships =
|
||||
users.map do |user|
|
||||
{
|
||||
user_id: user.id,
|
||||
chat_channel_id: channel.id,
|
||||
muted: false,
|
||||
following: true,
|
||||
desktop_notification_level: always_level,
|
||||
mobile_notification_level: always_level,
|
||||
created_at: Time.zone.now,
|
||||
updated_at: Time.zone.now,
|
||||
}
|
||||
end
|
||||
|
||||
context.added_user_ids =
|
||||
::Chat::UserChatChannelMembership
|
||||
.upsert_all(
|
||||
memberships,
|
||||
unique_by: %i[user_id chat_channel_id],
|
||||
returning: Arel.sql("user_id, (xmax = '0') as inserted"),
|
||||
)
|
||||
.select { |row| row["inserted"] }
|
||||
.map { |row| row["user_id"] }
|
||||
|
||||
::Chat::DirectMessageUser.upsert_all(
|
||||
context.added_user_ids.map do |id|
|
||||
{
|
||||
user_id: id,
|
||||
direct_message_channel_id: channel.chatable.id,
|
||||
created_at: Time.zone.now,
|
||||
updated_at: Time.zone.now,
|
||||
}
|
||||
end,
|
||||
unique_by: %i[direct_message_channel_id user_id],
|
||||
)
|
||||
end
|
||||
|
||||
def recompute_users_count(channel:, **)
|
||||
channel.update!(
|
||||
user_count: ::Chat::ChannelMembershipsQuery.count(channel),
|
||||
user_count_stale: false,
|
||||
)
|
||||
end
|
||||
|
||||
def notice_channel(guardian:, channel:, users:, **)
|
||||
added_users = users.select { |u| context.added_user_ids.include?(u.id) }
|
||||
|
||||
return if added_users.blank?
|
||||
|
||||
::Chat::CreateMessage.call(
|
||||
guardian: Discourse.system_user.guardian,
|
||||
chat_channel_id: channel.id,
|
||||
message:
|
||||
I18n.t(
|
||||
"chat.channel.users_invited_to_channel",
|
||||
invited_users: added_users.map { |u| "@#{u.username}" }.join(", "),
|
||||
inviting_user: "@#{guardian.user.username}",
|
||||
count: added_users.count,
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -7,7 +7,7 @@ module Chat
|
||||
# are passed in.
|
||||
#
|
||||
# @example
|
||||
# Service::Chat::CreateDirectMessageChannel.call(
|
||||
# ::Chat::CreateDirectMessageChannel.call(
|
||||
# guardian: guardian,
|
||||
# target_usernames: ["bob", "alice"]
|
||||
# )
|
||||
@ -32,10 +32,13 @@ module Chat
|
||||
class_name: Chat::DirectMessageChannel::CanCommunicateAllPartiesPolicy
|
||||
model :direct_message, :fetch_or_create_direct_message
|
||||
model :channel, :fetch_or_create_channel
|
||||
step :set_optional_name
|
||||
step :update_memberships
|
||||
step :recompute_users_count
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :name, :string
|
||||
attribute :target_usernames, :array
|
||||
validates :target_usernames, presence: true
|
||||
end
|
||||
@ -58,17 +61,26 @@ module Chat
|
||||
!user_comm_screener.actor_disallowing_all_pms?
|
||||
end
|
||||
|
||||
def fetch_or_create_direct_message(target_users:, **)
|
||||
Chat::DirectMessage.for_user_ids(target_users.map(&:id)) ||
|
||||
Chat::DirectMessage.create(user_ids: target_users.map(&:id))
|
||||
def fetch_or_create_direct_message(target_users:, contract:, **)
|
||||
ids = target_users.map(&:id)
|
||||
|
||||
if ids.size > 2 || contract.name.present?
|
||||
::Chat::DirectMessage.create(user_ids: ids, group: true)
|
||||
else
|
||||
::Chat::DirectMessage.for_user_ids(ids) || ::Chat::DirectMessage.create(user_ids: ids)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_or_create_channel(direct_message:, **)
|
||||
Chat::DirectMessageChannel.find_or_create_by(chatable: direct_message)
|
||||
::Chat::DirectMessageChannel.find_or_create_by(chatable: direct_message)
|
||||
end
|
||||
|
||||
def set_optional_name(channel:, contract:, **)
|
||||
channel.update!(name: contract.name) if contract.name&.length&.positive?
|
||||
end
|
||||
|
||||
def update_memberships(channel:, target_users:, **)
|
||||
always_level = Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
always_level = ::Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
memberships =
|
||||
target_users.map do |user|
|
||||
@ -84,10 +96,17 @@ module Chat
|
||||
}
|
||||
end
|
||||
|
||||
Chat::UserChatChannelMembership.upsert_all(
|
||||
::Chat::UserChatChannelMembership.upsert_all(
|
||||
memberships,
|
||||
unique_by: %i[user_id chat_channel_id],
|
||||
)
|
||||
end
|
||||
|
||||
def recompute_users_count(channel:, **)
|
||||
channel.update!(
|
||||
user_count: ::Chat::ChannelMembershipsQuery.count(channel),
|
||||
user_count_stale: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -22,9 +22,9 @@ module Chat
|
||||
policy :no_silenced_user
|
||||
contract
|
||||
model :channel
|
||||
step :enforce_system_membership
|
||||
policy :allowed_to_join_channel
|
||||
policy :allowed_to_create_message_in_channel, class_name: Chat::Channel::MessageCreationPolicy
|
||||
step :enforce_system_membership
|
||||
model :channel_membership
|
||||
model :reply, optional: true
|
||||
policy :ensure_reply_consistency
|
||||
@ -76,7 +76,7 @@ module Chat
|
||||
end
|
||||
|
||||
def enforce_system_membership(guardian:, channel:, **)
|
||||
channel.add(guardian.user) if guardian.user.is_system_user?
|
||||
channel.add(guardian.user) if guardian.user&.is_system_user?
|
||||
end
|
||||
|
||||
def fetch_channel_membership(guardian:, channel:, **)
|
||||
|
@ -59,7 +59,7 @@ module Chat
|
||||
private
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
::Chat::Channel.strict_loading.includes(:chatable).find_by(id: contract.channel_id)
|
||||
::Chat::Channel.includes(:chatable).find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def fetch_optional_membership(channel:, guardian:, **)
|
||||
|
@ -15,50 +15,41 @@ module Chat
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
contract
|
||||
step :set_mode
|
||||
step :clean_term
|
||||
step :fetch_memberships
|
||||
step :fetch_users
|
||||
step :fetch_category_channels
|
||||
step :fetch_direct_message_channels
|
||||
model :memberships
|
||||
model :users, optional: true
|
||||
model :category_channels, optional: true
|
||||
model :direct_message_channels, optional: true
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :term, default: ""
|
||||
attribute :term, :string, default: ""
|
||||
attribute :include_users, :boolean, default: true
|
||||
attribute :include_category_channels, :boolean, default: true
|
||||
attribute :include_direct_message_channels, :boolean, default: true
|
||||
attribute :excluded_memberships_channel_id, :integer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_mode
|
||||
context.mode =
|
||||
if context.contract.term&.start_with?("#")
|
||||
:channel
|
||||
elsif context.contract.term&.start_with?("@")
|
||||
:user
|
||||
else
|
||||
:all
|
||||
end
|
||||
end
|
||||
|
||||
def clean_term(contract:, **)
|
||||
context.term = contract.term.downcase&.gsub(/^#+/, "")&.gsub(/^@+/, "")&.strip
|
||||
end
|
||||
|
||||
def fetch_memberships(guardian:, **)
|
||||
context.memberships = ::Chat::ChannelMembershipManager.all_for_user(guardian.user)
|
||||
::Chat::ChannelMembershipManager.all_for_user(guardian.user)
|
||||
end
|
||||
|
||||
def fetch_users(guardian:, **)
|
||||
def fetch_users(guardian:, contract:, **)
|
||||
return unless contract.include_users
|
||||
return unless guardian.can_create_direct_message?
|
||||
return if context.mode == :channel
|
||||
context.users = search_users(context.term, guardian)
|
||||
search_users(context, guardian, contract)
|
||||
end
|
||||
|
||||
def fetch_category_channels(guardian:, **)
|
||||
return if context.mode == :user
|
||||
def fetch_category_channels(guardian:, contract:, **)
|
||||
return unless contract.include_category_channels
|
||||
return if !SiteSetting.enable_public_channels
|
||||
|
||||
context.category_channels =
|
||||
::Chat::ChannelFetcher.secured_public_channel_search(
|
||||
guardian,
|
||||
filter_on_category_name: false,
|
||||
@ -69,24 +60,20 @@ module Chat
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_direct_message_channels(guardian:, **args)
|
||||
return if context.mode == :user
|
||||
|
||||
user_ids = nil
|
||||
if context.term.length > 0
|
||||
user_ids =
|
||||
(context.users.nil? ? search_users(context.term, guardian) : context.users).map(&:id)
|
||||
end
|
||||
def fetch_direct_message_channels(guardian:, users:, contract:, **args)
|
||||
return unless contract.include_direct_message_channels
|
||||
|
||||
channels =
|
||||
::Chat::ChannelFetcher.secured_direct_message_channels_search(
|
||||
guardian.user.id,
|
||||
guardian,
|
||||
limit: 10,
|
||||
user_ids: user_ids,
|
||||
match_filter_on_starts_with: false,
|
||||
filter: context.term,
|
||||
) || []
|
||||
|
||||
if user_ids.present? && context.mode == :all
|
||||
if users && contract.include_users
|
||||
user_ids = users.map(&:id)
|
||||
channels =
|
||||
channels.reject do |channel|
|
||||
channel_user_ids = channel.allowed_user_ids - [guardian.user.id]
|
||||
@ -96,17 +83,31 @@ module Chat
|
||||
end
|
||||
end
|
||||
|
||||
context.direct_message_channels = channels
|
||||
channels
|
||||
end
|
||||
|
||||
def search_users(term, guardian)
|
||||
user_search = ::UserSearch.new(term, limit: 10)
|
||||
def search_users(context, guardian, contract)
|
||||
user_search = ::UserSearch.new(context.term, limit: 10)
|
||||
|
||||
if term.blank?
|
||||
user_search.scoped_users.includes(:user_option)
|
||||
if context.term.blank?
|
||||
user_search = user_search.scoped_users.real.includes(:user_option)
|
||||
else
|
||||
user_search.search.includes(:user_option)
|
||||
user_search = user_search.search.real.includes(:user_option)
|
||||
end
|
||||
|
||||
if context.excluded_memberships_channel_id
|
||||
user_search =
|
||||
user_search.where(
|
||||
"NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM user_chat_channel_memberships
|
||||
WHERE user_chat_channel_memberships.user_id = users.id AND user_chat_channel_memberships.chat_channel_id = ?
|
||||
)",
|
||||
context.excluded_memberships_channel_id,
|
||||
)
|
||||
end
|
||||
|
||||
user_search
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -7,7 +7,7 @@ module Chat
|
||||
# and threading_enabled are also editable.
|
||||
#
|
||||
# @example
|
||||
# Service::Chat::UpdateChannel.call(
|
||||
# ::Chat::UpdateChannel.call(
|
||||
# channel_id: 2,
|
||||
# guardian: guardian,
|
||||
# name: "SuperChannel",
|
||||
@ -26,13 +26,13 @@ module Chat
|
||||
# @option params_to_edit [String,nil] name
|
||||
# @option params_to_edit [String,nil] description
|
||||
# @option params_to_edit [String,nil] slug
|
||||
# @option params_to_edit [Boolean] threading_enabled
|
||||
# @option params_to_edit [Boolean] auto_join_users Only valid for {CategoryChannel}. Whether active users
|
||||
# with permission to see the category should automatically join the channel.
|
||||
# @option params_to_edit [Boolean] allow_channel_wide_mentions Allow the use of @here and @all in the channel.
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
model :channel, :fetch_channel
|
||||
policy :no_direct_message_channel
|
||||
policy :check_channel_permission
|
||||
contract default_values_from: :channel
|
||||
step :update_channel
|
||||
@ -62,10 +62,6 @@ module Chat
|
||||
Chat::Channel.find_by(id: channel_id)
|
||||
end
|
||||
|
||||
def no_direct_message_channel(channel:, **)
|
||||
!channel.direct_message_channel?
|
||||
end
|
||||
|
||||
def check_channel_permission(guardian:, channel:, **)
|
||||
guardian.can_preview_chat_channel?(channel) && guardian.can_edit_chat_channel?
|
||||
end
|
||||
|
@ -1,14 +1,19 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
|
||||
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
|
||||
export default class ChatChannelMessageEmojiPicker extends Component {
|
||||
@service chatChannelInfoRouteOriginManager;
|
||||
@service site;
|
||||
@service modal;
|
||||
@service chatGuardian;
|
||||
|
||||
membersLabel = I18n.t("chat.channel_info.tabs.members");
|
||||
settingsLabel = I18n.t("chat.channel_info.tabs.settings");
|
||||
@ -16,13 +21,25 @@ export default class ChatChannelMessageEmojiPicker extends Component {
|
||||
backToAllChannelsLabel = I18n.t("chat.channel_info.back_to_channel");
|
||||
|
||||
get showTabs() {
|
||||
return this.site.desktopView && this.args.channel.isOpen;
|
||||
}
|
||||
|
||||
get canEditChannel() {
|
||||
return (
|
||||
this.site.desktopView &&
|
||||
this.args.channel.membershipsCount > 1 &&
|
||||
this.args.channel.isOpen
|
||||
this.chatGuardian.canEditChatChannel() &&
|
||||
(this.args.channel.isCategoryChannel ||
|
||||
(this.args.channel.isDirectMessageChannel &&
|
||||
this.args.channel.chatable.group))
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
editChannelTitle() {
|
||||
return this.modal.show(ChatModalEditChannelName, {
|
||||
model: this.args.channel,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-full-page-header">
|
||||
<div class="chat-channel-header-details">
|
||||
@ -48,6 +65,14 @@ export default class ChatChannelMessageEmojiPicker extends Component {
|
||||
</div>
|
||||
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
|
||||
{{#if this.canEditChannel}}
|
||||
<DButton
|
||||
@icon="pencil-alt"
|
||||
class="btn-flat"
|
||||
@action={{this.editChannelTitle}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,24 +1,31 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import I18n from "discourse-i18n";
|
||||
import gt from "truth-helpers/helpers/gt";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import MessageCreator from "discourse/plugins/chat/discourse/components/chat/message-creator";
|
||||
import ChatUserInfo from "discourse/plugins/chat/discourse/components/chat-user-info";
|
||||
import DcFilterInput from "discourse/plugins/chat/discourse/components/dc-filter-input";
|
||||
import { MODES } from "./chat/message-creator/constants";
|
||||
|
||||
export default class ChatChannelMembers extends Component {
|
||||
@service appEvents;
|
||||
@service chatApi;
|
||||
@service modal;
|
||||
@service loadingSlider;
|
||||
|
||||
@tracked filter = "";
|
||||
@tracked showAddMembers = false;
|
||||
|
||||
filterPlaceholder = I18n.t("chat.members_view.filter_placeholder");
|
||||
noMembershipsFoundLabel = I18n.t("chat.channel.no_memberships_found");
|
||||
@ -30,6 +37,22 @@ export default class ChatChannelMembers extends Component {
|
||||
});
|
||||
});
|
||||
|
||||
onEnter = modifier((element, [callback]) => {
|
||||
const handler = (event) => {
|
||||
if (event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(event);
|
||||
};
|
||||
|
||||
element.addEventListener("keydown", handler);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("keydown", handler);
|
||||
};
|
||||
});
|
||||
|
||||
fill = modifier((element) => {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
if (isElementInViewport(element)) {
|
||||
@ -78,13 +101,41 @@ export default class ChatChannelMembers extends Component {
|
||||
this.load();
|
||||
}
|
||||
|
||||
@action
|
||||
addMember() {
|
||||
this.showAddMembers = true;
|
||||
}
|
||||
|
||||
@action
|
||||
hideAddMember() {
|
||||
this.showAddMembers = false;
|
||||
}
|
||||
|
||||
@action
|
||||
openMemberCard(user, event) {
|
||||
event.preventDefault();
|
||||
DiscourseURL.routeTo(userPath(user.username_lower));
|
||||
}
|
||||
|
||||
async debouncedLoad() {
|
||||
this.loadingSlider.transitionStarted();
|
||||
await this.members.load({ limit: 20 });
|
||||
this.loadingSlider.transitionEnded();
|
||||
}
|
||||
|
||||
get addMembersMode() {
|
||||
return MODES.add_members;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showAddMembers}}
|
||||
<MessageCreator
|
||||
@mode={{this.addMembersMode}}
|
||||
@channel={{@channel}}
|
||||
@onClose={{this.hideAddMember}}
|
||||
@onCancel={{this.hideAddMember}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="chat-channel-members">
|
||||
<DcFilterInput
|
||||
@class="chat-channel-members__filter"
|
||||
@ -94,12 +145,34 @@ export default class ChatChannelMembers extends Component {
|
||||
{{this.focusInput}}
|
||||
/>
|
||||
|
||||
{{#if (gt @channel.membershipsCount 0)}}
|
||||
<ul class="chat-channel-members__list" {{this.fill}}>
|
||||
{{#each this.members as |membership|}}
|
||||
<li class="chat-channel-members__list-item">
|
||||
<ChatUserInfo @user={{membership.user}} @avatarSize="tiny" />
|
||||
{{#if @channel.chatable.group}}
|
||||
<li
|
||||
class="chat-channel-members__list-item -add-member"
|
||||
role="button"
|
||||
{{on "click" this.addMember}}
|
||||
{{this.onEnter this.addMember}}
|
||||
tabindex="0"
|
||||
>
|
||||
{{icon "plus"}}
|
||||
<span>Add Member</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#each this.members as |membership|}}
|
||||
{{#unless (eq membership.user.id -1)}}
|
||||
<li
|
||||
class="chat-channel-members__list-item -member"
|
||||
{{on "click" (fn this.openMemberCard membership.user)}}
|
||||
{{this.onEnter (fn this.openMemberCard membership.user)}}
|
||||
tabindex="0"
|
||||
>
|
||||
<ChatUserInfo
|
||||
@user={{membership.user}}
|
||||
@avatarSize="tiny"
|
||||
@interactive={{false}}
|
||||
/>
|
||||
</li>
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{#if this.noResults}}
|
||||
<li
|
||||
@ -114,11 +187,7 @@ export default class ChatChannelMembers extends Component {
|
||||
<div {{this.loadMore}}>
|
||||
<br />
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="alert alert-info">
|
||||
{{this.noMembershipsLabel}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
@ -69,10 +69,6 @@ export default class ChatAboutScreen extends Component {
|
||||
return this.chatGuardian.canEditChatChannel();
|
||||
}
|
||||
|
||||
get shouldRenderTitleSection() {
|
||||
return this.args.channel.isCategoryChannel;
|
||||
}
|
||||
|
||||
get shouldRenderDescriptionSection() {
|
||||
return this.args.channel.isCategoryChannel;
|
||||
}
|
||||
@ -293,7 +289,7 @@ export default class ChatAboutScreen extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
onEditChannelName() {
|
||||
onEditChannelTitle() {
|
||||
return this.modal.show(ChatModalEditChannelName, {
|
||||
model: this.args.channel,
|
||||
});
|
||||
@ -309,7 +305,6 @@ export default class ChatAboutScreen extends Component {
|
||||
<template>
|
||||
<div class="chat-channel-settings">
|
||||
<ChatForm as |form|>
|
||||
{{#if this.shouldRenderTitleSection}}
|
||||
<form.section @title={{this.titleSectionTitle}} as |section|>
|
||||
<section.row>
|
||||
<:default>
|
||||
@ -333,7 +328,7 @@ export default class ChatAboutScreen extends Component {
|
||||
{{#if this.canEditChannel}}
|
||||
<DButton
|
||||
@label="chat.channel_settings.edit"
|
||||
@action={{this.onEditChannelName}}
|
||||
@action={{this.onEditChannelTitle}}
|
||||
class="edit-name-slug-btn btn-flat"
|
||||
/>
|
||||
{{/if}}
|
||||
@ -341,7 +336,6 @@ export default class ChatAboutScreen extends Component {
|
||||
|
||||
</section.row>
|
||||
</form.section>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.shouldRenderDescriptionSection}}
|
||||
<form.section @title={{this.descriptionSectionTitle}} as |section|>
|
||||
@ -567,7 +561,7 @@ export default class ChatAboutScreen extends Component {
|
||||
@channel={{@channel}}
|
||||
@options={{hash
|
||||
joinClass="btn-primary"
|
||||
leaveClass="btn-flat"
|
||||
leaveClass="btn-danger"
|
||||
joinIcon="sign-in-alt"
|
||||
leaveIcon="sign-out-alt"
|
||||
}}
|
||||
|
@ -0,0 +1,112 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { get, hash } from "@ember/helper";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import UserStatusMessage from "discourse/components/user-status-message";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
|
||||
import getFirstUser from "discourse/plugins/chat/discourse/lib/get-first-user";
|
||||
|
||||
export default class ChatChannelTitle extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get firstUser() {
|
||||
return getFirstUser(this.args.channel.chatable.users, this.currentUser);
|
||||
}
|
||||
|
||||
get users() {
|
||||
return this.args.channel.chatable.users;
|
||||
}
|
||||
|
||||
get groupDirectMessage() {
|
||||
return (
|
||||
this.args.channel.isDirectMessageChannel &&
|
||||
this.args.channel.chatable.group
|
||||
);
|
||||
}
|
||||
|
||||
get groupsDirectMessageTitle() {
|
||||
return this.args.channel.title || this.usernames;
|
||||
}
|
||||
|
||||
get usernames() {
|
||||
return this.users.mapBy("username").join(", ");
|
||||
}
|
||||
|
||||
get channelColorStyle() {
|
||||
return htmlSafe(`color: #${this.args.channel.chatable.color}`);
|
||||
}
|
||||
|
||||
get showUserStatus() {
|
||||
return !!(this.users.length === 1 && this.users[0].status);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__users-count">
|
||||
{{@channel.membershipsCount}}
|
||||
</span>
|
||||
{{else}}
|
||||
<div class="chat-channel-title__avatar">
|
||||
<ChatUserAvatar @user={{this.firstUser}} @interactive={{false}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.groupsDirectMessageTitle}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.firstUser.username}}
|
||||
</span>
|
||||
{{#if this.showUserStatus}}
|
||||
<UserStatusMessage
|
||||
@class="chat-channel-title__user-status-message"
|
||||
@status={{get this.users "0.status"}}
|
||||
@showDescription={{if this.site.mobileView "true"}}
|
||||
/>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="after-chat-channel-username"
|
||||
@outletArgs={{hash user=@user}}
|
||||
@tagName=""
|
||||
@connectorTagName=""
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @channel.isCategoryChannel}}
|
||||
<div class="chat-channel-title is-category">
|
||||
<span
|
||||
class="chat-channel-title__category-badge"
|
||||
style={{this.channelColorStyle}}
|
||||
>
|
||||
{{icon "d-chat"}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{icon "lock" class="chat-channel-title__restricted-category-icon"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="chat-channel-title__name">
|
||||
{{replaceEmoji @channel.title}}
|
||||
</span>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
{{#if @channel.chatable.users.length}}
|
||||
<div class="chat-channel-title__avatar">
|
||||
{{#if this.multiDm}}
|
||||
<span class="chat-channel-title__users-count">
|
||||
{{@channel.chatable.users.length}}
|
||||
</span>
|
||||
{{else}}
|
||||
<ChatUserAvatar @user={{get @channel.chatable.users "0"}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
{{#if @channel.chatable.users.length}}
|
||||
{{#if this.multiDm}}
|
||||
<span class="chat-channel-title__name">{{this.usernames}}</span>
|
||||
{{else}}
|
||||
{{#let (get @channel.chatable.users "0") as |user|}}
|
||||
<span class="chat-channel-title__name">{{user.username}}</span>
|
||||
{{#if this.showUserStatus}}
|
||||
<UserStatusMessage
|
||||
@class="chat-channel-title__user-status-message"
|
||||
@status={{get @channel.chatable.users "0.status"}}
|
||||
@showDescription={{if this.site.mobileView "true"}}
|
||||
/>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="after-chat-channel-username"
|
||||
@outletArgs={{hash user=user}}
|
||||
@tagName=""
|
||||
@connectorTagName=""
|
||||
/>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="chat-channel-title__name">Add users</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @channel.isCategoryChannel}}
|
||||
<div class="chat-channel-title is-category">
|
||||
<span
|
||||
class="chat-channel-title__category-badge"
|
||||
style={{this.channelColorStyle}}
|
||||
>
|
||||
{{d-icon "d-chat"}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{d-icon "lock" class="chat-channel-title__restricted-category-icon"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="chat-channel-title__name">
|
||||
{{replace-emoji @channel.title}}
|
||||
</span>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
@ -1,24 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default class ChatChannelTitle extends Component {
|
||||
get users() {
|
||||
return this.args.channel.chatable.users;
|
||||
}
|
||||
|
||||
get multiDm() {
|
||||
return this.users.length > 1;
|
||||
}
|
||||
|
||||
get usernames() {
|
||||
return this.users.mapBy("username").join(", ");
|
||||
}
|
||||
|
||||
get channelColorStyle() {
|
||||
return htmlSafe(`color: #${this.args.channel.chatable.color}`);
|
||||
}
|
||||
|
||||
get showUserStatus() {
|
||||
return !!(this.users.length === 1 && this.users[0].status);
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import or from "truth-helpers/helpers/or";
|
||||
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
|
||||
import ThreadsListButton from "discourse/plugins/chat/discourse/components/chat/thread/threads-list-button";
|
||||
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
|
||||
export default class ChatFullPageHeader extends Component {
|
||||
@service chatGuardian;
|
||||
@service chatStateManager;
|
||||
@service modal;
|
||||
@service router;
|
||||
@service site;
|
||||
|
||||
get displayed() {
|
||||
return this.args.displayed ?? true;
|
||||
}
|
||||
|
||||
get showThreadsListButton() {
|
||||
return (
|
||||
this.args.channel.threadingEnabled &&
|
||||
this.router.currentRoute.name !== "chat.channel.threads" &&
|
||||
this.router.currentRoute.name !== "chat.channel.thread.index" &&
|
||||
this.router.currentRoute.name !== "chat.channel.thread"
|
||||
);
|
||||
}
|
||||
|
||||
get canEditChannel() {
|
||||
return (
|
||||
this.chatGuardian.canEditChatChannel() &&
|
||||
(this.args.channel.isCategoryChannel ||
|
||||
(this.args.channel.isDirectMessageChannel &&
|
||||
this.args.channel.chatable.group))
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
editChannelTitle() {
|
||||
return this.modal.show(ChatModalEditChannelName, {
|
||||
model: this.args.channel,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-full-page-header"
|
||||
(unless @channel.isFollowing "-not-following")
|
||||
}}
|
||||
>
|
||||
<div class="chat-channel-header-details">
|
||||
{{#if this.site.mobileView}}
|
||||
<div class="chat-full-page-header__left-actions">
|
||||
<LinkTo
|
||||
@route="chat"
|
||||
class="chat-full-page-header__back-btn no-text btn-flat"
|
||||
>
|
||||
{{icon "chevron-left"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<LinkTo
|
||||
@route="chat.channel.info"
|
||||
@models={{@channel.routeModels}}
|
||||
class="chat-channel-title-wrapper"
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
</LinkTo>
|
||||
|
||||
{{#if this.canEditChannel}}
|
||||
<DButton
|
||||
@icon="pencil-alt"
|
||||
class="btn-flat"
|
||||
@action={{this.editChannelTitle}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or @channel.threadingEnabled this.site.desktopView)}}
|
||||
<div class="chat-full-page-header__right-actions">
|
||||
{{#if this.site.desktopView}}
|
||||
<DButton
|
||||
@icon="discourse-compress"
|
||||
@title="chat.close_full_page"
|
||||
class="open-drawer-btn btn-flat"
|
||||
@action={{@onCloseFullScreen}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showThreadsListButton}}
|
||||
<ThreadsListButton @channel={{@channel}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChatChannelStatus @channel={{@channel}} />
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-full-page-header"
|
||||
(unless @channel.isFollowing "-not-following")
|
||||
}}
|
||||
>
|
||||
<div class="chat-channel-header-details">
|
||||
{{#if this.site.mobileView}}
|
||||
<div class="chat-full-page-header__left-actions">
|
||||
<LinkTo
|
||||
@route="chat"
|
||||
class="chat-full-page-header__back-btn no-text btn-flat"
|
||||
>
|
||||
{{d-icon "chevron-left"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<LinkTo
|
||||
@route="chat.channel.info"
|
||||
@models={{@channel.routeModels}}
|
||||
class="chat-channel-title-wrapper"
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
</LinkTo>
|
||||
|
||||
{{#if (or @channel.threadingEnabled this.site.desktopView)}}
|
||||
<div class="chat-full-page-header__right-actions">
|
||||
{{#if this.site.desktopView}}
|
||||
<DButton
|
||||
@icon="discourse-compress"
|
||||
@title="chat.close_full_page"
|
||||
class="open-drawer-btn btn-flat"
|
||||
@action={{@onCloseFullScreen}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showThreadsListButton}}
|
||||
<Chat::Thread::ThreadsListButton @channel={{@channel}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChatChannelStatus @channel={{@channel}} />
|
||||
{{/if}}
|
@ -1,21 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatFullPageHeader extends Component {
|
||||
@service site;
|
||||
@service chatStateManager;
|
||||
@service router;
|
||||
|
||||
get displayed() {
|
||||
return this.args.displayed ?? true;
|
||||
}
|
||||
|
||||
get showThreadsListButton() {
|
||||
return (
|
||||
this.args.channel.threadingEnabled &&
|
||||
this.router.currentRoute.name !== "chat.channel.threads" &&
|
||||
this.router.currentRoute.name !== "chat.channel.thread.index" &&
|
||||
this.router.currentRoute.name !== "chat.channel.thread"
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
export default class ChatUserAvatar extends Component {
|
||||
@service chat;
|
||||
@ -37,19 +38,23 @@ export default class ChatUserAvatar extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
get userPath() {
|
||||
return userPath(this.args.user.username);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class={{concatClass "chat-user-avatar" (if this.isOnline "is-online")}}
|
||||
data-username={{@user.username}}
|
||||
>
|
||||
{{#if this.interactive}}
|
||||
<div
|
||||
role="button"
|
||||
class="chat-user-avatar__container clickable"
|
||||
<a
|
||||
class="chat-user-avatar__container"
|
||||
href={{this.userPath}}
|
||||
data-user-card={{@user.username}}
|
||||
>
|
||||
{{this.avatar}}
|
||||
</div>
|
||||
</a>
|
||||
{{else}}
|
||||
{{this.avatar}}
|
||||
{{/if}}
|
||||
|
@ -12,14 +12,25 @@ export default class ChatUserInfo extends Component {
|
||||
return userPath(this.args.user.username);
|
||||
}
|
||||
|
||||
get interactive() {
|
||||
return this.args.interactive ?? false;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @user}}
|
||||
<a href={{this.userPath}} data-user-card={{@user.username}}>
|
||||
<ChatUserAvatar @user={{@user}} @avatarSize={{this.avatarSize}} />
|
||||
</a>
|
||||
<ChatUserAvatar
|
||||
@user={{@user}}
|
||||
@avatarSize={{this.avatarSize}}
|
||||
@interactive={{this.interactive}}
|
||||
/>
|
||||
|
||||
{{#if this.interactive}}
|
||||
<a href={{this.userPath}} data-user-card={{@user.username}}>
|
||||
<ChatUserDisplayName @user={{@user}} />
|
||||
</a>
|
||||
{{else}}
|
||||
<ChatUserDisplayName @user={{@user}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
@ -93,6 +93,11 @@ export default class ChatComposerChannel extends ChatComposer {
|
||||
|
||||
#messageRecipients(channel) {
|
||||
if (channel.isDirectMessageChannel) {
|
||||
if (channel.chatable.group && channel.title) {
|
||||
return I18n.t("chat.placeholder_channel", {
|
||||
channelName: `#${channel.title}`,
|
||||
});
|
||||
} else {
|
||||
const directMessageRecipients = channel.chatable.users;
|
||||
if (
|
||||
directMessageRecipients.length === 1 &&
|
||||
@ -106,6 +111,7 @@ export default class ChatComposerChannel extends ChatComposer {
|
||||
.map((u) => u.name || `@${u.username}`)
|
||||
.join(I18n.t("word_connector.comma")),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return I18n.t("chat.placeholder_channel", {
|
||||
channelName: `#${channel.title}`,
|
||||
|
@ -1,141 +0,0 @@
|
||||
<div class="chat-message-creator__container">
|
||||
<div class="chat-message-creator">
|
||||
<div
|
||||
class="chat-message-creator__selection-container"
|
||||
{{did-insert this.focusInput}}
|
||||
...attributes
|
||||
>
|
||||
<div class="chat-message-creator__selection">
|
||||
<div class="chat-message-creator__search-icon-container">
|
||||
{{d-icon "search" class="chat-message-creator__search-icon"}}
|
||||
</div>
|
||||
|
||||
{{#each this.selection as |selection|}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-message-creator__selection-item"
|
||||
(concat "-" selection.type)
|
||||
(if
|
||||
(includes this.activeSelectionIdentifiers selection.identifier)
|
||||
"-active"
|
||||
)
|
||||
}}
|
||||
tabindex="-1"
|
||||
data-id={{selection.identifier}}
|
||||
{{on "click" (fn this.removeSelection selection.identifier)}}
|
||||
>
|
||||
{{component
|
||||
(concat "chat/message-creator/" selection.type "-selection")
|
||||
selection=selection
|
||||
}}
|
||||
<i
|
||||
class="chat-message-creator__selection__remove-btn"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{d-icon "times"}}
|
||||
</i>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<Input
|
||||
class="chat-message-creator__input"
|
||||
{{did-insert this.setQueryElement}}
|
||||
{{on "input" this.handleInput}}
|
||||
{{on "keydown" this.handleKeydown}}
|
||||
placeholder={{this.placeholder}}
|
||||
@value={{readonly this.query}}
|
||||
@type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DButton
|
||||
class="chat-message-creator__close-btn btn-flat"
|
||||
@icon="times"
|
||||
@action={{@onClose}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showResults}}
|
||||
<div class="chat-message-creator__content-container" role="presentation">
|
||||
<div
|
||||
class="chat-message-creator__content"
|
||||
role="listbox"
|
||||
aria-multiselectable="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{#if this.searchRequest.loading}}
|
||||
<div class="chat-message-creator__loader-container">
|
||||
<div class="chat-message-creator__loader spinner small"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#each this.searchRequest.value as |result|}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-message-creator__row"
|
||||
(concat "-" result.type)
|
||||
(unless result.enabled "-disabled")
|
||||
(if
|
||||
(eq this.activeResultIdentifier result.identifier) "-active"
|
||||
)
|
||||
(if
|
||||
(includes this.selectionIdentifiers result.identifier)
|
||||
"-selected"
|
||||
)
|
||||
}}
|
||||
data-id={{result.identifier}}
|
||||
tabindex="-1"
|
||||
role="option"
|
||||
{{on "click" (fn this.handleRowClick result.identifier)}}
|
||||
{{on "mousemove" (fn (mut this.activeResult) result)}}
|
||||
{{on "keydown" this.handleKeydown}}
|
||||
aria-selected={{if
|
||||
(includes this.selectionIdentifiers result.identifier)
|
||||
"true"
|
||||
"false"
|
||||
}}
|
||||
>
|
||||
{{component
|
||||
(concat "chat/message-creator/" result.type "-row")
|
||||
content=result
|
||||
selected=(includes
|
||||
this.selectionIdentifiers result.identifier
|
||||
)
|
||||
active=(eq this.activeResultIdentifier result.identifier)
|
||||
hasSelectedUsers=this.hasSelectedUsers
|
||||
}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.query.length}}
|
||||
<div class="chat-message-creator__no-items-container">
|
||||
<span class="chat-message-creator__no-items">
|
||||
{{i18n "chat.new_message_modal.no_items"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showFooter}}
|
||||
<div class="chat-message-creator__footer-container">
|
||||
<div class="chat-message-creator__footer">
|
||||
{{#if this.showShortcut}}
|
||||
<div class="chat-message-creator__shortcut">
|
||||
{{this.shortcutLabel}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.hasSelectedUsers}}
|
||||
<DButton
|
||||
class="chat-message-creator__open-dm-btn btn-primary"
|
||||
@action={{fn this.openChannel this.selection}}
|
||||
@translatedLabel={{this.openChannelLabel}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
@ -1,546 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { getOwner, setOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatChatable from "discourse/plugins/chat/discourse/models/chat-chatable";
|
||||
|
||||
const MAX_RESULTS = 10;
|
||||
const USER_PREFIX = "@";
|
||||
const CHANNEL_PREFIX = "#";
|
||||
const CHANNEL_TYPE = "channel";
|
||||
const USER_TYPE = "user";
|
||||
|
||||
class Search {
|
||||
@service("chat-api") api;
|
||||
@service chat;
|
||||
@service chatChannelsManager;
|
||||
|
||||
@tracked loading = false;
|
||||
@tracked value = [];
|
||||
@tracked query = "";
|
||||
|
||||
constructor(owner, options = {}) {
|
||||
setOwner(this, owner);
|
||||
|
||||
options.preload ??= false;
|
||||
options.onlyUsers ??= false;
|
||||
|
||||
if (!options.term && !options.preload) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.term && options.preload) {
|
||||
this.value = this.#loadExistingChannels();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.api
|
||||
.chatables({ term: options.term })
|
||||
.then((results) => {
|
||||
let chatables = [
|
||||
...results.users,
|
||||
...results.direct_message_channels,
|
||||
...results.category_channels,
|
||||
];
|
||||
|
||||
if (options.excludeUserId) {
|
||||
chatables = chatables.filter(
|
||||
(item) => item.identifier !== `u-${options.excludeUserId}`
|
||||
);
|
||||
}
|
||||
|
||||
this.value = chatables
|
||||
.map((item) => {
|
||||
const chatable = ChatChatable.create(item);
|
||||
chatable.tracking = this.#injectTracking(chatable);
|
||||
return chatable;
|
||||
})
|
||||
.slice(0, MAX_RESULTS);
|
||||
})
|
||||
.catch(() => (this.value = []))
|
||||
.finally(() => (this.loading = false));
|
||||
}
|
||||
|
||||
#loadExistingChannels() {
|
||||
return this.chatChannelsManager.allChannels
|
||||
.map((channel) => {
|
||||
let chatable;
|
||||
if (channel.chatable?.users?.length === 1) {
|
||||
chatable = ChatChatable.createUser(channel.chatable.users[0]);
|
||||
chatable.tracking = this.#injectTracking(chatable);
|
||||
} else {
|
||||
chatable = ChatChatable.createChannel(channel);
|
||||
chatable.tracking = channel.tracking;
|
||||
}
|
||||
return chatable;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(0, MAX_RESULTS);
|
||||
}
|
||||
|
||||
#injectTracking(chatable) {
|
||||
switch (chatable.type) {
|
||||
case CHANNEL_TYPE:
|
||||
return this.chatChannelsManager.allChannels.find(
|
||||
(channel) => channel.id === chatable.model.id
|
||||
)?.tracking;
|
||||
break;
|
||||
case USER_TYPE:
|
||||
return this.chatChannelsManager.directMessageChannels.find(
|
||||
(channel) =>
|
||||
channel.chatable.users.length === 1 &&
|
||||
channel.chatable.users[0].id === chatable.model.id
|
||||
)?.tracking;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class ChatMessageCreator extends Component {
|
||||
@service("chat-api") api;
|
||||
@service("chat-channel-composer") composer;
|
||||
@service chat;
|
||||
@service site;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked selection = new TrackedArray();
|
||||
@tracked activeSelection = new TrackedArray();
|
||||
@tracked query = "";
|
||||
@tracked queryElement = null;
|
||||
@tracked loading = false;
|
||||
@tracked activeSelectionIdentifiers = new TrackedArray();
|
||||
@tracked selectedIdentifiers = [];
|
||||
@tracked _activeResultIdentifier = null;
|
||||
|
||||
get placeholder() {
|
||||
if (
|
||||
this.siteSettings.enable_public_channels &&
|
||||
this.chat.userCanDirectMessage
|
||||
) {
|
||||
if (this.hasSelectedUsers) {
|
||||
return I18n.t("chat.new_message_modal.user_search_placeholder");
|
||||
} else {
|
||||
return I18n.t("chat.new_message_modal.default_search_placeholder");
|
||||
}
|
||||
} else if (this.siteSettings.enable_public_channels) {
|
||||
return I18n.t(
|
||||
"chat.new_message_modal.default_channel_search_placeholder"
|
||||
);
|
||||
} else if (this.chat.userCanDirectMessage) {
|
||||
if (this.hasSelectedUsers) {
|
||||
return I18n.t("chat.new_message_modal.user_search_placeholder");
|
||||
} else {
|
||||
return I18n.t("chat.new_message_modal.default_user_search_placeholder");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get showFooter() {
|
||||
return this.showShortcut || this.hasSelectedUsers;
|
||||
}
|
||||
|
||||
get showResults() {
|
||||
if (this.hasSelectedUsers && !this.query.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get shortcutLabel() {
|
||||
let username;
|
||||
|
||||
if (this.activeResult?.isUser) {
|
||||
username = this.activeResult.model.username;
|
||||
} else {
|
||||
username = this.activeResult.model.chatable.users[0].username;
|
||||
}
|
||||
|
||||
return htmlSafe(
|
||||
I18n.t("chat.new_message_modal.add_user_long", {
|
||||
username: escapeExpression(username),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get showShortcut() {
|
||||
return (
|
||||
!this.hasSelectedUsers &&
|
||||
this.searchRequest?.value?.length &&
|
||||
this.site.desktopView &&
|
||||
(this.activeResult?.isUser || this.activeResult?.isSingleUserChannel)
|
||||
);
|
||||
}
|
||||
|
||||
get activeResultIdentifier() {
|
||||
return (
|
||||
this._activeResultIdentifier ||
|
||||
this.searchRequest.value.find((result) => result.enabled)?.identifier
|
||||
);
|
||||
}
|
||||
|
||||
get hasSelectedUsers() {
|
||||
return this.selection.some((s) => s.isUser);
|
||||
}
|
||||
|
||||
get activeResult() {
|
||||
return this.searchRequest.value.findBy(
|
||||
"identifier",
|
||||
this.activeResultIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
set activeResult(result) {
|
||||
if (!result?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeResultIdentifier = result?.identifier;
|
||||
}
|
||||
|
||||
get selectionIdentifiers() {
|
||||
return this.selection.mapBy("identifier");
|
||||
}
|
||||
|
||||
get openChannelLabel() {
|
||||
const users = this.selection.mapBy("model");
|
||||
|
||||
return I18n.t("chat.placeholder_users", {
|
||||
commaSeparatedNames: users
|
||||
.map((u) => u.name || u.username)
|
||||
.join(I18n.t("word_connector.comma")),
|
||||
});
|
||||
}
|
||||
|
||||
@cached
|
||||
get searchRequest() {
|
||||
let term = this.query;
|
||||
|
||||
if (term?.length) {
|
||||
if (this.hasSelectedUsers && term.startsWith(CHANNEL_PREFIX)) {
|
||||
term = term.replace(/^#/, USER_PREFIX);
|
||||
}
|
||||
|
||||
if (this.hasSelectedUsers && !term.startsWith(USER_PREFIX)) {
|
||||
term = USER_PREFIX + term;
|
||||
}
|
||||
}
|
||||
|
||||
return new Search(getOwner(this), {
|
||||
term,
|
||||
preload: !this.selection?.length,
|
||||
onlyUsers: this.hasSelectedUsers,
|
||||
excludeUserId: this.hasSelectedUsers ? this.currentUser?.id : null,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onFilter(term) {
|
||||
this._activeResultIdentifier = null;
|
||||
this.activeSelectionIdentifiers = [];
|
||||
this.query = term;
|
||||
}
|
||||
|
||||
@action
|
||||
setQueryElement(element) {
|
||||
this.queryElement = element;
|
||||
}
|
||||
|
||||
@action
|
||||
focusInput() {
|
||||
schedule("afterRender", () => {
|
||||
this.queryElement.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
if (this.activeSelectionIdentifiers.length > 0) {
|
||||
this.activeSelectionIdentifiers = [];
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "a" && (event.metaKey || event.ctrlKey)) {
|
||||
this.activeSelectionIdentifiers = this.selection.mapBy("identifier");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
if (this.activeSelectionIdentifiers.length > 0) {
|
||||
this.activeSelectionIdentifiers.forEach((identifier) => {
|
||||
this.removeSelection(identifier);
|
||||
});
|
||||
this.activeSelectionIdentifiers = [];
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (this.activeResultIdentifier) {
|
||||
this.toggleSelection(this.activeResultIdentifier, {
|
||||
altSelection: event.shiftKey || event.ctrlKey,
|
||||
});
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (this.query?.length === 0) {
|
||||
this.openChannel(this.selection);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown" && this.searchRequest.value.length > 0) {
|
||||
this.activeSelectionIdentifiers = [];
|
||||
this._activeResultIdentifier = this.#getNextResult()?.identifier;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" && this.searchRequest.value.length > 0) {
|
||||
this.activeSelectionIdentifiers = [];
|
||||
this._activeResultIdentifier = this.#getPreviousResult()?.identifier;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const digit = this.#getDigit(event.code);
|
||||
if (event.ctrlKey && digit) {
|
||||
this._activeResultIdentifier = this.searchRequest.value.objectAt(
|
||||
digit - 1
|
||||
)?.identifier;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.selectionEnd !== 0 || event.target.selectionStart !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Backspace" && this.selection.length) {
|
||||
if (!this.activeSelectionIdentifiers.length) {
|
||||
this.activeSelectionIdentifiers = [this.#getLastSelection().identifier];
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else {
|
||||
this.activeSelectionIdentifiers.forEach((identifier) => {
|
||||
this.removeSelection(identifier);
|
||||
});
|
||||
this.activeSelectionIdentifiers = [];
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft" && !event.shiftKey) {
|
||||
this._activeResultIdentifier = null;
|
||||
this.activeSelectionIdentifiers = [
|
||||
this.#getPreviousSelection()?.identifier,
|
||||
].filter(Boolean);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight" && !event.shiftKey) {
|
||||
this._activeResultIdentifier = null;
|
||||
this.activeSelectionIdentifiers = [
|
||||
this.#getNextSelection()?.identifier,
|
||||
].filter(Boolean);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
replaceActiveSelection(selection) {
|
||||
this.activeSelection.clear();
|
||||
this.activeSelection.push(selection.identifier);
|
||||
}
|
||||
|
||||
@action
|
||||
handleInput(event) {
|
||||
discourseDebounce(this, this.onFilter, event.target.value, INPUT_DELAY);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSelection(identifier, options = {}) {
|
||||
if (this.selectionIdentifiers.includes(identifier)) {
|
||||
this.removeSelection(identifier, options);
|
||||
} else {
|
||||
this.addSelection(identifier, options);
|
||||
}
|
||||
|
||||
this.focusInput();
|
||||
}
|
||||
|
||||
@action
|
||||
handleRowClick(identifier, event) {
|
||||
this.toggleSelection(identifier, {
|
||||
altSelection: event.shiftKey || event.ctrlKey,
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@action
|
||||
removeSelection(identifier) {
|
||||
this.selection = this.selection.filter(
|
||||
(selection) => selection.identifier !== identifier
|
||||
);
|
||||
|
||||
this.#handleSelectionChange();
|
||||
}
|
||||
|
||||
@action
|
||||
addSelection(identifier, options = {}) {
|
||||
let selection = this.searchRequest.value.findBy("identifier", identifier);
|
||||
|
||||
if (!selection || !selection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.type === CHANNEL_TYPE && !selection.isSingleUserChannel) {
|
||||
this.openChannel([selection]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.hasSelectedUsers &&
|
||||
!options.altSelection &&
|
||||
!this.site.mobileView
|
||||
) {
|
||||
this.openChannel([selection]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.isSingleUserChannel) {
|
||||
const user = selection.model.chatable.users[0];
|
||||
selection = new ChatChatable({
|
||||
identifier: `u-${user.id}`,
|
||||
type: USER_TYPE,
|
||||
model: user,
|
||||
});
|
||||
}
|
||||
|
||||
this.selection = [
|
||||
...this.selection.filter((s) => s.type !== CHANNEL_TYPE),
|
||||
selection,
|
||||
];
|
||||
this.#handleSelectionChange();
|
||||
}
|
||||
|
||||
@action
|
||||
openChannel(selection) {
|
||||
if (selection.length === 1 && selection[0].type === CHANNEL_TYPE) {
|
||||
const channel = selection[0].model;
|
||||
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||
this.args.onClose?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const users = selection.filterBy("type", USER_TYPE).mapBy("model");
|
||||
this.chat
|
||||
.upsertDmChannelForUsernames(users.mapBy("username"))
|
||||
.then((channel) => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||
this.args.onClose?.();
|
||||
});
|
||||
}
|
||||
|
||||
#handleSelectionChange() {
|
||||
this.query = "";
|
||||
this.activeSelectionIdentifiers = [];
|
||||
this._activeResultIdentifier = null;
|
||||
}
|
||||
|
||||
#getPreviousSelection() {
|
||||
return this.#getPrevious(
|
||||
this.selection,
|
||||
this.activeSelectionIdentifiers?.[0]
|
||||
);
|
||||
}
|
||||
|
||||
#getNextSelection() {
|
||||
return this.#getNext(this.selection, this.activeSelectionIdentifiers?.[0]);
|
||||
}
|
||||
|
||||
#getLastSelection() {
|
||||
return this.selection[this.selection.length - 1];
|
||||
}
|
||||
|
||||
#getPreviousResult() {
|
||||
return this.#getPrevious(
|
||||
this.searchRequest.value,
|
||||
this.activeResultIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
#getNextResult() {
|
||||
return this.#getNext(this.searchRequest.value, this.activeResultIdentifier);
|
||||
}
|
||||
|
||||
#getNext(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex < list.length - 1) {
|
||||
return list.objectAt(currentIndex + 1);
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
#getPrevious(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return list.objectAt(currentIndex - 1);
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
#getDigit(input) {
|
||||
if (typeof input === "string") {
|
||||
const match = input.match(/Digit(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import Component from "@glimmer/component";
|
||||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export default class Action extends Component {
|
||||
<template>
|
||||
<DButton
|
||||
class="btn btn-flat"
|
||||
@icon={{@item.icon}}
|
||||
@translatedLabel={{@item.label}}
|
||||
/>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
import MembersCount from "./members-count";
|
||||
import MembersSelector from "./members-selector";
|
||||
|
||||
export default class AddMembers extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
@service router;
|
||||
@service toasts;
|
||||
@service siteSettings;
|
||||
@service loadingSlider;
|
||||
|
||||
get membersCount() {
|
||||
return (
|
||||
this.args.members?.length + (this.args.channel?.membershipsCount ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async saveGroupMembers() {
|
||||
try {
|
||||
this.loadingSlider.transitionStarted();
|
||||
|
||||
await this.chatApi.addMembersToChannel(
|
||||
this.args.channel.id,
|
||||
this.args.members.mapBy("model.username")
|
||||
);
|
||||
|
||||
this.toasts.success({ data: { message: I18n.t("saved") } });
|
||||
this.router.transitionTo(
|
||||
"chat.channel",
|
||||
...this.args.channel.routeModels
|
||||
);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loadingSlider.transitionEnded();
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__add-members-container">
|
||||
<div class="chat-message-creator__add-members">
|
||||
<MembersCount
|
||||
@count={{this.membersCount}}
|
||||
@max={{this.siteSettings.chat_max_direct_message_users}}
|
||||
/>
|
||||
|
||||
<MembersSelector
|
||||
@channel={{@channel}}
|
||||
@members={{@members}}
|
||||
@onChange={{@onChangeMembers}}
|
||||
@close={{@close}}
|
||||
@cancel={{@cancel}}
|
||||
/>
|
||||
|
||||
{{#if @members.length}}
|
||||
<div class="chat-message-creator__add-members-footer-container">
|
||||
<div class="chat-message-creator__add-members-footer">
|
||||
<DButton class="btn-flat" @label="cancel" @action={{@cancel}} />
|
||||
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@label="chat.direct_message_creator.add_to_channel"
|
||||
@action={{this.saveGroupMembers}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<ChatChannelTitle @channel={{@content.model}} />
|
||||
|
||||
{{#if (gt @content.tracking.unreadCount 0)}}
|
||||
<div
|
||||
class={{concat-class "unread-indicator" (if this.isUrgent "-urgent")}}
|
||||
></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.site.desktopView}}
|
||||
<span class="action-indicator">{{this.openChannelLabel}}</span>
|
||||
{{/if}}
|
@ -1,20 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class ChatMessageCreatorChannelRow extends Component {
|
||||
@service site;
|
||||
|
||||
get openChannelLabel() {
|
||||
return htmlSafe(I18n.t("chat.new_message_modal.open_channel"));
|
||||
}
|
||||
|
||||
get isUrgent() {
|
||||
return (
|
||||
this.args.content.model.isDirectMessageChannel ||
|
||||
(this.args.content.model.isCategoryChannel &&
|
||||
this.args.content.model.tracking.mentionCount > 0)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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 ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
|
||||
export default class Channel extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get isUrgent() {
|
||||
return (
|
||||
this.args.item.model.isDirectMessageChannel ||
|
||||
(this.args.item.model.isCategoryChannel &&
|
||||
this.args.item.model.tracking.mentionCount > 0)
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__chatable-category-channel">
|
||||
<ChatChannelTitle @channel={{@item.model}} />
|
||||
|
||||
{{#if (gt @item.tracking.unreadCount 0)}}
|
||||
<div
|
||||
class={{concatClass "unread-indicator" (if this.isUrgent "-urgent")}}
|
||||
></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export const MODES = {
|
||||
search: "SEARCH",
|
||||
new_group: "NEW_GROUP",
|
||||
add_members: "ADD_MEMBERS",
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import AddMembers from "./add-members";
|
||||
import { MODES } from "./constants";
|
||||
import NewGroup from "./new-group";
|
||||
import Search from "./search";
|
||||
|
||||
export default class ChatMessageCreator extends Component {
|
||||
@tracked mode = MODES.search;
|
||||
@tracked members = [];
|
||||
|
||||
get componentForMode() {
|
||||
switch (this.args.mode ?? this.mode) {
|
||||
case MODES.search:
|
||||
return Search;
|
||||
case MODES.new_group:
|
||||
return NewGroup;
|
||||
case MODES.add_members:
|
||||
return AddMembers;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changeMode(mode, members = []) {
|
||||
this.mode = mode;
|
||||
this.changeMembers(members);
|
||||
}
|
||||
|
||||
@action
|
||||
changeMembers(members) {
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@action
|
||||
cancelAction() {
|
||||
return this.args.onCancel?.() || this.changeMode(MODES.search);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator-container">
|
||||
<div class="chat-message-creator">
|
||||
<this.componentForMode
|
||||
@channel={{@channel}}
|
||||
@onChangeMode={{this.changeMode}}
|
||||
@onChangeMembers={{this.changeMembers}}
|
||||
@close={{@onClose}}
|
||||
@cancel={{this.cancelAction}}
|
||||
@members={{this.members}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { getOwner, setOwner } from "@ember/application";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import ChatChatable from "discourse/plugins/chat/discourse/models/chat-chatable";
|
||||
|
||||
const MAX_RESULTS = 10;
|
||||
|
||||
export default class ChatablesLoader {
|
||||
@service chatChannelsManager;
|
||||
@service loadingSlider;
|
||||
|
||||
constructor(context) {
|
||||
setOwner(this, getOwner(context));
|
||||
}
|
||||
|
||||
@bind
|
||||
async search(
|
||||
term,
|
||||
options = {
|
||||
includeUsers: true,
|
||||
includeCategoryChannels: true,
|
||||
includeDirectMessageChannels: true,
|
||||
excludedUserIds: null,
|
||||
}
|
||||
) {
|
||||
this.request?.abort();
|
||||
|
||||
try {
|
||||
this.loadingSlider.transitionStarted();
|
||||
this.request = ajax("/chat/api/chatables", {
|
||||
data: {
|
||||
term,
|
||||
include_users: options.includeUsers,
|
||||
include_category_channels: options.includeCategoryChannels,
|
||||
include_direct_message_channels: options.includeDirectMessageChannels,
|
||||
excluded_memberships_channel_id: options.excludedMembershipsChannelId,
|
||||
},
|
||||
});
|
||||
const results = await this.request;
|
||||
this.selectedItem = null;
|
||||
|
||||
this.loadingSlider.transitionEnded();
|
||||
|
||||
return [
|
||||
...results.users,
|
||||
...results.direct_message_channels,
|
||||
...results.category_channels,
|
||||
]
|
||||
.map((item) => {
|
||||
const chatable = ChatChatable.create(item);
|
||||
chatable.tracking = this.#injectTracking(chatable);
|
||||
return chatable;
|
||||
})
|
||||
.slice(0, MAX_RESULTS);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
}
|
||||
|
||||
#injectTracking(chatable) {
|
||||
return this.chatChannelsManager.allChannels.find(
|
||||
(channel) => channel.id === chatable.model.id
|
||||
)?.tracking;
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { modifier } from "ember-modifier";
|
||||
|
||||
export default class ListHandler extends Component {
|
||||
handleKeydown = modifier((element) => {
|
||||
const handler = (event) => {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onHighlight(
|
||||
this.#getNext(this.args.items, this.args.highlightedItem?.identifier)
|
||||
);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onHighlight(
|
||||
this.#getPrevious(
|
||||
this.args.items,
|
||||
this.args.highlightedItem?.identifier
|
||||
)
|
||||
);
|
||||
} else if (event.key === "Enter" && this.args.highlightedItem) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.shiftKey && this.args.onShifSelect) {
|
||||
this.args.onShifSelect(this.args.highlightedItem);
|
||||
} else {
|
||||
this.args.onSelect(this.args.highlightedItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener("keydown", handler);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("keydown", handler);
|
||||
};
|
||||
});
|
||||
|
||||
#getNext(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex < list.length - 1) {
|
||||
return list.objectAt(currentIndex + 1);
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
#getPrevious(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return list.objectAt(currentIndex - 1);
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<span style="display: contents" {{this.handleKeydown}} ...attributes>
|
||||
{{yield}}
|
||||
</span>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import Action from "./action";
|
||||
import Channel from "./channel";
|
||||
import User from "./user";
|
||||
|
||||
export default class List extends Component {
|
||||
componentForItem(type) {
|
||||
switch (type) {
|
||||
case "action":
|
||||
return Action;
|
||||
case "user":
|
||||
return User;
|
||||
case "channel":
|
||||
return Channel;
|
||||
}
|
||||
}
|
||||
|
||||
#getNext(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex < list.length - 1) {
|
||||
return list.objectAt(currentIndex + 1);
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
#getPrevious(list, currentIdentifier = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (currentIdentifier) {
|
||||
const currentIndex = list.mapBy("identifier").indexOf(currentIdentifier);
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return list.objectAt(currentIndex - 1);
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleEnter(item, event) {
|
||||
if (event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.args.onShiftSelect) {
|
||||
this.args.onShiftSelect?.(item);
|
||||
} else {
|
||||
this.args.onSelect?.(item);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleClick(item, event) {
|
||||
if (event.shiftKey && this.args.onShiftSelect) {
|
||||
this.args.onShiftSelect?.(item);
|
||||
} else {
|
||||
this.args.onSelect?.(item);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__list-container">
|
||||
<ul class="chat-message-creator__list">
|
||||
{{#each @items as |item|}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"chat-message-creator__list-item"
|
||||
(if
|
||||
(eq item.identifier @highlightedItem.identifier) "-highlighted"
|
||||
)
|
||||
}}
|
||||
{{on "click" (fn this.handleClick item)}}
|
||||
{{on "keypress" (fn this.handleEnter item)}}
|
||||
{{on "mouseenter" (fn @onHighlight item)}}
|
||||
{{on "mouseleave" (fn @onHighlight null)}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{{component (this.componentForItem item.type) item=item}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { fn } from "@ember/helper";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
|
||||
|
||||
export default class Member extends Component {
|
||||
<template>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"chat-message-creator__member btn-default"
|
||||
(if @highlighted "-highlighted")
|
||||
}}
|
||||
@action={{fn @onSelect @member}}
|
||||
>
|
||||
<ChatUserAvatar @user={{@member.model}} @interactive={{false}} />
|
||||
<span class="chat-message-creator__member-username">
|
||||
{{@member.model.username}}
|
||||
</span>
|
||||
{{icon "times"}}
|
||||
</DButton>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import Component from "@glimmer/component";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import I18n from "discourse-i18n";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
|
||||
export default class MembersCount extends Component {
|
||||
get countLabel() {
|
||||
return I18n.t("chat.direct_message_creator.members_counter", {
|
||||
count: this.args.count,
|
||||
max: this.args.max,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-message-creator__members-count"
|
||||
(if (eq @count @max) "-reached-limit")
|
||||
}}
|
||||
>
|
||||
{{this.countLabel}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatablesLoader from "./lib/chatables-loader";
|
||||
import List from "./list";
|
||||
import ListHandler from "./list-handler";
|
||||
import Members from "./members";
|
||||
|
||||
export default class MembersSelector extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked chatables = [];
|
||||
@tracked filter = "";
|
||||
@tracked highlightedMember;
|
||||
@tracked highlightedChatable;
|
||||
|
||||
placeholder = I18n.t("chat.direct_message_creator.group_name");
|
||||
|
||||
get items() {
|
||||
return this.chatables.filter(
|
||||
(c) => !this.highlightedMemberIds.includes(c.model.id)
|
||||
);
|
||||
}
|
||||
|
||||
get highlightedMemberIds() {
|
||||
return this.args.members.map((u) => u.model.id);
|
||||
}
|
||||
|
||||
@action
|
||||
highlightMember(member) {
|
||||
this.highlightedMember = member;
|
||||
}
|
||||
|
||||
@action
|
||||
highlightChatable(chatable) {
|
||||
this.highlightedChatable = chatable;
|
||||
}
|
||||
|
||||
@action
|
||||
selectChatable(chatable) {
|
||||
if (
|
||||
this.args.members.length + (this.args.channel?.membershipsCount ?? 0) >=
|
||||
this.siteSettings.chat_max_direct_message_users
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.highlightedMemberIds.includes(chatable.model.id)) {
|
||||
this.unselectMember(chatable);
|
||||
} else {
|
||||
this.args.onChange?.([...this.args.members, chatable]);
|
||||
this.highlightedChatable = this.items[0];
|
||||
}
|
||||
|
||||
this.filter = "";
|
||||
this.focusFilterAction?.();
|
||||
this.highlightedMember = null;
|
||||
}
|
||||
|
||||
@action
|
||||
registerFocusFilterAction(actionFn) {
|
||||
this.focusFilterAction = actionFn;
|
||||
}
|
||||
|
||||
@action
|
||||
onFilter(event) {
|
||||
this.searchHandler = discourseDebounce(
|
||||
this,
|
||||
this.fetch,
|
||||
event.target.value,
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async fetch(term) {
|
||||
this.highlightedMember = null;
|
||||
|
||||
const loader = new ChatablesLoader(this);
|
||||
this.chatables = await loader.search(term, {
|
||||
includeCategoryChannels: false,
|
||||
includeDirectMessageChannels: false,
|
||||
excludedMembershipsChannelId: this.args.channel?.id,
|
||||
});
|
||||
|
||||
this.highlightedChatable = this.items[0];
|
||||
}
|
||||
|
||||
@action
|
||||
unselectMember(removedMember) {
|
||||
this.args.onChange?.(
|
||||
this.args.members.filter((member) => member !== removedMember)
|
||||
);
|
||||
this.highlightedMember = null;
|
||||
this.highlightedChatable = this.items[0];
|
||||
this.focusFilterAction?.();
|
||||
}
|
||||
|
||||
<template>
|
||||
<ListHandler
|
||||
@items={{this.items}}
|
||||
@highlightedItem={{this.highlightedChatable}}
|
||||
@onHighlight={{this.highlightChatable}}
|
||||
@onSelect={{this.selectChatable}}
|
||||
>
|
||||
<div class="chat-message-creator__add-members-header-container">
|
||||
<div class="chat-message-creator__add-members-header">
|
||||
<Members
|
||||
@filter={{this.filter}}
|
||||
@members={{@members}}
|
||||
@highlightedMember={{this.highlightedMember}}
|
||||
@onFilter={{this.onFilter}}
|
||||
@registerFocusFilterAction={{this.registerFocusFilterAction}}
|
||||
@onHighlightMember={{this.highlightMember}}
|
||||
@onSelectMember={{this.unselectMember}}
|
||||
/>
|
||||
|
||||
<DButton
|
||||
class="btn-flat chat-message-creator__add-members__close-btn"
|
||||
@action={{@cancel}}
|
||||
@icon="times"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<List
|
||||
@items={{this.items}}
|
||||
@highlightedItem={{this.highlightedChatable}}
|
||||
@onSelect={{this.selectChatable}}
|
||||
@onHighlight={{this.highlightChatable}}
|
||||
/>
|
||||
|
||||
</ListHandler>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import Member from "./member";
|
||||
|
||||
export default class members extends Component {
|
||||
@action
|
||||
onFilter() {
|
||||
this.args.onFilter(...arguments);
|
||||
}
|
||||
|
||||
@action
|
||||
registerFocusFilterAction(element) {
|
||||
this.args.registerFocusFilterAction(() => element.focus());
|
||||
}
|
||||
|
||||
@action
|
||||
handleKeypress(event) {
|
||||
if (event.key === "Backspace" && event.target.value === "") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.args.highlightedMember) {
|
||||
this.args.onHighlightMember(this.args.members.lastObject);
|
||||
} else {
|
||||
this.args.onSelectMember(this.args.highlightedMember);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft" && event.target.value === "") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onHighlightMember(
|
||||
this.#getPrevious(this.args.members, this.args.highlightedMember)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowRight" && event.target.value === "") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onHighlightMember(
|
||||
this.#getNext(this.args.members, this.args.highlightedMember)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && this.args.highlightedMember) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onSelectMember(this.args.highlightedMember);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightedMember = null;
|
||||
}
|
||||
|
||||
#getNext(list, current = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (current?.identifier) {
|
||||
const currentIndex = list
|
||||
.mapBy("identifier")
|
||||
.indexOf(current?.identifier);
|
||||
|
||||
if (currentIndex < list.length - 1) {
|
||||
return list.objectAt(currentIndex + 1);
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
|
||||
#getPrevious(list, current = null) {
|
||||
if (list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list = list.filterBy("enabled");
|
||||
|
||||
if (current?.identifier) {
|
||||
const currentIndex = list
|
||||
.mapBy("identifier")
|
||||
.indexOf(current?.identifier);
|
||||
|
||||
if (currentIndex > 0) {
|
||||
return list.objectAt(currentIndex - 1);
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
} else {
|
||||
return list.objectAt(list.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__members-container">
|
||||
<div class="chat-message-creator__members">
|
||||
{{icon "search"}}
|
||||
|
||||
{{#each @members as |member|}}
|
||||
<Member
|
||||
@member={{member}}
|
||||
@onSelect={{@onSelectMember}}
|
||||
@highlighted={{eq member @highlightedMember}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
<Input
|
||||
placeholder="...add more users"
|
||||
class="chat-message-creator__members-input"
|
||||
@value={{@filter}}
|
||||
autofocus={{true}}
|
||||
{{on "input" this.onFilter}}
|
||||
{{on "keydown" this.handleKeypress}}
|
||||
{{didInsert this.registerFocusFilterAction}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
import MembersCount from "./members-count";
|
||||
import MembersSelector from "./members-selector";
|
||||
|
||||
export default class NewGroup extends Component {
|
||||
@service chat;
|
||||
@service router;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked newGroupTitle = "";
|
||||
|
||||
placeholder = I18n.t("chat.direct_message_creator.group_name");
|
||||
|
||||
get membersCount() {
|
||||
return this.args.members?.length;
|
||||
}
|
||||
|
||||
@action
|
||||
async createGroup() {
|
||||
try {
|
||||
const channel = await this.chat.upsertDmChannelForUsernames(
|
||||
this.args.members.mapBy("model.username"),
|
||||
this.newGroupTitle
|
||||
);
|
||||
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.args.close?.();
|
||||
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__new-group-container">
|
||||
<div class="chat-message-creator__new-group">
|
||||
<div class="chat-message-creator__new-group-header-container">
|
||||
<div class="chat-message-creator__new-group-header">
|
||||
<Input
|
||||
name="channel-name"
|
||||
class="chat-message-creator__new-group-header__input"
|
||||
placeholder={{this.placeholder}}
|
||||
@value={{this.newGroupTitle}}
|
||||
/>
|
||||
|
||||
<MembersCount
|
||||
@count={{this.membersCount}}
|
||||
@max={{this.siteSettings.chat_max_direct_message_users}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MembersSelector
|
||||
@members={{@members}}
|
||||
@channel={{@channel}}
|
||||
@onChange={{@onChangeMembers}}
|
||||
@close={{@close}}
|
||||
@cancel={{@cancel}}
|
||||
/>
|
||||
|
||||
{{#if @members.length}}
|
||||
<div class="chat-message-creator__new-group-footer-container">
|
||||
<div class="chat-message-creator__new-group-footer">
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@label="save"
|
||||
@action={{this.createGroup}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
|
||||
export default class ChatMessageCreatorSearchInput extends Component {
|
||||
<template>
|
||||
<div class="chat-message-creator__search-input-container">
|
||||
<div class="chat-message-creator__search-input">
|
||||
{{icon
|
||||
"search"
|
||||
class="chat-message-creator__search-input__search-icon"
|
||||
}}
|
||||
<Input
|
||||
class="chat-message-creator__search-input__input"
|
||||
placeholder="Filter"
|
||||
{{on "input" @onFilter}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { MODES } from "./constants";
|
||||
import ChatablesLoader from "./lib/chatables-loader";
|
||||
import List from "./list";
|
||||
import ListHandler from "./list-handler";
|
||||
import SearchInput from "./search-input";
|
||||
|
||||
export default class ChatMessageCreatorSearch extends Component {
|
||||
@service chat;
|
||||
@service router;
|
||||
|
||||
@tracked chatables = [];
|
||||
@tracked highlightedChatable;
|
||||
|
||||
get items() {
|
||||
return [
|
||||
{
|
||||
identifier: "new-group",
|
||||
type: "action",
|
||||
label: "New group chat",
|
||||
enabled: true,
|
||||
icon: "users",
|
||||
},
|
||||
...this.chatables,
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
prefillAddMembers(item) {
|
||||
this.args.onChangeMode(MODES.new_group, [item]);
|
||||
}
|
||||
|
||||
@action
|
||||
highlightChatable(chatable) {
|
||||
this.highlightedChatable = chatable;
|
||||
}
|
||||
|
||||
@action
|
||||
async selectChatable(item) {
|
||||
switch (item.type) {
|
||||
case "action":
|
||||
this.args.onChangeMode(MODES.new_group);
|
||||
break;
|
||||
case "user":
|
||||
await this.startOneToOneChannel(item.model.username);
|
||||
break;
|
||||
default:
|
||||
this.router.transitionTo("chat.channel", ...item.model.routeModels);
|
||||
this.args.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onFilter(event) {
|
||||
this.searchHandler = discourseDebounce(
|
||||
this,
|
||||
this.fetch,
|
||||
event.target.value,
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
async fetch(term) {
|
||||
const loader = new ChatablesLoader(this);
|
||||
this.chatables = await loader.search(term);
|
||||
|
||||
this.highlightedChatable = this.items[0];
|
||||
}
|
||||
|
||||
async startOneToOneChannel(username) {
|
||||
try {
|
||||
const channel = await this.chat.upsertDmChannelForUsernames([username]);
|
||||
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.args.close?.();
|
||||
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<ListHandler
|
||||
@items={{this.items}}
|
||||
@highlightedItem={{this.highlightedChatable}}
|
||||
@onHighlight={{this.highlightChatable}}
|
||||
@onSelect={{this.selectChatable}}
|
||||
@onShifSelect={{this.prefillAddMembers}}
|
||||
>
|
||||
<div class="chat-message-creator__search-container">
|
||||
<div class="chat-message-creator__search">
|
||||
<div class="chat-message-creator__section">
|
||||
<SearchInput @onFilter={{this.onFilter}} />
|
||||
|
||||
<DButton
|
||||
class="btn-flat chat-message-creator__search-input__cancel-button"
|
||||
@icon="times"
|
||||
@action={{@close}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<List
|
||||
@items={{this.items}}
|
||||
@highlightedItem={{this.highlightedChatable}}
|
||||
@onSelect={{this.selectChatable}}
|
||||
@onHighlight={{this.highlightChatable}}
|
||||
@onShiftSelect={{this.prefillAddMembers}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ListHandler>
|
||||
</template>
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<ChatUserAvatar @user={{@content.model}} />
|
||||
<ChatUserDisplayName @user={{@content.model}} />
|
||||
|
||||
{{#if (gt @content.tracking.unreadCount 0)}}
|
||||
<div class="unread-indicator -urgent"></div>
|
||||
{{/if}}
|
||||
|
||||
{{user-status @content.model currentUser=this.currentUser}}
|
||||
|
||||
{{#unless @content.enabled}}
|
||||
<span class="disabled-text">
|
||||
{{i18n "chat.new_message_modal.disabled_user"}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
|
||||
{{#if @selected}}
|
||||
{{#if this.site.mobileView}}
|
||||
<span class="selection-indicator -add">
|
||||
{{d-icon "check"}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span
|
||||
class={{concat-class "selection-indicator" (if @active "-remove" "-add")}}
|
||||
>
|
||||
{{d-icon (if @active "times" "check")}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if this.site.desktopView}}
|
||||
{{#if @hasSelectedUsers}}
|
||||
<span class="action-indicator">{{this.addUserLabel}}</span>
|
||||
{{else}}
|
||||
<span class="action-indicator">{{this.openChannelLabel}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
@ -1,17 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class ChatMessageCreatorUserRow extends Component {
|
||||
@service currentUser;
|
||||
@service site;
|
||||
|
||||
get openChannelLabel() {
|
||||
return htmlSafe(I18n.t("chat.new_message_modal.open_channel"));
|
||||
}
|
||||
|
||||
get addUserLabel() {
|
||||
return htmlSafe(I18n.t("chat.new_message_modal.add_user_short"));
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
<ChatUserAvatar @user={{@selection.model}} @showPresence={{false}} />
|
||||
|
||||
<span class="chat-message-creator__selection-item__username">
|
||||
{{@selection.model.username}}
|
||||
</span>
|
@ -0,0 +1,32 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import userStatus from "discourse/helpers/user-status";
|
||||
import I18n from "discourse-i18n";
|
||||
import gt from "truth-helpers/helpers/gt";
|
||||
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
|
||||
import ChatUserDisplayName from "discourse/plugins/chat/discourse/components/chat-user-display-name";
|
||||
|
||||
export default class ChatableUser extends Component {
|
||||
@service currentUser;
|
||||
|
||||
disabledUserLabel = I18n.t("chat.new_message_modal.disabled_user");
|
||||
|
||||
<template>
|
||||
<div class="chat-message-creator__chatable-user">
|
||||
<ChatUserAvatar @user={{@item.model}} @interactive={{false}} />
|
||||
<ChatUserDisplayName @user={{@item.model}} />
|
||||
|
||||
{{#if (gt @item.tracking.unreadCount 0)}}
|
||||
<div class="unread-indicator -urgent"></div>
|
||||
{{/if}}
|
||||
|
||||
{{userStatus @item.model currentUser=this.currentUser}}
|
||||
|
||||
{{#unless @item.enabled}}
|
||||
<span class="disabled-text">
|
||||
{{this.disabledUserLabel}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -4,18 +4,21 @@ import { action } from "@ember/object";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
||||
|
||||
const SLUG_MAX_LENGTH = 100;
|
||||
|
||||
export default class ChatModalEditChannelName extends Component {
|
||||
@service chatApi;
|
||||
@service router;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked editedName = this.channel.title;
|
||||
@tracked editedSlug = this.channel.slug;
|
||||
@tracked autoGeneratedSlug = "";
|
||||
@tracked
|
||||
autoGeneratedSlug = this.channel.slug ?? slugifyChannel(this.channel);
|
||||
@tracked flash;
|
||||
|
||||
#generateSlugHandler = null;
|
||||
@ -34,17 +37,23 @@ export default class ChatModalEditChannelName extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
onSave() {
|
||||
return this.chatApi
|
||||
.updateChannel(this.channel.id, {
|
||||
async onSave() {
|
||||
try {
|
||||
const result = await this.chatApi.updateChannel(this.channel.id, {
|
||||
name: this.editedName,
|
||||
slug: this.editedSlug || this.autoGeneratedSlug || this.channel.slug,
|
||||
})
|
||||
.then((result) => {
|
||||
});
|
||||
|
||||
this.channel.title = result.channel.title;
|
||||
this.args.closeModal();
|
||||
})
|
||||
.catch((error) => (this.flash = extractError(error)));
|
||||
this.channel.slug = result.channel.slug;
|
||||
await this.args.closeModal();
|
||||
await this.router.replaceWith(
|
||||
"chat.channel",
|
||||
...this.channel.routeModels
|
||||
);
|
||||
} catch (error) {
|
||||
this.flash = extractError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@ -77,11 +86,15 @@ export default class ChatModalEditChannelName extends Component {
|
||||
|
||||
// intentionally not showing AJAX error for this, we will autogenerate
|
||||
// the slug server-side if they leave it blank
|
||||
#generateSlug(name) {
|
||||
return ajax("/slugs.json", { type: "POST", data: { name } }).then(
|
||||
async #generateSlug(name) {
|
||||
try {
|
||||
await ajax("/slugs.json", { type: "POST", data: { name } }).then(
|
||||
(response) => {
|
||||
this.autoGeneratedSlug = response.slug;
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DModal from "discourse/components/d-modal";
|
||||
import MessageCreator from "discourse/plugins/chat/discourse/components/chat/message-creator";
|
||||
|
||||
export default class ChatModalNewMessage extends Component {
|
||||
@service chat;
|
||||
@service siteSettings;
|
||||
|
||||
get shouldRender() {
|
||||
return (
|
||||
this.siteSettings.enable_public_channels || this.chat.userCanDirectMessage
|
||||
);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldRender}}
|
||||
<DModal
|
||||
@closeModal={{@closeModal}}
|
||||
class="chat-modal-new-message"
|
||||
@title="chat.new_message_modal.title"
|
||||
@inline={{@inline}}
|
||||
>
|
||||
<MessageCreator @onClose={{@closeModal}} @channel={{@model}} />
|
||||
</DModal>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{{#if this.shouldRender}}
|
||||
<DModal
|
||||
@closeModal={{@closeModal}}
|
||||
class="chat-modal-new-message"
|
||||
@title="chat.new_message_modal.title"
|
||||
@inline={{@inline}}
|
||||
>
|
||||
<Chat::MessageCreator @onClose={{@closeModal}} />
|
||||
</DModal>
|
||||
{{/if}}
|
@ -1,13 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatModalNewMessage extends Component {
|
||||
@service chat;
|
||||
@service siteSettings;
|
||||
|
||||
get shouldRender() {
|
||||
return (
|
||||
this.siteSettings.enable_public_channels || this.chat.userCanDirectMessage
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class ThreadHeaderUnreadIndicator extends Component {
|
||||
@service currentUser;
|
||||
|
||||
unreadCountLabel = I18n.t("chat.unread_threads_count", {
|
||||
count: this.cappedUnreadCount,
|
||||
});
|
||||
|
||||
get unreadCount() {
|
||||
return this.args.channel.threadsManager.unreadThreadCount;
|
||||
}
|
||||
|
||||
get showUnreadIndicator() {
|
||||
return !this.currentUser.isInDoNotDisturb() && this.unreadCount > 0;
|
||||
}
|
||||
|
||||
get cappedUnreadCount() {
|
||||
return this.unreadCount > 99 ? "99+" : this.unreadCount;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showUnreadIndicator}}
|
||||
<div
|
||||
class="chat-thread-header-unread-indicator"
|
||||
title={{this.unreadCountLabel}}
|
||||
>
|
||||
<div
|
||||
class="chat-thread-header-unread-indicator__number"
|
||||
>{{this.cappedUnreadCount}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{{#if this.showUnreadIndicator}}
|
||||
<div
|
||||
class="chat-thread-header-unread-indicator"
|
||||
aria-label={{i18n "chat.unread_threads_count" count=this.unreadCountLabel}}
|
||||
title={{i18n "chat.unread_threads_count" count=this.unreadCountLabel}}
|
||||
>
|
||||
<div
|
||||
class="chat-thread-header-unread-indicator__number"
|
||||
>{{this.unreadCountLabel}}</div>
|
||||
</div>
|
||||
{{/if}}
|
@ -1,22 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatThreadHeaderUnreadIndicator extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get currentUserInDnD() {
|
||||
return this.currentUser.isInDoNotDisturb();
|
||||
}
|
||||
|
||||
get unreadCount() {
|
||||
return this.args.channel.threadsManager.unreadThreadCount;
|
||||
}
|
||||
|
||||
get showUnreadIndicator() {
|
||||
return !this.currentUserInDnD && this.unreadCount > 0;
|
||||
}
|
||||
|
||||
get unreadCountLabel() {
|
||||
return this.unreadCount > 99 ? "99+" : this.unreadCount;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n from "I18n";
|
||||
import ThreadHeaderUnreadIndicator from "discourse/plugins/chat/discourse/components/chat/thread/header-unread-indicator";
|
||||
|
||||
export default class ThreadsListButton extends Component {
|
||||
threadsListLabel = I18n.t("chat.threads.list");
|
||||
|
||||
<template>
|
||||
<LinkTo
|
||||
@route="chat.channel.threads"
|
||||
@models={{@channel.routeModels}}
|
||||
title={{this.threadsListLabel}}
|
||||
class={{concatClass
|
||||
"chat-threads-list-button"
|
||||
"btn"
|
||||
"btn-flat"
|
||||
(if @channel.threadsManager.unreadThreadCount "has-unreads")
|
||||
}}
|
||||
>
|
||||
{{icon "discourse-threads"}}
|
||||
|
||||
<ThreadHeaderUnreadIndicator @channel={{@channel}} />
|
||||
</LinkTo>
|
||||
</template>
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<LinkTo
|
||||
@route="chat.channel.threads"
|
||||
@models={{@channel.routeModels}}
|
||||
title={{i18n "chat.threads.list"}}
|
||||
class={{concat-class
|
||||
"chat-threads-list-button btn btn-flat"
|
||||
(if @channel.threadsManager.unreadThreadCount "has-unreads")
|
||||
}}
|
||||
>
|
||||
{{d-icon "discourse-threads"}}
|
||||
|
||||
<Chat::Thread::HeaderUnreadIndicator @channel={{@channel}} />
|
||||
</LinkTo>
|
@ -1,3 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatThreadsListButton extends Component {}
|
@ -6,7 +6,7 @@
|
||||
@icon={{this.options.leaveIcon}}
|
||||
@disabled={{this.isLoading}}
|
||||
class={{concat-class
|
||||
"toggle-channel-membership-button -leave btn-flat"
|
||||
"toggle-channel-membership-button -leave"
|
||||
this.options.leaveClass
|
||||
}}
|
||||
/>
|
||||
|
@ -8,9 +8,9 @@ import { emojiUnescape } from "discourse/lib/text";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { avatarUrl } from "discourse-common/lib/avatar-utils";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
|
||||
import getFirstUser from "discourse/plugins/chat/discourse/lib/get-first-user";
|
||||
import {
|
||||
CHAT_PANEL,
|
||||
initSidebarState,
|
||||
@ -84,10 +84,6 @@ export default {
|
||||
return htmlSafe(emojiUnescape(this.channel.escapedTitle));
|
||||
}
|
||||
|
||||
get prefixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get prefixValue() {
|
||||
return "d-chat";
|
||||
}
|
||||
@ -106,6 +102,18 @@ export default {
|
||||
return this.channel.chatable.read_restricted ? "lock" : "";
|
||||
}
|
||||
|
||||
get prefixCSSClass() {
|
||||
const activeUsers = this.chatService.presenceChannel.users;
|
||||
const user = this.channel.chatable.users[0];
|
||||
if (
|
||||
!!activeUsers?.findBy("id", user?.id) ||
|
||||
!!activeUsers?.findBy("username", user?.username)
|
||||
) {
|
||||
return "active";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
@ -206,24 +214,29 @@ export default {
|
||||
api.addSidebarSection(
|
||||
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||
const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink {
|
||||
constructor({ channel, chatService }) {
|
||||
route = "chat.channel";
|
||||
suffixType = "icon";
|
||||
suffixCSSClass = "urgent";
|
||||
hoverType = "icon";
|
||||
hoverValue = "times";
|
||||
hoverTitle = I18n.t("chat.direct_messages.leave");
|
||||
|
||||
constructor({ channel, chatService, currentUser }) {
|
||||
super(...arguments);
|
||||
this.channel = channel;
|
||||
this.chatService = chatService;
|
||||
|
||||
if (this.oneOnOneMessage) {
|
||||
const user = this.channel.chatable.users[0];
|
||||
if (user.username !== I18n.t("chat.deleted_chat_username")) {
|
||||
user.trackStatus();
|
||||
}
|
||||
}
|
||||
this.currentUser = currentUser;
|
||||
}
|
||||
|
||||
@bind
|
||||
willDestroy() {
|
||||
if (this.oneOnOneMessage) {
|
||||
this.channel.chatable.users[0].stopTrackingStatus();
|
||||
get contentComponentArgs() {
|
||||
return getFirstUser(
|
||||
this.channel.chatable.users,
|
||||
this.currentUser
|
||||
).get("status");
|
||||
}
|
||||
|
||||
get contentComponent() {
|
||||
return "user-status-message";
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -246,10 +259,6 @@ export default {
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
get route() {
|
||||
return "chat.channel";
|
||||
}
|
||||
|
||||
get models() {
|
||||
return this.channel.routeModels;
|
||||
}
|
||||
@ -260,53 +269,47 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
get oneOnOneMessage() {
|
||||
return this.channel.chatable.users.length === 1;
|
||||
}
|
||||
|
||||
get contentComponentArgs() {
|
||||
return this.channel.chatable.users[0].get("status");
|
||||
}
|
||||
|
||||
get contentComponent() {
|
||||
return "user-status-message";
|
||||
}
|
||||
|
||||
get text() {
|
||||
if (this.channel.chatable.group) {
|
||||
return this.channel.title;
|
||||
} else {
|
||||
const username = this.channel.escapedTitle.replaceAll("@", "");
|
||||
if (this.oneOnOneMessage) {
|
||||
return htmlSafe(
|
||||
`${escapeExpression(username)}${decorateUsername(
|
||||
escapeExpression(username)
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
get prefixType() {
|
||||
if (this.oneOnOneMessage) {
|
||||
return "image";
|
||||
} else {
|
||||
if (this.channel.chatable.group) {
|
||||
return "text";
|
||||
} else {
|
||||
return "image";
|
||||
}
|
||||
}
|
||||
|
||||
get prefixValue() {
|
||||
if (this.channel.chatable.users.length === 1) {
|
||||
if (this.channel.chatable.group) {
|
||||
return this.channel.membershipsCount;
|
||||
} else {
|
||||
return avatarUrl(
|
||||
this.channel.chatable.users[0].avatar_template,
|
||||
getFirstUser(this.channel.chatable.users, this.currentUser)
|
||||
.avatar_template,
|
||||
"tiny"
|
||||
);
|
||||
} else {
|
||||
return this.channel.chatable.users.length;
|
||||
}
|
||||
}
|
||||
|
||||
get prefixCSSClass() {
|
||||
const activeUsers = this.chatService.presenceChannel.users;
|
||||
const user = this.channel.chatable.users[0];
|
||||
console.log(this.channel.chatable);
|
||||
const user = getFirstUser(
|
||||
this.channel.chatable.users,
|
||||
this.currentUser
|
||||
);
|
||||
|
||||
if (
|
||||
!!activeUsers?.findBy("id", user?.id) ||
|
||||
!!activeUsers?.findBy("username", user?.username)
|
||||
@ -316,26 +319,10 @@ export default {
|
||||
return "";
|
||||
}
|
||||
|
||||
get suffixType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
return this.channel.tracking.unreadCount > 0 ? "circle" : "";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return "urgent";
|
||||
}
|
||||
|
||||
get hoverType() {
|
||||
return "icon";
|
||||
}
|
||||
|
||||
get hoverValue() {
|
||||
return "times";
|
||||
}
|
||||
|
||||
get hoverAction() {
|
||||
return (event) => {
|
||||
event.stopPropagation();
|
||||
@ -343,16 +330,13 @@ export default {
|
||||
this.chatService.unfollowChannel(this.channel);
|
||||
};
|
||||
}
|
||||
|
||||
get hoverTitle() {
|
||||
return I18n.t("chat.direct_messages.leave");
|
||||
}
|
||||
};
|
||||
|
||||
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
|
||||
@service site;
|
||||
@service modal;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
|
||||
@tracked
|
||||
userCanDirectMessage = this.chatService.userCanDirectMessage;
|
||||
@ -375,6 +359,7 @@ export default {
|
||||
new SidebarChatDirectMessagesSectionLink({
|
||||
channel,
|
||||
chatService: this.chatService,
|
||||
currentUser: this.currentUser,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -52,21 +52,29 @@ function messageFabricator(args = {}) {
|
||||
|
||||
function channelFabricator(args = {}) {
|
||||
const id = args.id || sequence++;
|
||||
const chatable = args.chatable || categoryFabricator();
|
||||
|
||||
const channel = ChatChannel.create({
|
||||
id,
|
||||
chatable_type:
|
||||
args.chatable?.type ||
|
||||
args.chatable_type ||
|
||||
CHATABLE_TYPES.categoryChannel,
|
||||
chatable_id: args.chatable?.id || args.chatable_id,
|
||||
title: args.title || "General",
|
||||
(chatable instanceof Category
|
||||
? CHATABLE_TYPES.categoryChannel
|
||||
: CHATABLE_TYPES.directMessageChannel) ||
|
||||
chatable?.type ||
|
||||
args.chatable_type,
|
||||
chatable_id: chatable?.id || args.chatable_id,
|
||||
title: args.title
|
||||
? args.title
|
||||
: chatable instanceof Category
|
||||
? "General"
|
||||
: null,
|
||||
description: args.description,
|
||||
chatable: args.chatable || categoryFabricator(),
|
||||
chatable,
|
||||
status: args.status || CHANNEL_STATUSES.open,
|
||||
slug: args.chatable?.slug || "general",
|
||||
slug: chatable?.slug || chatable instanceof Category ? "general" : null,
|
||||
meta: Object.assign({ can_delete_self: true }, args.meta || {}),
|
||||
archive_failed: args.archive_failed ?? false,
|
||||
memberships_count: args.memberships_count ?? 0,
|
||||
});
|
||||
|
||||
channel.lastMessage = messageFabricator({ channel });
|
||||
@ -78,7 +86,7 @@ function categoryFabricator(args = {}) {
|
||||
return Category.create({
|
||||
id: args.id || sequence++,
|
||||
color: args.color || "D56353",
|
||||
read_restricted: false,
|
||||
read_restricted: args.read_restricted ?? false,
|
||||
name: args.name || "General",
|
||||
slug: args.slug || "general",
|
||||
});
|
||||
@ -86,8 +94,8 @@ function categoryFabricator(args = {}) {
|
||||
|
||||
function directMessageFabricator(args = {}) {
|
||||
return ChatDirectMessage.create({
|
||||
id: args.id || sequence++,
|
||||
users: args.users || [userFabricator(), userFabricator()],
|
||||
group: args.group ?? false,
|
||||
users: args.users ?? [userFabricator(), userFabricator()],
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,6 +104,8 @@ function directMessageChannelFabricator(args = {}) {
|
||||
args.chatable ||
|
||||
directMessageFabricator({
|
||||
id: args.chatable_id || sequence++,
|
||||
group: args.group ?? false,
|
||||
users: args.users,
|
||||
});
|
||||
|
||||
return channelFabricator(
|
||||
@ -103,6 +113,7 @@ function directMessageChannelFabricator(args = {}) {
|
||||
chatable_type: CHATABLE_TYPES.directMessageChannel,
|
||||
chatable_id: directMessage.id,
|
||||
chatable: directMessage,
|
||||
memberships_count: directMessage.users.length,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
export default function getFirstUsers(users, currentUser) {
|
||||
return users.sort((a, b) => {
|
||||
if (a.id === currentUser.id) {
|
||||
return 1;
|
||||
}
|
||||
if (b.id === currentUser.id) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
})[0];
|
||||
}
|
@ -61,6 +61,7 @@ export default class ChatChannel {
|
||||
@tracked status;
|
||||
@tracked activeThread = null;
|
||||
@tracked meta;
|
||||
@tracked chatableId;
|
||||
@tracked chatableType;
|
||||
@tracked chatableUrl;
|
||||
@tracked autoJoinUsers = false;
|
||||
@ -89,12 +90,7 @@ export default class ChatChannel {
|
||||
this.threadingEnabled = args.threading_enabled;
|
||||
this.autoJoinUsers = args.auto_join_users;
|
||||
this.allowChannelWideMentions = args.allow_channel_wide_mentions;
|
||||
this.chatable = this.isDirectMessageChannel
|
||||
? ChatDirectMessage.create({
|
||||
id: args.chatable?.id,
|
||||
users: args.chatable?.users,
|
||||
})
|
||||
: Category.create(args.chatable);
|
||||
this.chatable = this.#initChatable(args.chatable || []);
|
||||
this.currentUserMembership = args.current_user_membership;
|
||||
|
||||
if (args.archive_completed || args.archive_failed) {
|
||||
@ -245,4 +241,23 @@ export default class ChatChannel {
|
||||
this._lastMessage = ChatMessage.create(this, message);
|
||||
}
|
||||
}
|
||||
|
||||
#initChatable(chatable) {
|
||||
if (
|
||||
!chatable ||
|
||||
chatable instanceof Category ||
|
||||
chatable instanceof ChatDirectMessage
|
||||
) {
|
||||
return chatable;
|
||||
} else {
|
||||
if (this.isDirectMessageChannel) {
|
||||
return ChatDirectMessage.create({
|
||||
users: chatable?.users,
|
||||
group: chatable?.group,
|
||||
});
|
||||
} else {
|
||||
return Category.create(chatable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import Category from "discourse/models/category";
|
||||
import User from "discourse/models/user";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
|
||||
@ -66,7 +67,7 @@ export default class ChatChatable {
|
||||
return this.type === "user";
|
||||
}
|
||||
|
||||
get isSingleUserChannel() {
|
||||
return this.type === "channel" && this.model?.chatable?.users?.length === 1;
|
||||
get isCategory() {
|
||||
return this instanceof Category;
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,13 @@ export default class ChatDirectMessage {
|
||||
return new ChatDirectMessage(args);
|
||||
}
|
||||
|
||||
@tracked id;
|
||||
@tracked users = null;
|
||||
@tracked group = false;
|
||||
|
||||
type = CHATABLE_TYPES.directMessageChannel;
|
||||
|
||||
constructor(args = {}) {
|
||||
this.id = args.id;
|
||||
this.group = args.group ?? false;
|
||||
this.users = this.#initUsers(args.users || []);
|
||||
}
|
||||
|
||||
@ -21,9 +21,9 @@ export default class ChatDirectMessage {
|
||||
return users.map((user) => {
|
||||
if (!user || user instanceof User) {
|
||||
return user;
|
||||
}
|
||||
|
||||
} else {
|
||||
return User.create(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ export default class ChatChannelInfoMembersRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
afterModel(model) {
|
||||
if (!model.isOpen || model.membershipsCount < 1) {
|
||||
if (!model.isOpen) {
|
||||
return this.router.replaceWith("chat.channel.info.settings");
|
||||
}
|
||||
}
|
||||
|
@ -490,6 +490,18 @@ export default class ChatApi extends Service {
|
||||
return this.#getRequest(`/channels/${channelId}/summarize`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add members to a channel.
|
||||
*
|
||||
* @param {number} channelId - The ID of the channel.
|
||||
* @param {Array<string>} usernames - The usernames of the users to add.
|
||||
*/
|
||||
addMembersToChannel(channelId, usernames) {
|
||||
return this.#postRequest(`/channels/${channelId}/memberships`, {
|
||||
usernames,
|
||||
});
|
||||
}
|
||||
|
||||
get #basePath() {
|
||||
return "/chat/api";
|
||||
}
|
||||
|
@ -385,10 +385,10 @@ export default class Chat extends Service {
|
||||
// @param {array} usernames - The usernames to create or fetch the direct message
|
||||
// channel for. The current user will automatically be included in the channel
|
||||
// when it is created.
|
||||
upsertDmChannelForUsernames(usernames) {
|
||||
upsertDmChannelForUsernames(usernames, name = null) {
|
||||
return ajax("/chat/api/direct-message-channels.json", {
|
||||
method: "POST",
|
||||
data: { target_usernames: usernames.uniq() },
|
||||
data: { target_usernames: usernames.uniq(), name },
|
||||
})
|
||||
.then((response) => {
|
||||
const channel = this.chatChannelsManager.store(response.channel);
|
||||
|
@ -1,8 +1,12 @@
|
||||
.chat-channel-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-message-creator__container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
.nav-pills {
|
||||
@ -12,6 +16,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel-members__add-members {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.chat-message-creator-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Info header
|
||||
.chat-channel-info-header {
|
||||
display: flex;
|
||||
|
@ -1,6 +1,6 @@
|
||||
.chat-channel-members {
|
||||
width: 50%;
|
||||
min-width: 320px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
|
||||
&__filter {
|
||||
margin-bottom: 1rem;
|
||||
@ -10,18 +10,36 @@
|
||||
display: flex;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
height: 42px;
|
||||
align-items: center;
|
||||
|
||||
&.-no-results {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-very-low);
|
||||
}
|
||||
|
||||
&.-member {
|
||||
.chat-user-avatar {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.-add-member {
|
||||
color: var(--tertiary);
|
||||
cursor: pointer;
|
||||
|
||||
.d-icon {
|
||||
background: var(--primary-low);
|
||||
color: var(--tertiary);
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
@ -1,6 +1,5 @@
|
||||
.chat-channel-settings {
|
||||
width: 50%;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
|
||||
.chat-channel-settings__slug {
|
||||
max-width: 250px;
|
||||
|
@ -74,11 +74,12 @@
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-down-1);
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.chat-channel-title__category-badge {
|
||||
|
@ -29,10 +29,8 @@
|
||||
}
|
||||
|
||||
&-content {
|
||||
background: var(--primary-very-low);
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,225 @@
|
||||
.chat-message-creator__section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-message-creator__add-members {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-message-creator__participants-count {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-message-creator__member {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: popIn 0.1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__member {
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--primary-medium);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&.-highlighted {
|
||||
border-color: var(--tertiary);
|
||||
}
|
||||
|
||||
&-username {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__add-members__close-btn {
|
||||
align-self: flex-start;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chat-message-creator__add-members-header {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
gap: 0.5rem;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__new-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
}
|
||||
|
||||
&__input {
|
||||
padding-inline: 0 !important;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
outline: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__new-group-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__participants-count {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-message-creator__members {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem;
|
||||
background: var(--primary-low);
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
min-height: 50px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.d-icon-search {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&-input {
|
||||
background: none !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
outline: 0 !important;
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
padding-inline: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__participants-count {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-message-creator__members-count {
|
||||
white-space: nowrap;
|
||||
|
||||
&.-reached-limit {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__add-members-footer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-item {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.d-icon-users {
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
padding: 2px 2px;
|
||||
box-sizing: border-box;
|
||||
color: var(--tertiary);
|
||||
background: var(--primary-low);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.-highlighted {
|
||||
background: var(--tertiary-very-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__chatable-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.chat-user-display-name {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--secondary-very-high);
|
||||
width: 100%;
|
||||
margin: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
height: 42px;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__search-icon {
|
||||
background: none !important;
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
&__input,
|
||||
&__input:focus {
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
appearance: none !important;
|
||||
outline: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
0% {
|
||||
transform: scale(0.1);
|
||||
@ -8,326 +230,3 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
--row-height: 36px;
|
||||
|
||||
&__search-icon {
|
||||
color: var(--primary-medium);
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--row-height);
|
||||
padding-inline: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
> * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
padding-inline: 0.25rem;
|
||||
align-items: center;
|
||||
border-radius: var(--d-border-radius);
|
||||
height: var(--row-height);
|
||||
|
||||
.unread-indicator {
|
||||
background: var(--tertiary);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&.-urgent {
|
||||
background: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
.selection-indicator {
|
||||
visibility: hidden;
|
||||
|
||||
font-size: var(--font-down-2);
|
||||
margin-left: auto;
|
||||
|
||||
&.-add {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.-remove {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.action-indicator {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--secondary-medium);
|
||||
align-items: center;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
kbd {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.-active {
|
||||
.action-indicator {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel-title__name {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-channel-title__avatar,
|
||||
.chat-channel-title__category-badge,
|
||||
.chat-user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-channel-title {
|
||||
&__users-count {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel-title__name,
|
||||
.chat-user-display-name {
|
||||
@include ellipsis;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
&.-selected {
|
||||
.selection-indicator {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.-disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&.-active {
|
||||
cursor: pointer;
|
||||
|
||||
.chat-user-display-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.-user {
|
||||
&.-disabled {
|
||||
.chat-user-display-name__username.-first {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.disabled-text {
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.25rem 1rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__close-btn {
|
||||
margin-bottom: auto;
|
||||
margin-left: 0.25rem;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
border-radius: var(--d-button-border-radius);
|
||||
}
|
||||
|
||||
&__selection {
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
background: var(--secondary-very-high);
|
||||
border-radius: var(--d-input-border-radius);
|
||||
padding: 3px;
|
||||
position: relative;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
&__input[type="text"],
|
||||
&__input[type="text"]:focus {
|
||||
background: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: 0;
|
||||
resize: none;
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
height: var(--row-height);
|
||||
flex: 1;
|
||||
width: auto;
|
||||
padding: 0 5px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__loader {
|
||||
&-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 0.5rem;
|
||||
height: var(--row-height);
|
||||
}
|
||||
}
|
||||
|
||||
&__selection-item {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
background: var(--primary-very-low);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--primary-low);
|
||||
height: calc(var(--row-height) - 6);
|
||||
padding-inline: 0.25rem;
|
||||
margin: 3px;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: popIn 0.1s ease-out;
|
||||
}
|
||||
|
||||
.d-icon-times {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chat-channel-title__name {
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
&__username {
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
&.-active {
|
||||
border-color: var(--secondary-high);
|
||||
}
|
||||
|
||||
&-remove-btn {
|
||||
padding-inline: 0.25rem;
|
||||
font-size: var(--font-down-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-medium);
|
||||
|
||||
.chat-message-creator__selection__remove-btn {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-items {
|
||||
&-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--row-height);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
&-container {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
&__open-dm-btn {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
@include ellipsis;
|
||||
padding: 0.5rem;
|
||||
max-width: 40%;
|
||||
|
||||
.d-button-label {
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-down-2);
|
||||
color: var(--secondary-medium);
|
||||
flex: 3;
|
||||
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
display: inline-flex;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
margin-inline: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,4 +31,22 @@
|
||||
margin: 10px auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-creator__new-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.chat-message-creator__search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-message-creator__add-members-header-container,
|
||||
.chat-message-creator__list-container {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary-medium);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
.chat-channel-info {
|
||||
max-width: 500px;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
.chat-message-creator {
|
||||
&__row {
|
||||
&.-active {
|
||||
background: var(--tertiary-very-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
// .chat-message-creator {
|
||||
// &__row {
|
||||
// &.-active {
|
||||
// background: var(--tertiary-very-low);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
@ -5,6 +5,7 @@
|
||||
@import "chat-index-full-page";
|
||||
@import "chat-message-actions";
|
||||
@import "chat-message";
|
||||
@import "chat-channel-info";
|
||||
@import "chat-message-creator";
|
||||
@import "chat-message-thread-indicator";
|
||||
@import "sidebar-extensions";
|
||||
|
@ -1,3 +1,3 @@
|
||||
.chat-channel-settings {
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
6
plugins/chat/assets/stylesheets/mobile/chat-form.scss
Normal file
6
plugins/chat/assets/stylesheets/mobile/chat-form.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.chat-form__section {
|
||||
&-content {
|
||||
background: var(--primary-very-low);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
.chat-message-creator {
|
||||
&__open-dm-btn {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__row {
|
||||
padding-block: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-channel-title__name,
|
||||
.chat-user-display-name {
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
|
||||
.chat-channel-title__category-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
svg:not(.chat-channel-title__restricted-category-icon) {
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-user-avatar {
|
||||
&__container {
|
||||
padding: 0;
|
||||
}
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,3 +16,4 @@
|
||||
@import "chat-channel-row";
|
||||
@import "chat-channel-members";
|
||||
@import "chat-channel-settings";
|
||||
@import "chat-form";
|
||||
|
@ -333,9 +333,11 @@ en:
|
||||
default_search_placeholder: "#a-channel, @somebody or anything"
|
||||
default_channel_search_placeholder: "#a-channel"
|
||||
default_user_search_placeholder: "@somebody"
|
||||
user_search_placeholder: "...add more users"
|
||||
user_search_placeholder: "...add more members"
|
||||
disabled_user: "has disabled chat"
|
||||
no_items: "No items"
|
||||
create_group_placeholder: "Group chat name (optional)"
|
||||
participants_counter: "%{selection_count}/%{max} participants"
|
||||
|
||||
channel_edit_name_slug_modal:
|
||||
title: Edit channel
|
||||
@ -350,10 +352,15 @@ en:
|
||||
description: Tell people what this channel is all about
|
||||
|
||||
direct_message_creator:
|
||||
add_to_channel: "Add to channel"
|
||||
title: New Message
|
||||
prefix: "To:"
|
||||
no_results: No results
|
||||
selected_user_title: "Deselect %{username}"
|
||||
group_name: "Group chat name (optional)"
|
||||
members_counter:
|
||||
one: "%{count}/%{max} member"
|
||||
other: "%{count}/%{max} members"
|
||||
|
||||
channel:
|
||||
no_memberships: This channel has no members
|
||||
|
@ -130,6 +130,9 @@ en:
|
||||
transcript_title: "Transcript of previous messages in %{channel_name}"
|
||||
transcript_body: "To give you more context, we included a transcript of the previous messages in this conversation (up to ten):\n\n%{transcript}"
|
||||
channel:
|
||||
users_invited_to_channel:
|
||||
one: "%{invited_users} has been invited by %{inviting_user}."
|
||||
other: "%{invited_users} have been invited by %{inviting_user}."
|
||||
archive:
|
||||
first_post_raw: "This topic is an archive of the [%{channel_name}](%{channel_url}) chat channel."
|
||||
messages_moved:
|
||||
|
@ -18,6 +18,7 @@ Chat::Engine.routes.draw do
|
||||
post "/channels/:channel_id/invites" => "channels_invites#create"
|
||||
post "/channels/:channel_id/archives" => "channels_archives#create"
|
||||
get "/channels/:channel_id/memberships" => "channels_memberships#index"
|
||||
post "/channels/:channel_id/memberships" => "channels_memberships#create"
|
||||
delete "/channels/:channel_id/memberships/me" => "channels_current_user_membership#destroy"
|
||||
post "/channels/:channel_id/memberships/me" => "channels_current_user_membership#create"
|
||||
put "/channels/:channel_id/notifications-settings/me" =>
|
||||
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddGroupFieldToDirectMessageChannels < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :direct_message_channels, :group, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SetMultiusersDirectMessageChannelsAsGroup < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
execute <<-SQL
|
||||
UPDATE direct_message_channels
|
||||
SET "group" = true
|
||||
WHERE id IN (
|
||||
SELECT direct_message_channel_id
|
||||
FROM direct_message_users
|
||||
GROUP BY direct_message_channel_id
|
||||
HAVING COUNT(user_id) > 2
|
||||
);
|
||||
SQL
|
||||
end
|
||||
end
|
@ -88,7 +88,17 @@ module Chat
|
||||
|
||||
allowed_channel_ids = generate_allowed_channel_ids_sql(guardian, exclude_dm_channels: true)
|
||||
|
||||
channels = Chat::Channel.includes(:last_message, chatable: [:topic_only_relative_url])
|
||||
channels =
|
||||
Chat::Channel.includes(
|
||||
:last_message,
|
||||
chatable: %i[
|
||||
topic_only_relative_url
|
||||
uploaded_background
|
||||
uploaded_background_dark
|
||||
uploaded_logo
|
||||
uploaded_logo_dark
|
||||
],
|
||||
)
|
||||
channels = channels.includes(:chat_channel_archive) if options[:include_archives]
|
||||
|
||||
channels =
|
||||
|
@ -33,9 +33,13 @@ Fabricator(:private_category_channel, from: :category_channel) do
|
||||
end
|
||||
|
||||
Fabricator(:direct_message_channel, from: :chat_channel) do
|
||||
transient :users, following: true, with_membership: true
|
||||
transient :users, :group, following: true, with_membership: true
|
||||
chatable do |attrs|
|
||||
Fabricate(:direct_message, users: attrs[:users] || [Fabricate(:user), Fabricate(:user)])
|
||||
Fabricate(
|
||||
:direct_message,
|
||||
users: attrs[:users] || [Fabricate(:user), Fabricate(:user)],
|
||||
group: attrs[:group] || false,
|
||||
)
|
||||
end
|
||||
status { :open }
|
||||
name nil
|
||||
|
@ -84,17 +84,19 @@ RSpec.describe "Outgoing chat webhooks" do
|
||||
expect(payload_channel["chatable_id"]).to eq(direct_message.id)
|
||||
expect(payload_channel["chatable_type"]).to eq("DirectMessage")
|
||||
expect(payload_channel["chatable_url"]).to be_nil
|
||||
expect(payload_channel["chatable"]["users"][0]["id"]).to eq(user2.id)
|
||||
expect(payload_channel["chatable"]["users"][0]["username"]).to eq(user2.username)
|
||||
expect(payload_channel["chatable"]["users"][0]["name"]).to eq(user2.name)
|
||||
expect(payload_channel["chatable"]["users"][0]["avatar_template"]).to eq(
|
||||
user2.avatar_template,
|
||||
)
|
||||
expect(payload_channel["chatable"]["users"][0]["can_chat"]).to eq(true)
|
||||
expect(payload_channel["chatable"]["users"][0]["has_chat_enabled"]).to eq(true)
|
||||
expect(payload_channel["title"]).to eq(channel.title(user1))
|
||||
expect(payload_channel["slug"]).to be_nil
|
||||
|
||||
membership =
|
||||
payload_channel["chatable"]["memberships"].detect { |m| m["user"]["id"] == user2.id }
|
||||
user = membership["user"]
|
||||
|
||||
expect(user["username"]).to eq(user2.username)
|
||||
expect(user["name"]).to eq(user2.name)
|
||||
expect(user["avatar_template"]).to eq(user2.avatar_template)
|
||||
expect(user["can_chat"]).to eq(true)
|
||||
expect(user["has_chat_enabled"]).to eq(true)
|
||||
|
||||
yield(payload_channel) if block_given?
|
||||
end
|
||||
|
||||
|
@ -85,7 +85,7 @@ RSpec.describe Chat::GuardianExtensions do
|
||||
end
|
||||
|
||||
it "returns true if the user is part of the direct message" do
|
||||
Chat::DirectMessageUser.create!(user: user, direct_message: chatable)
|
||||
channel.add(user)
|
||||
expect(guardian.can_join_chat_channel?(channel)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::Api::ChannelsMembershipsController do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) do
|
||||
Fabricate(:direct_message_channel, group: true, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
describe "success" do
|
||||
it "works" do
|
||||
post "/chat/api/channels/#{channel_1.id}/memberships",
|
||||
params: {
|
||||
usernames: [Fabricate(:user).username],
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when users can't be added" do
|
||||
before { channel_1.chatable.update(group: false) }
|
||||
|
||||
it "returns a 422" do
|
||||
post "/chat/api/channels/#{channel_1.id}/memberships",
|
||||
params: {
|
||||
usernames: [Fabricate(:user).username],
|
||||
}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
before { channel_1.chatable.update!(group: false) }
|
||||
|
||||
it "returns a 404" do
|
||||
get "/chat/api/channels/-999/messages", params: { usernames: [Fabricate(:user).username] }
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -14,13 +14,16 @@ RSpec.describe Chat::Api::ChatablesController do
|
||||
describe "without chat permissions" do
|
||||
it "errors errors for anon" do
|
||||
get "/chat/api/chatables"
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "errors when user cannot chat" do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
|
||||
sign_in(current_user)
|
||||
|
||||
get "/chat/api/chatables"
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
@ -28,9 +31,11 @@ RSpec.describe Chat::Api::ChatablesController do
|
||||
describe "with chat permissions" do
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
|
||||
before { sign_in(current_user) }
|
||||
before { channel_1.add(current_user) }
|
||||
|
||||
it "returns results" do
|
||||
sign_in(current_user)
|
||||
|
||||
get "/chat/api/chatables", params: { term: channel_1.name }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
@ -23,7 +23,10 @@ RSpec.describe Chat::Api::DirectMessagesController do
|
||||
describe "#create" do
|
||||
before { Group.refresh_automatic_groups! }
|
||||
|
||||
shared_examples "creating dms" do
|
||||
describe "dm with one other user" do
|
||||
let(:usernames) { user1.username }
|
||||
let(:direct_message_user_ids) { [current_user.id, user1.id] }
|
||||
|
||||
it "creates a new dm channel with username(s) provided" do
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
@ -35,31 +38,55 @@ RSpec.describe Chat::Api::DirectMessagesController do
|
||||
|
||||
it "returns existing dm channel if one exists for username(s)" do
|
||||
create_dm_channel(direct_message_user_ids)
|
||||
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
}.not_to change { Chat::DirectMessage.count }
|
||||
end
|
||||
end
|
||||
|
||||
describe "dm with one other user" do
|
||||
let(:usernames) { user1.username }
|
||||
let(:direct_message_user_ids) { [current_user.id, user1.id] }
|
||||
|
||||
include_examples "creating dms"
|
||||
end
|
||||
|
||||
describe "dm with myself" do
|
||||
let(:usernames) { [current_user.username] }
|
||||
let(:direct_message_user_ids) { [current_user.id] }
|
||||
|
||||
include_examples "creating dms"
|
||||
it "creates a new dm channel with username(s) provided" do
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
}.to change { Chat::DirectMessage.count }.by(1)
|
||||
expect(Chat::DirectMessage.last.direct_message_users.map(&:user_id)).to match_array(
|
||||
direct_message_user_ids,
|
||||
)
|
||||
end
|
||||
|
||||
it "returns existing dm channel if one exists for username(s)" do
|
||||
create_dm_channel(direct_message_user_ids)
|
||||
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
}.not_to change { Chat::DirectMessage.count }
|
||||
end
|
||||
end
|
||||
|
||||
describe "dm with two other users" do
|
||||
let(:usernames) { [user1, user2, user3].map(&:username) }
|
||||
let(:direct_message_user_ids) { [current_user.id, user1.id, user2.id, user3.id] }
|
||||
|
||||
include_examples "creating dms"
|
||||
it "creates a new dm channel with username(s) provided" do
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
}.to change { Chat::DirectMessage.count }.by(1)
|
||||
expect(Chat::DirectMessage.last.direct_message_users.map(&:user_id)).to match_array(
|
||||
direct_message_user_ids,
|
||||
)
|
||||
end
|
||||
|
||||
it "createsa new dm channel" do
|
||||
create_dm_channel(direct_message_user_ids)
|
||||
|
||||
expect {
|
||||
post "/chat/api/direct-message-channels.json", params: { target_usernames: [usernames] }
|
||||
}.to change { Chat::DirectMessage.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "creates Chat::UserChatChannelMembership records" do
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user