Files
chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb
Sony MathewandGitHub 66cfb26c77 feat: Add unread count filters feature flag (1/6) (#14885)
## Description

Adds the account-level `unread_count_for_filters` feature flag as the
dark-launch gate for filtered sidebar unread counts. This reuses the
deprecated `quoted_email_reply` flag slot, resets the reused bit for
existing accounts, and removes stale defaults so new accounts do not
reference the old flag.

This also adds the feature where we are now calculating the unread counts for built in filters like mentions, participating and unattended along with unread count for saved filters/folders.

Closes
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## Type of change

- [ ] 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
2026-07-08 23:50:40 +05:30

242 lines
8.3 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?
::Conversations::DeleteService.new(conversation: @conversation, user: Current.user, ip: request.ip).perform
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
::Conversations::UnreadCounts::FilteredCountInvalidator.new(Current.account).conversation_changed!
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')