feat: Add sidebar unread counts for filters (CW-7262) (#14726)
## Description Extends the conversation unread-count system so the left sidebar can show unread badges for Mentions, Participating, Unattended, and saved conversation folders. Folder badges reuse the existing `custom_filters` conversation filter semantics, store user-scoped Redis sets lazily, and skip unsupported folder filters so invalid saved folders continue to render without a badge. The Unattended badge counts all visible unread open conversations that match the existing unattended conversation scope. Closes - [CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders) ## What changed - Added user-scoped unread-count Redis keys and cache builders for mentions, participating conversations, unattended conversations, and saved folder filters. - Reused `Conversations::FilterService` through a relation-returning path so folder counts match the folder conversation list behavior. - Invalidated user filter caches from mention, participant, custom-filter, and relevant conversation update events. - Extended the unread-count endpoint payload and sidebar Vuex/sidebar rendering for the new badge counts, including the Unattended sidebar item. - Added Ruby, Enterprise, request, listener, and frontend store coverage for the new unread-count dimensions. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Created local validation folders for `john@acme.inc` and confirmed the unread-count payload includes open, resolved, and high-priority folder badges while excluding the invalid unsupported folder. - Added coverage for the Unattended badge rule: all visible unread open conversations matching `Conversation.unattended`. - Ran focused unread-count Ruby specs, including service, listener, request, and Enterprise counter coverage. - Ran frontend unread-count store specs. - Ran RuboCop on the touched Ruby files. - Ran ESLint through the project script; it completed with warnings in existing unrelated files and no errors. <img width="369" height="525" alt="Screenshot 2026-06-13 at 10 51 39 PM" src="https://github.com/user-attachments/assets/36b1d2c4-dac1-4f6f-9c0e-7ef5a6cc2975" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] Documentation changes are not required for this internal unread-count behavior - [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 - [x] No dependent downstream changes are required --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
co-authored by
Muhsin Keloth
parent
352f120c6a
commit
fe6368b42e
+52
-11
@@ -53,38 +53,79 @@ RSpec.describe Account do
|
||||
describe 'conversation unread counts feature flag' do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:user) { create(:user) }
|
||||
let(:store) { Conversations::UnreadCounts::Store }
|
||||
let(:inbox_key) { store.inbox_key(account.id, inbox.id) }
|
||||
|
||||
after do
|
||||
store.clear_account!(account.id)
|
||||
let(:filter_keys) do
|
||||
[
|
||||
store.user_mentions_key(account.id, user.id),
|
||||
store.user_participating_key(account.id, user.id),
|
||||
store.user_unattended_key(account.id, user.id),
|
||||
store.user_folder_key(account.id, user.id, 1)
|
||||
]
|
||||
end
|
||||
|
||||
it 'clears unread count cache when the feature is enabled' do
|
||||
after do
|
||||
store.clear_all_account!(account.id)
|
||||
end
|
||||
|
||||
it 'clears all unread count cache when the feature is enabled' do
|
||||
build_unread_count_cache
|
||||
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
|
||||
expect(store.base_ready?(account.id)).to be(false)
|
||||
expect(store.assignment_ready?(account.id)).to be(false)
|
||||
expect(store.counts_for_keys([inbox_key])).to eq(inbox_key => 0)
|
||||
expect_unread_count_cache_cleared
|
||||
end
|
||||
|
||||
it 'clears unread count cache when the feature is disabled' do
|
||||
it 'clears all unread count cache when the feature is disabled' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
build_unread_count_cache
|
||||
|
||||
account.disable_features!(:conversation_unread_counts)
|
||||
|
||||
expect(store.base_ready?(account.id)).to be(false)
|
||||
expect(store.assignment_ready?(account.id)).to be(false)
|
||||
expect(store.counts_for_keys([inbox_key])).to eq(inbox_key => 0)
|
||||
expect_unread_count_cache_cleared
|
||||
end
|
||||
|
||||
it 'clears all unread count cache when account cache keys are reset' do
|
||||
build_unread_count_cache
|
||||
|
||||
account.reset_cache_keys
|
||||
|
||||
expect_unread_count_cache_cleared
|
||||
end
|
||||
|
||||
def expect_unread_count_cache_cleared
|
||||
expect(unread_count_ready_markers).to all(be(false))
|
||||
expect(store.counts_for_keys(unread_count_keys).values).to all(eq(0))
|
||||
end
|
||||
|
||||
def unread_count_ready_markers
|
||||
[
|
||||
store.base_ready?(account.id),
|
||||
store.assignment_ready?(account.id),
|
||||
store.filters_ready?(account.id, user.id)
|
||||
]
|
||||
end
|
||||
|
||||
def unread_count_keys
|
||||
[inbox_key] + filter_keys
|
||||
end
|
||||
|
||||
def build_unread_count_cache
|
||||
store.mark_base_ready!(account.id)
|
||||
store.mark_assignment_ready!(account.id)
|
||||
store.mark_filters_ready!(account.id, user.id)
|
||||
store.add_base_membership(account_id: account.id, inbox_id: inbox.id, label_ids: [], conversation_id: 1)
|
||||
store.add_filter_memberships(
|
||||
account_id: account.id,
|
||||
user_id: user.id,
|
||||
filters: {
|
||||
mentions: [1],
|
||||
participating: [2],
|
||||
unattended: [3]
|
||||
},
|
||||
folders: { 1 => [4] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -42,4 +42,32 @@ RSpec.describe AccountUser do
|
||||
expect(user.assigned_conversations.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unread filter count invalidation' do
|
||||
let(:notifier) { instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true) }
|
||||
|
||||
before do
|
||||
allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
|
||||
end
|
||||
|
||||
it 'notifies when the account role changes' do
|
||||
account_user.update!(role: :administrator)
|
||||
|
||||
expect(Conversations::UnreadCounts::UserFilterNotifier).to have_received(:new).with(
|
||||
account: account_user.account,
|
||||
user: account_user.user
|
||||
)
|
||||
expect(notifier).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'notifies when account access is removed' do
|
||||
expect(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).with(
|
||||
account: account_user.account,
|
||||
user: account_user.user
|
||||
).and_return(notifier)
|
||||
expect(notifier).to receive(:perform)
|
||||
|
||||
account_user.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,4 +18,30 @@ RSpec.describe InboxMember do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unread filter count invalidation' do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:user) { create(:user, account: account, role: :agent) }
|
||||
let(:notifier) { instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true) }
|
||||
|
||||
before do
|
||||
allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
|
||||
end
|
||||
|
||||
it 'notifies when inbox access is added' do
|
||||
create(:inbox_member, inbox: inbox, user: user)
|
||||
|
||||
expect(Conversations::UnreadCounts::UserFilterNotifier).to have_received(:new).with(account: account, user: user)
|
||||
expect(notifier).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'notifies when inbox access is removed' do
|
||||
inbox_member = create(:inbox_member, inbox: inbox, user: user)
|
||||
expect(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).with(account: account, user: user).and_return(notifier)
|
||||
expect(notifier).to receive(:perform)
|
||||
|
||||
inbox_member.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user