# Pull Request Template ## Description This is the second PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * added api for unread counts * Added the store refresher and invalidation with event listeners * Added action cable event * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
241 lines
8.1 KiB
Ruby
241 lines
8.1 KiB
Ruby
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
|
include Events::Types
|
|
include DateRangeHelper
|
|
include HmacConcern
|
|
|
|
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
|
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
|
|
|
ATTACHMENT_RESULTS_PER_PAGE = 100
|
|
|
|
def index
|
|
result = conversation_finder.perform
|
|
@conversations = result[:conversations]
|
|
@conversations_count = result[:count]
|
|
end
|
|
|
|
def meta
|
|
result = conversation_finder.perform_meta_only
|
|
@conversations_count = result[:count]
|
|
end
|
|
|
|
def search
|
|
result = conversation_finder.perform
|
|
@conversations = result[:conversations]
|
|
@conversations_count = result[:count]
|
|
end
|
|
|
|
def attachments
|
|
@attachments_count = @conversation.attachments.count
|
|
@attachments = @conversation.attachments
|
|
.includes({ file_attachment: :blob }, message: [:inbox, { sender: { avatar_attachment: :blob } }])
|
|
.order(created_at: :desc)
|
|
.page(attachment_params[:page])
|
|
.per(ATTACHMENT_RESULTS_PER_PAGE)
|
|
end
|
|
|
|
def show; end
|
|
|
|
def create
|
|
ActiveRecord::Base.transaction do
|
|
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
|
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
|
end
|
|
end
|
|
|
|
def update
|
|
@conversation.update!(permitted_update_params)
|
|
end
|
|
|
|
def filter
|
|
result = ::Conversations::FilterService.new(params.permit!, current_user, current_account).perform
|
|
@conversations = result[:conversations]
|
|
@conversations_count = result[:count]
|
|
rescue CustomExceptions::CustomFilter::InvalidAttribute,
|
|
CustomExceptions::CustomFilter::InvalidOperator,
|
|
CustomExceptions::CustomFilter::InvalidQueryOperator,
|
|
CustomExceptions::CustomFilter::InvalidValue => e
|
|
render_could_not_create_error(e.message)
|
|
end
|
|
|
|
def mute
|
|
@conversation.mute!
|
|
head :ok
|
|
end
|
|
|
|
def unmute
|
|
@conversation.unmute!
|
|
head :ok
|
|
end
|
|
|
|
def transcript
|
|
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
|
return render_payment_required('Email transcript is not available on your plan') unless @conversation.account.email_transcript_enabled?
|
|
return head :too_many_requests unless @conversation.account.within_email_rate_limit?
|
|
|
|
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
|
@conversation.account.increment_email_sent_count
|
|
head :ok
|
|
end
|
|
|
|
def toggle_status
|
|
# FIXME: move this logic into a service object
|
|
if pending_to_open_by_bot?
|
|
@conversation.bot_handoff!
|
|
elsif params[:status].present?
|
|
set_conversation_status
|
|
@status = @conversation.save!
|
|
else
|
|
@status = @conversation.toggle_status
|
|
end
|
|
assign_conversation if should_assign_conversation?
|
|
end
|
|
|
|
def pending_to_open_by_bot?
|
|
return false unless Current.user.is_a?(AgentBot)
|
|
|
|
@conversation.status == 'pending' && params[:status] == 'open'
|
|
end
|
|
|
|
def should_assign_conversation?
|
|
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
|
end
|
|
|
|
def toggle_priority
|
|
@conversation.toggle_priority(params[:priority])
|
|
head :ok
|
|
end
|
|
|
|
def toggle_typing_status
|
|
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, Current.user, params)
|
|
typing_status_manager.toggle_typing_status
|
|
head :ok
|
|
end
|
|
|
|
def update_last_seen
|
|
# High-traffic accounts generate excessive DB writes when agents frequently switch between conversations.
|
|
# Throttle last_seen updates to once per hour when there are no unread messages to reduce DB load.
|
|
# Always update immediately if there are unread messages to maintain accurate read/unread state.
|
|
# Visiting a conversation should clear any unread inbox notifications for this conversation.
|
|
Notification::MarkConversationReadService.new(user: Current.user, account: Current.account, conversation: @conversation).perform
|
|
return update_last_seen_on_conversation(DateTime.now.utc, true) if assignee? && @conversation.assignee_unread_messages.any?
|
|
return update_last_seen_on_conversation(DateTime.now.utc, false) if !assignee? && @conversation.unread_messages.any?
|
|
|
|
# No unread messages - apply throttling to limit DB writes
|
|
return unless should_update_last_seen?
|
|
|
|
update_last_seen_on_conversation(DateTime.now.utc, assignee?)
|
|
end
|
|
|
|
def unread
|
|
last_incoming_message = @conversation.messages.incoming.last
|
|
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
|
|
update_last_seen_on_conversation(last_seen_at, true)
|
|
end
|
|
|
|
def custom_attributes
|
|
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
|
@conversation.save!
|
|
end
|
|
|
|
def destroy
|
|
authorize @conversation, :destroy?
|
|
::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
|
|
head :ok
|
|
end
|
|
|
|
private
|
|
|
|
def permitted_update_params
|
|
# TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute
|
|
params.permit(:priority)
|
|
end
|
|
|
|
def attachment_params
|
|
params.permit(:page)
|
|
end
|
|
|
|
def update_last_seen_on_conversation(last_seen_at, update_assignee)
|
|
updates = { agent_last_seen_at: last_seen_at }
|
|
updates[:assignee_last_seen_at] = last_seen_at if update_assignee.present?
|
|
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
@conversation.update_columns(updates)
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
|
|
::Conversations::UnreadCounts::Notifier.new(@conversation).perform
|
|
end
|
|
|
|
def should_update_last_seen?
|
|
# Update if at least one relevant timestamp is older than 1 hour or not set
|
|
# This prevents redundant DB writes when agents repeatedly view the same conversation
|
|
agent_needs_update = @conversation.agent_last_seen_at.blank? || @conversation.agent_last_seen_at < 1.hour.ago
|
|
return agent_needs_update unless assignee?
|
|
|
|
# For assignees, check both timestamps - update if either is old
|
|
assignee_needs_update = @conversation.assignee_last_seen_at.blank? || @conversation.assignee_last_seen_at < 1.hour.ago
|
|
agent_needs_update || assignee_needs_update
|
|
end
|
|
|
|
def set_conversation_status
|
|
@conversation.status = params[:status]
|
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
|
end
|
|
|
|
def assign_conversation
|
|
@conversation.assignee = current_user
|
|
@conversation.save!
|
|
end
|
|
|
|
def conversation
|
|
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
|
authorize @conversation, :show?
|
|
end
|
|
|
|
def inbox
|
|
return if params[:inbox_id].blank?
|
|
|
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
|
authorize @inbox, :show?
|
|
end
|
|
|
|
def contact
|
|
return if params[:contact_id].blank?
|
|
|
|
@contact = Current.account.contacts.find(params[:contact_id])
|
|
end
|
|
|
|
def contact_inbox
|
|
@contact_inbox = build_contact_inbox
|
|
|
|
# fallback for the old case where we do look up only using source id
|
|
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
|
|
# and deprecate the support of passing only source_id as the param
|
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
|
authorize @contact_inbox.inbox, :show?
|
|
rescue ActiveRecord::RecordNotUnique
|
|
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
|
|
end
|
|
|
|
def build_contact_inbox
|
|
return if @inbox.blank? || @contact.blank?
|
|
|
|
ContactInboxBuilder.new(
|
|
contact: @contact,
|
|
inbox: @inbox,
|
|
source_id: params[:source_id],
|
|
hmac_verified: hmac_verified?
|
|
).perform
|
|
end
|
|
|
|
def conversation_finder
|
|
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
|
end
|
|
|
|
def assignee?
|
|
@conversation.assignee_id? && Current.user == @conversation.assignee
|
|
end
|
|
end
|
|
|
|
Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController')
|