diff --git a/plugins/chat/app/controllers/api_controller.rb b/plugins/chat/app/controllers/api_controller.rb deleted file mode 100644 index 70bf35dc60..0000000000 --- a/plugins/chat/app/controllers/api_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -class Chat::Api < Chat::ChatBaseController - before_action :ensure_logged_in - before_action :ensure_can_chat - - include Chat::WithServiceHelper - - private - - def ensure_can_chat - raise Discourse::NotFound unless SiteSetting.chat_enabled - guardian.ensure_can_chat! - end - - def default_actions_for_service - proc do - on_success { render(json: success_json) } - on_failure { render(json: failed_json, status: 422) } - on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess } - on_failed_contract do - render( - json: failed_json.merge(errors: result[:"result.contract.default"].errors.full_messages), - status: 400, - ) - end - end - end -end diff --git a/plugins/chat/app/controllers/chat/api/chat_tracking_controller.rb b/plugins/chat/app/controllers/chat/api/tracking_controller.rb similarity index 84% rename from plugins/chat/app/controllers/chat/api/chat_tracking_controller.rb rename to plugins/chat/app/controllers/chat/api/tracking_controller.rb index 3aea2b0596..c3b631dc78 100644 --- a/plugins/chat/app/controllers/chat/api/chat_tracking_controller.rb +++ b/plugins/chat/app/controllers/chat/api/tracking_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Chat::Api::ChatTrackingController < Chat::Api +class Chat::Api::TrackingController < Chat::Api def read params.permit(:channel_id, :message_id) @@ -17,7 +17,7 @@ class Chat::Api::ChatTrackingController < Chat::Api private def mark_single_message_read(channel_id, message_id) - with_service(Chat::Service::UpdateUserLastRead) do + with_service(Chat::UpdateUserLastRead) do on_failed_policy(:ensure_message_id_recency) do raise Discourse::InvalidParameters.new(:message_id) end @@ -28,7 +28,7 @@ class Chat::Api::ChatTrackingController < Chat::Api end def mark_all_messages_read - with_service(Chat::Service::MarkAllUserChannelsRead) do + with_service(Chat::MarkAllUserChannelsRead) do on_success do render(json: success_json.merge(updated_memberships: result.updated_memberships)) end diff --git a/plugins/chat/app/controllers/chat_base_controller.rb b/plugins/chat/app/controllers/chat_base_controller.rb deleted file mode 100644 index 6e014502dd..0000000000 --- a/plugins/chat/app/controllers/chat_base_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Chat::ChatBaseController < ::ApplicationController - before_action :ensure_logged_in - before_action :ensure_can_chat - - private - - def ensure_can_chat - raise Discourse::NotFound unless SiteSetting.chat_enabled - guardian.ensure_can_chat! - end - - def set_channel_and_chatable_with_access_check(chat_channel_id: nil) - params.require(:chat_channel_id) if chat_channel_id.blank? - id_or_name = chat_channel_id || params[:chat_channel_id] - @chat_channel = Chat::ChatChannelFetcher.find_with_access_check(id_or_name, guardian) - @chatable = @chat_channel.chatable - end -end diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb deleted file mode 100644 index a4e30f7ccf..0000000000 --- a/plugins/chat/app/controllers/chat_controller.rb +++ /dev/null @@ -1,436 +0,0 @@ -# frozen_string_literal: true - -class Chat::ChatController < Chat::ChatBaseController - PAST_MESSAGE_LIMIT = 40 - FUTURE_MESSAGE_LIMIT = 40 - PAST = "past" - FUTURE = "future" - CHAT_DIRECTIONS = [PAST, FUTURE] - - # Other endpoints use set_channel_and_chatable_with_access_check, but - # these endpoints require a standalone find because they need to be - # able to get deleted channels and recover them. - before_action :find_chatable, only: %i[enable_chat disable_chat] - before_action :find_chat_message, - only: %i[delete restore lookup_message edit_message rebake message_link] - before_action :set_channel_and_chatable_with_access_check, - except: %i[ - respond - enable_chat - disable_chat - message_link - lookup_message - set_user_chat_status - dismiss_retention_reminder - flag - ] - - def respond - render - end - - def enable_chat - chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable) - - guardian.ensure_can_join_chat_channel!(chat_channel) if chat_channel - - if chat_channel && chat_channel.trashed? - chat_channel.recover! - elsif chat_channel - return render_json_error I18n.t("chat.already_enabled") - else - chat_channel = @chatable.chat_channel - guardian.ensure_can_join_chat_channel!(chat_channel) - end - - success = chat_channel.save - if success && chat_channel.chatable_has_custom_fields? - @chatable.custom_fields[Chat::HAS_CHAT_ENABLED] = true - @chatable.save! - end - - if success - membership = Chat::ChatChannelMembershipManager.new(channel).follow(user) - render_serialized(chat_channel, ChatChannelSerializer, membership: membership) - else - render_json_error(chat_channel) - end - - Chat::ChatChannelMembershipManager.new(channel).follow(user) - end - - def disable_chat - chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable) - guardian.ensure_can_join_chat_channel!(chat_channel) - return render json: success_json if chat_channel.trashed? - chat_channel.trash!(current_user) - - success = chat_channel.save - if success - if chat_channel.chatable_has_custom_fields? - @chatable.custom_fields.delete(Chat::HAS_CHAT_ENABLED) - @chatable.save! - end - - render json: success_json - else - render_json_error(chat_channel) - end - end - - def create_message - raise Discourse::InvalidAccess if current_user.silenced? - - Chat::ChatMessageRateLimiter.run!(current_user) - - @user_chat_channel_membership = - Chat::ChatChannelMembershipManager.new(@chat_channel).find_for_user( - current_user, - following: true, - ) - raise Discourse::InvalidAccess unless @user_chat_channel_membership - - reply_to_msg_id = params[:in_reply_to_id] - if reply_to_msg_id - rm = ChatMessage.find(reply_to_msg_id) - raise Discourse::NotFound if rm.chat_channel_id != @chat_channel.id - end - - content = params[:message] - - chat_message_creator = - Chat::ChatMessageCreator.create( - chat_channel: @chat_channel, - user: current_user, - in_reply_to_id: reply_to_msg_id, - content: content, - staged_id: params[:staged_id], - upload_ids: params[:upload_ids], - ) - - 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, - ) - - if @chat_channel.direct_message_channel? - # If any of the channel users is ignoring, muting, or preventing DMs from - # the current user then we shold not auto-follow the channel once again or - # publish the new channel. - user_ids_allowing_communication = - UserCommScreener.new( - acting_user: current_user, - target_user_ids: @chat_channel.user_chat_channel_memberships.pluck(:user_id), - ).allowing_actor_communication - - if user_ids_allowing_communication.any? - 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 - - ChatPublisher.publish_user_tracking_state( - current_user, - @chat_channel.id, - chat_message_creator.chat_message.id, - ) - render json: success_json - end - - def edit_message - chat_message_updater = - Chat::ChatMessageUpdater.update( - guardian: guardian, - chat_message: @message, - new_content: params[:new_message], - upload_ids: params[:upload_ids] || [], - ) - - return render_json_error(chat_message_updater.error) if chat_message_updater.failed? - - render json: success_json - end - - def messages - page_size = params[:page_size]&.to_i || 1000 - direction = params[:direction].to_s - message_id = params[:message_id] - if page_size > 50 || - ( - message_id.blank? ^ direction.blank? && - (direction.present? && !CHAT_DIRECTIONS.include?(direction)) - ) - raise Discourse::InvalidParameters - end - - messages = preloaded_chat_message_query.where(chat_channel: @chat_channel) - messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable) - - if message_id.present? - condition = direction == PAST ? "<" : ">" - messages = messages.where("id #{condition} ?", message_id.to_i) - end - - # NOTE: This order is reversed when we return the ChatView below if the direction - # is not FUTURE. - order = direction == FUTURE ? "ASC" : "DESC" - messages = messages.order("created_at #{order}, id #{order}").limit(page_size).to_a - - can_load_more_past = nil - can_load_more_future = nil - - if direction == FUTURE - can_load_more_future = messages.size == page_size - elsif direction == PAST - can_load_more_past = messages.size == page_size - else - # When direction is blank, we'll return the latest messages. - can_load_more_future = false - can_load_more_past = messages.size == page_size - end - - chat_view = - ChatView.new( - chat_channel: @chat_channel, - chat_messages: direction == FUTURE ? messages : messages.reverse, - user: current_user, - can_load_more_past: can_load_more_past, - can_load_more_future: can_load_more_future, - ) - render_serialized(chat_view, ChatViewSerializer, root: false) - end - - def react - params.require(%i[message_id emoji react_action]) - guardian.ensure_can_react! - - Chat::ChatMessageReactor.new(current_user, @chat_channel).react!( - message_id: params[:message_id], - react_action: params[:react_action].to_sym, - emoji: params[:emoji], - ) - - render json: success_json - end - - def delete - guardian.ensure_can_delete_chat!(@message, @chatable) - - ChatMessageDestroyer.new.trash_message(@message, current_user) - - head :ok - end - - def restore - chat_channel = @message.chat_channel - guardian.ensure_can_restore_chat!(@message, chat_channel.chatable) - updated = @message.recover! - if updated - ChatPublisher.publish_restore!(chat_channel, @message) - render json: success_json - else - render_json_error(@message) - end - end - - def rebake - guardian.ensure_can_rebake_chat_message!(@message) - @message.rebake!(invalidate_oneboxes: true) - render json: success_json - end - - def message_link - raise Discourse::NotFound if @message.blank? || @message.deleted_at.present? - raise Discourse::NotFound if @message.chat_channel.blank? - set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id) - render json: - success_json.merge( - chat_channel_id: @chat_channel.id, - chat_channel_title: @chat_channel.title(current_user), - ) - end - - def lookup_message - set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id) - - messages = preloaded_chat_message_query.where(chat_channel: @chat_channel) - messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable) - - past_messages = - messages - .where("created_at < ?", @message.created_at) - .order(created_at: :desc) - .limit(PAST_MESSAGE_LIMIT) - - future_messages = - messages - .where("created_at > ?", @message.created_at) - .order(created_at: :asc) - .limit(FUTURE_MESSAGE_LIMIT) - - can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT - can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT - messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat) - chat_view = - ChatView.new( - chat_channel: @chat_channel, - chat_messages: messages, - user: current_user, - can_load_more_past: can_load_more_past, - can_load_more_future: can_load_more_future, - ) - render_serialized(chat_view, ChatViewSerializer, root: false) - end - - def set_user_chat_status - params.require(:chat_enabled) - - current_user.user_option.update(chat_enabled: params[:chat_enabled]) - render json: { chat_enabled: current_user.user_option.chat_enabled } - end - - def invite_users - params.require(:user_ids) - - users = - User - .includes(:groups) - .joins(:user_option) - .where(user_options: { chat_enabled: true }) - .not_suspended - .where(id: params[:user_ids]) - users.each do |user| - guardian = Guardian.new(user) - if guardian.can_chat? && guardian.can_join_chat_channel?(@chat_channel) - data = { - message: "chat.invitation_notification", - chat_channel_id: @chat_channel.id, - chat_channel_title: @chat_channel.title(user), - chat_channel_slug: @chat_channel.slug, - invited_by_username: current_user.username, - } - data[:chat_message_id] = params[:chat_message_id] if params[:chat_message_id] - user.notifications.create( - notification_type: Notification.types[:chat_invitation], - high_priority: true, - data: data.to_json, - ) - end - end - - render json: success_json - end - - def dismiss_retention_reminder - params.require(:chatable_type) - guardian.ensure_can_chat! - unless ChatChannel.chatable_types.include?(params[:chatable_type]) - raise Discourse::InvalidParameters - end - - field = - ( - if ChatChannel.public_channel_chatable_types.include?(params[:chatable_type]) - :dismissed_channel_retention_reminder - else - :dismissed_dm_retention_reminder - end - ) - current_user.user_option.update(field => true) - render json: success_json - end - - def quote_messages - params.require(:message_ids) - - message_ids = params[:message_ids].map(&:to_i) - markdown = - ChatTranscriptService.new( - @chat_channel, - current_user, - messages_or_ids: message_ids, - ).generate_markdown - render json: success_json.merge(markdown: markdown) - end - - def flag - RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed! - - permitted_params = - params.permit( - %i[chat_message_id flag_type_id message is_warning take_action queue_for_review], - ) - - chat_message = - ChatMessage.includes(:chat_channel, :revisions).find(permitted_params[:chat_message_id]) - - flag_type_id = permitted_params[:flag_type_id].to_i - - if !ReviewableScore.types.values.include?(flag_type_id) - raise Discourse::InvalidParameters.new(:flag_type_id) - end - - set_channel_and_chatable_with_access_check(chat_channel_id: chat_message.chat_channel_id) - - result = - Chat::ChatReviewQueue.new.flag_message(chat_message, guardian, flag_type_id, permitted_params) - - if result[:success] - render json: success_json - else - render_json_error(result[:errors]) - end - end - - def set_draft - if params[:data].present? - ChatDraft.find_or_initialize_by( - user: current_user, - chat_channel_id: @chat_channel.id, - ).update!(data: params[:data]) - else - ChatDraft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all - end - - render json: success_json - end - - private - - def preloaded_chat_message_query - query = - ChatMessage - .includes(in_reply_to: [:user, chat_webhook_event: [:incoming_chat_webhook]]) - .includes(:revisions) - .includes(user: :primary_group) - .includes(chat_webhook_event: :incoming_chat_webhook) - .includes(reactions: :user) - .includes(:bookmarks) - .includes(:uploads) - .includes(chat_channel: :chatable) - - query = query.includes(user: :user_status) if SiteSetting.enable_user_status - - query - end - - def find_chatable - @chatable = Category.find_by(id: params[:chatable_id]) - guardian.ensure_can_moderate_chat!(@chatable) - end - - def find_chat_message - @message = preloaded_chat_message_query.with_deleted - @message = @message.where(chat_channel_id: params[:chat_channel_id]) if params[:chat_channel_id] - @message = @message.find_by(id: params[:message_id]) - raise Discourse::NotFound unless @message - end -end diff --git a/plugins/chat/app/services/chat/mark_all_user_channels_read.rb b/plugins/chat/app/services/chat/mark_all_user_channels_read.rb new file mode 100644 index 0000000000..44943c2e04 --- /dev/null +++ b/plugins/chat/app/services/chat/mark_all_user_channels_read.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Chat + # Service responsible for marking all the channels that a user is a + # member of _and following_ as read, including mentions. + # + # @example + # Chat::Service::MarkAllUserChannelsRead.call(guardian: guardian) + # + class MarkAllUserChannelsRead + include ::Service::Base + + # @!method call(guardian:) + # @param [Guardian] guardian + # @return [Chat::Service::Base::Context] + + transaction do + step :update_last_read_message_ids + step :mark_associated_mentions_as_read + end + + private + + def update_last_read_message_ids(guardian:, **) + updated_memberships = DB.query(<<~SQL, user_id: guardian.user.id) + UPDATE user_chat_channel_memberships + SET last_read_message_id = subquery.newest_message_id + FROM + ( + SELECT chat_messages.chat_channel_id, MAX(chat_messages.id) AS newest_message_id + FROM chat_messages + WHERE chat_messages.deleted_at IS NULL + GROUP BY chat_messages.chat_channel_id + ) AS subquery + WHERE user_chat_channel_memberships.chat_channel_id = subquery.chat_channel_id AND + subquery.newest_message_id > COALESCE(user_chat_channel_memberships.last_read_message_id, 0) AND + user_chat_channel_memberships.user_id = :user_id AND + user_chat_channel_memberships.following + RETURNING user_chat_channel_memberships.id AS membership_id, + user_chat_channel_memberships.chat_channel_id AS channel_id, + user_chat_channel_memberships.last_read_message_id; + SQL + context[:updated_memberships] = updated_memberships + end + + def mark_associated_mentions_as_read(guardian:, updated_memberships:, **) + return if updated_memberships.empty? + + Notification + .where(notification_type: ::Notification.types[:chat_mention]) + .where(user: guardian.user) + .where(read: false) + .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id") + .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id") + .where("chat_messages.chat_channel_id IN (?)", updated_memberships.map(&:channel_id)) + .update_all(read: true) + end + end +end diff --git a/plugins/chat/app/services/chat/update_user_last_read.rb b/plugins/chat/app/services/chat/update_user_last_read.rb index 0eb01159b2..3be52cdd98 100644 --- a/plugins/chat/app/services/chat/update_user_last_read.rb +++ b/plugins/chat/app/services/chat/update_user_last_read.rb @@ -4,23 +4,23 @@ module Chat # Service responsible for updating the last read message id of a membership. # # @example - # Chat::UpdateUserLastRead.call(user_id: 1, channel_id: 2, message_id: 3, guardian: guardian) + # Chat::UpdateUserLastRead.call(channel_id: 2, message_id: 3, guardian: guardian) # class UpdateUserLastRead - include Service::Base + include ::Service::Base - # @!method call(user_id:, channel_id:, message_id:, guardian:) - # @param [Integer] user_id + # @!method call(channel_id:, message_id:, guardian:) # @param [Integer] channel_id # @param [Integer] message_id # @param [Guardian] guardian - # @return [Service::Base::Context] + # @return [Chat::Service::Base::Context] contract - model :membership, :fetch_active_membership + model :channel + model :active_membership policy :invalid_access - policy :ensure_message_id_recency policy :ensure_message_exists + policy :ensure_message_id_recency step :update_last_read_message_id step :mark_associated_mentions_as_read step :publish_new_last_read_to_clients @@ -28,52 +28,52 @@ module Chat # @!visibility private class Contract attribute :message_id, :integer - attribute :user_id, :integer attribute :channel_id, :integer - validates :message_id, :user_id, :channel_id, presence: true + validates :message_id, :channel_id, presence: true end private - def fetch_active_membership(user_id:, channel_id:, **) - Chat::UserChatChannelMembership.includes(:user, :chat_channel).find_by( - user_id: user_id, - chat_channel_id: channel_id, - following: true, - ) + def fetch_channel(contract:, **) + ::Chat::Channel.find_by(id: contract.channel_id) end - def invalid_access(guardian:, membership:, **) - guardian.can_join_chat_channel?(membership.chat_channel) + def fetch_active_membership(guardian:, channel:, **) + ::Chat::ChannelMembershipManager.new(channel).find_for_user(guardian.user, following: true) end - def ensure_message_id_recency(message_id:, membership:, **) - !membership.last_read_message_id || message_id >= membership.last_read_message_id + def invalid_access(guardian:, active_membership:, **) + guardian.can_join_chat_channel?(active_membership.chat_channel) end - def ensure_message_exists(channel_id:, message_id:, **) - Chat::Message.with_deleted.exists?(chat_channel_id: channel_id, id: message_id) + def ensure_message_exists(channel:, contract:, **) + ::Chat::Message.with_deleted.exists?(chat_channel_id: channel.id, id: contract.message_id) end - def update_last_read_message_id(message_id:, membership:, **) - membership.update!(last_read_message_id: message_id) + def ensure_message_id_recency(contract:, active_membership:, **) + !active_membership.last_read_message_id || + contract.message_id >= active_membership.last_read_message_id end - def mark_associated_mentions_as_read(membership:, message_id:, **) - Notification + def update_last_read_message_id(contract:, active_membership:, **) + active_membership.update!(last_read_message_id: contract.message_id) + end + + def mark_associated_mentions_as_read(active_membership:, contract:, **) + ::Notification .where(notification_type: Notification.types[:chat_mention]) - .where(user: membership.user) + .where(user: active_membership.user) .where(read: false) .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id") .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id") - .where("chat_messages.id <= ?", message_id) - .where("chat_messages.chat_channel_id = ?", membership.chat_channel.id) + .where("chat_messages.id <= ?", contract.message_id) + .where("chat_messages.chat_channel_id = ?", active_membership.chat_channel.id) .update_all(read: true) end - def publish_new_last_read_to_clients(guardian:, channel_id:, message_id:, **) - Chat::Publisher.publish_user_tracking_state(guardian.user, channel_id, message_id) + def publish_new_last_read_to_clients(guardian:, channel:, contract:, **) + ::Chat::Publisher.publish_user_tracking_state(guardian.user, channel.id, contract.message_id) end end end diff --git a/plugins/chat/app/services/mark_all_user_channels_read.rb b/plugins/chat/app/services/mark_all_user_channels_read.rb deleted file mode 100644 index 8e5fb3dfbc..0000000000 --- a/plugins/chat/app/services/mark_all_user_channels_read.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module Chat - module Service - # Service responsible for marking all the channels that a user is a - # member of _and following_ as read, including mentions. - # - # @example - # Chat::Service::MarkAllUserChannelsRead.call(guardian: guardian) - # - class MarkAllUserChannelsRead - include Base - - # @!method call(guardian:) - # @param [Guardian] guardian - # @return [Chat::Service::Base::Context] - - transaction do - step :update_last_read_message_ids - step :mark_associated_mentions_as_read - end - - private - - def update_last_read_message_ids(guardian:, **) - updated_memberships = DB.query(<<~SQL, user_id: guardian.user.id) - UPDATE user_chat_channel_memberships - SET last_read_message_id = subquery.newest_message_id - FROM - ( - SELECT chat_messages.chat_channel_id, MAX(chat_messages.id) AS newest_message_id - FROM chat_messages - WHERE chat_messages.deleted_at IS NULL - GROUP BY chat_messages.chat_channel_id - ) AS subquery - WHERE user_chat_channel_memberships.chat_channel_id = subquery.chat_channel_id AND - subquery.newest_message_id > COALESCE(user_chat_channel_memberships.last_read_message_id, 0) AND - user_chat_channel_memberships.user_id = :user_id AND - user_chat_channel_memberships.following - RETURNING user_chat_channel_memberships.id AS membership_id, - user_chat_channel_memberships.chat_channel_id AS channel_id, - user_chat_channel_memberships.last_read_message_id; - SQL - context[:updated_memberships] = updated_memberships - end - - def mark_associated_mentions_as_read(guardian:, updated_memberships:, **) - return if updated_memberships.empty? - - Notification - .where(notification_type: Notification.types[:chat_mention]) - .where(user: guardian.user) - .where(read: false) - .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id") - .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id") - .where("chat_messages.chat_channel_id IN (?)", updated_memberships.map(&:channel_id)) - .update_all(read: true) - end - end - end -end diff --git a/plugins/chat/app/services/update_user_last_read.rb b/plugins/chat/app/services/update_user_last_read.rb deleted file mode 100644 index ac889d2ba6..0000000000 --- a/plugins/chat/app/services/update_user_last_read.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module Chat - module Service - # Service responsible for updating the last read message id of a membership. - # - # @example - # Chat::Service::UpdateUserLastRead.call(channel_id: 2, message_id: 3, guardian: guardian) - # - class UpdateUserLastRead - include Base - - # @!method call(channel_id:, message_id:, guardian:) - # @param [Integer] channel_id - # @param [Integer] message_id - # @param [Guardian] guardian - # @return [Chat::Service::Base::Context] - - contract - model :channel - model :active_membership - policy :invalid_access - policy :ensure_message_exists - policy :ensure_message_id_recency - step :update_last_read_message_id - step :mark_associated_mentions_as_read - step :publish_new_last_read_to_clients - - # @!visibility private - class Contract - attribute :message_id, :integer - attribute :channel_id, :integer - - validates :message_id, :channel_id, presence: true - end - - private - - def fetch_channel(contract:, **) - ChatChannel.find_by(id: contract.channel_id) - end - - def fetch_active_membership(guardian:, channel:, **) - Chat::ChatChannelMembershipManager.new(channel).find_for_user( - guardian.user, - following: true, - ) - end - - def invalid_access(guardian:, active_membership:, **) - guardian.can_join_chat_channel?(active_membership.chat_channel) - end - - def ensure_message_exists(channel:, contract:, **) - ChatMessage.with_deleted.exists?(chat_channel_id: channel.id, id: contract.message_id) - end - - def ensure_message_id_recency(contract:, active_membership:, **) - !active_membership.last_read_message_id || - contract.message_id >= active_membership.last_read_message_id - end - - def update_last_read_message_id(contract:, active_membership:, **) - active_membership.update!(last_read_message_id: contract.message_id) - end - - def mark_associated_mentions_as_read(active_membership:, contract:, **) - Notification - .where(notification_type: Notification.types[:chat_mention]) - .where(user: active_membership.user) - .where(read: false) - .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id") - .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id") - .where("chat_messages.id <= ?", contract.message_id) - .where("chat_messages.chat_channel_id = ?", active_membership.chat_channel.id) - .update_all(read: true) - end - - def publish_new_last_read_to_clients(guardian:, channel:, contract:, **) - ChatPublisher.publish_user_tracking_state(guardian.user, channel.id, contract.message_id) - end - end - end -end diff --git a/plugins/chat/config/routes.rb b/plugins/chat/config/routes.rb index 0c66fac78e..a51a5da3dc 100644 --- a/plugins/chat/config/routes.rb +++ b/plugins/chat/config/routes.rb @@ -17,6 +17,7 @@ Chat::Engine.routes.draw do post "/channels/:channel_id/memberships/me" => "channels_current_user_membership#create" put "/channels/:channel_id/notifications-settings/me" => "channels_current_user_notifications_settings#update" + put "/tracking/read/me" => "tracking#read" # Category chatables controller hints. Only used by staff members, we don't want to leak category permissions. get "/category-chatables/:id/permissions" => "category_chatables#permissions",