chore: Enable Participating tab for conversations (#11714)

## Summary

This PR enables the **Participating** conversation view in the main
sidebar and keeps the behavior aligned with existing conversation views.

## What changed

- Added **Participating** under Conversations in the new sidebar.
- Added a guard in conversation realtime `addConversation` flow so
generic `conversation.created` events are not injected while the user is
on Participating view.
- Added participating route mapping in conversation-list redirect helper
so list redirects resolve correctly to `/participating/conversations`.

## Scope notes

- Kept changes minimal and consistent with current `develop` behavior.
- No additional update-event filtering was added beyond what existing
views already do.

---------


Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Sojan Jose
2026-04-15 17:03:39 +05:30
committed by GitHub
parent 3f9f054c43
commit b96bf41234
13 changed files with 216 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ import { createPendingMessage } from 'dashboard/helper/commons';
import {
buildConversationList,
isOnMentionsView,
isOnParticipatingView,
isOnUnattendedView,
isOnFoldersView,
} from './helpers/actionHelpers';
@@ -371,6 +372,7 @@ const actions = {
!hasAppliedFilters &&
!isOnFoldersView(rootState) &&
!isOnMentionsView(rootState) &&
!isOnParticipatingView(rootState) &&
!isOnUnattendedView(rootState) &&
isMatchingInboxFilter
) {
@@ -395,6 +397,7 @@ const actions = {
const {
meta: { sender },
} = conversation;
commit(types.UPDATE_CONVERSATION, conversation);
dispatch('conversationLabels/setConversationLabel', {

View File

@@ -102,6 +102,19 @@ const getters = {
return isUnAssigned && shouldFilter;
});
},
getParticipatingChats: (_state, _, __, rootGetters) => activeFilters => {
const currentUserId = rootGetters.getCurrentUser?.id;
const getWatchers = rootGetters['conversationWatchers/getByConversationId'];
return _state.allConversations.filter(conversation => {
const watchers = getWatchers(conversation.id);
// Watchers are only loaded for the conversation open in the detail
// panel. If loaded and current user is not in them, filter it out.
if (watchers && !watchers.some(w => w.id === currentUserId)) {
return false;
}
return applyPageFilters(conversation, activeFilters);
});
},
getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
const currentUser = rootGetters.getCurrentUser;
const currentUserId = rootGetters.getCurrentUser.id;

View File

@@ -30,6 +30,14 @@ export const isOnUnattendedView = ({ route: { name: routeName } }) => {
return UNATTENDED_ROUTES.includes(routeName);
};
export const isOnParticipatingView = ({ route: { name: routeName } }) => {
const PARTICIPATING_ROUTES = [
'conversation_participating',
'conversation_through_participating',
];
return PARTICIPATING_ROUTES.includes(routeName);
};
export const isOnFoldersView = ({ route: { name: routeName } }) => {
const FOLDER_ROUTES = [
'folder_conversations',

View File

@@ -1,4 +1,8 @@
import { isOnMentionsView, isOnFoldersView } from '../actionHelpers';
import {
isOnMentionsView,
isOnFoldersView,
isOnParticipatingView,
} from '../actionHelpers';
describe('#isOnMentionsView', () => {
it('return valid responses when passing the state', () => {
@@ -24,3 +28,19 @@ describe('#isOnFoldersView', () => {
);
});
});
describe('#isOnParticipatingView', () => {
it('return valid responses when passing the state', () => {
expect(
isOnParticipatingView({ route: { name: 'conversation_participating' } })
).toBe(true);
expect(
isOnParticipatingView({
route: { name: 'conversation_through_participating' },
})
).toBe(true);
expect(
isOnParticipatingView({ route: { name: 'conversation_messages' } })
).toBe(false);
});
});

View File

@@ -258,7 +258,11 @@ export const mutations = {
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
}
} else {
_state.allConversations.push(conversation);
const { conversationType } = _state.conversationFilters || {};
const { MENTION, PARTICIPATING } = wootConstants.CONVERSATION_TYPE;
if (![MENTION, PARTICIPATING].includes(conversationType)) {
_state.allConversations.push(conversation);
}
}
},

View File

@@ -183,6 +183,73 @@ describe('#getters', () => {
]);
});
});
describe('#getParticipatingChats', () => {
const conversationList = [
{ id: 1, inbox_id: 2, status: 1, meta: { assignee: { id: 1 } } },
{ id: 2, inbox_id: 2, status: 1, meta: {} },
{ id: 3, inbox_id: 3, status: 1, meta: { assignee: { id: 2 } } },
];
it('returns all conversations when watchers are not loaded', () => {
const state = {
allConversations: conversationList,
participatingConversationIds: {},
};
const rootGetters = {
getCurrentUser: { id: 1 },
'conversationWatchers/getByConversationId': () => undefined,
};
const result = getters.getParticipatingChats(
state,
{},
{},
rootGetters
)({ status: 1 });
expect(result).toEqual(conversationList);
});
it('filters out conversation when watchers loaded and user not participating', () => {
const state = {
allConversations: conversationList,
participatingConversationIds: {},
};
const rootGetters = {
getCurrentUser: { id: 1 },
'conversationWatchers/getByConversationId': id => {
if (id === 2) return [{ id: 3 }];
return undefined;
},
};
const result = getters.getParticipatingChats(
state,
{},
{},
rootGetters
)({ status: 1 });
expect(result).toEqual([conversationList[0], conversationList[2]]);
});
it('keeps conversation when watchers loaded and user is participating', () => {
const state = {
allConversations: conversationList,
participatingConversationIds: {},
};
const rootGetters = {
getCurrentUser: { id: 1 },
'conversationWatchers/getByConversationId': id => {
if (id === 1) return [{ id: 1 }, { id: 2 }];
return undefined;
},
};
const result = getters.getParticipatingChats(
state,
{},
{},
rootGetters
)({ status: 1 });
expect(result).toEqual(conversationList);
});
});
describe('#getConversationById', () => {
it('get conversations based on id', () => {
const state = {

View File

@@ -786,9 +786,55 @@ describe('#mutations', () => {
});
});
it('should add conversation if not found', () => {
it('should add conversation if not found on normal view', () => {
const state = {
allConversations: [],
conversationFilters: {},
};
const conversation = {
id: 1,
status: 'open',
};
mutations[types.UPDATE_CONVERSATION](state, conversation);
expect(state.allConversations).toEqual([conversation]);
});
it('should not add conversation if not found on participating view', () => {
const state = {
allConversations: [],
conversationFilters: { conversationType: 'participating' },
};
const conversation = {
id: 1,
status: 'open',
};
mutations[types.UPDATE_CONVERSATION](state, conversation);
expect(state.allConversations).toEqual([]);
});
it('should not add conversation if not found on mention view', () => {
const state = {
allConversations: [],
conversationFilters: { conversationType: 'mention' },
};
const conversation = {
id: 1,
status: 'open',
};
mutations[types.UPDATE_CONVERSATION](state, conversation);
expect(state.allConversations).toEqual([]);
});
it('should add conversation if not found on unattended view', () => {
const state = {
allConversations: [],
conversationFilters: { conversationType: 'unattended' },
};
const conversation = {