chore: added a new sort by unread option for conversations (CW-7152) (#14512)

## Description

Added a new option for the sort by option in conversation filters called
unread. This is to filter out unread conversations.
Fixes # CW-7152

## 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 manually in local.

<img width="1397" height="603" alt="Screenshot 2026-05-20 at 10 40
20 PM"
src="https://github.com/user-attachments/assets/6c60263e-907b-419f-a7ed-06010bbe8736"
/>


## 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
This commit is contained in:
Sony Mathew
2026-06-15 17:58:04 +05:30
committed by GitHub
parent 396631ad7d
commit 4816e923b6
7 changed files with 109 additions and 0 deletions
+1
View File
@@ -12,6 +12,7 @@ class ConversationFinder
'waiting_since_asc' => %w[sort_on_waiting_since asc],
'waiting_since_desc' => %w[sort_on_waiting_since desc],
'priority_desc_created_at_asc' => %w[sort_on_priority_created_at desc],
'unread' => %w[sort_on_unread desc],
# To be removed in v3.5.0
'latest' => %w[sort_on_last_activity_at desc],
@@ -308,6 +308,15 @@ function filterByAssigneeTab(conversations) {
return [...conversations];
}
function sortByUnreadStatus(conversations) {
return [...conversations].sort((a, b) => {
const unreadCountDiff = (b.unread_count || 0) - (a.unread_count || 0);
if (unreadCountDiff !== 0) return unreadCountDiff;
return (b.last_activity_at || 0) - (a.last_activity_at || 0);
});
}
const conversationList = computed(() => {
let localConversationList = [];
@@ -337,6 +346,13 @@ const conversationList = computed(() => {
});
}
if (
!hasAppliedFiltersOrActiveFolders.value &&
activeSortBy.value === wootConstants.SORT_BY_TYPE.UNREAD
) {
localConversationList = sortByUnreadStatus(localConversationList);
}
return localConversationList;
});
@@ -78,6 +78,10 @@ const chatSortOptions = computed(() => [
label: t('CHAT_LIST.SORT_ORDER_ITEMS.created_at_asc.TEXT'),
value: 'created_at_asc',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.unread.TEXT'),
value: 'unread',
},
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.priority_desc.TEXT'),
value: 'priority_desc',
@@ -27,6 +27,7 @@ export default {
WAITING_SINCE_ASC: 'waiting_since_asc',
WAITING_SINCE_DESC: 'waiting_since_desc',
PRIORITY_DESC_CREATED_AT_ASC: 'priority_desc_created_at_asc',
UNREAD: 'unread',
},
ARTICLE_STATUS_TYPES: {
DRAFT: 0,
@@ -79,6 +79,9 @@
},
"priority_desc_created_at_asc": {
"TEXT": "Priority: Highest first, Created: Oldest first"
},
"unread": {
"TEXT": "Unread Count: Highest first"
}
},
"ATTACHMENTS": {
+23
View File
@@ -78,6 +78,9 @@ class Conversation < ApplicationRecord
scope :unassigned, -> { where(assignee_id: nil) }
scope :assigned, -> { where.not(assignee_id: nil) }
scope :assigned_to, ->(agent) { where(assignee_id: agent.id) }
scope :sort_on_unread, lambda { |_direction|
order(unread_messages_count_arel.desc).sort_on_last_activity_at('desc')
}
scope :unattended, -> { where(first_reply_created_at: nil).or(where.not(waiting_since: nil)) }
scope :resolvable_not_waiting, lambda { |auto_resolve_after|
return none if auto_resolve_after.to_i.zero?
@@ -206,6 +209,26 @@ class Conversation < ApplicationRecord
inbox.inbox_type == 'Twitter' && additional_attributes['type'] == 'tweet'
end
def self.unread_messages_count_arel
messages = Message.arel_table
conversations = arel_table
unread_messages = messages
.project(messages[:id].count)
.where(unread_messages_condition(messages, conversations))
Arel::Nodes::Grouping.new(unread_messages.ast)
end
def self.unread_messages_condition(messages, conversations)
messages[:conversation_id].eq(conversations[:id])
.and(messages[:account_id].eq(conversations[:account_id]))
.and(messages[:message_type].eq(Message.message_types[:incoming]))
.and(
conversations[:agent_last_seen_at].eq(nil)
.or(messages[:created_at].gt(conversations[:agent_last_seen_at]))
)
end
def recent_messages
messages.chat.last(5)
end
+61
View File
@@ -96,6 +96,67 @@ describe ConversationFinder do
end
end
context 'with unread sort' do
let(:params) { { status: 'open', sort_by: 'unread' } }
it 'returns all conversations matching the selected status with the highest unread count first' do
most_unread_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.hour.ago)
unread_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.hour.ago)
read_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.minute.from_now)
resolved_unread_conversation = create(:conversation, account: account, inbox: inbox, status: 'resolved',
agent_last_seen_at: 1.hour.ago)
[most_unread_conversation, unread_conversation, read_conversation, resolved_unread_conversation].each do |conversation|
create(:message, account: account, inbox: inbox, conversation: conversation,
message_type: :incoming, created_at: 5.minutes.ago)
end
create(:message, account: account, inbox: inbox, conversation: most_unread_conversation,
message_type: :incoming, created_at: 4.minutes.ago)
resolved_unread_conversation.update!(status: 'resolved')
read_conversation.update!(last_activity_at: 1.minute.from_now)
unread_conversation.update!(last_activity_at: 2.minutes.from_now)
result = conversation_finder.perform
conversation_ids = result[:conversations].map(&:id)
expect(conversation_ids).to include(most_unread_conversation.id, unread_conversation.id, read_conversation.id)
expect(conversation_ids).not_to include(resolved_unread_conversation.id)
expect(conversation_ids.index(most_unread_conversation.id)).to be < conversation_ids.index(unread_conversation.id)
expect(conversation_ids.index(unread_conversation.id)).to be < conversation_ids.index(read_conversation.id)
end
it 'includes private incoming messages in unread counts used for ordering' do
private_unread_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.hour.ago)
unread_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.hour.ago)
read_conversation = create(:conversation, account: account, inbox: inbox,
agent_last_seen_at: 1.minute.from_now)
2.times do
create(:message, account: account, inbox: inbox, conversation: private_unread_conversation,
message_type: :incoming, private: true, created_at: 5.minutes.ago)
end
create(:message, account: account, inbox: inbox, conversation: unread_conversation,
message_type: :incoming, created_at: 5.minutes.ago)
create(:message, account: account, inbox: inbox, conversation: read_conversation,
message_type: :incoming, created_at: 5.minutes.ago)
private_unread_conversation.update!(last_activity_at: 10.minutes.ago)
unread_conversation.update!(last_activity_at: 2.minutes.from_now)
read_conversation.update!(last_activity_at: 1.minute.from_now)
result = conversation_finder.perform
conversation_ids = result[:conversations].map(&:id)
expect(private_unread_conversation.unread_incoming_messages.count).to eq 2
expect(conversation_ids.index(private_unread_conversation.id)).to be < conversation_ids.index(unread_conversation.id)
expect(conversation_ids.index(unread_conversation.id)).to be < conversation_ids.index(read_conversation.id)
end
end
context 'with assignee_type assigned' do
let(:params) { { assignee_type: 'assigned' } }