diff --git a/VERSION_CW b/VERSION_CW
index 5c517bf11..fb0557132 100644
--- a/VERSION_CW
+++ b/VERSION_CW
@@ -1 +1 @@
-4.15.0
+4.15.1
diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb
index 9d53ec97e..74bf903f5 100644
--- a/app/finders/conversation_finder.rb
+++ b/app/finders/conversation_finder.rb
@@ -142,7 +142,7 @@ class ConversationFinder
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
when 'participating'
- @conversations = @conversations.where(id: current_user.participating_conversations.where(account_id: current_account.id).select(:id))
+ @conversations = current_user.participating_conversations.where(account_id: current_account.id)
when 'unattended'
@conversations = @conversations.unattended
end
diff --git a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue
index 234737008..a995cf510 100644
--- a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue
+++ b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue
@@ -22,10 +22,6 @@ const props = defineProps({
type: [Number, String],
default: 0,
},
- badgeTooltip: {
- type: String,
- default: '',
- },
});
const reauthorizationRequired = computed(() => {
@@ -38,7 +34,7 @@ const reauthorizationRequired = computed(() => {
})
);
-const unreadBadgeTooltips = computed(() => ({
- ALL: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.ALL'),
- INBOX: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.INBOX'),
- LABEL: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.LABEL'),
- TEAM: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.TEAM'),
- FOLDER: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.FOLDER'),
- MENTIONS: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.MENTIONS'),
- PARTICIPATING: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.PARTICIPATING'),
- UNATTENDED: t('SIDEBAR.UNREAD_COUNT_TOOLTIP.UNATTENDED'),
-}));
-
-const unreadBadgeTooltip = type => unreadBadgeTooltips.value[type];
-
const closeMobileSidebar = () => {
if (!props.isMobileSidebarOpen) return;
emit('closeMobileSidebar');
@@ -360,15 +335,12 @@ const menuItems = computed(() => {
label: t('SIDEBAR.ALL_CONVERSATIONS'),
icon: 'i-lucide-inbox',
badgeCount: allUnreadCount.value,
- badgeTooltip: unreadBadgeTooltip('ALL'),
activeOn: ['inbox_conversation'],
to: accountScopedRoute('home'),
},
{
name: 'Mentions',
label: t('SIDEBAR.MENTIONED_CONVERSATIONS'),
- badgeCount: mentionsUnreadCount.value,
- badgeTooltip: unreadBadgeTooltip('MENTIONS'),
icon: 'i-lucide-at-sign',
activeOn: ['conversation_through_mentions'],
to: accountScopedRoute('conversation_mentions'),
@@ -376,8 +348,6 @@ const menuItems = computed(() => {
{
name: 'Participating',
label: t('SIDEBAR.PARTICIPATING_CONVERSATIONS'),
- badgeCount: participatingUnreadCount.value,
- badgeTooltip: unreadBadgeTooltip('PARTICIPATING'),
icon: 'i-lucide-user-round-check',
activeOn: ['conversation_through_participating'],
to: accountScopedRoute('conversation_participating'),
@@ -385,8 +355,6 @@ const menuItems = computed(() => {
{
name: 'Unattended',
activeOn: ['conversation_through_unattended'],
- badgeCount: unattendedUnreadCount.value,
- badgeTooltip: unreadBadgeTooltip('UNATTENDED'),
label: t('SIDEBAR.UNATTENDED_CONVERSATIONS'),
icon: 'i-lucide-clock-alert',
to: accountScopedRoute('conversation_unattended'),
@@ -402,8 +370,6 @@ const menuItems = computed(() => {
children: sortedFolders.value.map(view => ({
name: `${view.name}-${view.id}`,
label: view.name,
- badgeCount: getFolderUnreadCount.value(view.id),
- badgeTooltip: unreadBadgeTooltip('FOLDER'),
to: accountScopedRoute('folder_conversations', { id: view.id }),
})),
},
@@ -419,7 +385,6 @@ const menuItems = computed(() => {
name: `${team.name}-${team.id}`,
label: team.name,
badgeCount: getTeamUnreadCount.value(team.id),
- badgeTooltip: unreadBadgeTooltip('TEAM'),
to: accountScopedRoute('team_conversations', { teamId: team.id }),
})),
},
@@ -435,7 +400,6 @@ const menuItems = computed(() => {
name: `${inbox.name}-${inbox.id}`,
label: inbox.name,
badgeCount: getInboxUnreadCount.value(inbox.id),
- badgeTooltip: unreadBadgeTooltip('INBOX'),
icon: h(ChannelIcon, { inbox, class: 'size-[16px]' }),
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
component: leafProps =>
@@ -444,7 +408,6 @@ const menuItems = computed(() => {
active: leafProps.active,
inbox,
badgeCount: leafProps.badgeCount,
- badgeTooltip: leafProps.badgeTooltip,
}),
})),
},
@@ -460,7 +423,6 @@ const menuItems = computed(() => {
name: `${label.title}-${label.id}`,
label: label.title,
badgeCount: getLabelUnreadCount.value(label.id),
- badgeTooltip: unreadBadgeTooltip('LABEL'),
icon: h('span', {
class: `size-[8px] rounded-sm`,
style: { backgroundColor: label.color },
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarCollapsedPopover.vue b/app/javascript/dashboard/components-next/sidebar/SidebarCollapsedPopover.vue
index 8f1de246e..2e431eb7e 100644
--- a/app/javascript/dashboard/components-next/sidebar/SidebarCollapsedPopover.vue
+++ b/app/javascript/dashboard/components-next/sidebar/SidebarCollapsedPopover.vue
@@ -197,10 +197,7 @@ onMounted(async () => {
class="size-4 flex-shrink-0"
/>
{{ subChild.label }}
-
+
@@ -223,10 +220,7 @@ onMounted(async () => {
class="size-4 flex-shrink-0"
/>
{{ child.label }}
-
+
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
index 404bbe3f3..1d23ad0ed 100644
--- a/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
+++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
@@ -12,7 +12,6 @@ const props = defineProps({
active: { type: Boolean, default: false },
component: { type: Function, default: null },
badgeCount: { type: [Number, String], default: 0 },
- badgeTooltip: { type: String, default: '' },
hideTreeLine: { type: Boolean, default: false },
thinTreeLine: { type: Boolean, default: false },
});
@@ -53,14 +52,14 @@ const TREE_CONNECTOR =
{{ label }}
-
+
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarUnreadBadge.vue b/app/javascript/dashboard/components-next/sidebar/SidebarUnreadBadge.vue
index 2a91bfcb4..95d95236a 100644
--- a/app/javascript/dashboard/components-next/sidebar/SidebarUnreadBadge.vue
+++ b/app/javascript/dashboard/components-next/sidebar/SidebarUnreadBadge.vue
@@ -3,7 +3,6 @@ import { computed } from 'vue';
const props = defineProps({
count: { type: [Number, String], default: 0 },
- tooltip: { type: String, default: '' },
});
const normalizedCount = computed(() => {
@@ -19,7 +18,6 @@ const displayCount = computed(() =>
diff --git a/app/javascript/dashboard/components-next/sidebar/specs/ChannelLeaf.spec.js b/app/javascript/dashboard/components-next/sidebar/specs/ChannelLeaf.spec.js
index c2ee5219f..03d963134 100644
--- a/app/javascript/dashboard/components-next/sidebar/specs/ChannelLeaf.spec.js
+++ b/app/javascript/dashboard/components-next/sidebar/specs/ChannelLeaf.spec.js
@@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils';
import ChannelLeaf from '../ChannelLeaf.vue';
-import SidebarUnreadBadge from '../SidebarUnreadBadge.vue';
const mountChannelLeaf = props =>
mount(ChannelLeaf, {
@@ -29,17 +28,6 @@ describe('ChannelLeaf', () => {
expect(badge.text()).toBe('3');
});
- it('passes tooltip copy to unread badge', () => {
- const wrapper = mountChannelLeaf({
- badgeCount: 3,
- badgeTooltip: 'Total unread conversations in this inbox',
- });
-
- expect(wrapper.findComponent(SidebarUnreadBadge).props('tooltip')).toBe(
- 'Total unread conversations in this inbox'
- );
- });
-
it('does not render unread badge when count is zero', () => {
const wrapper = mountChannelLeaf({ badgeCount: 0 });
diff --git a/app/javascript/dashboard/components-next/sidebar/specs/SidebarGroupLeaf.spec.js b/app/javascript/dashboard/components-next/sidebar/specs/SidebarGroupLeaf.spec.js
index b63ca901c..79b62d191 100644
--- a/app/javascript/dashboard/components-next/sidebar/specs/SidebarGroupLeaf.spec.js
+++ b/app/javascript/dashboard/components-next/sidebar/specs/SidebarGroupLeaf.spec.js
@@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils';
import { h } from 'vue';
import SidebarGroupLeaf from '../SidebarGroupLeaf.vue';
-import SidebarUnreadBadge from '../SidebarUnreadBadge.vue';
vi.mock('../provider', () => ({
useSidebarContext: () => ({
@@ -45,17 +44,6 @@ describe('SidebarGroupLeaf', () => {
expect(badge.text()).toBe('7');
});
- it('passes tooltip copy to unread badge', () => {
- const wrapper = mountLeaf({
- badgeCount: 7,
- badgeTooltip: 'Total unread conversations in this label',
- });
-
- expect(wrapper.findComponent(SidebarUnreadBadge).props('tooltip')).toBe(
- 'Total unread conversations in this label'
- );
- });
-
it('does not render unread badge when count is zero', () => {
const wrapper = mountLeaf({ badgeCount: 0 });
@@ -75,17 +63,14 @@ describe('SidebarGroupLeaf', () => {
it('passes unread count to custom leaf components', () => {
const wrapper = mountLeaf({
badgeCount: 4,
- badgeTooltip: 'Total unread conversations in this inbox',
component: leafProps =>
h(
'span',
{ 'data-test-id': 'custom-leaf-count' },
- `${leafProps.badgeCount}:${leafProps.badgeTooltip}`
+ leafProps.badgeCount
),
});
- expect(wrapper.find('[data-test-id="custom-leaf-count"]').text()).toBe(
- '4:Total unread conversations in this inbox'
- );
+ expect(wrapper.find('[data-test-id="custom-leaf-count"]').text()).toBe('4');
});
});
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 9afe4f963..5e2543698 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -316,16 +316,6 @@
"MENTIONED_CONVERSATIONS": "Mentions",
"PARTICIPATING_CONVERSATIONS": "Participating",
"UNATTENDED_CONVERSATIONS": "Unattended",
- "UNREAD_COUNT_TOOLTIP": {
- "ALL": "Total unread conversations",
- "INBOX": "Total unread conversations in this inbox",
- "LABEL": "Total unread conversations in this label",
- "TEAM": "Total unread conversations in this team",
- "FOLDER": "Total unread conversations in this folder",
- "MENTIONS": "Unread conversations where you were mentioned",
- "PARTICIPATING": "Unread conversations you're participating in",
- "UNATTENDED": "Unread unattended conversations"
- },
"REPORTS": "Reports",
"SETTINGS": "Settings",
"CONTACTS": "Contacts",
diff --git a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js
index 03bbb31eb..c03249311 100644
--- a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js
+++ b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js
@@ -6,10 +6,6 @@ export const state = {
inboxes: {},
labels: {},
teams: {},
- mentionsCount: 0,
- participatingCount: 0,
- unattendedCount: 0,
- folders: {},
};
const normalizeCount = count => {
@@ -41,18 +37,6 @@ export const getters = {
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;
},
@@ -62,9 +46,6 @@ export const getters = {
getTeamUnreadCounts($state) {
return $state.teams;
},
- getFolderUnreadCounts($state) {
- return $state.folders;
- },
};
export const actions = {
@@ -87,10 +68,6 @@ export const mutations = {
$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);
},
};
diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
index 311aed95e..9fe19e22d 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
@@ -57,32 +57,10 @@ describe('#getters', () => {
inboxes: { 1: 2 },
labels: { 3: 4 },
teams: { 5: 6 },
- folders: { 8: 9 },
};
expect(getters.getInboxUnreadCounts(state)).toEqual({ 1: 2 });
expect(getters.getLabelUnreadCounts(state)).toEqual({ 3: 4 });
expect(getters.getTeamUnreadCounts(state)).toEqual({ 5: 6 });
- expect(getters.getFolderUnreadCounts(state)).toEqual({ 8: 9 });
- });
-
- it('returns mentions, participating, unattended, and folder unread counts', () => {
- const state = {
- allCount: 0,
- inboxes: {},
- labels: {},
- teams: {},
- mentionsCount: 3,
- participatingCount: 4,
- unattendedCount: 5,
- folders: { 7: 8 },
- };
-
- expect(getters.getMentionsUnreadCount(state)).toBe(3);
- expect(getters.getParticipatingUnreadCount(state)).toBe(4);
- expect(getters.getUnattendedUnreadCount(state)).toBe(5);
- expect(getters.getFolderUnreadCount(state)(7)).toBe(8);
- expect(getters.getFolderUnreadCount(state)('7')).toBe(8);
- expect(getters.getFolderUnreadCount(state)(8)).toBe(0);
});
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js
index aa36ed904..8941d7430 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js
@@ -21,13 +21,6 @@ describe('#mutations', () => {
6: '7',
7: 0,
},
- mentions_count: '8',
- participating_count: 9,
- unattended_count: '10',
- folders: {
- 11: '12',
- 12: 0,
- },
});
expect(state).toEqual({
@@ -35,10 +28,6 @@ describe('#mutations', () => {
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
- mentionsCount: 8,
- participatingCount: 9,
- unattendedCount: 10,
- folders: { 11: 12 },
});
});
@@ -48,10 +37,6 @@ describe('#mutations', () => {
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
- mentionsCount: 8,
- participatingCount: 9,
- unattendedCount: 10,
- folders: { 10: 11 },
};
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {});
@@ -61,10 +46,6 @@ describe('#mutations', () => {
inboxes: {},
labels: {},
teams: {},
- mentionsCount: 0,
- participatingCount: 0,
- unattendedCount: 0,
- folders: {},
});
});
diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb
index 12b0a9017..3bc221504 100644
--- a/app/listeners/action_cable_listener.rb
+++ b/app/listeners/action_cable_listener.rb
@@ -91,10 +91,10 @@ class ActionCableListener < BaseListener
end
def conversation_unread_count_changed(event)
- account, inbox_members, include_admins = ::Conversations::UnreadCounts::BroadcastScope.new(event).perform
+ account, inbox_members = ::Conversations::UnreadCounts::BroadcastScope.new(event).perform
return if account.blank? || !account.feature_enabled?('conversation_unread_counts')
- tokens = include_admins ? user_tokens(account, inbox_members) : inbox_members.pluck(:pubsub_token)
+ tokens = user_tokens(account, inbox_members)
broadcast(account, tokens, CONVERSATION_UNREAD_COUNT_CHANGED, {})
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 98da91b67..667058a2f 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -181,7 +181,7 @@ class Account < ApplicationRecord
end
def clear_unread_conversation_counts_cache
- ::Conversations::UnreadCounts::Store.clear_all_account!(id)
+ ::Conversations::UnreadCounts::Store.clear_account!(id)
end
trigger.after(:insert).for_each(:row) do
diff --git a/app/models/account_user.rb b/app/models/account_user.rb
index 4f135f508..bbcb0e010 100644
--- a/app/models/account_user.rb
+++ b/app/models/account_user.rb
@@ -39,7 +39,6 @@ class AccountUser < ApplicationRecord
after_create_commit :notify_creation, :create_notification_setting
after_destroy :notify_deletion, :remove_user_from_account
after_save :update_presence_in_redis, if: :saved_change_to_availability?
- after_commit :notify_unread_filter_counts_changed, on: [:update, :destroy], if: :unread_filter_access_changed?
validates :user_id, uniqueness: { scope: :account_id }
@@ -80,14 +79,6 @@ class AccountUser < ApplicationRecord
def update_presence_in_redis
OnlineStatusTracker.set_status(account.id, user.id, availability)
end
-
- def unread_filter_access_changed?
- destroyed? || previous_changes.key?('role') || previous_changes.key?('custom_role_id')
- end
-
- def notify_unread_filter_counts_changed
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: user).perform
- end
end
AccountUser.prepend_mod_with('AccountUser')
diff --git a/app/models/concerns/cache_keys.rb b/app/models/concerns/cache_keys.rb
index 3ad9bbadc..b37d7faa6 100644
--- a/app/models/concerns/cache_keys.rb
+++ b/app/models/concerns/cache_keys.rb
@@ -30,6 +30,7 @@ module CacheKeys
update_cache_key_for_account(id, model.name.underscore)
end
+ ::Conversations::UnreadCounts::Store.clear_account!(id)
dispatch_cache_update_event
end
diff --git a/app/models/conversation_participant.rb b/app/models/conversation_participant.rb
index d45719378..830eb7baa 100644
--- a/app/models/conversation_participant.rb
+++ b/app/models/conversation_participant.rb
@@ -28,7 +28,6 @@ class ConversationParticipant < ApplicationRecord
belongs_to :user
before_validation :ensure_account_id
- after_commit :notify_unread_filter_counts_changed, on: [:create, :destroy]
private
@@ -39,8 +38,4 @@ class ConversationParticipant < ApplicationRecord
def ensure_inbox_access
errors.add(:user, 'must have inbox access') if conversation && conversation.inbox.assignable_agents.exclude?(user)
end
-
- def notify_unread_filter_counts_changed
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: user).perform
- end
end
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
index 9234f9600..6d64c0447 100644
--- a/app/models/custom_filter.rb
+++ b/app/models/custom_filter.rb
@@ -22,19 +22,10 @@ class CustomFilter < ApplicationRecord
enum filter_type: { conversation: 0, contact: 1, report: 2 }
validate :validate_number_of_filters
- after_commit :notify_unread_filter_counts_changed, on: [:create, :update, :destroy]
def validate_number_of_filters
return true if account.custom_filters.where(user_id: user_id).size < Limits::MAX_CUSTOM_FILTERS_PER_USER
errors.add :account_id, I18n.t('errors.custom_filters.number_of_records')
end
-
- private
-
- def notify_unread_filter_counts_changed
- return unless conversation?
-
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: user).perform
- end
end
diff --git a/app/models/inbox_member.rb b/app/models/inbox_member.rb
index d0f553a98..d4a36ccc8 100644
--- a/app/models/inbox_member.rb
+++ b/app/models/inbox_member.rb
@@ -23,9 +23,7 @@ class InboxMember < ApplicationRecord
belongs_to :inbox
after_create :add_agent_to_round_robin
- before_destroy :cache_unread_filter_notification_context
after_destroy :remove_agent_from_round_robin
- after_commit :notify_unread_filter_counts_changed, on: [:create, :destroy]
private
@@ -36,16 +34,6 @@ class InboxMember < ApplicationRecord
def remove_agent_from_round_robin
::AutoAssignment::InboxRoundRobinService.new(inbox: inbox).remove_agent_from_queue(user_id) if inbox.present?
end
-
- def cache_unread_filter_notification_context
- @unread_filter_account = inbox&.account
- @unread_filter_user = user
- end
-
- def notify_unread_filter_counts_changed
- account = @unread_filter_account || inbox&.account
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: @unread_filter_user || user).perform
- end
end
InboxMember.include_mod_with('Audit::InboxMember')
diff --git a/app/models/mention.rb b/app/models/mention.rb
index 5d3539078..0e0fe762a 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -32,7 +32,6 @@ class Mention < ApplicationRecord
belongs_to :user
after_commit :notify_mentioned_user
- after_commit :notify_unread_filter_counts_changed, on: [:create, :destroy]
scope :latest, -> { order(mentioned_at: :desc) }
@@ -56,8 +55,4 @@ class Mention < ApplicationRecord
def notify_mentioned_user
Rails.configuration.dispatcher.dispatch(CONVERSATION_MENTIONED, Time.zone.now, user: user, conversation: conversation)
end
-
- def notify_unread_filter_counts_changed
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: user).perform
- end
end
diff --git a/app/services/conversations/filter_service.rb b/app/services/conversations/filter_service.rb
index 79527ac2f..db2892c31 100644
--- a/app/services/conversations/filter_service.rb
+++ b/app/services/conversations/filter_service.rb
@@ -7,7 +7,8 @@ class Conversations::FilterService < FilterService
end
def perform
- @conversations = filtered_relation
+ validate_query_operator
+ @conversations = query_builder(@filters['conversations'])
mine_count, unassigned_count, all_count, = set_count_for_all_conversations
assigned_count = all_count - unassigned_count
@@ -22,13 +23,6 @@ class Conversations::FilterService < FilterService
}
end
- def filtered_relation
- validate_query_operator
- return base_relation if @params[:payload].blank?
-
- query_builder(@filters['conversations'])
- end
-
def base_relation
conversations = @account.conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
diff --git a/app/services/conversations/unread_counts/broadcast_scope.rb b/app/services/conversations/unread_counts/broadcast_scope.rb
index ea43c1e8e..8e47050e3 100644
--- a/app/services/conversations/unread_counts/broadcast_scope.rb
+++ b/app/services/conversations/unread_counts/broadcast_scope.rb
@@ -6,23 +6,13 @@ class Conversations::UnreadCounts::BroadcastScope
end
def perform
- return user_scope if user.present?
- return [conversation.account, conversation.inbox.members, true] if conversation.present?
+ return [conversation.account, conversation.inbox.members] if conversation.present?
deleted_conversation_scope
end
private
- def user
- event.data[:user]
- end
-
- def user_scope
- account = event.data[:account] || user.account
- [account, [user], false]
- end
-
def conversation
event.data[:conversation]
end
@@ -34,7 +24,7 @@ class Conversations::UnreadCounts::BroadcastScope
account = Account.find_by(id: conversation_data[:account_id])
return if account.blank?
- [account, inbox_members_for(account, conversation_data[:inbox_id]), true]
+ [account, inbox_members_for(account, conversation_data[:inbox_id])]
end
def inbox_members_for(account, inbox_id)
diff --git a/app/services/conversations/unread_counts/build_lock_keys.rb b/app/services/conversations/unread_counts/build_lock_keys.rb
deleted file mode 100644
index 06b922c88..000000000
--- a/app/services/conversations/unread_counts/build_lock_keys.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Conversations::UnreadCounts::BuildLockKeys
- private
-
- def base_build_lock_key
- format(Redis::Alfred::UNREAD_CONVERSATIONS_BASE_BUILD_LOCK, account_id: account.id)
- end
-
- def assignment_build_lock_key
- format(Redis::Alfred::UNREAD_CONVERSATIONS_ASSIGNMENT_BUILD_LOCK, account_id: account.id)
- end
-
- def filters_build_lock_key
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FILTERS_BUILD_LOCK, account_id: account.id, user_id: user.id)
- end
-end
diff --git a/app/services/conversations/unread_counts/builder.rb b/app/services/conversations/unread_counts/builder.rb
index e35d14b7c..54e466dbd 100644
--- a/app/services/conversations/unread_counts/builder.rb
+++ b/app/services/conversations/unread_counts/builder.rb
@@ -1,14 +1,5 @@
class Conversations::UnreadCounts::Builder
- PARTICIPATING_PERMISSION = 'conversation_participating_manage'.freeze
- RELATIVE_DATE_FILTER_OPERATOR = 'days_before'.freeze
BATCH_SIZE = 1000
- FILTER_ERRORS = [
- ActiveRecord::StatementInvalid,
- CustomExceptions::CustomFilter::InvalidAttribute,
- CustomExceptions::CustomFilter::InvalidOperator,
- CustomExceptions::CustomFilter::InvalidQueryOperator,
- CustomExceptions::CustomFilter::InvalidValue
- ].freeze
attr_reader :account
@@ -33,37 +24,10 @@ class Conversations::UnreadCounts::Builder
build_assignment!
end
- def build_filters_for!(user)
- store.clear_user_filters!(account.id, user.id)
- version_snapshot = store.filter_version_snapshot(account.id, user.id)
- custom_filters = conversation_custom_filters(user).to_a
-
- store.add_filter_memberships(
- account_id: account.id,
- user_id: user.id,
- filters: {
- mentions: mentioned_unread_conversation_ids(user),
- participating: participating_unread_conversation_ids(user),
- unattended: unattended_unread_conversation_ids(user)
- },
- folders: folder_unread_conversation_ids(custom_filters, user)
- )
- mark_filters_ready_if_current(user, custom_filters, version_snapshot)
- end
-
private
- def mark_filters_ready_if_current(user, custom_filters, version_snapshot)
- store.mark_filters_ready_if_current!(
- account.id,
- user.id,
- version_snapshot: version_snapshot,
- expires_in: filters_ready_ttl(custom_filters)
- )
- end
-
def write_memberships(assignment:)
- unread_conversations(open_only: true).in_batches(of: BATCH_SIZE) do |relation|
+ unread_conversations.in_batches(of: BATCH_SIZE) do |relation|
columns = %i[id inbox_id assignee_id cached_label_list team_id]
memberships = relation.pluck(*columns).map do |id, inbox_id, assignee_id, cached_label_list, team_id|
{
@@ -79,99 +43,14 @@ class Conversations::UnreadCounts::Builder
end
end
- def mentioned_unread_conversation_ids(user)
- visible_unread_conversations(user, open_only: true)
- .joins(:mentions)
- .where(mentions: { account_id: account.id, user_id: user.id })
- .pluck(:id)
- end
-
- def participating_unread_conversation_ids(user)
- participating_visible_unread_conversations(user, open_only: true)
- .where(id: user.participating_conversations.where(account_id: account.id).select(:id))
- .pluck(:id)
- end
-
- def unattended_unread_conversation_ids(user)
- visible_unread_conversations(user, open_only: true)
- .unattended
- .pluck(:id)
- end
-
- def folder_unread_conversation_ids(custom_filters, user)
- custom_filters.each_with_object({}) do |custom_filter, result|
- result[custom_filter.id] = unread_ids_for_filter(custom_filter, user)
- rescue *FILTER_ERRORS
- next
- end
- end
-
- def conversation_custom_filters(user)
- account.custom_filters.where(user: user, filter_type: :conversation)
- end
-
- def filters_ready_ttl(custom_filters)
- return Conversations::UnreadCounts::READY_TTL unless relative_date_filter?(custom_filters)
-
- seconds_until_next_day
- end
-
- def relative_date_filter?(custom_filters)
- custom_filters.any? do |custom_filter|
- Array(custom_filter.query.with_indifferent_access[:payload]).any? do |condition|
- condition[:filter_operator] == RELATIVE_DATE_FILTER_OPERATOR
- end
- end
- end
-
- def seconds_until_next_day
- [(Time.zone.tomorrow.beginning_of_day - Time.current).ceil, 1].max
- end
-
- def unread_ids_for_filter(custom_filter, user)
- filter_relation = ::Conversations::FilterService.new(custom_filter.query.with_indifferent_access, user, account).filtered_relation
- filter_relation
- .where(id: unread_conversations(open_only: false).select(:id))
- .reorder(nil)
- .distinct
- .pluck(:id)
- end
-
- def unread_conversations(open_only:)
- conversations = account.conversations
- conversations = conversations.open if open_only
-
- conversations.joins(:messages)
- .merge(Message.incoming.reorder(nil))
- .where(messages: { account_id: account.id })
- .where(unread_since_last_seen_condition)
- .distinct
- end
-
- def visible_unread_conversations(user, open_only:)
- ::Conversations::PermissionFilterService.new(unread_conversations(open_only: open_only), user, account).perform
- end
-
- def participating_visible_unread_conversations(user, open_only:)
- return inbox_visible_unread_conversations(user, open_only: open_only) if custom_role_participating_permission?(user)
-
- visible_unread_conversations(user, open_only: open_only)
- end
-
- def inbox_visible_unread_conversations(user, open_only:)
- conversations = unread_conversations(open_only: open_only)
- return conversations if account_user_for(user)&.administrator?
-
- conversations.where(inbox: user.inboxes.where(account_id: account.id))
- end
-
- def custom_role_participating_permission?(user)
- account_user = account_user_for(user)
- account_user&.agent? && account_user.custom_role_id.present? && account_user.permissions.include?(PARTICIPATING_PERMISSION)
- end
-
- def account_user_for(user)
- account.account_users.find_by(user_id: user.id)
+ def unread_conversations
+ account.conversations
+ .open
+ .joins(:messages)
+ .merge(Message.incoming.reorder(nil))
+ .where(messages: { account_id: account.id })
+ .where(unread_since_last_seen_condition)
+ .distinct
end
def unread_since_last_seen_condition
diff --git a/app/services/conversations/unread_counts/counter.rb b/app/services/conversations/unread_counts/counter.rb
index 08a6c1072..b1ba5ddb0 100644
--- a/app/services/conversations/unread_counts/counter.rb
+++ b/app/services/conversations/unread_counts/counter.rb
@@ -1,7 +1,4 @@
class Conversations::UnreadCounts::Counter
- include ::Conversations::UnreadCounts::BuildLockKeys
- include ::Conversations::UnreadCounts::FilterCounter
-
MANAGE_ALL_PERMISSION = 'conversation_manage'.freeze
UNASSIGNED_PERMISSION = 'conversation_unassigned_manage'.freeze
PARTICIPATING_PERMISSION = 'conversation_participating_manage'.freeze
@@ -21,38 +18,31 @@ class Conversations::UnreadCounts::Counter
ensure_base_cache!
ensure_assignment_cache! if assignment_mode?
- ensure_filters_cache!
inbox_counts = unread_inbox_counts
- filter_counts = unread_filter_counts
{
all_count: inbox_counts.values.sum,
inboxes: inbox_counts,
labels: unread_label_counts,
- teams: unread_team_counts,
- mentions_count: filter_counts[:mentions_count],
- participating_count: filter_counts[:participating_count],
- unattended_count: filter_counts[:unattended_count],
- folders: filter_counts[:folders]
+ teams: unread_team_counts
}
end
private
def ensure_base_cache!
- ensure_cache_ready!(ready: -> { store.base_ready?(account.id) }, lock_key: base_build_lock_key) { builder.build_base! }
+ ensure_cache_ready!(
+ ready: -> { store.base_ready?(account.id) },
+ lock_key: base_build_lock_key
+ ) { ::Conversations::UnreadCounts::Builder.new(account).build_base! }
end
def ensure_assignment_cache!
- ensure_cache_ready!(ready: -> { store.assignment_ready?(account.id) }, lock_key: assignment_build_lock_key) { builder.build_assignment! }
- end
-
- def ensure_filters_cache!
ensure_cache_ready!(
- ready: -> { store.filters_ready?(account.id, user.id) },
- lock_key: filters_build_lock_key
- ) { builder.build_filters_for!(user) }
+ ready: -> { store.assignment_ready?(account.id) },
+ lock_key: assignment_build_lock_key
+ ) { ::Conversations::UnreadCounts::Builder.new(account).build_assignment! }
end
def ensure_cache_ready!(ready:, lock_key:)
@@ -61,10 +51,9 @@ class Conversations::UnreadCounts::Counter
loop do
return if ready.call
- lock_acquired = lock_manager.with_lock(lock_key, BUILD_LOCK_TTL) { yield unless ready.call }
- return if ready.call
+ return if lock_manager.with_lock(lock_key, BUILD_LOCK_TTL) { yield unless ready.call }
- wait_for_cache_ready(ready) unless lock_acquired
+ wait_for_cache_ready(ready)
end
end
@@ -73,6 +62,14 @@ class Conversations::UnreadCounts::Counter
sleep BUILD_WAIT_INTERVAL until ready.call || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
end
+ def base_build_lock_key
+ format(Redis::Alfred::UNREAD_CONVERSATIONS_BASE_BUILD_LOCK, account_id: account.id)
+ end
+
+ def assignment_build_lock_key
+ format(Redis::Alfred::UNREAD_CONVERSATIONS_ASSIGNMENT_BUILD_LOCK, account_id: account.id)
+ end
+
def unread_inbox_counts
counts_for_grouped_keys(visible_inbox_ids.index_with { |inbox_id| inbox_keys_for_mode(inbox_id) })
end
@@ -197,14 +194,10 @@ class Conversations::UnreadCounts::Counter
end
def empty_counts
- { all_count: 0, inboxes: {}, labels: {}, teams: {}, mentions_count: 0, participating_count: 0, unattended_count: 0, folders: {} }
+ { all_count: 0, inboxes: {}, labels: {}, teams: {} }
end
def store
::Conversations::UnreadCounts::Store
end
-
- def builder
- @builder ||= ::Conversations::UnreadCounts::Builder.new(account)
- end
end
diff --git a/app/services/conversations/unread_counts/filter_counter.rb b/app/services/conversations/unread_counts/filter_counter.rb
deleted file mode 100644
index 79f376d87..000000000
--- a/app/services/conversations/unread_counts/filter_counter.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module Conversations::UnreadCounts::FilterCounter
- private
-
- def unread_filter_counts
- keys = user_filter_keys
- counts_by_key = store.counts_for_keys(keys.values + folder_keys.values)
-
- {
- mentions_count: counts_by_key[keys[:mentions]].to_i,
- participating_count: counts_by_key[keys[:participating]].to_i,
- unattended_count: counts_by_key[keys[:unattended]].to_i,
- folders: folder_counts(counts_by_key)
- }
- end
-
- def user_filter_keys
- {
- mentions: store.user_mentions_key(account.id, user.id),
- participating: store.user_participating_key(account.id, user.id),
- unattended: store.user_unattended_key(account.id, user.id)
- }
- end
-
- def conversation_folder_ids
- @conversation_folder_ids ||= account.custom_filters.where(user: user, filter_type: :conversation).pluck(:id)
- end
-
- def folder_keys
- @folder_keys ||= conversation_folder_ids.index_with do |custom_filter_id|
- store.user_folder_key(account.id, user.id, custom_filter_id)
- end
- end
-
- def folder_counts(counts_by_key)
- folder_keys.each_with_object({}) do |(custom_filter_id, key), result|
- count = counts_by_key[key].to_i
- result[custom_filter_id.to_s] = count if count.positive?
- end
- end
-end
diff --git a/app/services/conversations/unread_counts/listener.rb b/app/services/conversations/unread_counts/listener.rb
index 28bbf2ac3..28792d884 100644
--- a/app/services/conversations/unread_counts/listener.rb
+++ b/app/services/conversations/unread_counts/listener.rb
@@ -3,13 +3,10 @@ class Conversations::UnreadCounts::Listener < BaseListener
def message_created(event)
message, = extract_message_and_account(event)
+ return unless message.incoming?
return unless message.account.feature_enabled?('conversation_unread_counts')
- if message.incoming?
- refresh(message.conversation)
- else
- notify_filter_counts_changed(message.conversation)
- end
+ refresh(message.conversation)
end
def conversation_status_changed(event)
@@ -18,21 +15,10 @@ class Conversations::UnreadCounts::Listener < BaseListener
end
def conversation_updated(event)
+ return unless label_changed?(event.data[:changed_attributes])
+
conversation, = extract_conversation_and_account(event)
- return unless conversation.account.feature_enabled?('conversation_unread_counts')
-
- if label_changed?(event.data[:changed_attributes])
- refresh(conversation, event.data[:changed_attributes])
- elsif folder_filter_changed?(event.data[:changed_attributes])
- notify_filter_counts_changed(conversation)
- end
- end
-
- def conversation_contact_changed(event)
- conversation, = extract_conversation_and_account(event)
- return unless conversation.account.feature_enabled?('conversation_unread_counts')
-
- notify_filter_counts_changed(conversation)
+ refresh(conversation, event.data[:changed_attributes])
end
def assignee_changed(event)
@@ -51,10 +37,7 @@ class Conversations::UnreadCounts::Listener < BaseListener
account = Account.find_by(id: conversation_data[:account_id])
return unless account&.feature_enabled?('conversation_unread_counts')
-
- filters_cleared = store.clear_filter_caches!(account.id)
- memberships_removed = remove_deleted_conversation(account, conversation_data)
- return unless memberships_removed || filters_cleared
+ return unless remove_deleted_conversation(account, conversation_data)
Rails.configuration.dispatcher.dispatch(CONVERSATION_UNREAD_COUNT_CHANGED, Time.zone.now, conversation_data: conversation_data.to_h)
end
@@ -107,16 +90,6 @@ class Conversations::UnreadCounts::Listener < BaseListener
changed_attributes.key?('cached_label_list') || changed_attributes.key?(:cached_label_list)
end
- def folder_filter_changed?(changed_attributes)
- changed_attributes.present?
- end
-
- def notify_filter_counts_changed(conversation)
- return unless store.clear_filter_caches!(conversation.account_id)
-
- Rails.configuration.dispatcher.dispatch(CONVERSATION_UNREAD_COUNT_CHANGED, Time.zone.now, conversation: conversation)
- end
-
def store
::Conversations::UnreadCounts::Store
end
diff --git a/app/services/conversations/unread_counts/notifier.rb b/app/services/conversations/unread_counts/notifier.rb
index aac4af7c0..652fbde3a 100644
--- a/app/services/conversations/unread_counts/notifier.rb
+++ b/app/services/conversations/unread_counts/notifier.rb
@@ -11,9 +11,7 @@ class Conversations::UnreadCounts::Notifier
def perform
return false unless conversation.account.feature_enabled?('conversation_unread_counts')
- filters_cleared = ::Conversations::UnreadCounts::Store.clear_filter_caches!(conversation.account_id)
- memberships_refreshed = ::Conversations::UnreadCounts::Refresher.new(conversation, changed_attributes: changed_attributes).perform
- return false unless memberships_refreshed || filters_cleared
+ return false unless ::Conversations::UnreadCounts::Refresher.new(conversation, changed_attributes: changed_attributes).perform
Rails.configuration.dispatcher.dispatch(CONVERSATION_UNREAD_COUNT_CHANGED, Time.zone.now, conversation: conversation)
true
diff --git a/app/services/conversations/unread_counts/store.rb b/app/services/conversations/unread_counts/store.rb
index 6aa9cef05..e51fc5c09 100644
--- a/app/services/conversations/unread_counts/store.rb
+++ b/app/services/conversations/unread_counts/store.rb
@@ -1,6 +1,5 @@
class Conversations::UnreadCounts::Store
extend ::Conversations::UnreadCounts::StoreKeys
- extend ::Conversations::UnreadCounts::UserFilterStore
class << self
def base_ready?(account_id)
@@ -20,10 +19,6 @@ class Conversations::UnreadCounts::Store
end
def clear_account!(account_id)
- account_key_patterns(account_id).each { |pattern| delete_matching(pattern) }
- end
-
- def clear_all_account!(account_id)
delete_matching("#{account_prefix(account_id)}::*")
end
@@ -183,11 +178,9 @@ class Conversations::UnreadCounts::Store
end
def delete_matching(pattern)
- deleted = 0
Redis::Alfred.scan_each(match: pattern, count: 1000) do |key|
- deleted += 1 if Redis::Alfred.delete(key)
+ Redis::Alfred.delete(key)
end
- deleted.positive?
end
def assignment_key_patterns(account_id)
@@ -202,16 +195,5 @@ class Conversations::UnreadCounts::Store
"#{prefix}::TEAM::*::INBOX::*::ASSIGNEE::*"
]
end
-
- def account_key_patterns(account_id)
- prefix = account_prefix(account_id)
- [
- base_ready_key(account_id),
- assignment_ready_key(account_id),
- "#{prefix}::INBOX::*",
- "#{prefix}::LABEL::*::INBOX::*",
- "#{prefix}::TEAM::*::INBOX::*"
- ]
- end
end
end
diff --git a/app/services/conversations/unread_counts/store_keys.rb b/app/services/conversations/unread_counts/store_keys.rb
index a5b8c43ec..bbb6d101a 100644
--- a/app/services/conversations/unread_counts/store_keys.rb
+++ b/app/services/conversations/unread_counts/store_keys.rb
@@ -40,20 +40,4 @@ module Conversations::UnreadCounts::StoreKeys
def team_inbox_assignee_key(account_id, team_id, inbox_id, user_id)
format(Redis::Alfred::UNREAD_CONVERSATIONS_TEAM_INBOX_ASSIGNEE, account_id: account_id, team_id: team_id, inbox_id: inbox_id, user_id: user_id)
end
-
- def user_mentions_key(account_id, user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_MENTIONS, account_id: account_id, user_id: user_id)
- end
-
- def user_participating_key(account_id, user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_PARTICIPATING, account_id: account_id, user_id: user_id)
- end
-
- def user_unattended_key(account_id, user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_UNATTENDED, account_id: account_id, user_id: user_id)
- end
-
- def user_folder_key(account_id, user_id, custom_filter_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FOLDER, account_id: account_id, user_id: user_id, custom_filter_id: custom_filter_id)
- end
end
diff --git a/app/services/conversations/unread_counts/user_filter_notifier.rb b/app/services/conversations/unread_counts/user_filter_notifier.rb
deleted file mode 100644
index 94dd95a0f..000000000
--- a/app/services/conversations/unread_counts/user_filter_notifier.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Conversations::UnreadCounts::UserFilterNotifier
- include Events::Types
-
- attr_reader :account, :user
-
- def initialize(account:, user:)
- @account = account
- @user = user
- end
-
- def perform
- return false if account.blank? || user.blank?
- return false unless account.feature_enabled?('conversation_unread_counts')
-
- ::Conversations::UnreadCounts::Store.clear_user_filters!(account.id, user.id)
- Rails.configuration.dispatcher.dispatch(CONVERSATION_UNREAD_COUNT_CHANGED, Time.zone.now, account: account, user: user)
- true
- end
-end
diff --git a/app/services/conversations/unread_counts/user_filter_store.rb b/app/services/conversations/unread_counts/user_filter_store.rb
deleted file mode 100644
index 3533acb56..000000000
--- a/app/services/conversations/unread_counts/user_filter_store.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-module Conversations::UnreadCounts::UserFilterStore
- USER_FILTER_KEY_SUFFIXES = [
- 'READY::FILTERS',
- 'MENTIONS',
- 'PARTICIPATING',
- 'UNATTENDED',
- 'FOLDER::*'
- ].freeze
-
- def filters_ready?(account_id, user_id)
- Redis::Alfred.exists?(filters_ready_key(account_id, user_id))
- end
-
- def mark_filters_ready!(account_id, user_id, expires_in: Conversations::UnreadCounts::READY_TTL)
- Redis::Alfred.set(filters_ready_key(account_id, user_id), Time.current.to_i, ex: expires_in)
- end
-
- def mark_filters_ready_if_current!(account_id, user_id, version_snapshot:, expires_in: Conversations::UnreadCounts::READY_TTL)
- return false unless filter_version_snapshot(account_id, user_id) == version_snapshot
-
- mark_filters_ready!(account_id, user_id, expires_in: expires_in)
- end
-
- def filter_version_snapshot(account_id, user_id)
- {
- account: filter_version(account_filter_version_key(account_id)),
- user: filter_version(user_filter_version_key(account_id, user_id))
- }
- end
-
- def clear_filter_caches!(account_id)
- bump_filter_version(account_filter_version_key(account_id))
- delete_user_filter_patterns("#{account_prefix(account_id)}::USER::*")
- end
-
- def clear_user_filters!(account_id, user_id)
- bump_filter_version(user_filter_version_key(account_id, user_id))
- delete_user_filter_patterns(user_filter_prefix(account_id, user_id))
- end
-
- def add_filter_memberships(account_id:, user_id:, filters:, folders:)
- memberships = {
- user_mentions_key(account_id, user_id) => filters[:mentions],
- user_participating_key(account_id, user_id) => filters[:participating],
- user_unattended_key(account_id, user_id) => filters[:unattended]
- }
- folders.each do |custom_filter_id, conversation_ids|
- memberships[user_folder_key(account_id, user_id, custom_filter_id)] = conversation_ids
- end
-
- write_membership_sets(memberships)
- end
-
- private
-
- def filters_ready_key(account_id, user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FILTERS_READY, account_id: account_id, user_id: user_id)
- end
-
- def account_filter_version_key(account_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_FILTERS_VERSION, account_id: account_id)
- end
-
- def user_filter_version_key(account_id, user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FILTERS_VERSION, account_id: account_id, user_id: user_id)
- end
-
- def user_filter_prefix(account_id, user_id)
- "#{account_prefix(account_id)}::USER::#{user_id}"
- end
-
- def filter_version(key)
- Redis::Alfred.get(key).to_i
- end
-
- def bump_filter_version(key)
- Redis::Alfred.incr(key).tap { Redis::Alfred.expire(key, Conversations::UnreadCounts::SET_TTL) }
- end
-
- def delete_user_filter_patterns(prefix)
- deleted = false
- USER_FILTER_KEY_SUFFIXES.each do |suffix|
- deleted = delete_matching("#{prefix}::#{suffix}") || deleted
- end
- deleted
- end
-
- def write_membership_sets(memberships)
- memberships = memberships.transform_values { |conversation_ids| Array(conversation_ids).compact_blank }
- memberships = memberships.select { |_key, conversation_ids| conversation_ids.present? }
- return if memberships.blank?
-
- Redis::Alfred.pipelined do |pipeline|
- memberships.each do |key, conversation_ids|
- conversation_ids.each { |conversation_id| pipeline.sadd(key, conversation_id) }
- pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
- end
- end
- end
-end
diff --git a/app/services/filter_service.rb b/app/services/filter_service.rb
index 33ca04680..25f118d48 100644
--- a/app/services/filter_service.rb
+++ b/app/services/filter_service.rb
@@ -11,7 +11,7 @@ class FilterService
}.with_indifferent_access
def initialize(params, user)
- @params = normalize_params(params)
+ @params = params
@user = user
file = File.read('./lib/filters/filter_keys.yml')
@filters = YAML.safe_load(file)
@@ -140,16 +140,6 @@ class FilterService
private
- def normalize_params(params)
- return params unless params.respond_to?(:with_indifferent_access)
-
- normalized_params = params.with_indifferent_access
- normalized_params[:payload] = Array(normalized_params[:payload]).map do |condition|
- condition.respond_to?(:with_indifferent_access) ? condition.with_indifferent_access : condition
- end
- normalized_params
- end
-
def standard_attribute_data_type(attribute_key)
@filters.each_value do |section|
return section.dig(attribute_key, 'data_type') if section.is_a?(Hash) && section.key?(attribute_key)
diff --git a/config/app.yml b/config/app.yml
index c2c192018..a55b621f6 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '4.15.0'
+ version: '4.15.1'
development:
<<: *shared
diff --git a/enterprise/app/finders/enterprise/conversation_finder.rb b/enterprise/app/finders/enterprise/conversation_finder.rb
index f1f2e5514..24e6b493d 100644
--- a/enterprise/app/finders/enterprise/conversation_finder.rb
+++ b/enterprise/app/finders/enterprise/conversation_finder.rb
@@ -1,31 +1,5 @@
module Enterprise::ConversationFinder
- def filter_by_conversation_type
- return super unless params[:conversation_type] == 'participating' && custom_role_participating_permission?
-
- @conversations = participating_visible_conversations.where(
- id: current_user.participating_conversations.where(account_id: current_account.id).select(:id)
- )
- end
-
def conversations_base_query
current_account.feature_enabled?('sla') ? super.includes(:applied_sla, :sla_events) : super
end
-
- private
-
- def participating_visible_conversations
- conversations = current_account.conversations
- conversations = conversations.where(inbox_id: @inbox_ids) if params[:inbox_id]
- return conversations if account_user&.administrator?
-
- conversations.where(inbox: current_user.inboxes.where(account_id: current_account.id))
- end
-
- def custom_role_participating_permission?
- account_user&.agent? && account_user.custom_role_id.present? && account_user.permissions.include?('conversation_participating_manage')
- end
-
- def account_user
- @account_user ||= current_account.account_users.find_by(user_id: current_user.id)
- end
end
diff --git a/enterprise/app/models/custom_role.rb b/enterprise/app/models/custom_role.rb
index 4085000b1..666f91378 100644
--- a/enterprise/app/models/custom_role.rb
+++ b/enterprise/app/models/custom_role.rb
@@ -28,9 +28,6 @@ class CustomRole < ApplicationRecord
belongs_to :account
has_many :account_users, dependent: :nullify
- before_destroy :cache_users_for_unread_filter_notification, prepend: true
- after_commit :notify_unread_filter_counts_changed, on: [:update, :destroy], if: :unread_filter_access_changed?
-
PERMISSIONS = %w[
conversation_manage
conversation_unassigned_manage
@@ -42,24 +39,4 @@ class CustomRole < ApplicationRecord
validates :name, presence: true
validates :permissions, inclusion: { in: PERMISSIONS }
-
- private
-
- def unread_filter_access_changed?
- destroyed? || previous_changes.key?('permissions')
- end
-
- def cache_users_for_unread_filter_notification
- @users_for_unread_filter_notification = account_users.includes(:user).map(&:user)
- end
-
- def users_for_unread_filter_notification
- @users_for_unread_filter_notification || account_users.includes(:user).map(&:user)
- end
-
- def notify_unread_filter_counts_changed
- users_for_unread_filter_notification.each do |user|
- ::Conversations::UnreadCounts::UserFilterNotifier.new(account: account, user: user).perform
- end
- end
end
diff --git a/lib/redis/redis_keys.rb b/lib/redis/redis_keys.rb
index 381cafb98..bc7fb551e 100644
--- a/lib/redis/redis_keys.rb
+++ b/lib/redis/redis_keys.rb
@@ -14,15 +14,8 @@ module Redis::RedisKeys
UNREAD_CONVERSATIONS_ACCOUNT_PREFIX = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d'.freeze
UNREAD_CONVERSATIONS_BASE_READY = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::READY::BASE'.freeze
UNREAD_CONVERSATIONS_ASSIGNMENT_READY = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::READY::ASSIGNMENT'.freeze
- UNREAD_CONVERSATIONS_USER_FILTERS_READY =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::READY::FILTERS'.freeze
- UNREAD_CONVERSATIONS_FILTERS_VERSION = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::VERSION::FILTERS'.freeze
- UNREAD_CONVERSATIONS_USER_FILTERS_VERSION =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::VERSION::FILTERS'.freeze
UNREAD_CONVERSATIONS_BASE_BUILD_LOCK = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::BUILD_LOCK::BASE'.freeze
UNREAD_CONVERSATIONS_ASSIGNMENT_BUILD_LOCK = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::BUILD_LOCK::ASSIGNMENT'.freeze
- UNREAD_CONVERSATIONS_USER_FILTERS_BUILD_LOCK =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::BUILD_LOCK::FILTERS'.freeze
UNREAD_CONVERSATIONS_INBOX = 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::INBOX::%d'.freeze
UNREAD_CONVERSATIONS_LABEL_INBOX =
'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::LABEL::%d::INBOX::%d'.freeze
@@ -40,14 +33,6 @@ module Redis::RedisKeys
'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::TEAM::%d::INBOX::%d::UNASSIGNED'.freeze
UNREAD_CONVERSATIONS_TEAM_INBOX_ASSIGNEE =
'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::TEAM::%d::INBOX::%d::ASSIGNEE::%d'.freeze
- UNREAD_CONVERSATIONS_USER_MENTIONS =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::MENTIONS'.freeze
- UNREAD_CONVERSATIONS_USER_PARTICIPATING =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::PARTICIPATING'.freeze
- UNREAD_CONVERSATIONS_USER_UNATTENDED =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::UNATTENDED'.freeze
- UNREAD_CONVERSATIONS_USER_FOLDER =
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::%d::USER::%d::FOLDER::%d'.freeze
## User Keys
# SSO Auth Tokens
diff --git a/package.json b/package.json
index b8ab60839..65dd48d27 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "4.15.0",
+ "version": "4.15.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
index de3cc4a3c..bdb8117ac 100644
--- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe 'Conversations API', type: :request do
end
after do
- Conversations::UnreadCounts::Store.clear_all_account!(account.id)
+ Conversations::UnreadCounts::Store.clear_account!(account.id)
end
context 'when conversation unread counts feature is enabled' do
@@ -144,42 +144,7 @@ RSpec.describe 'Conversations API', type: :request do
'all_count' => 1,
'inboxes' => { visible_inbox.id.to_s => 1 },
'labels' => { label.id.to_s => 1 },
- 'teams' => {},
- 'mentions_count' => 0,
- 'participating_count' => 0,
- 'unattended_count' => 1,
- 'folders' => {}
- )
- end
-
- it 'returns unread counts for mentions, participating conversations, unattended conversations, and folders' do
- mentioned_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- participating_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- resolved_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- resolved_conversation.update!(status: :resolved)
- custom_filter = create(:custom_filter, account: account, user: agent, filter_type: :conversation, query: {
- payload: [{
- attribute_key: 'status',
- filter_operator: 'equal_to',
- values: ['resolved'],
- query_operator: nil,
- custom_attribute_type: ''
- }]
- })
-
- create(:mention, account: account, conversation: mentioned_conversation, user: agent)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: agent)
-
- get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
- headers: agent.create_new_auth_token,
- as: :json
-
- expect(response).to have_http_status(:success)
- expect(response.parsed_body['payload']).to include(
- 'mentions_count' => 1,
- 'participating_count' => 1,
- 'unattended_count' => 2,
- 'folders' => { custom_filter.id.to_s => 1 }
+ 'teams' => {}
)
end
@@ -897,7 +862,7 @@ RSpec.describe 'Conversations API', type: :request do
expect(response).to have_http_status(:success)
expect(Conversations::UnreadCounts::Store.counts_for_keys([inbox_key])).to eq(inbox_key => 0)
ensure
- Conversations::UnreadCounts::Store.clear_all_account!(account.id)
+ Conversations::UnreadCounts::Store.clear_account!(account.id)
end
it 'updates both if one timestamp is old even when the other is recent' do
@@ -984,7 +949,7 @@ RSpec.describe 'Conversations API', type: :request do
expect(response).to have_http_status(:success)
expect(Conversations::UnreadCounts::Store.counts_for_keys([inbox_key])).to eq(inbox_key => 1)
ensure
- Conversations::UnreadCounts::Store.clear_all_account!(account.id)
+ Conversations::UnreadCounts::Store.clear_account!(account.id)
end
end
end
diff --git a/spec/controllers/super_admin/accounts_controller_spec.rb b/spec/controllers/super_admin/accounts_controller_spec.rb
index 917499cd5..e4ff81a08 100644
--- a/spec/controllers/super_admin/accounts_controller_spec.rb
+++ b/spec/controllers/super_admin/accounts_controller_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Super Admin accounts API', type: :request do
end
after do
- Conversations::UnreadCounts::Store.clear_all_account!(account.id)
+ Conversations::UnreadCounts::Store.clear_account!(account.id)
end
context 'when it is an unauthenticated user' do
diff --git a/spec/enterprise/finders/conversation_finder_spec.rb b/spec/enterprise/finders/conversation_finder_spec.rb
deleted file mode 100644
index f8b9fce58..000000000
--- a/spec/enterprise/finders/conversation_finder_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe ConversationFinder do
- describe '#perform' do
- it 'returns participant-only conversations for custom roles with participating permission' do
- account = create(:account)
- agent = create(:user, account: account, role: :agent)
- other_agent = create(:user, account: account, role: :agent)
- inbox = create(:inbox, account: account, enable_auto_assignment: false)
- custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
- participating_conversation = create(:conversation, account: account, inbox: inbox, assignee: other_agent)
-
- create(:inbox_member, user: agent, inbox: inbox)
- create(:inbox_member, user: other_agent, inbox: inbox)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: agent)
- account.account_users.find_by!(user_id: agent.id).update!(custom_role: custom_role)
- Current.account = account
-
- result = described_class.new(agent, { status: 'open', conversation_type: 'participating' }).perform
-
- expect(result[:conversations].map(&:id)).to include(participating_conversation.id)
- end
- end
-end
diff --git a/spec/enterprise/models/account_user_spec.rb b/spec/enterprise/models/account_user_spec.rb
index e157d9acd..fb572e86a 100644
--- a/spec/enterprise/models/account_user_spec.rb
+++ b/spec/enterprise/models/account_user_spec.rb
@@ -29,24 +29,6 @@ RSpec.describe AccountUser, type: :model do
end
end
- describe 'unread filter count invalidation' do
- it 'notifies when the assigned custom role changes' do
- account = create(:account)
- custom_role = create(:custom_role, account: account)
- account_user = create(:account_user, account: account)
- notifier = instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true)
- allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
-
- account_user.update!(custom_role: custom_role)
-
- expect(Conversations::UnreadCounts::UserFilterNotifier).to have_received(:new).with(
- account: account,
- user: account_user.user
- )
- expect(notifier).to have_received(:perform)
- end
- end
-
describe 'audit log' do
context 'when account user is created' do
it 'has associated audit log created' do
diff --git a/spec/enterprise/models/custom_role_spec.rb b/spec/enterprise/models/custom_role_spec.rb
index 553a5a33a..f63f3c2dd 100644
--- a/spec/enterprise/models/custom_role_spec.rb
+++ b/spec/enterprise/models/custom_role_spec.rb
@@ -9,38 +9,4 @@ RSpec.describe CustomRole, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
end
-
- describe 'unread filter count invalidation' do
- it 'notifies assigned users when permissions change' do
- account = create(:account)
- custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
- account_user = create(:account_user, account: account, custom_role: custom_role)
- notifier = instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true)
- allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
-
- custom_role.update!(permissions: ['conversation_manage'])
-
- expect(Conversations::UnreadCounts::UserFilterNotifier).to have_received(:new).with(
- account: account,
- user: account_user.user
- )
- expect(notifier).to have_received(:perform)
- end
-
- it 'notifies assigned users when the custom role is destroyed' do
- account = create(:account)
- custom_role = create(:custom_role, account: account, permissions: ['conversation_participating_manage'])
- account_user = create(:account_user, account: account, custom_role: custom_role)
- notifier = instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true)
- allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
-
- custom_role.destroy!
-
- expect(Conversations::UnreadCounts::UserFilterNotifier).to have_received(:new).with(
- account: account,
- user: account_user.user
- )
- expect(notifier).to have_received(:perform)
- end
- end
end
diff --git a/spec/enterprise/services/conversations/unread_counts/counter_spec.rb b/spec/enterprise/services/conversations/unread_counts/counter_spec.rb
index 86a93bd8c..e8f850e78 100644
--- a/spec/enterprise/services/conversations/unread_counts/counter_spec.rb
+++ b/spec/enterprise/services/conversations/unread_counts/counter_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
end
after do
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
it 'uses base counts for custom roles with conversation_manage permission' do
@@ -26,16 +26,10 @@ RSpec.describe Conversations::UnreadCounts::Counter do
result = described_class.new(account: account, user: agent).perform
- expect(result).to eq(
- all_count: 2,
- inboxes: { inbox.id.to_s => 2 },
- labels: { label.id.to_s => 2 },
- teams: { team.id.to_s => 2 },
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 2,
- folders: {}
- )
+ expect(result[:all_count]).to eq(2)
+ expect(result[:inboxes]).to eq(inbox.id.to_s => 2)
+ expect(result[:labels]).to eq(label.id.to_s => 2)
+ expect(result[:teams]).to eq(team.id.to_s => 2)
expect(store.assignment_ready?(account.id)).to be(false)
end
@@ -43,21 +37,14 @@ RSpec.describe Conversations::UnreadCounts::Counter do
account_user.update!(custom_role: create(:custom_role, account: account, permissions: ['conversation_unassigned_manage']))
create_unread_conversation(account: account, inbox: inbox, labels: [label.title], assignee: agent, team: team)
create_unread_conversation(account: account, inbox: inbox, labels: [label.title], team: team)
- other_assigned_conversation = create_unread_conversation(account: account, inbox: inbox, labels: [label.title], assignee: other_agent, team: team)
- create(:conversation_participant, account: account, conversation: other_assigned_conversation, user: agent)
+ create_unread_conversation(account: account, inbox: inbox, labels: [label.title], assignee: other_agent, team: team)
result = described_class.new(account: account, user: agent).perform
- expect(result).to eq(
- all_count: 2,
- inboxes: { inbox.id.to_s => 2 },
- labels: { label.id.to_s => 2 },
- teams: { team.id.to_s => 2 },
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 2,
- folders: {}
- )
+ expect(result[:all_count]).to eq(2)
+ expect(result[:inboxes]).to eq(inbox.id.to_s => 2)
+ expect(result[:labels]).to eq(label.id.to_s => 2)
+ expect(result[:teams]).to eq(team.id.to_s => 2)
expect(store.assignment_ready?(account.id)).to be(true)
end
@@ -65,21 +52,13 @@ RSpec.describe Conversations::UnreadCounts::Counter do
account_user.update!(custom_role: create(:custom_role, account: account, permissions: ['conversation_participating_manage']))
create_unread_conversation(account: account, inbox: inbox, labels: [label.title], assignee: agent, team: team)
create_unread_conversation(account: account, inbox: inbox, labels: [label.title], team: team)
- participating_conversation = create_unread_conversation(account: account, inbox: inbox, labels: [label.title], team: team)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: agent)
result = described_class.new(account: account, user: agent).perform
- expect(result).to eq(
- all_count: 1,
- inboxes: { inbox.id.to_s => 1 },
- labels: { label.id.to_s => 1 },
- teams: { team.id.to_s => 1 },
- mentions_count: 0,
- participating_count: 1,
- unattended_count: 1,
- folders: {}
- )
+ expect(result[:all_count]).to eq(1)
+ expect(result[:inboxes]).to eq(inbox.id.to_s => 1)
+ expect(result[:labels]).to eq(label.id.to_s => 1)
+ expect(result[:teams]).to eq(team.id.to_s => 1)
expect(store.assignment_ready?(account.id)).to be(true)
end
@@ -89,16 +68,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
result = described_class.new(account: account, user: agent).perform
- expect(result).to eq(
- all_count: 0,
- inboxes: {},
- labels: {},
- teams: {},
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 0,
- folders: {}
- )
+ expect(result).to eq(all_count: 0, inboxes: {}, labels: {}, teams: {})
expect(store.base_ready?(account.id)).to be(false)
expect(store.assignment_ready?(account.id)).to be(false)
end
diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb
index 31cbceb6a..64f134fe2 100644
--- a/spec/finders/conversation_finder_spec.rb
+++ b/spec/finders/conversation_finder_spec.rb
@@ -208,25 +208,6 @@ describe ConversationFinder do
end
end
- context 'with participating conversation type' do
- let(:params) { { status: 'open', conversation_type: 'participating' } }
-
- it 'does not return participating conversations from inboxes where the agent is no longer a member' do
- visible_conversation = create(:conversation, account: account, inbox: inbox)
- inaccessible_conversation = create(:conversation, account: account, inbox: restricted_inbox)
- create(:inbox_member, user: user_1, inbox: restricted_inbox)
- create(:conversation_participant, account: account, conversation: visible_conversation, user: user_1)
- create(:conversation_participant, account: account, conversation: inaccessible_conversation, user: user_1)
- InboxMember.find_by!(user: user_1, inbox: restricted_inbox).destroy!
-
- result = conversation_finder.perform
- conversation_ids = result[:conversations].map(&:id)
-
- expect(conversation_ids).to include(visible_conversation.id)
- expect(conversation_ids).not_to include(inaccessible_conversation.id)
- end
- end
-
context 'without source' do
let(:params) { {} }
diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb
index 7a774dbd5..cdb9a93cb 100644
--- a/spec/listeners/action_cable_listener_spec.rb
+++ b/spec/listeners/action_cable_listener_spec.rb
@@ -294,19 +294,5 @@ describe ActionCableListener do
listener.conversation_unread_count_changed(event)
end
-
- it 'supports user-scoped unread count refresh events' do
- event = Events::Base.new(event_name, Time.zone.now, account: account, user: agent)
-
- expect(ActionCableBroadcastJob).to receive(:perform_later).with(
- a_collection_containing_exactly(agent.pubsub_token),
- 'conversation.unread_count_changed',
- {
- account_id: account.id
- }
- )
-
- listener.conversation_unread_count_changed(event)
- end
end
end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index d77a16fdb..38ca9694a 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -53,79 +53,38 @@ 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) }
- 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
after do
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
- it 'clears all unread count cache when the feature is enabled' do
+ it 'clears unread count cache when the feature is enabled' do
build_unread_count_cache
account.enable_features!(:conversation_unread_counts)
- expect_unread_count_cache_cleared
+ 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)
end
- it 'clears all unread count cache when the feature is disabled' do
+ it 'clears 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_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
+ 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)
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
diff --git a/spec/models/account_user_spec.rb b/spec/models/account_user_spec.rb
index 7acfc19cd..e5a560fe9 100644
--- a/spec/models/account_user_spec.rb
+++ b/spec/models/account_user_spec.rb
@@ -42,32 +42,4 @@ 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
diff --git a/spec/models/inbox_member_spec.rb b/spec/models/inbox_member_spec.rb
index df569c695..b081c546c 100644
--- a/spec/models/inbox_member_spec.rb
+++ b/spec/models/inbox_member_spec.rb
@@ -18,30 +18,4 @@ 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
diff --git a/spec/services/conversations/unread_counts/builder_spec.rb b/spec/services/conversations/unread_counts/builder_spec.rb
index fe1bdb6fa..4b3aec230 100644
--- a/spec/services/conversations/unread_counts/builder_spec.rb
+++ b/spec/services/conversations/unread_counts/builder_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Conversations::UnreadCounts::Builder do
let(:store) { Conversations::UnreadCounts::Store }
after do
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
describe '#build_base!' do
@@ -75,173 +75,6 @@ RSpec.describe Conversations::UnreadCounts::Builder do
end
end
- describe '#build_filters_for!' do
- before do
- create(:inbox_member, user: assignee, inbox: inbox)
- end
-
- it 'stores unread open conversations by mentions and participating dimensions' do
- mentioned_conversation = create_unread_conversation(account: account, inbox: inbox)
- participating_conversation = create_unread_conversation(account: account, inbox: inbox)
- resolved_mentioned_conversation = create_unread_conversation(account: account, inbox: inbox)
- inaccessible_conversation = create_unread_conversation(account: account, inbox: create(:inbox, account: account))
- resolved_mentioned_conversation.update!(status: :resolved)
-
- create(:mention, account: account, conversation: mentioned_conversation, user: assignee)
- create(:mention, account: account, conversation: resolved_mentioned_conversation, user: assignee)
- create(:mention, account: account, conversation: inaccessible_conversation, user: assignee)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: assignee)
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(store.filters_ready?(account.id, assignee.id)).to be(true)
- expect(redis_set_members(store.user_mentions_key(account.id, assignee.id))).to contain_exactly(mentioned_conversation.id.to_s)
- expect(redis_set_members(store.user_participating_key(account.id, assignee.id))).to contain_exactly(participating_conversation.id.to_s)
- end
-
- it 'excludes participating conversations that are no longer visible to the user' do
- participating_conversation = create_unread_conversation(account: account, inbox: inbox)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: assignee)
- InboxMember.find_by!(user: assignee, inbox: inbox).destroy!
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(redis_set_members(store.user_participating_key(account.id, assignee.id))).to be_empty
- end
-
- it 'stores visible unread open unattended conversations' do
- no_first_reply_conversation = create_unread_conversation(account: account, inbox: inbox)
- waiting_conversation = create_unread_conversation(account: account, inbox: inbox)
- attended_conversation = create_unread_conversation(account: account, inbox: inbox)
- inaccessible_conversation = create_unread_conversation(account: account, inbox: create(:inbox, account: account))
- resolved_conversation = create_unread_conversation(account: account, inbox: inbox)
- create_read_conversation
-
- waiting_conversation.update!(first_reply_created_at: 5.minutes.ago)
- attended_conversation.update!(first_reply_created_at: 5.minutes.ago, waiting_since: nil)
- inaccessible_conversation.update!(first_reply_created_at: nil)
- resolved_conversation.update!(status: :resolved)
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(redis_set_members(store.user_unattended_key(account.id, assignee.id))).to contain_exactly(
- no_first_reply_conversation.id.to_s,
- waiting_conversation.id.to_s
- )
- end
-
- it 'stores folder memberships using the saved filter status conditions' do
- resolved_conversation = create_unread_conversation(account: account, inbox: inbox)
- resolved_conversation.update!(status: :resolved)
- create_unread_conversation(account: account, inbox: inbox, assignee: assignee)
- custom_filter = create(
- :custom_filter, account: account, user: assignee, filter_type: :conversation, query: filter_query('status', ['resolved'])
- )
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(redis_set_members(store.user_folder_key(account.id, assignee.id, custom_filter.id))).to contain_exactly(resolved_conversation.id.to_s)
- end
-
- it 'loads folder filters after taking the invalidation version snapshot' do
- create_unread_conversation(account: account, inbox: inbox)
- resolved_conversation = create_unread_conversation(account: account, inbox: inbox)
- resolved_conversation.update!(status: :resolved)
- custom_filter = create(
- :custom_filter, account: account, user: assignee, filter_type: :conversation, query: filter_query('status', ['open'])
- )
- notifier = instance_double(Conversations::UnreadCounts::UserFilterNotifier, perform: true)
- allow(Conversations::UnreadCounts::UserFilterNotifier).to receive(:new).and_return(notifier)
- filter_updated = false
- allow(store).to receive(:filter_version_snapshot).and_wrap_original do |method, *args|
- method.call(*args).tap do
- next if filter_updated
-
- filter_updated = true
- custom_filter.update!(query: filter_query('status', ['resolved']))
- end
- end
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(redis_set_members(store.user_folder_key(account.id, assignee.id, custom_filter.id))).to contain_exactly(resolved_conversation.id.to_s)
- end
-
- it 'expires relative-date folder caches at the next date boundary' do
- create(
- :custom_filter,
- account: account,
- user: assignee,
- filter_type: :conversation,
- query: filter_query('created_at', [7], filter_operator: 'days_before')
- )
- allow(store).to receive(:mark_filters_ready_if_current!).and_call_original
- expected_ttl = nil
-
- travel_to Time.zone.local(2026, 1, 1, 9, 30, 0) do
- expected_ttl = (Time.zone.tomorrow.beginning_of_day - Time.current).ceil
- described_class.new(account).build_filters_for!(assignee)
- end
-
- expect(store).to have_received(:mark_filters_ready_if_current!).with(
- account.id,
- assignee.id,
- version_snapshot: kind_of(Hash),
- expires_in: expected_ttl
- )
- end
-
- it 'does not mark filters ready when user filters are invalidated during the build' do
- conversation = create_unread_conversation(account: account, inbox: inbox)
- create(:mention, account: account, conversation: conversation, user: assignee)
- clear_user_filters_after_membership_write
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(store.filters_ready?(account.id, assignee.id)).to be(false)
- expect(redis_set_members(store.user_mentions_key(account.id, assignee.id))).to be_empty
- end
-
- it 'does not mark filters ready when account filters are invalidated during the build' do
- conversation = create_unread_conversation(account: account, inbox: inbox)
- create(:mention, account: account, conversation: conversation, user: assignee)
- clear_filter_caches_after_membership_write
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(store.filters_ready?(account.id, assignee.id)).to be(false)
- expect(redis_set_members(store.user_mentions_key(account.id, assignee.id))).to be_empty
- end
-
- it 'skips invalid folder filters and still marks the user filter cache ready' do
- create_unread_conversation(account: account, inbox: inbox)
- invalid_filter = create(
- :custom_filter, account: account, user: assignee, filter_type: :conversation, query: filter_query('missing_attribute', ['open'])
- )
-
- described_class.new(account).build_filters_for!(assignee)
-
- expect(store.filters_ready?(account.id, assignee.id)).to be(true)
- expect(redis_set_members(store.user_folder_key(account.id, assignee.id, invalid_filter.id))).to be_empty
- end
-
- it 'skips folder filters that fail when the SQL query is executed' do
- conversation = create_unread_conversation(account: account, inbox: inbox)
- invalid_filter = create(
- :custom_filter,
- account: account,
- user: assignee,
- filter_type: :conversation,
- query: filter_query('display_id', [conversation.display_id.to_s], filter_operator: 'contains')
- )
-
- expect { described_class.new(account).build_filters_for!(assignee) }.not_to raise_error
-
- expect(store.filters_ready?(account.id, assignee.id)).to be(true)
- expect(redis_set_members(store.user_folder_key(account.id, assignee.id, invalid_filter.id))).to be_empty
- end
- end
-
def create_read_conversation
conversation = create(:conversation, account: account, inbox: inbox, agent_last_seen_at: 1.minute.from_now)
create(:message, account: account, inbox: inbox, conversation: conversation, message_type: :incoming)
@@ -257,32 +90,4 @@ RSpec.describe Conversations::UnreadCounts::Builder do
def redis_set_members(key)
Redis::Alfred.pipelined { |pipeline| pipeline.smembers(key) }.first
end
-
- def clear_user_filters_after_membership_write
- allow(store).to receive(:add_filter_memberships).and_wrap_original do |method, *args, **kwargs|
- method.call(*args, **kwargs)
- store.clear_user_filters!(account.id, assignee.id)
- end
- end
-
- def clear_filter_caches_after_membership_write
- allow(store).to receive(:add_filter_memberships).and_wrap_original do |method, *args, **kwargs|
- method.call(*args, **kwargs)
- store.clear_filter_caches!(account.id)
- end
- end
-
- def filter_query(attribute_key, values, filter_operator: 'equal_to')
- {
- payload: [
- {
- attribute_key: attribute_key,
- filter_operator: filter_operator,
- values: values,
- query_operator: nil,
- custom_attribute_type: ''
- }
- ]
- }
- end
end
diff --git a/spec/services/conversations/unread_counts/counter_spec.rb b/spec/services/conversations/unread_counts/counter_spec.rb
index c3c338355..723f2d437 100644
--- a/spec/services/conversations/unread_counts/counter_spec.rb
+++ b/spec/services/conversations/unread_counts/counter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
end
after do
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
it 'builds the base cache on demand' do
@@ -32,7 +32,6 @@ RSpec.describe Conversations::UnreadCounts::Counter do
lock_key = "UNREAD_CONVERSATIONS::V1::ACCOUNT::#{account.id}::BUILD_LOCK::BASE"
lock_manager = instance_double(Redis::LockManager)
allow(Redis::LockManager).to receive(:new).and_return(lock_manager)
- allow(lock_manager).to receive(:with_lock).and_yield.and_return(true)
allow(lock_manager).to receive(:with_lock).with(lock_key, described_class::BUILD_LOCK_TTL).and_yield.and_return(true)
create_unread_conversation(account: account, inbox: visible_inbox, labels: [label.title], team: visible_team)
@@ -47,33 +46,13 @@ RSpec.describe Conversations::UnreadCounts::Counter do
counter = described_class.new(account: account, user: agent)
allow(Redis::LockManager).to receive(:new).and_return(lock_manager)
- allow(counter).to receive(:wait_for_cache_ready) do
- store.mark_base_ready!(account.id)
- store.mark_filters_ready!(account.id, agent.id)
- end
+ allow(counter).to receive(:wait_for_cache_ready) { store.mark_base_ready!(account.id) }
expect(Conversations::UnreadCounts::Builder).not_to receive(:new)
counter.perform
expect(counter).to have_received(:wait_for_cache_ready)
expect(store.base_ready?(account.id)).to be(true)
- expect(store.filters_ready?(account.id, agent.id)).to be(true)
- end
-
- it 'retries when a build finishes without marking the cache ready' do
- builder = instance_double(Conversations::UnreadCounts::Builder)
- attempts = 0
- allow(Conversations::UnreadCounts::Builder).to receive(:new).and_return(builder)
- allow(builder).to receive(:build_base!) do
- attempts += 1
- store.mark_base_ready!(account.id) if attempts == 2
- end
- allow(builder).to receive(:build_filters_for!) { store.mark_filters_ready!(account.id, agent.id) }
-
- described_class.new(account: account, user: agent).perform
-
- expect(builder).to have_received(:build_base!).twice
- expect(store.base_ready?(account.id)).to be(true)
end
it 'counts unread conversations only across inboxes visible to a normal agent' do
@@ -86,11 +65,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
all_count: 1,
inboxes: { visible_inbox.id.to_s => 1 },
labels: { label.id.to_s => 1 },
- teams: { visible_team.id.to_s => 1 },
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 1,
- folders: {}
+ teams: { visible_team.id.to_s => 1 }
)
end
@@ -104,11 +79,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
all_count: 2,
inboxes: { visible_inbox.id.to_s => 1, hidden_inbox.id.to_s => 1 },
labels: { label.id.to_s => 2 },
- teams: { visible_team.id.to_s => 2 },
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 2,
- folders: {}
+ teams: { visible_team.id.to_s => 2 }
)
end
@@ -121,46 +92,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do
all_count: 1,
inboxes: { visible_inbox.id.to_s => 1 },
labels: {},
- teams: { visible_team.id.to_s => 1 },
- mentions_count: 0,
- participating_count: 0,
- unattended_count: 1,
- folders: {}
+ teams: { visible_team.id.to_s => 1 }
)
end
-
- it 'returns mention, participating, unattended, and valid folder unread counts for the user' do
- mentioned_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- participating_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- resolved_conversation = create_unread_conversation(account: account, inbox: visible_inbox)
- resolved_conversation.update!(status: :resolved)
- valid_folder = create(:custom_filter, account: account, user: agent, filter_type: :conversation, query: filter_query('status', ['resolved']))
- invalid_folder = create(:custom_filter, account: account, user: agent, filter_type: :conversation, query: filter_query('unknown', ['open']))
-
- create(:mention, account: account, conversation: mentioned_conversation, user: agent)
- create(:conversation_participant, account: account, conversation: participating_conversation, user: agent)
-
- result = described_class.new(account: account, user: agent).perform
-
- expect(result[:mentions_count]).to eq(1)
- expect(result[:participating_count]).to eq(1)
- expect(result[:unattended_count]).to eq(2)
- expect(result[:folders]).to eq(valid_folder.id.to_s => 1)
- expect(result[:folders]).not_to have_key(invalid_folder.id.to_s)
- expect(store.filters_ready?(account.id, agent.id)).to be(true)
- end
-
- def filter_query(attribute_key, values)
- {
- payload: [
- {
- attribute_key: attribute_key,
- filter_operator: 'equal_to',
- values: values,
- query_operator: nil,
- custom_attribute_type: ''
- }
- ]
- }
- end
end
diff --git a/spec/services/conversations/unread_counts/listener_spec.rb b/spec/services/conversations/unread_counts/listener_spec.rb
index 0cf31bd3c..fbb0a0835 100644
--- a/spec/services/conversations/unread_counts/listener_spec.rb
+++ b/spec/services/conversations/unread_counts/listener_spec.rb
@@ -21,22 +21,13 @@ RSpec.describe Conversations::UnreadCounts::Listener do
expect(notifier).to have_received(:perform)
end
- it 'clears user filter counts when a non-incoming message updates last activity' do
- account.enable_features!(:conversation_unread_counts)
+ it 'ignores outgoing message creation' do
message = create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :outgoing)
event = Events::Base.new('message.created', Time.zone.now, message: message)
- allow(store).to receive(:clear_filter_caches!).and_return(true)
- allow(Rails.configuration.dispatcher).to receive(:dispatch)
listener.message_created(event)
expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new)
- expect(store).to have_received(:clear_filter_caches!).with(account.id)
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
- 'conversation.unread_count_changed',
- kind_of(Time),
- conversation: conversation
- )
end
it 'ignores incoming message creation when conversation unread counts are disabled' do
@@ -61,7 +52,6 @@ RSpec.describe Conversations::UnreadCounts::Listener do
end
it 'refreshes unread counts when labels change' do
- account.enable_features!(:conversation_unread_counts)
changed_attributes = { label_list: [%w[old], %w[new]] }
event = Events::Base.new('conversation.updated', Time.zone.now, conversation: conversation, changed_attributes: changed_attributes)
@@ -71,43 +61,8 @@ RSpec.describe Conversations::UnreadCounts::Listener do
expect(notifier).to have_received(:perform)
end
- it 'clears user filter counts when a folder filter dimension changes' do
- account.enable_features!(:conversation_unread_counts)
+ it 'ignores conversation updates unrelated to unread count dimensions' do
event = Events::Base.new('conversation.updated', Time.zone.now, conversation: conversation, changed_attributes: { priority: [nil, 'high'] })
- allow(store).to receive(:clear_filter_caches!).and_return(true)
- allow(Rails.configuration.dispatcher).to receive(:dispatch)
-
- listener.conversation_updated(event)
-
- expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new)
- expect(store).to have_received(:clear_filter_caches!).with(account.id)
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
- 'conversation.unread_count_changed',
- kind_of(Time),
- conversation: conversation
- )
- end
-
- it 'clears user filter counts when the conversation contact changes' do
- account.enable_features!(:conversation_unread_counts)
- event = Events::Base.new('conversation.contact_changed', Time.zone.now, conversation: conversation)
- allow(store).to receive(:clear_filter_caches!).and_return(true)
- allow(Rails.configuration.dispatcher).to receive(:dispatch)
-
- listener.conversation_contact_changed(event)
-
- expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new)
- expect(store).to have_received(:clear_filter_caches!).with(account.id)
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
- 'conversation.unread_count_changed',
- kind_of(Time),
- conversation: conversation
- )
- end
-
- it 'ignores conversation updates without changed attributes' do
- account.enable_features!(:conversation_unread_counts)
- event = Events::Base.new('conversation.updated', Time.zone.now, conversation: conversation, changed_attributes: {})
listener.conversation_updated(event)
@@ -173,7 +128,7 @@ RSpec.describe Conversations::UnreadCounts::Listener do
conversation_data: conversation_data.stringify_keys
)
ensure
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
def deleted_conversation_data(conversation)
diff --git a/spec/services/conversations/unread_counts/notifier_spec.rb b/spec/services/conversations/unread_counts/notifier_spec.rb
index 645c2bc17..1b35d37f6 100644
--- a/spec/services/conversations/unread_counts/notifier_spec.rb
+++ b/spec/services/conversations/unread_counts/notifier_spec.rb
@@ -29,18 +29,6 @@ RSpec.describe Conversations::UnreadCounts::Notifier do
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
end
-
- it 'dispatches unread count changed event when user filter caches were cleared' do
- allow(Conversations::UnreadCounts::Store).to receive(:clear_filter_caches!).and_return(true)
-
- described_class.new(conversation).perform
-
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
- 'conversation.unread_count_changed',
- kind_of(Time),
- conversation: conversation
- )
- end
end
context 'when conversation unread counts feature is disabled' do
diff --git a/spec/services/conversations/unread_counts/refresher_spec.rb b/spec/services/conversations/unread_counts/refresher_spec.rb
index e961c21c3..5f361e7aa 100644
--- a/spec/services/conversations/unread_counts/refresher_spec.rb
+++ b/spec/services/conversations/unread_counts/refresher_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Conversations::UnreadCounts::Refresher do
let(:store) { Conversations::UnreadCounts::Store }
after do
- store.clear_all_account!(account.id)
+ store.clear_account!(account.id)
end
it 'does not update redis when unread caches are not ready' do
diff --git a/spec/services/conversations/unread_counts/store_spec.rb b/spec/services/conversations/unread_counts/store_spec.rb
index 9e1311727..fcf21c985 100644
--- a/spec/services/conversations/unread_counts/store_spec.rb
+++ b/spec/services/conversations/unread_counts/store_spec.rb
@@ -7,10 +7,9 @@ RSpec.describe Conversations::UnreadCounts::Store do
let(:user_id) { 4 }
let(:conversation_id) { 5 }
let(:team_id) { 6 }
- let(:other_user_id) { 8 }
after do
- described_class.clear_all_account!(account_id)
+ described_class.clear_account!(account_id)
end
describe 'key builders' do
@@ -46,69 +45,20 @@ RSpec.describe Conversations::UnreadCounts::Store do
'UNREAD_CONVERSATIONS::V1::ACCOUNT::1::TEAM::6::INBOX::2::ASSIGNEE::4'
)
end
-
- it 'builds user filter keys using the Redis key naming convention' do
- expect(described_class.user_mentions_key(account_id, user_id)).to eq(
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::1::USER::4::MENTIONS'
- )
- expect(described_class.user_participating_key(account_id, user_id)).to eq(
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::1::USER::4::PARTICIPATING'
- )
- expect(described_class.user_unattended_key(account_id, user_id)).to eq(
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::1::USER::4::UNATTENDED'
- )
- expect(described_class.user_folder_key(account_id, user_id, 7)).to eq(
- 'UNREAD_CONVERSATIONS::V1::ACCOUNT::1::USER::4::FOLDER::7'
- )
- end
end
describe 'ready markers' do
- it 'starts with all ready markers missing' do
+ it 'tracks base and assignment readiness independently' do
expect(described_class.base_ready?(account_id)).to be(false)
expect(described_class.assignment_ready?(account_id)).to be(false)
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
- end
- it 'tracks base, assignment, and user filter readiness independently' do
described_class.mark_base_ready!(account_id)
described_class.mark_assignment_ready!(account_id)
- described_class.mark_filters_ready!(account_id, user_id)
expect(described_class.base_ready?(account_id)).to be(true)
expect(described_class.assignment_ready?(account_id)).to be(true)
- expect(described_class.filters_ready?(account_id, user_id)).to be(true)
expect(ttl_for('UNREAD_CONVERSATIONS::V1::ACCOUNT::1::READY::BASE')).to be_within(5).of(Conversations::UnreadCounts::READY_TTL)
expect(ttl_for('UNREAD_CONVERSATIONS::V1::ACCOUNT::1::READY::ASSIGNMENT')).to be_within(5).of(Conversations::UnreadCounts::READY_TTL)
- expect(ttl_for('UNREAD_CONVERSATIONS::V1::ACCOUNT::1::USER::4::READY::FILTERS')).to be_within(5).of(
- Conversations::UnreadCounts::READY_TTL
- )
- end
-
- it 'tracks filter invalidation versions independently' do
- expect(described_class.filter_version_snapshot(account_id, user_id)).to eq(account: 0, user: 0)
-
- expect(described_class.clear_user_filters!(account_id, user_id)).to be(false)
-
- expect(described_class.filter_version_snapshot(account_id, user_id)).to eq(account: 0, user: 1)
- expect(ttl_for(user_filter_version_key)).to be_within(5).of(Conversations::UnreadCounts::SET_TTL)
-
- expect(described_class.clear_filter_caches!(account_id)).to be(false)
-
- expect(described_class.filter_version_snapshot(account_id, user_id)).to eq(account: 1, user: 1)
- expect(ttl_for(account_filter_version_key)).to be_within(5).of(Conversations::UnreadCounts::SET_TTL)
- end
-
- it 'marks filter caches ready only when the invalidation version is current' do
- version_snapshot = described_class.filter_version_snapshot(account_id, user_id)
-
- expect(described_class.mark_filters_ready_if_current!(account_id, user_id, version_snapshot: version_snapshot)).to be_truthy
- expect(described_class.filters_ready?(account_id, user_id)).to be(true)
-
- described_class.clear_user_filters!(account_id, user_id)
-
- expect(described_class.mark_filters_ready_if_current!(account_id, user_id, version_snapshot: version_snapshot)).to be(false)
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
end
end
@@ -200,94 +150,9 @@ RSpec.describe Conversations::UnreadCounts::Store do
expect(base_keys.map { |key| ttl_for(key) }).to all(be_within(5).of(Conversations::UnreadCounts::SET_TTL))
end
- it 'adds, counts, and clears user filter memberships' do
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: user_id,
- filters: {
- mentions: [conversation_id],
- participating: [conversation_id],
- unattended: [conversation_id]
- },
- folders: { 7 => [conversation_id] }
- )
- described_class.mark_filters_ready!(account_id, user_id)
-
- expect(described_class.counts_for_keys(user_filter_keys)).to eq(
- described_class.user_mentions_key(account_id, user_id) => 1,
- described_class.user_participating_key(account_id, user_id) => 1,
- described_class.user_unattended_key(account_id, user_id) => 1,
- described_class.user_folder_key(account_id, user_id, 7) => 1
- )
- expect(user_filter_keys.map { |key| ttl_for(key) }).to all(be_within(5).of(Conversations::UnreadCounts::SET_TTL))
-
- expect(described_class.clear_user_filters!(account_id, user_id)).to be(true)
-
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
- expect(described_class.counts_for_keys(user_filter_keys).values).to all(eq(0))
- end
-
- it 'preserves the user filter build lock when clearing one user filter cache' do
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: user_id,
- filters: {
- mentions: [conversation_id],
- participating: [conversation_id],
- unattended: [conversation_id]
- },
- folders: { 7 => [conversation_id] }
- )
- described_class.mark_filters_ready!(account_id, user_id)
- Redis::Alfred.set(user_filter_build_lock_key, 'locked')
-
- expect(described_class.clear_user_filters!(account_id, user_id)).to be(true)
-
- expect(Redis::Alfred.exists?(user_filter_build_lock_key)).to be(true)
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
- expect(described_class.counts_for_keys(user_filter_keys).values).to all(eq(0))
- end
-
- it 'preserves user filter build locks when clearing all account filter caches' do
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: user_id,
- filters: {
- mentions: [conversation_id],
- participating: [],
- unattended: []
- },
- folders: {}
- )
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: other_user_id,
- filters: {
- mentions: [conversation_id],
- participating: [],
- unattended: []
- },
- folders: {}
- )
- described_class.mark_filters_ready!(account_id, user_id)
- described_class.mark_filters_ready!(account_id, other_user_id)
- Redis::Alfred.set(user_filter_build_lock_key, 'locked')
- Redis::Alfred.set(user_filter_build_lock_key(other_user_id), 'locked')
-
- expect(described_class.clear_filter_caches!(account_id)).to be(true)
-
- expect(Redis::Alfred.exists?(user_filter_build_lock_key)).to be(true)
- expect(Redis::Alfred.exists?(user_filter_build_lock_key(other_user_id))).to be(true)
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
- expect(described_class.filters_ready?(account_id, other_user_id)).to be(false)
- expect(described_class.counts_for_keys([described_class.user_mentions_key(account_id, user_id)]).values).to all(eq(0))
- expect(described_class.counts_for_keys([described_class.user_mentions_key(account_id, other_user_id)]).values).to all(eq(0))
- end
-
- it 'clears account memberships without clearing user filter memberships' do
+ it 'clears all account memberships' do
described_class.mark_base_ready!(account_id)
described_class.mark_assignment_ready!(account_id)
- described_class.mark_filters_ready!(account_id, user_id)
described_class.add_base_membership(
account_id: account_id,
inbox_id: inbox_id,
@@ -303,69 +168,13 @@ RSpec.describe Conversations::UnreadCounts::Store do
team_id: team_id,
conversation_id: conversation_id
)
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: user_id,
- filters: {
- mentions: [conversation_id],
- participating: [],
- unattended: []
- },
- folders: {}
- )
- Redis::Alfred.set(user_filter_build_lock_key, 'locked')
described_class.clear_account!(account_id)
expect(described_class.base_ready?(account_id)).to be(false)
expect(described_class.assignment_ready?(account_id)).to be(false)
- expect(described_class.filters_ready?(account_id, user_id)).to be(true)
expect(described_class.counts_for_keys(base_keys).values).to all(eq(0))
expect(described_class.counts_for_keys(assignment_keys).values).to all(eq(0))
- expect(described_class.counts_for_keys([described_class.user_mentions_key(account_id, user_id)]).values).to all(eq(1))
- expect(Redis::Alfred.exists?(user_filter_build_lock_key)).to be(true)
- end
-
- it 'clears all account unread count keys' do
- described_class.mark_base_ready!(account_id)
- described_class.mark_assignment_ready!(account_id)
- described_class.mark_filters_ready!(account_id, user_id)
- described_class.add_base_membership(
- account_id: account_id,
- inbox_id: inbox_id,
- label_ids: [label_id],
- team_id: team_id,
- conversation_id: conversation_id
- )
- described_class.add_assignment_membership(
- account_id: account_id,
- inbox_id: inbox_id,
- label_ids: [label_id],
- assignee_id: user_id,
- team_id: team_id,
- conversation_id: conversation_id
- )
- described_class.add_filter_memberships(
- account_id: account_id,
- user_id: user_id,
- filters: {
- mentions: [conversation_id],
- participating: [],
- unattended: []
- },
- folders: {}
- )
- Redis::Alfred.set(user_filter_build_lock_key, 'locked')
-
- described_class.clear_all_account!(account_id)
-
- expect(described_class.base_ready?(account_id)).to be(false)
- expect(described_class.assignment_ready?(account_id)).to be(false)
- expect(described_class.filters_ready?(account_id, user_id)).to be(false)
- expect(described_class.counts_for_keys(base_keys).values).to all(eq(0))
- expect(described_class.counts_for_keys(assignment_keys).values).to all(eq(0))
- expect(described_class.counts_for_keys([described_class.user_mentions_key(account_id, user_id)]).values).to all(eq(0))
- expect(Redis::Alfred.exists?(user_filter_build_lock_key)).to be(false)
end
end
@@ -385,27 +194,6 @@ RSpec.describe Conversations::UnreadCounts::Store do
]
end
- def user_filter_keys(filter_user_id = user_id)
- [
- described_class.user_mentions_key(account_id, filter_user_id),
- described_class.user_participating_key(account_id, filter_user_id),
- described_class.user_unattended_key(account_id, filter_user_id),
- described_class.user_folder_key(account_id, filter_user_id, 7)
- ]
- end
-
- def user_filter_build_lock_key(filter_user_id = user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FILTERS_BUILD_LOCK, account_id: account_id, user_id: filter_user_id)
- end
-
- def account_filter_version_key
- format(Redis::Alfred::UNREAD_CONVERSATIONS_FILTERS_VERSION, account_id: account_id)
- end
-
- def user_filter_version_key(filter_user_id = user_id)
- format(Redis::Alfred::UNREAD_CONVERSATIONS_USER_FILTERS_VERSION, account_id: account_id, user_id: filter_user_id)
- end
-
def ttl_for(key)
Redis::Alfred.ttl(key)
end
diff --git a/spec/services/conversations/unread_counts/user_filter_notifier_spec.rb b/spec/services/conversations/unread_counts/user_filter_notifier_spec.rb
deleted file mode 100644
index 1d898b103..000000000
--- a/spec/services/conversations/unread_counts/user_filter_notifier_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe Conversations::UnreadCounts::UserFilterNotifier do
- let(:account) { create(:account) }
- let(:user) { create(:user, account: account) }
- let(:store) { Conversations::UnreadCounts::Store }
-
- after do
- store.clear_all_account!(account.id)
- end
-
- it 'clears the user filter cache and dispatches an unread count refresh event' do
- account.enable_features!(:conversation_unread_counts)
- store.mark_filters_ready!(account.id, user.id)
- allow(Rails.configuration.dispatcher).to receive(:dispatch)
-
- described_class.new(account: account, user: user).perform
-
- expect(store.filters_ready?(account.id, user.id)).to be(false)
- expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
- 'conversation.unread_count_changed',
- kind_of(Time),
- account: account,
- user: user
- )
- end
-
- it 'does nothing when conversation unread counts are disabled' do
- store.mark_filters_ready!(account.id, user.id)
- allow(Rails.configuration.dispatcher).to receive(:dispatch)
-
- described_class.new(account: account, user: user).perform
-
- expect(store.filters_ready?(account.id, user.id)).to be(true)
- expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
- end
-end