# 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>
200 lines
7.2 KiB
Ruby
200 lines
7.2 KiB
Ruby
class Conversations::UnreadCounts::Store
|
|
extend ::Conversations::UnreadCounts::StoreKeys
|
|
|
|
class << self
|
|
def base_ready?(account_id)
|
|
Redis::Alfred.exists?(base_ready_key(account_id))
|
|
end
|
|
|
|
def assignment_ready?(account_id)
|
|
Redis::Alfred.exists?(assignment_ready_key(account_id))
|
|
end
|
|
|
|
def mark_base_ready!(account_id)
|
|
Redis::Alfred.set(base_ready_key(account_id), Time.current.to_i, ex: Conversations::UnreadCounts::READY_TTL)
|
|
end
|
|
|
|
def mark_assignment_ready!(account_id)
|
|
Redis::Alfred.set(assignment_ready_key(account_id), Time.current.to_i, ex: Conversations::UnreadCounts::READY_TTL)
|
|
end
|
|
|
|
def clear_account!(account_id)
|
|
delete_matching("#{account_prefix(account_id)}::*")
|
|
end
|
|
|
|
def clear_assignment!(account_id)
|
|
assignment_key_patterns(account_id).each { |pattern| delete_matching(pattern) }
|
|
end
|
|
|
|
def add_base_membership(account_id:, inbox_id:, label_ids:, conversation_id:, team_id: nil)
|
|
add_to_sets(base_keys(account_id, inbox_id, label_ids, team_id), conversation_id)
|
|
end
|
|
|
|
def remove_base_membership(account_id:, inbox_ids:, label_ids:, conversation_id:, team_ids: [])
|
|
keys = Array(inbox_ids).flat_map { |inbox_id| removable_base_keys(account_id, inbox_id, label_ids, team_ids) }
|
|
remove_from_sets(keys, conversation_id)
|
|
end
|
|
|
|
def add_assignment_membership(account_id:, conversation_id:, **membership)
|
|
add_to_sets(
|
|
assignment_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:assignee_id], membership[:team_id]),
|
|
conversation_id
|
|
)
|
|
end
|
|
|
|
def remove_assignment_membership(account_id:, conversation_id:, **membership)
|
|
keys = Array(membership[:inbox_ids]).flat_map do |inbox_id|
|
|
Array(membership[:assignee_ids]).flat_map do |assignee_id|
|
|
removable_assignment_keys(account_id, inbox_id, membership[:label_ids], assignee_id, membership[:team_ids])
|
|
end
|
|
end
|
|
remove_from_sets(keys, conversation_id)
|
|
end
|
|
|
|
def add_memberships(account_id:, memberships:, assignment: false)
|
|
return if memberships.blank?
|
|
|
|
Redis::Alfred.pipelined do |pipeline|
|
|
memberships.each do |membership|
|
|
keys = if assignment
|
|
assignment_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:assignee_id], membership[:team_id])
|
|
else
|
|
base_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:team_id])
|
|
end
|
|
|
|
keys.each do |key|
|
|
pipeline.sadd(key, membership[:conversation_id])
|
|
pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def counts_for_keys(keys)
|
|
keys = keys.compact_blank
|
|
return {} if keys.blank?
|
|
|
|
counts = Redis::Alfred.pipelined do |pipeline|
|
|
keys.each { |key| pipeline.scard(key) }
|
|
end
|
|
keys.zip(counts).to_h
|
|
end
|
|
|
|
def memberships_for_keys(keys, conversation_id)
|
|
keys = keys.compact_blank
|
|
return {} if keys.blank?
|
|
|
|
memberships = Redis::Alfred.pipelined do |pipeline|
|
|
keys.each { |key| pipeline.sismember(key, conversation_id) }
|
|
end
|
|
keys.zip(memberships.map { |membership| membership == true || membership == 1 }).to_h
|
|
end
|
|
|
|
private
|
|
|
|
def base_ready_key(account_id)
|
|
format(Redis::Alfred::UNREAD_CONVERSATIONS_BASE_READY, account_id: account_id)
|
|
end
|
|
|
|
def assignment_ready_key(account_id)
|
|
format(Redis::Alfred::UNREAD_CONVERSATIONS_ASSIGNMENT_READY, account_id: account_id)
|
|
end
|
|
|
|
def account_prefix(account_id)
|
|
format(Redis::Alfred::UNREAD_CONVERSATIONS_ACCOUNT_PREFIX, account_id: account_id)
|
|
end
|
|
|
|
def base_keys(account_id, inbox_id, label_ids, team_id = nil)
|
|
keys = [inbox_key(account_id, inbox_id)] + Array(label_ids).map { |label_id| label_inbox_key(account_id, label_id, inbox_id) }
|
|
keys << team_inbox_key(account_id, team_id, inbox_id) if team_id.present?
|
|
keys
|
|
end
|
|
|
|
def removable_base_keys(account_id, inbox_id, label_ids, team_ids)
|
|
keys = base_keys(account_id, inbox_id, label_ids)
|
|
keys.concat(Array(team_ids).compact_blank.map { |team_id| team_inbox_key(account_id, team_id, inbox_id) })
|
|
end
|
|
|
|
def assignment_keys(account_id, inbox_id, label_ids, assignee_id, team_id = nil)
|
|
keys = assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
|
|
keys << team_assignment_key(account_id, team_id, inbox_id, assignee_id) if team_id.present?
|
|
keys
|
|
end
|
|
|
|
def removable_assignment_keys(account_id, inbox_id, label_ids, assignee_id, team_ids)
|
|
keys = assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
|
|
keys.concat(Array(team_ids).compact_blank.map { |team_id| team_assignment_key(account_id, team_id, inbox_id, assignee_id) })
|
|
end
|
|
|
|
def assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
|
|
return assignee_keys(account_id, inbox_id, label_ids, assignee_id) if assignee_id.present?
|
|
|
|
unassigned_keys(account_id, inbox_id, label_ids)
|
|
end
|
|
|
|
def assignee_keys(account_id, inbox_id, label_ids, assignee_id)
|
|
[inbox_assignee_key(account_id, inbox_id, assignee_id)] +
|
|
Array(label_ids).map { |label_id| label_inbox_assignee_key(account_id, label_id, inbox_id, assignee_id) }
|
|
end
|
|
|
|
def unassigned_keys(account_id, inbox_id, label_ids)
|
|
[inbox_unassigned_key(account_id, inbox_id)] +
|
|
Array(label_ids).map { |label_id| label_inbox_unassigned_key(account_id, label_id, inbox_id) }
|
|
end
|
|
|
|
def team_assignment_key(account_id, team_id, inbox_id, assignee_id)
|
|
return team_inbox_assignee_key(account_id, team_id, inbox_id, assignee_id) if assignee_id.present?
|
|
|
|
team_inbox_unassigned_key(account_id, team_id, inbox_id)
|
|
end
|
|
|
|
def add_to_sets(keys, conversation_id)
|
|
write_to_sets(keys) { |pipeline, key| pipeline.sadd(key, conversation_id) }
|
|
end
|
|
|
|
def remove_from_sets(keys, conversation_id)
|
|
keys = keys.compact_blank
|
|
return false if keys.blank?
|
|
|
|
results = Redis::Alfred.pipelined do |pipeline|
|
|
keys.each do |key|
|
|
pipeline.srem(key, conversation_id)
|
|
pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
|
|
end
|
|
end
|
|
results.each_slice(2).any? { |removed, _| removed == true || (removed.respond_to?(:to_i) && removed.to_i.positive?) }
|
|
end
|
|
|
|
def write_to_sets(keys)
|
|
keys = keys.compact_blank
|
|
return if keys.blank?
|
|
|
|
Redis::Alfred.pipelined do |pipeline|
|
|
keys.each do |key|
|
|
yield(pipeline, key)
|
|
pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
|
|
end
|
|
end
|
|
end
|
|
|
|
def delete_matching(pattern)
|
|
Redis::Alfred.scan_each(match: pattern, count: 1000) do |key|
|
|
Redis::Alfred.delete(key)
|
|
end
|
|
end
|
|
|
|
def assignment_key_patterns(account_id)
|
|
prefix = account_prefix(account_id)
|
|
[
|
|
assignment_ready_key(account_id),
|
|
"#{prefix}::INBOX::*::UNASSIGNED",
|
|
"#{prefix}::INBOX::*::ASSIGNEE::*",
|
|
"#{prefix}::LABEL::*::INBOX::*::UNASSIGNED",
|
|
"#{prefix}::LABEL::*::INBOX::*::ASSIGNEE::*",
|
|
"#{prefix}::TEAM::*::INBOX::*::UNASSIGNED",
|
|
"#{prefix}::TEAM::*::INBOX::*::ASSIGNEE::*"
|
|
]
|
|
end
|
|
end
|
|
end
|