diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 94fb9385ce5..e963f2fa003 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -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); diff --git a/app/serializers/user_status_serializer.rb b/app/serializers/user_status_serializer.rb index 2866398d1b2..aba929520c2 100644 --- a/app/serializers/user_status_serializer.rb +++ b/app/serializers/user_status_serializer.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb b/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb deleted file mode 100644 index 727811c9ca6..00000000000 --- a/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb +++ /dev/null @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb b/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb deleted file mode 100644 index 57c0055424d..00000000000 --- a/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb +++ /dev/null @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb new file mode 100644 index 00000000000..759348ef4ec --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb index 6658b381eb7..9e70a95d015 100644 --- a/plugins/chat/app/controllers/api/chat_channels_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb b/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb new file mode 100644 index 00000000000..91422f9d673 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_current_user_membership_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb b/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb new file mode 100644 index 00000000000..d9a8f4ac57e --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_current_user_notifications_settings_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb b/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb new file mode 100644 index 00000000000..c50a30735e7 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_memberships_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb b/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb new file mode 100644 index 00000000000..d0d3ff1777f --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_messages_moves_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_channels_status_controller.rb b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb new file mode 100644 index 00000000000..78b1ac3f2cb --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_channels_status_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_chatables_controller.rb b/plugins/chat/app/controllers/api/chat_chatables_controller.rb new file mode 100644 index 00000000000..9eaec32b89b --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_chatables_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb b/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb new file mode 100644 index 00000000000..ecc01163606 --- /dev/null +++ b/plugins/chat/app/controllers/api/chat_current_user_channels_controller.rb @@ -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 diff --git a/plugins/chat/app/controllers/chat_channels_controller.rb b/plugins/chat/app/controllers/chat_channels_controller.rb deleted file mode 100644 index cecc5b2f1fd..00000000000 --- a/plugins/chat/app/controllers/chat_channels_controller.rb +++ /dev/null @@ -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 diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb index c4ad14e13dd..6b7d74d020b 100644 --- a/plugins/chat/app/controllers/chat_controller.rb +++ b/plugins/chat/app/controllers/chat_controller.rb @@ -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! diff --git a/plugins/chat/app/controllers/direct_messages_controller.rb b/plugins/chat/app/controllers/direct_messages_controller.rb index fb07f66874b..b0100a95a89 100644 --- a/plugins/chat/app/controllers/direct_messages_controller.rb +++ b/plugins/chat/app/controllers/direct_messages_controller.rb @@ -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 diff --git a/plugins/chat/app/jobs/regular/chat_channel_archive.rb b/plugins/chat/app/jobs/regular/chat_channel_archive.rb index c5eb878d33b..33e270dd220 100644 --- a/plugins/chat/app/jobs/regular/chat_channel_archive.rb +++ b/plugins/chat/app/jobs/regular/chat_channel_archive.rb @@ -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}", diff --git a/plugins/chat/app/serializers/chat_channel_serializer.rb b/plugins/chat/app/serializers/chat_channel_serializer.rb index fe321c7fa0e..9193334177d 100644 --- a/plugins/chat/app/serializers/chat_channel_serializer.rb +++ b/plugins/chat/app/serializers/chat_channel_serializer.rb @@ -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 diff --git a/plugins/chat/app/serializers/structured_channel_serializer.rb b/plugins/chat/app/serializers/structured_channel_serializer.rb index 8ecbc6103d8..a197c36c62e 100644 --- a/plugins/chat/app/serializers/structured_channel_serializer.rb +++ b/plugins/chat/app/serializers/structured_channel_serializer.rb @@ -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 diff --git a/plugins/chat/app/services/chat_publisher.rb b/plugins/chat/app/services/chat_publisher.rb index 5a1027d04d4..6dff51e6566 100644 --- a/plugins/chat/app/services/chat_publisher.rb +++ b/plugins/chat/app/services/chat_publisher.rb @@ -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], ) diff --git a/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js index 51f4b36f251..400f1d389be 100644 --- a/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js @@ -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}`; diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs index 4247db6e27d..a4100a897fa 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.hbs @@ -1,5 +1,11 @@ -{{#if (and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel)}} - +{{#if + (and this.showMobileDirectMessageButton this.canCreateDirectMessageChannel) +}} + {{d-icon "plus"}} {{/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}}
@@ -26,7 +35,11 @@ {{/if}} {{i18n "chat.chat_channels"}} - + {{d-icon "pencil-alt"}}
@@ -40,7 +53,7 @@
{{else}} - {{#each this.publicChannels as |channel|}} + {{#each this.chatChannelsManager.publicMessageChannels as |channel|}} {{i18n "chat.direct_messages.title"}} - {{#if (and this.canCreateDirectMessageChannel (not this.showMobileDirectMessageButton))}} - + {{#if + (and + this.canCreateDirectMessageChannel + (not this.showMobileDirectMessageButton) + ) + }} + {{d-icon "plus"}} {{/if}} @@ -84,11 +106,8 @@ {{/if}}
- {{#each this.sortedDirectMessageChannels as |channel|}} - + {{#each this.chatChannelsManager.truncatedDirectMessageChannels as |channel|}} + {{/each}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.js b/plugins/chat/assets/javascripts/discourse/components/channels-list.js index 796db2a1b94..9cac0ff735e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/channels-list.js +++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.js @@ -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 diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs index 04b7b31cf6c..5098f17b76e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.hbs @@ -1,12 +1,16 @@ {{#if this.chatProgressBarContainer}} {{#in-element this.chatProgressBarContainer}} - + {{/in-element}} {{/if}}
{{#if this.site.mobileView}} - + {{d-icon "chevron-left"}} {{/if}} @@ -17,7 +21,10 @@ {{/if}} @@ -49,7 +56,7 @@ />
- {{#if (and (not this.channels.length) (not this.isLoading))}} + {{#if (and (not this.channelsCollection.length) (not this.channelsCollection.loading))}}
{{i18n "chat.empty_state.title"}}
@@ -60,16 +67,16 @@
- {{else if this.channels.length}} + {{else if this.channelsCollection.length}}
- {{#each this.channels as |channel|}} + {{#each this.channelsCollection as |channel|}} {{/each}}
- {{#unless this.isLoading}} + {{#unless this.channelsCollection.loading}} {{/unless}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js index a9d66616cdf..39ea226ef68 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js @@ -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(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs index 323bc228731..889ec62b31f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs @@ -59,7 +59,6 @@
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js index 3b4d038645d..0537ff51bd2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js @@ -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); - }); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js index 2569653fac3..57a800b4127 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js @@ -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; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js index e8f20834527..7f76393cdcc 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js @@ -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() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs index 411dc94ea38..beb29b614df 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.hbs @@ -1,33 +1,34 @@ -{{#if this.channel}} +{{#if @channel}}
- {{replace-emoji this.channel.escapedTitle}} + {{replace-emoji @channel.escapedTitle}} - {{#if this.channel.chatable.read_restricted}} + {{#if @channel.chatable.read_restricted}} {{d-icon "lock" class="chat-channel-card__read-restricted"}} {{/if}}
- {{#if this.channel.current_user_membership.muted}} + {{#if @channel.currentUserMembership.muted}} @@ -47,32 +48,30 @@
- {{#if this.channel.description}} + {{#if @channel.description}}
- {{replace-emoji this.channel.escapedDescription}} + {{replace-emoji @channel.escapedDescription}}
{{/if}}
- {{#if this.channel.isFollowing}} + {{#if @channel.isFollowing}}
{{i18n "chat.joined"}}
- {{else if this.channel.isJoinable}} + {{else if @channel.isJoinable}} {{/if}} - {{#if (gt this.channel.membershipsCount 0)}} + {{#if (gt @channel.membershipsCount 0)}} {{i18n "chat.channel.memberships_count" - count=this.channel.membershipsCount + count=@channel.membershipsCount }} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js index 3392fe9f059..36a8f64e98b 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js @@ -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(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js index 3f38523f186..4943ae1e34b 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js @@ -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(); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs index 90834e6051b..5acfc7a5267 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.hbs @@ -1,6 +1,6 @@ {{#if this.chatProgressBarContainer}} {{#in-element this.chatProgressBarContainer}} - + {{/in-element}} {{/if}} @@ -22,15 +22,15 @@ >
- {{#each this.members as |member|}} + {{#each this.members as |membership|}} - - + + {{else}} {{#unless this.isFetchingMembers}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js index 49907dbd68c..dea4cad847d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js @@ -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() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs index 2e7f8f58005..eed8d6c84c2 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.hbs @@ -13,7 +13,6 @@ {{#if this.showJoinButton}} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js index 23be8f9a672..954313febe9 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js @@ -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); - }); - } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs index 83776ff9f77..e77ee34205f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.hbs @@ -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") diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js index e703be69381..3fe267f6f63 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js @@ -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() { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js index bfd46d64c0e..1937ff6bdae 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js @@ -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); + } + }); + }, }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs index 5597f74a5c9..9ea17a5e4ed 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.hbs @@ -3,13 +3,13 @@
- {{#unless this.channel.current_user_membership.muted}} + {{#unless this.channel.currentUserMembership.muted}}
{{i18n "chat.settings.mobile_notification_level"}}
{ - 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) { diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js index 87c0cfd9b21..5ed0b2dbdf5 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js @@ -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); } } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs index 4a53e0296a9..478d225337f 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.hbs @@ -1,5 +1,16 @@ -{{#if this.hasUnread}} -
-
{{this.unreadCount}}
+{{#if (gt @channel.currentUserMembership.unread_count 0)}} +
+
{{@channel.currentUserMembership.unread_count}}
{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js deleted file mode 100644 index f24cd64a31b..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js +++ /dev/null @@ -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; - }, -}); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js index e374564c35b..f8f00d3e920 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js @@ -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 }) ) ); }) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js b/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js index 3052f153983..35dab11417d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer.js @@ -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 diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs new file mode 100644 index 00000000000..c8af41c7876 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.hbs @@ -0,0 +1,9 @@ +{{#if (gt this.chatChannelsManager.unreadUrgentCount 0)}} +
+
+
{{this.chatChannelsManager.unreadUrgentCount}}
+
+
+{{else if (gt this.chatChannelsManager.unreadCount 0)}} +
+{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js new file mode 100644 index 00000000000..df39429e489 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon-unread-indicator.js @@ -0,0 +1,6 @@ +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; + +export default class ChatHeaderIconUnreadIndicator extends Component { + @service chatChannelsManager; +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs new file mode 100644 index 00000000000..4eb8b8ad9d8 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.hbs @@ -0,0 +1,21 @@ +{{#if (and this.chatStateManager.isFullPageActive this.site.desktopView)}} + + {{d-icon "comment"}} + + {{#unless this.currentUserInDnD}} + + {{/unless}} + +{{else}} + + {{d-icon "comment"}} + + {{#unless this.currentUserInDnD}} + + {{/unless}} + +{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js new file mode 100644 index 00000000000..9753f8ab005 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/components/chat-header-icon.js @@ -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 + ); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs index 2e1696714df..66d3a0e149c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs @@ -21,6 +21,7 @@ {{/if}}
+
{{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js index 4104ba0ba9f..320017945f5 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js @@ -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); }) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs index 2c7ba4de58e..48c9c87792d 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.hbs @@ -6,11 +6,23 @@

{{this.instructionsText}}

- + diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js index 3045b7f4ed7..7d404060d25 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-move-to-channel-modal-inner.js @@ -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); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs index f93d4213caf..db545f463f6 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.hbs @@ -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}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.js b/plugins/chat/assets/javascripts/discourse/components/chat-message.js index 867610bdf31..8f82d105015 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.js @@ -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); + }); } }); }, diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js index e7ae0190ffc..eb44133b1f8 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-to-topic-selector.js @@ -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, diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js index 76edf0f21ee..a736c4e9011 100644 --- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js +++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.js @@ -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"); diff --git a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs index a8b2d2c538c..00751d14915 100644 --- a/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/toggle-channel-membership-button.hbs @@ -1,4 +1,4 @@ -{{#if this.channel.isFollowing}} +{{#if @channel.currentUserMembership.following}} { 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; }); } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js index 0efdb70bbf1..85e834963ae 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-description.js @@ -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) => { diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js index 9eb3dbde1f5..d57ad3f6ce6 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js @@ -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) => { diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js index c7976a24ff7..d33ec8fd222 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js @@ -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, }); } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js index 720e6f635f3..65c132080d9 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info.js @@ -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); - } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js index 00b9b8e0aef..ae2ed12f483 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js @@ -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", ""); diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js index ee754a1e75e..c892853691c 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-setup.js @@ -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(); }, }; diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js index e7c417e8e1e..547632c6c77 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js @@ -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() { diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js b/plugins/chat/assets/javascripts/discourse/lib/chat-api.js deleted file mode 100644 index 3b373570a72..00000000000 --- a/plugins/chat/assets/javascripts/discourse/lib/chat-api.js +++ /dev/null @@ -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); - } -} diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js index 7dca63e974b..182b01a1ec3 100644 --- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js +++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js @@ -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; }, }); diff --git a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js index 8e97e26bfe2..9d732e82fc4 100644 --- a/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js +++ b/plugins/chat/assets/javascripts/discourse/models/user-chat-channel-membership.js @@ -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); + }, +}); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js new file mode 100644 index 00000000000..7fc075e2cee --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-browse-archived.js @@ -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"); + } + } +} diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js index 7516fe58bb1..ba6c91c245e 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-by-name.js @@ -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")); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js index 181c9ffb690..a92d5e882f0 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-about.js @@ -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"); } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js index ffc3bc589b4..c7bfd1c0905 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-index.js @@ -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"); diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js index 25d2e4eb93a..d3fba6f97de 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-members.js @@ -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"); } } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js new file mode 100644 index 00000000000..61635238534 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel-info-settings.js @@ -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"); + } + } +} diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js index 17e6a2f7456..7e49d1f764e 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js @@ -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(); - } } diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js index fbf8cd0a3cb..027f21b4672 100644 --- a/plugins/chat/assets/javascripts/discourse/routes/chat-index.js +++ b/plugins/chat/assets/javascripts/discourse/routes/chat-index.js @@ -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; } } } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js new file mode 100644 index 00000000000..4a24d8a9b6e --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js @@ -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, + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js new file mode 100644 index 00000000000..4ba6ab2365b --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js @@ -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; + } + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js index b76f8420e44..bc51a26394e 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-message-visibility-observer.js @@ -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(); } }); diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js index 12df7f6382e..ee79bb20bb3 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-state-manager.js @@ -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; } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js new file mode 100644 index 00000000000..cf627de4d57 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js @@ -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; + } + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js index 26feb212750..03b9164a058 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat.js @@ -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); } diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs index dc261f00413..89a38629ad6 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-index.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs index 5211ae3ff72..a9cb4f67d41 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs @@ -1 +1,5 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs index b6e654e73d6..e80afabd3ba 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-members.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs index d267701835a..a30950bc79e 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-settings.hbs @@ -1 +1 @@ - + diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs index e7e0ba3aacb..fdb757c23c3 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info.hbs @@ -3,16 +3,17 @@
{{#if this.chatChannelInfoRouteOriginManager.isBrowse}} - + {{d-icon "chevron-left"}} {{else}} @@ -21,7 +22,7 @@ {{/if}}
- +
@@ -35,16 +36,13 @@ > {{i18n (concat "chat.channel_info.tabs." tab)}} {{#if (eq tab "members")}} - ({{this.model.chatChannel.membershipsCount}}) + ({{this.model.membershipsCount}}) {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs index e39cc9a9d59..14199193dec 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs @@ -17,15 +17,15 @@ {{/if}} -