feat: Ability to block contacts permanently (#8922)

Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2024-02-22 03:48:42 +05:30
committed by GitHub
parent c031cb19d2
commit ae4c8d818f
10 changed files with 63 additions and 28 deletions

View File

@@ -148,7 +148,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def permitted_params
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {})
end
def contact_custom_attributes

View File

@@ -78,7 +78,7 @@ describe('MoveActions', () => {
expect(window.bus.$emit).toBeCalledWith(
'newToastMessage',
'This conversation is muted for 6 hours',
'This contact is blocked successfully. You will not be notified of any future conversations.',
undefined
);
});
@@ -104,7 +104,7 @@ describe('MoveActions', () => {
expect(window.bus.$emit).toBeCalledWith(
'newToastMessage',
'This conversation is unmuted',
'This contact is unblocked successfully.',
undefined
);
});

View File

@@ -39,10 +39,10 @@
},
"MERGE_CONTACT": "Merge contact",
"CONTACT_ACTIONS": "Contact actions",
"MUTE_CONTACT": "Mute Conversation",
"UNMUTE_CONTACT": "Unmute Conversation",
"MUTED_SUCCESS": "This conversation is muted for 6 hours",
"UNMUTED_SUCCESS": "This conversation is unmuted",
"MUTE_CONTACT": "Block Contact",
"UNMUTE_CONTACT": "Unblock Contact",
"MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.",
"UNMUTED_SUCCESS": "This contact is unblocked successfully.",
"SEND_TRANSCRIPT": "Send Transcript",
"EDIT_LABEL": "Edit",
"SIDEBAR_SECTIONS": {

View File

@@ -3,26 +3,16 @@ module ConversationMuteHelpers
def mute!
resolved!
Redis::Alfred.setex(mute_key, 1, mute_period)
contact.update(blocked: true)
create_muted_message
end
def unmute!
Redis::Alfred.delete(mute_key)
contact.update(blocked: false)
create_unmuted_message
end
def muted?
Redis::Alfred.get(mute_key).present?
end
private
def mute_key
format(Redis::RedisKeys::CONVERSATION_MUTE_KEY, id: id)
end
def mute_period
6.hours
contact.blocked?
end
end

View File

@@ -6,6 +6,7 @@
#
# id :integer not null, primary key
# additional_attributes :jsonb
# blocked :boolean default(FALSE), not null
# contact_type :integer default("visitor")
# country_code :string default("")
# custom_attributes :jsonb
@@ -24,6 +25,7 @@
# Indexes
#
# index_contacts_on_account_id (account_id)
# index_contacts_on_blocked (blocked)
# index_contacts_on_lower_email_account_id (lower((email)::text), account_id)
# index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin
# index_contacts_on_nonempty_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))

View File

@@ -61,6 +61,7 @@ class Conversation < ApplicationRecord
validates :account_id, presence: true
validates :inbox_id, presence: true
validates :contact_id, presence: true
before_validation :validate_additional_attributes
validates :additional_attributes, jsonb_attributes_length: true
validates :custom_attributes, jsonb_attributes_length: true
@@ -103,7 +104,7 @@ class Conversation < ApplicationRecord
has_many :attachments, through: :messages
before_save :ensure_snooze_until_reset
before_create :mark_conversation_pending_if_bot
before_create :determine_conversation_status
before_create :ensure_waiting_since
after_update_commit :execute_after_update_commit_callbacks
@@ -226,7 +227,9 @@ class Conversation < ApplicationRecord
self.additional_attributes = {} unless additional_attributes.is_a?(Hash)
end
def mark_conversation_pending_if_bot
def determine_conversation_status
self.status = :resolved and return if contact.blocked?
# Message template hooks aren't executed for conversations from campaigns
# So making these conversations open for agent visibility
return if campaign.present?

View File

@@ -0,0 +1,6 @@
class AddBlockedToContacts < ActiveRecord::Migration[7.0]
def change
add_column :contacts, :blocked, :boolean, default: false, null: false
add_index :contacts, :blocked
end
end

View File

@@ -10,6 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -423,10 +424,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do
t.string "last_name", default: ""
t.string "location", default: ""
t.string "country_code", default: ""
t.boolean "blocked", default: false, null: false
t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id"
t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
t.index ["account_id"], name: "index_contacts_on_account_id"
t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
t.index ["blocked"], name: "index_contacts_on_blocked"
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
t.index ["name", "email", "phone_number", "identifier"], name: "index_contacts_on_name_email_phone_number_identifier", opclass: :gin_trgm_ops, using: :gin

View File

@@ -557,6 +557,27 @@ RSpec.describe 'Contacts API', type: :request do
expect(response).to have_http_status(:success)
expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(contact, 'http://example.com/avatar.png')
end
it 'allows blocking of contact' do
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: { blocked: true },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.blocked).to be(true)
end
it 'allows unblocking of contact' do
contact.update(blocked: true)
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
params: { blocked: false },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.blocked).to be(false)
end
end
end

View File

@@ -372,9 +372,9 @@ RSpec.describe Conversation do
expect(conversation.reload.resolved?).to be(true)
end
it 'marks conversation as muted in redis' do
it 'blocks the contact' do
mute!
expect(Redis::Alfred.get(conversation.send(:mute_key))).not_to be_nil
expect(conversation.reload.contact.blocked?).to be(true)
end
it 'creates mute message' do
@@ -400,10 +400,9 @@ RSpec.describe Conversation do
expect { unmute! }.not_to(change { conversation.reload.status })
end
it 'marks conversation as muted in redis' do
expect { unmute! }
.to change { Redis::Alfred.get(conversation.send(:mute_key)) }
.to nil
it 'unblocks the contact' do
unmute!
expect(conversation.reload.contact.blocked?).to be(false)
end
it 'creates unmute message' do
@@ -549,6 +548,17 @@ RSpec.describe Conversation do
end
end
describe 'when conversation is created by blocked contact' do
let(:account) { create(:account) }
let(:blocked_contact) { create(:contact, account: account, blocked: true) }
let(:inbox) { create(:inbox, account: account) }
it 'creates conversation in resolved state' do
conversation = create(:conversation, account: account, contact: blocked_contact, inbox: inbox)
expect(conversation.status).to eq('resolved')
end
end
describe '#botinbox: when conversation created inside inbox with agent bot' do
let!(:bot_inbox) { create(:agent_bot_inbox) }
let(:conversation) { create(:conversation, inbox: bot_inbox.inbox) }