manu conflicts fix
This commit is contained in:
parent
6e13a9a5d0
commit
4e3dec65b0
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user