mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: new jump to channel menu (#22383)
This commit replaces two existing screens: - draft - channel selection modal Main features compared to existing solutions - features are now combined, meaning you can for example create multi users DM - it will show users with chat disabled - it shows unread state - hopefully a better look/feel - lots of small details and fixes... Other noticeable fixes - starting a DM with a user, even from the user card and clicking <kbd>Chat</kbd> will not show a green dot for the target user (or even the channel) until a message is actually sent - it should almost never do a full page reload anymore --------- Co-authored-by: Martin Brennan <mjrbrennan@gmail.com> Co-authored-by: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com> Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com> Co-authored-by: Mark VanLandingham <markvanlan@gmail.com>
This commit is contained in:
parent
e72153dd1a
commit
d75d64bf16
@ -1,7 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
CHANNEL_EDITABLE_PARAMS ||= %i[name description slug]
|
||||||
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions threading_enabled]
|
CATEGORY_CHANNEL_EDITABLE_PARAMS ||= %i[
|
||||||
|
auto_join_users
|
||||||
|
allow_channel_wide_mentions
|
||||||
|
threading_enabled
|
||||||
|
]
|
||||||
|
|
||||||
class Chat::Api::ChannelsController < Chat::ApiController
|
class Chat::Api::ChannelsController < Chat::ApiController
|
||||||
def index
|
def index
|
||||||
@ -12,7 +16,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
|||||||
options[:status] = Chat::Channel.statuses[permitted[:status]] ? permitted[:status] : nil
|
options[:status] = Chat::Channel.statuses[permitted[:status]] ? permitted[:status] : nil
|
||||||
|
|
||||||
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
||||||
channels = Chat::ChannelFetcher.secured_public_channels(guardian, memberships, options)
|
channels = Chat::ChannelFetcher.secured_public_channels(guardian, options)
|
||||||
serialized_channels =
|
serialized_channels =
|
||||||
channels.map do |channel|
|
channels.map do |channel|
|
||||||
Chat::ChannelSerializer.new(
|
Chat::ChannelSerializer.new(
|
||||||
|
@ -1,83 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Chat::Api::ChatablesController < Chat::ApiController
|
class Chat::Api::ChatablesController < Chat::ApiController
|
||||||
|
before_action :ensure_logged_in
|
||||||
|
|
||||||
def index
|
def index
|
||||||
params.require(:filter)
|
with_service(::Chat::SearchChatable) do
|
||||||
filter = params[:filter].downcase
|
on_success { render_serialized(result, ::Chat::ChatablesSerializer, root: false) }
|
||||||
|
|
||||||
memberships = Chat::ChannelMembershipManager.all_for_user(current_user)
|
|
||||||
|
|
||||||
public_channels =
|
|
||||||
Chat::ChannelFetcher.secured_public_channels(
|
|
||||||
guardian,
|
|
||||||
memberships,
|
|
||||||
filter: filter,
|
|
||||||
status: :open,
|
|
||||||
)
|
|
||||||
|
|
||||||
users = User.joins(:user_option).where.not(id: current_user.id)
|
|
||||||
if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
|
|
||||||
users =
|
|
||||||
users
|
|
||||||
.joins(:groups)
|
|
||||||
.where(groups: { id: Chat.allowed_group_ids })
|
|
||||||
.or(users.joins(:groups).staff)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
users = users.where(user_option: { chat_enabled: true })
|
|
||||||
like_filter = "%#{filter}%"
|
|
||||||
if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
|
|
||||||
users = users.where("users.username_lower ILIKE ?", like_filter)
|
|
||||||
else
|
|
||||||
users =
|
|
||||||
users.where(
|
|
||||||
"LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?",
|
|
||||||
like_filter,
|
|
||||||
like_filter,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
users = users.limit(25).uniq
|
|
||||||
|
|
||||||
direct_message_channels =
|
|
||||||
if users.count > 0
|
|
||||||
# FIXME: investigate the cost of this query
|
|
||||||
Chat::DirectMessageChannel
|
|
||||||
.includes(chatable: :users)
|
|
||||||
.joins(direct_message: :direct_message_users)
|
|
||||||
.group(1)
|
|
||||||
.having(
|
|
||||||
"ARRAY[?] <@ ARRAY_AGG(user_id) AND ARRAY[?] && ARRAY_AGG(user_id)",
|
|
||||||
[current_user.id],
|
|
||||||
users.map(&:id),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
user_ids_with_channel = []
|
|
||||||
direct_message_channels.each do |dm_channel|
|
|
||||||
user_ids = dm_channel.chatable.users.map(&:id)
|
|
||||||
user_ids_with_channel.concat(user_ids) if user_ids.count < 3
|
|
||||||
end
|
|
||||||
|
|
||||||
users_without_channel = users.filter { |u| !user_ids_with_channel.include?(u.id) }
|
|
||||||
|
|
||||||
if current_user.username.downcase.start_with?(filter)
|
|
||||||
# We filtered out the current user for the query earlier, but check to see
|
|
||||||
# if they should be included, and add.
|
|
||||||
users_without_channel << current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
render_serialized(
|
|
||||||
{
|
|
||||||
public_channels: public_channels,
|
|
||||||
direct_message_channels: direct_message_channels,
|
|
||||||
users: users_without_channel,
|
|
||||||
memberships: memberships,
|
|
||||||
},
|
|
||||||
Chat::ChannelSearchSerializer,
|
|
||||||
root: false,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -83,10 +83,7 @@ module Chat
|
|||||||
Chat::MessageRateLimiter.run!(current_user)
|
Chat::MessageRateLimiter.run!(current_user)
|
||||||
|
|
||||||
@user_chat_channel_membership =
|
@user_chat_channel_membership =
|
||||||
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
|
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(current_user)
|
||||||
current_user,
|
|
||||||
following: true,
|
|
||||||
)
|
|
||||||
raise Discourse::InvalidAccess unless @user_chat_channel_membership
|
raise Discourse::InvalidAccess unless @user_chat_channel_membership
|
||||||
|
|
||||||
reply_to_msg_id = params[:in_reply_to_id]
|
reply_to_msg_id = params[:in_reply_to_id]
|
||||||
|
@ -7,6 +7,7 @@ module Chat
|
|||||||
:desktop_notification_level,
|
:desktop_notification_level,
|
||||||
:mobile_notification_level,
|
:mobile_notification_level,
|
||||||
:chat_channel_id,
|
:chat_channel_id,
|
||||||
:last_read_message_id
|
:last_read_message_id,
|
||||||
|
:last_viewed_at
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class ChatableUserSerializer < ::Chat::UserWithCustomFieldsAndStatusSerializer
|
||||||
|
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
|
63
plugins/chat/app/serializers/chat/chatables_serializer.rb
Normal file
63
plugins/chat/app/serializers/chat/chatables_serializer.rb
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
class ChatablesSerializer < ::ApplicationSerializer
|
||||||
|
attributes :users
|
||||||
|
attributes :direct_message_channels
|
||||||
|
attributes :category_channels
|
||||||
|
|
||||||
|
def users
|
||||||
|
(object.users || [])
|
||||||
|
.map do |user|
|
||||||
|
{
|
||||||
|
identifier: "u-#{user.id}",
|
||||||
|
model: ::Chat::ChatableUserSerializer.new(user, scope: scope, root: false),
|
||||||
|
type: "user",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
.as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_message_channels
|
||||||
|
(object.direct_message_channels || [])
|
||||||
|
.map do |channel|
|
||||||
|
{
|
||||||
|
identifier: "c-#{channel.id}",
|
||||||
|
type: "channel",
|
||||||
|
model:
|
||||||
|
::Chat::ChannelSerializer.new(
|
||||||
|
channel,
|
||||||
|
scope: scope,
|
||||||
|
root: false,
|
||||||
|
membership: channel_membership(channel.id),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
.as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def category_channels
|
||||||
|
(object.category_channels || [])
|
||||||
|
.map do |channel|
|
||||||
|
{
|
||||||
|
identifier: "c-#{channel.id}",
|
||||||
|
type: "channel",
|
||||||
|
model:
|
||||||
|
::Chat::ChannelSerializer.new(
|
||||||
|
channel,
|
||||||
|
scope: scope,
|
||||||
|
root: false,
|
||||||
|
membership: channel_membership(channel.id),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
.as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def channel_membership(channel_id)
|
||||||
|
object.memberships.find { |membership| membership.chat_channel_id == channel_id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -4,7 +4,7 @@ module Chat
|
|||||||
class DirectMessageSerializer < ApplicationSerializer
|
class DirectMessageSerializer < ApplicationSerializer
|
||||||
attributes :id
|
attributes :id
|
||||||
|
|
||||||
has_many :users, serializer: Chat::UserWithCustomFieldsAndStatusSerializer, embed: :objects
|
has_many :users, serializer: Chat::ChatableUserSerializer, embed: :objects
|
||||||
|
|
||||||
def users
|
def users
|
||||||
users = object.direct_message_users.map(&:user).map { |u| u || Chat::DeletedUser.new }
|
users = object.direct_message_users.map(&:user).map { |u| u || Chat::DeletedUser.new }
|
||||||
|
@ -33,7 +33,6 @@ module Chat
|
|||||||
model :direct_message, :fetch_or_create_direct_message
|
model :direct_message, :fetch_or_create_direct_message
|
||||||
model :channel, :fetch_or_create_channel
|
model :channel, :fetch_or_create_channel
|
||||||
step :update_memberships
|
step :update_memberships
|
||||||
step :publish_channel
|
|
||||||
|
|
||||||
# @!visibility private
|
# @!visibility private
|
||||||
class Contract
|
class Contract
|
||||||
@ -68,7 +67,7 @@ module Chat
|
|||||||
Chat::DirectMessageChannel.find_or_create_by(chatable: direct_message)
|
Chat::DirectMessageChannel.find_or_create_by(chatable: direct_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_memberships(guardian:, channel:, target_users:, **)
|
def update_memberships(channel:, target_users:, **)
|
||||||
always_level = Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
always_level = Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||||
|
|
||||||
memberships =
|
memberships =
|
||||||
@ -77,7 +76,7 @@ module Chat
|
|||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
chat_channel_id: channel.id,
|
chat_channel_id: channel.id,
|
||||||
muted: false,
|
muted: false,
|
||||||
following: true,
|
following: false,
|
||||||
desktop_notification_level: always_level,
|
desktop_notification_level: always_level,
|
||||||
mobile_notification_level: always_level,
|
mobile_notification_level: always_level,
|
||||||
created_at: Time.zone.now,
|
created_at: Time.zone.now,
|
||||||
@ -90,9 +89,5 @@ module Chat
|
|||||||
unique_by: %i[user_id chat_channel_id],
|
unique_by: %i[user_id chat_channel_id],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_channel(channel:, target_users:, **)
|
|
||||||
Chat::Publisher.publish_new_channel(channel, target_users)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -364,23 +364,26 @@ module Chat
|
|||||||
NEW_CHANNEL_MESSAGE_BUS_CHANNEL = "/chat/new-channel"
|
NEW_CHANNEL_MESSAGE_BUS_CHANNEL = "/chat/new-channel"
|
||||||
|
|
||||||
def self.publish_new_channel(chat_channel, users)
|
def self.publish_new_channel(chat_channel, users)
|
||||||
users.each do |user|
|
Chat::UserChatChannelMembership
|
||||||
# FIXME: This could generate a lot of queries depending on the amount of users
|
.includes(:user)
|
||||||
membership = chat_channel.membership_for(user)
|
.where(chat_channel: chat_channel, user: users)
|
||||||
|
.find_in_batches do |memberships|
|
||||||
|
memberships.each do |membership|
|
||||||
|
serialized_channel =
|
||||||
|
Chat::ChannelSerializer.new(
|
||||||
|
chat_channel,
|
||||||
|
scope: Guardian.new(membership.user), # We need a guardian here for direct messages
|
||||||
|
root: :channel,
|
||||||
|
membership: membership,
|
||||||
|
).as_json
|
||||||
|
|
||||||
# TODO: this event is problematic as some code will update the membership before calling it
|
MessageBus.publish(
|
||||||
# and other code will update it after calling it
|
NEW_CHANNEL_MESSAGE_BUS_CHANNEL,
|
||||||
# it means frontend must handle logic for both cases
|
serialized_channel,
|
||||||
serialized_channel =
|
user_ids: [membership.user.id],
|
||||||
Chat::ChannelSerializer.new(
|
)
|
||||||
chat_channel,
|
end
|
||||||
scope: Guardian.new(user), # We need a guardian here for direct messages
|
end
|
||||||
root: :channel,
|
|
||||||
membership: membership,
|
|
||||||
).as_json
|
|
||||||
|
|
||||||
MessageBus.publish(NEW_CHANNEL_MESSAGE_BUS_CHANNEL, serialized_channel, user_ids: [user.id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_inaccessible_mentions(
|
def self.publish_inaccessible_mentions(
|
||||||
|
109
plugins/chat/app/services/chat/search_chatable.rb
Normal file
109
plugins/chat/app/services/chat/search_chatable.rb
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chat
|
||||||
|
# Returns a list of chatables (users, category channels, direct message channels) that can be chatted with.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# Chat::SearchChatable.call(term: "@bob", guardian: guardian)
|
||||||
|
#
|
||||||
|
class SearchChatable
|
||||||
|
include Service::Base
|
||||||
|
|
||||||
|
# @!method call(term:, guardian:)
|
||||||
|
# @param [String] term
|
||||||
|
# @param [Guardian] guardian
|
||||||
|
# @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
|
||||||
|
|
||||||
|
# @!visibility private
|
||||||
|
class Contract
|
||||||
|
attribute :term, default: ""
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_users(guardian:, **)
|
||||||
|
return unless guardian.can_create_direct_message?
|
||||||
|
return if context.mode == :channel
|
||||||
|
context.users = search_users(context.term, guardian)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_category_channels(guardian:, **)
|
||||||
|
return if context.mode == :user
|
||||||
|
|
||||||
|
context.category_channels =
|
||||||
|
Chat::ChannelFetcher.secured_public_channels(
|
||||||
|
guardian,
|
||||||
|
filter: context.term,
|
||||||
|
status: :open,
|
||||||
|
limit: 10,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
channels =
|
||||||
|
Chat::ChannelFetcher.secured_direct_message_channels_search(
|
||||||
|
guardian.user.id,
|
||||||
|
guardian,
|
||||||
|
limit: 10,
|
||||||
|
user_ids: user_ids,
|
||||||
|
) || []
|
||||||
|
|
||||||
|
if user_ids.present? && context.mode == :all
|
||||||
|
channels =
|
||||||
|
channels.reject do |channel|
|
||||||
|
channel_user_ids = channel.allowed_user_ids - [guardian.user.id]
|
||||||
|
channel.allowed_user_ids.length == 1 &&
|
||||||
|
user_ids.include?(channel.allowed_user_ids.first) ||
|
||||||
|
channel_user_ids.length == 1 && user_ids.include?(channel_user_ids.first)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context.direct_message_channels = channels
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_users(term, guardian)
|
||||||
|
user_search = UserSearch.new(term, limit: 10)
|
||||||
|
|
||||||
|
if term.blank?
|
||||||
|
user_search.scoped_users.includes(:user_option)
|
||||||
|
else
|
||||||
|
user_search.search.includes(:user_option)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -18,6 +18,8 @@ module Service
|
|||||||
|
|
||||||
# Simple structure to hold the context of the service during its whole lifecycle.
|
# Simple structure to hold the context of the service during its whole lifecycle.
|
||||||
class Context < OpenStruct
|
class Context < OpenStruct
|
||||||
|
include ActiveModel::Serialization
|
||||||
|
|
||||||
# @return [Boolean] returns +true+ if the context is set as successful (default)
|
# @return [Boolean] returns +true+ if the context is set as successful (default)
|
||||||
def success?
|
def success?
|
||||||
!failure?
|
!failure?
|
||||||
|
@ -21,7 +21,6 @@ export default function () {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.route("draft-channel", { path: "/draft-channel" });
|
|
||||||
this.route("browse", { path: "/browse" }, function () {
|
this.route("browse", { path: "/browse" }, function () {
|
||||||
this.route("all", { path: "/all" });
|
this.route("all", { path: "/all" });
|
||||||
this.route("closed", { path: "/closed" });
|
this.route("closed", { path: "/closed" });
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
{{#if this.showMobileDirectMessageButton}}
|
{{#if this.showMobileDirectMessageButton}}
|
||||||
<LinkTo
|
<DButton
|
||||||
@route="chat.draft-channel"
|
@icon="plus"
|
||||||
class="btn-flat open-draft-channel-page-btn keep-mobile-sidebar-open btn-floating"
|
class="no-text btn-flat open-new-message-btn keep-mobile-sidebar-open btn-floating"
|
||||||
title={{i18n "chat.direct_messages.new"}}
|
@action={{this.openNewMessageModal}}
|
||||||
>
|
title={{i18n this.createDirectMessageChannelLabel}}
|
||||||
{{d-icon "plus"}}
|
/>
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -95,13 +94,12 @@
|
|||||||
(not this.showMobileDirectMessageButton)
|
(not this.showMobileDirectMessageButton)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
<LinkTo
|
<DButton
|
||||||
@route="chat.draft-channel"
|
@icon="plus"
|
||||||
class="btn no-text btn-flat open-draft-channel-page-btn"
|
class="no-text btn-flat open-new-message-btn"
|
||||||
|
@action={{this.openNewMessageModal}}
|
||||||
title={{i18n this.createDirectMessageChannelLabel}}
|
title={{i18n this.createDirectMessageChannelLabel}}
|
||||||
>
|
/>
|
||||||
{{d-icon "plus"}}
|
|
||||||
</LinkTo>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -4,6 +4,8 @@ import { action } from "@ember/object";
|
|||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||||
|
|
||||||
export default class ChannelsList extends Component {
|
export default class ChannelsList extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service router;
|
@service router;
|
||||||
@ -12,6 +14,7 @@ export default class ChannelsList extends Component {
|
|||||||
@service site;
|
@service site;
|
||||||
@service session;
|
@service session;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
@tracked hasScrollbar = false;
|
@tracked hasScrollbar = false;
|
||||||
|
|
||||||
@ -25,6 +28,11 @@ export default class ChannelsList extends Component {
|
|||||||
this.computeHasScrollbar(entries[0].target);
|
this.computeHasScrollbar(entries[0].target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openNewMessageModal() {
|
||||||
|
this.modal.show(ChatNewMessageModal);
|
||||||
|
}
|
||||||
|
|
||||||
get showMobileDirectMessageButton() {
|
get showMobileDirectMessageButton() {
|
||||||
return this.site.mobileView && this.canCreateDirectMessageChannel;
|
return this.site.mobileView && this.canCreateDirectMessageChannel;
|
||||||
}
|
}
|
||||||
|
@ -59,10 +59,10 @@
|
|||||||
<span class="empty-state-title">{{i18n "chat.empty_state.title"}}</span>
|
<span class="empty-state-title">{{i18n "chat.empty_state.title"}}</span>
|
||||||
<div class="empty-state-body">
|
<div class="empty-state-body">
|
||||||
<p>{{i18n "chat.empty_state.direct_message"}}</p>
|
<p>{{i18n "chat.empty_state.direct_message"}}</p>
|
||||||
|
<DButton
|
||||||
<LinkTo @route={{concat "chat.draft-channel"}}>
|
@action={{this.showChatNewMessageModal}}
|
||||||
{{i18n "chat.empty_state.direct_message_cta"}}
|
label="chat.empty_state.direct_message_cta"
|
||||||
</LinkTo>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else if this.channelsCollection.length}}
|
{{else if this.channelsCollection.length}}
|
||||||
|
@ -5,6 +5,7 @@ import { schedule } from "@ember/runloop";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||||
|
|
||||||
const TABS = ["all", "open", "closed", "archived"];
|
const TABS = ["all", "open", "closed", "archived"];
|
||||||
|
|
||||||
@ -38,6 +39,11 @@ export default class ChatBrowseView extends Component {
|
|||||||
return document.querySelector("#chat-progress-bar-container");
|
return document.querySelector("#chat-progress-bar-container");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showChatNewMessageModal() {
|
||||||
|
this.modal.show(ChatNewMessageModal);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onScroll() {
|
onScroll() {
|
||||||
discourseDebounce(
|
discourseDebounce(
|
||||||
|
@ -19,7 +19,7 @@ export default class ChatChannelMembersView extends Component {
|
|||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (!this.channel || this.channel.isDraft) {
|
if (!this.channel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
<div
|
|
||||||
class={{this.rowClassNames}}
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
{{on "click" this.handleClick}}
|
|
||||||
data-id={{this.model.id}}
|
|
||||||
>
|
|
||||||
{{#if this.model.user}}
|
|
||||||
{{avatar this.model imageSize="tiny"}}
|
|
||||||
<span class="username">
|
|
||||||
{{this.model.username}}
|
|
||||||
</span>
|
|
||||||
{{else}}
|
|
||||||
<ChatChannelTitle @channel={{this.model}} />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: "",
|
|
||||||
|
|
||||||
isFocused: false,
|
|
||||||
|
|
||||||
@discourseComputed("model", "isFocused")
|
|
||||||
rowClassNames(model, isFocused) {
|
|
||||||
return `chat-channel-selection-row ${isFocused ? "focused" : ""} ${
|
|
||||||
this.model.user ? "user-row" : "channel-row"
|
|
||||||
}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleClick(event) {
|
|
||||||
if (this.onClick) {
|
|
||||||
this.onClick(this.model);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,33 +0,0 @@
|
|||||||
<DModalBody @title="chat.channel_selector.title">
|
|
||||||
<div id="chat-channel-selector-modal-inner">
|
|
||||||
<div class="chat-channel-selector-input-container">
|
|
||||||
<span class="search-icon">
|
|
||||||
{{d-icon "search"}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id="chat-channel-selector-input"
|
|
||||||
@type="text"
|
|
||||||
@value={{this.filter}}
|
|
||||||
autocomplete="off"
|
|
||||||
{{on "input" (action "search" value="target.value")}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="channels">
|
|
||||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
|
||||||
{{#each this.channels as |channel|}}
|
|
||||||
<ChatChannelSelectionRow
|
|
||||||
@isFocused={{eq channel this.focusedRow}}
|
|
||||||
@model={{channel}}
|
|
||||||
@onClick={{this.switchChannel}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<div class="no-channels-notice">
|
|
||||||
{{i18n "chat.channel_selector.no_channels"}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</ConditionalLoadingSpinner>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DModalBody>
|
|
@ -1,235 +0,0 @@
|
|||||||
import Component from "@ember/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
|
||||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
|
||||||
import { isPresent } from "@ember/utils";
|
|
||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
|
||||||
import User from "discourse/models/user";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
chat: service(),
|
|
||||||
tagName: "",
|
|
||||||
filter: "",
|
|
||||||
channels: null,
|
|
||||||
searchIndex: 0,
|
|
||||||
loading: false,
|
|
||||||
chatChannelsManager: service(),
|
|
||||||
router: service(),
|
|
||||||
focusedRow: null,
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.appEvents.on("chat-channel-selector-modal:close", this.close);
|
|
||||||
document.addEventListener("keyup", this.onKeyUp);
|
|
||||||
document
|
|
||||||
.getElementById("chat-channel-selector-modal-inner")
|
|
||||||
?.addEventListener("mouseover", this.mouseover);
|
|
||||||
document.getElementById("chat-channel-selector-input")?.focus();
|
|
||||||
|
|
||||||
this.getInitialChannels();
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.appEvents.off("chat-channel-selector-modal:close", this.close);
|
|
||||||
document.removeEventListener("keyup", this.onKeyUp);
|
|
||||||
document
|
|
||||||
.getElementById("chat-channel-selector-modal-inner")
|
|
||||||
?.removeEventListener("mouseover", this.mouseover);
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
mouseover(e) {
|
|
||||||
if (e.target.classList.contains("chat-channel-selection-row")) {
|
|
||||||
let channel;
|
|
||||||
const id = parseInt(e.target.dataset.id, 10);
|
|
||||||
if (e.target.classList.contains("channel-row")) {
|
|
||||||
channel = this.channels.findBy("id", id);
|
|
||||||
} else {
|
|
||||||
channel = this.channels.find((c) => c.user && c.id === id);
|
|
||||||
}
|
|
||||||
if (channel) {
|
|
||||||
this.set("focusedRow", channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
onKeyUp(e) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
let focusedChannel = this.channels.find((c) => c === this.focusedRow);
|
|
||||||
this.switchChannel(focusedChannel);
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === "ArrowDown") {
|
|
||||||
this.arrowNavigateChannels("down");
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
this.arrowNavigateChannels("up");
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
arrowNavigateChannels(direction) {
|
|
||||||
const indexOfFocused = this.channels.findIndex(
|
|
||||||
(c) => c === this.focusedRow
|
|
||||||
);
|
|
||||||
if (indexOfFocused > -1) {
|
|
||||||
const nextIndex = direction === "down" ? 1 : -1;
|
|
||||||
const nextChannel = this.channels[indexOfFocused + nextIndex];
|
|
||||||
if (nextChannel) {
|
|
||||||
this.set("focusedRow", nextChannel);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.set("focusedRow", this.channels[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
let focusedChannel = document.querySelector(
|
|
||||||
"#chat-channel-selector-modal-inner .chat-channel-selection-row.focused"
|
|
||||||
);
|
|
||||||
focusedChannel?.scrollIntoView({ block: "nearest", inline: "start" });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
switchChannel(channel) {
|
|
||||||
if (channel instanceof User) {
|
|
||||||
return this.fetchOrCreateChannelForUser(channel).then((response) => {
|
|
||||||
const newChannel = this.chatChannelsManager.store(response.channel);
|
|
||||||
return this.chatChannelsManager.follow(newChannel).then((c) => {
|
|
||||||
this.router.transitionTo("chat.channel", ...c.routeModels);
|
|
||||||
this.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.chatChannelsManager.follow(channel).then((c) => {
|
|
||||||
this.router.transitionTo("chat.channel", ...c.routeModels);
|
|
||||||
this.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
search(value) {
|
|
||||||
if (isPresent(value?.trim())) {
|
|
||||||
discourseDebounce(
|
|
||||||
this,
|
|
||||||
this.fetchChannelsFromServer,
|
|
||||||
value?.trim(),
|
|
||||||
INPUT_DELAY
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
discourseDebounce(this, this.getInitialChannels, INPUT_DELAY);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
fetchChannelsFromServer(filter) {
|
|
||||||
if (this.isDestroyed || this.isDestroying) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
loading: true,
|
|
||||||
searchIndex: this.searchIndex + 1,
|
|
||||||
});
|
|
||||||
const thisSearchIndex = this.searchIndex;
|
|
||||||
ajax("/chat/api/chatables", { data: { filter } })
|
|
||||||
.then((searchModel) => {
|
|
||||||
if (this.searchIndex === thisSearchIndex) {
|
|
||||||
this.set("searchModel", searchModel);
|
|
||||||
let channels = searchModel.public_channels
|
|
||||||
.concat(searchModel.direct_message_channels, searchModel.users)
|
|
||||||
.map((c) => {
|
|
||||||
if (
|
|
||||||
c.chatable_type === "DirectMessage" ||
|
|
||||||
c.chatable_type === "Category"
|
|
||||||
) {
|
|
||||||
return ChatChannel.create(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
return User.create(c);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
channels,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
this.focusFirstChannel(this.channels);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
getInitialChannels() {
|
|
||||||
if (this.isDestroyed || this.isDestroying) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channels = this.getChannelsWithFilter(this.filter);
|
|
||||||
this.set("channels", channels);
|
|
||||||
this.focusFirstChannel(channels);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
fetchOrCreateChannelForUser(user) {
|
|
||||||
return ajax("/chat/api/direct-message-channels.json", {
|
|
||||||
method: "POST",
|
|
||||||
data: { target_usernames: [user.username] },
|
|
||||||
}).catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
|
|
||||||
focusFirstChannel(channels) {
|
|
||||||
if (channels[0]) {
|
|
||||||
this.set("focusedRow", channels[0]);
|
|
||||||
} else {
|
|
||||||
this.set("focusedRow", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
|
|
||||||
let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => {
|
|
||||||
return new Date(a.lastMessageSentAt) > new Date(b.lastMessageSentAt)
|
|
||||||
? -1
|
|
||||||
: 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const trimmedFilter = filter.trim();
|
|
||||||
const lowerCasedFilter = filter.toLowerCase();
|
|
||||||
|
|
||||||
return sortedChannels.filter((channel) => {
|
|
||||||
if (
|
|
||||||
opts.excludeActiveChannel &&
|
|
||||||
this.chat.activeChannel?.id === channel.id
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!trimmedFilter.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel.isDirectMessageChannel) {
|
|
||||||
let userFound = false;
|
|
||||||
channel.chatable.users.forEach((user) => {
|
|
||||||
if (
|
|
||||||
user.username.toLowerCase().includes(lowerCasedFilter) ||
|
|
||||||
user.name?.toLowerCase().includes(lowerCasedFilter)
|
|
||||||
) {
|
|
||||||
return (userFound = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return userFound;
|
|
||||||
} else {
|
|
||||||
return channel.title.toLowerCase().includes(lowerCasedFilter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,14 +1,6 @@
|
|||||||
{{#if @channel.isDraft}}
|
{{#if @channel.isDirectMessageChannel}}
|
||||||
<div class="chat-channel-title is-draft">
|
<div class="chat-channel-title is-dm">
|
||||||
<span class="chat-channel-title__name">{{@channel.title}}</span>
|
{{#if @channel.chatable.users.length}}
|
||||||
{{#if (has-block)}}
|
|
||||||
{{yield}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{#if @channel.isDirectMessageChannel}}
|
|
||||||
<div class="chat-channel-title is-dm">
|
|
||||||
|
|
||||||
<div class="chat-channel-title__avatar">
|
<div class="chat-channel-title__avatar">
|
||||||
{{#if this.multiDm}}
|
{{#if this.multiDm}}
|
||||||
<span class="chat-channel-title__users-count">
|
<span class="chat-channel-title__users-count">
|
||||||
@ -18,9 +10,11 @@
|
|||||||
<ChatUserAvatar @user={{@channel.chatable.users.firstObject}} />
|
<ChatUserAvatar @user={{@channel.chatable.users.firstObject}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<div class="chat-channel-title__user-info">
|
<div class="chat-channel-title__user-info">
|
||||||
<div class="chat-channel-title__usernames">
|
<div class="chat-channel-title__usernames">
|
||||||
|
{{#if @channel.chatable.users.length}}
|
||||||
{{#if this.multiDm}}
|
{{#if this.multiDm}}
|
||||||
<span class="chat-channel-title__name">{{this.usernames}}</span>
|
<span class="chat-channel-title__name">{{this.usernames}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -41,31 +35,33 @@
|
|||||||
/>
|
/>
|
||||||
{{/let}}
|
{{/let}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
{{else}}
|
||||||
</div>
|
<span class="chat-channel-title__name">Add users</span>
|
||||||
|
|
||||||
{{#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}}
|
{{/if}}
|
||||||
</span>
|
</div>
|
||||||
<span class="chat-channel-title__name">
|
|
||||||
{{replace-emoji @channel.title}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{#if (has-block)}}
|
|
||||||
{{yield}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
|
||||||
|
{{#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}}
|
{{/if}}
|
@ -74,14 +74,14 @@
|
|||||||
@pane={{this.pane}}
|
@pane={{this.pane}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
{{#if (and (not @channel.isFollowing) @channel.isCategoryChannel)}}
|
||||||
|
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||||
|
{{else}}
|
||||||
<Chat::Composer::Channel
|
<Chat::Composer::Channel
|
||||||
@channel={{@channel}}
|
@channel={{@channel}}
|
||||||
@uploadDropZone={{this.uploadDropZone}}
|
@uploadDropZone={{this.uploadDropZone}}
|
||||||
@onSendMessage={{this.onSendMessage}}
|
@onSendMessage={{this.onSendMessage}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
|
||||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import { action } from "@ember/object";
|
|||||||
// TODO (martin) Remove this when the handleSentMessage logic inside chatChannelPaneSubscriptionsManager
|
// TODO (martin) Remove this when the handleSentMessage logic inside chatChannelPaneSubscriptionsManager
|
||||||
// is moved over from this file completely.
|
// is moved over from this file completely.
|
||||||
import { handleStagedMessage } from "discourse/plugins/chat/discourse/services/chat-pane-base-subscriptions-manager";
|
import { handleStagedMessage } from "discourse/plugins/chat/discourse/services/chat-pane-base-subscriptions-manager";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { cancel, later, next, schedule } from "@ember/runloop";
|
import { cancel, later, next, schedule } from "@ember/runloop";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
@ -736,33 +735,6 @@ export default class ChatLivePane extends Component {
|
|||||||
|
|
||||||
resetIdle();
|
resetIdle();
|
||||||
|
|
||||||
// TODO: all send message logic is due for massive refactoring
|
|
||||||
// This is all the possible case Im currently aware of
|
|
||||||
// - messaging to a public channel where you are not a member yet (preview = true)
|
|
||||||
// - messaging to an existing direct channel you were not tracking yet through dm creator (channel draft)
|
|
||||||
// - messaging to a new direct channel through DM creator (channel draft)
|
|
||||||
// - message to a direct channel you were tracking (preview = false, not draft)
|
|
||||||
// - message to a public channel you were tracking (preview = false, not draft)
|
|
||||||
// - message to a channel when we haven't loaded all future messages yet.
|
|
||||||
if (!this.args.channel.isFollowing || this.args.channel.isDraft) {
|
|
||||||
const data = {
|
|
||||||
message: message.message,
|
|
||||||
upload_ids: message.uploads.map((upload) => upload.id),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resetComposerMessage();
|
|
||||||
|
|
||||||
return this._upsertChannelWithMessage(this.args.channel, data).finally(
|
|
||||||
() => {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pane.sending = false;
|
|
||||||
this.scrollToLatestMessage();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.args.channel.stageMessage(message);
|
await this.args.channel.stageMessage(message);
|
||||||
this.resetComposerMessage();
|
this.resetComposerMessage();
|
||||||
|
|
||||||
@ -790,26 +762,6 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _upsertChannelWithMessage(channel, data) {
|
|
||||||
let promise = Promise.resolve(channel);
|
|
||||||
|
|
||||||
if (channel.isDirectMessageChannel || channel.isDraft) {
|
|
||||||
promise = this.chat.upsertDmChannelForUsernames(
|
|
||||||
channel.chatable.users.mapBy("username")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then((c) =>
|
|
||||||
ajax(`/chat/${c.id}.json`, {
|
|
||||||
type: "POST",
|
|
||||||
data,
|
|
||||||
}).then(() => {
|
|
||||||
this.pane.sending = false;
|
|
||||||
this.router.transitionTo("chat.channel", "-", c.id);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSendError(id, error) {
|
_onSendError(id, error) {
|
||||||
const stagedMessage = this.args.channel.findStagedMessage(id);
|
const stagedMessage = this.args.channel.findStagedMessage(id);
|
||||||
if (stagedMessage) {
|
if (stagedMessage) {
|
||||||
@ -977,14 +929,9 @@ export default class ChatLivePane extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.args.channel.isDraft) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.composer.focus({ addText: event.key });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
this.composer.focus({ addText: event.key });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
{{on "focusin" (fn this.computeIsFocused true)}}
|
{{on "focusin" (fn this.computeIsFocused true)}}
|
||||||
{{on "focusout" (fn this.computeIsFocused false)}}
|
{{on "focusout" (fn this.computeIsFocused false)}}
|
||||||
{{did-insert this.setupAutocomplete}}
|
{{did-insert this.setupAutocomplete}}
|
||||||
|
{{did-insert this.composer.focus}}
|
||||||
data-chat-composer-context={{this.context}}
|
data-chat-composer-context={{this.context}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +45,7 @@ export default class ChatComposer extends Component {
|
|||||||
@tracked presenceChannelName;
|
@tracked presenceChannelName;
|
||||||
|
|
||||||
get shouldRenderReplyingIndicator() {
|
get shouldRenderReplyingIndicator() {
|
||||||
return !this.args.channel?.isDraft;
|
return this.args.channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get shouldRenderMessageDetails() {
|
get shouldRenderMessageDetails() {
|
||||||
@ -89,7 +89,7 @@ export default class ChatComposer extends Component {
|
|||||||
setupTextareaInteractor(textarea) {
|
setupTextareaInteractor(textarea) {
|
||||||
this.composer.textarea = new TextareaInteractor(getOwner(this), textarea);
|
this.composer.textarea = new TextareaInteractor(getOwner(this), textarea);
|
||||||
|
|
||||||
if (this.site.desktopView) {
|
if (this.site.desktopView && this.args.autofocus) {
|
||||||
this.composer.focus({ ensureAtEnd: true, refreshHeight: true });
|
this.composer.focus({ ensureAtEnd: true, refreshHeight: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,10 +250,6 @@ export default class ChatComposer extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.args.channel.isDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chatComposerPresenceManager.notifyState(
|
this.chatComposerPresenceManager.notifyState(
|
||||||
this.presenceChannelName,
|
this.presenceChannelName,
|
||||||
!this.currentMessage.editing && this.hasContent
|
!this.currentMessage.editing && this.hasContent
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
<div class="chat-draft">
|
|
||||||
{{#if this.site.mobileView}}
|
|
||||||
<header
|
|
||||||
class="chat-draft-header"
|
|
||||||
{{did-insert this.setChatDraftHeaderHeight}}
|
|
||||||
{{will-destroy this.unsetChatDraftHeaderHeight}}
|
|
||||||
>
|
|
||||||
<FlatButton
|
|
||||||
@class="chat-draft-header__btn btn"
|
|
||||||
@icon="chevron-left"
|
|
||||||
@title="chat.draft_channel_screen.cancel"
|
|
||||||
@action={{action "onCancelChatDraft"}}
|
|
||||||
/>
|
|
||||||
<h2 class="chat-draft-header__title">
|
|
||||||
{{d-icon "d-chat"}}
|
|
||||||
{{i18n "chat.draft_channel_screen.header"}}
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<DirectMessageCreator
|
|
||||||
@onChangeSelectedUsers={{action "onChangeSelectedUsers"}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if this.previewedChannel}}
|
|
||||||
<ChatChannel @channel={{this.previewedChannel}} @includeHeader={{false}} />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
@ -1,64 +0,0 @@
|
|||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import Component from "@ember/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
|
||||||
|
|
||||||
export default class ChatDraftChannelScreen extends Component {
|
|
||||||
@service chat;
|
|
||||||
@service router;
|
|
||||||
tagName = "";
|
|
||||||
|
|
||||||
@action
|
|
||||||
onCancelChatDraft() {
|
|
||||||
return this.router.transitionTo("chat.index");
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setChatDraftHeaderHeight(element) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--chat-draft-header-height",
|
|
||||||
`${element.clientHeight}px`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
unsetChatDraftHeaderHeight() {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--chat-draft-header-height",
|
|
||||||
"0px"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onChangeSelectedUsers(users) {
|
|
||||||
this._fetchPreviewedChannel(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onSwitchFromDraftChannel(channel) {
|
|
||||||
channel.isDraft = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_fetchPreviewedChannel(users) {
|
|
||||||
this.set("previewedChannel", null);
|
|
||||||
|
|
||||||
return this.chat
|
|
||||||
.getDmChannelForUsernames(users.mapBy("username"))
|
|
||||||
.then((response) => {
|
|
||||||
const channel = ChatChannel.create(response.channel);
|
|
||||||
channel.isDraft = true;
|
|
||||||
this.set("previewedChannel", channel);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (error?.jqXHR?.status === 404) {
|
|
||||||
this.set(
|
|
||||||
"previewedChannel",
|
|
||||||
ChatChannel.createDirectMessageChannelDraft({
|
|
||||||
users: cloneJSON(users),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
<ChatDrawer::Header @toggleExpand={{@drawerActions.toggleExpand}}>
|
|
||||||
<ChatDrawer::Header::LeftActions />
|
|
||||||
<ChatDrawer::Header::Title @title="chat.direct_message_creator.title" />
|
|
||||||
<ChatDrawer::Header::RightActions @drawerActions={{@drawerActions}} />
|
|
||||||
</ChatDrawer::Header>
|
|
||||||
|
|
||||||
{{#if this.chatStateManager.isDrawerExpanded}}
|
|
||||||
<div class="chat-drawer-content">
|
|
||||||
<ChatDraftChannelScreen />
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
@ -1,6 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
|
|
||||||
export default class ChatDrawerDraftChannel extends Component {
|
|
||||||
@service chatStateManager;
|
|
||||||
}
|
|
@ -1,8 +1,4 @@
|
|||||||
{{#if
|
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
|
||||||
(and
|
|
||||||
this.chatStateManager.isFullPageActive this.displayed (not @channel.isDraft)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
<div
|
<div
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-full-page-header"
|
"chat-full-page-header"
|
||||||
|
@ -9,11 +9,10 @@ export default class ChatRetentionReminder extends Component {
|
|||||||
|
|
||||||
get show() {
|
get show() {
|
||||||
return (
|
return (
|
||||||
!this.args.channel?.isDraft &&
|
(this.args.channel?.isDirectMessageChannel &&
|
||||||
((this.args.channel?.isDirectMessageChannel &&
|
|
||||||
this.currentUser?.get("needs_dm_retention_reminder")) ||
|
this.currentUser?.get("needs_dm_retention_reminder")) ||
|
||||||
(this.args.channel?.isCategoryChannel &&
|
(this.args.channel?.isCategoryChannel &&
|
||||||
this.currentUser?.get("needs_channel_retention_reminder")))
|
this.currentUser?.get("needs_channel_retention_reminder"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
<div
|
<div
|
||||||
class="chat-user-avatar
|
class="chat-user-avatar {{if (and this.isOnline @showPresence) 'is-online'}}"
|
||||||
{{if (and this.isOnline this.showPresence) 'is-online'}}"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
class="chat-user-avatar-container clickable"
|
class="chat-user-avatar-container clickable"
|
||||||
data-user-card={{this.user.username}}
|
data-user-card={{@user.username}}
|
||||||
>
|
>
|
||||||
{{avatar this.user imageSize=this.avatarSize}}
|
{{avatar @user imageSize=this.avatarSize}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,23 +1,19 @@
|
|||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class ChatUserAvatar extends Component {
|
export default class ChatUserAvatar extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
tagName = "";
|
|
||||||
|
|
||||||
user = null;
|
get avatarSize() {
|
||||||
|
return this.args.avatarSize || "tiny";
|
||||||
|
}
|
||||||
|
|
||||||
avatarSize = "tiny";
|
|
||||||
showPresence = true;
|
|
||||||
|
|
||||||
@computed("chat.presenceChannel.users.[]", "user.{id,username}")
|
|
||||||
get isOnline() {
|
get isOnline() {
|
||||||
const users = this.chat.presenceChannel?.users;
|
const users = (this.args.chat || this.chat).presenceChannel?.users;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!!users?.findBy("id", this.user?.id) ||
|
!!users?.findBy("id", this.args.user?.id) ||
|
||||||
!!users?.findBy("username", this.user?.username)
|
!!users?.findBy("username", this.args.user?.username)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
<span class="chat-user-display-name">
|
<span class="chat-user-display-name">
|
||||||
{{#if this.shouldShowNameFirst}}
|
{{#if this.shouldShowNameFirst}}
|
||||||
<span class="chat-user-display-name__name">{{this.user.name}}</span>
|
<span class="chat-user-display-name__name -first">{{@user.name}}</span>
|
||||||
<span class="separator">—</span>
|
<span class="separator">—</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<span class="chat-user-display-name__username">
|
<span
|
||||||
|
class={{concat-class
|
||||||
|
"chat-user-display-name__username"
|
||||||
|
(unless this.shouldShowNameFirst "-first")
|
||||||
|
}}
|
||||||
|
>
|
||||||
{{this.formattedUsername}}
|
{{this.formattedUsername}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{{#if this.shouldShowNameLast}}
|
{{#if this.shouldShowNameLast}}
|
||||||
<span class="separator">—</span>
|
<span class="separator">—</span>
|
||||||
<span class="chat-user-display-name__name">{{this.user.name}}</span>
|
<span class="chat-user-display-name__name">{{@user.name}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
@ -1,32 +1,26 @@
|
|||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { formatUsername } from "discourse/lib/utilities";
|
import { formatUsername } from "discourse/lib/utilities";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class ChatUserDisplayName extends Component {
|
export default class ChatUserDisplayName extends Component {
|
||||||
tagName = "";
|
@service siteSettings;
|
||||||
user = null;
|
|
||||||
|
|
||||||
@computed
|
|
||||||
get shouldPrioritizeNameInUx() {
|
get shouldPrioritizeNameInUx() {
|
||||||
return !this.siteSettings.prioritize_username_in_ux;
|
return !this.siteSettings.prioritize_username_in_ux;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("user.name")
|
|
||||||
get hasValidName() {
|
get hasValidName() {
|
||||||
return this.user?.name && this.user?.name.trim().length > 0;
|
return this.args.user?.name && this.args.user.name.trim().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("user.username")
|
|
||||||
get formattedUsername() {
|
get formattedUsername() {
|
||||||
return formatUsername(this.user?.username);
|
return formatUsername(this.args.user?.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("shouldPrioritizeNameInUx", "hasValidName")
|
|
||||||
get shouldShowNameFirst() {
|
get shouldShowNameFirst() {
|
||||||
return this.shouldPrioritizeNameInUx && this.hasValidName;
|
return this.shouldPrioritizeNameInUx && this.hasValidName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed("shouldPrioritizeNameInUx", "hasValidName")
|
|
||||||
get shouldShowNameLast() {
|
get shouldShowNameLast() {
|
||||||
return !this.shouldPrioritizeNameInUx && this.hasValidName;
|
return !this.shouldPrioritizeNameInUx && this.hasValidName;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { inject as service } from "@ember/service";
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
|
|
||||||
export default class ChatComposerChannel extends ChatComposer {
|
export default class ChatComposerChannel extends ChatComposer {
|
||||||
@service("chat-channel-composer") composer;
|
@service("chat-channel-composer") composer;
|
||||||
@ -22,8 +21,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||||||
|
|
||||||
get disabled() {
|
get disabled() {
|
||||||
return (
|
return (
|
||||||
(this.args.channel.isDraft &&
|
|
||||||
isEmpty(this.args.channel?.chatable?.users)) ||
|
|
||||||
!this.chat.userCanInteractWithChat ||
|
!this.chat.userCanInteractWithChat ||
|
||||||
!this.args.channel.canModifyMessages(this.currentUser)
|
!this.args.channel.canModifyMessages(this.currentUser)
|
||||||
);
|
);
|
||||||
@ -36,10 +33,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
persistDraft() {
|
persistDraft() {
|
||||||
if (this.args.channel?.isDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chatDraftsManager.add(this.currentMessage);
|
this.chatDraftsManager.add(this.currentMessage);
|
||||||
|
|
||||||
this._persistHandler = discourseDebounce(
|
this._persistHandler = discourseDebounce(
|
||||||
@ -75,18 +68,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.args.channel.isDraft) {
|
|
||||||
if (this.args.channel?.chatable?.users?.length) {
|
|
||||||
return I18n.t("chat.placeholder_start_conversation_users", {
|
|
||||||
commaSeparatedUsernames: this.args.channel.chatable.users
|
|
||||||
.mapBy("username")
|
|
||||||
.join(I18n.t("word_connector.comma")),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return I18n.t("chat.placeholder_start_conversation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.chat.userCanInteractWithChat) {
|
if (!this.chat.userCanInteractWithChat) {
|
||||||
return I18n.t("chat.placeholder_silenced");
|
return I18n.t("chat.placeholder_silenced");
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
<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>
|
@ -0,0 +1,522 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { cached, tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
import { schedule } from "@ember/runloop";
|
||||||
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
|
import { getOwner, setOwner } from "@ember/application";
|
||||||
|
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import ChatChatable from "discourse/plugins/chat/discourse/models/chat-chatable";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (channel.chatable?.users?.length === 1) {
|
||||||
|
return ChatChatable.createUser(channel.chatable.users[0]);
|
||||||
|
}
|
||||||
|
const 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;
|
||||||
|
|
||||||
|
@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.hasSelectedUsers) {
|
||||||
|
return I18n.t("chat.new_message_modal.user_search_placeholder");
|
||||||
|
} else {
|
||||||
|
return I18n.t("chat.new_message_modal.default_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);
|
||||||
|
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) => {
|
||||||
|
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,20 @@
|
|||||||
|
<ChatChannelTitle @channel={{@content.model}} />
|
||||||
|
|
||||||
|
{{#if (gt @content.tracking.unreadCount 0)}}
|
||||||
|
<div
|
||||||
|
class={{concat-class
|
||||||
|
"unread-indicator"
|
||||||
|
(if
|
||||||
|
(or
|
||||||
|
@content.model.isDirectMessageChannel
|
||||||
|
(gt @content.model.tracking.mentionCount 0)
|
||||||
|
)
|
||||||
|
"-urgent"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.site.desktopView}}
|
||||||
|
<span class="action-indicator">{{this.openChannelLabel}}</span>
|
||||||
|
{{/if}}
|
@ -0,0 +1,12 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
export default class ChatMessageCreatorChannelRow extends Component {
|
||||||
|
@service site;
|
||||||
|
|
||||||
|
get openChannelLabel() {
|
||||||
|
return htmlSafe(I18n.t("chat.new_message_modal.open_channel"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<ChatUserAvatar @user={{@content.model}} @showPresence={{true}} />
|
||||||
|
<ChatUserDisplayName @user={{@content.model}} />
|
||||||
|
|
||||||
|
{{#if (gt @content.tracking.unreadCount 0)}}
|
||||||
|
<div class="unread-indicator"></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}}
|
@ -0,0 +1,17 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import I18n from "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"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
<ChatUserAvatar @user={{@selection.model}} @showPresence={{true}} />
|
||||||
|
|
||||||
|
<span class="chat-message-creator__selection-item__username">
|
||||||
|
{{@selection.model.username}}
|
||||||
|
</span>
|
@ -1,96 +0,0 @@
|
|||||||
{{#if this.chatProgressBarContainer}}
|
|
||||||
{{#in-element this.chatProgressBarContainer}}
|
|
||||||
<DProgressBar @key="dm-recipients-loader" @isLoading={{this.isFiltering}} />
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if (and this.channel.isDraft (not this.isLoading))}}
|
|
||||||
<div
|
|
||||||
class="direct-message-creator"
|
|
||||||
{{did-insert this.setDirectMessageCreatorHeight}}
|
|
||||||
{{will-destroy this.unsetDirectMessageCreatorHeight}}
|
|
||||||
{{did-update this.setDirectMessageCreatorHeight this.selectedUsers}}
|
|
||||||
{{did-update this.setDirectMessageCreatorHeight this.users}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="filter-area {{if this.isFilterFocused 'is-focused'}}"
|
|
||||||
role="button"
|
|
||||||
{{on "click" this.focusFilter}}
|
|
||||||
>
|
|
||||||
|
|
||||||
<span class="prefix">
|
|
||||||
{{i18n "chat.direct_message_creator.prefix"}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="recipients">
|
|
||||||
{{#each this.selectedUsers as |selectedUser|}}
|
|
||||||
<DButton
|
|
||||||
@class={{concat
|
|
||||||
"selected-user"
|
|
||||||
(if
|
|
||||||
(eq this.highlightedSelectedUser selectedUser) " is-highlighted"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
@action={{action "deselectUser" selectedUser}}
|
|
||||||
@translatedTitle={{i18n
|
|
||||||
"chat.direct_message_creator.selected_user_title"
|
|
||||||
username=selectedUser.username
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChatUserAvatar @user={{selectedUser}} />
|
|
||||||
<span class="username">{{selectedUser.username}}</span>
|
|
||||||
{{d-icon "times"}}
|
|
||||||
</DButton>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
class="filter-usernames"
|
|
||||||
@value={{this.term}}
|
|
||||||
autofocus="autofocus"
|
|
||||||
{{on "input" (action "onFilterInput" value="target.value")}}
|
|
||||||
{{on "focusin" (action (mut this.isFilterFocused) true)}}
|
|
||||||
{{on "focusout" (action "onFilterInputFocusOut")}}
|
|
||||||
{{on "keyup" (action "handleFilterKeyUp")}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.shouldRenderResults}}
|
|
||||||
{{#if this.users}}
|
|
||||||
<div class="results-container">
|
|
||||||
<ul class="results">
|
|
||||||
{{#each this.users as |user|}}
|
|
||||||
<li
|
|
||||||
class="user {{if (eq this.focusedUser user) 'is-focused'}}"
|
|
||||||
data-username={{user.username}}
|
|
||||||
role="button"
|
|
||||||
tabindex="-1"
|
|
||||||
{{on "click" (action "selectUser" user)}}
|
|
||||||
{{on "mouseenter" (action (mut this.focusedUser) user)}}
|
|
||||||
{{on "focus" (action (mut this.focusedUser) user)}}
|
|
||||||
{{on "keyup" (action "handleUserKeyUp" user)}}
|
|
||||||
>
|
|
||||||
<ChatUserAvatar @user={{user}} @avatarSize="medium" />
|
|
||||||
<UserInfo
|
|
||||||
@user={{user}}
|
|
||||||
@includeLink={{false}}
|
|
||||||
@includeAvatar={{false}}
|
|
||||||
@showStatus={{true}}
|
|
||||||
@showStatusDescription={{true}}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{#if this.term.length}}
|
|
||||||
<div class="no-results-container">
|
|
||||||
<p class="no-results">
|
|
||||||
{{i18n "chat.direct_message_creator.no_results"}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
@ -1,331 +0,0 @@
|
|||||||
import { caretPosition } from "discourse/lib/utilities";
|
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
import Component from "@ember/component";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
|
||||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { gt, not } from "@ember/object/computed";
|
|
||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: "",
|
|
||||||
users: null,
|
|
||||||
selectedUsers: null,
|
|
||||||
term: null,
|
|
||||||
isFiltering: false,
|
|
||||||
isFilterFocused: false,
|
|
||||||
highlightedSelectedUser: null,
|
|
||||||
focusedUser: null,
|
|
||||||
chat: service(),
|
|
||||||
router: service(),
|
|
||||||
chatStateManager: service(),
|
|
||||||
isLoading: false,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.set("users", []);
|
|
||||||
this.set("selectedUsers", []);
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.filterUsernames();
|
|
||||||
},
|
|
||||||
|
|
||||||
didReceiveAttrs() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.set("term", null);
|
|
||||||
|
|
||||||
this.focusFilter();
|
|
||||||
|
|
||||||
if (!this.hasSelection) {
|
|
||||||
this.filterUsernames();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hasSelection: gt("channel.chatable.users.length", 0),
|
|
||||||
|
|
||||||
@discourseComputed
|
|
||||||
chatProgressBarContainer() {
|
|
||||||
return document.querySelector("#chat-progress-bar-container");
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
filterUsernames(term = null) {
|
|
||||||
this.set("isFiltering", true);
|
|
||||||
|
|
||||||
this.chat
|
|
||||||
.searchPossibleDirectMessageUsers({
|
|
||||||
term,
|
|
||||||
limit: 6,
|
|
||||||
exclude: this.channel.chatable?.users?.mapBy("username") || [],
|
|
||||||
lastSeenUsers: isEmpty(term) ? true : false,
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r !== "__CANCELLED") {
|
|
||||||
this.set("users", r.users || []);
|
|
||||||
this.set("focusedUser", this.users.firstObject);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("isFiltering", false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldRenderResults: not("isFiltering"),
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectUser(user) {
|
|
||||||
this.selectedUsers.pushObject(user);
|
|
||||||
this.users.removeObject(user);
|
|
||||||
this.set("users", []);
|
|
||||||
this.set("focusedUser", null);
|
|
||||||
this.set("highlightedSelectedUser", null);
|
|
||||||
this.set("term", null);
|
|
||||||
this.focusFilter();
|
|
||||||
this.onChangeSelectedUsers?.(this.selectedUsers);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
deselectUser(user) {
|
|
||||||
this.users.removeObject(user);
|
|
||||||
this.selectedUsers.removeObject(user);
|
|
||||||
this.set("focusedUser", this.users.firstObject);
|
|
||||||
this.set("highlightedSelectedUser", null);
|
|
||||||
this.set("term", null);
|
|
||||||
|
|
||||||
if (isEmpty(this.selectedUsers)) {
|
|
||||||
this.filterUsernames();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.focusFilter();
|
|
||||||
this.onChangeSelectedUsers?.(this.selectedUsers);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
focusFilter() {
|
|
||||||
this.set("isFilterFocused", true);
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
document.querySelector(".filter-usernames")?.focus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
setDirectMessageCreatorHeight(element) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--chat-direct-message-creator-height",
|
|
||||||
`${element.clientHeight}px`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
unsetDirectMessageCreatorHeight() {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--chat-direct-message-creator-height",
|
|
||||||
"0px"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
onFilterInput(term) {
|
|
||||||
this.set("term", term);
|
|
||||||
this.set("users", []);
|
|
||||||
|
|
||||||
if (!term?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("isFiltering", true);
|
|
||||||
|
|
||||||
discourseDebounce(this, this.filterUsernames, term, INPUT_DELAY);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleUserKeyUp(user, event) {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
this.selectUser(user);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
onFilterInputFocusOut() {
|
|
||||||
this.set("isFilterFocused", false);
|
|
||||||
this.set("highlightedSelectedUser", null);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
leaveChannel() {
|
|
||||||
this.router.transitionTo("chat.index");
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleFilterKeyUp(event) {
|
|
||||||
if (event.key === "Tab") {
|
|
||||||
const enabledComposer = document.querySelector(".chat-composer__input");
|
|
||||||
if (enabledComposer && !enabledComposer.disabled) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
enabledComposer.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(event.key === "Enter" || event.key === "Backspace") &&
|
|
||||||
this.highlightedSelectedUser
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.deselectUser(this.highlightedSelectedUser);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Backspace" && isEmpty(this.term) && this.hasSelection) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.deselectUser(this.channel.chatable.users.lastObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Enter" && this.focusedUser) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.selectUser(this.focusedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
||||||
this._handleVerticalArrowKeys(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Escape" && this.highlightedSelectedUser) {
|
|
||||||
this.set("highlightedSelectedUser", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
||||||
this._handleHorizontalArrowKeys(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_firstSelectWithArrows(event) {
|
|
||||||
if (event.key === "ArrowRight") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
const position = caretPosition(
|
|
||||||
document.querySelector(".filter-usernames")
|
|
||||||
);
|
|
||||||
if (position > 0) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.set(
|
|
||||||
"highlightedSelectedUser",
|
|
||||||
this.channel.chatable.users.lastObject
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_changeSelectionWithArrows(event) {
|
|
||||||
if (event.key === "ArrowRight") {
|
|
||||||
if (
|
|
||||||
this.highlightedSelectedUser === this.channel.chatable.users.lastObject
|
|
||||||
) {
|
|
||||||
this.set("highlightedSelectedUser", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.channel.chatable.users.length === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._highlightNextSelectedUser(event.key === "ArrowLeft" ? -1 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
if (this.channel.chatable.users.length === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._highlightNextSelectedUser(event.key === "ArrowLeft" ? -1 : 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_highlightNextSelectedUser(modifier) {
|
|
||||||
const newIndex =
|
|
||||||
this.channel.chatable.users.indexOf(this.highlightedSelectedUser) +
|
|
||||||
modifier;
|
|
||||||
|
|
||||||
if (this.channel.chatable.users.objectAt(newIndex)) {
|
|
||||||
this.set(
|
|
||||||
"highlightedSelectedUser",
|
|
||||||
this.channel.chatable.users.objectAt(newIndex)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.set(
|
|
||||||
"highlightedSelectedUser",
|
|
||||||
event.key === "ArrowLeft"
|
|
||||||
? this.channel.chatable.users.lastObject
|
|
||||||
: this.channel.chatable.users.firstObject
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleHorizontalArrowKeys(event) {
|
|
||||||
const position = caretPosition(document.querySelector(".filter-usernames"));
|
|
||||||
if (position > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.highlightedSelectedUser) {
|
|
||||||
this._firstSelectWithArrows(event);
|
|
||||||
} else {
|
|
||||||
this._changeSelectionWithArrows(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleVerticalArrowKeys(event) {
|
|
||||||
if (isEmpty(this.users)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (!this.focusedUser) {
|
|
||||||
this.set("focusedUser", this.users.firstObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifier = event.key === "ArrowUp" ? -1 : 1;
|
|
||||||
const newIndex = this.users.indexOf(this.focusedUser) + modifier;
|
|
||||||
|
|
||||||
if (this.users.objectAt(newIndex)) {
|
|
||||||
this.set("focusedUser", this.users.objectAt(newIndex));
|
|
||||||
} else {
|
|
||||||
this.set(
|
|
||||||
"focusedUser",
|
|
||||||
event.key === "ArrowUp" ? this.users.lastObject : this.users.firstObject
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
@ -0,0 +1,7 @@
|
|||||||
|
<DModal
|
||||||
|
@closeModal={{@closeModal}}
|
||||||
|
class="chat-new-message-modal"
|
||||||
|
@title="chat.new_message_modal.title"
|
||||||
|
>
|
||||||
|
<Chat::MessageCreator @onClose={{route-action "closeModal"}} />
|
||||||
|
</DModal>
|
@ -0,0 +1,6 @@
|
|||||||
|
import Component from "@ember/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatNewMessageModal extends Component {
|
||||||
|
@service chat;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||||
|
|
||||||
const APPLE =
|
const APPLE =
|
||||||
navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
|
navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
|
||||||
@ -16,6 +16,7 @@ export default {
|
|||||||
|
|
||||||
const router = container.lookup("service:router");
|
const router = container.lookup("service:router");
|
||||||
const appEvents = container.lookup("service:app-events");
|
const appEvents = container.lookup("service:app-events");
|
||||||
|
const modal = container.lookup("service:modal");
|
||||||
const chatStateManager = container.lookup("service:chat-state-manager");
|
const chatStateManager = container.lookup("service:chat-state-manager");
|
||||||
const chatThreadPane = container.lookup("service:chat-thread-pane");
|
const chatThreadPane = container.lookup("service:chat-thread-pane");
|
||||||
const chatThreadListPane = container.lookup(
|
const chatThreadListPane = container.lookup(
|
||||||
@ -27,11 +28,7 @@ export default {
|
|||||||
const openChannelSelector = (e) => {
|
const openChannelSelector = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (document.getElementById("chat-channel-selector-modal-inner")) {
|
modal.show(ChatNewMessageModal);
|
||||||
appEvents.trigger("chat-channel-selector-modal:close");
|
|
||||||
} else {
|
|
||||||
showModal("chat-channel-selector-modal");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveUpShortcut = (e) => {
|
const handleMoveUpShortcut = (e) => {
|
||||||
|
@ -9,6 +9,7 @@ import { emojiUnescape } from "discourse/lib/text";
|
|||||||
import { decorateUsername } from "discourse/helpers/decorate-username-selector";
|
import { decorateUsername } from "discourse/helpers/decorate-username-selector";
|
||||||
import { until } from "discourse/lib/formatter";
|
import { until } from "discourse/lib/formatter";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "chat-sidebar",
|
name: "chat-sidebar",
|
||||||
@ -329,6 +330,7 @@ export default {
|
|||||||
|
|
||||||
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
|
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
|
||||||
@service site;
|
@service site;
|
||||||
|
@service modal;
|
||||||
@service router;
|
@service router;
|
||||||
@tracked userCanDirectMessage =
|
@tracked userCanDirectMessage =
|
||||||
this.chatService.userCanDirectMessage;
|
this.chatService.userCanDirectMessage;
|
||||||
@ -377,7 +379,7 @@ export default {
|
|||||||
id: "startDm",
|
id: "startDm",
|
||||||
title: I18n.t("chat.direct_messages.new"),
|
title: I18n.t("chat.direct_messages.new"),
|
||||||
action: () => {
|
action: () => {
|
||||||
this.router.transitionTo("chat.draft-channel");
|
this.modal.show(ChatNewMessageModal);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -56,19 +56,7 @@ export default class ChatChannel {
|
|||||||
return new ChatChannel(args);
|
return new ChatChannel(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createDirectMessageChannelDraft(args = {}) {
|
|
||||||
const channel = ChatChannel.create({
|
|
||||||
chatable_type: CHATABLE_TYPES.directMessageChannel,
|
|
||||||
chatable: {
|
|
||||||
users: args.users || [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
channel.isDraft = true;
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@tracked currentUserMembership = null;
|
@tracked currentUserMembership = null;
|
||||||
@tracked isDraft = false;
|
|
||||||
@tracked title;
|
@tracked title;
|
||||||
@tracked slug;
|
@tracked slug;
|
||||||
@tracked description;
|
@tracked description;
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
|
import User from "discourse/models/user";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatChatable {
|
||||||
|
static create(args = {}) {
|
||||||
|
return new ChatChatable(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static createUser(model) {
|
||||||
|
return new ChatChatable({
|
||||||
|
type: "user",
|
||||||
|
model,
|
||||||
|
identifier: `u-${model.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static createChannel(model) {
|
||||||
|
return new ChatChatable({
|
||||||
|
type: "channel",
|
||||||
|
model,
|
||||||
|
identifier: `c-${model.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@service chatChannelsManager;
|
||||||
|
|
||||||
|
@tracked identifier;
|
||||||
|
@tracked type;
|
||||||
|
@tracked model;
|
||||||
|
@tracked enabled = true;
|
||||||
|
@tracked tracking;
|
||||||
|
|
||||||
|
constructor(args = {}) {
|
||||||
|
this.identifier = args.identifier;
|
||||||
|
this.type = args.type;
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case "channel":
|
||||||
|
if (args.model.chatable?.users?.length === 1) {
|
||||||
|
this.enabled = args.model.chatable?.users[0].has_chat_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.model instanceof ChatChannel) {
|
||||||
|
this.model = args.model;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model = ChatChannel.create(args.model);
|
||||||
|
break;
|
||||||
|
case "user":
|
||||||
|
this.enabled = args.model.has_chat_enabled;
|
||||||
|
|
||||||
|
if (args.model instanceof User) {
|
||||||
|
this.model = args.model;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model = User.create(args.model);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUser() {
|
||||||
|
return this.type === "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSingleUserChannel() {
|
||||||
|
return this.type === "channel" && this.model?.chatable?.users?.length === 1;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ export default class UserChatChannelMembership {
|
|||||||
@tracked mobileNotificationLevel = null;
|
@tracked mobileNotificationLevel = null;
|
||||||
@tracked lastReadMessageId = null;
|
@tracked lastReadMessageId = null;
|
||||||
@tracked user = null;
|
@tracked user = null;
|
||||||
|
@tracked lastViewedAt = null;
|
||||||
|
|
||||||
constructor(args = {}) {
|
constructor(args = {}) {
|
||||||
this.following = args.following;
|
this.following = args.following;
|
||||||
@ -19,6 +20,7 @@ export default class UserChatChannelMembership {
|
|||||||
this.desktopNotificationLevel = args.desktop_notification_level;
|
this.desktopNotificationLevel = args.desktop_notification_level;
|
||||||
this.mobileNotificationLevel = args.mobile_notification_level;
|
this.mobileNotificationLevel = args.mobile_notification_level;
|
||||||
this.lastReadMessageId = args.last_read_message_id;
|
this.lastReadMessageId = args.last_read_message_id;
|
||||||
|
this.lastViewedAt = args.last_viewed_at;
|
||||||
this.user = this.#initUserModel(args.user);
|
this.user = this.#initUserModel(args.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ export default class ChatRoute extends DiscourseRoute {
|
|||||||
"chat.channel-legacy",
|
"chat.channel-legacy",
|
||||||
"chat",
|
"chat",
|
||||||
"chat.index",
|
"chat.index",
|
||||||
"chat.draft-channel",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -408,6 +408,17 @@ export default class ChatApi extends Service {
|
|||||||
return this.#putRequest(`/channels/read`);
|
return this.#putRequest(`/channels/read`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all possible chatables.
|
||||||
|
*
|
||||||
|
* @param {term} string - The term to search for. # prefix will scope to channels, @ to users.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
chatables(args = {}) {
|
||||||
|
return this.#getRequest("/chatables", args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks messages for a single user chat channel membership as read. If no
|
* Marks messages for a single user chat channel membership as read. If no
|
||||||
* message ID is provided, then the latest message for the channel is fetched
|
* message ID is provided, then the latest message for the channel is fetched
|
||||||
|
@ -101,6 +101,16 @@ export default class ChatChannelsManager extends Service {
|
|||||||
delete this._cached[model.id];
|
delete this._cached[model.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get allChannels() {
|
||||||
|
return [...this.publicMessageChannels, ...this.directMessageChannels].sort(
|
||||||
|
(a, b) => {
|
||||||
|
return b?.currentUserMembership?.lastViewedAt?.localeCompare?.(
|
||||||
|
a?.currentUserMembership?.lastViewedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get publicMessageChannels() {
|
get publicMessageChannels() {
|
||||||
return this.channels
|
return this.channels
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import Service, { inject as service } from "@ember/service";
|
import Service, { inject as service } from "@ember/service";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import ChatDrawerDraftChannel from "discourse/plugins/chat/discourse/components/chat-drawer/draft-channel";
|
|
||||||
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
|
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
|
||||||
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
|
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
|
||||||
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
|
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
|
||||||
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
||||||
|
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
"chat.draft-channel": { name: ChatDrawerDraftChannel },
|
|
||||||
"chat.channel": { name: ChatDrawerChannel },
|
"chat.channel": { name: ChatDrawerChannel },
|
||||||
"chat.channel.thread": {
|
"chat.channel.thread": {
|
||||||
name: ChatDrawerThread,
|
name: ChatDrawerThread,
|
||||||
|
@ -354,6 +354,7 @@ export default class ChatSubscriptionsManager extends Service {
|
|||||||
this.chatChannelsManager.find(data.channel.id).then((channel) => {
|
this.chatChannelsManager.find(data.channel.id).then((channel) => {
|
||||||
// we need to refresh here to have correct last message ids
|
// we need to refresh here to have correct last message ids
|
||||||
channel.meta = data.channel.meta;
|
channel.meta = data.channel.meta;
|
||||||
|
channel.updateMembership(data.channel.current_user_membership);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
channel.isDirectMessageChannel &&
|
channel.isDirectMessageChannel &&
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import userSearch from "discourse/lib/user-search";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import Service, { inject as service } from "@ember/service";
|
import Service, { inject as service } from "@ember/service";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
@ -282,11 +281,6 @@ export default class Chat extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPossibleDirectMessageUsers(options) {
|
|
||||||
// TODO: implement a chat specific user search function
|
|
||||||
return userSearch(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdealFirstChannelId() {
|
getIdealFirstChannelId() {
|
||||||
// When user opens chat we need to give them the 'best' channel when they enter.
|
// When user opens chat we need to give them the 'best' channel when they enter.
|
||||||
//
|
//
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
:root {
|
|
||||||
--chat-channel-selector-input-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-channel-selector-modal-modal.modal.in {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat-channel-selector-modal-inner {
|
|
||||||
width: 500px;
|
|
||||||
height: 350px;
|
|
||||||
|
|
||||||
.chat-channel-selector-input-container {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--primary-high);
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat-channel-selector-input {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--chat-channel-selector-input-height);
|
|
||||||
padding-left: 30px;
|
|
||||||
margin: 0 0 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.channels {
|
|
||||||
height: calc(100% - var(--chat-channel-selector-input-height));
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.no-channels-notice {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-channel-selection-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 2.5em;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
|
|
||||||
&.focused {
|
|
||||||
background: var(--primary-low);
|
|
||||||
}
|
|
||||||
.username {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
.chat-channel-title {
|
|
||||||
color: var(--primary-high);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-channel-unread-indicator {
|
|
||||||
border: none;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
.full-page-chat.teams-sidebar-on {
|
|
||||||
.chat-draft {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-draft {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75em 10px;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
font-size: var(--font-0);
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--primary);
|
|
||||||
@include ellipsis;
|
|
||||||
|
|
||||||
.d-icon {
|
|
||||||
height: 1.5em;
|
|
||||||
width: 1.5em;
|
|
||||||
color: var(--primary-medium);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-composer__wrapper {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,26 +2,18 @@
|
|||||||
// desktop and mobile
|
// desktop and mobile
|
||||||
height: calc(
|
height: calc(
|
||||||
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
||||||
var(--chat-draft-header-height, 0px) -
|
var(--composer-height, 0px)
|
||||||
var(--chat-direct-message-creator-height, 0px) -
|
|
||||||
var(--composer-height, 0px) - $inset
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// mobile with keyboard opened
|
// mobile with keyboard opened
|
||||||
.keyboard-visible & {
|
.keyboard-visible & {
|
||||||
height: calc(
|
height: calc(var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px));
|
||||||
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
|
||||||
var(--chat-draft-header-height, 0px) -
|
|
||||||
var(--chat-direct-message-creator-height, 0px)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ipad
|
// ipad
|
||||||
.footer-nav-ipad & {
|
.footer-nav-ipad & {
|
||||||
height: calc(
|
height: calc(
|
||||||
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
||||||
var(--chat-draft-header-height, 0px) -
|
|
||||||
var(--chat-direct-message-creator-height, 0px) -
|
|
||||||
var(--composer-height, 0px)
|
var(--composer-height, 0px)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.btn-floating.open-draft-channel-page-btn {
|
.btn-floating.open-new-message-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: var(--tertiary);
|
background: var(--tertiary);
|
||||||
bottom: 2rem;
|
bottom: 2rem;
|
||||||
|
305
plugins/chat/assets/stylesheets/common/chat-message-creator.scss
Normal file
305
plugins/chat/assets/stylesheets/common/chat-message-creator.scss
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
.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: 5px;
|
||||||
|
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 {
|
||||||
|
visibility: hidden;
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
color: var(--secondary-medium);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-active {
|
||||||
|
.action-indicator {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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__name,
|
||||||
|
.chat-user-display-name {
|
||||||
|
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 {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&-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: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selection {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
background: var(--secondary-very-high);
|
||||||
|
border-radius: 5px;
|
||||||
|
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-low);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--primary-very-low);
|
||||||
|
height: calc(var(--row-height) - 6);
|
||||||
|
padding-inline: 0.25rem;
|
||||||
|
margin: 3px;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
.chat-new-message-modal {
|
||||||
|
& + .modal-backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-inner-container {
|
||||||
|
width: var(--modal-max-width);
|
||||||
|
box-shadow: var(--shadow-dropdown);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device & {
|
||||||
|
.modal-inner-container {
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0 auto auto auto;
|
||||||
|
box-shadow: var(--shadow-modal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-mobile-device & {
|
||||||
|
.modal-inner-container {
|
||||||
|
margin: 10px auto auto auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
&-container {
|
&-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
15
plugins/chat/assets/stylesheets/common/chat-section.scss
Normal file
15
plugins/chat/assets/stylesheets/common/chat-section.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.chat-section {
|
||||||
|
border-bottom: 1px solid var(--primary-low);
|
||||||
|
padding: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
align-items: baseline;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,197 +0,0 @@
|
|||||||
.direct-message-creator {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.title-area {
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: var(--font-up-1);
|
|
||||||
line-height: var(--font-up-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-area {
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
cursor: text;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.is-focused {
|
|
||||||
background: var(--primary-very-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prefix {
|
|
||||||
line-height: 34px;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-user {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1px 0.25rem 0.25rem 1px;
|
|
||||||
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
|
|
||||||
background: var(--primary-very-low);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--primary-300);
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-highlighted {
|
|
||||||
border-color: var(--tertiary);
|
|
||||||
|
|
||||||
.d-icon {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
margin: 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: var(--primary-very-low);
|
|
||||||
color: var(--primary);
|
|
||||||
|
|
||||||
&:not(.is-highlighted) {
|
|
||||||
border-color: var(--tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-icon {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipients {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: -0.25rem;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& + .btn {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-usernames {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
min-width: 80px;
|
|
||||||
margin: 1px 0 0 0;
|
|
||||||
appearance: none;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
background: none;
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-container {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
z-index: z("dropdown");
|
|
||||||
background: var(--secondary);
|
|
||||||
|
|
||||||
.user {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
list-style: none;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: 0;
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
margin: 0.25rem;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-focused {
|
|
||||||
background: var(--tertiary-very-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
margin-left: 0.25em;
|
|
||||||
color: var(--primary-high);
|
|
||||||
font-size: var(--font-up-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .user {
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-status-message {
|
|
||||||
margin-left: 0.3em;
|
|
||||||
|
|
||||||
.emoji {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.25em;
|
|
||||||
&:last-child {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
background: var(--secondary);
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fetching-preview-message {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-existing-channel {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
@import "chat-unread-indicator";
|
@import "chat-unread-indicator";
|
||||||
@import "chat-height-mixin";
|
@import "chat-height-mixin";
|
||||||
|
@import "chat-thread-header-buttons";
|
||||||
@import "base-common";
|
@import "base-common";
|
||||||
@import "sidebar-extensions";
|
@import "sidebar-extensions";
|
||||||
@import "chat-browse";
|
@import "chat-browse";
|
||||||
@ -7,7 +8,6 @@
|
|||||||
@import "chat-channel-card";
|
@import "chat-channel-card";
|
||||||
@import "chat-channel-info";
|
@import "chat-channel-info";
|
||||||
@import "chat-channel-preview-card";
|
@import "chat-channel-preview-card";
|
||||||
@import "chat-channel-selector-modal";
|
|
||||||
@import "chat-channel-settings-saved-indicator";
|
@import "chat-channel-settings-saved-indicator";
|
||||||
@import "chat-channel-title";
|
@import "chat-channel-title";
|
||||||
@import "chat-composer-dropdown";
|
@import "chat-composer-dropdown";
|
||||||
@ -15,7 +15,6 @@
|
|||||||
@import "chat-composer-uploads";
|
@import "chat-composer-uploads";
|
||||||
@import "chat-composer";
|
@import "chat-composer";
|
||||||
@import "chat-composer-button";
|
@import "chat-composer-button";
|
||||||
@import "chat-draft-channel";
|
|
||||||
@import "chat-drawer";
|
@import "chat-drawer";
|
||||||
@import "chat-emoji-picker";
|
@import "chat-emoji-picker";
|
||||||
@import "chat-form";
|
@import "chat-form";
|
||||||
@ -45,14 +44,12 @@
|
|||||||
@import "create-channel-modal";
|
@import "create-channel-modal";
|
||||||
@import "d-progress-bar";
|
@import "d-progress-bar";
|
||||||
@import "dc-filter-input";
|
@import "dc-filter-input";
|
||||||
@import "direct-message-creator";
|
|
||||||
@import "full-page-chat-header";
|
@import "full-page-chat-header";
|
||||||
@import "incoming-chat-webhooks";
|
@import "incoming-chat-webhooks";
|
||||||
@import "reviewable-chat-message";
|
@import "reviewable-chat-message";
|
||||||
@import "chat-thread-list-item";
|
@import "chat-thread-list-item";
|
||||||
@import "chat-threads-list";
|
@import "chat-threads-list";
|
||||||
@import "chat-composer-separator";
|
@import "chat-composer-separator";
|
||||||
@import "chat-thread-header-buttons";
|
|
||||||
@import "chat-thread-header";
|
@import "chat-thread-header";
|
||||||
@import "chat-thread-list-header";
|
@import "chat-thread-list-header";
|
||||||
@import "chat-thread-unread-indicator";
|
@import "chat-thread-unread-indicator";
|
||||||
@ -60,3 +57,5 @@
|
|||||||
@import "channel-summary-modal";
|
@import "channel-summary-modal";
|
||||||
@import "chat-message-mention-warning";
|
@import "chat-message-mention-warning";
|
||||||
@import "chat-message-error";
|
@import "chat-message-error";
|
||||||
|
@import "chat-new-message-modal";
|
||||||
|
@import "chat-message-creator";
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-draft-channel-page-btn,
|
.open-new-message-btn,
|
||||||
.open-browse-page-btn,
|
.open-browse-page-btn,
|
||||||
.edit-channels-dropdown .select-kit-header,
|
.edit-channels-dropdown .select-kit-header,
|
||||||
.chat-channel-leave-btn {
|
.chat-channel-leave-btn {
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
.chat-message-creator {
|
||||||
|
&__row {
|
||||||
|
&.-active {
|
||||||
|
background: var(--tertiary-very-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,6 @@
|
|||||||
@import "chat-index-full-page";
|
@import "chat-index-full-page";
|
||||||
@import "chat-message-actions";
|
@import "chat-message-actions";
|
||||||
@import "chat-message";
|
@import "chat-message";
|
||||||
|
@import "chat-message-creator";
|
||||||
@import "chat-message-thread-indicator";
|
@import "chat-message-thread-indicator";
|
||||||
@import "sidebar-extensions";
|
@import "sidebar-extensions";
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
.chat-message-creator {
|
||||||
|
&__open-dm-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,4 @@
|
|||||||
@import "chat-threads-list";
|
@import "chat-threads-list";
|
||||||
@import "chat-thread-settings-modal";
|
@import "chat-thread-settings-modal";
|
||||||
@import "chat-message-thread-indicator";
|
@import "chat-message-thread-indicator";
|
||||||
|
@import "chat-message-creator";
|
||||||
|
@ -324,6 +324,16 @@ en:
|
|||||||
members: Members
|
members: Members
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
|
||||||
|
new_message_modal:
|
||||||
|
title: Send message
|
||||||
|
add_user_long: <kbd>shift + click</kbd> or <kbd>shift + enter</kbd><span>Add @%{username}</span>
|
||||||
|
add_user_short: <span>Add user</span>
|
||||||
|
open_channel: <span>Open channel</span>
|
||||||
|
default_search_placeholder: "#a-channel, @somebody or anything"
|
||||||
|
user_search_placeholder: "...add more users"
|
||||||
|
disabled_user: "has disabled chat"
|
||||||
|
no_items: "No items"
|
||||||
|
|
||||||
channel_edit_name_slug_modal:
|
channel_edit_name_slug_modal:
|
||||||
title: Edit channel
|
title: Edit channel
|
||||||
input_placeholder: Add a name
|
input_placeholder: Add a name
|
||||||
@ -342,10 +352,6 @@ en:
|
|||||||
no_results: No results
|
no_results: No results
|
||||||
selected_user_title: "Deselect %{username}"
|
selected_user_title: "Deselect %{username}"
|
||||||
|
|
||||||
channel_selector:
|
|
||||||
title: "Jump to channel"
|
|
||||||
no_channels: "No channels match your search"
|
|
||||||
|
|
||||||
channel:
|
channel:
|
||||||
no_memberships: This channel has no members
|
no_memberships: This channel has no members
|
||||||
no_memberships_found: No members found
|
no_memberships_found: No members found
|
||||||
|
@ -51,7 +51,6 @@ Chat::Engine.routes.draw do
|
|||||||
|
|
||||||
# direct_messages_controller routes
|
# direct_messages_controller routes
|
||||||
get "/direct_messages" => "direct_messages#index"
|
get "/direct_messages" => "direct_messages#index"
|
||||||
post "/direct_messages/create" => "direct_messages#create"
|
|
||||||
|
|
||||||
# incoming_webhooks_controller routes
|
# incoming_webhooks_controller routes
|
||||||
post "/hooks/:key" => "incoming_webhooks#create_message"
|
post "/hooks/:key" => "incoming_webhooks#create_message"
|
||||||
@ -66,7 +65,6 @@ Chat::Engine.routes.draw do
|
|||||||
get "/browse/closed" => "chat#respond"
|
get "/browse/closed" => "chat#respond"
|
||||||
get "/browse/open" => "chat#respond"
|
get "/browse/open" => "chat#respond"
|
||||||
get "/browse/archived" => "chat#respond"
|
get "/browse/archived" => "chat#respond"
|
||||||
get "/draft-channel" => "chat#respond"
|
|
||||||
post "/enable" => "chat#enable_chat"
|
post "/enable" => "chat#enable_chat"
|
||||||
post "/disable" => "chat#disable_chat"
|
post "/disable" => "chat#disable_chat"
|
||||||
post "/dismiss-retention-reminder" => "chat#dismiss_retention_reminder"
|
post "/dismiss-retention-reminder" => "chat#dismiss_retention_reminder"
|
||||||
|
@ -6,10 +6,8 @@ module Chat
|
|||||||
|
|
||||||
def self.structured(guardian, include_threads: false)
|
def self.structured(guardian, include_threads: false)
|
||||||
memberships = Chat::ChannelMembershipManager.all_for_user(guardian.user)
|
memberships = Chat::ChannelMembershipManager.all_for_user(guardian.user)
|
||||||
public_channels =
|
public_channels = secured_public_channels(guardian, status: :open, following: true)
|
||||||
secured_public_channels(guardian, memberships, status: :open, following: true)
|
direct_message_channels = secured_direct_message_channels(guardian.user.id, guardian)
|
||||||
direct_message_channels =
|
|
||||||
secured_direct_message_channels(guardian.user.id, memberships, guardian)
|
|
||||||
{
|
{
|
||||||
public_channels: public_channels,
|
public_channels: public_channels,
|
||||||
direct_message_channels: direct_message_channels,
|
direct_message_channels: direct_message_channels,
|
||||||
@ -152,7 +150,7 @@ module Chat
|
|||||||
channels.limit(options[:limit]).offset(options[:offset])
|
channels.limit(options[:limit]).offset(options[:offset])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.secured_public_channels(guardian, memberships, options = { following: true })
|
def self.secured_public_channels(guardian, options = { following: true })
|
||||||
channels =
|
channels =
|
||||||
secured_public_channel_search(
|
secured_public_channel_search(
|
||||||
guardian,
|
guardian,
|
||||||
@ -174,19 +172,60 @@ module Chat
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.secured_direct_message_channels(user_id, memberships, guardian)
|
def self.secured_direct_message_channels(user_id, guardian)
|
||||||
query = Chat::Channel.includes(chatable: [{ direct_message_users: :user }, :users])
|
secured_direct_message_channels_search(user_id, guardian, following: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.secured_direct_message_channels_search(user_id, guardian, options = {})
|
||||||
|
query =
|
||||||
|
Chat::Channel.strict_loading.includes(
|
||||||
|
chatable: [{ direct_message_users: [user: :user_option] }, :users],
|
||||||
|
)
|
||||||
query = query.includes(chatable: [{ users: :user_status }]) if SiteSetting.enable_user_status
|
query = query.includes(chatable: [{ users: :user_status }]) if SiteSetting.enable_user_status
|
||||||
|
query = query.joins(:user_chat_channel_memberships)
|
||||||
|
|
||||||
channels =
|
scoped_channels =
|
||||||
|
Chat::Channel
|
||||||
|
.joins(
|
||||||
|
"INNER JOIN direct_message_channels ON direct_message_channels.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'DirectMessage'",
|
||||||
|
)
|
||||||
|
.joins(
|
||||||
|
"INNER JOIN direct_message_users ON direct_message_users.direct_message_channel_id = direct_message_channels.id",
|
||||||
|
)
|
||||||
|
.where("direct_message_users.user_id = :user_id", user_id: user_id)
|
||||||
|
|
||||||
|
if options[:user_ids]
|
||||||
|
scoped_channels =
|
||||||
|
scoped_channels.where(
|
||||||
|
"EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM direct_message_channels AS dmc
|
||||||
|
INNER JOIN direct_message_users AS dmu ON dmu.direct_message_channel_id = dmc.id
|
||||||
|
WHERE dmc.id = chat_channels.chatable_id AND dmu.user_id IN (:user_ids)
|
||||||
|
)",
|
||||||
|
user_ids: options[:user_ids],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.key?(:following)
|
||||||
|
query =
|
||||||
|
query.where(
|
||||||
|
user_chat_channel_memberships: {
|
||||||
|
user_id: user_id,
|
||||||
|
following: options[:following],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
query = query.where(user_chat_channel_memberships: { user_id: user_id })
|
||||||
|
end
|
||||||
|
|
||||||
|
query =
|
||||||
query
|
query
|
||||||
.joins(:user_chat_channel_memberships)
|
|
||||||
.where(user_chat_channel_memberships: { user_id: user_id, following: true })
|
|
||||||
.where(chatable_type: Chat::Channel.direct_channel_chatable_types)
|
.where(chatable_type: Chat::Channel.direct_channel_chatable_types)
|
||||||
.where("chat_channels.id IN (#{generate_allowed_channel_ids_sql(guardian)})")
|
.where(chat_channels: { id: scoped_channels })
|
||||||
.order(last_message_sent_at: :desc)
|
.order(last_message_sent_at: :desc)
|
||||||
.to_a
|
|
||||||
|
|
||||||
|
channels = query.to_a
|
||||||
preload_fields =
|
preload_fields =
|
||||||
User.allowed_user_custom_fields(guardian) +
|
User.allowed_user_custom_fields(guardian) +
|
||||||
UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" }
|
UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" }
|
||||||
|
@ -197,9 +197,7 @@ describe Chat::ChannelFetcher do
|
|||||||
|
|
||||||
it "does not include DM channels" do
|
it "does not include DM channels" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -207,7 +205,6 @@ describe Chat::ChannelFetcher do
|
|||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(
|
described_class.secured_public_channels(
|
||||||
guardian,
|
guardian,
|
||||||
memberships,
|
|
||||||
following: following,
|
following: following,
|
||||||
filter: "support",
|
filter: "support",
|
||||||
).map(&:id),
|
).map(&:id),
|
||||||
@ -218,7 +215,6 @@ describe Chat::ChannelFetcher do
|
|||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(
|
described_class.secured_public_channels(
|
||||||
guardian,
|
guardian,
|
||||||
memberships,
|
|
||||||
following: following,
|
following: following,
|
||||||
filter: "cool stuff",
|
filter: "cool stuff",
|
||||||
).map(&:id),
|
).map(&:id),
|
||||||
@ -227,33 +223,29 @@ describe Chat::ChannelFetcher do
|
|||||||
|
|
||||||
it "can filter by an array of slugs" do
|
it "can filter by an array of slugs" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, slugs: ["support"]).map(
|
described_class.secured_public_channels(guardian, slugs: ["support"]).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns nothing if the array of slugs is empty" do
|
it "returns nothing if the array of slugs is empty" do
|
||||||
expect(
|
expect(described_class.secured_public_channels(guardian, slugs: []).map(&:id)).to eq([])
|
||||||
described_class.secured_public_channels(guardian, memberships, slugs: []).map(&:id),
|
|
||||||
).to eq([])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can filter by status" do
|
it "can filter by status" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, status: "closed").map(&:id),
|
described_class.secured_public_channels(guardian, status: "closed").map(&:id),
|
||||||
).to match_array([])
|
).to match_array([])
|
||||||
|
|
||||||
category_channel.closed!(Discourse.system_user)
|
category_channel.closed!(Discourse.system_user)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, status: "closed").map(&:id),
|
described_class.secured_public_channels(guardian, status: "closed").map(&:id),
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can filter by following" do
|
it "can filter by following" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: true).map(&:id),
|
described_class.secured_public_channels(guardian, following: true).map(&:id),
|
||||||
).to be_blank
|
).to be_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -262,21 +254,19 @@ describe Chat::ChannelFetcher do
|
|||||||
another_channel = Fabricate(:category_channel)
|
another_channel = Fabricate(:category_channel)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: false).map(&:id),
|
described_class.secured_public_channels(guardian, following: false).map(&:id),
|
||||||
).to match_array([category_channel.id, another_channel.id])
|
).to match_array([category_channel.id, another_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "ensures offset is >= 0" do
|
it "ensures offset is >= 0" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, offset: -235).map(&:id),
|
described_class.secured_public_channels(guardian, offset: -235).map(&:id),
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "ensures limit is > 0" do
|
it "ensures limit is > 0" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, limit: -1, offset: 0).map(
|
described_class.secured_public_channels(guardian, limit: -1, offset: 0).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -284,17 +274,15 @@ describe Chat::ChannelFetcher do
|
|||||||
over_limit = Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS + 1
|
over_limit = Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS + 1
|
||||||
over_limit.times { Fabricate(:category_channel) }
|
over_limit.times { Fabricate(:category_channel) }
|
||||||
|
|
||||||
expect(
|
expect(described_class.secured_public_channels(guardian, limit: over_limit).length).to eq(
|
||||||
described_class.secured_public_channels(guardian, memberships, limit: over_limit).length,
|
Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS,
|
||||||
).to eq(Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not show the user category channels they cannot access" do
|
it "does not show the user category channels they cannot access" do
|
||||||
category_channel.update!(chatable: private_category)
|
category_channel.update!(chatable: private_category)
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to be_empty
|
).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -303,9 +291,7 @@ describe Chat::ChannelFetcher do
|
|||||||
|
|
||||||
it "only returns channels where the user is a member and is following the channel" do
|
it "only returns channels where the user is a member and is following the channel" do
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to be_empty
|
).to be_empty
|
||||||
|
|
||||||
Chat::UserChatChannelMembership.create!(
|
Chat::UserChatChannelMembership.create!(
|
||||||
@ -315,9 +301,7 @@ describe Chat::ChannelFetcher do
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||||
&:id
|
|
||||||
),
|
|
||||||
).to match_array([category_channel.id])
|
).to match_array([category_channel.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -369,9 +353,9 @@ describe Chat::ChannelFetcher do
|
|||||||
direct_message_channel1.update!(last_message_sent_at: 1.day.ago)
|
direct_message_channel1.update!(last_message_sent_at: 1.day.ago)
|
||||||
direct_message_channel2.update!(last_message_sent_at: 1.hour.ago)
|
direct_message_channel2.update!(last_message_sent_at: 1.hour.ago)
|
||||||
|
|
||||||
expect(
|
expect(described_class.secured_direct_message_channels(user1.id, guardian).map(&:id)).to eq(
|
||||||
described_class.secured_direct_message_channels(user1.id, memberships, guardian).map(&:id),
|
[direct_message_channel2.id, direct_message_channel1.id],
|
||||||
).to eq([direct_message_channel2.id, direct_message_channel1.id])
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not include direct message channels where the user is a member but not a direct_message_user" do
|
it "does not include direct message channels where the user is a member but not a direct_message_user" do
|
||||||
@ -384,7 +368,7 @@ describe Chat::ChannelFetcher do
|
|||||||
Chat::DirectMessageUser.create!(direct_message: dm_channel1, user: user2)
|
Chat::DirectMessageUser.create!(direct_message: dm_channel1, user: user2)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.secured_direct_message_channels(user1.id, memberships, guardian).map(&:id),
|
described_class.secured_direct_message_channels(user1.id, guardian).map(&:id),
|
||||||
).not_to include(direct_message_channel1.id)
|
).not_to include(direct_message_channel1.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -8,197 +8,35 @@ RSpec.describe Chat::Api::ChatablesController do
|
|||||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#index" do
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
fab!(:user) { Fabricate(:user, username: "johndoe", name: "John Doe") }
|
|
||||||
|
|
||||||
|
describe "#index" do
|
||||||
describe "without chat permissions" do
|
describe "without chat permissions" do
|
||||||
it "errors errors for anon" do
|
it "errors errors for anon" do
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
get "/chat/api/chatables"
|
||||||
expect(response.status).to eq(403)
|
expect(response.status).to eq(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "errors when user cannot chat" do
|
it "errors when user cannot chat" do
|
||||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
|
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
|
||||||
sign_in(user)
|
sign_in(current_user)
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
get "/chat/api/chatables"
|
||||||
expect(response.status).to eq(403)
|
expect(response.status).to eq(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "with chat permissions" do
|
describe "with chat permissions" do
|
||||||
fab!(:other_user) { Fabricate(:user, username: "janemay", name: "Jane May") }
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
fab!(:admin) { Fabricate(:admin, username: "andyjones", name: "Andy Jones") }
|
|
||||||
fab!(:category) { Fabricate(:category) }
|
|
||||||
fab!(:chat_channel) { Fabricate(:category_channel, chatable: category) }
|
|
||||||
fab!(:dm_chat_channel) { Fabricate(:direct_message_channel, users: [user, admin]) }
|
|
||||||
|
|
||||||
before do
|
before { sign_in(current_user) }
|
||||||
chat_channel.update(name: "something")
|
|
||||||
sign_in(user)
|
it "returns results" do
|
||||||
end
|
get "/chat/api/chatables", params: { term: channel_1.name }
|
||||||
|
|
||||||
it "returns the correct channels with filter 'so'" do
|
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(chat_channel.id)
|
expect(response.parsed_body["category_channels"][0]["identifier"]).to eq(
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
"c-#{channel_1.id}",
|
||||||
expect(response.parsed_body["users"].count).to eq(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the correct channels with filter 'something'" do
|
|
||||||
get "/chat/api/chatables", params: { filter: "something" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(chat_channel.id)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["users"].count).to eq(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the correct channels with filter 'andyjones'" do
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
expect(response.parsed_body["users"].count).to eq(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the current user inside the users array if their username matches the filter too" do
|
|
||||||
user.update!(username: "andysmith")
|
|
||||||
get "/chat/api/chatables", params: { filter: "andy" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
expect(response.parsed_body["users"].map { |u| u["id"] }).to match_array([user.id])
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns no channels with a whacky filter" do
|
|
||||||
get "/chat/api/chatables", params: { filter: "hello good sir" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["users"].count).to eq(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "only returns open channels" do
|
|
||||||
chat_channel.update(status: Chat::Channel.statuses[:closed])
|
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
chat_channel.update(status: Chat::Channel.statuses[:read_only])
|
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
chat_channel.update(status: Chat::Channel.statuses[:archived])
|
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
# Now set status to open and the channel is there!
|
|
||||||
chat_channel.update(status: Chat::Channel.statuses[:open])
|
|
||||||
get "/chat/api/chatables", params: { filter: "so" }
|
|
||||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "only finds users by username_lower if not enable_names" do
|
|
||||||
SiteSetting.enable_names = false
|
|
||||||
get "/chat/api/chatables", params: { filter: "Andy J" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "only finds users by username if prioritize_username_in_ux" do
|
|
||||||
SiteSetting.prioritize_username_in_ux = true
|
|
||||||
get "/chat/api/chatables", params: { filter: "Andy J" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "can find users by name or username if not prioritize_username_in_ux and enable_names" do
|
|
||||||
SiteSetting.prioritize_username_in_ux = false
|
|
||||||
SiteSetting.enable_names = true
|
|
||||||
get "/chat/api/chatables", params: { filter: "Andy J" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"].count).to eq(0)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not return DM channels for users who do not have chat enabled" do
|
|
||||||
admin.user_option.update!(chat_enabled: false)
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
xit "does not return DM channels for users who are not in the chat allowed group" do
|
|
||||||
group = Fabricate(:group, name: "chatpeeps")
|
|
||||||
SiteSetting.chat_allowed_groups = group.id
|
|
||||||
GroupUser.create(user: user, group: group)
|
|
||||||
dm_chat_channel_2 = Fabricate(:direct_message_channel, users: [user, other_user])
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: "janemay" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["direct_message_channels"].count).to eq(0)
|
|
||||||
|
|
||||||
GroupUser.create(user: other_user, group: group)
|
|
||||||
get "/chat/api/chatables", params: { filter: "janemay" }
|
|
||||||
if response.status == 500
|
|
||||||
puts "ERROR in ChatablesController spec:\n"
|
|
||||||
puts response.body
|
|
||||||
end
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel_2.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns DM channels for staff users even if they are not in chat_allowed_groups" do
|
|
||||||
group = Fabricate(:group, name: "chatpeeps")
|
|
||||||
SiteSetting.chat_allowed_groups = group.id
|
|
||||||
GroupUser.create(user: user, group: group)
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: "andyjones" }
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns followed channels" do
|
|
||||||
Fabricate(
|
|
||||||
:user_chat_channel_membership,
|
|
||||||
user: user,
|
|
||||||
chat_channel: chat_channel,
|
|
||||||
following: true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: chat_channel.name }
|
|
||||||
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(chat_channel.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns not followed channels" do
|
|
||||||
Fabricate(
|
|
||||||
:user_chat_channel_membership,
|
|
||||||
user: user,
|
|
||||||
chat_channel: chat_channel,
|
|
||||||
following: false,
|
|
||||||
)
|
|
||||||
|
|
||||||
get "/chat/api/chatables", params: { filter: chat_channel.name }
|
|
||||||
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(chat_channel.id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -49,7 +49,7 @@ RSpec.describe Chat::CreateDirectMessageChannel do
|
|||||||
)
|
)
|
||||||
result.channel.user_chat_channel_memberships.each do |membership|
|
result.channel.user_chat_channel_memberships.each do |membership|
|
||||||
expect(membership).to have_attributes(
|
expect(membership).to have_attributes(
|
||||||
following: true,
|
following: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
desktop_notification_level: "always",
|
desktop_notification_level: "always",
|
||||||
mobile_notification_level: "always",
|
mobile_notification_level: "always",
|
||||||
@ -57,12 +57,6 @@ RSpec.describe Chat::CreateDirectMessageChannel do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "publishes the new channel" do
|
|
||||||
messages =
|
|
||||||
MessageBus.track_publish(Chat::Publisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL) { result }
|
|
||||||
expect(messages.first.data[:channel][:title]).to eq("@elaine, @lechuck")
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when there is an existing direct message channel for the target users" do
|
context "when there is an existing direct message channel for the target users" do
|
||||||
before { described_class.call(params) }
|
before { described_class.call(params) }
|
||||||
|
|
||||||
|
139
plugins/chat/spec/services/chat/search_chatable_spec.rb
Normal file
139
plugins/chat/spec/services/chat/search_chatable_spec.rb
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe Chat::SearchChatable do
|
||||||
|
describe ".call" do
|
||||||
|
subject(:result) { described_class.call(params) }
|
||||||
|
|
||||||
|
fab!(:current_user) { Fabricate(:user, username: "bob-user") }
|
||||||
|
fab!(:sam) { Fabricate(:user, username: "sam-user") }
|
||||||
|
fab!(:charlie) { Fabricate(:user, username: "charlie-user") }
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel, name: "bob-channel") }
|
||||||
|
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, sam]) }
|
||||||
|
fab!(:channel_3) { Fabricate(:direct_message_channel, users: [current_user, sam, charlie]) }
|
||||||
|
fab!(:channel_4) { Fabricate(:direct_message_channel, users: [sam, charlie]) }
|
||||||
|
|
||||||
|
let(:guardian) { Guardian.new(current_user) }
|
||||||
|
let(:params) { { guardian: guardian, term: term } }
|
||||||
|
let(:term) { "" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.direct_message_enabled_groups = Group::AUTO_GROUPS[:everyone]
|
||||||
|
# simpler user search without having to worry about user search data
|
||||||
|
SiteSetting.enable_names = false
|
||||||
|
return unless guardian.can_create_direct_message?
|
||||||
|
channel_1.add(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when all steps pass" do
|
||||||
|
it "sets the service result as successful" do
|
||||||
|
expect(result).to be_a_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns chatables" do
|
||||||
|
expect(result.memberships).to contain_exactly(
|
||||||
|
channel_1.membership_for(current_user),
|
||||||
|
channel_2.membership_for(current_user),
|
||||||
|
channel_3.membership_for(current_user),
|
||||||
|
)
|
||||||
|
expect(result.category_channels).to contain_exactly(channel_1)
|
||||||
|
expect(result.direct_message_channels).to contain_exactly(channel_2, channel_3)
|
||||||
|
expect(result.users).to include(current_user, sam)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn’t return direct message of other users" do
|
||||||
|
expect(result.direct_message_channels).to_not include(channel_4)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with private channel" do
|
||||||
|
fab!(:private_channel_1) { Fabricate(:private_category_channel, name: "private") }
|
||||||
|
let(:term) { "#private" }
|
||||||
|
|
||||||
|
it "doesn’t return category channels you can't access" do
|
||||||
|
expect(result.category_channels).to_not include(private_channel_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when term is prefixed with #" do
|
||||||
|
let(:term) { "#" }
|
||||||
|
|
||||||
|
it "doesn’t return users" do
|
||||||
|
expect(result.users).to be_blank
|
||||||
|
expect(result.category_channels).to contain_exactly(channel_1)
|
||||||
|
expect(result.direct_message_channels).to contain_exactly(channel_2, channel_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when term is prefixed with @" do
|
||||||
|
let(:term) { "@" }
|
||||||
|
|
||||||
|
it "doesn’t return channels" do
|
||||||
|
expect(result.users).to include(current_user, sam)
|
||||||
|
expect(result.category_channels).to be_blank
|
||||||
|
expect(result.direct_message_channels).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering" do
|
||||||
|
context "with full match" do
|
||||||
|
let(:term) { "bob" }
|
||||||
|
|
||||||
|
it "returns matching channels" do
|
||||||
|
expect(result.users).to contain_exactly(current_user)
|
||||||
|
expect(result.category_channels).to contain_exactly(channel_1)
|
||||||
|
expect(result.direct_message_channels).to contain_exactly(channel_2, channel_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with partial match" do
|
||||||
|
let(:term) { "cha" }
|
||||||
|
|
||||||
|
it "returns matching channels" do
|
||||||
|
expect(result.users).to contain_exactly(charlie)
|
||||||
|
expect(result.category_channels).to contain_exactly(channel_1)
|
||||||
|
expect(result.direct_message_channels).to contain_exactly(channel_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering with non existing term" do
|
||||||
|
let(:term) { "xxxxxxxxxx" }
|
||||||
|
|
||||||
|
it "returns matching channels" do
|
||||||
|
expect(result.users).to be_blank
|
||||||
|
expect(result.category_channels).to be_blank
|
||||||
|
expect(result.direct_message_channels).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering with @prefix" do
|
||||||
|
let(:term) { "@bob" }
|
||||||
|
|
||||||
|
it "returns matching channels" do
|
||||||
|
expect(result.users).to contain_exactly(current_user)
|
||||||
|
expect(result.category_channels).to be_blank
|
||||||
|
expect(result.direct_message_channels).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering with #prefix" do
|
||||||
|
let(:term) { "#bob" }
|
||||||
|
|
||||||
|
it "returns matching channels" do
|
||||||
|
expect(result.users).to be_blank
|
||||||
|
expect(result.category_channels).to contain_exactly(channel_1)
|
||||||
|
expect(result.direct_message_channels).to contain_exactly(channel_2, channel_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when current user can't created direct messages" do
|
||||||
|
let(:term) { "@bob" }
|
||||||
|
|
||||||
|
before { SiteSetting.direct_message_enabled_groups = Group::AUTO_GROUPS[:staff] }
|
||||||
|
|
||||||
|
it "doesn’t return users" do
|
||||||
|
expect(result.users).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -6,7 +6,8 @@
|
|||||||
"muted",
|
"muted",
|
||||||
"desktop_notification_level",
|
"desktop_notification_level",
|
||||||
"mobile_notification_level",
|
"mobile_notification_level",
|
||||||
"following"
|
"following",
|
||||||
|
"last_viewed_at"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"chat_channel_id": { "type": "number" },
|
"chat_channel_id": { "type": "number" },
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"muted": { "type": "boolean" },
|
"muted": { "type": "boolean" },
|
||||||
"desktop_notification_level": { "type": "string" },
|
"desktop_notification_level": { "type": "string" },
|
||||||
"mobile_notification_level": { "type": "string" },
|
"mobile_notification_level": { "type": "string" },
|
||||||
|
"last_viewed_at": { "type": "string" },
|
||||||
"following": { "type": "boolean" },
|
"following": { "type": "boolean" },
|
||||||
"user": {
|
"user": {
|
||||||
"type": ["object", "null"],
|
"type": ["object", "null"],
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe "Channel selector modal", type: :system do
|
|
||||||
fab!(:current_user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
|
||||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
|
||||||
let(:key_modifier) { RUBY_PLATFORM =~ /darwin/i ? :meta : :control }
|
|
||||||
|
|
||||||
before do
|
|
||||||
chat_system_bootstrap
|
|
||||||
sign_in(current_user)
|
|
||||||
visit("/")
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when used with public channel" do
|
|
||||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
|
||||||
|
|
||||||
it "works" do
|
|
||||||
find("body").send_keys([key_modifier, "k"])
|
|
||||||
find("#chat-channel-selector-input").fill_in(with: channel_1.title)
|
|
||||||
find(".chat-channel-selection-row[data-id='#{channel_1.id}']").click
|
|
||||||
|
|
||||||
channel_page.send_message("Hello world")
|
|
||||||
|
|
||||||
expect(channel_page).to have_message(text: "Hello world")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when used with user" do
|
|
||||||
fab!(:user_1) { Fabricate(:user) }
|
|
||||||
|
|
||||||
it "works" do
|
|
||||||
find("body").send_keys([key_modifier, "k"])
|
|
||||||
find("#chat-channel-selector-input").fill_in(with: user_1.username)
|
|
||||||
find(".chat-channel-selection-row[data-id='#{user_1.id}']").click
|
|
||||||
|
|
||||||
channel_page.send_message("Hello world")
|
|
||||||
|
|
||||||
expect(channel_page).to have_message(text: "Hello world")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when used with dm channel" do
|
|
||||||
fab!(:dm_channel_1) { Fabricate(:direct_message_channel, users: [current_user]) }
|
|
||||||
|
|
||||||
it "works" do
|
|
||||||
find("body").send_keys([key_modifier, "k"])
|
|
||||||
find("#chat-channel-selector-input").fill_in(with: current_user.username)
|
|
||||||
find(".chat-channel-selection-row[data-id='#{dm_channel_1.id}']").click
|
|
||||||
channel_page.send_message("Hello world")
|
|
||||||
|
|
||||||
expect(channel_page).to have_message(text: "Hello world")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when on a channel" do
|
|
||||||
fab!(:channel_1) { Fabricate(:category_channel) }
|
|
||||||
|
|
||||||
it "it doesn’t include current channel" do
|
|
||||||
chat_page.visit_channel(channel_1)
|
|
||||||
find("body").send_keys([key_modifier, "k"])
|
|
||||||
find("#chat-channel-selector-input").click
|
|
||||||
|
|
||||||
expect(page).to have_no_css(".chat-channel-selection-row[data-id='#{channel_1.id}']")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with limited access channels" do
|
|
||||||
fab!(:group_1) { Fabricate(:group) }
|
|
||||||
fab!(:channel_1) { Fabricate(:private_category_channel, group: group_1) }
|
|
||||||
|
|
||||||
it "it doesn’t include limited access channel" do
|
|
||||||
find("body").send_keys([key_modifier, "k"])
|
|
||||||
find("#chat-channel-selector-input").fill_in(with: channel_1.title)
|
|
||||||
|
|
||||||
expect(page).to have_no_css(".chat-channel-selection-row[data-id='#{channel_1.id}']")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,26 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe "Draft message", type: :system do
|
|
||||||
fab!(:current_user) { Fabricate(:admin) }
|
|
||||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
|
||||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
|
||||||
let(:drawer) { PageObjects::Pages::ChatDrawer.new }
|
|
||||||
|
|
||||||
before do
|
|
||||||
chat_system_bootstrap
|
|
||||||
sign_in(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when current user never interacted with other user" do
|
|
||||||
fab!(:user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
it "opens channel info page" do
|
|
||||||
visit("/chat/draft-channel")
|
|
||||||
expect(page).to have_selector(".results")
|
|
||||||
|
|
||||||
find(".results .user:nth-child(1)").click
|
|
||||||
|
|
||||||
expect(channel_page).to have_no_loading_skeleton
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -121,8 +121,8 @@ RSpec.describe "List channels | mobile", type: :system, mobile: true do
|
|||||||
|
|
||||||
it "has a new dm channel button" do
|
it "has a new dm channel button" do
|
||||||
visit("/chat")
|
visit("/chat")
|
||||||
find(".open-draft-channel-page-btn").click
|
find(".open-new-message-btn").click
|
||||||
|
|
||||||
expect(page).to have_current_path("/chat/draft-channel")
|
expect(chat.message_creator).to be_opened
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -223,37 +223,15 @@ RSpec.describe "Navigation", type: :system do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when starting draft from sidebar with drawer preferred" do
|
|
||||||
it "opens draft in drawer" do
|
|
||||||
visit("/")
|
|
||||||
sidebar_page.open_draft_channel
|
|
||||||
|
|
||||||
expect(page).to have_current_path("/")
|
|
||||||
expect(page).to have_css(".chat-drawer.is-expanded .direct-message-creator")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when starting draft from drawer with drawer preferred" do
|
|
||||||
it "opens draft in drawer" do
|
|
||||||
visit("/")
|
|
||||||
chat_page.open_from_header
|
|
||||||
chat_drawer_page.open_draft_channel
|
|
||||||
|
|
||||||
expect(page).to have_current_path("/")
|
|
||||||
expect(page).to have_css(".chat-drawer.is-expanded .direct-message-creator")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when starting draft from sidebar with full page preferred" do
|
context "when starting draft from sidebar with full page preferred" do
|
||||||
it "opens draft in full page" do
|
it "opens draft in full page" do
|
||||||
visit("/")
|
visit("/")
|
||||||
chat_page.open_from_header
|
chat_page.open_from_header
|
||||||
chat_drawer_page.maximize
|
chat_drawer_page.maximize
|
||||||
visit("/")
|
visit("/")
|
||||||
sidebar_page.open_draft_channel
|
chat_page.open_new_message
|
||||||
|
|
||||||
expect(page).to have_current_path("/chat/draft-channel")
|
expect(chat_page.message_creator).to be_opened
|
||||||
expect(page).not_to have_css(".chat-drawer.is-expanded")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
375
plugins/chat/spec/system/new_message_spec.rb
Normal file
375
plugins/chat/spec/system/new_message_spec.rb
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "New message", type: :system do
|
||||||
|
fab!(:current_user) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# simpler user search without having to worry about user search data
|
||||||
|
SiteSetting.enable_names = false
|
||||||
|
|
||||||
|
chat_system_bootstrap
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cmd + k opens new message" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_opened
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the the content is not filtered" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:user_2) { Fabricate(:user) }
|
||||||
|
fab!(:direct_message_channel_1) do
|
||||||
|
Fabricate(:direct_message_channel, users: [current_user, user_1])
|
||||||
|
end
|
||||||
|
fab!(:direct_message_channel_2) { Fabricate(:direct_message_channel, users: [user_1, user_2]) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
it "lists channels the user is following" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_1)
|
||||||
|
# it lists user_1 instead of this channel as it's a 1:1 channel
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_2)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(
|
||||||
|
direct_message_channel_1,
|
||||||
|
current_user: current_user,
|
||||||
|
)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(
|
||||||
|
direct_message_channel_2,
|
||||||
|
current_user: current_user,
|
||||||
|
)
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with no selection" do
|
||||||
|
context "when clicking a row" do
|
||||||
|
context "when the row is a channel" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.click_row(channel_1)
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the row is a user" do
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.click_row(user_1)
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when shift clicking a row" do
|
||||||
|
context "when the row is a channel" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.shift_click_row(channel_1)
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the row is a user" do
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
|
||||||
|
it "adds the user" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.shift_click_row(user_1)
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when pressing enter" do
|
||||||
|
context "when the row is a channel" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.click_row(channel_1)
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the row is a user" do
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.click_row(user_1)
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when pressing shift+enter" do
|
||||||
|
context "when the row is a channel" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
it "opens the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.shift_enter_shortcut
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the row is a user" do
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
|
||||||
|
it "adds the user" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.shift_enter_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when navigating content with arrows" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel, name: "channela") }
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel, name: "channelb") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_1.add(current_user)
|
||||||
|
channel_2.add(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "changes active content" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_1, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_down_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_2, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_down_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_1, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_up_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_2, active: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with disabled content" do
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
|
||||||
|
before { user_1.user_option.update!(chat_enabled: false) }
|
||||||
|
|
||||||
|
it "doesn’t make the content active" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1, inactive: true, disabled: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel, name: "bob-channel") }
|
||||||
|
fab!(:user_1) { Fabricate(:user, username: "bob-user") }
|
||||||
|
fab!(:user_2) { Fabricate(:user) }
|
||||||
|
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
fab!(:channel_3) { Fabricate(:direct_message_channel, users: [current_user, user_1, user_2]) }
|
||||||
|
|
||||||
|
before { channel_1.add(current_user) }
|
||||||
|
|
||||||
|
context "with no prefix" do
|
||||||
|
it "lists all matching content" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
chat_page.message_creator.filter("bob")
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_2)
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_3)
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with channel prefix" do
|
||||||
|
it "lists matching channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
chat_page.message_creator.filter("#bob")
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_2)
|
||||||
|
expect(chat_page.message_creator).to be_listing(channel_3)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with user prefix" do
|
||||||
|
it "lists matching users" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
|
||||||
|
chat_page.message_creator.filter("@bob")
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_2)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_3)
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with selection" do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel, name: "bob-channel") }
|
||||||
|
fab!(:user_1) { Fabricate(:user, username: "bob-user") }
|
||||||
|
fab!(:user_2) { Fabricate(:user, username: "bobby-user") }
|
||||||
|
fab!(:user_3) { Fabricate(:user, username: "sam-user") }
|
||||||
|
fab!(:channel_2) { Fabricate(:direct_message_channel, users: [current_user, user_1]) }
|
||||||
|
fab!(:channel_3) { Fabricate(:direct_message_channel, users: [current_user, user_2]) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_1.add(current_user)
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_new_message
|
||||||
|
chat_page.message_creator.shift_click_row(user_1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when pressing enter" do
|
||||||
|
it "opens the channel" do
|
||||||
|
chat_page.message_creator.enter_shortcut
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_2.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when clicking cta" do
|
||||||
|
it "opens the channel" do
|
||||||
|
chat_page.message_creator.click_cta
|
||||||
|
|
||||||
|
expect(chat_page).to have_drawer(channel_id: channel_2.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when filtering" do
|
||||||
|
it "shows only matching users regarless of prefix" do
|
||||||
|
chat_page.message_creator.filter("#bob")
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_2)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(user_3)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_1)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_2)
|
||||||
|
expect(chat_page.message_creator).to be_not_listing(channel_3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows selected user as selected in content" do
|
||||||
|
chat_page.message_creator.filter("@bob")
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_1, selected: true)
|
||||||
|
expect(chat_page.message_creator).to be_listing(user_2, selected: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when clicking another user" do
|
||||||
|
it "adds it to the selection" do
|
||||||
|
chat_page.message_creator.filter("@bob")
|
||||||
|
chat_page.message_creator.click_row(user_2)
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1)
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when pressing backspace" do
|
||||||
|
it "removes it" do
|
||||||
|
chat_page.message_creator.backspace_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.backspace_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_not_selecting(user_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when navigating selection with arrow left/right" do
|
||||||
|
it "changes active item" do
|
||||||
|
chat_page.message_creator.filter("@bob")
|
||||||
|
chat_page.message_creator.click_row(user_2)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_left_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_2, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_left_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_left_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_2, active: true)
|
||||||
|
|
||||||
|
chat_page.message_creator.arrow_right_shortcut
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_selecting(user_1, active: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when clicking selection" do
|
||||||
|
it "removes it" do
|
||||||
|
chat_page.message_creator.click_item(user_1)
|
||||||
|
|
||||||
|
expect(chat_page.message_creator).to be_not_selecting(user_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,6 +3,12 @@
|
|||||||
module PageObjects
|
module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
class Chat < PageObjects::Pages::Base
|
class Chat < PageObjects::Pages::Base
|
||||||
|
MODIFIER = RUBY_PLATFORM =~ /darwin/i ? :meta : :control
|
||||||
|
|
||||||
|
def message_creator
|
||||||
|
@message_creator ||= PageObjects::Components::Chat::MessageCreator.new
|
||||||
|
end
|
||||||
|
|
||||||
def prefers_full_page
|
def prefers_full_page
|
||||||
page.execute_script(
|
page.execute_script(
|
||||||
"window.localStorage.setItem('discourse_chat_preferred_mode', '\"FULL_PAGE_CHAT\"');",
|
"window.localStorage.setItem('discourse_chat_preferred_mode', '\"FULL_PAGE_CHAT\"');",
|
||||||
@ -17,6 +23,10 @@ module PageObjects
|
|||||||
visit("/chat")
|
visit("/chat")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def open_new_message
|
||||||
|
send_keys([MODIFIER, "k"])
|
||||||
|
end
|
||||||
|
|
||||||
def has_drawer?(channel_id: nil, expanded: true)
|
def has_drawer?(channel_id: nil, expanded: true)
|
||||||
drawer?(expectation: true, channel_id: channel_id, expanded: expanded)
|
drawer?(expectation: true, channel_id: channel_id, expanded: expanded)
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,10 @@ module PageObjects
|
|||||||
input.value.blank?
|
input.value.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
component.has_css?(".chat-composer.is-enabled")
|
||||||
|
end
|
||||||
|
|
||||||
def has_saved_draft?
|
def has_saved_draft?
|
||||||
component.has_css?(".chat-composer.is-draft-saved")
|
component.has_css?(".chat-composer.is-draft-saved")
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Components
|
||||||
|
module Chat
|
||||||
|
class MessageCreator < PageObjects::Components::Base
|
||||||
|
attr_reader :context
|
||||||
|
|
||||||
|
SELECTOR = ".chat-new-message-modal"
|
||||||
|
|
||||||
|
def component
|
||||||
|
find(SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def input
|
||||||
|
component.find(".chat-message-creator__input")
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter(query = "")
|
||||||
|
input.fill_in(with: query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def opened?
|
||||||
|
page.has_css?(SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enter_shortcut
|
||||||
|
input.send_keys(:enter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def backspace_shortcut
|
||||||
|
input.send_keys(:backspace)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shift_enter_shortcut
|
||||||
|
input.send_keys(:shift, :enter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_cta
|
||||||
|
component.find(".chat-message-creator__open-dm-btn").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def arrow_left_shortcut
|
||||||
|
input.send_keys(:arrow_left)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arrow_right_shortcut
|
||||||
|
input.send_keys(:arrow_right)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arrow_down_shortcut
|
||||||
|
input.send_keys(:arrow_down)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arrow_up_shortcut
|
||||||
|
input.send_keys(:arrow_up)
|
||||||
|
end
|
||||||
|
|
||||||
|
def listing?(chatable, **args)
|
||||||
|
component.has_css?(build_row_selector(chatable, **args))
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_listing?(chatable, **args)
|
||||||
|
component.has_no_css?(build_row_selector(chatable, **args))
|
||||||
|
end
|
||||||
|
|
||||||
|
def selecting?(chatable, **args)
|
||||||
|
component.has_css?(build_item_selector(chatable, **args))
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_selecting?(chatable, **args)
|
||||||
|
component.has_no_css?(build_item_selector(chatable, **args))
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_item(chatable, **args)
|
||||||
|
component.find(build_item_selector(chatable, **args)).click
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_row(chatable, **args)
|
||||||
|
component.find(build_row_selector(chatable, **args)).click
|
||||||
|
end
|
||||||
|
|
||||||
|
def shift_click_row(chatable, **args)
|
||||||
|
component.find(build_row_selector(chatable, **args)).click(:shift)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_item_selector(chatable, **args)
|
||||||
|
selector = ".chat-message-creator__selection-item"
|
||||||
|
selector += content_selector(**args)
|
||||||
|
selector += chatable_selector(chatable)
|
||||||
|
selector
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_row_selector(chatable, **args)
|
||||||
|
selector = ".chat-message-creator__row"
|
||||||
|
selector += content_selector(**args)
|
||||||
|
selector += chatable_selector(chatable)
|
||||||
|
selector
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_selector(**args)
|
||||||
|
selector = ""
|
||||||
|
selector = ".-disabled" if args[:disabled]
|
||||||
|
selector = ".-selected" if args[:selected]
|
||||||
|
selector = ":not(.-disabled)" if args[:enabled]
|
||||||
|
if args[:active]
|
||||||
|
selector += ".-active"
|
||||||
|
elsif args[:inactive]
|
||||||
|
selector += ":not(.-active)"
|
||||||
|
end
|
||||||
|
selector
|
||||||
|
end
|
||||||
|
|
||||||
|
def chatable_selector(chatable)
|
||||||
|
selector = ""
|
||||||
|
if chatable.try(:category_channel?)
|
||||||
|
selector += ".-channel"
|
||||||
|
selector += "[data-id='c-#{chatable.id}']"
|
||||||
|
elsif chatable.try(:direct_message_channel?)
|
||||||
|
selector += ".-channel"
|
||||||
|
selector += "[data-id='c-#{chatable.id}']"
|
||||||
|
else
|
||||||
|
selector += ".-user"
|
||||||
|
selector += "[data-id='u-#{chatable.id}']"
|
||||||
|
end
|
||||||
|
selector
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -8,10 +8,6 @@ module PageObjects
|
|||||||
find("#{VISIBLE_DRAWER} .open-browse-page-btn").click
|
find("#{VISIBLE_DRAWER} .open-browse-page-btn").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_draft_channel
|
|
||||||
find("#{VISIBLE_DRAWER} .open-draft-channel-page-btn").click
|
|
||||||
end
|
|
||||||
|
|
||||||
def close
|
def close
|
||||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__close-btn").click
|
find("#{VISIBLE_DRAWER} .chat-drawer-header__close-btn").click
|
||||||
end
|
end
|
||||||
|
@ -11,13 +11,6 @@ module PageObjects
|
|||||||
find(".sidebar-section[data-section-name='chat-dms']")
|
find(".sidebar-section[data-section-name='chat-dms']")
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_draft_channel
|
|
||||||
find(
|
|
||||||
".sidebar-section[data-section-name='chat-dms'] .sidebar-section-header-button",
|
|
||||||
visible: false,
|
|
||||||
).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def open_browse
|
def open_browse
|
||||||
find(
|
find(
|
||||||
".sidebar-section[data-section-name='chat-channels'] .sidebar-section-header-button",
|
".sidebar-section[data-section-name='chat-channels'] .sidebar-section-header-button",
|
||||||
|
@ -11,6 +11,7 @@ RSpec.describe "Visit channel", type: :system do
|
|||||||
fab!(:inaccessible_dm_channel_1) { Fabricate(:direct_message_channel) }
|
fab!(:inaccessible_dm_channel_1) { Fabricate(:direct_message_channel) }
|
||||||
|
|
||||||
let(:chat) { PageObjects::Pages::Chat.new }
|
let(:chat) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
|
||||||
before { chat_system_bootstrap }
|
before { chat_system_bootstrap }
|
||||||
|
|
||||||
@ -143,13 +144,7 @@ RSpec.describe "Visit channel", type: :system do
|
|||||||
it "allows to join it" do
|
it "allows to join it" do
|
||||||
chat.visit_channel(dm_channel_1)
|
chat.visit_channel(dm_channel_1)
|
||||||
|
|
||||||
expect(page).to have_content(I18n.t("js.chat.channel_settings.join_channel"))
|
expect(channel_page.composer).to be_enabled
|
||||||
end
|
|
||||||
|
|
||||||
it "shows a preview of the channel" do
|
|
||||||
chat.visit_channel(dm_channel_1)
|
|
||||||
|
|
||||||
expect(chat).to have_message(message_1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -37,7 +37,7 @@ module("Discourse Chat | Component | chat-user-avatar", function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
hbs`<ChatUserAvatar @chat={{this.chat}} @user={{this.user}} />`
|
hbs`<ChatUserAvatar @showPresence={{true}} @chat={{this.chat}} @user={{this.user}} />`
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.true(
|
assert.true(
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
||||||
import { click, fillIn, render } from "@ember/test-helpers";
|
|
||||||
import hbs from "htmlbars-inline-precompile";
|
|
||||||
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
|
|
||||||
import { module, test } from "qunit";
|
|
||||||
|
|
||||||
function mockChat(context, options = {}) {
|
|
||||||
const mock = context.container.lookup("service:chat");
|
|
||||||
mock.searchPossibleDirectMessageUsers = () => {
|
|
||||||
return Promise.resolve({
|
|
||||||
users: options.users || [{ username: "hawk" }, { username: "mark" }],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
mock.getDmChannelForUsernames = () => {
|
|
||||||
return Promise.resolve({ chat_channel: fabricators.channel() });
|
|
||||||
};
|
|
||||||
return mock;
|
|
||||||
}
|
|
||||||
|
|
||||||
module("Discourse Chat | Component | direct-message-creator", function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
test("search", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn(".filter-usernames", "hawk");
|
|
||||||
assert.true(exists("li.user[data-username='hawk']"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("select/deselect", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
assert.false(exists(".selected-user"));
|
|
||||||
|
|
||||||
await fillIn(".filter-usernames", "hawk");
|
|
||||||
await click("li.user[data-username='hawk']");
|
|
||||||
assert.true(exists(".selected-user"));
|
|
||||||
|
|
||||||
await click(".selected-user");
|
|
||||||
assert.false(exists(".selected-user"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("no search results", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this, { users: [] }));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn(".filter-usernames", "bad cat");
|
|
||||||
assert.true(exists(".no-results"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("loads user on first load", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.true(exists("li.user[data-username='hawk']"));
|
|
||||||
assert.true(exists("li.user[data-username='mark']"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("do not load more users after selection", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("li.user[data-username='hawk']");
|
|
||||||
assert.false(exists("li.user[data-username='mark']"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("apply is-focused to filter-area on focus input", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} /><button class="test-blur">blur</button>`
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".filter-usernames");
|
|
||||||
assert.true(exists(".filter-area.is-focused"));
|
|
||||||
|
|
||||||
await click(".test-blur");
|
|
||||||
assert.false(exists(".filter-area.is-focused"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("state is reset on channel change", async function (assert) {
|
|
||||||
this.set("chat", mockChat(this));
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn(".filter-usernames", "hawk");
|
|
||||||
assert.strictEqual(query(".filter-usernames").value, "hawk");
|
|
||||||
|
|
||||||
this.set("channel", fabricators.channel());
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
assert.strictEqual(query(".filter-usernames").value, "");
|
|
||||||
assert.true(exists(".filter-area.is-focused"));
|
|
||||||
assert.true(exists("li.user[data-username='hawk']"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows user status", async function (assert) {
|
|
||||||
const userWithStatus = {
|
|
||||||
username: "hawk",
|
|
||||||
status: { emoji: "tooth", description: "off to dentist" },
|
|
||||||
};
|
|
||||||
const chat = mockChat(this, { users: [userWithStatus] });
|
|
||||||
this.set("chat", chat);
|
|
||||||
this.set("channel", ChatChannel.createDirectMessageChannelDraft());
|
|
||||||
|
|
||||||
await render(
|
|
||||||
hbs`<DirectMessageCreator @channel={{this.channel}} @chat={{this.chat}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn(".filter-usernames", "hawk");
|
|
||||||
assert.true(exists(".user-status-message"));
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user