## 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
214 lines
6.2 KiB
Ruby
214 lines
6.2 KiB
Ruby
class Conversations::UnreadCounts::Counter
|
|
MANAGE_ALL_PERMISSION = 'conversation_manage'.freeze
|
|
UNASSIGNED_PERMISSION = 'conversation_unassigned_manage'.freeze
|
|
PARTICIPATING_PERMISSION = 'conversation_participating_manage'.freeze
|
|
BUILD_LOCK_TTL = 15.minutes.to_i
|
|
BUILD_WAIT_TIMEOUT = 30.seconds.to_i
|
|
BUILD_WAIT_INTERVAL = 0.1.seconds
|
|
|
|
attr_reader :account, :user
|
|
|
|
def initialize(account:, user:)
|
|
@account = account
|
|
@user = user
|
|
end
|
|
|
|
def perform
|
|
return with_filtered_counts(empty_counts, build: false) if permission_mode == :none
|
|
|
|
ensure_base_cache!
|
|
ensure_assignment_cache! if assignment_mode?
|
|
|
|
inbox_counts = unread_inbox_counts
|
|
|
|
{
|
|
all_count: inbox_counts.values.sum,
|
|
inboxes: inbox_counts,
|
|
labels: unread_label_counts,
|
|
teams: unread_team_counts
|
|
}.then { |counts| with_filtered_counts(counts) }
|
|
end
|
|
|
|
private
|
|
|
|
def with_filtered_counts(counts, build: true)
|
|
return counts unless account.feature_enabled?(::Conversations::UnreadCounts::FilteredCounter::FEATURE_FLAG)
|
|
|
|
counts.merge(build ? filtered_counter.perform : ::Conversations::UnreadCounts::FilteredCounter.empty_counts)
|
|
end
|
|
|
|
def ensure_base_cache!
|
|
ensure_cache_ready!(
|
|
ready: -> { store.base_ready?(account.id) },
|
|
lock_key: base_build_lock_key
|
|
) { ::Conversations::UnreadCounts::Builder.new(account).build_base! }
|
|
end
|
|
|
|
def ensure_assignment_cache!
|
|
ensure_cache_ready!(
|
|
ready: -> { store.assignment_ready?(account.id) },
|
|
lock_key: assignment_build_lock_key
|
|
) { ::Conversations::UnreadCounts::Builder.new(account).build_assignment! }
|
|
end
|
|
|
|
def ensure_cache_ready!(ready:, lock_key:)
|
|
lock_manager = Redis::LockManager.new
|
|
|
|
loop do
|
|
return if ready.call
|
|
|
|
return if lock_manager.with_lock(lock_key, BUILD_LOCK_TTL) { yield unless ready.call }
|
|
|
|
wait_for_cache_ready(ready)
|
|
end
|
|
end
|
|
|
|
def wait_for_cache_ready(ready)
|
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + BUILD_WAIT_TIMEOUT
|
|
sleep BUILD_WAIT_INTERVAL until ready.call || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
end
|
|
|
|
def base_build_lock_key
|
|
format(Redis::Alfred::UNREAD_CONVERSATIONS_BASE_BUILD_LOCK, account_id: account.id)
|
|
end
|
|
|
|
def assignment_build_lock_key
|
|
format(Redis::Alfred::UNREAD_CONVERSATIONS_ASSIGNMENT_BUILD_LOCK, account_id: account.id)
|
|
end
|
|
|
|
def unread_inbox_counts
|
|
counts_for_grouped_keys(visible_inbox_ids.index_with { |inbox_id| inbox_keys_for_mode(inbox_id) })
|
|
end
|
|
|
|
def unread_label_counts
|
|
keys_by_id = Hash.new { |hash, key| hash[key] = [] }
|
|
sidebar_label_ids.each do |label_id|
|
|
visible_inbox_ids.each do |inbox_id|
|
|
keys_by_id[label_id].concat(label_inbox_keys_for_mode(label_id, inbox_id))
|
|
end
|
|
end
|
|
|
|
counts_for_grouped_keys(keys_by_id)
|
|
end
|
|
|
|
def unread_team_counts
|
|
keys_by_id = Hash.new { |hash, key| hash[key] = [] }
|
|
visible_team_ids.each do |team_id|
|
|
visible_inbox_ids.each do |inbox_id|
|
|
keys_by_id[team_id].concat(team_inbox_keys_for_mode(team_id, inbox_id))
|
|
end
|
|
end
|
|
|
|
counts_for_grouped_keys(keys_by_id)
|
|
end
|
|
|
|
def inbox_keys_for_mode(inbox_id)
|
|
case permission_mode
|
|
when :base
|
|
[store.inbox_key(account.id, inbox_id)]
|
|
when :unassigned_and_mine
|
|
[store.inbox_unassigned_key(account.id, inbox_id), store.inbox_assignee_key(account.id, inbox_id, user.id)]
|
|
when :mine
|
|
[store.inbox_assignee_key(account.id, inbox_id, user.id)]
|
|
end
|
|
end
|
|
|
|
def label_inbox_keys_for_mode(label_id, inbox_id)
|
|
case permission_mode
|
|
when :base
|
|
[store.label_inbox_key(account.id, label_id, inbox_id)]
|
|
when :unassigned_and_mine
|
|
[
|
|
store.label_inbox_unassigned_key(account.id, label_id, inbox_id),
|
|
store.label_inbox_assignee_key(account.id, label_id, inbox_id, user.id)
|
|
]
|
|
when :mine
|
|
[store.label_inbox_assignee_key(account.id, label_id, inbox_id, user.id)]
|
|
end
|
|
end
|
|
|
|
def team_inbox_keys_for_mode(team_id, inbox_id)
|
|
case permission_mode
|
|
when :base
|
|
[store.team_inbox_key(account.id, team_id, inbox_id)]
|
|
when :unassigned_and_mine
|
|
[
|
|
store.team_inbox_unassigned_key(account.id, team_id, inbox_id),
|
|
store.team_inbox_assignee_key(account.id, team_id, inbox_id, user.id)
|
|
]
|
|
when :mine
|
|
[store.team_inbox_assignee_key(account.id, team_id, inbox_id, user.id)]
|
|
end
|
|
end
|
|
|
|
def counts_for_grouped_keys(keys_by_id)
|
|
counts_by_key = store.counts_for_keys(keys_by_id.values.flatten)
|
|
|
|
keys_by_id.each_with_object({}) do |(id, keys), result|
|
|
count = keys.sum { |key| counts_by_key[key].to_i }
|
|
result[id.to_s] = count if count.positive?
|
|
end
|
|
end
|
|
|
|
def assignment_mode?
|
|
%i[unassigned_and_mine mine].include?(permission_mode)
|
|
end
|
|
|
|
def permission_mode
|
|
@permission_mode ||=
|
|
if !custom_role_agent? || permissions.include?(MANAGE_ALL_PERMISSION)
|
|
:base
|
|
elsif permissions.include?(UNASSIGNED_PERMISSION)
|
|
:unassigned_and_mine
|
|
elsif permissions.include?(PARTICIPATING_PERMISSION)
|
|
:mine
|
|
else
|
|
:none
|
|
end
|
|
end
|
|
|
|
def custom_role_agent?
|
|
account_user&.agent? && account_user.custom_role_id.present?
|
|
end
|
|
|
|
def permissions
|
|
account_user&.permissions || []
|
|
end
|
|
|
|
def account_user
|
|
@account_user ||= account.account_users.find_by(user_id: user.id)
|
|
end
|
|
|
|
def visible_inbox_ids
|
|
@visible_inbox_ids ||= if account_user&.administrator?
|
|
account.inboxes.pluck(:id)
|
|
else
|
|
user.inboxes.where(account_id: account.id).pluck(:id)
|
|
end
|
|
end
|
|
|
|
def sidebar_label_ids
|
|
@sidebar_label_ids ||= account.labels.where(show_on_sidebar: true).pluck(:id)
|
|
end
|
|
|
|
def visible_team_ids
|
|
@visible_team_ids ||= if account_user&.administrator?
|
|
account.teams.pluck(:id)
|
|
else
|
|
user.teams.where(account_id: account.id).pluck(:id)
|
|
end
|
|
end
|
|
|
|
def empty_counts
|
|
{ all_count: 0, inboxes: {}, labels: {}, teams: {} }
|
|
end
|
|
|
|
def store
|
|
::Conversations::UnreadCounts::Store
|
|
end
|
|
|
|
def filtered_counter
|
|
@filtered_counter ||= ::Conversations::UnreadCounts::FilteredCounter.new(account: account, user: user)
|
|
end
|
|
end
|