From 88e2661ca600a44197ffb08193b58e6fb33e819b Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Tue, 2 Jun 2026 14:35:37 +0530 Subject: [PATCH] feat(conversations): remove unread count feature flag (CW-7237) (#14610) ## Description Make conversation unread counts always available at runtime by removing account feature checks from the API endpoint, unread-count listener, notifier, and ActionCable broadcast path. Update the dashboard to fetch sidebar unread counts for the active account without checking FEATURE_FLAGS.CONVERSATION_UNREAD_COUNTS, and remove the now-unused store clear action that only supported the disabled state. Keep the feature entry in config/features.yml to preserve flag bit order, but mark it enabled and deprecated so fresh installs default to the always-on behavior while feature-management UI hides it. Leave existing installation default rows untouched; no migration is included, so upgraded installs may still store the old flag value but runtime behavior no longer depends on it. Update specs around the new always-on contract and remove obsolete disabled-feature assertions. Fixes # CW-7237 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Update specs around the new always-on contract and remove obsolete disabled-feature assertions. Ran specs locally for the changes. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../conversations/unread_counts_controller.rb | 10 ---- .../components-next/sidebar/Sidebar.vue | 16 +---- app/javascript/dashboard/featureFlags.js | 1 - .../dashboard/helper/actionCable.js | 14 ----- .../helper/specs/actionCable.spec.js | 16 ----- .../store/modules/conversationUnreadCounts.js | 3 - .../conversationUnreadCounts/actions.spec.js | 11 ---- app/listeners/action_cable_listener.rb | 2 +- app/models/account.rb | 1 - .../conversations/unread_counts/listener.rb | 3 +- .../conversations/unread_counts/notifier.rb | 2 - config/features.yml | 3 +- config/locales/en.yml | 3 - .../accounts/conversations_controller_spec.rb | 59 +++++++------------ spec/listeners/action_cable_listener_spec.rb | 12 ---- spec/models/account_spec.rb | 38 ------------ .../unread_counts/listener_spec.rb | 13 ---- .../unread_counts/notifier_spec.rb | 16 ----- 18 files changed, 27 insertions(+), 196 deletions(-) diff --git a/app/controllers/api/v1/accounts/conversations/unread_counts_controller.rb b/app/controllers/api/v1/accounts/conversations/unread_counts_controller.rb index d9f15613b..6b1ee90e6 100644 --- a/app/controllers/api/v1/accounts/conversations/unread_counts_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/unread_counts_controller.rb @@ -1,16 +1,6 @@ class Api::V1::Accounts::Conversations::UnreadCountsController < Api::V1::Accounts::BaseController - before_action :ensure_unread_counts_enabled - def index counts = ::Conversations::UnreadCounts::Counter.new(account: Current.account, user: Current.user).perform render json: { payload: counts } end - - private - - def ensure_unread_counts_enabled - return if Current.account.feature_enabled?('conversation_unread_counts') - - render json: { error: I18n.t('errors.conversations.unread_counts.feature_not_enabled') }, status: :forbidden - end end diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index d9f523dfc..0fccfbee5 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -61,21 +61,9 @@ const hasAdvancedAssignment = computed(() => { ); }); -const hasConversationUnreadCounts = computed(() => { - return isFeatureEnabledonAccount.value( - accountId.value, - FEATURE_FLAGS.CONVERSATION_UNREAD_COUNTS - ); -}); - -const fetchConversationUnreadCounts = ([currentAccountId, isEnabled]) => { +const fetchConversationUnreadCounts = currentAccountId => { if (!currentAccountId) return; - if (!isEnabled) { - store.dispatch('conversationUnreadCounts/clear'); - return; - } - store.dispatch('conversationUnreadCounts/get'); }; @@ -200,7 +188,7 @@ onMounted(() => { store.dispatch('customViews/get', 'contact'); }); -watch([accountId, hasConversationUnreadCounts], fetchConversationUnreadCounts, { +watch(accountId, fetchConversationUnreadCounts, { immediate: true, }); diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 00a79763b..b97e30984 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -46,7 +46,6 @@ export const FEATURE_FLAGS = { COMPANIES: 'companies', ADVANCED_SEARCH: 'advanced_search', CONVERSATION_REQUIRED_ATTRIBUTES: 'conversation_required_attributes', - CONVERSATION_UNREAD_COUNTS: 'conversation_unread_counts', }; export const PREMIUM_FEATURES = [ diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index e6f2993e4..a2caab5f5 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -13,7 +13,6 @@ import { } from 'dashboard/composables/useWhatsappCallSession'; import { VOICE_CALL_PROVIDERS } from 'dashboard/helper/inbox'; import { VOICE_CALL_DIRECTION } from 'dashboard/components-next/message/constants'; -import { FEATURE_FLAGS } from 'dashboard/featureFlags'; const { isImpersonating } = useImpersonation(); const UNREAD_COUNTS_REFETCH_THROTTLE_MS = 5000; @@ -172,23 +171,10 @@ class ActionCableConnector extends BaseActionCableConnector { }; fetchConversationUnreadCounts = () => { - if (!this.isConversationUnreadCountsEnabled()) return; - this.lastUnreadCountsFetchAt = Date.now(); this.app.$store.dispatch('conversationUnreadCounts/get'); }; - isConversationUnreadCountsEnabled = () => { - const accountId = this.app.$store.getters.getCurrentAccountId; - const isFeatureEnabled = - this.app.$store.getters['accounts/isFeatureEnabledonAccount']; - - return isFeatureEnabled?.( - accountId, - FEATURE_FLAGS.CONVERSATION_UNREAD_COUNTS - ); - }; - onTypingOn = ({ conversation, user }) => { const conversationId = conversation.id; diff --git a/app/javascript/dashboard/helper/specs/actionCable.spec.js b/app/javascript/dashboard/helper/specs/actionCable.spec.js index 8ba411a5f..2085b415a 100644 --- a/app/javascript/dashboard/helper/specs/actionCable.spec.js +++ b/app/javascript/dashboard/helper/specs/actionCable.spec.js @@ -30,7 +30,6 @@ describe('ActionCableConnector - Copilot Tests', () => { dispatch: mockDispatch, getters: { getCurrentAccountId: 1, - 'accounts/isFeatureEnabledonAccount': vi.fn(() => true), }, }, }; @@ -89,21 +88,6 @@ describe('ActionCableConnector - Copilot Tests', () => { expect(mockDispatch).toHaveBeenCalledWith('conversationUnreadCounts/get'); }); - it('does not refetch unread counts when unread count feature is disabled', () => { - store.$store.getters[ - 'accounts/isFeatureEnabledonAccount' - ].mockReturnValue(false); - - actionCable.onReceived({ - event: 'conversation.unread_count_changed', - data: { account_id: 1 }, - }); - - expect(mockDispatch).not.toHaveBeenCalledWith( - 'conversationUnreadCounts/get' - ); - }); - it('should throttle unread count refetches for repeated events', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-01-01T00:00:00Z')); diff --git a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js index 0503c0806..edffed4fd 100644 --- a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js +++ b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js @@ -48,9 +48,6 @@ export const actions = { // Ignore errors so the sidebar can continue rendering without badges. } }, - clear({ commit }) { - commit(types.SET_CONVERSATION_UNREAD_COUNTS, {}); - }, }; export const mutations = { diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js index 3100cdd10..0974689d5 100644 --- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js @@ -39,15 +39,4 @@ describe('#actions', () => { expect(commit).not.toHaveBeenCalled(); }); }); - - describe('#clear', () => { - it('clears unread counts', () => { - actions.clear({ commit }); - - expect(commit).toHaveBeenCalledWith( - types.SET_CONVERSATION_UNREAD_COUNTS, - {} - ); - }); - }); }); diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index 3bc221504..86fba29a0 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -92,7 +92,7 @@ class ActionCableListener < BaseListener def conversation_unread_count_changed(event) account, inbox_members = ::Conversations::UnreadCounts::BroadcastScope.new(event).perform - return if account.blank? || !account.feature_enabled?('conversation_unread_counts') + return if account.blank? tokens = user_tokens(account, inbox_members) diff --git a/app/models/account.rb b/app/models/account.rb index 667058a2f..8d66acca8 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -109,7 +109,6 @@ class Account < ApplicationRecord before_validation :validate_limit_keys after_create_commit :notify_creation - after_update_commit :clear_unread_conversation_counts_cache, if: :saved_change_to_feature_conversation_unread_counts? after_destroy :remove_account_sequences def agents diff --git a/app/services/conversations/unread_counts/listener.rb b/app/services/conversations/unread_counts/listener.rb index 28792d884..729613639 100644 --- a/app/services/conversations/unread_counts/listener.rb +++ b/app/services/conversations/unread_counts/listener.rb @@ -4,7 +4,6 @@ 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') refresh(message.conversation) end @@ -36,7 +35,7 @@ class Conversations::UnreadCounts::Listener < BaseListener return if conversation_data.blank? account = Account.find_by(id: conversation_data[:account_id]) - return unless account&.feature_enabled?('conversation_unread_counts') + return if account.blank? 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) diff --git a/app/services/conversations/unread_counts/notifier.rb b/app/services/conversations/unread_counts/notifier.rb index 652fbde3a..461945f2c 100644 --- a/app/services/conversations/unread_counts/notifier.rb +++ b/app/services/conversations/unread_counts/notifier.rb @@ -9,8 +9,6 @@ class Conversations::UnreadCounts::Notifier end def perform - return false unless conversation.account.feature_enabled?('conversation_unread_counts') - 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) diff --git a/config/features.yml b/config/features.yml index 03105588b..8444b7379 100644 --- a/config/features.yml +++ b/config/features.yml @@ -19,8 +19,9 @@ help_url: https://chwt.app/hc/fb - name: conversation_unread_counts display_name: Conversation Unread Counts - enabled: false + enabled: true chatwoot_internal: true + deprecated: true - name: ip_lookup display_name: IP Lookup enabled: false diff --git a/config/locales/en.yml b/config/locales/en.yml index b11a4fdae..ed52f27d8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -81,9 +81,6 @@ en: saml: feature_not_enabled: SAML feature not enabled for this account sso_not_enabled: SAML SSO is not enabled for this installation - conversations: - unread_counts: - feature_not_enabled: Conversation unread counts feature not enabled for this account data_import: data_type: invalid: Invalid data type diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index f8fd446d2..39bdb398c 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -126,47 +126,32 @@ RSpec.describe 'Conversations API', type: :request do Conversations::UnreadCounts::Store.clear_account!(account.id) end - context 'when conversation unread counts feature is enabled' do - before do - account.enable_features!(:conversation_unread_counts) - end + it 'returns unread conversation counts scoped to the signed-in user' do + create_unread_conversation(account: account, inbox: visible_inbox, labels: [label.title]) + create_unread_conversation(account: account, inbox: hidden_inbox, labels: [label.title]) - it 'returns unread conversation counts scoped to the signed-in user' do - create_unread_conversation(account: account, inbox: visible_inbox, labels: [label.title]) - create_unread_conversation(account: account, inbox: hidden_inbox, labels: [label.title]) - - 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 eq( - 'inboxes' => { visible_inbox.id.to_s => 1 }, - 'labels' => { label.id.to_s => 1 }, - 'teams' => {} - ) - end - - it 'returns unread team conversation counts scoped to the signed-in user' do - create_unread_conversation(account: account, inbox: visible_inbox, team: team) - create_unread_conversation(account: account, inbox: hidden_inbox, team: team) - - 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']['teams']).to eq(team.id.to_s => 1) - end - end - - it 'returns forbidden when conversation unread counts feature is disabled' do get "/api/v1/accounts/#{account.id}/conversations/unread_counts", headers: agent.create_new_auth_token, as: :json - expect(response).to have_http_status(:forbidden) - expect(response.parsed_body['error']).to eq('Conversation unread counts feature not enabled for this account') + expect(response).to have_http_status(:success) + expect(response.parsed_body['payload']).to eq( + 'inboxes' => { visible_inbox.id.to_s => 1 }, + 'labels' => { label.id.to_s => 1 }, + 'teams' => {} + ) + end + + it 'returns unread team conversation counts scoped to the signed-in user' do + create_unread_conversation(account: account, inbox: visible_inbox, team: team) + create_unread_conversation(account: account, inbox: hidden_inbox, team: team) + + 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']['teams']).to eq(team.id.to_s => 1) end end end @@ -848,7 +833,6 @@ RSpec.describe 'Conversations API', type: :request do end it 'refreshes unread count cache when conversation is marked read' do - account.enable_features!(:conversation_unread_counts) conversation.update!(agent_last_seen_at: 1.hour.ago) create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming, created_at: 5.minutes.ago) Conversations::UnreadCounts::Builder.new(account).build_base! @@ -936,7 +920,6 @@ RSpec.describe 'Conversations API', type: :request do end it 'refreshes unread count cache when conversation is marked unread' do - account.enable_features!(:conversation_unread_counts) conversation.update!(agent_last_seen_at: 1.minute.from_now, assignee_last_seen_at: 1.minute.from_now) Conversations::UnreadCounts::Builder.new(account).build_base! diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index cdb9a93cb..f8d50060f 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -237,10 +237,6 @@ describe ActionCableListener do let!(:agent_without_inbox_access) { create(:user, account: account, role: :agent) } let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) } - before do - account.enable_features!(:conversation_unread_counts) - end - it 'sends a lightweight refresh event to inbox agents and admins' do expect(conversation.inbox.reload.inbox_members.count).to eq(1) @@ -265,14 +261,6 @@ describe ActionCableListener do listener.conversation_unread_count_changed(event) end - it 'does not broadcast when conversation unread counts feature is disabled' do - account.disable_features!(:conversation_unread_counts) - - expect(ActionCableBroadcastJob).not_to receive(:perform_later) - - listener.conversation_unread_count_changed(event) - end - it 'supports deleted conversation data' do event = Events::Base.new( event_name, diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 38ca9694a..76dbbcba2 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -50,44 +50,6 @@ RSpec.describe Account do end end - describe 'conversation unread counts feature flag' do - let(:account) { create(:account) } - let(:inbox) { create(:inbox, account: account) } - let(:store) { Conversations::UnreadCounts::Store } - let(:inbox_key) { store.inbox_key(account.id, inbox.id) } - - after do - store.clear_account!(account.id) - end - - it 'clears unread count cache when the feature is enabled' do - build_unread_count_cache - - account.enable_features!(:conversation_unread_counts) - - 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 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(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.add_base_membership(account_id: account.id, inbox_id: inbox.id, label_ids: [], conversation_id: 1) - end - end - describe 'inbound_email_domain' do let(:account) { create(:account) } diff --git a/spec/services/conversations/unread_counts/listener_spec.rb b/spec/services/conversations/unread_counts/listener_spec.rb index fbb0a0835..a8a400365 100644 --- a/spec/services/conversations/unread_counts/listener_spec.rb +++ b/spec/services/conversations/unread_counts/listener_spec.rb @@ -11,7 +11,6 @@ RSpec.describe Conversations::UnreadCounts::Listener do end it 'refreshes unread counts when an incoming message is created' do - account.enable_features!(:conversation_unread_counts) message = create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming) event = Events::Base.new('message.created', Time.zone.now, message: message) @@ -30,17 +29,6 @@ RSpec.describe Conversations::UnreadCounts::Listener do expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new) end - it 'ignores incoming message creation when conversation unread counts are disabled' do - message = create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming) - event = Events::Base.new('message.created', Time.zone.now, message: message) - - expect(message).not_to receive(:conversation) - - listener.message_created(event) - - expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new) - end - it 'refreshes unread counts when conversation status changes' do changed_attributes = { 'status' => %w[open resolved] } event = Events::Base.new('conversation.status_changed', Time.zone.now, conversation: conversation, changed_attributes: changed_attributes) @@ -90,7 +78,6 @@ RSpec.describe Conversations::UnreadCounts::Listener do end it 'removes unread count memberships when a conversation is deleted' do - account.enable_features!(:conversation_unread_counts) label = create(:label, account: account) team = create(:team, account: account) assignee = create(:user, account: account) diff --git a/spec/services/conversations/unread_counts/notifier_spec.rb b/spec/services/conversations/unread_counts/notifier_spec.rb index 1b35d37f6..6c6028548 100644 --- a/spec/services/conversations/unread_counts/notifier_spec.rb +++ b/spec/services/conversations/unread_counts/notifier_spec.rb @@ -6,7 +6,6 @@ RSpec.describe Conversations::UnreadCounts::Notifier do let(:refresh_result) { true } before do - conversation.account.enable_features!(:conversation_unread_counts) allow(Conversations::UnreadCounts::Refresher).to receive(:new).and_return(refresher) allow(Rails.configuration.dispatcher).to receive(:dispatch) end @@ -30,19 +29,4 @@ RSpec.describe Conversations::UnreadCounts::Notifier do expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) end end - - context 'when conversation unread counts feature is disabled' do - before do - conversation.account.disable_features!(:conversation_unread_counts) - allow(Conversations::UnreadCounts::Store).to receive(:clear_account!) - end - - it 'does not refresh, clear cache, or dispatch unread count changed event' do - described_class.new(conversation).perform - - expect(Conversations::UnreadCounts::Refresher).not_to have_received(:new) - expect(Conversations::UnreadCounts::Store).not_to have_received(:clear_account!) - expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) - end - end end