DEV: start glimmer-ification and optimisations of chat plugin (#19531)
Note this is a very large PR, and some of it could have been splited, but keeping it one chunk made it to merge conflicts and to revert if necessary. Actual new code logic is also not that much, as most of the changes are removing js tests, adding system specs or moving things around. To make it possible this commit is doing the following changes: - converting (and adding new) existing js acceptances tests into system tests. This change was necessary to ensure as little regressions as possible while changing paradigm - moving away from store. Using glimmer and tracked properties requires to have class objects everywhere and as a result works well with models. However store/adapters are suffering from many bugs and limitations. As a workaround the `chat-api` and `chat-channels-manager` are an answer to this problem by encapsulating backend calls and frontend storage logic; while still using js models. - dropping `appEvents` as much as possible. Using tracked properties and a better local storage of channel models, allows to be much more reactive and doesn’t require arbitrary manual updates everywhere in the app. - while working on replacing store, the existing work of a chat api (backend) has been continued to support more cases. - removing code from the `chat` service to separate concerns, `chat-subscriptions-manager` and `chat-channels-manager`, being the largest examples of where the code has been rewritten/moved. Future wok: - improve behavior when closing/deleting a channel, it's already slightly buggy on live, it's rare enough that it's not a big issue, but should be improved - improve page objects used in chat - move more endpoints to the API - finish temporarily skipped tests - extract more code from the `chat` service - use glimmer for `chat-messages` - separate concerns in `chat-live-pane` - eventually add js tests for `chat-api`, `chat-channels-manager` and `chat-subscriptions-manager`, they are indirectly heavy tested through system tests but it would be nice to at least test the public API <!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
parent
269b6177c1
commit
d2e24f9569
|
@ -58,7 +58,11 @@ export default {
|
|||
this.onDoNotDisturb
|
||||
);
|
||||
|
||||
this.messageBus.subscribe(`/user-status`, this.onUserStatus);
|
||||
this.messageBus.subscribe(
|
||||
`/user-status`,
|
||||
this.onUserStatus,
|
||||
this.currentUser.status?.message_bus_last_id
|
||||
);
|
||||
|
||||
this.messageBus.subscribe("/categories", this.onCategories);
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserStatusSerializer < ApplicationSerializer
|
||||
attributes :description, :emoji, :ends_at
|
||||
attributes :description, :emoji, :ends_at, :message_bus_last_id
|
||||
|
||||
def message_bus_last_id
|
||||
MessageBus.last_id("/user-status")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelMembershipsController < Chat::Api::ChatChannelsController
|
||||
def index
|
||||
channel = find_chat_channel
|
||||
|
||||
offset = (params[:offset] || 0).to_i
|
||||
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
||||
|
||||
memberships =
|
||||
ChatChannelMembershipsQuery.call(
|
||||
channel,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
username: params[:username],
|
||||
)
|
||||
|
||||
render_serialized(memberships, UserChatChannelMembershipSerializer, root: false)
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
|
||||
|
||||
class Chat::Api::ChatChannelNotificationsSettingsController < Chat::Api::ChatChannelsController
|
||||
def update
|
||||
settings_params = params.permit(MEMBERSHIP_EDITABLE_PARAMS)
|
||||
membership = find_membership
|
||||
membership.update!(settings_params.to_h)
|
||||
render_serialized(membership, UserChatChannelMembershipSerializer, root: false)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsController
|
||||
def create
|
||||
existing_archive = channel_from_params.chat_channel_archive
|
||||
|
||||
if existing_archive.present?
|
||||
guardian.ensure_can_change_channel_status!(channel_from_params, :archived)
|
||||
raise Discourse::InvalidAccess if !existing_archive.failed?
|
||||
Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: channel_from_params)
|
||||
else
|
||||
archive_params =
|
||||
params
|
||||
.require(:archive)
|
||||
.tap do |ca|
|
||||
ca.require(:type)
|
||||
ca.permit(:title, :topic_id, :category_id, tags: [])
|
||||
end
|
||||
|
||||
new_topic = archive_params[:type] == "new_topic"
|
||||
raise Discourse::InvalidParameters if new_topic && archive_params[:title].blank?
|
||||
raise Discourse::InvalidParameters if !new_topic && archive_params[:topic_id].blank?
|
||||
|
||||
if !guardian.can_change_channel_status?(channel_from_params, :read_only)
|
||||
raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived"))
|
||||
end
|
||||
|
||||
Chat::ChatChannelArchiveService.begin_archive_process(
|
||||
chat_channel: channel_from_params,
|
||||
acting_user: current_user,
|
||||
topic_params: {
|
||||
topic_id: archive_params[:topic_id],
|
||||
topic_title: archive_params[:title],
|
||||
category_id: archive_params[:category_id],
|
||||
tags: archive_params[:tags],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
CHAT_CHANNEL_EDITABLE_PARAMS = %i[name description]
|
||||
CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
|
||||
CHANNEL_EDITABLE_PARAMS = %i[name description]
|
||||
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
|
||||
|
||||
class Chat::Api::ChatChannelsController < Chat::Api
|
||||
def index
|
||||
|
@ -9,6 +9,9 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||
params.permit(:filter, :limit, :offset),
|
||||
).symbolize_keys!
|
||||
|
||||
options[:offset] = options[:offset].to_i
|
||||
options[:limit] = (options[:limit] || 25).to_i
|
||||
|
||||
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
|
||||
channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options)
|
||||
|
||||
|
@ -20,73 +23,166 @@ class Chat::Api::ChatChannelsController < Chat::Api
|
|||
membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
|
||||
)
|
||||
end
|
||||
render json: serialized_channels, root: false
|
||||
|
||||
pagination_options =
|
||||
options.slice(:offset, :limit, :filter).merge(offset: options[:offset] + options[:limit])
|
||||
pagination_params = pagination_options.map { |k, v| "#{k}=#{v}" }.join("&")
|
||||
render json: serialized_channels,
|
||||
root: "channels",
|
||||
meta: {
|
||||
load_more_url: "/chat/api/channels?#{pagination_params}",
|
||||
}
|
||||
end
|
||||
|
||||
def destroy
|
||||
confirmation = params.require(:channel).require(:name_confirmation)&.downcase
|
||||
guardian.ensure_can_delete_chat_channel!
|
||||
|
||||
if channel_from_params.title(current_user).downcase != confirmation
|
||||
raise Discourse::InvalidParameters.new(:name_confirmation)
|
||||
end
|
||||
|
||||
begin
|
||||
ChatChannel.transaction do
|
||||
channel_from_params.trash!(current_user)
|
||||
StaffActionLogger.new(current_user).log_custom(
|
||||
"chat_channel_delete",
|
||||
{
|
||||
chat_channel_id: channel_from_params.id,
|
||||
chat_channel_name: channel_from_params.title(current_user),
|
||||
},
|
||||
)
|
||||
end
|
||||
rescue ActiveRecord::Rollback
|
||||
return render_json_error(I18n.t("chat.errors.delete_channel_failed"))
|
||||
end
|
||||
|
||||
Jobs.enqueue(:chat_channel_delete, { chat_channel_id: channel_from_params.id })
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def create
|
||||
channel_params =
|
||||
params.require(:channel).permit(:chatable_id, :name, :description, :auto_join_users)
|
||||
|
||||
guardian.ensure_can_create_chat_channel!
|
||||
if channel_params[:name].length > SiteSetting.max_topic_title_length
|
||||
raise Discourse::InvalidParameters.new(:name)
|
||||
end
|
||||
|
||||
if ChatChannel.exists?(
|
||||
chatable_type: "Category",
|
||||
chatable_id: channel_params[:chatable_id],
|
||||
name: channel_params[:name],
|
||||
)
|
||||
raise Discourse::InvalidParameters.new(I18n.t("chat.errors.channel_exists_for_category"))
|
||||
end
|
||||
|
||||
chatable = Category.find_by(id: channel_params[:chatable_id])
|
||||
raise Discourse::NotFound unless chatable
|
||||
|
||||
auto_join_users =
|
||||
ActiveRecord::Type::Boolean.new.deserialize(channel_params[:auto_join_users]) || false
|
||||
|
||||
channel =
|
||||
chatable.create_chat_channel!(
|
||||
name: channel_params[:name],
|
||||
description: channel_params[:description],
|
||||
user_count: 1,
|
||||
auto_join_users: auto_join_users,
|
||||
)
|
||||
|
||||
channel.user_chat_channel_memberships.create!(user: current_user, following: true)
|
||||
|
||||
if channel.auto_join_users
|
||||
Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
|
||||
end
|
||||
|
||||
render_serialized(
|
||||
channel,
|
||||
ChatChannelSerializer,
|
||||
membership: channel.membership_for(current_user),
|
||||
root: "channel",
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
render_serialized(
|
||||
channel_from_params,
|
||||
ChatChannelSerializer,
|
||||
membership: channel_from_params.membership_for(current_user),
|
||||
root: "channel",
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
guardian.ensure_can_edit_chat_channel!
|
||||
|
||||
chat_channel = find_chat_channel
|
||||
|
||||
if chat_channel.direct_message_channel?
|
||||
if channel_from_params.direct_message_channel?
|
||||
raise Discourse::InvalidParameters.new(
|
||||
I18n.t("chat.errors.cant_update_direct_message_channel"),
|
||||
)
|
||||
end
|
||||
|
||||
params_to_edit = editable_params(params, chat_channel)
|
||||
params_to_edit = editable_params(params, channel_from_params)
|
||||
params_to_edit.each { |k, v| params_to_edit[k] = nil if params_to_edit[k].blank? }
|
||||
|
||||
if ActiveRecord::Type::Boolean.new.deserialize(params_to_edit[:auto_join_users])
|
||||
auto_join_limiter(chat_channel).performed!
|
||||
auto_join_limiter(channel_from_params).performed!
|
||||
end
|
||||
|
||||
chat_channel.update!(params_to_edit)
|
||||
channel_from_params.update!(params_to_edit)
|
||||
|
||||
ChatPublisher.publish_chat_channel_edit(chat_channel, current_user)
|
||||
ChatPublisher.publish_chat_channel_edit(channel_from_params, current_user)
|
||||
|
||||
if chat_channel.category_channel? && chat_channel.auto_join_users
|
||||
Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
|
||||
if channel_from_params.category_channel? && channel_from_params.auto_join_users
|
||||
Chat::ChatChannelMembershipManager.new(
|
||||
channel_from_params,
|
||||
).enforce_automatic_channel_memberships
|
||||
end
|
||||
|
||||
render_serialized(
|
||||
chat_channel,
|
||||
channel_from_params,
|
||||
ChatChannelSerializer,
|
||||
root: false,
|
||||
membership: chat_channel.membership_for(current_user),
|
||||
root: "channel",
|
||||
membership: channel_from_params.membership_for(current_user),
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_chat_channel
|
||||
chat_channel = ChatChannel.find(params.require(:chat_channel_id))
|
||||
guardian.ensure_can_join_chat_channel!(chat_channel)
|
||||
chat_channel
|
||||
def channel_from_params
|
||||
@channel ||=
|
||||
begin
|
||||
channel = ChatChannel.find(params.require(:channel_id))
|
||||
guardian.ensure_can_preview_chat_channel!(channel)
|
||||
channel
|
||||
end
|
||||
end
|
||||
|
||||
def find_membership
|
||||
chat_channel = find_chat_channel
|
||||
membership = Chat::ChatChannelMembershipManager.new(chat_channel).find_for_user(current_user)
|
||||
raise Discourse::NotFound if membership.blank?
|
||||
membership
|
||||
def membership_from_params
|
||||
@membership ||=
|
||||
begin
|
||||
membership =
|
||||
Chat::ChatChannelMembershipManager.new(channel_from_params).find_for_user(current_user)
|
||||
raise Discourse::NotFound if membership.blank?
|
||||
membership
|
||||
end
|
||||
end
|
||||
|
||||
def auto_join_limiter(chat_channel)
|
||||
def auto_join_limiter(channel)
|
||||
RateLimiter.new(
|
||||
current_user,
|
||||
"auto_join_users_channel_#{chat_channel.id}",
|
||||
"auto_join_users_channel_#{channel.id}",
|
||||
1,
|
||||
3.minutes,
|
||||
apply_limit_to_staff: true,
|
||||
)
|
||||
end
|
||||
|
||||
def editable_params(params, chat_channel)
|
||||
permitted_params = CHAT_CHANNEL_EDITABLE_PARAMS
|
||||
|
||||
permitted_params += CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS if chat_channel.category_channel?
|
||||
|
||||
params.permit(*permitted_params)
|
||||
def editable_params(params, channel)
|
||||
permitted_params = CHANNEL_EDITABLE_PARAMS
|
||||
permitted_params += CATEGORY_CHANNEL_EDITABLE_PARAMS if channel.category_channel?
|
||||
params.require(:channel).permit(*permitted_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelsCurrentUserMembershipController < Chat::Api::ChatChannelsController
|
||||
def create
|
||||
guardian.ensure_can_join_chat_channel!(channel_from_params)
|
||||
|
||||
render_serialized(
|
||||
channel_from_params.add(current_user),
|
||||
UserChatChannelMembershipSerializer,
|
||||
root: "membership",
|
||||
)
|
||||
end
|
||||
|
||||
def destroy
|
||||
render_serialized(
|
||||
channel_from_params.remove(current_user),
|
||||
UserChatChannelMembershipSerializer,
|
||||
root: "membership",
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
|
||||
|
||||
class Chat::Api::ChatChannelsCurrentUserNotificationsSettingsController < Chat::Api::ChatChannelsController
|
||||
def update
|
||||
settings_params = params.require(:notifications_settings).permit(MEMBERSHIP_EDITABLE_PARAMS)
|
||||
membership_from_params.update!(settings_params.to_h)
|
||||
render_serialized(
|
||||
membership_from_params,
|
||||
UserChatChannelMembershipSerializer,
|
||||
root: "membership",
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelsMembershipsController < Chat::Api::ChatChannelsController
|
||||
def index
|
||||
params.permit(:username, :offset, :limit)
|
||||
|
||||
offset = params[:offset].to_i
|
||||
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
||||
|
||||
memberships =
|
||||
ChatChannelMembershipsQuery.call(
|
||||
channel_from_params,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
username: params[:username],
|
||||
)
|
||||
|
||||
render_serialized(
|
||||
memberships,
|
||||
UserChatChannelMembershipSerializer,
|
||||
root: "memberships",
|
||||
meta: {
|
||||
total_rows: channel_from_params.user_count,
|
||||
load_more_url:
|
||||
"/chat/api/channels/#{channel_from_params.id}/memberships?offset=#{offset + limit}&limit=#{limit}&username=#{params[:username]}",
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelsMessagesMovesController < Chat::Api::ChatChannelsController
|
||||
def create
|
||||
move_params = params.require(:move)
|
||||
move_params.require(:message_ids)
|
||||
move_params.require(:destination_channel_id)
|
||||
|
||||
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(channel_from_params)
|
||||
destination_channel =
|
||||
Chat::ChatChannelFetcher.find_with_access_check(
|
||||
move_params[:destination_channel_id],
|
||||
guardian,
|
||||
)
|
||||
|
||||
begin
|
||||
message_ids = move_params[:message_ids].map(&:to_i)
|
||||
moved_messages =
|
||||
Chat::MessageMover.new(
|
||||
acting_user: current_user,
|
||||
source_channel: channel_from_params,
|
||||
message_ids: message_ids,
|
||||
).move_to_channel(destination_channel)
|
||||
rescue Chat::MessageMover::NoMessagesFound, Chat::MessageMover::InvalidChannel => err
|
||||
return render_json_error(err.message)
|
||||
end
|
||||
|
||||
render json:
|
||||
success_json.merge(
|
||||
destination_channel_id: destination_channel.id,
|
||||
destination_channel_title: destination_channel.title(current_user),
|
||||
first_moved_message_id: moved_messages.first.id,
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChannelsStatusController < Chat::Api::ChatChannelsController
|
||||
def update
|
||||
status = params.require(:status)
|
||||
|
||||
# we only want to use this endpoint for open/closed status changes,
|
||||
# the others are more "special" and are handled by the archive endpoint
|
||||
if !ChatChannel.statuses.keys.include?(status) || status == "read_only" || status == "archive"
|
||||
raise Discourse::InvalidParameters
|
||||
end
|
||||
|
||||
guardian.ensure_can_change_channel_status!(channel_from_params, status.to_sym)
|
||||
channel_from_params.public_send("#{status}!", current_user)
|
||||
|
||||
render_serialized(channel_from_params, ChatChannelSerializer, root: "channel")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatChatablesController < Chat::Api
|
||||
def index
|
||||
params.require(:filter)
|
||||
filter = params[:filter].downcase
|
||||
|
||||
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
|
||||
public_channels =
|
||||
Chat::ChatChannelFetcher.secured_public_channels(
|
||||
guardian,
|
||||
memberships,
|
||||
filter: filter,
|
||||
status: :open,
|
||||
)
|
||||
|
||||
users = User.joins(:user_option).where.not(id: current_user.id)
|
||||
if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
|
||||
users =
|
||||
users
|
||||
.joins(:groups)
|
||||
.where(groups: { id: Chat.allowed_group_ids })
|
||||
.or(users.joins(:groups).staff)
|
||||
end
|
||||
|
||||
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
|
||||
ChatChannel
|
||||
.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,
|
||||
},
|
||||
ChatChannelSearchSerializer,
|
||||
root: false,
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChatCurrentUserChannelsController < Chat::Api
|
||||
def index
|
||||
structured = Chat::ChatChannelFetcher.structured(guardian)
|
||||
render_serialized(structured, ChatChannelIndexSerializer, root: false)
|
||||
end
|
||||
end
|
|
@ -1,250 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::ChatChannelsController < Chat::ChatBaseController
|
||||
before_action :set_channel_and_chatable_with_access_check, except: %i[index create search]
|
||||
|
||||
def index
|
||||
structured = Chat::ChatChannelFetcher.structured(guardian)
|
||||
render_serialized(structured, ChatChannelIndexSerializer, root: false)
|
||||
end
|
||||
|
||||
def show
|
||||
render_serialized(
|
||||
@chat_channel,
|
||||
ChatChannelSerializer,
|
||||
membership: @chat_channel.membership_for(current_user),
|
||||
root: false,
|
||||
)
|
||||
end
|
||||
|
||||
def follow
|
||||
membership = @chat_channel.add(current_user)
|
||||
|
||||
render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false)
|
||||
end
|
||||
|
||||
def unfollow
|
||||
membership = @chat_channel.remove(current_user)
|
||||
|
||||
render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false)
|
||||
end
|
||||
|
||||
def create
|
||||
params.require(%i[id name])
|
||||
guardian.ensure_can_create_chat_channel!
|
||||
if params[:name].length > SiteSetting.max_topic_title_length
|
||||
raise Discourse::InvalidParameters.new(:name)
|
||||
end
|
||||
|
||||
exists =
|
||||
ChatChannel.exists?(chatable_type: "Category", chatable_id: params[:id], name: params[:name])
|
||||
if exists
|
||||
raise Discourse::InvalidParameters.new(I18n.t("chat.errors.channel_exists_for_category"))
|
||||
end
|
||||
|
||||
chatable = Category.find_by(id: params[:id])
|
||||
raise Discourse::NotFound unless chatable
|
||||
|
||||
auto_join_users = ActiveRecord::Type::Boolean.new.deserialize(params[:auto_join_users]) || false
|
||||
|
||||
chat_channel =
|
||||
chatable.create_chat_channel!(
|
||||
name: params[:name],
|
||||
description: params[:description],
|
||||
user_count: 1,
|
||||
auto_join_users: auto_join_users,
|
||||
)
|
||||
chat_channel.user_chat_channel_memberships.create!(user: current_user, following: true)
|
||||
|
||||
if chat_channel.auto_join_users
|
||||
Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
|
||||
end
|
||||
|
||||
render_serialized(
|
||||
chat_channel,
|
||||
ChatChannelSerializer,
|
||||
membership: chat_channel.membership_for(current_user),
|
||||
)
|
||||
end
|
||||
|
||||
def edit
|
||||
guardian.ensure_can_edit_chat_channel!
|
||||
if (params[:name]&.length || 0) > SiteSetting.max_topic_title_length
|
||||
raise Discourse::InvalidParameters.new(:name)
|
||||
end
|
||||
|
||||
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
|
||||
raise Discourse::NotFound unless chat_channel
|
||||
|
||||
chat_channel.name = params[:name] if params[:name]
|
||||
chat_channel.description = params[:description] if params[:description]
|
||||
chat_channel.save!
|
||||
|
||||
ChatPublisher.publish_chat_channel_edit(chat_channel, current_user)
|
||||
render_serialized(
|
||||
chat_channel,
|
||||
ChatChannelSerializer,
|
||||
membership: chat_channel.membership_for(current_user),
|
||||
)
|
||||
end
|
||||
|
||||
def search
|
||||
params.require(:filter)
|
||||
filter = params[:filter]&.downcase
|
||||
memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
|
||||
public_channels =
|
||||
Chat::ChatChannelFetcher.secured_public_channels(
|
||||
guardian,
|
||||
memberships,
|
||||
filter: filter,
|
||||
status: :open,
|
||||
)
|
||||
|
||||
users = User.joins(:user_option).where.not(id: current_user.id)
|
||||
if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
|
||||
users =
|
||||
users
|
||||
.joins(:groups)
|
||||
.where(groups: { id: Chat.allowed_group_ids })
|
||||
.or(users.joins(:groups).staff)
|
||||
end
|
||||
|
||||
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
|
||||
ChatChannel
|
||||
.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,
|
||||
},
|
||||
ChatChannelSearchSerializer,
|
||||
root: false,
|
||||
)
|
||||
end
|
||||
|
||||
def archive
|
||||
params.require(:type)
|
||||
|
||||
if params[:type] == "newTopic" ? params[:title].blank? : params[:topic_id].blank?
|
||||
raise Discourse::InvalidParameters
|
||||
end
|
||||
|
||||
if !guardian.can_change_channel_status?(@chat_channel, :read_only)
|
||||
raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived"))
|
||||
end
|
||||
|
||||
Chat::ChatChannelArchiveService.begin_archive_process(
|
||||
chat_channel: @chat_channel,
|
||||
acting_user: current_user,
|
||||
topic_params: {
|
||||
topic_id: params[:topic_id],
|
||||
topic_title: params[:title],
|
||||
category_id: params[:category_id],
|
||||
tags: params[:tags],
|
||||
},
|
||||
)
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def retry_archive
|
||||
guardian.ensure_can_change_channel_status!(@chat_channel, :archived)
|
||||
|
||||
archive = @chat_channel.chat_channel_archive
|
||||
raise Discourse::NotFound if archive.blank?
|
||||
raise Discourse::InvalidAccess if !archive.failed?
|
||||
|
||||
Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: @chat_channel)
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def change_status
|
||||
params.require(:status)
|
||||
|
||||
# we only want to use this endpoint for open/closed status changes,
|
||||
# the others are more "special" and are handled by the archive endpoint
|
||||
if !ChatChannel.statuses.keys.include?(params[:status]) || params[:status] == "read_only" ||
|
||||
params[:status] == "archive"
|
||||
raise Discourse::InvalidParameters
|
||||
end
|
||||
|
||||
guardian.ensure_can_change_channel_status!(@chat_channel, params[:status].to_sym)
|
||||
@chat_channel.public_send("#{params[:status]}!", current_user)
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def destroy
|
||||
params.require(:channel_name_confirmation)
|
||||
|
||||
guardian.ensure_can_delete_chat_channel!
|
||||
|
||||
if @chat_channel.title(current_user).downcase != params[:channel_name_confirmation].downcase
|
||||
raise Discourse::InvalidParameters.new(:channel_name_confirmation)
|
||||
end
|
||||
|
||||
begin
|
||||
ChatChannel.transaction do
|
||||
@chat_channel.trash!(current_user)
|
||||
StaffActionLogger.new(current_user).log_custom(
|
||||
"chat_channel_delete",
|
||||
{
|
||||
chat_channel_id: @chat_channel.id,
|
||||
chat_channel_name: @chat_channel.title(current_user),
|
||||
},
|
||||
)
|
||||
end
|
||||
rescue ActiveRecord::Rollback
|
||||
return render_json_error(I18n.t("chat.errors.delete_channel_failed"))
|
||||
end
|
||||
|
||||
Jobs.enqueue(:chat_channel_delete, { chat_channel_id: @chat_channel.id })
|
||||
render json: success_json
|
||||
end
|
||||
end
|
|
@ -110,7 +110,9 @@ class Chat::ChatController < Chat::ChatBaseController
|
|||
|
||||
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
|
||||
|
||||
@user_chat_channel_membership.update(last_read_message_id: chat_message_creator.chat_message.id)
|
||||
@user_chat_channel_membership.update!(
|
||||
last_read_message_id: chat_message_creator.chat_message.id,
|
||||
)
|
||||
|
||||
if @chat_channel.direct_message_channel?
|
||||
# If any of the channel users is ignoring, muting, or preventing DMs from
|
||||
|
@ -123,14 +125,15 @@ class Chat::ChatController < Chat::ChatBaseController
|
|||
).allowing_actor_communication
|
||||
|
||||
if user_ids_allowing_communication.any?
|
||||
@chat_channel
|
||||
.user_chat_channel_memberships
|
||||
.where(user_id: user_ids_allowing_communication)
|
||||
.update_all(following: true)
|
||||
ChatPublisher.publish_new_channel(
|
||||
@chat_channel,
|
||||
@chat_channel.chatable.users.where(id: user_ids_allowing_communication),
|
||||
)
|
||||
|
||||
@chat_channel
|
||||
.user_chat_channel_memberships
|
||||
.where(user_id: user_ids_allowing_communication)
|
||||
.update_all(following: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -398,34 +401,6 @@ class Chat::ChatController < Chat::ChatBaseController
|
|||
render json: success_json.merge(markdown: markdown)
|
||||
end
|
||||
|
||||
def move_messages_to_channel
|
||||
params.require(:message_ids)
|
||||
params.require(:destination_channel_id)
|
||||
|
||||
raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(@chat_channel)
|
||||
destination_channel =
|
||||
Chat::ChatChannelFetcher.find_with_access_check(params[:destination_channel_id], guardian)
|
||||
|
||||
begin
|
||||
message_ids = params[:message_ids].map(&:to_i)
|
||||
moved_messages =
|
||||
Chat::MessageMover.new(
|
||||
acting_user: current_user,
|
||||
source_channel: @chat_channel,
|
||||
message_ids: message_ids,
|
||||
).move_to_channel(destination_channel)
|
||||
rescue Chat::MessageMover::NoMessagesFound, Chat::MessageMover::InvalidChannel => err
|
||||
return render_json_error(err.message)
|
||||
end
|
||||
|
||||
render json:
|
||||
success_json.merge(
|
||||
destination_channel_id: destination_channel.id,
|
||||
destination_channel_title: destination_channel.title(current_user),
|
||||
first_moved_message_id: moved_messages.first.id,
|
||||
)
|
||||
end
|
||||
|
||||
def flag
|
||||
RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed!
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class Chat::DirectMessagesController < Chat::ChatBaseController
|
|||
render_serialized(
|
||||
chat_channel,
|
||||
ChatChannelSerializer,
|
||||
root: "chat_channel",
|
||||
root: "channel",
|
||||
membership: chat_channel.membership_for(current_user),
|
||||
)
|
||||
rescue Chat::DirectMessageChannelCreator::NotAllowed => err
|
||||
|
@ -31,7 +31,7 @@ class Chat::DirectMessagesController < Chat::ChatBaseController
|
|||
render_serialized(
|
||||
chat_channel,
|
||||
ChatChannelSerializer,
|
||||
root: "chat_channel",
|
||||
root: "channel",
|
||||
membership: chat_channel.membership_for(current_user),
|
||||
)
|
||||
else
|
||||
|
|
|
@ -15,7 +15,19 @@ module Jobs
|
|||
return
|
||||
end
|
||||
|
||||
return if channel_archive.complete?
|
||||
if channel_archive.complete?
|
||||
channel_archive.chat_channel.update!(status: :archived)
|
||||
|
||||
ChatPublisher.publish_archive_status(
|
||||
channel_archive.chat_channel,
|
||||
archive_status: :success,
|
||||
archived_messages: channel_archive.archived_messages,
|
||||
archive_topic_id: channel_archive.destination_topic_id,
|
||||
total_messages: channel_archive.total_messages,
|
||||
)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
DistributedMutex.synchronize(
|
||||
"archive_chat_channel_#{channel_archive.chat_channel_id}",
|
||||
|
|
|
@ -20,7 +20,7 @@ class ChatChannelSerializer < ApplicationSerializer
|
|||
:archive_topic_id,
|
||||
:memberships_count,
|
||||
:current_user_membership,
|
||||
:message_bus_last_ids
|
||||
:meta
|
||||
|
||||
def initialize(object, opts)
|
||||
super(object, opts)
|
||||
|
@ -61,7 +61,7 @@ class ChatChannelSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def include_archive_status?
|
||||
scope.is_staff? && object.archived? && archive.present?
|
||||
scope.is_staff? && (object.archived? || archive&.failed?) && archive.present?
|
||||
end
|
||||
|
||||
def archive_completed
|
||||
|
@ -88,8 +88,11 @@ class ChatChannelSerializer < ApplicationSerializer
|
|||
scope.can_edit_chat_channel?
|
||||
end
|
||||
|
||||
def include_current_user_membership?
|
||||
@current_user_membership.present?
|
||||
end
|
||||
|
||||
def current_user_membership
|
||||
return if !@current_user_membership
|
||||
@current_user_membership.chat_channel = object
|
||||
UserChatChannelMembershipSerializer.new(
|
||||
@current_user_membership,
|
||||
|
@ -98,10 +101,17 @@ class ChatChannelSerializer < ApplicationSerializer
|
|||
).as_json
|
||||
end
|
||||
|
||||
def message_bus_last_ids
|
||||
def meta
|
||||
{
|
||||
new_messages: @opts[:new_messages_message_bus_last_id] || MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)),
|
||||
new_mentions: @opts[:new_mentions_message_bus_last_id] || MessageBus.last_id(ChatPublisher.new_mentions_message_bus_channel(object.id)),
|
||||
message_bus_last_ids: {
|
||||
new_messages:
|
||||
@opts[:new_messages_message_bus_last_id] ||
|
||||
MessageBus.last_id(ChatPublisher.new_messages_message_bus_channel(object.id)),
|
||||
new_mentions:
|
||||
@opts[:new_mentions_message_bus_last_id] ||
|
||||
MessageBus.last_id(ChatPublisher.new_mentions_message_bus_channel(object.id)),
|
||||
archive_status: MessageBus.last_id("/chat/channel-archive-status"),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StructuredChannelSerializer < ApplicationSerializer
|
||||
attributes :public_channels, :direct_message_channels, :message_bus_last_ids
|
||||
attributes :public_channels, :direct_message_channels, :meta
|
||||
|
||||
def public_channels
|
||||
object[:public_channels].map do |channel|
|
||||
|
@ -10,8 +10,10 @@ class StructuredChannelSerializer < ApplicationSerializer
|
|||
root: nil,
|
||||
scope: scope,
|
||||
membership: channel_membership(channel.id),
|
||||
new_messages_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)],
|
||||
new_mentions_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)]
|
||||
new_messages_message_bus_last_id:
|
||||
chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)],
|
||||
new_mentions_message_bus_last_id:
|
||||
chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -23,8 +25,10 @@ class StructuredChannelSerializer < ApplicationSerializer
|
|||
root: nil,
|
||||
scope: scope,
|
||||
membership: channel_membership(channel.id),
|
||||
new_messages_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)],
|
||||
new_mentions_message_bus_last_id: chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)]
|
||||
new_messages_message_bus_last_id:
|
||||
chat_message_bus_last_ids[ChatPublisher.new_messages_message_bus_channel(channel.id)],
|
||||
new_mentions_message_bus_last_id:
|
||||
chat_message_bus_last_ids[ChatPublisher.new_mentions_message_bus_channel(channel.id)],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -34,47 +38,54 @@ class StructuredChannelSerializer < ApplicationSerializer
|
|||
object[:memberships].find { |membership| membership.chat_channel_id == channel_id }
|
||||
end
|
||||
|
||||
def message_bus_last_ids
|
||||
ids = {
|
||||
channel_metadata: chat_message_bus_last_ids[ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL],
|
||||
def meta
|
||||
last_ids = {
|
||||
channel_metadata:
|
||||
chat_message_bus_last_ids[ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL],
|
||||
channel_edits: chat_message_bus_last_ids[ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL],
|
||||
channel_status: chat_message_bus_last_ids[ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL],
|
||||
new_channel: chat_message_bus_last_ids[ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL]
|
||||
new_channel: chat_message_bus_last_ids[ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL],
|
||||
}
|
||||
|
||||
if id = chat_message_bus_last_ids[ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id)]
|
||||
ids[:user_tracking_state] = id
|
||||
if id =
|
||||
chat_message_bus_last_ids[
|
||||
ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id)
|
||||
]
|
||||
last_ids[:user_tracking_state] = id
|
||||
end
|
||||
|
||||
ids
|
||||
{ message_bus_last_ids: last_ids }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def chat_message_bus_last_ids
|
||||
@chat_message_bus_last_ids ||= begin
|
||||
message_bus_channels = [
|
||||
ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL,
|
||||
]
|
||||
@chat_message_bus_last_ids ||=
|
||||
begin
|
||||
message_bus_channels = [
|
||||
ChatPublisher::CHANNEL_METADATA_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::CHANNEL_EDITS_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::CHANNEL_STATUS_MESSAGE_BUS_CHANNEL,
|
||||
ChatPublisher::NEW_CHANNEL_MESSAGE_BUS_CHANNEL,
|
||||
]
|
||||
|
||||
if !scope.anonymous?
|
||||
message_bus_channels.push(ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id))
|
||||
if !scope.anonymous?
|
||||
message_bus_channels.push(
|
||||
ChatPublisher.user_tracking_state_message_bus_channel(scope.user.id),
|
||||
)
|
||||
end
|
||||
|
||||
object[:public_channels].each do |channel|
|
||||
message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id))
|
||||
message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id))
|
||||
end
|
||||
|
||||
object[:direct_message_channels].each do |channel|
|
||||
message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id))
|
||||
message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id))
|
||||
end
|
||||
|
||||
MessageBus.last_ids(*message_bus_channels)
|
||||
end
|
||||
|
||||
object[:public_channels].each do |channel|
|
||||
message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id))
|
||||
message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id))
|
||||
end
|
||||
|
||||
object[:direct_message_channels].each do |channel|
|
||||
message_bus_channels.push(ChatPublisher.new_messages_message_bus_channel(channel.id))
|
||||
message_bus_channels.push(ChatPublisher.new_mentions_message_bus_channel(channel.id))
|
||||
end
|
||||
|
||||
MessageBus.last_ids(*message_bus_channels)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ module ChatPublisher
|
|||
MessageBus.publish(
|
||||
self.new_messages_message_bus_channel(chat_channel.id),
|
||||
{
|
||||
channel_id: chat_channel.id,
|
||||
message_id: chat_message.id,
|
||||
user_id: chat_message.user.id,
|
||||
username: chat_message.user.username,
|
||||
|
@ -145,7 +146,7 @@ module ChatPublisher
|
|||
def self.publish_new_mention(user_id, chat_channel_id, chat_message_id)
|
||||
MessageBus.publish(
|
||||
self.new_mentions_message_bus_channel(chat_channel_id),
|
||||
{ message_id: chat_message_id }.as_json,
|
||||
{ message_id: chat_message_id, channel_id: chat_channel_id }.as_json,
|
||||
user_ids: [user_id],
|
||||
)
|
||||
end
|
||||
|
@ -154,13 +155,20 @@ module ChatPublisher
|
|||
|
||||
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)
|
||||
|
||||
# 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 =
|
||||
ChatChannelSerializer.new(
|
||||
chat_channel,
|
||||
scope: Guardian.new(user), # We need a guardian here for direct messages
|
||||
root: :chat_channel,
|
||||
membership: chat_channel.membership_for(user),
|
||||
root: :channel,
|
||||
membership: membership,
|
||||
).as_json
|
||||
|
||||
MessageBus.publish(NEW_CHANNEL_MESSAGE_BUS_CHANNEL, serialized_channel, user_ids: [user.id])
|
||||
end
|
||||
end
|
||||
|
@ -179,9 +187,10 @@ module ChatPublisher
|
|||
type: :mention_warning,
|
||||
chat_message_id: chat_message.id,
|
||||
cannot_see: cannot_chat_users.map { |u| { username: u.username, id: u.id } }.as_json,
|
||||
without_membership: without_membership.map { |u| { username: u.username, id: u.id } }.as_json,
|
||||
without_membership:
|
||||
without_membership.map { |u| { username: u.username, id: u.id } }.as_json,
|
||||
groups_with_too_many_members: too_many_members.map(&:name).as_json,
|
||||
group_mentions_disabled: mentions_disabled.map(&:name).as_json
|
||||
group_mentions_disabled: mentions_disabled.map(&:name).as_json,
|
||||
},
|
||||
user_ids: [user_id],
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default class ChatMessage extends RESTAdapter {
|
||||
jsonMode = true;
|
||||
|
||||
pathFor(store, type, findArgs) {
|
||||
if (findArgs.targetMessageId) {
|
||||
return `/chat/lookup/${findArgs.targetMessageId}.json?chat_channel_id=${findArgs.channelId}`;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
{{#if (and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel)}}
|
||||
<LinkTo @route="chat.draft-channel" class="btn-flat open-draft-channel-page-btn keep-mobile-sidebar-open btn-floating" title={{i18n this.createDirectMessageChannelLabel}}>
|
||||
{{#if
|
||||
(and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel)
|
||||
}}
|
||||
<LinkTo
|
||||
@route="chat.draft-channel"
|
||||
class="btn-flat open-draft-channel-page-btn keep-mobile-sidebar-open btn-floating"
|
||||
title={{i18n this.createDirectMessageChannelLabel}}
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
@ -8,7 +14,10 @@
|
|||
role="region"
|
||||
aria-label={{i18n "chat.aria_roles.channels_list"}}
|
||||
class="channels-list"
|
||||
{{on "scroll" (if this.chatStateManager.isFullPageActive this.storeScrollPosition (noop))}}
|
||||
{{on
|
||||
"scroll"
|
||||
(if this.chatStateManager.isFullPageActive this.storeScrollPosition (noop))
|
||||
}}
|
||||
>
|
||||
{{#if this.displayPublicChannels}}
|
||||
<div class="chat-channel-divider public-channels-section">
|
||||
|
@ -26,7 +35,11 @@
|
|||
{{/if}}
|
||||
<span class="channel-title">{{i18n "chat.chat_channels"}}</span>
|
||||
|
||||
<LinkTo @route="chat.browse" class="btn no-text btn-flat open-browse-page-btn title-action" title={{i18n "chat.channels_list_popup.browse"}}>
|
||||
<LinkTo
|
||||
@route="chat.browse"
|
||||
class="btn no-text btn-flat open-browse-page-btn title-action"
|
||||
title={{i18n "chat.channels_list_popup.browse"}}
|
||||
>
|
||||
{{d-icon "pencil-alt"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
@ -40,7 +53,7 @@
|
|||
</LinkTo>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#each this.publicChannels as |channel|}}
|
||||
{{#each this.chatChannelsManager.publicMessageChannels as |channel|}}
|
||||
<ChatChannelRow
|
||||
@channel={{channel}}
|
||||
@options={{hash settingsButton=true}}
|
||||
|
@ -75,8 +88,17 @@
|
|||
{{/if}}
|
||||
<span class="channel-title">{{i18n "chat.direct_messages.title"}}</span>
|
||||
|
||||
{{#if (and this.canCreateDirectMessageChannel (not this.showMobileDirectMessageButton))}}
|
||||
<LinkTo @route="chat.draft-channel" class="btn no-text btn-flat open-draft-channel-page-btn" title={{i18n this.createDirectMessageChannelLabel}}>
|
||||
{{#if
|
||||
(and
|
||||
this.canCreateDirectMessageChannel
|
||||
(not this.showMobileDirectMessageButton)
|
||||
)
|
||||
}}
|
||||
<LinkTo
|
||||
@route="chat.draft-channel"
|
||||
class="btn no-text btn-flat open-draft-channel-page-btn"
|
||||
title={{i18n this.createDirectMessageChannelLabel}}
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
@ -84,11 +106,8 @@
|
|||
{{/if}}
|
||||
|
||||
<div id="direct-message-channels" class={{this.directMessageChannelClasses}}>
|
||||
{{#each this.sortedDirectMessageChannels as |channel|}}
|
||||
<ChatChannelRow
|
||||
@channel={{channel}}
|
||||
@options={{hash leaveButton=true}}
|
||||
/>
|
||||
{{#each this.chatChannelsManager.truncatedDirectMessageChannels as |channel|}}
|
||||
<ChatChannelRow @channel={{channel}} @options={{hash leaveButton=true}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,18 +3,18 @@ import Component from "@ember/component";
|
|||
import { action, computed } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { and, empty, reads } from "@ember/object/computed";
|
||||
import { and, empty } from "@ember/object/computed";
|
||||
|
||||
export default class ChannelsList extends Component {
|
||||
@service chat;
|
||||
@service router;
|
||||
@service chatStateManager;
|
||||
@service chatChannelsManager;
|
||||
tagName = "";
|
||||
inSidebar = false;
|
||||
toggleSection = null;
|
||||
@reads("chat.publicChannels.[]") publicChannels;
|
||||
@reads("chat.directMessageChannels.[]") directMessageChannels;
|
||||
@empty("publicChannels") publicChannelsEmpty;
|
||||
@empty("chatChannelsManager.publicMessageChannels")
|
||||
publicMessageChannelsEmpty;
|
||||
@and("site.mobileView", "showDirectMessageChannels")
|
||||
showMobileDirectMessageButton;
|
||||
|
||||
|
@ -27,11 +27,14 @@ export default class ChannelsList extends Component {
|
|||
return "chat.direct_messages.new";
|
||||
}
|
||||
|
||||
@computed("canCreateDirectMessageChannel", "directMessageChannels")
|
||||
@computed(
|
||||
"canCreateDirectMessageChannel",
|
||||
"chatChannelsManager.directMessageChannels"
|
||||
)
|
||||
get showDirectMessageChannels() {
|
||||
return (
|
||||
this.canCreateDirectMessageChannel ||
|
||||
this.directMessageChannels?.length > 0
|
||||
this.chatChannelsManager.directMessageChannels?.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -39,17 +42,6 @@ export default class ChannelsList extends Component {
|
|||
return this.chat.userCanDirectMessage;
|
||||
}
|
||||
|
||||
@computed("directMessageChannels.@each.last_message_sent_at")
|
||||
get sortedDirectMessageChannels() {
|
||||
if (!this.directMessageChannels?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.chat.truncateDirectMessageChannels(
|
||||
this.chat.sortDirectMessageChannels(this.directMessageChannels)
|
||||
);
|
||||
}
|
||||
|
||||
@computed("inSidebar")
|
||||
get publicChannelClasses() {
|
||||
return `channels-list-container public-channels ${
|
||||
|
@ -58,11 +50,11 @@ export default class ChannelsList extends Component {
|
|||
}
|
||||
|
||||
@computed(
|
||||
"publicChannelsEmpty",
|
||||
"publicMessageChannelsEmpty",
|
||||
"currentUser.{staff,has_joinable_public_channels}"
|
||||
)
|
||||
get displayPublicChannels() {
|
||||
if (this.publicChannelsEmpty) {
|
||||
if (this.publicMessageChannelsEmpty) {
|
||||
return (
|
||||
this.currentUser?.staff ||
|
||||
this.currentUser?.has_joinable_public_channels
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
{{#if this.chatProgressBarContainer}}
|
||||
{{#in-element this.chatProgressBarContainer}}
|
||||
<DProgressBar @key="browse-list" @isLoading={{this.isLoading}} />
|
||||
<DProgressBar @key="browse-list" @isLoading={{this.channelsCollection.loading}} />
|
||||
{{/in-element}}
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-browse-view__header chat-full-page-header">
|
||||
{{#if this.site.mobileView}}
|
||||
<LinkTo @route="chat.index" class="chat-full-page-header__back-btn no-text btn-flat btn" title={{i18n "chat.browse.back"}}>
|
||||
<LinkTo
|
||||
@route="chat.index"
|
||||
class="chat-full-page-header__back-btn no-text btn-flat btn"
|
||||
title={{i18n "chat.browse.back"}}
|
||||
>
|
||||
{{d-icon "chevron-left"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
@ -17,7 +21,10 @@
|
|||
<DButton
|
||||
@action={{action "createChannel"}}
|
||||
@icon="plus"
|
||||
@class={{concat-class "new-channel-btn" (if this.site.mobileView "btn-flat")}}
|
||||
@class={{concat-class
|
||||
"new-channel-btn"
|
||||
(if this.site.mobileView "btn-flat")
|
||||
}}
|
||||
@label={{if this.site.desktopView "chat.create_channel.title"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
@ -49,7 +56,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{{#if (and (not this.channels.length) (not this.isLoading))}}
|
||||
{{#if (and (not this.channelsCollection.length) (not this.channelsCollection.loading))}}
|
||||
<div class="empty-state">
|
||||
<span class="empty-state-title">{{i18n "chat.empty_state.title"}}</span>
|
||||
<div class="empty-state-body">
|
||||
|
@ -60,16 +67,16 @@
|
|||
</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
{{else if this.channels.length}}
|
||||
{{else if this.channelsCollection.length}}
|
||||
<div class="chat-browse-view__content_wrapper">
|
||||
<div class="chat-browse-view__content">
|
||||
<div class="chat-browse-view__cards">
|
||||
{{#each this.channels as |channel|}}
|
||||
{{#each this.channelsCollection as |channel|}}
|
||||
<ChatChannelCard @channel={{channel}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#unless this.isLoading}}
|
||||
{{#unless this.channelsCollection.loading}}
|
||||
<OnVisibilityAction @action={{action "onScroll"}} />
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
|
|
@ -1,62 +1,28 @@
|
|||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
const TABS = ["all", "open", "closed", "archived"];
|
||||
const PER_PAGE = 20;
|
||||
|
||||
export default class ChatBrowseView extends Component {
|
||||
@service router;
|
||||
@tracked isLoading = false;
|
||||
@tracked channels = [];
|
||||
@service chatApi;
|
||||
tagName = "";
|
||||
|
||||
offset = 0;
|
||||
canLoadMore = true;
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.channels = [];
|
||||
this.canLoadMore = true;
|
||||
this.offset = 0;
|
||||
this.fetchChannels();
|
||||
}
|
||||
|
||||
async fetchChannels(params) {
|
||||
if (this.isLoading || !this.canLoadMore) {
|
||||
return;
|
||||
if (!this.channelsCollection) {
|
||||
this.set("channelsCollection", this.chatApi.channels());
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const results = await ChatApi.chatChannels({
|
||||
limit: PER_PAGE,
|
||||
offset: this.offset,
|
||||
status: this.status,
|
||||
filter: this.filter,
|
||||
...params,
|
||||
});
|
||||
|
||||
if (results.length) {
|
||||
this.channels.pushObjects(results);
|
||||
}
|
||||
|
||||
if (results.length < PER_PAGE) {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} finally {
|
||||
this.offset = this.offset + PER_PAGE;
|
||||
this.isLoading = false;
|
||||
}
|
||||
this.channelsCollection.load({
|
||||
filter: this.filter,
|
||||
status: this.status,
|
||||
});
|
||||
}
|
||||
|
||||
@computed("siteSettings.chat_allow_archiving_channels")
|
||||
|
@ -74,19 +40,20 @@ export default class ChatBrowseView extends Component {
|
|||
|
||||
@action
|
||||
onScroll() {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
discourseDebounce(this, this.fetchChannels, INPUT_DELAY);
|
||||
discourseDebounce(
|
||||
this,
|
||||
this.channelsCollection.loadMore,
|
||||
{ filter: this.filter, status: this.status },
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
debouncedFiltering(event) {
|
||||
discourseDebounce(
|
||||
this,
|
||||
this.filterChannels,
|
||||
event.target.value,
|
||||
this.channelsCollection.load,
|
||||
{ filter: event.target.value, status: this.status },
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
@ -100,14 +67,4 @@ export default class ChatBrowseView extends Component {
|
|||
focusFilterInput(input) {
|
||||
schedule("afterRender", () => input?.focus());
|
||||
}
|
||||
|
||||
@bind
|
||||
filterChannels(filter) {
|
||||
this.canLoadMore = true;
|
||||
this.filter = filter;
|
||||
this.channels = [];
|
||||
this.offset = 0;
|
||||
|
||||
this.fetchChannels();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
<div class="chat-form__section">
|
||||
<ToggleChannelMembershipButton
|
||||
@channel={{this.channel}}
|
||||
@onToggle={{action "afterMembershipToggle"}}
|
||||
@options={{hash joinClass="btn-primary" leaveClass="btn-flat" joinIcon="sign-in-alt" leaveIcon="sign-out-alt"}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatChannelAboutView extends Component {
|
||||
|
@ -9,11 +8,4 @@ export default class ChatChannelAboutView extends Component {
|
|||
onEditChatChannelTitle = null;
|
||||
onEditChatChannelDescription = null;
|
||||
isLoading = false;
|
||||
|
||||
@action
|
||||
afterMembershipToggle() {
|
||||
this.chat.forceRefreshChannels().then(() => {
|
||||
this.chat.openChannel(this.channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { isEmpty } from "@ember/utils";
|
|||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { action } from "@ember/object";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import {
|
||||
|
@ -14,13 +13,15 @@ import {
|
|||
} from "discourse/plugins/chat/discourse/components/chat-to-topic-selector";
|
||||
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Component.extend({
|
||||
export default Component.extend(ModalFunctionality, {
|
||||
chat: service(),
|
||||
chatApi: service(),
|
||||
tagName: "",
|
||||
chatChannel: null,
|
||||
|
||||
selection: "newTopic",
|
||||
selection: NEW_TOPIC_SELECTION,
|
||||
newTopic: equal("selection", NEW_TOPIC_SELECTION),
|
||||
existingTopic: equal("selection", EXISTING_TOPIC_SELECTION),
|
||||
|
||||
|
@ -34,18 +35,12 @@ export default Component.extend({
|
|||
@action
|
||||
archiveChannel() {
|
||||
this.set("saving", true);
|
||||
return ajax({
|
||||
url: `/chat/chat_channels/${this.chatChannel.id}/archive.json`,
|
||||
type: "PUT",
|
||||
data: this._data(),
|
||||
})
|
||||
.then(() => {
|
||||
this.appEvents.trigger("modal-body:flash", {
|
||||
text: I18n.t("chat.channel_archive.process_started"),
|
||||
messageClass: "success",
|
||||
});
|
||||
|
||||
this.chatChannel.set("status", CHANNEL_STATUSES.archived);
|
||||
return this.chatApi
|
||||
.createChannelArchive(this.chatChannel.id, this._data())
|
||||
.then((result) => {
|
||||
this.flash(I18n.t("chat.channel_archive.process_started"), "success");
|
||||
result.target.status = CHANNEL_STATUSES.archived;
|
||||
|
||||
discourseLater(() => {
|
||||
this.closeModal();
|
||||
|
@ -58,7 +53,6 @@ export default Component.extend({
|
|||
_data() {
|
||||
const data = {
|
||||
type: this.selection,
|
||||
chat_channel_id: this.chatChannel.id,
|
||||
};
|
||||
if (this.newTopic) {
|
||||
data.title = this.topicTitle;
|
||||
|
|
|
@ -2,14 +2,15 @@ import Component from "@ember/component";
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import I18n from "I18n";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { action } from "@ember/object";
|
||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Component.extend({
|
||||
channel: null,
|
||||
tagName: "",
|
||||
chatApi: service(),
|
||||
|
||||
@discourseComputed(
|
||||
"channel.status",
|
||||
|
@ -43,26 +44,32 @@ export default Component.extend({
|
|||
|
||||
@action
|
||||
retryArchive() {
|
||||
return ajax({
|
||||
url: `/chat/chat_channels/${this.channel.id}/retry_archive.json`,
|
||||
type: "PUT",
|
||||
})
|
||||
.then(() => {
|
||||
this.channel.set("archive_failed", false);
|
||||
})
|
||||
return this.chatApi
|
||||
.createChannelArchive(this.channel.id)
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.currentUser.admin) {
|
||||
this.messageBus.subscribe("/chat/channel-archive-status", this.onMessage);
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-archive-status",
|
||||
this.onMessage,
|
||||
this.channel.meta.message_bus_last_ids.archive_status
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.messageBus.unsubscribe("/chat/channel-archive-status", this.onMessage);
|
||||
|
||||
if (this.currentUser.admin) {
|
||||
this.messageBus.unsubscribe(
|
||||
"/chat/channel-archive-status",
|
||||
this.onMessage
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_getTopicUrl() {
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
{{#if this.channel}}
|
||||
{{#if @channel}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-channel-card"
|
||||
(if this.channel.isClosed "-closed")
|
||||
(if this.channel.isArchived "-archived")
|
||||
(if @channel.isClosed "-closed")
|
||||
(if @channel.isArchived "-archived")
|
||||
}}
|
||||
style={{border-color this.channel.chatable.color}}
|
||||
style={{border-color @channel.chatable.color}}
|
||||
data-channel-id={{@channel.id}}
|
||||
>
|
||||
<div class="chat-channel-card__header">
|
||||
<LinkTo
|
||||
@route="chat.channel"
|
||||
@models={{array this.channel.id (slugify-channel this.channel)}}
|
||||
@models={{array @channel.id (slugify-channel @channel)}}
|
||||
class="chat-channel-card__name-container"
|
||||
>
|
||||
<span class="chat-channel-card__name">
|
||||
{{replace-emoji this.channel.escapedTitle}}
|
||||
{{replace-emoji @channel.escapedTitle}}
|
||||
</span>
|
||||
{{#if this.channel.chatable.read_restricted}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{d-icon "lock" class="chat-channel-card__read-restricted"}}
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="chat-channel-card__header-actions">
|
||||
{{#if this.channel.current_user_membership.muted}}
|
||||
{{#if @channel.currentUserMembership.muted}}
|
||||
<LinkTo
|
||||
@route="chat.channel.info.settings"
|
||||
@models={{array
|
||||
this.channel.id
|
||||
(slugify-channel this.channel)
|
||||
@channel.id
|
||||
(slugify-channel @channel)
|
||||
}}
|
||||
class="chat-channel-card__tag -muted"
|
||||
tabindex="-1"
|
||||
|
@ -38,7 +39,7 @@
|
|||
|
||||
<LinkTo
|
||||
@route="chat.channel.info.settings"
|
||||
@models={{array this.channel.id (slugify-channel this.channel)}}
|
||||
@models={{array @channel.id (slugify-channel @channel)}}
|
||||
class="chat-channel-card__setting"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
@ -47,32 +48,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.channel.description}}
|
||||
{{#if @channel.description}}
|
||||
<div class="chat-channel-card__description">
|
||||
{{replace-emoji this.channel.escapedDescription}}
|
||||
{{replace-emoji @channel.escapedDescription}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-card__cta">
|
||||
{{#if this.channel.isFollowing}}
|
||||
{{#if @channel.isFollowing}}
|
||||
<div class="chat-channel-card__tags">
|
||||
<span class="chat-channel-card__tag -joined">
|
||||
{{i18n "chat.joined"}}
|
||||
</span>
|
||||
|
||||
<ToggleChannelMembershipButton
|
||||
@channel={{this.channel}}
|
||||
@onToggle={{action "afterMembershipToggle"}}
|
||||
@channel={{@channel}}
|
||||
@options={{hash
|
||||
leaveClass="btn-link btn-small chat-channel-card__leave-btn"
|
||||
labelType="short"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{else if this.channel.isJoinable}}
|
||||
{{else if @channel.isJoinable}}
|
||||
<ToggleChannelMembershipButton
|
||||
@channel={{this.channel}}
|
||||
@onToggle={{action "afterMembershipToggle"}}
|
||||
@channel={{@channel}}
|
||||
@options={{hash
|
||||
joinClass="btn-primary btn-small chat-channel-card__join-btn"
|
||||
labelType="short"
|
||||
|
@ -80,16 +79,16 @@
|
|||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (gt this.channel.membershipsCount 0)}}
|
||||
{{#if (gt @channel.membershipsCount 0)}}
|
||||
<LinkTo
|
||||
@route="chat.channel.info.members"
|
||||
@models={{array this.channel.id (slugify-channel this.channel)}}
|
||||
@models={{array @channel.id (slugify-channel @channel)}}
|
||||
class="chat-channel-card__members"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{i18n
|
||||
"chat.channel.memberships_count"
|
||||
count=this.channel.membershipsCount
|
||||
count=@channel.membershipsCount
|
||||
}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatChannelCard extends Component {
|
||||
@service chat;
|
||||
tagName = "";
|
||||
|
||||
@action
|
||||
afterMembershipToggle() {
|
||||
this.chat.forceRefreshChannels();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,15 @@ import { isEmpty } from "@ember/utils";
|
|||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Component.extend({
|
||||
export default Component.extend(ModalFunctionality, {
|
||||
chat: service(),
|
||||
chatApi: service(),
|
||||
router: service(),
|
||||
tagName: "",
|
||||
chatChannel: null,
|
||||
|
@ -37,16 +38,14 @@ export default Component.extend({
|
|||
@action
|
||||
deleteChannel() {
|
||||
this.set("deleting", true);
|
||||
return ajax(`/chat/chat_channels/${this.chatChannel.id}.json`, {
|
||||
method: "DELETE",
|
||||
data: { channel_name_confirmation: this.channelNameConfirmation },
|
||||
})
|
||||
|
||||
return this.chatApi
|
||||
.destroyChannel(this.chatChannel.id, {
|
||||
name_confirmation: this.channelNameConfirmation,
|
||||
})
|
||||
.then(() => {
|
||||
this.set("confirmed", true);
|
||||
this.appEvents.trigger("modal-body:flash", {
|
||||
text: I18n.t("chat.channel_delete.process_started"),
|
||||
messageClass: "success",
|
||||
});
|
||||
this.flash(I18n.t("chat.channel_delete.process_started"), "success");
|
||||
|
||||
discourseLater(() => {
|
||||
this.closeModal();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{#if this.chatProgressBarContainer}}
|
||||
{{#in-element this.chatProgressBarContainer}}
|
||||
<DProgressBar @key="members-view" @isLoading={{this.isFetchingMembers}} />
|
||||
<DProgressBar @key="members-view" @isLoading={{this.members.loading}} />
|
||||
{{/in-element}}
|
||||
{{/if}}
|
||||
|
||||
|
@ -22,15 +22,15 @@
|
|||
>
|
||||
|
||||
<div role="list" class="channel-members-view__list">
|
||||
{{#each this.members as |member|}}
|
||||
{{#each this.members as |membership|}}
|
||||
<a
|
||||
class="channel-members-view__list-item"
|
||||
href={{member.user.userPath}}
|
||||
data-user-card={{member.user.username}}
|
||||
href={{membership.user.userPath}}
|
||||
data-user-card={{membership.user.username}}
|
||||
tabindex="0"
|
||||
>
|
||||
<ChatUserAvatar @user={{member.user}} @avatarSize="medium" />
|
||||
<ChatUserDisplayName @user={{member.user}} />
|
||||
<ChatUserAvatar @user={{membership.user}} @avatarSize="medium" />
|
||||
<ChatUserDisplayName @user={{membership.user}} />
|
||||
</a>
|
||||
{{else}}
|
||||
{{#unless this.isFetchingMembers}}
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
|
||||
const LIMIT = 50;
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatChannelMembersView extends Component {
|
||||
@service chatApi;
|
||||
|
||||
tagName = "";
|
||||
channel = null;
|
||||
members = null;
|
||||
isSearchFocused = false;
|
||||
isFetchingMembers = false;
|
||||
onlineUsers = null;
|
||||
offset = 0;
|
||||
filter = null;
|
||||
inputSelector = "channel-members-view__search-input";
|
||||
canLoadMore = true;
|
||||
members = null;
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
@ -28,14 +24,15 @@ export default class ChatChannelMembersView extends Component {
|
|||
}
|
||||
|
||||
this._focusSearch();
|
||||
this.set("members", []);
|
||||
this.fetchMembers();
|
||||
this.set("members", this.chatApi.listChannelMemberships(this.channel.id));
|
||||
this.members.load();
|
||||
|
||||
this.appEvents.on("chat:refresh-channel-members", this, "onFilterMembers");
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.off("chat:refresh-channel-members", this, "onFilterMembers");
|
||||
}
|
||||
|
||||
|
@ -46,59 +43,18 @@ export default class ChatChannelMembersView extends Component {
|
|||
@action
|
||||
onFilterMembers(username) {
|
||||
this.set("filter", username);
|
||||
this.set("offset", 0);
|
||||
this.set("canLoadMore", true);
|
||||
|
||||
discourseDebounce(
|
||||
this,
|
||||
this.fetchMembers,
|
||||
this.filter,
|
||||
this.offset,
|
||||
this.members.load,
|
||||
{ username: this.filter },
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
loadMore() {
|
||||
if (!this.canLoadMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
discourseDebounce(
|
||||
this,
|
||||
this.fetchMembers,
|
||||
this.filter,
|
||||
this.offset,
|
||||
INPUT_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
fetchMembersHandler(id, params = {}) {
|
||||
return ChatApi.chatChannelMemberships(id, params);
|
||||
}
|
||||
|
||||
fetchMembers(filter = null, offset = 0) {
|
||||
this.set("isFetchingMembers", true);
|
||||
|
||||
return this.fetchMembersHandler(this.channel.id, {
|
||||
username: filter,
|
||||
offset,
|
||||
})
|
||||
.then((response) => {
|
||||
if (this.offset === 0) {
|
||||
this.set("members", []);
|
||||
}
|
||||
|
||||
if (isEmpty(response)) {
|
||||
this.set("canLoadMore", false);
|
||||
} else {
|
||||
this.set("offset", this.offset + LIMIT);
|
||||
this.members.pushObjects(response);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("isFetchingMembers", false);
|
||||
});
|
||||
discourseDebounce(this, this.members.loadMore, INPUT_DELAY);
|
||||
}
|
||||
|
||||
_focusSearch() {
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
{{#if this.showJoinButton}}
|
||||
<ToggleChannelMembershipButton
|
||||
@channel={{this.channel}}
|
||||
@onToggle={{this.afterMembershipToggle}}
|
||||
@options={{hash joinClass="btn-primary"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { computed } from "@ember/object";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
|
@ -16,11 +16,4 @@ export default class ChatChannelPreviewCard extends Component {
|
|||
get hasDescription() {
|
||||
return !isEmpty(this.channel.description);
|
||||
}
|
||||
|
||||
@action
|
||||
afterMembershipToggle() {
|
||||
this.chat.forceRefreshChannels().then(() => {
|
||||
this.chat.openChannel(this.channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class={{concat-class
|
||||
"chat-channel-row"
|
||||
(if @channel.focused "focused")
|
||||
(if @channel.current_user_membership.muted "muted")
|
||||
(if @channel.currentUserMembership.muted "muted")
|
||||
(if @options.leaveButton "can-leave")
|
||||
(if (eq this.chat.activeChannel.id @channel.id) "active")
|
||||
(if this.channelHasUnread "has-unread")
|
||||
|
|
|
@ -19,11 +19,7 @@ export default class ChatChannelRow extends Component {
|
|||
}
|
||||
|
||||
get channelHasUnread() {
|
||||
return (
|
||||
this.currentUser.get(
|
||||
`chat_channel_tracking_state.${this.args.channel?.id}.unread_count`
|
||||
) > 0
|
||||
);
|
||||
return this.args.channel.currentUserMembership.unread_count > 0;
|
||||
}
|
||||
|
||||
get #firstDirectMessageUser() {
|
||||
|
|
|
@ -17,24 +17,24 @@ export default Component.extend({
|
|||
channels: null,
|
||||
searchIndex: 0,
|
||||
loading: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.on("chat-channel-selector-modal:close", this.close);
|
||||
this.getInitialChannels();
|
||||
},
|
||||
chatChannelsManager: service(),
|
||||
|
||||
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
|
||||
|
@ -101,16 +101,17 @@ export default Component.extend({
|
|||
switchChannel(channel) {
|
||||
if (channel.user) {
|
||||
return this.fetchOrCreateChannelForUser(channel).then((response) => {
|
||||
this.chat
|
||||
.startTrackingChannel(ChatChannel.create(response.chat_channel))
|
||||
.then((newlyTracked) => {
|
||||
this.chat.openChannel(newlyTracked);
|
||||
this.close();
|
||||
});
|
||||
const newChannel = this.chatChannelsManager.store(response.channel);
|
||||
return this.chatChannelsManager.follow(newChannel).then((c) => {
|
||||
this.chat.openChannel(c);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.chat.openChannel(channel);
|
||||
this.close();
|
||||
return this.chatChannelsManager.follow(channel).then((c) => {
|
||||
this.chat.openChannel(c);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -135,7 +136,7 @@ export default Component.extend({
|
|||
searchIndex: this.searchIndex + 1,
|
||||
});
|
||||
const thisSearchIndex = this.searchIndex;
|
||||
ajax("/chat/chat_channels/search", { data: { filter } })
|
||||
ajax("/chat/api/chatables", { data: { filter } })
|
||||
.then((searchModel) => {
|
||||
if (this.searchIndex === thisSearchIndex) {
|
||||
this.set("searchModel", searchModel);
|
||||
|
@ -149,7 +150,11 @@ export default Component.extend({
|
|||
}
|
||||
});
|
||||
this.setProperties({
|
||||
channels: channels.map((channel) => ChatChannel.create(channel)),
|
||||
channels: channels.map((channel) => {
|
||||
return channel.user
|
||||
? ChatChannel.create(channel)
|
||||
: this.chatChannelsManager.store(channel);
|
||||
}),
|
||||
loading: false,
|
||||
});
|
||||
this.focusFirstChannel(this.channels);
|
||||
|
@ -160,10 +165,9 @@ export default Component.extend({
|
|||
|
||||
@action
|
||||
getInitialChannels() {
|
||||
return this.chat.getChannelsWithFilter(this.filter).then((channels) => {
|
||||
this.focusFirstChannel(channels);
|
||||
this.set("channels", channels);
|
||||
});
|
||||
const channels = this.getChannelsWithFilter(this.filter);
|
||||
this.set("channels", channels);
|
||||
this.focusFirstChannel(channels);
|
||||
},
|
||||
|
||||
@action
|
||||
|
@ -178,4 +182,44 @@ export default Component.extend({
|
|||
channels.forEach((c) => c.set("focused", false));
|
||||
channels[0]?.set("focused", true);
|
||||
},
|
||||
|
||||
getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
|
||||
let sortedChannels = this.chatChannelsManager.channels.sort((a, b) => {
|
||||
return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at)
|
||||
? -1
|
||||
: 1;
|
||||
});
|
||||
|
||||
const trimmedFilter = filter.trim();
|
||||
const lowerCasedFilter = filter.toLowerCase();
|
||||
const { activeChannel } = this;
|
||||
|
||||
return sortedChannels.filter((channel) => {
|
||||
if (
|
||||
opts.excludeActiveChannel &&
|
||||
activeChannel &&
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<label class="chat-form__label">
|
||||
<span>{{i18n "chat.settings.mute"}}</span>
|
||||
<ChatChannelSettingsSavedIndicator
|
||||
@property={{this.channel.current_user_membership.muted}}
|
||||
@property={{this.channel.currentUserMembership.muted}}
|
||||
/>
|
||||
</label>
|
||||
<div class="chat-form__control">
|
||||
<ComboBox
|
||||
@content={{this.mutedOptions}}
|
||||
@value={{this.channel.current_user_membership.muted}}
|
||||
@value={{this.channel.currentUserMembership.muted}}
|
||||
@valueProperty="value"
|
||||
@class="channel-settings-view__muted-selector"
|
||||
@onChange={{action (fn this.saveNotificationSettings "muted")}}
|
||||
|
@ -17,18 +17,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#unless this.channel.current_user_membership.muted}}
|
||||
{{#unless this.channel.currentUserMembership.muted}}
|
||||
<div class="chat-form__field">
|
||||
<label class="chat-form__label">
|
||||
<span>{{i18n "chat.settings.desktop_notification_level"}}</span>
|
||||
<ChatChannelSettingsSavedIndicator
|
||||
@property={{this.channel.current_user_membership.desktop_notification_level}}
|
||||
@property={{this.channel.currentUserMembership.desktop_notification_level}}
|
||||
/>
|
||||
</label>
|
||||
<div class="chat-form__control">
|
||||
<ComboBox
|
||||
@content={{this.notificationLevels}}
|
||||
@value={{this.channel.current_user_membership.desktop_notification_level}}
|
||||
@value={{this.channel.currentUserMembership.desktop_notification_level}}
|
||||
@valueProperty="value"
|
||||
@class="channel-settings-view__desktop-notification-level-selector"
|
||||
@onChange={{action
|
||||
|
@ -42,13 +42,13 @@
|
|||
<label class="chat-form__label">
|
||||
<span>{{i18n "chat.settings.mobile_notification_level"}}</span>
|
||||
<ChatChannelSettingsSavedIndicator
|
||||
@property={{this.channel.current_user_membership.mobile_notification_level}}
|
||||
@property={{this.channel.currentUserMembership.mobile_notification_level}}
|
||||
/>
|
||||
</label>
|
||||
<div class="chat-form__control">
|
||||
<ComboBox
|
||||
@content={{this.notificationLevels}}
|
||||
@value={{this.channel.current_user_membership.mobile_notification_level}}
|
||||
@value={{this.channel.currentUserMembership.mobile_notification_level}}
|
||||
@valueProperty="value"
|
||||
@class="channel-settings-view__mobile-notification-level-selector"
|
||||
@onChange={{action
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import I18n from "I18n";
|
||||
import { Promise } from "rsvp";
|
||||
|
@ -33,6 +32,7 @@ const CHANNEL_WIDE_MENTIONS_OPTIONS = [
|
|||
|
||||
export default class ChatChannelSettingsView extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
@service chatGuardian;
|
||||
@service router;
|
||||
@service dialog;
|
||||
|
@ -86,16 +86,26 @@ export default class ChatChannelSettingsView extends Component {
|
|||
|
||||
const settings = {};
|
||||
settings[key] = value;
|
||||
return ChatApi.updateChatChannelNotificationsSettings(
|
||||
this.channel.id,
|
||||
settings
|
||||
).then((membership) => {
|
||||
this.channel.current_user_membership.setProperties({
|
||||
muted: membership.muted,
|
||||
desktop_notification_level: membership.desktop_notification_level,
|
||||
mobile_notification_level: membership.mobile_notification_level,
|
||||
return this.chatApi
|
||||
.updateCurrentUserChatChannelNotificationsSettings(
|
||||
this.channel.id,
|
||||
settings
|
||||
)
|
||||
.then((result) => {
|
||||
[
|
||||
"muted",
|
||||
"desktop_notification_level",
|
||||
"mobile_notification_level",
|
||||
].forEach((property) => {
|
||||
if (
|
||||
result.membership[property] !==
|
||||
this.channel.currentUserMembership[property]
|
||||
) {
|
||||
this.channel.currentUserMembership[property] =
|
||||
result.membership[property];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -155,9 +165,10 @@ export default class ChatChannelSettingsView extends Component {
|
|||
|
||||
const payload = {};
|
||||
payload[property] = value;
|
||||
return ChatApi.modifyChatChannel(channel.id, payload)
|
||||
.then((updatedChannel) => {
|
||||
channel.set(property, updatedChannel[property]);
|
||||
return this.chatApi
|
||||
.updateChannel(channel.id, payload)
|
||||
.then((result) => {
|
||||
channel.set(property, result.channel[property]);
|
||||
})
|
||||
.catch((event) => {
|
||||
if (event.jqXHR?.responseJSON?.errors) {
|
||||
|
|
|
@ -3,12 +3,12 @@ import { htmlSafe } from "@ember/template";
|
|||
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import I18n from "I18n";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default class ChatChannelToggleView extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
@service router;
|
||||
tagName = "";
|
||||
channel = null;
|
||||
|
@ -47,16 +47,11 @@ export default class ChatChannelToggleView extends Component {
|
|||
? CHANNEL_STATUSES.open
|
||||
: CHANNEL_STATUSES.closed;
|
||||
|
||||
return ajax(`/chat/chat_channels/${this.channel.id}/change_status.json`, {
|
||||
method: "PUT",
|
||||
data: { status },
|
||||
})
|
||||
.then(() => {
|
||||
this.channel.set("status", status);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
return this.chatApi
|
||||
.updateChannelStatus(this.channel.id, status)
|
||||
.finally(() => {
|
||||
this.onStatusChange?.(this.channel);
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
{{#if this.hasUnread}}
|
||||
<div class="chat-channel-unread-indicator {{if this.isUrgent "urgent"}}">
|
||||
<div class="number">{{this.unreadCount}}</div>
|
||||
{{#if (gt @channel.currentUserMembership.unread_count 0)}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"chat-channel-unread-indicator"
|
||||
(if
|
||||
(or
|
||||
@channel.isDirectMessageChannel
|
||||
(gt @channel.currentUserMembership.unread_mentions 0)
|
||||
)
|
||||
"urgent"
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div class="number">{{@channel.currentUserMembership.unread_count}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import { equal, gt } from "@ember/object/computed";
|
||||
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
channel: null,
|
||||
|
||||
isDirectMessage: equal(
|
||||
"channel.chatable_type",
|
||||
CHATABLE_TYPES.directMessageChannel
|
||||
),
|
||||
|
||||
hasUnread: gt("unreadCount", 0),
|
||||
|
||||
@discourseComputed(
|
||||
"currentUser.chat_channel_tracking_state.@each.{unread_count,unread_mentions}",
|
||||
"channel.id"
|
||||
)
|
||||
channelTrackingState(state, channelId) {
|
||||
return state?.[channelId];
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"channelTrackingState.unread_mentions",
|
||||
"channel",
|
||||
"isDirectMessage"
|
||||
)
|
||||
isUrgent(unreadMentions, channel, isDirectMessage) {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
return isDirectMessage || unreadMentions > 0;
|
||||
},
|
||||
|
||||
@discourseComputed("channelTrackingState.unread_count", "channel")
|
||||
unreadCount(unreadCount, channel) {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
return unreadCount || 0;
|
||||
},
|
||||
});
|
|
@ -34,7 +34,7 @@ export default class ChatDraftChannelScreen extends Component {
|
|||
this.set(
|
||||
"previewedChannel",
|
||||
ChatChannel.create(
|
||||
Object.assign({}, response.chat_channel, { isDraft: true })
|
||||
Object.assign({}, response.channel, { isDraft: true })
|
||||
)
|
||||
);
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ export default Component.extend({
|
|||
draftChannelView: equal("view", DRAFT_CHANNEL_VIEW),
|
||||
chat: service(),
|
||||
router: service(),
|
||||
chatChannelsManager: service(),
|
||||
chatStateManager: service(),
|
||||
loading: false,
|
||||
showClose: true, // TODO - false when on same topic
|
||||
|
@ -40,7 +41,6 @@ export default Component.extend({
|
|||
this,
|
||||
"openChannelAtMessage"
|
||||
);
|
||||
this.appEvents.on("chat:refresh-channels", this, "refreshChannels");
|
||||
this.appEvents.on("composer:closed", this, "_checkSize");
|
||||
this.appEvents.on("composer:opened", this, "_checkSize");
|
||||
this.appEvents.on("composer:resized", this, "_checkSize");
|
||||
|
@ -68,7 +68,6 @@ export default Component.extend({
|
|||
this,
|
||||
"openChannelAtMessage"
|
||||
);
|
||||
this.appEvents.off("chat:refresh-channels", this, "refreshChannels");
|
||||
this.appEvents.off("composer:closed", this, "_checkSize");
|
||||
this.appEvents.off("composer:opened", this, "_checkSize");
|
||||
this.appEvents.off("composer:resized", this, "_checkSize");
|
||||
|
@ -198,12 +197,9 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"chat.activeChannel",
|
||||
"currentUser.chat_channel_tracking_state"
|
||||
)
|
||||
unreadCount(activeChannel, trackingState) {
|
||||
return trackingState[activeChannel.id]?.unread_count || 0;
|
||||
@discourseComputed("chat.activeChannel.currentUserMembership.unread_count")
|
||||
unreadCount(count) {
|
||||
return count || 0;
|
||||
},
|
||||
|
||||
@action
|
||||
|
@ -218,7 +214,6 @@ export default Component.extend({
|
|||
switch (route.name) {
|
||||
case "chat":
|
||||
this.set("view", LIST_VIEW);
|
||||
this.refreshChannels();
|
||||
this.appEvents.trigger("chat:float-toggled", false);
|
||||
return;
|
||||
case "chat.draft-channel":
|
||||
|
@ -226,8 +221,8 @@ export default Component.extend({
|
|||
this.appEvents.trigger("chat:float-toggled", false);
|
||||
return;
|
||||
case "chat.channel":
|
||||
return this.chat
|
||||
.getChannelBy("id", route.params.channelId)
|
||||
return this.chatChannelsManager
|
||||
.find(route.params.channelId)
|
||||
.then((channel) => {
|
||||
this.chat.set("messageId", route.queryParams.messageId);
|
||||
this.chat.setActiveChannel(channel);
|
||||
|
@ -262,32 +257,6 @@ export default Component.extend({
|
|||
this.appEvents.trigger("chat:float-toggled", true);
|
||||
},
|
||||
|
||||
@action
|
||||
refreshChannels() {
|
||||
if (this.view === LIST_VIEW) {
|
||||
this.fetchChannels();
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
fetchChannels() {
|
||||
this.set("loading", true);
|
||||
|
||||
this.chat.getChannels().then(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
loading: false,
|
||||
view: LIST_VIEW,
|
||||
});
|
||||
|
||||
this.chatStateManager.didExpandDrawer();
|
||||
this.chat.setActiveChannel(null);
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
switchChannel(channel) {
|
||||
// we need next here to ensure we correctly let the time for routes transitions
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{{#if (gt this.chatChannelsManager.unreadUrgentCount 0)}}
|
||||
<div class="chat-channel-unread-indicator urgent">
|
||||
<div class="number-wrap">
|
||||
<div class="number">{{this.chatChannelsManager.unreadUrgentCount}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if (gt this.chatChannelsManager.unreadCount 0)}}
|
||||
<div class="chat-channel-unread-indicator"></div>
|
||||
{{/if}}
|
|
@ -0,0 +1,6 @@
|
|||
import { inject as service } from "@ember/service";
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatHeaderIconUnreadIndicator extends Component {
|
||||
@service chatChannelsManager;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{{#if (and this.chatStateManager.isFullPageActive this.site.desktopView)}}
|
||||
<span class={{concat-class "icon" (if this.isActive "active")}}>
|
||||
{{d-icon "comment"}}
|
||||
|
||||
{{#unless this.currentUserInDnD}}
|
||||
<ChatHeaderIconUnreadIndicator />
|
||||
{{/unless}}
|
||||
</span>
|
||||
{{else}}
|
||||
<a
|
||||
href={{this.href}}
|
||||
tabindex="0"
|
||||
class={{concat-class "icon" (if this.isActive "active")}}
|
||||
>
|
||||
{{d-icon "comment"}}
|
||||
|
||||
{{#unless this.currentUserInDnD}}
|
||||
<ChatHeaderIconUnreadIndicator />
|
||||
{{/unless}}
|
||||
</a>
|
||||
{{/if}}
|
|
@ -0,0 +1,31 @@
|
|||
import { inject as service } from "@ember/service";
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatHeaderIcon extends Component {
|
||||
@service currentUser;
|
||||
@service site;
|
||||
@service chatStateManager;
|
||||
|
||||
get currentUserInDnD() {
|
||||
return this.currentUser.isInDoNotDisturb();
|
||||
}
|
||||
|
||||
get href() {
|
||||
if (this.chatStateManager.isFullPageActive && this.site.mobileView) {
|
||||
return "/chat";
|
||||
}
|
||||
|
||||
if (this.chatStateManager.isDrawerActive) {
|
||||
return "/chat";
|
||||
} else {
|
||||
return this.chatStateManager.lastKnownChatURL || "/chat";
|
||||
}
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return (
|
||||
this.chatStateManager.isFullPageActive ||
|
||||
this.chatStateManager.isDrawerActive
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ChatChannelArchiveStatus @channel={{this.chatChannel}} />
|
||||
<ChatChannelStatus @channel={{this.chatChannel}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed, {
|
||||
|
@ -83,10 +81,12 @@ export default Component.extend({
|
|||
_mentionWarningsSeen: null, // Hash
|
||||
|
||||
chat: service(),
|
||||
chatChannelsManager: service(),
|
||||
router: service(),
|
||||
chatEmojiPickerManager: service(),
|
||||
chatComposerPresenceManager: service(),
|
||||
chatStateManager: service(),
|
||||
chatApi: service(),
|
||||
|
||||
getCachedChannelDetails: null,
|
||||
clearCachedChannelDetails: null,
|
||||
|
@ -546,8 +546,7 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
_getLastReadId() {
|
||||
return this.currentUser?.chat_channel_tracking_state?.[this.chatChannel.id]
|
||||
?.chat_message_id;
|
||||
return this.chatChannel.currentUserMembership.chat_message_id;
|
||||
},
|
||||
|
||||
_markLastReadMessage(opts = { reRender: false }) {
|
||||
|
@ -563,7 +562,6 @@ export default Component.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
this.set("lastSendReadMessageId", lastReadId);
|
||||
const indexOfLastReadMessage =
|
||||
this.messages.findIndex((m) => m.id === lastReadId) || 0;
|
||||
let newestUnreadMessage = this.messages[indexOfLastReadMessage + 1];
|
||||
|
@ -1009,7 +1007,8 @@ export default Component.extend({
|
|||
// Start ajax request but don't return here, we want to stage the message instantly when all messages are loaded.
|
||||
// Otherwise, we'll fetch latest and scroll to the one we just created.
|
||||
// Return a resolved promise below.
|
||||
const msgCreationPromise = ChatApi.sendMessage(this.chatChannel.id, data)
|
||||
const msgCreationPromise = this.chatApi
|
||||
.sendMessage(this.chatChannel.id, data)
|
||||
.catch((error) => {
|
||||
this._onSendError(data.staged_id, error);
|
||||
})
|
||||
|
@ -1047,33 +1046,25 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
async _upsertChannelWithMessage(channel, message, uploads) {
|
||||
let promise;
|
||||
let promise = Promise.resolve(channel);
|
||||
|
||||
if (channel.isDirectMessageChannel || channel.isDraft) {
|
||||
promise = this.chat.upsertDmChannelForUsernames(
|
||||
channel.chatable.users.mapBy("username")
|
||||
);
|
||||
} else {
|
||||
promise = ChatApi.loading(channel.id).then(() => channel);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then((c) => {
|
||||
c.current_user_membership.set("following", true);
|
||||
return this.chat.startTrackingChannel(c);
|
||||
return promise.then((c) =>
|
||||
ajax(`/chat/${c.id}.json`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
message,
|
||||
upload_ids: (uploads || []).mapBy("id"),
|
||||
},
|
||||
}).then(() => {
|
||||
this.onSwitchChannel(c);
|
||||
})
|
||||
.then((c) =>
|
||||
ajax(`/chat/${c.id}.json`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
message,
|
||||
upload_ids: (uploads || []).mapBy("id"),
|
||||
},
|
||||
}).then(() => {
|
||||
this.chat.forceRefreshChannels();
|
||||
this.onSwitchChannel(ChatChannel.create(c));
|
||||
})
|
||||
);
|
||||
);
|
||||
},
|
||||
|
||||
_onSendError(stagedId, error) {
|
||||
|
@ -1103,7 +1094,8 @@ export default Component.extend({
|
|||
staged_id: stagedMessage.stagedId,
|
||||
};
|
||||
|
||||
ChatApi.sendMessage(this.chatChannel.id, data)
|
||||
this.chatApi
|
||||
.sendMessage(this.chatChannel.id, data)
|
||||
.catch((error) => {
|
||||
this._onSendError(data.staged_id, error);
|
||||
})
|
||||
|
|
|
@ -6,11 +6,23 @@
|
|||
<p>{{this.instructionsText}}</p>
|
||||
</div>
|
||||
|
||||
<ChatChannelChooser @class="chat-move-message-channel-chooser" @content={{this.availableChannels}} @value={{this.destinationChannelId}} @nameProperty="title" />
|
||||
<ChatChannelChooser
|
||||
@class="chat-move-message-channel-chooser"
|
||||
@content={{this.availableChannels}}
|
||||
@value={{this.destinationChannelId}}
|
||||
@nameProperty="title"
|
||||
/>
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
<DButton @class="btn-primary" @icon="sign-out-alt" @disabled={{this.disableMoveButton}} @action={{action "moveMessages"}} @label="chat.move_to_channel.confirm_move" @id="chat-confirm-move-messages-to-channel" />
|
||||
<DButton
|
||||
@class="btn-primary"
|
||||
@icon="sign-out-alt"
|
||||
@disabled={{this.disableMoveButton}}
|
||||
@action={{action "moveMessages"}}
|
||||
@label="chat.move_to_channel.confirm_move"
|
||||
@id="chat-confirm-move-messages-to-channel"
|
||||
/>
|
||||
|
||||
<DButton @label="cancel" @class="btn-flat" @action={{this.closeModal}} />
|
||||
</div>
|
||||
|
|
|
@ -3,14 +3,15 @@ import I18n from "I18n";
|
|||
import { reads } from "@ember/object/computed";
|
||||
import { isBlank } from "@ember/utils";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default class MoveToChannelModalInner extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
@service router;
|
||||
@service chatChannelsManager;
|
||||
tagName = "";
|
||||
sourceChannel = null;
|
||||
destinationChannelId = null;
|
||||
|
@ -23,31 +24,25 @@ export default class MoveToChannelModalInner extends Component {
|
|||
return isBlank(this.destinationChannelId);
|
||||
}
|
||||
|
||||
@computed("chat.publicChannels.[]")
|
||||
@computed("chatChannelsManager.publicMessageChannels.[]")
|
||||
get availableChannels() {
|
||||
return this.chat.publicChannels.rejectBy("id", this.sourceChannel.id);
|
||||
return this.chatChannelsManager.publicMessageChannels.rejectBy(
|
||||
"id",
|
||||
this.sourceChannel.id
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
moveMessages() {
|
||||
return ajax(
|
||||
`/chat/${this.sourceChannel.id}/move_messages_to_channel.json`,
|
||||
{
|
||||
method: "PUT",
|
||||
data: {
|
||||
message_ids: this.selectedMessageIds,
|
||||
destination_channel_id: this.destinationChannelId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return this.chatApi
|
||||
.moveChannelMessages(this.sourceChannel.id, {
|
||||
message_ids: this.selectedMessageIds,
|
||||
destination_channel_id: this.destinationChannelId,
|
||||
})
|
||||
.then((response) => {
|
||||
this.router.transitionTo(
|
||||
"chat.channel",
|
||||
return this.chat.openChannelAtMessage(
|
||||
response.destination_channel_id,
|
||||
response.destination_channel_title,
|
||||
{
|
||||
queryParams: { messageId: response.first_moved_message_id },
|
||||
}
|
||||
response.first_moved_message_id
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
(if this.selectingMessages "selecting-messages")
|
||||
}}
|
||||
data-id={{or this.message.id this.message.stagedId}}
|
||||
data-staged-id={{if this.message.staged this.message.stagedId}}
|
||||
>
|
||||
{{#if this.show}}
|
||||
{{#if this.selectingMessages}}
|
||||
|
|
|
@ -43,6 +43,7 @@ export default Component.extend({
|
|||
onHoverMessage: null,
|
||||
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
|
||||
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
|
||||
chatChannelsManager: service("chat-channels-manager"),
|
||||
adminTools: optionalService(),
|
||||
_hasSubscribedToAppEvents: false,
|
||||
tagName: "",
|
||||
|
@ -589,13 +590,11 @@ export default Component.extend({
|
|||
// so we will fully refresh if we were not members of the channel
|
||||
// already
|
||||
if (!this.chatChannel.isFollowing || this.chatChannel.isDraft) {
|
||||
this.chat.forceRefreshChannels().then(() => {
|
||||
return this.chat
|
||||
.getChannelBy("id", this.chatChannel.id)
|
||||
.then((reactedChannel) => {
|
||||
this.onSwitchChannel(reactedChannel);
|
||||
});
|
||||
});
|
||||
return this.chatChannelsManager
|
||||
.getChannel(this.chatChannel.id)
|
||||
.then((reactedChannel) => {
|
||||
this.onSwitchChannel(reactedChannel);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -3,9 +3,9 @@ import { htmlSafe } from "@ember/template";
|
|||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias, equal } from "@ember/object/computed";
|
||||
|
||||
export const NEW_TOPIC_SELECTION = "newTopic";
|
||||
export const EXISTING_TOPIC_SELECTION = "existingTopic";
|
||||
export const NEW_MESSAGE_SELECTION = "newMessage";
|
||||
export const NEW_TOPIC_SELECTION = "new_topic";
|
||||
export const EXISTING_TOPIC_SELECTION = "existing_topic";
|
||||
export const NEW_MESSAGE_SELECTION = "new_message";
|
||||
|
||||
export default Component.extend({
|
||||
newTopicSelection: NEW_TOPIC_SELECTION,
|
||||
|
|
|
@ -10,9 +10,6 @@ export default Component.extend({
|
|||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.on("chat:refresh-channels", this, "refreshModel");
|
||||
this.appEvents.on("chat:refresh-channel", this, "_refreshChannel");
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
|
@ -25,8 +22,6 @@ export default Component.extend({
|
|||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.off("chat:refresh-channels", this, "refreshModel");
|
||||
this.appEvents.off("chat:refresh-channel", this, "_refreshChannel");
|
||||
document.removeEventListener("keydown", this._autoFocusChatComposer);
|
||||
},
|
||||
|
||||
|
@ -77,12 +72,6 @@ export default Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_refreshChannel(channelId) {
|
||||
if (this.chat.activeChannel?.id === channelId) {
|
||||
this.refreshModel(true);
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
navigateToIndex() {
|
||||
this.router.transitionTo("chat.index");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{#if this.channel.isFollowing}}
|
||||
{{#if @channel.currentUserMembership.following}}
|
||||
<DButton
|
||||
@action={{action "onLeaveChannel"}}
|
||||
@translatedLabel={{this.label}}
|
||||
|
|
|
@ -1,53 +1,44 @@
|
|||
import Component from "@ember/component";
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { action, computed } from "@ember/object";
|
||||
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
export default class ToggleChannelMembershipButton extends Component {
|
||||
@service chat;
|
||||
|
||||
tagName = "";
|
||||
channel = null;
|
||||
@tracked isLoading = false;
|
||||
onToggle = null;
|
||||
options = null;
|
||||
isLoading = false;
|
||||
options = {};
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.set(
|
||||
"options",
|
||||
Object.assign(
|
||||
{
|
||||
labelType: "normal",
|
||||
joinTitle: I18n.t("chat.channel_settings.join_channel"),
|
||||
joinIcon: "",
|
||||
joinClass: "",
|
||||
leaveTitle: I18n.t("chat.channel_settings.leave_channel"),
|
||||
leaveIcon: "",
|
||||
leaveClass: "",
|
||||
},
|
||||
this.options || {}
|
||||
)
|
||||
);
|
||||
this.options = {
|
||||
labelType: "normal",
|
||||
joinTitle: I18n.t("chat.channel_settings.join_channel"),
|
||||
joinIcon: "",
|
||||
joinClass: "",
|
||||
leaveTitle: I18n.t("chat.channel_settings.leave_channel"),
|
||||
leaveIcon: "",
|
||||
leaveClass: "",
|
||||
...this.args.options,
|
||||
};
|
||||
}
|
||||
|
||||
@computed("channel.current_user_membership.following")
|
||||
get label() {
|
||||
if (this.options.labelType === "none") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this.options.labelType === "short") {
|
||||
if (this.channel.isFollowing) {
|
||||
if (this.args.channel.currentUserMembership.following) {
|
||||
return I18n.t("chat.channel_settings.leave");
|
||||
} else {
|
||||
return I18n.t("chat.channel_settings.join");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.channel.isFollowing) {
|
||||
if (this.args.channel.currentUserMembership.following) {
|
||||
return I18n.t("chat.channel_settings.leave_channel");
|
||||
} else {
|
||||
return I18n.t("chat.channel_settings.join_channel");
|
||||
|
@ -56,10 +47,10 @@ export default class ToggleChannelMembershipButton extends Component {
|
|||
|
||||
@action
|
||||
onJoinChannel() {
|
||||
this.set("isLoading", true);
|
||||
this.isLoading = true;
|
||||
|
||||
return this.chat
|
||||
.followChannel(this.channel)
|
||||
.followChannel(this.args.channel)
|
||||
.then(() => {
|
||||
this.onToggle?.();
|
||||
})
|
||||
|
@ -69,16 +60,16 @@ export default class ToggleChannelMembershipButton extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.set("isLoading", false);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onLeaveChannel() {
|
||||
this.set("isLoading", true);
|
||||
this.isLoading = true;
|
||||
|
||||
return this.chat
|
||||
.unfollowChannel(this.channel)
|
||||
.unfollowChannel(this.args.channel)
|
||||
.then(() => {
|
||||
this.onToggle?.();
|
||||
})
|
||||
|
@ -88,7 +79,7 @@ export default class ToggleChannelMembershipButton extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.set("isLoading", false);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action, computed } from "@ember/object";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatChannelEditDescriptionController extends Controller.extend(
|
||||
ModalFunctionality
|
||||
) {
|
||||
@service chatApi;
|
||||
editedDescription = "";
|
||||
|
||||
@computed("model.description", "editedDescription")
|
||||
|
@ -27,11 +28,12 @@ export default class ChatChannelEditDescriptionController extends Controller.ext
|
|||
|
||||
@action
|
||||
onSaveChatChannelDescription() {
|
||||
return ChatApi.modifyChatChannel(this.model.id, {
|
||||
description: this.editedDescription,
|
||||
})
|
||||
.then((chatChannel) => {
|
||||
this.model.set("description", chatChannel.description);
|
||||
return this.chatApi
|
||||
.updateChannel(this.model.id, {
|
||||
description: this.editedDescription,
|
||||
})
|
||||
.then((result) => {
|
||||
this.model.set("description", result.channel.description);
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((event) => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action, computed } from "@ember/object";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
|
||||
import { inject as service } from "@ember/service";
|
||||
export default class ChatChannelEditTitleController extends Controller.extend(
|
||||
ModalFunctionality
|
||||
) {
|
||||
@service chatApi;
|
||||
editedTitle = "";
|
||||
|
||||
@computed("model.title", "editedTitle")
|
||||
|
@ -27,11 +27,12 @@ export default class ChatChannelEditTitleController extends Controller.extend(
|
|||
|
||||
@action
|
||||
onSaveChatChannelTitle() {
|
||||
return ChatApi.modifyChatChannel(this.model.id, {
|
||||
name: this.editedTitle,
|
||||
})
|
||||
.then((chatChannel) => {
|
||||
this.model.set("title", chatChannel.title);
|
||||
return this.chatApi
|
||||
.updateChannel(this.model.id, {
|
||||
name: this.editedTitle,
|
||||
})
|
||||
.then((result) => {
|
||||
this.model.set("title", result.channel.title);
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((event) => {
|
||||
|
|
|
@ -8,13 +8,13 @@ export default class ChatChannelInfoAboutController extends Controller.extend(
|
|||
) {
|
||||
@action
|
||||
onEditChatChannelTitle() {
|
||||
showModal("chat-channel-edit-title", { model: this.model?.chatChannel });
|
||||
showModal("chat-channel-edit-title", { model: this.model });
|
||||
}
|
||||
|
||||
@action
|
||||
onEditChatChannelDescription() {
|
||||
showModal("chat-channel-edit-description", {
|
||||
model: this.model?.chatChannel,
|
||||
model: this.model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { reads } from "@ember/object/computed";
|
||||
import { computed } from "@ember/object";
|
||||
|
||||
export default class ChatChannelInfoIndexController extends Controller {
|
||||
@service router;
|
||||
|
@ -10,28 +10,25 @@ export default class ChatChannelInfoIndexController extends Controller {
|
|||
|
||||
@reads("router.currentRoute.localName") tab;
|
||||
|
||||
@computed("model.chatChannel.{membershipsCount,status}")
|
||||
@computed("model.{membershipsCount,status,currentUserMembership.following}")
|
||||
get tabs() {
|
||||
const tabs = [];
|
||||
|
||||
if (!this.model.chatChannel.isDirectMessageChannel) {
|
||||
if (!this.model.isDirectMessageChannel) {
|
||||
tabs.push("about");
|
||||
}
|
||||
|
||||
if (
|
||||
this.model.chatChannel.isOpen &&
|
||||
this.model.chatChannel.membershipsCount >= 1
|
||||
) {
|
||||
if (this.model.isOpen && this.model.membershipsCount >= 1) {
|
||||
tabs.push("members");
|
||||
}
|
||||
|
||||
tabs.push("settings");
|
||||
if (
|
||||
this.currentUser?.staff ||
|
||||
this.model.currentUserMembership?.following
|
||||
) {
|
||||
tabs.push("settings");
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
@action
|
||||
switchChannel(channel) {
|
||||
return this.chat.openChannel(channel);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import Controller from "@ember/controller";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import I18n from "I18n";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { gt, notEmpty } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
@ -23,6 +20,8 @@ export default class CreateChannelController extends Controller.extend(
|
|||
) {
|
||||
@service chat;
|
||||
@service dialog;
|
||||
@service chatChannelsManager;
|
||||
@service chatApi;
|
||||
|
||||
category = null;
|
||||
categoryId = null;
|
||||
|
@ -57,20 +56,18 @@ export default class CreateChannelController extends Controller.extend(
|
|||
|
||||
_createChannel() {
|
||||
const data = {
|
||||
id: this.categoryId,
|
||||
chatable_id: this.categoryId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
auto_join_users: this.autoJoinUsers,
|
||||
};
|
||||
|
||||
return ajax("/chat/chat_channels", { method: "PUT", data })
|
||||
.then((response) => {
|
||||
const chatChannel = ChatChannel.create(response.chat_channel);
|
||||
|
||||
return this.chat.startTrackingChannel(chatChannel).then(() => {
|
||||
this.send("closeModal");
|
||||
this.chat.openChannel(chatChannel);
|
||||
});
|
||||
return this.chatApi
|
||||
.createChannel(data)
|
||||
.then((channel) => {
|
||||
this.send("closeModal");
|
||||
this.chatChannelsManager.follow(channel);
|
||||
this.chat.openChannel(channel);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.flash(e.jqXHR.responseJSON.errors[0], "error");
|
||||
|
@ -117,24 +114,26 @@ export default class CreateChannelController extends Controller.extend(
|
|||
if (category) {
|
||||
const fullSlug = this._buildCategorySlug(category);
|
||||
|
||||
return ChatApi.categoryPermissions(category.id).then((catPermissions) => {
|
||||
this._updateAutoJoinConfirmWarning(category, catPermissions);
|
||||
const allowedGroups = catPermissions.allowed_groups;
|
||||
const translationKey =
|
||||
allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups";
|
||||
return this.chatApi
|
||||
.categoryPermissions(category.id)
|
||||
.then((catPermissions) => {
|
||||
this._updateAutoJoinConfirmWarning(category, catPermissions);
|
||||
const allowedGroups = catPermissions.allowed_groups;
|
||||
const translationKey =
|
||||
allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups";
|
||||
|
||||
this.set(
|
||||
"categoryPermissionsHint",
|
||||
htmlSafe(
|
||||
I18n.t(`chat.create_channel.choose_category.${translationKey}`, {
|
||||
link: `/c/${escapeExpression(fullSlug)}/edit/security`,
|
||||
hint: escapeExpression(allowedGroups[0]),
|
||||
hint_2: escapeExpression(allowedGroups[1]),
|
||||
count: allowedGroups.length,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
this.set(
|
||||
"categoryPermissionsHint",
|
||||
htmlSafe(
|
||||
I18n.t(`chat.create_channel.choose_category.${translationKey}`, {
|
||||
link: `/c/${escapeExpression(fullSlug)}/edit/security`,
|
||||
hint: escapeExpression(allowedGroups[0]),
|
||||
hint_2: escapeExpression(allowedGroups[1]),
|
||||
count: allowedGroups.length,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.set("categoryPermissionsHint", DEFAULT_HINT);
|
||||
this.set("autoJoinWarning", "");
|
||||
|
|
|
@ -12,15 +12,13 @@ export default {
|
|||
name: "chat-setup",
|
||||
initialize(container) {
|
||||
this.chatService = container.lookup("service:chat");
|
||||
|
||||
if (!this.chatService.userCanChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.siteSettings = container.lookup("service:site-settings");
|
||||
this.appEvents = container.lookup("service:appEvents");
|
||||
this.appEvents.on("discourse:focus-changed", this, "_handleFocusChanged");
|
||||
|
||||
if (!this.chatService.userCanChat) {
|
||||
return;
|
||||
}
|
||||
withPluginApi("0.12.1", (api) => {
|
||||
api.registerChatComposerButton({
|
||||
id: "chat-upload-btn",
|
||||
|
@ -99,8 +97,6 @@ export default {
|
|||
const currentUser = api.getCurrentUser();
|
||||
if (currentUser?.chat_channels) {
|
||||
this.chatService.setupWithPreloadedChannels(currentUser.chat_channels);
|
||||
} else {
|
||||
this.chatService.setupWithoutPreloadedChannels();
|
||||
}
|
||||
|
||||
const chatNotificationManager = container.lookup(
|
||||
|
@ -115,19 +111,7 @@ export default {
|
|||
|
||||
api.addCardClickListenerSelector(".chat-drawer-outlet");
|
||||
|
||||
api.dispatchWidgetAppEvent(
|
||||
"site-header",
|
||||
"header-chat-link",
|
||||
"chat:rerender-header"
|
||||
);
|
||||
|
||||
api.dispatchWidgetAppEvent(
|
||||
"sidebar-header",
|
||||
"header-chat-link",
|
||||
"chat:rerender-header"
|
||||
);
|
||||
|
||||
api.addToHeaderIcons("header-chat-link");
|
||||
api.addToHeaderIcons("chat-header-icon");
|
||||
|
||||
api.decorateChatMessage(function (chatMessage, chatChannel) {
|
||||
if (!this.currentUser) {
|
||||
|
@ -155,17 +139,22 @@ export default {
|
|||
},
|
||||
|
||||
teardown() {
|
||||
this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged");
|
||||
|
||||
if (!this.chatService.userCanChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appEvents.off("discourse:focus-changed", this, "_handleFocusChanged");
|
||||
_lastForcedRefreshAt = null;
|
||||
clearChatComposerButtons();
|
||||
},
|
||||
|
||||
@bind
|
||||
_handleFocusChanged(hasFocus) {
|
||||
if (!this.chatService.userCanChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasFocus) {
|
||||
_lastForcedRefreshAt = Date.now();
|
||||
return;
|
||||
|
@ -179,6 +168,5 @@ export default {
|
|||
}
|
||||
|
||||
_lastForcedRefreshAt = Date.now();
|
||||
this.chatService.refreshTrackingState();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,41 +25,12 @@ export default {
|
|||
api.addSidebarSection(
|
||||
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||
const SidebarChatChannelsSectionLink = class extends BaseCustomSidebarSectionLink {
|
||||
@tracked chatChannelTrackingState =
|
||||
this.chatService.currentUser.chat_channel_tracking_state[
|
||||
this.channel.id
|
||||
];
|
||||
|
||||
constructor({ channel, chatService }) {
|
||||
super(...arguments);
|
||||
this.channel = channel;
|
||||
this.chatService = chatService;
|
||||
}
|
||||
|
||||
@bind
|
||||
willDestroy() {
|
||||
this.chatService.appEvents.off(
|
||||
"chat:user-tracking-state-changed",
|
||||
this._refreshTrackingState
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
didInsert() {
|
||||
this.chatService.appEvents.on(
|
||||
"chat:user-tracking-state-changed",
|
||||
this._refreshTrackingState
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_refreshTrackingState() {
|
||||
this.chatChannelTrackingState =
|
||||
this.chatService.currentUser.chat_channel_tracking_state[
|
||||
this.channel.id
|
||||
];
|
||||
}
|
||||
|
||||
get name() {
|
||||
return dasherize(slugifyChannel(this.channel));
|
||||
}
|
||||
|
@ -68,7 +39,7 @@ export default {
|
|||
get classNames() {
|
||||
const classes = [];
|
||||
|
||||
if (this.channel.current_user_membership.muted) {
|
||||
if (this.channel.currentUserMembership.muted) {
|
||||
classes.push("sidebar-section-link--muted");
|
||||
}
|
||||
|
||||
|
@ -76,6 +47,8 @@ export default {
|
|||
classes.push("sidebar-section-link--active");
|
||||
}
|
||||
|
||||
classes.push(`channel-${this.channel.id}`);
|
||||
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
|
@ -118,26 +91,19 @@ export default {
|
|||
}
|
||||
|
||||
get suffixValue() {
|
||||
return this.chatChannelTrackingState?.unread_count > 0
|
||||
return this.channel.currentUserMembership.unread_count > 0
|
||||
? "circle"
|
||||
: "";
|
||||
}
|
||||
|
||||
get suffixCSSClass() {
|
||||
return this.chatChannelTrackingState?.unread_mentions > 0
|
||||
return this.channel.currentUserMembership.unread_mentions > 0
|
||||
? "urgent"
|
||||
: "unread";
|
||||
}
|
||||
};
|
||||
|
||||
const SidebarChatChannelsSection = class extends BaseCustomSidebarSection {
|
||||
@tracked sectionLinks = [];
|
||||
|
||||
@tracked sectionIndicator =
|
||||
this.chatService.publicChannels &&
|
||||
this.chatService.publicChannels[0].current_user_membership
|
||||
.unread_count;
|
||||
|
||||
@tracked currentUserCanJoinPublicChannels =
|
||||
this.sidebar.currentUser &&
|
||||
(this.sidebar.currentUser.staff ||
|
||||
|
@ -150,37 +116,20 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.chatService = container.lookup("service:chat");
|
||||
this.router = container.lookup("service:router");
|
||||
this.appEvents = container.lookup("service:app-events");
|
||||
this.appEvents.on("chat:refresh-channels", this._refreshChannels);
|
||||
this._refreshChannels();
|
||||
}
|
||||
|
||||
@bind
|
||||
willDestroy() {
|
||||
if (!this.appEvents) {
|
||||
return;
|
||||
}
|
||||
this.appEvents.off(
|
||||
"chat:refresh-channels",
|
||||
this._refreshChannels
|
||||
this.chatChannelsManager = container.lookup(
|
||||
"service:chat-channels-manager"
|
||||
);
|
||||
this.router = container.lookup("service:router");
|
||||
}
|
||||
|
||||
@bind
|
||||
_refreshChannels() {
|
||||
const newSectionLinks = [];
|
||||
this.chatService.getChannels().then((channels) => {
|
||||
channels.publicChannels.forEach((channel) => {
|
||||
newSectionLinks.push(
|
||||
new SidebarChatChannelsSectionLink({
|
||||
channel,
|
||||
chatService: this.chatService,
|
||||
})
|
||||
);
|
||||
});
|
||||
this.sectionLinks = newSectionLinks;
|
||||
});
|
||||
get sectionLinks() {
|
||||
return this.chatChannelsManager.publicMessageChannels.map(
|
||||
(channel) =>
|
||||
new SidebarChatChannelsSectionLink({
|
||||
channel,
|
||||
chatService: this.chatService,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
@ -228,11 +177,6 @@ export default {
|
|||
api.addSidebarSection(
|
||||
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||
const SidebarChatDirectMessagesSectionLink = class extends BaseCustomSidebarSectionLink {
|
||||
@tracked chatChannelTrackingState =
|
||||
this.chatService.currentUser.chat_channel_tracking_state[
|
||||
this.channel.id
|
||||
];
|
||||
|
||||
constructor({ channel, chatService }) {
|
||||
super(...arguments);
|
||||
this.channel = channel;
|
||||
|
@ -258,7 +202,7 @@ export default {
|
|||
get classNames() {
|
||||
const classes = [];
|
||||
|
||||
if (this.channel.current_user_membership.muted) {
|
||||
if (this.channel.currentUserMembership.muted) {
|
||||
classes.push("sidebar-section-link--muted");
|
||||
}
|
||||
|
||||
|
@ -266,6 +210,8 @@ export default {
|
|||
classes.push("sidebar-section-link--active");
|
||||
}
|
||||
|
||||
classes.push(`channel-${this.channel.id}`);
|
||||
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
|
@ -340,7 +286,7 @@ export default {
|
|||
}
|
||||
|
||||
get suffixValue() {
|
||||
return this.chatChannelTrackingState?.unread_count > 0
|
||||
return this.channel.currentUserMembership.unread_count > 0
|
||||
? "circle"
|
||||
: "";
|
||||
}
|
||||
|
@ -396,7 +342,6 @@ export default {
|
|||
const SidebarChatDirectMessagesSection = class extends BaseCustomSidebarSection {
|
||||
@service site;
|
||||
@service router;
|
||||
@tracked sectionLinks = [];
|
||||
@tracked userCanDirectMessage =
|
||||
this.chatService.userCanDirectMessage;
|
||||
|
||||
|
@ -407,40 +352,19 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.chatService = container.lookup("service:chat");
|
||||
this.chatService.appEvents.on(
|
||||
"chat:user-tracking-state-changed",
|
||||
this._refreshDirectMessageChannels
|
||||
);
|
||||
this._refreshDirectMessageChannels();
|
||||
}
|
||||
|
||||
@bind
|
||||
willDestroy() {
|
||||
if (container.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
this.chatService.appEvents.off(
|
||||
"chat:user-tracking-state-changed",
|
||||
this._refreshDirectMessageChannels
|
||||
this.chatChannelsManager = container.lookup(
|
||||
"service:chat-channels-manager"
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_refreshDirectMessageChannels() {
|
||||
const newSectionLinks = [];
|
||||
this.chatService.getChannels().then((channels) => {
|
||||
this.chatService
|
||||
.truncateDirectMessageChannels(channels.directMessageChannels)
|
||||
.forEach((channel) => {
|
||||
newSectionLinks.push(
|
||||
new SidebarChatDirectMessagesSectionLink({
|
||||
channel,
|
||||
chatService: this.chatService,
|
||||
})
|
||||
);
|
||||
});
|
||||
this.sectionLinks = newSectionLinks;
|
||||
});
|
||||
get sectionLinks() {
|
||||
return this.chatChannelsManager.truncatedDirectMessageChannels.map(
|
||||
(channel) =>
|
||||
new SidebarChatDirectMessagesSectionLink({
|
||||
channel,
|
||||
chatService: this.chatService,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
export default class ChatApi {
|
||||
static async chatChannelMemberships(channelId, data) {
|
||||
return await ajax(`/chat/api/chat_channels/${channelId}/memberships.json`, {
|
||||
data,
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
|
||||
static async updateChatChannelNotificationsSettings(channelId, data = {}) {
|
||||
return await ajax(
|
||||
`/chat/api/chat_channels/${channelId}/notifications_settings.json`,
|
||||
{
|
||||
method: "PUT",
|
||||
data,
|
||||
}
|
||||
).catch(popupAjaxError);
|
||||
}
|
||||
|
||||
static async sendMessage(channelId, data = {}) {
|
||||
return ajax(`/chat/${channelId}.json`, {
|
||||
ignoreUnsent: false,
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static async chatChannels(data = {}) {
|
||||
if (data?.status === "all") {
|
||||
delete data.status;
|
||||
}
|
||||
|
||||
return await ajax(`/chat/api/chat_channels.json`, {
|
||||
method: "GET",
|
||||
data,
|
||||
})
|
||||
.then((channels) =>
|
||||
channels.map((channel) => ChatChannel.create(channel))
|
||||
)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
static async modifyChatChannel(channelId, data) {
|
||||
return await this._performRequest(
|
||||
`/chat/api/chat_channels/${channelId}.json`,
|
||||
{
|
||||
method: "PUT",
|
||||
data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async unfollowChatChannel(channel) {
|
||||
return await this._performRequest(
|
||||
`/chat/chat_channels/${channel.id}/unfollow.json`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
).then((updatedChannel) => {
|
||||
channel.updateMembership(updatedChannel.current_user_membership);
|
||||
|
||||
// doesn't matter if this is inaccurate, it will be eventually consistent
|
||||
// via the channel-metadata MessageBus channel
|
||||
channel.set("memberships_count", channel.memberships_count - 1);
|
||||
return channel;
|
||||
});
|
||||
}
|
||||
|
||||
static async followChatChannel(channel) {
|
||||
return await this._performRequest(
|
||||
`/chat/chat_channels/${channel.id}/follow.json`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
).then((updatedChannel) => {
|
||||
channel.updateMembership(updatedChannel.current_user_membership);
|
||||
|
||||
// doesn't matter if this is inaccurate, it will be eventually consistent
|
||||
// via the channel-metadata MessageBus channel
|
||||
channel.set("memberships_count", channel.memberships_count + 1);
|
||||
return channel;
|
||||
});
|
||||
}
|
||||
|
||||
static async categoryPermissions(categoryId) {
|
||||
return await this._performRequest(
|
||||
`/chat/api/category-chatables/${categoryId}/permissions.json`
|
||||
);
|
||||
}
|
||||
|
||||
static async _performRequest(...args) {
|
||||
return await ajax(...args).catch(popupAjaxError);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import RestModel from "discourse/models/rest";
|
||||
import I18n from "I18n";
|
||||
import { computed } from "@ember/object";
|
||||
import User from "discourse/models/user";
|
||||
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export const CHATABLE_TYPES = {
|
||||
directMessageChannel: "DirectMessage",
|
||||
categoryChannel: "Category",
|
||||
};
|
||||
|
||||
export const CHANNEL_STATUSES = {
|
||||
open: "open",
|
||||
readOnly: "read_only",
|
||||
|
@ -38,13 +39,10 @@ export function channelStatusIcon(channelStatus) {
|
|||
switch (channelStatus) {
|
||||
case CHANNEL_STATUSES.closed:
|
||||
return "lock";
|
||||
break;
|
||||
case CHANNEL_STATUSES.readOnly:
|
||||
return "comment-slash";
|
||||
break;
|
||||
case CHANNEL_STATUSES.archived:
|
||||
return "archive";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,62 +58,51 @@ const READONLY_STATUSES = [
|
|||
];
|
||||
|
||||
export default class ChatChannel extends RestModel {
|
||||
isDraft = false;
|
||||
lastSendReadMessageId = null;
|
||||
@tracked currentUserMembership = null;
|
||||
@tracked isDraft = false;
|
||||
@tracked title;
|
||||
@tracked description;
|
||||
@tracked chatableType;
|
||||
@tracked status;
|
||||
|
||||
@computed("title")
|
||||
get escapedTitle() {
|
||||
return escapeExpression(this.title);
|
||||
}
|
||||
|
||||
@computed("description")
|
||||
get escapedDescription() {
|
||||
return escapeExpression(this.description);
|
||||
}
|
||||
|
||||
@computed("chatable_type")
|
||||
get isDirectMessageChannel() {
|
||||
return this.chatable_type === CHATABLE_TYPES.directMessageChannel;
|
||||
}
|
||||
|
||||
@computed("chatable_type")
|
||||
get isCategoryChannel() {
|
||||
return this.chatable_type === CHATABLE_TYPES.categoryChannel;
|
||||
}
|
||||
|
||||
@computed("status")
|
||||
get isOpen() {
|
||||
return !this.status || this.status === CHANNEL_STATUSES.open;
|
||||
}
|
||||
|
||||
@computed("status")
|
||||
get isReadOnly() {
|
||||
return this.status === CHANNEL_STATUSES.readOnly;
|
||||
}
|
||||
|
||||
@computed("status")
|
||||
get isClosed() {
|
||||
return this.status === CHANNEL_STATUSES.closed;
|
||||
}
|
||||
|
||||
@computed("status")
|
||||
get isArchived() {
|
||||
return this.status === CHANNEL_STATUSES.archived;
|
||||
}
|
||||
|
||||
@computed("isArchived", "isOpen")
|
||||
get isJoinable() {
|
||||
return this.isOpen && !this.isArchived;
|
||||
}
|
||||
|
||||
@computed("memberships_count")
|
||||
get membershipsCount() {
|
||||
return this.memberships_count;
|
||||
}
|
||||
|
||||
@computed("current_user_membership.following")
|
||||
get isFollowing() {
|
||||
return this.current_user_membership.following;
|
||||
return this.currentUserMembership.following;
|
||||
}
|
||||
|
||||
canModifyMessages(user) {
|
||||
|
@ -127,12 +114,12 @@ export default class ChatChannel extends RestModel {
|
|||
}
|
||||
|
||||
updateMembership(membership) {
|
||||
this.current_user_membership.setProperties({
|
||||
following: membership.following,
|
||||
muted: membership.muted,
|
||||
desktop_notification_level: membership.desktop_notification_level,
|
||||
mobile_notification_level: membership.mobile_notification_level,
|
||||
});
|
||||
this.currentUserMembership.following = membership.following;
|
||||
this.currentUserMembership.muted = membership.muted;
|
||||
this.currentUserMembership.desktop_notification_level =
|
||||
membership.desktop_notification_level;
|
||||
this.currentUserMembership.mobile_notification_level =
|
||||
membership.mobile_notification_level;
|
||||
}
|
||||
|
||||
updateLastReadMessage(messageId) {
|
||||
|
@ -143,7 +130,7 @@ export default class ChatChannel extends RestModel {
|
|||
return ajax(`/chat/${this.id}/read/${messageId}.json`, {
|
||||
method: "PUT",
|
||||
}).then(() => {
|
||||
this.set("lastSendReadMessageId", messageId);
|
||||
this.currentUserMembership.last_read_message_id = messageId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -151,11 +138,12 @@ export default class ChatChannel extends RestModel {
|
|||
ChatChannel.reopenClass({
|
||||
create(args) {
|
||||
args = args || {};
|
||||
|
||||
this._initUserModels(args);
|
||||
this._initUserMembership(args);
|
||||
|
||||
args.lastSendReadMessageId =
|
||||
args.current_user_membership?.last_read_message_id;
|
||||
args.chatableType = args.chatable_type;
|
||||
args.membershipsCount = args.memberships_count;
|
||||
|
||||
return this._super(args);
|
||||
},
|
||||
|
@ -170,11 +158,11 @@ ChatChannel.reopenClass({
|
|||
},
|
||||
|
||||
_initUserMembership(args) {
|
||||
if (args.current_user_membership instanceof UserChatChannelMembership) {
|
||||
if (args.currentUserMembership instanceof UserChatChannelMembership) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.current_user_membership = UserChatChannelMembership.create(
|
||||
args.currentUserMembership = UserChatChannelMembership.create(
|
||||
args.current_user_membership || {
|
||||
following: false,
|
||||
muted: false,
|
||||
|
@ -182,6 +170,8 @@ ChatChannel.reopenClass({
|
|||
unread_mentions: 0,
|
||||
}
|
||||
);
|
||||
|
||||
delete args.current_user_membership;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,30 @@
|
|||
import RestModel from "discourse/models/rest";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import User from "discourse/models/user";
|
||||
export default class UserChatChannelMembership extends RestModel {
|
||||
@tracked following = false;
|
||||
@tracked muted = false;
|
||||
@tracked unread_count = 0;
|
||||
@tracked unread_mentions = 0;
|
||||
@tracked chat_message_id = null;
|
||||
@tracked chat_channel_id = null;
|
||||
@tracked desktop_notification_level = null;
|
||||
@tracked mobile_notification_level = null;
|
||||
@tracked last_read_message_id = null;
|
||||
}
|
||||
|
||||
export default class UserChatChannelMembership extends RestModel {}
|
||||
UserChatChannelMembership.reopenClass({
|
||||
create(args) {
|
||||
args = args || {};
|
||||
this._initUser(args);
|
||||
return this._super(args);
|
||||
},
|
||||
|
||||
_initUser(args) {
|
||||
if (args.user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.user = User.create(args.user);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class ChatBrowseIndexRoute extends DiscourseRoute {
|
||||
afterModel() {
|
||||
if (!this.siteSettings.chat_allow_archiving_channels) {
|
||||
this.replaceWith("chat.browse");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,8 +13,8 @@ export default class ChatChannelByNameRoute extends DiscourseRoute {
|
|||
.then((response) => {
|
||||
this.transitionTo(
|
||||
"chat.channel",
|
||||
response.chat_channel.id,
|
||||
response.chat_channel.title
|
||||
response.channel.id,
|
||||
response.channel.title
|
||||
);
|
||||
})
|
||||
.catch(() => this.replaceWith("/404"));
|
||||
|
|
|
@ -2,7 +2,7 @@ import DiscourseRoute from "discourse/routes/discourse";
|
|||
|
||||
export default class ChatChannelInfoAboutRoute extends DiscourseRoute {
|
||||
afterModel(model) {
|
||||
if (model.chatChannel.isDirectMessageChannel) {
|
||||
if (model.isDirectMessageChannel) {
|
||||
this.replaceWith("chat.channel.info.index");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import DiscourseRoute from "discourse/routes/discourse";
|
|||
|
||||
export default class ChatChannelInfoIndexRoute extends DiscourseRoute {
|
||||
afterModel(model) {
|
||||
if (model.chatChannel.isDirectMessageChannel) {
|
||||
if (model.chatChannel.isOpen && model.chatChannel.membershipsCount >= 1) {
|
||||
if (model.isDirectMessageChannel) {
|
||||
if (model.isOpen && model.membershipsCount >= 1) {
|
||||
this.replaceWith("chat.channel.info.members");
|
||||
} else {
|
||||
this.replaceWith("chat.channel.info.settings");
|
||||
|
|
|
@ -2,8 +2,12 @@ import DiscourseRoute from "discourse/routes/discourse";
|
|||
|
||||
export default class ChatChannelInfoMembersRoute extends DiscourseRoute {
|
||||
afterModel(model) {
|
||||
if (!model.chatChannel.isOpen) {
|
||||
this.replaceWith("chat.channel.info.settings");
|
||||
if (!model.isOpen) {
|
||||
return this.replaceWith("chat.channel.info.settings");
|
||||
}
|
||||
|
||||
if (model.membershipsCount < 1) {
|
||||
return this.replaceWith("chat.channel.info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class ChatChannelInfoSettingsRoute extends DiscourseRoute {
|
||||
afterModel(model) {
|
||||
if (!this.currentUser?.staff && !model.currentUserMembership?.following) {
|
||||
this.replaceWith("chat.channel.info");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +1,23 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import Promise from "rsvp";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
|
||||
|
||||
export default class ChatChannelRoute extends DiscourseRoute {
|
||||
@service chat;
|
||||
@service router;
|
||||
@service chatChannelsManager;
|
||||
|
||||
async model(params) {
|
||||
let [chatChannel, channels] = await Promise.all([
|
||||
this.getChannel(params.channelId),
|
||||
this.chat.getChannels(),
|
||||
]);
|
||||
|
||||
return EmberObject.create({
|
||||
chatChannel,
|
||||
channels,
|
||||
});
|
||||
}
|
||||
|
||||
async getChannel(id) {
|
||||
let channel = await this.chat.getChannelBy("id", id);
|
||||
if (!channel || this.forceRefetchChannel) {
|
||||
channel = await this.getChannelFromServer(id);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
async getChannelFromServer(id) {
|
||||
return ajax(`/chat/chat_channels/${id}`)
|
||||
.then((response) => ChatChannel.create(response))
|
||||
.catch(() => this.replaceWith("/404"));
|
||||
return this.chatChannelsManager.find(params.channelId);
|
||||
}
|
||||
|
||||
afterModel(model) {
|
||||
this.chat.setActiveChannel(model?.chatChannel);
|
||||
this.chat.setActiveChannel(model);
|
||||
|
||||
const queryParams = this.paramsFor(this.routeName);
|
||||
const slug = slugifyChannel(model.chatChannel);
|
||||
const slug = slugifyChannel(model);
|
||||
if (queryParams?.channelTitle !== slug) {
|
||||
this.router.replaceWith("chat.channel.index", model.chatChannel.id, slug);
|
||||
this.router.replaceWith("chat.channel.index", model.id, slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,10 +29,4 @@ export default class ChatChannelRoute extends DiscourseRoute {
|
|||
this.controller.set("messageId", null);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
refreshModel(forceRefetchChannel = false) {
|
||||
this.forceRefetchChannel = forceRefetchChannel;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,36 +3,29 @@ import { inject as service } from "@ember/service";
|
|||
|
||||
export default class ChatIndexRoute extends DiscourseRoute {
|
||||
@service chat;
|
||||
@service chatChannelsManager;
|
||||
@service router;
|
||||
|
||||
redirect() {
|
||||
// Always want the channel index on mobile.
|
||||
if (this.site.mobileView) {
|
||||
return; // Always want the channel index on mobile.
|
||||
return;
|
||||
}
|
||||
|
||||
// We are on desktop. Check for a channel to enter and transition if so.
|
||||
// Otherwise, `setupController` will fetch all available
|
||||
return this.chat.getIdealFirstChannelIdAndTitle().then((channelInfo) => {
|
||||
if (channelInfo) {
|
||||
return this.chat.getChannelBy("id", channelInfo.id).then((c) => {
|
||||
return this.chat.openChannel(c);
|
||||
});
|
||||
} else {
|
||||
return this.router.transitionTo("chat.browse");
|
||||
}
|
||||
});
|
||||
// We are on desktop. Check for a channel to enter and transition if so
|
||||
const id = this.chat.getIdealFirstChannelId();
|
||||
if (id) {
|
||||
return this.chatChannelsManager.find(id).then((c) => {
|
||||
return this.chat.openChannel(c);
|
||||
});
|
||||
} else {
|
||||
return this.router.transitionTo("chat.browse");
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
if (this.site.mobileView) {
|
||||
return this.chat.getChannels().then((channels) => {
|
||||
if (
|
||||
channels.publicChannels.length ||
|
||||
channels.directMessageChannels.length
|
||||
) {
|
||||
return channels;
|
||||
}
|
||||
});
|
||||
return this.chatChannelsManager.channels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
class Collection {
|
||||
@tracked items = [];
|
||||
@tracked meta = {};
|
||||
@tracked loading = false;
|
||||
|
||||
constructor(resourceURL, handler) {
|
||||
this._resourceURL = resourceURL;
|
||||
this._handler = handler;
|
||||
this._fetchedAll = false;
|
||||
}
|
||||
|
||||
get loadMoreURL() {
|
||||
return this.meta.load_more_url;
|
||||
}
|
||||
|
||||
get totalRows() {
|
||||
return this.meta.total_rows;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
|
||||
[Symbol.iterator]() {
|
||||
let index = 0;
|
||||
|
||||
return {
|
||||
next: () => {
|
||||
if (index < this.items.length) {
|
||||
return { value: this.items[index++], done: false };
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bind
|
||||
load(params = {}) {
|
||||
this._fetchedAll = false;
|
||||
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const filteredQueryParams = Object.entries(params).filter(
|
||||
([, v]) => v !== undefined
|
||||
);
|
||||
const queryString = new URLSearchParams(filteredQueryParams).toString();
|
||||
|
||||
const endpoint = this._resourceURL + (queryString ? `?${queryString}` : "");
|
||||
return this.#fetch(endpoint)
|
||||
.then((result) => {
|
||||
this.items = this._handler(result);
|
||||
this.meta = result.meta;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
loadMore() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._fetchedAll ||
|
||||
(this.totalRows && this.items.length >= this.totalRows)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (this.loadMoreURL) {
|
||||
promise = this.#fetch(this.loadMoreURL).then((result) => {
|
||||
const newItems = this._handler(result);
|
||||
|
||||
if (newItems.length) {
|
||||
this.items = this.items.concat(newItems);
|
||||
} else {
|
||||
this._fetchedAll = true;
|
||||
}
|
||||
this.meta = result.meta;
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
#fetch(url) {
|
||||
return ajax(url, { type: "GET" });
|
||||
}
|
||||
}
|
||||
|
||||
export default class ChatApi extends Service {
|
||||
@service chatChannelsManager;
|
||||
|
||||
getChannel(channelId) {
|
||||
return this.#getRequest(`/channels/${channelId}`).then((result) =>
|
||||
this.chatChannelsManager.store(result.channel)
|
||||
);
|
||||
}
|
||||
|
||||
channels() {
|
||||
return new Collection(`${this.#basePath}/channels`, (response) => {
|
||||
return response.channels.map((channel) =>
|
||||
this.chatChannelsManager.store(channel)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
moveChannelMessages(channelId, data = {}) {
|
||||
return this.#postRequest(`/channels/${channelId}/messages/moves`, {
|
||||
move: data,
|
||||
});
|
||||
}
|
||||
|
||||
destroyChannel(channelId, data = {}) {
|
||||
return this.#deleteRequest(`/channels/${channelId}`, { channel: data });
|
||||
}
|
||||
|
||||
createChannel(data = {}) {
|
||||
return this.#postRequest("/channels", { channel: data }).then((response) =>
|
||||
this.chatChannelsManager.store(response.channel)
|
||||
);
|
||||
}
|
||||
|
||||
categoryPermissions(categoryId) {
|
||||
return ajax(`/chat/api/category-chatables/${categoryId}/permissions`);
|
||||
}
|
||||
|
||||
sendMessage(channelId, data = {}) {
|
||||
return ajax(`/chat/${channelId}`, {
|
||||
ignoreUnsent: false,
|
||||
type: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
createChannelArchive(channelId, data = {}) {
|
||||
return this.#postRequest(`/channels/${channelId}/archives`, {
|
||||
archive: data,
|
||||
});
|
||||
}
|
||||
|
||||
updateChannel(channelId, data = {}) {
|
||||
return this.#putRequest(`/channels/${channelId}`, { channel: data });
|
||||
}
|
||||
|
||||
updateChannelStatus(channelId, status) {
|
||||
return this.#putRequest(`/channels/${channelId}/status`, { status });
|
||||
}
|
||||
|
||||
listChannelMemberships(channelId) {
|
||||
return new Collection(
|
||||
`${this.#basePath}/channels/${channelId}/memberships`,
|
||||
(response) => {
|
||||
return response.memberships.map((membership) =>
|
||||
UserChatChannelMembership.create(membership)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
listCurrentUserChannels() {
|
||||
return this.#getRequest(`/channels/me`).then((result) => {
|
||||
return (result?.channels || []).map((channel) =>
|
||||
this.chatChannelsManager.store(channel)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
followChannel(channelId) {
|
||||
return this.#postRequest(`/channels/${channelId}/memberships/me`).then(
|
||||
(result) => UserChatChannelMembership.create(result.membership)
|
||||
);
|
||||
}
|
||||
|
||||
unfollowChannel(channelId) {
|
||||
return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then(
|
||||
(result) => UserChatChannelMembership.create(result.membership)
|
||||
);
|
||||
}
|
||||
|
||||
updateCurrentUserChatChannelNotificationsSettings(channelId, data = {}) {
|
||||
return this.#putRequest(
|
||||
`/channels/${channelId}/notifications-settings/me`,
|
||||
{ notifications_settings: data }
|
||||
);
|
||||
}
|
||||
|
||||
get #basePath() {
|
||||
return "/chat/api";
|
||||
}
|
||||
|
||||
#getRequest(endpoint, data = {}) {
|
||||
return ajax(`${this.#basePath}/${endpoint}`, {
|
||||
type: "GET",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
#putRequest(endpoint, data = {}) {
|
||||
return ajax(`${this.#basePath}/${endpoint}`, {
|
||||
type: "PUT",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
#postRequest(endpoint, data = {}) {
|
||||
return ajax(`${this.#basePath}/${endpoint}`, {
|
||||
type: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
#deleteRequest(endpoint, data = {}) {
|
||||
return ajax(`${this.#basePath}/${endpoint}`, {
|
||||
type: "DELETE",
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import Promise from "rsvp";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
const DIRECT_MESSAGE_CHANNELS_LIMIT = 20;
|
||||
|
||||
export default class ChatChannelsManager extends Service {
|
||||
@service chatSubscriptionsManager;
|
||||
@service chatApi;
|
||||
@service currentUser;
|
||||
@tracked _cached = new TrackedObject();
|
||||
|
||||
get channels() {
|
||||
return Object.values(this._cached);
|
||||
}
|
||||
|
||||
async find(id) {
|
||||
const existingChannel = this.#findStale(id);
|
||||
if (existingChannel) {
|
||||
return Promise.resolve(existingChannel);
|
||||
} else {
|
||||
return this.#find(id);
|
||||
}
|
||||
}
|
||||
|
||||
store(channelObject) {
|
||||
let model = this.#findStale(channelObject.id);
|
||||
|
||||
if (!model) {
|
||||
model = ChatChannel.create(channelObject);
|
||||
this.#cache(model);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
async follow(model) {
|
||||
this.chatSubscriptionsManager.startChannelSubscription(model);
|
||||
|
||||
if (!model.currentUserMembership.following) {
|
||||
return this.chatApi.followChannel(model.id).then((membership) => {
|
||||
model.currentUserMembership.following = membership.following;
|
||||
model.currentUserMembership.muted = membership.muted;
|
||||
model.currentUserMembership.desktop_notification_level =
|
||||
membership.desktop_notification_level;
|
||||
model.currentUserMembership.mobile_notification_level =
|
||||
membership.mobile_notification_level;
|
||||
|
||||
return model;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(model);
|
||||
}
|
||||
}
|
||||
|
||||
async unfollow(model) {
|
||||
this.chatSubscriptionsManager.stopChannelSubscription(model);
|
||||
|
||||
return this.chatApi.unfollowChannel(model.id).then((membership) => {
|
||||
model.currentUserMembership = membership;
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
|
||||
get unreadCount() {
|
||||
let count = 0;
|
||||
this.publicMessageChannels.forEach((channel) => {
|
||||
count += channel.currentUserMembership.unread_count || 0;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
get unreadUrgentCount() {
|
||||
let count = 0;
|
||||
this.channels.forEach((channel) => {
|
||||
if (channel.isDirectMessageChannel) {
|
||||
count += channel.currentUserMembership.unread_count || 0;
|
||||
}
|
||||
count += channel.currentUserMembership.unread_mentions || 0;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
get publicMessageChannels() {
|
||||
return this.channels.filter(
|
||||
(channel) =>
|
||||
channel.isCategoryChannel && channel.currentUserMembership.following
|
||||
);
|
||||
}
|
||||
|
||||
get directMessageChannels() {
|
||||
return this.#sortDirectMessageChannels(
|
||||
this.channels.filter((channel) => {
|
||||
const membership = channel.currentUserMembership;
|
||||
return channel.isDirectMessageChannel && membership.following;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get truncatedDirectMessageChannels() {
|
||||
return this.directMessageChannels.slice(0, DIRECT_MESSAGE_CHANNELS_LIMIT);
|
||||
}
|
||||
|
||||
async #find(id) {
|
||||
return this.chatApi.getChannel(id).then((channel) => {
|
||||
this.#cache(channel);
|
||||
return channel;
|
||||
});
|
||||
}
|
||||
|
||||
#cache(channel) {
|
||||
this._cached[channel.id] = channel;
|
||||
}
|
||||
|
||||
#findStale(id) {
|
||||
return this._cached[id];
|
||||
}
|
||||
|
||||
#sortDirectMessageChannels(channels) {
|
||||
return channels.sort((a, b) => {
|
||||
const unreadCountA = a.currentUserMembership.unread_count || 0;
|
||||
const unreadCountB = b.currentUserMembership.unread_count || 0;
|
||||
if (unreadCountA === unreadCountB) {
|
||||
return new Date(a.get("last_message_sent_at")) >
|
||||
new Date(b.get("last_message_sent_at"))
|
||||
? -1
|
||||
: 1;
|
||||
} else {
|
||||
return unreadCountA > unreadCountB ? -1 : 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,11 @@ export default class ChatMessageVisibilityObserver extends Service {
|
|||
entries.forEach((entry) => {
|
||||
entry.target.dataset.visible = entry.isIntersecting;
|
||||
|
||||
if (entry.isIntersecting && !isTesting()) {
|
||||
if (
|
||||
!entry.target.dataset.stagedId &&
|
||||
entry.isIntersecting &&
|
||||
!isTesting()
|
||||
) {
|
||||
this.chat.updateLastReadMessage();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -86,6 +86,10 @@ export default class ChatStateManager extends Service {
|
|||
return this.router.currentRouteName?.startsWith("chat");
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return this.isFullPageActive || this.isDrawerActive;
|
||||
}
|
||||
|
||||
storeAppURL(URL = null) {
|
||||
this._appURL = URL || this.router.currentURL;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
|
||||
export default class ChatSubscriptionsManager extends Service {
|
||||
@service store;
|
||||
@service chatChannelsManager;
|
||||
@service currentUser;
|
||||
@service appEvents;
|
||||
|
||||
_channelSubscriptions = new Set();
|
||||
|
||||
startChannelSubscription(channel) {
|
||||
if (
|
||||
channel.currentUserMembership.muted ||
|
||||
this._channelSubscriptions.has(channel.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._channelSubscriptions.add(channel.id);
|
||||
|
||||
if (!channel.isDirectMessageChannel) {
|
||||
this._startChannelMentionsSubscription(channel);
|
||||
}
|
||||
|
||||
this._startChannelNewMessagesSubscription(channel);
|
||||
}
|
||||
|
||||
stopChannelSubscription(channel) {
|
||||
this.messageBus.unsubscribe(
|
||||
`/chat/${channel.id}/new-messages`,
|
||||
this._onNewMessages
|
||||
);
|
||||
if (!channel.isDirectMessageChannel) {
|
||||
this.messageBus.unsubscribe(
|
||||
`/chat/${channel.id}/new-mentions`,
|
||||
this._onNewMentions
|
||||
);
|
||||
}
|
||||
|
||||
this._channelSubscriptions.delete(channel.id);
|
||||
}
|
||||
|
||||
startChannelsSubscriptions(messageBusIds) {
|
||||
this._startNewChannelSubscription(messageBusIds.new_channel);
|
||||
this._startUserTrackingStateSubscription(messageBusIds.user_tracking_state);
|
||||
this._startChannelsEditsSubscription(messageBusIds.channel_edits);
|
||||
this._startChannelsStatusChangesSubscription(messageBusIds.channel_status);
|
||||
this._startChannelsMetadataChangesSubscription(
|
||||
messageBusIds.channel_metadata
|
||||
);
|
||||
}
|
||||
|
||||
stopChannelsSubscriptions() {
|
||||
this._stopNewChannelSubscription();
|
||||
this._stopUserTrackingStateSubscription();
|
||||
this._stopChannelsEditsSubscription();
|
||||
this._stopChannelsStatusChangesSubscription();
|
||||
this._stopChannelsMetadataChangesSubscription();
|
||||
|
||||
(this.chatChannelsManager.channels || []).forEach((channel) => {
|
||||
this.stopChannelSubscription(channel);
|
||||
});
|
||||
}
|
||||
|
||||
_startChannelMentionsSubscription(channel) {
|
||||
this.messageBus.subscribe(
|
||||
`/chat/${channel.id}/new-mentions`,
|
||||
this._onNewMentions,
|
||||
channel.meta.message_bus_last_ids.new_mentions
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onNewMentions(busData) {
|
||||
this.chatChannelsManager.find(busData.channel_id).then((channel) => {
|
||||
const membership = channel.currentUserMembership;
|
||||
if (membership) {
|
||||
membership.unread_mentions = (membership.unread_mentions || 0) + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_startChannelNewMessagesSubscription(channel) {
|
||||
this.messageBus.subscribe(
|
||||
`/chat/${channel.id}/new-messages`,
|
||||
this._onNewMessages,
|
||||
channel.meta.message_bus_last_ids.new_messages
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onNewMessages(busData) {
|
||||
this.chatChannelsManager.find(busData.channel_id).then((channel) => {
|
||||
if (busData.user_id === this.currentUser.id) {
|
||||
// User sent message, update tracking state to no unread
|
||||
channel.currentUserMembership.chat_message_id = busData.message_id;
|
||||
} else {
|
||||
// Ignored user sent message, update tracking state to no unread
|
||||
if (this.currentUser.ignored_users.includes(busData.username)) {
|
||||
channel.currentUserMembership.chat_message_id = busData.message_id;
|
||||
} else {
|
||||
// Message from other user. Increment trackings state
|
||||
if (
|
||||
busData.message_id >
|
||||
(channel.currentUserMembership.chat_message_id || 0)
|
||||
) {
|
||||
channel.currentUserMembership.unread_count =
|
||||
channel.currentUserMembership.unread_count + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel.set("last_message_sent_at", new Date());
|
||||
});
|
||||
}
|
||||
|
||||
_startUserTrackingStateSubscription(lastId) {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageBus.subscribe(
|
||||
`/chat/user-tracking-state/${this.currentUser.id}`,
|
||||
this._onUserTrackingStateUpdate,
|
||||
lastId
|
||||
);
|
||||
}
|
||||
|
||||
_stopUserTrackingStateSubscription() {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageBus.unsubscribe(
|
||||
`/chat/user-tracking-state/${this.currentUser.id}`,
|
||||
this._onUserTrackingStateUpdate
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onUserTrackingStateUpdate(data) {
|
||||
this.chatChannelsManager.find(data.chat_channel_id).then((channel) => {
|
||||
if (
|
||||
channel?.currentUserMembership?.chat_message_id < data.chat_message_id
|
||||
) {
|
||||
channel.currentUserMembership.chat_message_id = data.chat_message_id;
|
||||
channel.currentUserMembership.unread_count = 0;
|
||||
channel.currentUserMembership.unread_mentions = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_startNewChannelSubscription(lastId) {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/new-channel",
|
||||
this._onNewChannelSubscription,
|
||||
lastId
|
||||
);
|
||||
}
|
||||
|
||||
_stopNewChannelSubscription() {
|
||||
this.messageBus.unsubscribe(
|
||||
"/chat/new-channel",
|
||||
this._onNewChannelSubscription
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onNewChannelSubscription(data) {
|
||||
this.chatChannelsManager.find(data.channel.id).then((channel) => {
|
||||
// we need to refrehs here to have correct last message ids
|
||||
channel.meta = data.channel.meta;
|
||||
|
||||
if (
|
||||
channel.isDirectMessageChannel &&
|
||||
!channel.currentUserMembership.following
|
||||
) {
|
||||
channel.currentUserMembership.unread_count = 1;
|
||||
}
|
||||
|
||||
this.chatChannelsManager.follow(channel);
|
||||
});
|
||||
}
|
||||
|
||||
_startChannelsMetadataChangesSubscription(lastId) {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-metadata",
|
||||
this._onChannelMetadata,
|
||||
lastId
|
||||
);
|
||||
}
|
||||
|
||||
_startChannelsEditsSubscription(lastId) {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-edits",
|
||||
this._onChannelEdits,
|
||||
lastId
|
||||
);
|
||||
}
|
||||
|
||||
_startChannelsStatusChangesSubscription(lastId) {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-status",
|
||||
this._onChannelStatus,
|
||||
lastId
|
||||
);
|
||||
}
|
||||
|
||||
_stopChannelsStatusChangesSubscription() {
|
||||
this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus);
|
||||
}
|
||||
|
||||
_stopChannelsEditsSubscription() {
|
||||
this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits);
|
||||
}
|
||||
|
||||
_stopChannelsMetadataChangesSubscription() {
|
||||
this.messageBus.unsubscribe(
|
||||
"/chat/channel-metadata",
|
||||
this._onChannelMetadata
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelMetadata(busData) {
|
||||
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
|
||||
if (channel) {
|
||||
channel.setProperties({
|
||||
memberships_count: busData.memberships_count,
|
||||
});
|
||||
this.appEvents.trigger("chat:refresh-channel-members");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelEdits(busData) {
|
||||
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
|
||||
if (channel) {
|
||||
channel.setProperties({
|
||||
title: busData.name,
|
||||
description: busData.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelStatus(busData) {
|
||||
this.chatChannelsManager.find(busData.chat_channel_id).then((channel) => {
|
||||
channel.set("status", busData.status);
|
||||
|
||||
// it is not possible for the user to set their last read message id
|
||||
// if the channel has been archived, because all the messages have
|
||||
// been deleted. we don't want them seeing the blue dot anymore so
|
||||
// just completely reset the unreads
|
||||
if (busData.status === CHANNEL_STATUSES.archived) {
|
||||
channel.currentUserMembership.unread_count = 0;
|
||||
channel.currentUserMembership.unread_mentions = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,22 +5,15 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import Site from "discourse/models/site";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { A } from "@ember/array";
|
||||
import { generateCookFunction } from "discourse/lib/text";
|
||||
import { cancel, next } from "@ember/runloop";
|
||||
import { and } from "@ember/object/computed";
|
||||
import { computed } from "@ember/object";
|
||||
import { Promise } from "rsvp";
|
||||
import ChatChannel, {
|
||||
CHANNEL_STATUSES,
|
||||
CHATABLE_TYPES,
|
||||
} from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import EmberObject, { computed } from "@ember/object";
|
||||
import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import userPresent from "discourse/lib/user-presence";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export const LIST_VIEW = "list_view";
|
||||
export const CHAT_VIEW = "chat_view";
|
||||
|
@ -36,30 +29,21 @@ const READ_INTERVAL = 1000;
|
|||
export default class Chat extends Service {
|
||||
@service appEvents;
|
||||
@service chatNotificationManager;
|
||||
@service chatSubscriptionsManager;
|
||||
@service chatStateManager;
|
||||
@service presence;
|
||||
@service router;
|
||||
@service site;
|
||||
@service chatChannelsManager;
|
||||
|
||||
activeChannel = null;
|
||||
allChannels = null;
|
||||
cook = null;
|
||||
directMessageChannels = null;
|
||||
hasFetchedChannels = false;
|
||||
hasUnreadMessages = false;
|
||||
idToTitleMap = null;
|
||||
lastUserTrackingMessageId = null;
|
||||
|
||||
messageId = null;
|
||||
presenceChannel = null;
|
||||
publicChannels = null;
|
||||
sidebarActive = false;
|
||||
unreadUrgentCount = null;
|
||||
directMessagesLimit = 20;
|
||||
isNetworkUnreliable = false;
|
||||
@and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat;
|
||||
_fetchingChannels = null;
|
||||
_onNewMentionsCallbacks = new Map();
|
||||
_onNewMessagesCallbacks = new Map();
|
||||
|
||||
@computed("currentUser.staff", "currentUser.groups.[]")
|
||||
get userCanDirectMessage() {
|
||||
|
@ -81,7 +65,6 @@ export default class Chat extends Service {
|
|||
super.init(...arguments);
|
||||
|
||||
if (this.userCanChat) {
|
||||
this.set("allChannels", []);
|
||||
this.presenceChannel = this.presence.getChannel("/chat/online");
|
||||
this.draftStore = {};
|
||||
|
||||
|
@ -114,38 +97,24 @@ export default class Chat extends Service {
|
|||
}
|
||||
|
||||
setupWithPreloadedChannels(channels) {
|
||||
this.currentUser.set("chat_channel_tracking_state", {});
|
||||
this._processChannels(channels || {});
|
||||
this.subscribeToChannelMessageBus();
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
this.appEvents.trigger("chat:refresh-channels");
|
||||
}
|
||||
this.chatSubscriptionsManager.startChannelsSubscriptions(
|
||||
channels.meta.message_bus_last_ids
|
||||
);
|
||||
this.presenceChannel.subscribe(channels.global_presence_channel_state);
|
||||
|
||||
setupWithoutPreloadedChannels() {
|
||||
this.getChannels().then(() => {
|
||||
this.subscribeToChannelMessageBus();
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToChannelMessageBus() {
|
||||
this._subscribeToNewChannelUpdates();
|
||||
this._subscribeToUserTrackingChannel();
|
||||
this._subscribeToChannelEdits();
|
||||
this._subscribeToChannelMetadata();
|
||||
this._subscribeToChannelStatusChange();
|
||||
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
||||
(channelObject) => {
|
||||
const channel = this.chatChannelsManager.store(channelObject);
|
||||
return this.chatChannelsManager.follow(channel);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
|
||||
if (this.userCanChat) {
|
||||
this.set("allChannels", null);
|
||||
this._unsubscribeFromNewDmChannelUpdates();
|
||||
this._unsubscribeFromUserTrackingChannel();
|
||||
this._unsubscribeFromChannelEdits();
|
||||
this._unsubscribeFromChannelMetadata();
|
||||
this._unsubscribeFromChannelStatusChange();
|
||||
this._unsubscribeFromAllChatChannels();
|
||||
this.chatSubscriptionsManager.stopChannelsSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,10 +155,7 @@ export default class Chat extends Service {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.chatStateManager.isFullPageActive ||
|
||||
this.chatStateManager.isDrawerActive
|
||||
) {
|
||||
if (this.chatStateManager.isActive) {
|
||||
this.presenceChannel.enter({ activeOptions: CHAT_ONLINE_OPTIONS });
|
||||
} else {
|
||||
this.presenceChannel.leave();
|
||||
|
@ -199,61 +165,10 @@ export default class Chat extends Service {
|
|||
|
||||
getDocumentTitleCount() {
|
||||
return this.chatNotificationManager.shouldCountChatInDocTitle()
|
||||
? this.unreadUrgentCount
|
||||
? this.chatChannelsManager.unreadUrgentCount
|
||||
: 0;
|
||||
}
|
||||
|
||||
_channelObject() {
|
||||
return {
|
||||
publicChannels: this.publicChannels,
|
||||
directMessageChannels: this.directMessageChannels,
|
||||
};
|
||||
}
|
||||
|
||||
truncateDirectMessageChannels(channels) {
|
||||
return channels.slice(0, this.directMessagesLimit);
|
||||
}
|
||||
|
||||
async getChannelsWithFilter(filter, opts = { excludeActiveChannel: true }) {
|
||||
let sortedChannels = this.allChannels.sort((a, b) => {
|
||||
return new Date(a.last_message_sent_at) > new Date(b.last_message_sent_at)
|
||||
? -1
|
||||
: 1;
|
||||
});
|
||||
|
||||
const trimmedFilter = filter.trim();
|
||||
const lowerCasedFilter = filter.toLowerCase();
|
||||
const { activeChannel } = this;
|
||||
|
||||
return sortedChannels.filter((channel) => {
|
||||
if (
|
||||
opts.excludeActiveChannel &&
|
||||
activeChannel &&
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switchChannelUpOrDown(direction) {
|
||||
const { activeChannel } = this;
|
||||
if (!activeChannel) {
|
||||
|
@ -262,15 +177,11 @@ export default class Chat extends Service {
|
|||
|
||||
let currentList, otherList;
|
||||
if (activeChannel.isDirectMessageChannel) {
|
||||
currentList = this.truncateDirectMessageChannels(
|
||||
this.directMessageChannels
|
||||
);
|
||||
otherList = this.publicChannels;
|
||||
currentList = this.chatChannelsManager.truncatedDirectMessageChannels;
|
||||
otherList = this.chatChannelsManager.publicMessageChannels;
|
||||
} else {
|
||||
currentList = this.publicChannels;
|
||||
otherList = this.truncateDirectMessageChannels(
|
||||
this.directMessageChannels
|
||||
);
|
||||
currentList = this.chatChannelsManager.publicMessageChannels;
|
||||
otherList = this.chatChannelsManager.truncatedDirectMessageChannels;
|
||||
}
|
||||
|
||||
const directionUp = direction === "up";
|
||||
|
@ -296,109 +207,6 @@ export default class Chat extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
getChannels() {
|
||||
return new Promise((resolve) => {
|
||||
if (this.hasFetchedChannels) {
|
||||
return resolve(this._channelObject());
|
||||
}
|
||||
|
||||
if (!this._fetchingChannels) {
|
||||
this._fetchingChannels = this._refreshChannels();
|
||||
}
|
||||
|
||||
this._fetchingChannels
|
||||
.then(() => resolve(this._channelObject()))
|
||||
.finally(() => (this._fetchingChannels = null));
|
||||
});
|
||||
}
|
||||
|
||||
forceRefreshChannels() {
|
||||
this.set("hasFetchedChannels", false);
|
||||
this._unsubscribeFromAllChatChannels();
|
||||
return this.getChannels();
|
||||
}
|
||||
|
||||
refreshTrackingState() {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ajax("/chat/chat_channels.json")
|
||||
.then((response) => {
|
||||
this.currentUser.set("chat_channel_tracking_state", {});
|
||||
(response.direct_message_channels || []).forEach((channel) => {
|
||||
this._updateUserTrackingState(channel);
|
||||
});
|
||||
(response.public_channels || []).forEach((channel) => {
|
||||
this._updateUserTrackingState(channel);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
});
|
||||
}
|
||||
|
||||
_refreshChannels() {
|
||||
return new Promise((resolve) => {
|
||||
this.setProperties({
|
||||
loading: true,
|
||||
allChannels: [],
|
||||
});
|
||||
this.currentUser.set("chat_channel_tracking_state", {});
|
||||
ajax("/chat/chat_channels.json").then((channels) => {
|
||||
this._processChannels(channels);
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
this.appEvents.trigger("chat:refresh-channels");
|
||||
resolve(this._channelObject());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_processChannels(channels) {
|
||||
// Must be set first because `processChannels` relies on this data.
|
||||
this.set("messageBusLastIds", channels.message_bus_last_ids);
|
||||
this.setProperties({
|
||||
publicChannels: A(
|
||||
this.sortPublicChannels(
|
||||
(channels.public_channels || []).map((channel) =>
|
||||
this.processChannel(channel)
|
||||
)
|
||||
)
|
||||
),
|
||||
directMessageChannels: A(
|
||||
this.sortDirectMessageChannels(
|
||||
(channels.direct_message_channels || []).map((channel) =>
|
||||
this.processChannel(channel)
|
||||
)
|
||||
)
|
||||
),
|
||||
hasFetchedChannels: true,
|
||||
loading: false,
|
||||
});
|
||||
const idToTitleMap = {};
|
||||
this.allChannels.forEach((c) => {
|
||||
idToTitleMap[c.id] = c.title;
|
||||
});
|
||||
this.set("idToTitleMap", idToTitleMap);
|
||||
this.presenceChannel.subscribe(channels.global_presence_channel_state);
|
||||
}
|
||||
|
||||
reSortDirectMessageChannels() {
|
||||
this.set(
|
||||
"directMessageChannels",
|
||||
this.sortDirectMessageChannels(this.directMessageChannels)
|
||||
);
|
||||
}
|
||||
|
||||
async getChannelBy(key, value) {
|
||||
return this.getChannels().then(() => {
|
||||
if (!isNaN(value)) {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
return (this.allChannels || []).findBy(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
searchPossibleDirectMessageUsers(options) {
|
||||
// TODO: implement a chat specific user search function
|
||||
return userSearch(options);
|
||||
|
@ -414,99 +222,54 @@ export default class Chat extends Service {
|
|||
// if that is present and in the list of channels the user can access.
|
||||
// If none of these options exist, then we get the first public channel,
|
||||
// or failing that the first DM channel.
|
||||
return this.getChannels().then(() => {
|
||||
// Defined in order of significance.
|
||||
let publicChannelWithMention,
|
||||
dmChannelWithUnread,
|
||||
publicChannelWithUnread,
|
||||
publicChannel,
|
||||
dmChannel,
|
||||
defaultChannel;
|
||||
// Defined in order of significance.
|
||||
let publicChannelWithMention,
|
||||
dmChannelWithUnread,
|
||||
publicChannelWithUnread,
|
||||
publicChannel,
|
||||
dmChannel,
|
||||
defaultChannel;
|
||||
|
||||
for (const [channel, state] of Object.entries(
|
||||
this.currentUser.chat_channel_tracking_state
|
||||
)) {
|
||||
if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) {
|
||||
if (!dmChannelWithUnread && state.unread_count > 0) {
|
||||
dmChannelWithUnread = channel;
|
||||
} else if (!dmChannel) {
|
||||
dmChannel = channel;
|
||||
}
|
||||
} else {
|
||||
if (state.unread_mentions > 0) {
|
||||
publicChannelWithMention = channel;
|
||||
break; // <- We have a public channel with a mention. Break and return this.
|
||||
} else if (!publicChannelWithUnread && state.unread_count > 0) {
|
||||
publicChannelWithUnread = channel;
|
||||
} else if (
|
||||
!defaultChannel &&
|
||||
parseInt(this.siteSettings.chat_default_channel_id || 0, 10) ===
|
||||
parseInt(channel, 10)
|
||||
) {
|
||||
defaultChannel = channel;
|
||||
} else if (!publicChannel) {
|
||||
publicChannel = channel;
|
||||
}
|
||||
this.chatChannelsManager.channels.forEach((channel) => {
|
||||
const membership = channel.currentUserMembership;
|
||||
|
||||
if (channel.isDirectMessageChannel) {
|
||||
if (!dmChannelWithUnread && membership.unread_count > 0) {
|
||||
dmChannelWithUnread = channel.id;
|
||||
} else if (!dmChannel) {
|
||||
dmChannel = channel.id;
|
||||
}
|
||||
} else {
|
||||
if (membership.unread_mentions > 0) {
|
||||
publicChannelWithMention = channel.id;
|
||||
return; // <- We have a public channel with a mention. Break and return this.
|
||||
} else if (!publicChannelWithUnread && membership.unread_count > 0) {
|
||||
publicChannelWithUnread = channel.id;
|
||||
} else if (
|
||||
!defaultChannel &&
|
||||
parseInt(this.siteSettings.chat_default_channel_id || 0, 10) ===
|
||||
channel.id
|
||||
) {
|
||||
defaultChannel = channel.id;
|
||||
} else if (!publicChannel) {
|
||||
publicChannel = channel.id;
|
||||
}
|
||||
}
|
||||
return (
|
||||
publicChannelWithMention ||
|
||||
dmChannelWithUnread ||
|
||||
publicChannelWithUnread ||
|
||||
defaultChannel ||
|
||||
publicChannel ||
|
||||
dmChannel
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sortPublicChannels(channels) {
|
||||
return channels.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
sortDirectMessageChannels(channels) {
|
||||
return channels.sort((a, b) => {
|
||||
const unreadCountA =
|
||||
this.currentUser.chat_channel_tracking_state[a.id]?.unread_count || 0;
|
||||
const unreadCountB =
|
||||
this.currentUser.chat_channel_tracking_state[b.id]?.unread_count || 0;
|
||||
if (unreadCountA === unreadCountB) {
|
||||
return new Date(a.last_message_sent_at) >
|
||||
new Date(b.last_message_sent_at)
|
||||
? -1
|
||||
: 1;
|
||||
} else {
|
||||
return unreadCountA > unreadCountB ? -1 : 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIdealFirstChannelIdAndTitle() {
|
||||
return this.getIdealFirstChannelId().then((channelId) => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
id: channelId,
|
||||
title: this.idToTitleMap[channelId],
|
||||
};
|
||||
});
|
||||
return (
|
||||
publicChannelWithMention ||
|
||||
dmChannelWithUnread ||
|
||||
publicChannelWithUnread ||
|
||||
defaultChannel ||
|
||||
publicChannel ||
|
||||
dmChannel
|
||||
);
|
||||
}
|
||||
|
||||
async openChannelAtMessage(channelId, messageId = null) {
|
||||
let channel = await this.getChannelBy("id", channelId);
|
||||
if (channel) {
|
||||
return this.chatChannelsManager.find(channelId).then((channel) => {
|
||||
return this._openFoundChannelAtMessage(channel, messageId);
|
||||
}
|
||||
|
||||
return ajax(`/chat/chat_channels/${channelId}`).then((response) => {
|
||||
const queryParams = messageId ? { messageId } : {};
|
||||
return this.router.transitionTo(
|
||||
"chat.channel",
|
||||
response.id,
|
||||
slugifyChannel(response),
|
||||
{ queryParams }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -559,380 +322,18 @@ export default class Chat extends Service {
|
|||
this.appEvents.trigger("chat-live-pane:highlight-message", messageId);
|
||||
}
|
||||
|
||||
async startTrackingChannel(channel) {
|
||||
if (!channel.current_user_membership.following) {
|
||||
return;
|
||||
}
|
||||
|
||||
let existingChannel = await this.getChannelBy("id", channel.id);
|
||||
if (existingChannel) {
|
||||
return existingChannel; // User is already tracking this channel. return!
|
||||
}
|
||||
|
||||
const existingChannels = channel.isDirectMessageChannel
|
||||
? this.directMessageChannels
|
||||
: this.publicChannels;
|
||||
|
||||
// this check shouldn't be needed given the previous check to existingChannel
|
||||
// this is a safety net, to ensure we never track duplicated channels
|
||||
existingChannel = existingChannels.findBy("id", channel.id);
|
||||
if (existingChannel) {
|
||||
return existingChannel;
|
||||
}
|
||||
|
||||
const newChannel = this.processChannel(channel);
|
||||
existingChannels.pushObject(newChannel);
|
||||
this.currentUser.chat_channel_tracking_state[channel.id] =
|
||||
EmberObject.create({
|
||||
unread_count: 1,
|
||||
unread_mentions: 0,
|
||||
chatable_type: channel.chatable_type,
|
||||
});
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
if (channel.isDirectMessageChannel) {
|
||||
this.reSortDirectMessageChannels();
|
||||
}
|
||||
if (channel.isPublicChannel) {
|
||||
this.set("publicChannels", this.sortPublicChannels(this.publicChannels));
|
||||
}
|
||||
this.appEvents.trigger("chat:refresh-channels");
|
||||
return newChannel;
|
||||
}
|
||||
|
||||
async stopTrackingChannel(channel) {
|
||||
return this.getChannelBy("id", channel.id).then((existingChannel) => {
|
||||
if (existingChannel) {
|
||||
return this.forceRefreshChannels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_subscribeToChannelMetadata() {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-metadata",
|
||||
this._onChannelMetadata,
|
||||
this.messageBusLastIds.channel_metadata
|
||||
);
|
||||
}
|
||||
|
||||
_subscribeToChannelEdits() {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/channel-edits",
|
||||
this._onChannelEdits,
|
||||
this.messageBusLastIds.channel_edits
|
||||
);
|
||||
}
|
||||
|
||||
_subscribeToChannelStatusChange() {
|
||||
this.messageBus.subscribe("/chat/channel-status", this._onChannelStatus);
|
||||
}
|
||||
|
||||
_unsubscribeFromChannelStatusChange() {
|
||||
this.messageBus.unsubscribe("/chat/channel-status", this._onChannelStatus);
|
||||
}
|
||||
|
||||
_unsubscribeFromChannelEdits() {
|
||||
this.messageBus.unsubscribe("/chat/channel-edits", this._onChannelEdits);
|
||||
}
|
||||
|
||||
_unsubscribeFromChannelMetadata() {
|
||||
this.messageBus.unsubscribe(
|
||||
"/chat/channel-metadata",
|
||||
this._onChannelMetadata
|
||||
);
|
||||
}
|
||||
|
||||
_subscribeToNewChannelUpdates() {
|
||||
this.messageBus.subscribe(
|
||||
"/chat/new-channel",
|
||||
this._onNewChannel,
|
||||
this.messageBusLastIds.new_channel
|
||||
);
|
||||
}
|
||||
|
||||
_unsubscribeFromNewDmChannelUpdates() {
|
||||
this.messageBus.unsubscribe("/chat/new-channel", this._onNewChannel);
|
||||
}
|
||||
|
||||
_subscribeToSingleUpdateChannel(channel) {
|
||||
if (channel.current_user_membership.muted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We do this first so we don't multi-subscribe to mention + messages
|
||||
// messageBus channels for this chat channel, since _subscribeToSingleUpdateChannel
|
||||
// is called from multiple places.
|
||||
this._unsubscribeFromChatChannel(channel);
|
||||
|
||||
if (!channel.isDirectMessageChannel) {
|
||||
this._subscribeToMentionChannel(channel);
|
||||
}
|
||||
|
||||
this._subscribeToNewMessagesChannel(channel);
|
||||
}
|
||||
|
||||
_subscribeToMentionChannel(channel) {
|
||||
const onNewMentions = () => {
|
||||
const trackingState =
|
||||
this.currentUser.chat_channel_tracking_state[channel.id];
|
||||
|
||||
if (trackingState) {
|
||||
const count = (trackingState.unread_mentions || 0) + 1;
|
||||
trackingState.set("unread_mentions", count);
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
}
|
||||
};
|
||||
|
||||
this._onNewMentionsCallbacks.set(channel.id, onNewMentions);
|
||||
|
||||
this.messageBus.subscribe(
|
||||
`/chat/${channel.id}/new-mentions`,
|
||||
onNewMentions,
|
||||
channel.message_bus_last_ids.new_mentions
|
||||
);
|
||||
}
|
||||
|
||||
_subscribeToNewMessagesChannel(channel) {
|
||||
const onNewMessages = (busData) => {
|
||||
const trackingState =
|
||||
this.currentUser.chat_channel_tracking_state[channel.id];
|
||||
|
||||
if (busData.user_id === this.currentUser.id) {
|
||||
// User sent message, update tracking state to no unread
|
||||
trackingState.set("chat_message_id", busData.message_id);
|
||||
} else {
|
||||
// Ignored user sent message, update tracking state to no unread
|
||||
if (this.currentUser.ignored_users.includes(busData.username)) {
|
||||
trackingState.set("chat_message_id", busData.message_id);
|
||||
} else {
|
||||
// Message from other user. Increment trackings state
|
||||
if (busData.message_id > (trackingState.chat_message_id || 0)) {
|
||||
trackingState.set("unread_count", trackingState.unread_count + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
channel.set("last_message_sent_at", new Date());
|
||||
|
||||
const directMessageChannel = (this.directMessageChannels || []).findBy(
|
||||
"id",
|
||||
parseInt(channel.id, 10)
|
||||
);
|
||||
|
||||
if (directMessageChannel) {
|
||||
this.reSortDirectMessageChannels();
|
||||
}
|
||||
};
|
||||
|
||||
this._onNewMessagesCallbacks.set(channel.id, onNewMessages);
|
||||
|
||||
this.messageBus.subscribe(
|
||||
`/chat/${channel.id}/new-messages`,
|
||||
onNewMessages,
|
||||
channel.message_bus_last_ids.new_messages
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelMetadata(busData) {
|
||||
this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
|
||||
if (channel) {
|
||||
channel.setProperties({
|
||||
memberships_count: busData.memberships_count,
|
||||
});
|
||||
this.appEvents.trigger("chat:refresh-channel-members");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelEdits(busData) {
|
||||
this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
|
||||
if (channel) {
|
||||
channel.setProperties({
|
||||
title: busData.name,
|
||||
description: busData.description,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_onChannelStatus(busData) {
|
||||
this.getChannelBy("id", busData.chat_channel_id).then((channel) => {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.set("status", busData.status);
|
||||
|
||||
// it is not possible for the user to set their last read message id
|
||||
// if the channel has been archived, because all the messages have
|
||||
// been deleted. we don't want them seeing the blue dot anymore so
|
||||
// just completely reset the unreads
|
||||
if (busData.status === CHANNEL_STATUSES.archived) {
|
||||
this.currentUser.chat_channel_tracking_state[channel.id] = {
|
||||
unread_count: 0,
|
||||
unread_mentions: 0,
|
||||
chatable_type: channel.chatable_type,
|
||||
};
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
}
|
||||
|
||||
this.appEvents.trigger("chat:refresh-channel", channel.id);
|
||||
}, this.messageBusLastIds.channel_status);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onNewChannel(busData) {
|
||||
this.startTrackingChannel(ChatChannel.create(busData.chat_channel));
|
||||
}
|
||||
|
||||
async followChannel(channel) {
|
||||
return ChatApi.followChatChannel(channel).then(() => {
|
||||
this.startTrackingChannel(channel);
|
||||
this._subscribeToSingleUpdateChannel(channel);
|
||||
});
|
||||
return this.chatChannelsManager.follow(channel);
|
||||
}
|
||||
|
||||
async unfollowChannel(channel) {
|
||||
return ChatApi.unfollowChatChannel(channel).then(() => {
|
||||
this._unsubscribeFromChatChannel(channel);
|
||||
this.stopTrackingChannel(channel);
|
||||
|
||||
return this.chatChannelsManager.unfollow(channel).then(() => {
|
||||
if (channel === this.activeChannel && channel.isDirectMessageChannel) {
|
||||
this.router.transitionTo("chat");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_unsubscribeFromAllChatChannels() {
|
||||
(this.allChannels || []).forEach((channel) => {
|
||||
this._unsubscribeFromChatChannel(channel);
|
||||
});
|
||||
}
|
||||
|
||||
_unsubscribeFromChatChannel(channel) {
|
||||
this.messageBus.unsubscribe("/chat/*", this._onNewMessagesCallbacks);
|
||||
if (!channel.isDirectMessageChannel) {
|
||||
this.messageBus.unsubscribe("/chat/*", this._onNewMentionsCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
_subscribeToUserTrackingChannel() {
|
||||
this.messageBus.subscribe(
|
||||
`/chat/user-tracking-state/${this.currentUser.id}`,
|
||||
this._onUserTrackingState,
|
||||
this.messageBusLastIds.user_tracking_state
|
||||
);
|
||||
}
|
||||
|
||||
_unsubscribeFromUserTrackingChannel() {
|
||||
this.messageBus.unsubscribe(
|
||||
`/chat/user-tracking-state/${this.currentUser.id}`,
|
||||
this._onUserTrackingState
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
_onUserTrackingState(busData, _, messageId) {
|
||||
const lastId = this.lastUserTrackingMessageId;
|
||||
|
||||
// we don't want this state to go backwards, only catch
|
||||
// up if messages from messagebus were missed
|
||||
if (!lastId || messageId > lastId) {
|
||||
this.lastUserTrackingMessageId = messageId;
|
||||
}
|
||||
|
||||
// we are too far out of sync, we should resync everything.
|
||||
// this will trigger a route transition and blur the chat input
|
||||
if (lastId && messageId > lastId + 1) {
|
||||
return this.forceRefreshChannels();
|
||||
}
|
||||
|
||||
const trackingState =
|
||||
this.currentUser.chat_channel_tracking_state[busData.chat_channel_id];
|
||||
|
||||
if (trackingState) {
|
||||
trackingState.set("chat_message_id", busData.chat_message_id);
|
||||
trackingState.set("unread_count", 0);
|
||||
trackingState.set("unread_mentions", 0);
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
resetTrackingStateForChannel(channelId) {
|
||||
const trackingState =
|
||||
this.currentUser.chat_channel_tracking_state[channelId];
|
||||
if (trackingState) {
|
||||
trackingState.set("unread_count", 0);
|
||||
this.userChatChannelTrackingStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
userChatChannelTrackingStateChanged() {
|
||||
this._recalculateUnreadMessages();
|
||||
this.appEvents.trigger("chat:user-tracking-state-changed");
|
||||
}
|
||||
|
||||
_recalculateUnreadMessages() {
|
||||
let unreadPublicCount = 0;
|
||||
let unreadUrgentCount = 0;
|
||||
let headerNeedsRerender = false;
|
||||
|
||||
Object.values(this.currentUser.chat_channel_tracking_state).forEach(
|
||||
(state) => {
|
||||
if (state.muted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.chatable_type === CHATABLE_TYPES.directMessageChannel) {
|
||||
unreadUrgentCount += state.unread_count || 0;
|
||||
} else {
|
||||
unreadUrgentCount += state.unread_mentions || 0;
|
||||
unreadPublicCount += state.unread_count || 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let hasUnreadPublic = unreadPublicCount > 0;
|
||||
if (hasUnreadPublic !== this.hasUnreadMessages) {
|
||||
headerNeedsRerender = true;
|
||||
this.set("hasUnreadMessages", hasUnreadPublic);
|
||||
}
|
||||
|
||||
if (unreadUrgentCount !== this.unreadUrgentCount) {
|
||||
headerNeedsRerender = true;
|
||||
this.set("unreadUrgentCount", unreadUrgentCount);
|
||||
}
|
||||
|
||||
this.currentUser.notifyPropertyChange("chat_channel_tracking_state");
|
||||
if (headerNeedsRerender) {
|
||||
this.appEvents.trigger("chat:rerender-header");
|
||||
this.appEvents.trigger("notifications:changed");
|
||||
}
|
||||
}
|
||||
|
||||
processChannel(channel) {
|
||||
channel = ChatChannel.create(channel);
|
||||
this._subscribeToSingleUpdateChannel(channel);
|
||||
this._updateUserTrackingState(channel);
|
||||
this.allChannels.push(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
_updateUserTrackingState(channel) {
|
||||
this.currentUser.chat_channel_tracking_state[channel.id] =
|
||||
EmberObject.create({
|
||||
chatable_type: channel.chatable_type,
|
||||
muted: channel.current_user_membership.muted,
|
||||
unread_count: channel.current_user_membership.unread_count,
|
||||
unread_mentions: channel.current_user_membership.unread_mentions,
|
||||
chat_message_id: channel.current_user_membership.last_read_message_id,
|
||||
});
|
||||
}
|
||||
|
||||
upsertDmChannelForUser(channel, user) {
|
||||
const usernames = (channel.chatable.users || [])
|
||||
.mapBy("username")
|
||||
|
@ -951,9 +352,9 @@ export default class Chat extends Service {
|
|||
data: { usernames: usernames.uniq() },
|
||||
})
|
||||
.then((response) => {
|
||||
const chatChannel = ChatChannel.create(response.chat_channel);
|
||||
this.startTrackingChannel(chatChannel);
|
||||
return chatChannel;
|
||||
const channel = this.chatChannelsManager.store(response.channel);
|
||||
this.chatChannelsManager.follow(channel);
|
||||
return channel;
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
@ -1031,17 +432,8 @@ export default class Chat extends Service {
|
|||
10
|
||||
);
|
||||
|
||||
const hasUnreadMessages = latestUnreadMsgId > channel.lastSendReadMessageId;
|
||||
|
||||
if (
|
||||
!hasUnreadMessages &&
|
||||
this.currentUser.chat_channel_tracking_state[this.activeChannel.id]
|
||||
?.unread_count > 0
|
||||
) {
|
||||
// Weird state here where the chat_channel_tracking_state is wrong. Need to reset it.
|
||||
this.resetTrackingStateForChannel(this.activeChannel.id);
|
||||
}
|
||||
|
||||
const hasUnreadMessages =
|
||||
latestUnreadMsgId > channel.currentUserMembership.last_read_message_id;
|
||||
if (hasUnreadMessages) {
|
||||
channel.updateLastReadMessage(latestUnreadMsgId);
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<FullPageChat @refreshModel={{route-action "refreshModel"}} />
|
||||
<FullPageChat />
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
<ChatChannelAboutView @channel={{this.model.chatChannel}} @onEditChatChannelTitle={{action "onEditChatChannelTitle"}} @onEditChatChannelDescription={{action "onEditChatChannelDescription"}} />
|
||||
<ChatChannelAboutView
|
||||
@channel={{this.model}}
|
||||
@onEditChatChannelTitle={{action "onEditChatChannelTitle"}}
|
||||
@onEditChatChannelDescription={{action "onEditChatChannelDescription"}}
|
||||
/>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<ChatChannelMembersView @channel={{this.model.chatChannel}} />
|
||||
<ChatChannelMembersView @channel={{this.model}} />
|
||||
|
|
|
@ -1 +1 @@
|
|||
<ChatChannelSettingsView @channel={{this.model.chatChannel}} />
|
||||
<ChatChannelSettingsView @channel={{this.model}} />
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
<div class="chat-channel-header-details">
|
||||
<div class="chat-full-page-header__left-actions">
|
||||
{{#if this.chatChannelInfoRouteOriginManager.isBrowse}}
|
||||
<LinkTo @route="chat.browse" class="chat-full-page-header__back-btn no-text btn-flat btn" title={{i18n "chat.channel_info.back_to_all_channel"}}>
|
||||
<LinkTo
|
||||
@route="chat.browse"
|
||||
class="chat-full-page-header__back-btn no-text btn-flat btn"
|
||||
title={{i18n "chat.channel_info.back_to_all_channel"}}
|
||||
>
|
||||
{{d-icon "chevron-left"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="chat.channel"
|
||||
@models={{array
|
||||
this.model.chatChannel.id
|
||||
(slugify-channel this.model.chatChannel)
|
||||
}}
|
||||
@models={{array this.model.id (slugify-channel this.model)}}
|
||||
class="chat-full-page-header__back-btn no-text btn-flat btn"
|
||||
title={{i18n "chat.channel_info.back_to_channel"}}
|
||||
>
|
||||
|
@ -21,7 +22,7 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ChatChannelTitle @channel={{this.model.chatChannel}} />
|
||||
<ChatChannelTitle @channel={{this.model}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -35,16 +36,13 @@
|
|||
>
|
||||
<LinkTo
|
||||
@route={{concat "chat.channel.info." tab}}
|
||||
@models={{array
|
||||
this.model.chatChannel.id
|
||||
(slugify-channel this.model.chatChannel)
|
||||
}}
|
||||
@models={{array this.model.id (slugify-channel this.model)}}
|
||||
class="chat-tabs-list__link"
|
||||
>
|
||||
<span>{{i18n (concat "chat.channel_info.tabs." tab)}}</span>
|
||||
{{#if (eq tab "members")}}
|
||||
<span class="chat-tabs__memberships-count">
|
||||
({{this.model.chatChannel.membershipsCount}})
|
||||
({{this.model.membershipsCount}})
|
||||
</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
</label>
|
||||
{{/if}}
|
||||
|
||||
<label class="create-channel-label">
|
||||
<label for="channel-name" class="create-channel-label">
|
||||
{{i18n "chat.create_channel.name"}}
|
||||
</label>
|
||||
<Input class="create-channel-name-input" @type="text" @value={{this.name}} />
|
||||
<Input name="channel-name" class="create-channel-name-input" @type="text" @value={{this.name}} />
|
||||
|
||||
<label class="create-channel-label">
|
||||
<label for="channel-description" class="create-channel-label">
|
||||
{{i18n "chat.create_channel.description"}}
|
||||
</label>
|
||||
<Input class="create-channel-description-input" @type="textarea" @value={{this.description}} />
|
||||
<Input name="channel-description" class="create-channel-description-input" @type="textarea" @value={{this.description}} />
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -1,87 +1,24 @@
|
|||
import getURL from "discourse-common/lib/get-url";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import RenderGlimmer from "discourse/widgets/render-glimmer";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
|
||||
export default createWidget("header-chat-link", {
|
||||
buildKey: () => "header-chat-link",
|
||||
chat: null,
|
||||
tagName: "li.header-dropdown-toggle.open-chat",
|
||||
title: "chat.title",
|
||||
services: ["chat", "router", "chatStateManager"],
|
||||
export default createWidget("chat-header-icon", {
|
||||
tagName: "li.header-dropdown-toggle.chat-header-icon",
|
||||
title: "chat.title_capitalized",
|
||||
|
||||
services: ["chat"],
|
||||
|
||||
html() {
|
||||
if (!this.chat.userCanChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentUser.isInDoNotDisturb()) {
|
||||
return this.chatLinkHtml();
|
||||
}
|
||||
|
||||
let indicator;
|
||||
if (this.chat.unreadUrgentCount) {
|
||||
indicator = h(
|
||||
"div.chat-channel-unread-indicator.urgent",
|
||||
{},
|
||||
h(
|
||||
"div.number-wrap",
|
||||
{},
|
||||
h("div.number", {}, this.chat.unreadUrgentCount)
|
||||
)
|
||||
);
|
||||
} else if (this.chat.hasUnreadMessages) {
|
||||
indicator = h("div.chat-channel-unread-indicator");
|
||||
}
|
||||
|
||||
return this.chatLinkHtml(indicator);
|
||||
},
|
||||
|
||||
chatLinkHtml(indicatorNode) {
|
||||
return h(
|
||||
`a.icon${
|
||||
this.chatStateManager.isFullPageActive ||
|
||||
this.chatStateManager.isDrawerActive
|
||||
? ".active"
|
||||
: ""
|
||||
}`,
|
||||
{ attributes: { tabindex: 0 } },
|
||||
[iconNode("comment"), indicatorNode].filter(Boolean)
|
||||
);
|
||||
},
|
||||
|
||||
mouseUp(e) {
|
||||
if (e.which === 2) {
|
||||
// Middle mouse click
|
||||
window.open(getURL("/chat"), "_blank").focus();
|
||||
}
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.code === "Enter") {
|
||||
return this.click();
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
if (this.chatStateManager.isFullPageActive && this.site.desktopView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chatStateManager.isFullPageActive && this.site.mobileView) {
|
||||
return this.router.transitionTo("chat");
|
||||
}
|
||||
|
||||
if (this.chatStateManager.isDrawerActive) {
|
||||
return this.router.transitionTo("chat");
|
||||
} else {
|
||||
return this.router.transitionTo(
|
||||
this.chatStateManager.lastKnownChatURL || "chat"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
chatRerenderHeader() {
|
||||
this.scheduleRerender();
|
||||
return [
|
||||
new RenderGlimmer(
|
||||
this,
|
||||
"div.widget-component-connector",
|
||||
hbs`<ChatHeaderIcon />`
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -103,8 +103,30 @@ $float-height: 530px;
|
|||
}
|
||||
}
|
||||
|
||||
.header-dropdown-toggle.open-chat {
|
||||
.header-dropdown-toggle.chat-header-icon {
|
||||
.icon {
|
||||
.chat-channel-unread-indicator {
|
||||
border: 2px solid var(--header_background);
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
transition: border-color linear 0.15s;
|
||||
}
|
||||
}
|
||||
|
||||
span.icon {
|
||||
cursor: auto;
|
||||
|
||||
&:hover {
|
||||
.d-icon {
|
||||
color: var(--header_primary-low-mid);
|
||||
}
|
||||
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.icon {
|
||||
&.active {
|
||||
.d-icon-comment {
|
||||
color: var(--primary-medium);
|
||||
|
@ -116,14 +138,6 @@ $float-height: 530px;
|
|||
border-color: var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel-unread-indicator {
|
||||
border: 2px solid var(--header_background);
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
transition: border-color linear 0.15s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ html.has-full-page-chat body {
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
.header-dropdown-toggle.open-chat {
|
||||
.header-dropdown-toggle.chat-header-icon {
|
||||
.icon {
|
||||
&.active {
|
||||
border: 1px solid var(--primary-low);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.header-dropdown-toggle.open-chat {
|
||||
.header-dropdown-toggle.chat-header-icon {
|
||||
.chat-channel-unread-indicator {
|
||||
border-color: var(--primary-very-low);
|
||||
}
|
||||
|
|
|
@ -116,10 +116,10 @@ en:
|
|||
without_membership:
|
||||
one: "%{username} has not joined this channel."
|
||||
other: "%{username} and %{others} have not joined this channel."
|
||||
group_mentions_disabled:
|
||||
group_mentions_disabled:
|
||||
one: "%{group_name} doesn't allow mentions"
|
||||
other: "%{group_name} and %{others} doesn't allow mentions"
|
||||
too_many_members:
|
||||
too_many_members:
|
||||
one: "%{group_name} has too many members. No one was notified"
|
||||
other: "%{group_name} and %{others} have too many members. No one was notified"
|
||||
warning_multiple:
|
||||
|
|
|
@ -45,6 +45,7 @@ class Chat::ChatChannelArchiveService
|
|||
:chat_channel_archive,
|
||||
chat_channel_archive_id: chat_channel.chat_channel_archive.id,
|
||||
)
|
||||
chat_channel.chat_channel_archive
|
||||
end
|
||||
|
||||
attr_reader :chat_channel_archive, :chat_channel, :chat_channel_title
|
||||
|
|
|
@ -19,14 +19,15 @@ class Chat::ChatMessageReactor
|
|||
message = ensure_chat_message!(message_id)
|
||||
validate_max_reactions!(message, react_action, emoji)
|
||||
|
||||
reaction = nil
|
||||
ActiveRecord::Base.transaction do
|
||||
enforce_channel_membership!
|
||||
create_reaction(message, react_action, emoji)
|
||||
reaction = create_reaction(message, react_action, emoji)
|
||||
end
|
||||
|
||||
publish_reaction(message, react_action, emoji)
|
||||
|
||||
message
|
||||
reaction
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -120,7 +120,6 @@ after_initialize do
|
|||
)
|
||||
load File.expand_path("../app/controllers/chat_base_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/chat_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/chat_channels_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/emojis_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/direct_messages_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/incoming_chat_webhooks_controller.rb", __FILE__)
|
||||
|
@ -207,13 +206,25 @@ after_initialize do
|
|||
load File.expand_path("../app/services/chat_message_destroyer.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/chat_channels_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/chat_channel_memberships_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/chat_current_user_channels_controller.rb", __FILE__)
|
||||
load File.expand_path(
|
||||
"../app/controllers/api/chat_channel_notifications_settings_controller.rb",
|
||||
"../app/controllers/api/chat_channels_current_user_membership_controller.rb",
|
||||
__FILE__,
|
||||
)
|
||||
load File.expand_path("../app/controllers/api/chat_channels_memberships_controller.rb", __FILE__)
|
||||
load File.expand_path(
|
||||
"../app/controllers/api/chat_channels_messages_moves_controller.rb",
|
||||
__FILE__,
|
||||
)
|
||||
load File.expand_path("../app/controllers/api/chat_channels_archives_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/chat_channels_status_controller.rb", __FILE__)
|
||||
load File.expand_path(
|
||||
"../app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb",
|
||||
__FILE__,
|
||||
)
|
||||
load File.expand_path("../app/controllers/api/category_chatables_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/hints_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/controllers/api/chat_chatables_controller.rb", __FILE__)
|
||||
load File.expand_path("../app/queries/chat_channel_memberships_query.rb", __FILE__)
|
||||
|
||||
if Discourse.allow_dev_populate?
|
||||
|
@ -245,7 +256,6 @@ after_initialize do
|
|||
Category.prepend Chat::CategoryExtension
|
||||
User.prepend Chat::UserExtension
|
||||
Jobs::UserEmail.prepend Chat::UserEmailExtension
|
||||
|
||||
Bookmark.register_bookmarkable(ChatMessageBookmarkable)
|
||||
end
|
||||
|
||||
|
@ -579,12 +589,23 @@ after_initialize do
|
|||
end
|
||||
|
||||
Chat::Engine.routes.draw do
|
||||
namespace :api do
|
||||
get "/chat_channels" => "chat_channels#index"
|
||||
get "/chat_channels/:chat_channel_id/memberships" => "chat_channel_memberships#index"
|
||||
put "/chat_channels/:chat_channel_id" => "chat_channels#update"
|
||||
put "/chat_channels/:chat_channel_id/notifications_settings" =>
|
||||
"chat_channel_notifications_settings#update"
|
||||
namespace :api, defaults: { format: :json } do
|
||||
get "/chatables" => "chat_chatables#index"
|
||||
get "/channels" => "chat_channels#index"
|
||||
get "/channels/me" => "chat_current_user_channels#index"
|
||||
post "/channels" => "chat_channels#create"
|
||||
delete "/channels/:channel_id" => "chat_channels#destroy"
|
||||
put "/channels/:channel_id" => "chat_channels#update"
|
||||
get "/channels/:channel_id" => "chat_channels#show"
|
||||
put "/channels/:channel_id/status" => "chat_channels_status#update"
|
||||
post "/channels/:channel_id/messages/moves" => "chat_channels_messages_moves#create"
|
||||
post "/channels/:channel_id/archives" => "chat_channels_archives#create"
|
||||
get "/channels/:channel_id/memberships" => "chat_channels_memberships#index"
|
||||
delete "/channels/:channel_id/memberships/me" =>
|
||||
"chat_channels_current_user_membership#destroy"
|
||||
post "/channels/:channel_id/memberships/me" => "chat_channels_current_user_membership#create"
|
||||
put "/channels/:channel_id/notifications-settings/me" =>
|
||||
"chat_channels_current_user_notifications_settings#update"
|
||||
|
||||
# Category chatables controller hints. Only used by staff members, we don't want to leak category permissions.
|
||||
get "/category-chatables/:id/permissions" => "category_chatables#permissions",
|
||||
|
@ -607,18 +628,9 @@ after_initialize do
|
|||
|
||||
# chat_channel_controller routes
|
||||
get "/chat_channels" => "chat_channels#index"
|
||||
put "/chat_channels" => "chat_channels#create"
|
||||
get "/chat_channels/search" => "chat_channels#search"
|
||||
post "/chat_channels/:chat_channel_id" => "chat_channels#edit"
|
||||
post "/chat_channels/:chat_channel_id/notification_settings" =>
|
||||
"chat_channels#notification_settings"
|
||||
post "/chat_channels/:chat_channel_id/follow" => "chat_channels#follow"
|
||||
post "/chat_channels/:chat_channel_id/unfollow" => "chat_channels#unfollow"
|
||||
get "/chat_channels/:chat_channel_id" => "chat_channels#show"
|
||||
put "/chat_channels/:chat_channel_id/archive" => "chat_channels#archive"
|
||||
put "/chat_channels/:chat_channel_id/retry_archive" => "chat_channels#retry_archive"
|
||||
put "/chat_channels/:chat_channel_id/change_status" => "chat_channels#change_status"
|
||||
delete "/chat_channels/:chat_channel_id" => "chat_channels#destroy"
|
||||
|
||||
# chat_controller routes
|
||||
get "/" => "chat#respond"
|
||||
|
@ -645,7 +657,6 @@ after_initialize do
|
|||
put "/:chat_channel_id/:message_id/rebake" => "chat#rebake"
|
||||
post "/:chat_channel_id/:message_id/flag" => "chat#flag"
|
||||
post "/:chat_channel_id/quote" => "chat#quote_messages"
|
||||
put "/:chat_channel_id/move_messages_to_channel" => "chat#move_messages_to_channel"
|
||||
put "/:chat_channel_id/restore/:message_id" => "chat#restore"
|
||||
get "/lookup/:message_id" => "chat#lookup_message"
|
||||
put "/:chat_channel_id/read/:message_id" => "chat#update_user_last_read"
|
||||
|
@ -742,7 +753,7 @@ after_initialize do
|
|||
}
|
||||
|
||||
register_user_destroyer_on_content_deletion_callback(
|
||||
Proc.new { |user| Jobs.enqueue(:delete_user_messages, user_id: user.id) }
|
||||
Proc.new { |user| Jobs.enqueue(:delete_user_messages, user_id: user.id) },
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -16,19 +16,37 @@ Fabricator(:chat_channel) do
|
|||
end
|
||||
chatable { Fabricate(:category) }
|
||||
type do |attrs|
|
||||
attrs[:chatable_type] == "Category" || attrs[:chatable]&.is_a?(Category) ? "CategoryChannel" : "DirectMessageChannel"
|
||||
if attrs[:chatable_type] == "Category" || attrs[:chatable]&.is_a?(Category)
|
||||
"CategoryChannel"
|
||||
else
|
||||
"DirectMessageChannel"
|
||||
end
|
||||
end
|
||||
status { :open }
|
||||
end
|
||||
|
||||
Fabricator(:category_channel, from: :chat_channel, class_name: :category_channel) {}
|
||||
|
||||
Fabricator(:private_category_channel, from: :category_channel, class_name: :category_channel) do
|
||||
transient :group
|
||||
chatable { |attrs| Fabricate(:private_category, group: attrs[:group] || Group[:staff]) }
|
||||
end
|
||||
|
||||
Fabricator(:direct_message_channel, from: :chat_channel, class_name: :direct_message_channel) do
|
||||
transient :users
|
||||
transient :users, following: true, with_membership: true
|
||||
chatable do |attrs|
|
||||
Fabricate(:direct_message, users: attrs[:users] || [Fabricate(:user), Fabricate(:user)])
|
||||
end
|
||||
status { :open }
|
||||
name nil
|
||||
after_create do |channel, attrs|
|
||||
if attrs[:with_membership]
|
||||
channel.chatable.users.each do |user|
|
||||
membership = channel.add(user)
|
||||
membership.update!(following: false) if attrs[:following] == false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Fabricator(:chat_message) do
|
||||
|
@ -37,6 +55,7 @@ Fabricator(:chat_message) do
|
|||
message "Beep boop"
|
||||
cooked { |attrs| ChatMessage.cook(attrs[:message]) }
|
||||
cooked_version ChatMessage::BAKED_VERSION
|
||||
in_reply_to nil
|
||||
end
|
||||
|
||||
Fabricator(:chat_mention) do
|
||||
|
@ -49,6 +68,9 @@ Fabricator(:chat_message_reaction) do
|
|||
chat_message { Fabricate(:chat_message) }
|
||||
user { Fabricate(:user) }
|
||||
emoji { %w[+1 tada heart joffrey_facepalm].sample }
|
||||
after_build do |chat_message_reaction|
|
||||
chat_message_reaction.chat_message.chat_channel.add(chat_message_reaction.user)
|
||||
end
|
||||
end
|
||||
|
||||
Fabricator(:chat_upload) do
|
||||
|
|
|
@ -138,7 +138,7 @@ describe Jobs::AutoJoinChannelBatch do
|
|||
end
|
||||
|
||||
expect(messages.size).to eq(1)
|
||||
expect(messages.first.data.dig(:chat_channel, :id)).to eq(channel.id)
|
||||
expect(messages.first.data.dig(:channel, :id)).to eq(channel.id)
|
||||
end
|
||||
|
||||
describe "context when the channel's category is read restricted" do
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue