feat: Unread Count: Frontend changes for showing unread count badges (3/3)[CW-6851] (#14372)

# Pull Request Template

## Description
This is the third and final PR in a series of PRs for Introducing unread
counts in the sidebar for inboxes and labels.

In this PR:
* Added frontend changes to show the badges for unread counts for
Inboxes and Labels
* 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?

Tested this locally. Cases to test:
* Send a message from the widget and see if the count changes
* Mark a conversation as unread and see the count change for inbox
* Open an unread conversation as agent and see the count go down
* Add a label to an unread conversation from sidebar right click action
without opening the conversation and see the count of un-reads on the
label change

Added the screenshot of how it will look like

<img width="614" height="990" alt="Screenshot 2026-05-05 at 7 00 11 PM"
src="https://github.com/user-attachments/assets/99fbaa9f-bcf2-4d8d-86e2-5727f652a9dd"
/>



## 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>
This commit is contained in:
Sony Mathew
2026-05-20 19:21:25 +05:30
committed by GitHub
co-authored by Sojan Jose
parent 27f2c2b392
commit f33e469e9a
21 changed files with 626 additions and 6 deletions
@@ -0,0 +1,70 @@
import ConversationAPI from '../../api/conversations';
import types from '../mutation-types';
export const state = {
inboxes: {},
labels: {},
teams: {},
};
const normalizeCounts = counts => {
return Object.entries(counts || {}).reduce((result, [id, count]) => {
const parsedCount = Number(count);
if (Number.isFinite(parsedCount) && parsedCount > 0) {
result[String(id)] = parsedCount;
}
return result;
}, {});
};
export const getters = {
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;
},
getInboxUnreadCounts($state) {
return $state.inboxes;
},
getLabelUnreadCounts($state) {
return $state.labels;
},
getTeamUnreadCounts($state) {
return $state.teams;
},
};
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.inboxes = normalizeCounts(payload.inboxes);
$state.labels = normalizeCounts(payload.labels);
$state.teams = normalizeCounts(payload.teams);
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
@@ -0,0 +1,53 @@
import axios from 'axios';
import { actions } from '../../conversationUnreadCounts';
import types from '../../../mutation-types';
const commit = vi.fn();
global.axios = axios;
vi.mock('axios');
describe('#actions', () => {
beforeEach(() => {
commit.mockClear();
axios.get.mockReset();
});
describe('#get', () => {
it('commits unread counts when API is successful', async () => {
const payload = {
inboxes: { 1: '2' },
labels: { 3: 4 },
teams: { 5: 6 },
};
axios.get.mockResolvedValue({ data: { payload } });
await actions.get({ commit });
expect(axios.get).toHaveBeenCalledWith(
'/api/v1/conversations/unread_counts'
);
expect(commit.mock.calls).toEqual([
[types.SET_CONVERSATION_UNREAD_COUNTS, payload],
]);
});
it('does not commit when API fails', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit });
expect(commit).not.toHaveBeenCalled();
});
});
describe('#clear', () => {
it('clears unread counts', () => {
actions.clear({ commit });
expect(commit).toHaveBeenCalledWith(
types.SET_CONVERSATION_UNREAD_COUNTS,
{}
);
});
});
});
@@ -0,0 +1,51 @@
import { getters } from '../../conversationUnreadCounts';
describe('#getters', () => {
it('returns inbox unread count by id', () => {
const state = {
inboxes: { 1: 2 },
labels: {},
teams: {},
};
expect(getters.getInboxUnreadCount(state)(1)).toBe(2);
expect(getters.getInboxUnreadCount(state)('1')).toBe(2);
expect(getters.getInboxUnreadCount(state)(2)).toBe(0);
});
it('returns label unread count by id', () => {
const state = {
inboxes: {},
labels: { 3: 4 },
teams: {},
};
expect(getters.getLabelUnreadCount(state)(3)).toBe(4);
expect(getters.getLabelUnreadCount(state)('3')).toBe(4);
expect(getters.getLabelUnreadCount(state)(4)).toBe(0);
});
it('returns team unread count by id', () => {
const state = {
inboxes: {},
labels: {},
teams: { 5: 6 },
};
expect(getters.getTeamUnreadCount(state)(5)).toBe(6);
expect(getters.getTeamUnreadCount(state)('5')).toBe(6);
expect(getters.getTeamUnreadCount(state)(6)).toBe(0);
});
it('returns unread count maps', () => {
const state = {
inboxes: { 1: 2 },
labels: { 3: 4 },
teams: { 5: 6 },
};
expect(getters.getInboxUnreadCounts(state)).toEqual({ 1: 2 });
expect(getters.getLabelUnreadCounts(state)).toEqual({ 3: 4 });
expect(getters.getTeamUnreadCounts(state)).toEqual({ 5: 6 });
});
});
@@ -0,0 +1,48 @@
import types from '../../../mutation-types';
import { mutations } from '../../conversationUnreadCounts';
describe('#mutations', () => {
describe('#SET_CONVERSATION_UNREAD_COUNTS', () => {
it('normalizes unread count payload', () => {
const state = { inboxes: {}, labels: {}, teams: {} };
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {
inboxes: {
1: '2',
2: 0,
3: 'invalid',
},
labels: {
4: 5,
5: -1,
},
teams: {
6: '7',
7: 0,
},
});
expect(state).toEqual({
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
});
});
it('clears counts when payload is empty', () => {
const state = {
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
};
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {});
expect(state).toEqual({
inboxes: {},
labels: {},
teams: {},
});
});
});
});