mirror of
https://github.com/discourse/discourse.git
synced 2025-03-22 13:05:35 +08: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
plugins/chat
app
controllers/chat
serializers/chat
base_channel_membership_serializer.rbchatable_user_serializer.rbchatables_serializer.rbdirect_message_serializer.rb
services
assets
javascripts/discourse
chat-route-map.js
components
channels-list.hbschannels-list.jschat-browse-view.hbschat-browse-view.jschat-channel-members-view.jschat-channel-selection-row.hbschat-channel-selection-row.jschat-channel-selector-modal-inner.hbschat-channel-selector-modal-inner.jschat-channel-title.hbschat-channel.hbschat-channel.jschat-composer.hbschat-composer.jschat-draft-channel-screen.hbschat-draft-channel-screen.js
chat-drawer
chat-full-page-header.hbschat-retention-reminder.jschat-user-avatar.hbschat-user-avatar.jschat-user-display-name.hbschat-user-display-name.jschat
direct-message-creator.hbsdirect-message-creator.jsmodal
initializers
models
routes
services
stylesheets
config
lib/chat
spec
lib/chat
requests/chat/api
services/chat
support/api/schemas
system
test/javascripts/components
@ -1,7 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
|
||||
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions threading_enabled]
|
||||
CHANNEL_EDITABLE_PARAMS ||= %i[name description slug]
|
||||
CATEGORY_CHANNEL_EDITABLE_PARAMS ||= %i[
|
||||
auto_join_users
|
||||
allow_channel_wide_mentions
|
||||
threading_enabled
|
||||
]
|
||||
|
||||
class Chat::Api::ChannelsController < Chat::ApiController
|
||||
def index
|
||||
@ -12,7 +16,7 @@ class Chat::Api::ChannelsController < Chat::ApiController
|
||||
options[:status] = Chat::Channel.statuses[permitted[:status]] ? permitted[:status] : nil
|
||||
|
||||
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 =
|
||||
channels.map do |channel|
|
||||
Chat::ChannelSerializer.new(
|
||||
|
@ -1,83 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatablesController < Chat::ApiController
|
||||
before_action :ensure_logged_in
|
||||
|
||||
def index
|
||||
params.require(:filter)
|
||||
filter = params[:filter].downcase
|
||||
|
||||
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)
|
||||
with_service(::Chat::SearchChatable) do
|
||||
on_success { render_serialized(result, ::Chat::ChatablesSerializer, root: false) }
|
||||
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
|
||||
|
@ -83,10 +83,7 @@ module Chat
|
||||
Chat::MessageRateLimiter.run!(current_user)
|
||||
|
||||
@user_chat_channel_membership =
|
||||
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(
|
||||
current_user,
|
||||
following: true,
|
||||
)
|
||||
Chat::ChannelMembershipManager.new(@chat_channel).find_for_user(current_user)
|
||||
raise Discourse::InvalidAccess unless @user_chat_channel_membership
|
||||
|
||||
reply_to_msg_id = params[:in_reply_to_id]
|
||||
|
@ -7,6 +7,7 @@ module Chat
|
||||
:desktop_notification_level,
|
||||
:mobile_notification_level,
|
||||
:chat_channel_id,
|
||||
:last_read_message_id
|
||||
:last_read_message_id,
|
||||
:last_viewed_at
|
||||
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
|
||||
attributes :id
|
||||
|
||||
has_many :users, serializer: Chat::UserWithCustomFieldsAndStatusSerializer, embed: :objects
|
||||
has_many :users, serializer: Chat::ChatableUserSerializer, embed: :objects
|
||||
|
||||
def users
|
||||
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 :channel, :fetch_or_create_channel
|
||||
step :update_memberships
|
||||
step :publish_channel
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
@ -68,7 +67,7 @@ module Chat
|
||||
Chat::DirectMessageChannel.find_or_create_by(chatable: direct_message)
|
||||
end
|
||||
|
||||
def update_memberships(guardian:, channel:, target_users:, **)
|
||||
def update_memberships(channel:, target_users:, **)
|
||||
always_level = Chat::UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
|
||||
|
||||
memberships =
|
||||
@ -77,7 +76,7 @@ module Chat
|
||||
user_id: user.id,
|
||||
chat_channel_id: channel.id,
|
||||
muted: false,
|
||||
following: true,
|
||||
following: false,
|
||||
desktop_notification_level: always_level,
|
||||
mobile_notification_level: always_level,
|
||||
created_at: Time.zone.now,
|
||||
@ -90,9 +89,5 @@ module Chat
|
||||
unique_by: %i[user_id chat_channel_id],
|
||||
)
|
||||
end
|
||||
|
||||
def publish_channel(channel:, target_users:, **)
|
||||
Chat::Publisher.publish_new_channel(channel, target_users)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -364,23 +364,26 @@ module Chat
|
||||
NEW_CHANNEL_MESSAGE_BUS_CHANNEL = "/chat/new-channel"
|
||||
|
||||
def self.publish_new_channel(chat_channel, users)
|
||||
users.each do |user|
|
||||
# FIXME: This could generate a lot of queries depending on the amount of users
|
||||
membership = chat_channel.membership_for(user)
|
||||
Chat::UserChatChannelMembership
|
||||
.includes(: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
|
||||
# and other code will update it after calling it
|
||||
# it means frontend must handle logic for both cases
|
||||
serialized_channel =
|
||||
Chat::ChannelSerializer.new(
|
||||
chat_channel,
|
||||
scope: Guardian.new(user), # We need a guardian here for direct messages
|
||||
root: :channel,
|
||||
membership: membership,
|
||||
).as_json
|
||||
|
||||
MessageBus.publish(NEW_CHANNEL_MESSAGE_BUS_CHANNEL, serialized_channel, user_ids: [user.id])
|
||||
end
|
||||
MessageBus.publish(
|
||||
NEW_CHANNEL_MESSAGE_BUS_CHANNEL,
|
||||
serialized_channel,
|
||||
user_ids: [membership.user.id],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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.
|
||||
class Context < OpenStruct
|
||||
include ActiveModel::Serialization
|
||||
|
||||
# @return [Boolean] returns +true+ if the context is set as successful (default)
|
||||
def success?
|
||||
!failure?
|
||||
|
@ -21,7 +21,6 @@ export default function () {
|
||||
}
|
||||
);
|
||||
|
||||
this.route("draft-channel", { path: "/draft-channel" });
|
||||
this.route("browse", { path: "/browse" }, function () {
|
||||
this.route("all", { path: "/all" });
|
||||
this.route("closed", { path: "/closed" });
|
||||
|
@ -1,11 +1,10 @@
|
||||
{{#if this.showMobileDirectMessageButton}}
|
||||
<LinkTo
|
||||
@route="chat.draft-channel"
|
||||
class="btn-flat open-draft-channel-page-btn keep-mobile-sidebar-open btn-floating"
|
||||
title={{i18n "chat.direct_messages.new"}}
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
</LinkTo>
|
||||
<DButton
|
||||
@icon="plus"
|
||||
class="no-text btn-flat open-new-message-btn keep-mobile-sidebar-open btn-floating"
|
||||
@action={{this.openNewMessageModal}}
|
||||
title={{i18n this.createDirectMessageChannelLabel}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<div
|
||||
@ -95,13 +94,12 @@
|
||||
(not this.showMobileDirectMessageButton)
|
||||
)
|
||||
}}
|
||||
<LinkTo
|
||||
@route="chat.draft-channel"
|
||||
class="btn no-text btn-flat open-draft-channel-page-btn"
|
||||
<DButton
|
||||
@icon="plus"
|
||||
class="no-text btn-flat open-new-message-btn"
|
||||
@action={{this.openNewMessageModal}}
|
||||
title={{i18n this.createDirectMessageChannelLabel}}
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
</LinkTo>
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -4,6 +4,8 @@ import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||
|
||||
export default class ChannelsList extends Component {
|
||||
@service chat;
|
||||
@service router;
|
||||
@ -12,6 +14,7 @@ export default class ChannelsList extends Component {
|
||||
@service site;
|
||||
@service session;
|
||||
@service currentUser;
|
||||
@service modal;
|
||||
|
||||
@tracked hasScrollbar = false;
|
||||
|
||||
@ -25,6 +28,11 @@ export default class ChannelsList extends Component {
|
||||
this.computeHasScrollbar(entries[0].target);
|
||||
}
|
||||
|
||||
@action
|
||||
openNewMessageModal() {
|
||||
this.modal.show(ChatNewMessageModal);
|
||||
}
|
||||
|
||||
get showMobileDirectMessageButton() {
|
||||
return this.site.mobileView && this.canCreateDirectMessageChannel;
|
||||
}
|
||||
|
@ -59,10 +59,10 @@
|
||||
<span class="empty-state-title">{{i18n "chat.empty_state.title"}}</span>
|
||||
<div class="empty-state-body">
|
||||
<p>{{i18n "chat.empty_state.direct_message"}}</p>
|
||||
|
||||
<LinkTo @route={{concat "chat.draft-channel"}}>
|
||||
{{i18n "chat.empty_state.direct_message_cta"}}
|
||||
</LinkTo>
|
||||
<DButton
|
||||
@action={{this.showChatNewMessageModal}}
|
||||
label="chat.empty_state.direct_message_cta"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{else if this.channelsCollection.length}}
|
||||
|
@ -5,6 +5,7 @@ import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
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"];
|
||||
|
||||
@ -38,6 +39,11 @@ export default class ChatBrowseView extends Component {
|
||||
return document.querySelector("#chat-progress-bar-container");
|
||||
}
|
||||
|
||||
@action
|
||||
showChatNewMessageModal() {
|
||||
this.modal.show(ChatNewMessageModal);
|
||||
}
|
||||
|
||||
@action
|
||||
onScroll() {
|
||||
discourseDebounce(
|
||||
|
@ -19,7 +19,7 @@ export default class ChatChannelMembersView extends Component {
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (!this.channel || this.channel.isDraft) {
|
||||
if (!this.channel) {
|
||||
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}}
|
||||
<div class="chat-channel-title is-draft">
|
||||
<span class="chat-channel-title__name">{{@channel.title}}</span>
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
{{#if @channel.chatable.users.length}}
|
||||
<div class="chat-channel-title__avatar">
|
||||
{{#if this.multiDm}}
|
||||
<span class="chat-channel-title__users-count">
|
||||
@ -18,9 +10,11 @@
|
||||
<ChatUserAvatar @user={{@channel.chatable.users.firstObject}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
{{#if @channel.chatable.users.length}}
|
||||
{{#if this.multiDm}}
|
||||
<span class="chat-channel-title__name">{{this.usernames}}</span>
|
||||
{{else}}
|
||||
@ -41,31 +35,33 @@
|
||||
/>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @channel.isCategoryChannel}}
|
||||
<div class="chat-channel-title is-category">
|
||||
<span
|
||||
class="chat-channel-title__category-badge"
|
||||
style={{this.channelColorStyle}}
|
||||
>
|
||||
{{d-icon "d-chat"}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{d-icon "lock" class="chat-channel-title__restricted-category-icon"}}
|
||||
{{else}}
|
||||
<span class="chat-channel-title__name">Add users</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
<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}}
|
@ -74,14 +74,14 @@
|
||||
@pane={{this.pane}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
||||
{{#if (and (not @channel.isFollowing) @channel.isCategoryChannel)}}
|
||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||
{{else}}
|
||||
<Chat::Composer::Channel
|
||||
@channel={{@channel}}
|
||||
@uploadDropZone={{this.uploadDropZone}}
|
||||
@onSendMessage={{this.onSendMessage}}
|
||||
/>
|
||||
{{else}}
|
||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { action } from "@ember/object";
|
||||
// TODO (martin) Remove this when the handleSentMessage logic inside chatChannelPaneSubscriptionsManager
|
||||
// is moved over from this file completely.
|
||||
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 { cancel, later, next, schedule } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
@ -736,33 +735,6 @@ export default class ChatLivePane extends Component {
|
||||
|
||||
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);
|
||||
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) {
|
||||
const stagedMessage = this.args.channel.findStagedMessage(id);
|
||||
if (stagedMessage) {
|
||||
@ -977,14 +929,9 @@ export default class ChatLivePane extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.args.channel.isDraft) {
|
||||
event.preventDefault();
|
||||
this.composer.focus({ addText: event.key });
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.composer.focus({ addText: event.key });
|
||||
return;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -65,6 +65,7 @@
|
||||
{{on "focusin" (fn this.computeIsFocused true)}}
|
||||
{{on "focusout" (fn this.computeIsFocused false)}}
|
||||
{{did-insert this.setupAutocomplete}}
|
||||
{{did-insert this.composer.focus}}
|
||||
data-chat-composer-context={{this.context}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@ export default class ChatComposer extends Component {
|
||||
@tracked presenceChannelName;
|
||||
|
||||
get shouldRenderReplyingIndicator() {
|
||||
return !this.args.channel?.isDraft;
|
||||
return this.args.channel;
|
||||
}
|
||||
|
||||
get shouldRenderMessageDetails() {
|
||||
@ -89,7 +89,7 @@ export default class ChatComposer extends Component {
|
||||
setupTextareaInteractor(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 });
|
||||
}
|
||||
}
|
||||
@ -250,10 +250,6 @@ export default class ChatComposer extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.args.channel.isDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatComposerPresenceManager.notifyState(
|
||||
this.presenceChannelName,
|
||||
!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
|
||||
(and
|
||||
this.chatStateManager.isFullPageActive this.displayed (not @channel.isDraft)
|
||||
)
|
||||
}}
|
||||
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-full-page-header"
|
||||
|
@ -9,11 +9,10 @@ export default class ChatRetentionReminder extends Component {
|
||||
|
||||
get show() {
|
||||
return (
|
||||
!this.args.channel?.isDraft &&
|
||||
((this.args.channel?.isDirectMessageChannel &&
|
||||
(this.args.channel?.isDirectMessageChannel &&
|
||||
this.currentUser?.get("needs_dm_retention_reminder")) ||
|
||||
(this.args.channel?.isCategoryChannel &&
|
||||
this.currentUser?.get("needs_channel_retention_reminder")))
|
||||
(this.args.channel?.isCategoryChannel &&
|
||||
this.currentUser?.get("needs_channel_retention_reminder"))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
<div
|
||||
class="chat-user-avatar
|
||||
{{if (and this.isOnline this.showPresence) 'is-online'}}"
|
||||
class="chat-user-avatar {{if (and this.isOnline @showPresence) 'is-online'}}"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
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>
|
@ -1,23 +1,19 @@
|
||||
import Component from "@ember/component";
|
||||
import { computed } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatUserAvatar extends Component {
|
||||
@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() {
|
||||
const users = this.chat.presenceChannel?.users;
|
||||
const users = (this.args.chat || this.chat).presenceChannel?.users;
|
||||
|
||||
return (
|
||||
!!users?.findBy("id", this.user?.id) ||
|
||||
!!users?.findBy("username", this.user?.username)
|
||||
!!users?.findBy("id", this.args.user?.id) ||
|
||||
!!users?.findBy("username", this.args.user?.username)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
<span class="chat-user-display-name">
|
||||
{{#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>
|
||||
{{/if}}
|
||||
|
||||
<span class="chat-user-display-name__username">
|
||||
<span
|
||||
class={{concat-class
|
||||
"chat-user-display-name__username"
|
||||
(unless this.shouldShowNameFirst "-first")
|
||||
}}
|
||||
>
|
||||
{{this.formattedUsername}}
|
||||
</span>
|
||||
|
||||
{{#if this.shouldShowNameLast}}
|
||||
<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}}
|
||||
</span>
|
@ -1,32 +1,26 @@
|
||||
import Component from "@ember/component";
|
||||
import { computed } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatUserDisplayName extends Component {
|
||||
tagName = "";
|
||||
user = null;
|
||||
@service siteSettings;
|
||||
|
||||
@computed
|
||||
get shouldPrioritizeNameInUx() {
|
||||
return !this.siteSettings.prioritize_username_in_ux;
|
||||
}
|
||||
|
||||
@computed("user.name")
|
||||
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() {
|
||||
return formatUsername(this.user?.username);
|
||||
return formatUsername(this.args.user?.username);
|
||||
}
|
||||
|
||||
@computed("shouldPrioritizeNameInUx", "hasValidName")
|
||||
get shouldShowNameFirst() {
|
||||
return this.shouldPrioritizeNameInUx && this.hasValidName;
|
||||
}
|
||||
|
||||
@computed("shouldPrioritizeNameInUx", "hasValidName")
|
||||
get shouldShowNameLast() {
|
||||
return !this.shouldPrioritizeNameInUx && this.hasValidName;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { inject as service } from "@ember/service";
|
||||
import I18n from "I18n";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { action } from "@ember/object";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
|
||||
export default class ChatComposerChannel extends ChatComposer {
|
||||
@service("chat-channel-composer") composer;
|
||||
@ -22,8 +21,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
||||
|
||||
get disabled() {
|
||||
return (
|
||||
(this.args.channel.isDraft &&
|
||||
isEmpty(this.args.channel?.chatable?.users)) ||
|
||||
!this.chat.userCanInteractWithChat ||
|
||||
!this.args.channel.canModifyMessages(this.currentUser)
|
||||
);
|
||||
@ -36,10 +33,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
||||
|
||||
@action
|
||||
persistDraft() {
|
||||
if (this.args.channel?.isDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatDraftsManager.add(this.currentMessage);
|
||||
|
||||
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) {
|
||||
return I18n.t("chat.placeholder_silenced");
|
||||
} 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;
|
||||
}
|
||||
}
|
20
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel-row.hbs
Normal file
20
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel-row.hbs
Normal file
@ -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}}
|
12
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel-row.js
Normal file
12
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/channel-row.js
Normal file
@ -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"));
|
||||
}
|
||||
}
|
36
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-row.hbs
Normal file
36
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-row.hbs
Normal file
@ -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}}
|
17
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-row.js
Normal file
17
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-row.js
Normal file
@ -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"));
|
||||
}
|
||||
}
|
5
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-selection.hbs
Normal file
5
plugins/chat/assets/javascripts/discourse/components/chat/message-creator/user-selection.hbs
Normal file
@ -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 showModal from "discourse/lib/show-modal";
|
||||
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||
|
||||
const APPLE =
|
||||
navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
|
||||
@ -16,6 +16,7 @@ export default {
|
||||
|
||||
const router = container.lookup("service:router");
|
||||
const appEvents = container.lookup("service:app-events");
|
||||
const modal = container.lookup("service:modal");
|
||||
const chatStateManager = container.lookup("service:chat-state-manager");
|
||||
const chatThreadPane = container.lookup("service:chat-thread-pane");
|
||||
const chatThreadListPane = container.lookup(
|
||||
@ -27,11 +28,7 @@ export default {
|
||||
const openChannelSelector = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (document.getElementById("chat-channel-selector-modal-inner")) {
|
||||
appEvents.trigger("chat-channel-selector-modal:close");
|
||||
} else {
|
||||
showModal("chat-channel-selector-modal");
|
||||
}
|
||||
modal.show(ChatNewMessageModal);
|
||||
};
|
||||
|
||||
const handleMoveUpShortcut = (e) => {
|
||||
|
@ -9,6 +9,7 @@ import { emojiUnescape } from "discourse/lib/text";
|
||||
import { decorateUsername } from "discourse/helpers/decorate-username-selector";
|
||||
import { until } from "discourse/lib/formatter";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
|
||||
|
||||
export default {
|
||||
name: "chat-sidebar",
|
||||
@ -329,6 +330,7 @@ export default {
|
||||
|
||||
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
|
||||
@service site;
|
||||
@service modal;
|
||||
@service router;
|
||||
@tracked userCanDirectMessage =
|
||||
this.chatService.userCanDirectMessage;
|
||||
@ -377,7 +379,7 @@ export default {
|
||||
id: "startDm",
|
||||
title: I18n.t("chat.direct_messages.new"),
|
||||
action: () => {
|
||||
this.router.transitionTo("chat.draft-channel");
|
||||
this.modal.show(ChatNewMessageModal);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -56,19 +56,7 @@ export default class ChatChannel {
|
||||
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 isDraft = false;
|
||||
@tracked title;
|
||||
@tracked slug;
|
||||
@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 lastReadMessageId = null;
|
||||
@tracked user = null;
|
||||
@tracked lastViewedAt = null;
|
||||
|
||||
constructor(args = {}) {
|
||||
this.following = args.following;
|
||||
@ -19,6 +20,7 @@ export default class UserChatChannelMembership {
|
||||
this.desktopNotificationLevel = args.desktop_notification_level;
|
||||
this.mobileNotificationLevel = args.mobile_notification_level;
|
||||
this.lastReadMessageId = args.last_read_message_id;
|
||||
this.lastViewedAt = args.last_viewed_at;
|
||||
this.user = this.#initUserModel(args.user);
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,6 @@ export default class ChatRoute extends DiscourseRoute {
|
||||
"chat.channel-legacy",
|
||||
"chat",
|
||||
"chat.index",
|
||||
"chat.draft-channel",
|
||||
];
|
||||
|
||||
if (
|
||||
|
@ -408,6 +408,17 @@ export default class ChatApi extends Service {
|
||||
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
|
||||
* 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];
|
||||
}
|
||||
|
||||
get allChannels() {
|
||||
return [...this.publicMessageChannels, ...this.directMessageChannels].sort(
|
||||
(a, b) => {
|
||||
return b?.currentUserMembership?.lastViewedAt?.localeCompare?.(
|
||||
a?.currentUserMembership?.lastViewedAt
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get publicMessageChannels() {
|
||||
return this.channels
|
||||
.filter(
|
||||
|
@ -1,13 +1,11 @@
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
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 ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
|
||||
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
|
||||
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
||||
|
||||
const ROUTES = {
|
||||
"chat.draft-channel": { name: ChatDrawerDraftChannel },
|
||||
"chat.channel": { name: ChatDrawerChannel },
|
||||
"chat.channel.thread": {
|
||||
name: ChatDrawerThread,
|
||||
|
@ -354,6 +354,7 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
this.chatChannelsManager.find(data.channel.id).then((channel) => {
|
||||
// we need to refresh here to have correct last message ids
|
||||
channel.meta = data.channel.meta;
|
||||
channel.updateMembership(data.channel.current_user_membership);
|
||||
|
||||
if (
|
||||
channel.isDirectMessageChannel &&
|
||||
|
@ -1,6 +1,5 @@
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
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() {
|
||||
// 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
|
||||
height: calc(
|
||||
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) - $inset
|
||||
var(--composer-height, 0px)
|
||||
);
|
||||
|
||||
// mobile with keyboard opened
|
||||
.keyboard-visible & {
|
||||
height: calc(
|
||||
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
|
||||
var(--chat-draft-header-height, 0px) -
|
||||
var(--chat-direct-message-creator-height, 0px)
|
||||
);
|
||||
height: calc(var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px));
|
||||
}
|
||||
|
||||
// ipad
|
||||
.footer-nav-ipad & {
|
||||
height: calc(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.btn-floating.open-draft-channel-page-btn {
|
||||
.btn-floating.open-new-message-btn {
|
||||
position: fixed;
|
||||
background: var(--tertiary);
|
||||
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 {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&: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-height-mixin";
|
||||
@import "chat-thread-header-buttons";
|
||||
@import "base-common";
|
||||
@import "sidebar-extensions";
|
||||
@import "chat-browse";
|
||||
@ -7,7 +8,6 @@
|
||||
@import "chat-channel-card";
|
||||
@import "chat-channel-info";
|
||||
@import "chat-channel-preview-card";
|
||||
@import "chat-channel-selector-modal";
|
||||
@import "chat-channel-settings-saved-indicator";
|
||||
@import "chat-channel-title";
|
||||
@import "chat-composer-dropdown";
|
||||
@ -15,7 +15,6 @@
|
||||
@import "chat-composer-uploads";
|
||||
@import "chat-composer";
|
||||
@import "chat-composer-button";
|
||||
@import "chat-draft-channel";
|
||||
@import "chat-drawer";
|
||||
@import "chat-emoji-picker";
|
||||
@import "chat-form";
|
||||
@ -45,14 +44,12 @@
|
||||
@import "create-channel-modal";
|
||||
@import "d-progress-bar";
|
||||
@import "dc-filter-input";
|
||||
@import "direct-message-creator";
|
||||
@import "full-page-chat-header";
|
||||
@import "incoming-chat-webhooks";
|
||||
@import "reviewable-chat-message";
|
||||
@import "chat-thread-list-item";
|
||||
@import "chat-threads-list";
|
||||
@import "chat-composer-separator";
|
||||
@import "chat-thread-header-buttons";
|
||||
@import "chat-thread-header";
|
||||
@import "chat-thread-list-header";
|
||||
@import "chat-thread-unread-indicator";
|
||||
@ -60,3 +57,5 @@
|
||||
@import "channel-summary-modal";
|
||||
@import "chat-message-mention-warning";
|
||||
@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,
|
||||
.edit-channels-dropdown .select-kit-header,
|
||||
.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-message-actions";
|
||||
@import "chat-message";
|
||||
@import "chat-message-creator";
|
||||
@import "chat-message-thread-indicator";
|
||||
@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-thread-settings-modal";
|
||||
@import "chat-message-thread-indicator";
|
||||
@import "chat-message-creator";
|
||||
|
@ -324,6 +324,16 @@ en:
|
||||
members: Members
|
||||
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:
|
||||
title: Edit channel
|
||||
input_placeholder: Add a name
|
||||
@ -342,10 +352,6 @@ en:
|
||||
no_results: No results
|
||||
selected_user_title: "Deselect %{username}"
|
||||
|
||||
channel_selector:
|
||||
title: "Jump to channel"
|
||||
no_channels: "No channels match your search"
|
||||
|
||||
channel:
|
||||
no_memberships: This channel has no members
|
||||
no_memberships_found: No members found
|
||||
|
@ -51,7 +51,6 @@ Chat::Engine.routes.draw do
|
||||
|
||||
# direct_messages_controller routes
|
||||
get "/direct_messages" => "direct_messages#index"
|
||||
post "/direct_messages/create" => "direct_messages#create"
|
||||
|
||||
# incoming_webhooks_controller routes
|
||||
post "/hooks/:key" => "incoming_webhooks#create_message"
|
||||
@ -66,7 +65,6 @@ Chat::Engine.routes.draw do
|
||||
get "/browse/closed" => "chat#respond"
|
||||
get "/browse/open" => "chat#respond"
|
||||
get "/browse/archived" => "chat#respond"
|
||||
get "/draft-channel" => "chat#respond"
|
||||
post "/enable" => "chat#enable_chat"
|
||||
post "/disable" => "chat#disable_chat"
|
||||
post "/dismiss-retention-reminder" => "chat#dismiss_retention_reminder"
|
||||
|
@ -6,10 +6,8 @@ module Chat
|
||||
|
||||
def self.structured(guardian, include_threads: false)
|
||||
memberships = Chat::ChannelMembershipManager.all_for_user(guardian.user)
|
||||
public_channels =
|
||||
secured_public_channels(guardian, memberships, status: :open, following: true)
|
||||
direct_message_channels =
|
||||
secured_direct_message_channels(guardian.user.id, memberships, guardian)
|
||||
public_channels = secured_public_channels(guardian, status: :open, following: true)
|
||||
direct_message_channels = secured_direct_message_channels(guardian.user.id, guardian)
|
||||
{
|
||||
public_channels: public_channels,
|
||||
direct_message_channels: direct_message_channels,
|
||||
@ -152,7 +150,7 @@ module Chat
|
||||
channels.limit(options[:limit]).offset(options[:offset])
|
||||
end
|
||||
|
||||
def self.secured_public_channels(guardian, memberships, options = { following: true })
|
||||
def self.secured_public_channels(guardian, options = { following: true })
|
||||
channels =
|
||||
secured_public_channel_search(
|
||||
guardian,
|
||||
@ -174,19 +172,60 @@ module Chat
|
||||
)
|
||||
end
|
||||
|
||||
def self.secured_direct_message_channels(user_id, memberships, guardian)
|
||||
query = Chat::Channel.includes(chatable: [{ direct_message_users: :user }, :users])
|
||||
def self.secured_direct_message_channels(user_id, guardian)
|
||||
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.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
|
||||
.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("chat_channels.id IN (#{generate_allowed_channel_ids_sql(guardian)})")
|
||||
.where(chat_channels: { id: scoped_channels })
|
||||
.order(last_message_sent_at: :desc)
|
||||
.to_a
|
||||
|
||||
channels = query.to_a
|
||||
preload_fields =
|
||||
User.allowed_user_custom_fields(guardian) +
|
||||
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
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||
).to match_array([category_channel.id])
|
||||
end
|
||||
|
||||
@ -207,7 +205,6 @@ describe Chat::ChannelFetcher do
|
||||
expect(
|
||||
described_class.secured_public_channels(
|
||||
guardian,
|
||||
memberships,
|
||||
following: following,
|
||||
filter: "support",
|
||||
).map(&:id),
|
||||
@ -218,7 +215,6 @@ describe Chat::ChannelFetcher do
|
||||
expect(
|
||||
described_class.secured_public_channels(
|
||||
guardian,
|
||||
memberships,
|
||||
following: following,
|
||||
filter: "cool stuff",
|
||||
).map(&:id),
|
||||
@ -227,33 +223,29 @@ describe Chat::ChannelFetcher do
|
||||
|
||||
it "can filter by an array of slugs" do
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, slugs: ["support"]).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, slugs: ["support"]).map(&:id),
|
||||
).to match_array([category_channel.id])
|
||||
end
|
||||
|
||||
it "returns nothing if the array of slugs is empty" do
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, slugs: []).map(&:id),
|
||||
).to eq([])
|
||||
expect(described_class.secured_public_channels(guardian, slugs: []).map(&:id)).to eq([])
|
||||
end
|
||||
|
||||
it "can filter by status" do
|
||||
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.closed!(Discourse.system_user)
|
||||
|
||||
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])
|
||||
end
|
||||
|
||||
it "can filter by following" do
|
||||
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
|
||||
end
|
||||
|
||||
@ -262,21 +254,19 @@ describe Chat::ChannelFetcher do
|
||||
another_channel = Fabricate(:category_channel)
|
||||
|
||||
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])
|
||||
end
|
||||
|
||||
it "ensures offset is >= 0" do
|
||||
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])
|
||||
end
|
||||
|
||||
it "ensures limit is > 0" do
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, limit: -1, offset: 0).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, limit: -1, offset: 0).map(&:id),
|
||||
).to match_array([category_channel.id])
|
||||
end
|
||||
|
||||
@ -284,17 +274,15 @@ describe Chat::ChannelFetcher do
|
||||
over_limit = Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS + 1
|
||||
over_limit.times { Fabricate(:category_channel) }
|
||||
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, limit: over_limit).length,
|
||||
).to eq(Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS)
|
||||
expect(described_class.secured_public_channels(guardian, limit: over_limit).length).to eq(
|
||||
Chat::ChannelFetcher::MAX_PUBLIC_CHANNEL_RESULTS,
|
||||
)
|
||||
end
|
||||
|
||||
it "does not show the user category channels they cannot access" do
|
||||
category_channel.update!(chatable: private_category)
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||
).to be_empty
|
||||
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
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||
).to be_empty
|
||||
|
||||
Chat::UserChatChannelMembership.create!(
|
||||
@ -315,9 +301,7 @@ describe Chat::ChannelFetcher do
|
||||
)
|
||||
|
||||
expect(
|
||||
described_class.secured_public_channels(guardian, memberships, following: following).map(
|
||||
&:id
|
||||
),
|
||||
described_class.secured_public_channels(guardian, following: following).map(&:id),
|
||||
).to match_array([category_channel.id])
|
||||
end
|
||||
|
||||
@ -369,9 +353,9 @@ describe Chat::ChannelFetcher do
|
||||
direct_message_channel1.update!(last_message_sent_at: 1.day.ago)
|
||||
direct_message_channel2.update!(last_message_sent_at: 1.hour.ago)
|
||||
|
||||
expect(
|
||||
described_class.secured_direct_message_channels(user1.id, memberships, guardian).map(&:id),
|
||||
).to eq([direct_message_channel2.id, direct_message_channel1.id])
|
||||
expect(described_class.secured_direct_message_channels(user1.id, guardian).map(&:id)).to eq(
|
||||
[direct_message_channel2.id, direct_message_channel1.id],
|
||||
)
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -8,197 +8,35 @@ RSpec.describe Chat::Api::ChatablesController do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
end
|
||||
|
||||
describe "#index" do
|
||||
fab!(:user) { Fabricate(:user, username: "johndoe", name: "John Doe") }
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
describe "#index" do
|
||||
describe "without chat permissions" do
|
||||
it "errors errors for anon" do
|
||||
get "/chat/api/chatables", params: { filter: "so" }
|
||||
get "/chat/api/chatables"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "errors when user cannot chat" do
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
|
||||
sign_in(user)
|
||||
get "/chat/api/chatables", params: { filter: "so" }
|
||||
sign_in(current_user)
|
||||
get "/chat/api/chatables"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with chat permissions" do
|
||||
fab!(:other_user) { Fabricate(:user, username: "janemay", name: "Jane May") }
|
||||
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]) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
|
||||
before do
|
||||
chat_channel.update(name: "something")
|
||||
sign_in(user)
|
||||
end
|
||||
before { sign_in(current_user) }
|
||||
|
||||
it "returns results" do
|
||||
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.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 '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,
|
||||
expect(response.parsed_body["category_channels"][0]["identifier"]).to eq(
|
||||
"c-#{channel_1.id}",
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -49,7 +49,7 @@ RSpec.describe Chat::CreateDirectMessageChannel do
|
||||
)
|
||||
result.channel.user_chat_channel_memberships.each do |membership|
|
||||
expect(membership).to have_attributes(
|
||||
following: true,
|
||||
following: false,
|
||||
muted: false,
|
||||
desktop_notification_level: "always",
|
||||
mobile_notification_level: "always",
|
||||
@ -57,12 +57,6 @@ RSpec.describe Chat::CreateDirectMessageChannel do
|
||||
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
|
||||
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",
|
||||
"desktop_notification_level",
|
||||
"mobile_notification_level",
|
||||
"following"
|
||||
"following",
|
||||
"last_viewed_at"
|
||||
],
|
||||
"properties": {
|
||||
"chat_channel_id": { "type": "number" },
|
||||
@ -14,6 +15,7 @@
|
||||
"muted": { "type": "boolean" },
|
||||
"desktop_notification_level": { "type": "string" },
|
||||
"mobile_notification_level": { "type": "string" },
|
||||
"last_viewed_at": { "type": "string" },
|
||||
"following": { "type": "boolean" },
|
||||
"user": {
|
||||
"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
|
||||
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
|
||||
|
@ -223,37 +223,15 @@ RSpec.describe "Navigation", type: :system do
|
||||
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
|
||||
it "opens draft in full page" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
chat_drawer_page.maximize
|
||||
visit("/")
|
||||
sidebar_page.open_draft_channel
|
||||
chat_page.open_new_message
|
||||
|
||||
expect(page).to have_current_path("/chat/draft-channel")
|
||||
expect(page).not_to have_css(".chat-drawer.is-expanded")
|
||||
expect(chat_page.message_creator).to be_opened
|
||||
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 Pages
|
||||
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
|
||||
page.execute_script(
|
||||
"window.localStorage.setItem('discourse_chat_preferred_mode', '\"FULL_PAGE_CHAT\"');",
|
||||
@ -17,6 +23,10 @@ module PageObjects
|
||||
visit("/chat")
|
||||
end
|
||||
|
||||
def open_new_message
|
||||
send_keys([MODIFIER, "k"])
|
||||
end
|
||||
|
||||
def has_drawer?(channel_id: nil, expanded: true)
|
||||
drawer?(expectation: true, channel_id: channel_id, expanded: expanded)
|
||||
end
|
||||
|
@ -18,6 +18,10 @@ module PageObjects
|
||||
input.value.blank?
|
||||
end
|
||||
|
||||
def enabled?
|
||||
component.has_css?(".chat-composer.is-enabled")
|
||||
end
|
||||
|
||||
def has_saved_draft?
|
||||
component.has_css?(".chat-composer.is-draft-saved")
|
||||
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
|
||||
end
|
||||
|
||||
def open_draft_channel
|
||||
find("#{VISIBLE_DRAWER} .open-draft-channel-page-btn").click
|
||||
end
|
||||
|
||||
def close
|
||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__close-btn").click
|
||||
end
|
||||
|
@ -11,13 +11,6 @@ module PageObjects
|
||||
find(".sidebar-section[data-section-name='chat-dms']")
|
||||
end
|
||||
|
||||
def open_draft_channel
|
||||
find(
|
||||
".sidebar-section[data-section-name='chat-dms'] .sidebar-section-header-button",
|
||||
visible: false,
|
||||
).click
|
||||
end
|
||||
|
||||
def open_browse
|
||||
find(
|
||||
".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) }
|
||||
|
||||
let(:chat) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
|
||||
before { chat_system_bootstrap }
|
||||
|
||||
@ -143,13 +144,7 @@ RSpec.describe "Visit channel", type: :system do
|
||||
it "allows to join it" do
|
||||
chat.visit_channel(dm_channel_1)
|
||||
|
||||
expect(page).to have_content(I18n.t("js.chat.channel_settings.join_channel"))
|
||||
end
|
||||
|
||||
it "shows a preview of the channel" do
|
||||
chat.visit_channel(dm_channel_1)
|
||||
|
||||
expect(chat).to have_message(message_1)
|
||||
expect(channel_page.composer).to be_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ module("Discourse Chat | Component | chat-user-avatar", function (hooks) {
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<ChatUserAvatar @chat={{this.chat}} @user={{this.user}} />`
|
||||
hbs`<ChatUserAvatar @showPresence={{true}} @chat={{this.chat}} @user={{this.user}} />`
|
||||
);
|
||||
|
||||
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…
x
Reference in New Issue
Block a user