Files
chatwoot/app/javascript/dashboard/store/modules/conversationUnreadCounts.js
T
fe6368b42e 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>
2026-06-16 23:46:49 +05:30

104 lines
2.7 KiB
JavaScript

import ConversationAPI from '../../api/conversations';
import types from '../mutation-types';
export const state = {
allCount: 0,
inboxes: {},
labels: {},
teams: {},
mentionsCount: 0,
participatingCount: 0,
unattendedCount: 0,
folders: {},
};
const normalizeCount = count => {
const parsedCount = Number(count);
return Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 0;
};
const normalizeCounts = counts => {
return Object.entries(counts || {}).reduce((result, [id, count]) => {
const parsedCount = normalizeCount(count);
if (parsedCount > 0) {
result[String(id)] = parsedCount;
}
return result;
}, {});
};
export const getters = {
getAllUnreadCount($state) {
return $state.allCount;
},
getInboxUnreadCount: $state => inboxId => {
return $state.inboxes[String(inboxId)] || 0;
},
getLabelUnreadCount: $state => labelId => {
return $state.labels[String(labelId)] || 0;
},
getTeamUnreadCount: $state => teamId => {
return $state.teams[String(teamId)] || 0;
},
getMentionsUnreadCount($state) {
return $state.mentionsCount;
},
getParticipatingUnreadCount($state) {
return $state.participatingCount;
},
getUnattendedUnreadCount($state) {
return $state.unattendedCount;
},
getFolderUnreadCount: $state => folderId => {
return $state.folders[String(folderId)] || 0;
},
getInboxUnreadCounts($state) {
return $state.inboxes;
},
getLabelUnreadCounts($state) {
return $state.labels;
},
getTeamUnreadCounts($state) {
return $state.teams;
},
getFolderUnreadCounts($state) {
return $state.folders;
},
};
export const actions = {
get: async function getUnreadCounts({ commit }) {
try {
const response = await ConversationAPI.getUnreadCounts();
commit(types.SET_CONVERSATION_UNREAD_COUNTS, response.data.payload);
} catch (error) {
// Ignore errors so the sidebar can continue rendering without badges.
}
},
clear({ commit }) {
commit(types.SET_CONVERSATION_UNREAD_COUNTS, {});
},
};
export const mutations = {
[types.SET_CONVERSATION_UNREAD_COUNTS]($state, payload = {}) {
$state.allCount = normalizeCount(payload.all_count);
$state.inboxes = normalizeCounts(payload.inboxes);
$state.labels = normalizeCounts(payload.labels);
$state.teams = normalizeCounts(payload.teams);
$state.mentionsCount = normalizeCount(payload.mentions_count);
$state.participatingCount = normalizeCount(payload.participating_count);
$state.unattendedCount = normalizeCount(payload.unattended_count);
$state.folders = normalizeCounts(payload.folders);
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};