Merge branch 'develop' into feat/new-auth-screens

This commit is contained in:
Muhsin Keloth 2022-10-17 13:00:22 +05:30 committed by GitHub
commit 0f79135f20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 581 additions and 421 deletions

View file

@ -7,8 +7,8 @@ end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
indent_style = spaces indent_style = space
tab_width = 2 tab_width = 2
[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] [*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
indent_size = 2 indent_size = 2

View file

@ -58,6 +58,6 @@ jobs:
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
push: true push: true
tags: ${{ env.DOCKER_TAG }} tags: ${{ env.DOCKER_TAG }}

5
.gitignore vendored
View file

@ -39,9 +39,6 @@ public/packs*
*.un~ *.un~
.jest-cache .jest-cache
#VS Code files
.vscode
# ignore jetbrains IDE files # ignore jetbrains IDE files
.idea .idea
@ -62,4 +59,4 @@ package-lock.json
test/cypress/videos/* test/cypress/videos/*
/config/master.key /config/master.key
/config/*.enc /config/*.enc

View file

@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb' - 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb' - 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
RSpec/ExampleLength: RSpec/ExampleLength:

32
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,32 @@
{
"recommendations": [
// Spell check
"streetsidesoftware.code-spell-checker",
// Better Comments
"aaron-bond.better-comments",
// Rails Test Runner
"davidpallinder.rails-test-runner",
// Eslint
"dbaeumer.vscode-eslint",
// Auto Close Tag
"formulahendry.auto-close-tag",
// Auto Rename Tag
"formulahendry.auto-rename-tag",
// Hight light colors
"naumovs.color-highlight",
// GitLens
"eamodio.gitlens",
// Ruby
"rebornix.ruby",
// Vue
"octref.vetur",
// Prettier
"esbenp.prettier-vscode",
// Dot Env
"mikestead.dotenv",
// HTML CSS Support
"ecmel.vscode-html-css",
// Tailwind CSS Intellisense
"bradlc.vscode-tailwindcss",
]
}

View file

@ -1,13 +1,12 @@
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
class ContactInboxBuilder class ContactInboxBuilder
pattr_initialize [:contact_id!, :inbox_id!, :source_id] pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
def perform def perform
@contact = Contact.find(contact_id) @source_id ||= generate_source_id
@inbox = @contact.account.inboxes.find(inbox_id) create_contact_inbox if source_id.present?
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
source_id = @source_id || generate_source_id
create_contact_inbox(source_id) if source_id.present?
end end
private private
@ -19,23 +18,37 @@ class ContactInboxBuilder
when 'Channel::Whatsapp' when 'Channel::Whatsapp'
wa_source_id wa_source_id
when 'Channel::Email' when 'Channel::Email'
@contact.email email_source_id
when 'Channel::Sms' when 'Channel::Sms'
@contact.phone_number phone_source_id
when 'Channel::Api' when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid SecureRandom.uuid
else
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
end end
end end
def email_source_id
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
@contact.email
end
def phone_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
@contact.phone_number
end
def wa_source_id def wa_source_id
return unless @contact.phone_number raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
# whatsapp doesn't want the + in e164 format # whatsapp doesn't want the + in e164 format
@contact.phone_number.delete('+').to_s @contact.phone_number.delete('+').to_s
end end
def twilio_source_id def twilio_source_id
return unless @contact.phone_number raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
case @inbox.channel.medium case @inbox.channel.medium
when 'sms' when 'sms'
@ -45,11 +58,11 @@ class ContactInboxBuilder
end end
end end
def create_contact_inbox(source_id) def create_contact_inbox
::ContactInbox.find_or_create_by!( ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: @contact.id, contact_id: @contact.id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: source_id source_id: @source_id
) )
end end
end end

View file

@ -1,25 +1,47 @@
class ContactBuilder # This Builder will create a contact and contact inbox with specified attributes.
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified] # If an existing identified contact exisits, it will be returned.
# for contact inbox logic it uses the contact inbox builder
class ContactInboxWithContactBuilder
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
def perform def perform
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) find_or_create_contact_and_contact_inbox
return contact_inbox if contact_inbox # in case of race conditions where contact is created by another thread
# we will try to find the contact and create a contact inbox
rescue ActiveRecord::RecordNotUnique
find_or_create_contact_and_contact_inbox
end
build_contact_inbox def find_or_create_contact_and_contact_inbox
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
return @contact_inbox if @contact_inbox
ActiveRecord::Base.transaction(requires_new: true) do
build_contact_with_contact_inbox
update_contact_avatar(@contact) unless @contact.avatar.attached?
@contact_inbox
end
end end
private private
def build_contact_with_contact_inbox
@contact = find_contact || create_contact
@contact_inbox = create_contact_inbox
end
def account def account
@account ||= inbox.account @account ||= inbox.account
end end
def create_contact_inbox(contact) def create_contact_inbox
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( ContactInboxBuilder.new(
contact_id: contact.id, contact: @contact,
inbox_id: inbox.id, inbox: @inbox,
source_id: source_id source_id: @source_id,
) hmac_verified: hmac_verified
).perform
end end
def update_contact_avatar(contact) def update_contact_avatar(contact)
@ -61,16 +83,4 @@ class ContactBuilder
account.contacts.find_by(phone_number: phone_number) account.contacts.find_by(phone_number: phone_number)
end end
def build_contact_inbox
ActiveRecord::Base.transaction do
contact = find_contact || create_contact
contact_inbox = create_contact_inbox(contact)
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
Rails.logger.error e
raise e
end
end
end end

View file

@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if @inbox.channel.reauthorization_required? return if @inbox.channel.reauthorization_required?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
build_contact build_contact_inbox
build_message build_message
end end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
rescue StandardError => e rescue StandardError => e
@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
private private
def contact def build_contact_inbox
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact @contact_inbox = ::ContactInboxWithContactBuilder.new(
end source_id: @sender_id,
inbox: @inbox,
def build_contact contact_attributes: contact_params
return if contact.present? ).perform
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
end end
def build_message def build_message
@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end end
end end
def ensure_contact_avatar
return if contact_params[:remote_avatar_url].blank?
return if @contact.avatar.attached?
Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
end
def conversation def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation @conversation ||= Conversation.find_by(conversation_params) || build_conversation
end end
def build_conversation def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge( Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id contact_inbox_id: @contact_inbox.id
)) ))
@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
account_id: @inbox.account_id, account_id: @inbox.account_id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
contact_id: contact.id contact_id: @contact_inbox.contact_id
} }
end end
@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type, message_type: @message_type,
content: response.content, content: response.content,
source_id: response.identifier, source_id: response.identifier,
sender: @outgoing_echo ? nil : contact sender: @outgoing_echo ? nil : @contact_inbox.contact
} }
end end
@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id, account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || '' avatar_url: result['profile_pic']
} }
end end

View file

@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
before_action :ensure_inbox, only: [:create] before_action :ensure_inbox, only: [:create]
def create def create
source_id = params[:source_id] || SecureRandom.uuid @contact_inbox = ContactInboxBuilder.new(
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id) contact: @contact,
inbox: @inbox,
source_id: params[:source_id]
).perform
end end
private private

View file

@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
return if params[:inbox_id].blank? return if params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id]) inbox = Current.account.inboxes.find(params[:inbox_id])
source_id = params[:source_id] || SecureRandom.uuid ContactInboxBuilder.new(
ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id) contact: @contact,
inbox: inbox,
source_id: params[:source_id]
).perform
end end
def permitted_params def permitted_params

View file

@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include DateRangeHelper include DateRangeHelper
before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :contact_inbox, only: [:create] before_action :inbox, :contact, :contact_inbox, only: [:create]
def index def index
result = conversation_finder.perform result = conversation_finder.perform
@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
authorize @conversation.inbox, :show? authorize @conversation.inbox, :show?
end end
def inbox
return if params[:inbox_id].blank?
@inbox = Current.account.inboxes.find(params[:inbox_id])
authorize @inbox, :show?
end
def contact
return if params[:contact_id].blank?
@contact = Current.account.contacts.find(params[:contact_id])
end
def contact_inbox def contact_inbox
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
# fallback for the old case where we do look up only using source id
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
# and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show? authorize @contact_inbox.inbox, :show?
end end
def build_contact_inbox def build_contact_inbox
return if params[:contact_id].blank? || params[:inbox_id].blank? return if @inbox.blank? || @contact.blank?
inbox = Current.account.inboxes.find(params[:inbox_id])
authorize inbox, :show?
ContactInboxBuilder.new( ContactInboxBuilder.new(
contact_id: params[:contact_id], contact: @contact,
inbox_id: inbox.id, inbox: @inbox,
source_id: params[:source_id] source_id: params[:source_id]
).perform ).perform
end end

View file

@ -13,6 +13,8 @@ module RequestExceptionHandler
render_not_found_error('Resource could not be found') render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action') render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
render_could_not_create_error(e.message)
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
Current.reset Current.reset

View file

@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
def create def create
source_id = params[:source_id] || SecureRandom.uuid source_id = params[:source_id] || SecureRandom.uuid
@contact_inbox = ::ContactBuilder.new( @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id, source_id: source_id,
inbox: @inbox_channel.inbox, inbox: @inbox_channel.inbox,
contact_attributes: permitted_params.except(:identifier, :identifier_hash) contact_attributes: permitted_params.except(:identifier, :identifier_hash)

View file

@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
} }
&.clear { &.clear {
color: var(--w-700);
&.secondary {
color: var(--s-700)
}
&.success {
color: var(--g-700)
}
&.alert {
color: var(--r-700)
}
&.warning { &.warning {
color: var(--y-600); color: var(--y-700)
} }
&:hover { &:hover {
@ -146,6 +159,8 @@ $default-button-height: 4.0rem;
&.small { &.small {
height: var(--space-large); height: var(--space-large);
padding-bottom: var(--space-smaller);
padding-top: var(--space-smaller);
} }
&.large { &.large {

View file

@ -14,15 +14,9 @@
} }
.modal--close { .modal--close {
border-radius: 50%;
color: $color-heading;
cursor: pointer;
font-size: $font-size-big;
line-height: $space-normal;
padding: $space-normal;
position: absolute; position: absolute;
right: $space-micro; right: $space-small;
top: $space-micro; top: $space-small;
&:hover { &:hover {
background: $color-background; background: $color-background;

View file

@ -7,9 +7,13 @@
@click="onBackDropClick" @click="onBackDropClick"
> >
<div :class="modalContainerClassName" @click.stop> <div :class="modalContainerClassName" @click.stop>
<button class="modal--close" @click="close"> <woot-button
<fluent-icon icon="dismiss" /> color-scheme="secondary"
</button> icon="dismiss"
variant="clear"
class="modal--close"
@click="close"
/>
<slot /> <slot />
</div> </div>
</div> </div>

View file

@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [
label: 'HELP_CENTER.TITLE', label: 'HELP_CENTER.TITLE',
featureFlag: 'help_center', featureFlag: 'help_center',
toState: frontendURL(`accounts/${accountId}/portals`), toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'list_all_portals', toStateName: 'default_portal_articles',
roles: ['administrator'], roles: ['administrator'],
}, },
{ {

View file

@ -78,7 +78,7 @@ export default {
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) { if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
initials = initials.replace(/[a-z]+/g, ''); initials = initials.replace(/[a-z]+/g, '');
} }
initials = initials.substr(0, 2).toUpperCase(); initials = initials.substring(0, 2).toUpperCase();
return initials; return initials;
}, },
}, },

View file

@ -420,6 +420,8 @@ export default {
<style lang="scss"> <style lang="scss">
.wrap { .wrap {
> .bubble { > .bubble {
min-width: 128px;
&.is-image, &.is-image,
&.is-video { &.is-video {
padding: 0; padding: 0;

View file

@ -477,7 +477,7 @@ export default {
const hasNextWord = updatedMessage.includes(' '); const hasNextWord = updatedMessage.includes(' ');
const isShortCodeActive = this.hasSlashCommand && !hasNextWord; const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
if (isShortCodeActive) { if (isShortCodeActive) {
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length); this.mentionSearchKey = updatedMessage.substring(1);
this.showMentions = true; this.showMentions = true;
} else { } else {
this.mentionSearchKey = ''; this.mentionSearchKey = '';

View file

@ -12,12 +12,12 @@
"404": "Šim kontam nav piesaistīts neviens aģents", "404": "Šim kontam nav piesaistīts neviens aģents",
"TITLE": "Pārvaldīt Jūsu komandas aģentus", "TITLE": "Pārvaldīt Jūsu komandas aģentus",
"DESC": "Jūs varat pievienot/noņemt aģentus pie/no savas komandas.", "DESC": "Jūs varat pievienot/noņemt aģentus pie/no savas komandas.",
"NAME": "Vārds", "NAME": "Nosaukums",
"EMAIL": "e-pasts", "EMAIL": "Epasts",
"STATUS": "Statuss", "STATUS": "Statuss",
"ACTIONS": "Darbības", "ACTIONS": "Darbības",
"VERIFIED": "Pārbaudīts", "VERIFIED": "Pārbaudīts",
"VERIFICATION_PENDING": "Gaida apstiprinājumu" "VERIFICATION_PENDING": "Tiek gaidīta verifikācija"
}, },
"ADD": { "ADD": {
"TITLE": "Pievienot aģentu Jūsu komandai", "TITLE": "Pievienot aģentu Jūsu komandai",

View file

@ -81,7 +81,7 @@
}, },
"LIST": { "LIST": {
"TABLE_HEADER": [ "TABLE_HEADER": [
"Vārds", "Nosaukums",
"Apraksts", "Apraksts",
"Tips", "Tips",
"Atslēga" "Atslēga"

View file

@ -40,7 +40,7 @@
}, },
"LIST": { "LIST": {
"TABLE_HEADER": [ "TABLE_HEADER": [
"Vārds", "Nosaukums",
"Apraksts", "Apraksts",
"Aktīvs", "Aktīvs",
"Izveidots" "Izveidots"

View file

@ -202,7 +202,7 @@
"404": "Neviena kontaktpersona neatbilst jūsu meklēšanas vaicājumam 🔍", "404": "Neviena kontaktpersona neatbilst jūsu meklēšanas vaicājumam 🔍",
"NO_CONTACTS": "Nav pieejamu kontaktpersonu", "NO_CONTACTS": "Nav pieejamu kontaktpersonu",
"TABLE_HEADER": { "TABLE_HEADER": {
"NAME": "Vārds", "NAME": "Nosaukums",
"PHONE_NUMBER": "Telefona Numurs", "PHONE_NUMBER": "Telefona Numurs",
"CONVERSATIONS": "Sarunas", "CONVERSATIONS": "Sarunas",
"LAST_ACTIVITY": "Pēdējās Darbības", "LAST_ACTIVITY": "Pēdējās Darbības",

View file

@ -26,7 +26,7 @@
"days_before": "Ir x dienas pirms" "days_before": "Ir x dienas pirms"
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"NAME": "Vārds", "NAME": "Nosaukums",
"EMAIL": "E-pasts", "EMAIL": "E-pasts",
"PHONE_NUMBER": "Tālruņa numurs", "PHONE_NUMBER": "Tālruņa numurs",
"IDENTIFIER": "Identifikators", "IDENTIFIER": "Identifikators",

View file

@ -151,7 +151,7 @@
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "Kopēt", "COPY": "Kopēt",
"DELETE": "Dzēst", "DELETE": "Dzēst",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses" "CREATE_A_CANNED_RESPONSE": "Pievienot sagatavotajām atbildēm"
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {

View file

@ -71,7 +71,7 @@
"LOADING_MESSAGE": "Notiek paziņojumu ielāde...", "LOADING_MESSAGE": "Notiek paziņojumu ielāde...",
"404": "Nav paziņojumu", "404": "Nav paziņojumu",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Vārds", "Nosaukums",
"Telefona numurs", "Telefona numurs",
"Sarunas", "Sarunas",
"Pēdējā Sazināšanās" "Pēdējā Sazināšanās"

View file

@ -92,7 +92,7 @@
"PORTAL_CONFIG": { "PORTAL_CONFIG": {
"TITLE": "Portāla Konfigurācijas", "TITLE": "Portāla Konfigurācijas",
"ITEMS": { "ITEMS": {
"NAME": "Vārds", "NAME": "Nosaukums",
"DOMAIN": "Pielāgots domēns", "DOMAIN": "Pielāgots domēns",
"SLUG": "Slug", "SLUG": "Slug",
"TITLE": "Portāla nosaukums", "TITLE": "Portāla nosaukums",
@ -144,7 +144,7 @@
"TITLE": "Kategorijas iekš", "TITLE": "Kategorijas iekš",
"NEW_CATEGORY": "Jauna kategorija", "NEW_CATEGORY": "Jauna kategorija",
"TABLE": { "TABLE": {
"NAME": "Vārds", "NAME": "Nosaukums",
"DESCRIPTION": "Apraksts", "DESCRIPTION": "Apraksts",
"LOCALE": "Lokalizācija", "LOCALE": "Lokalizācija",
"ARTICLE_COUNT": "Rakstu skaits", "ARTICLE_COUNT": "Rakstu skaits",
@ -204,7 +204,7 @@
"HELP_TEXT": "Šis logotips tiks attēlots portāla galvenē." "HELP_TEXT": "Šis logotips tiks attēlots portāla galvenē."
}, },
"NAME": { "NAME": {
"LABEL": "Vārds", "LABEL": "Nosaukums",
"PLACEHOLDER": "Portāla nosaukums", "PLACEHOLDER": "Portāla nosaukums",
"HELP_TEXT": "Nosaukums tiks izmantots publiskajā portālā iekšēji.", "HELP_TEXT": "Nosaukums tiks izmantots publiskajā portālā iekšēji.",
"ERROR": "Nepieciešams nosaukums" "ERROR": "Nepieciešams nosaukums"
@ -344,7 +344,7 @@
"PORTAL": "Portāls", "PORTAL": "Portāls",
"LOCALE": "Lokalizācija", "LOCALE": "Lokalizācija",
"NAME": { "NAME": {
"LABEL": "Vārds", "LABEL": "Nosaukums",
"PLACEHOLDER": "Kategorijas nosaukums", "PLACEHOLDER": "Kategorijas nosaukums",
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.", "HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
"ERROR": "Nepieciešams nosaukums" "ERROR": "Nepieciešams nosaukums"
@ -375,7 +375,7 @@
"PORTAL": "Portāls", "PORTAL": "Portāls",
"LOCALE": "Lokalizācija", "LOCALE": "Lokalizācija",
"NAME": { "NAME": {
"LABEL": "Vārds", "LABEL": "Nosaukums",
"PLACEHOLDER": "Kategorijas nosaukums", "PLACEHOLDER": "Kategorijas nosaukums",
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.", "HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
"ERROR": "Nepieciešams nosaukums" "ERROR": "Nepieciešams nosaukums"

View file

@ -239,7 +239,9 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Atzvanīšanas URL", "TITLE": "Atzvanīšanas URL",
"SUBTITLE": "Jums ir facebook izstrādātāju portālā jānokonfigurē webhook URL, izmantojot šeit minēto URL." "SUBTITLE": "Jums Facebook izstrādātāju portālā ir jānokonfigurē webhoot URL un verifikācijas token ar tālāk norādītajām vērtībām.",
"WEBHOOK_URL": "Webhook URL",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verifikācijas Token"
}, },
"SUBMIT_BUTTON": "Izveidot WhatsApp kanālu", "SUBMIT_BUTTON": "Izveidot WhatsApp kanālu",
"API": { "API": {
@ -357,7 +359,7 @@
}, },
"FINISH": { "FINISH": {
"TITLE": "Jūsu Iesūtne ir gatava!", "TITLE": "Jūsu Iesūtne ir gatava!",
"MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu ", "MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu",
"BUTTON_TEXT": "Iet uz", "BUTTON_TEXT": "Iet uz",
"MORE_SETTINGS": "Papildu iestatījumi", "MORE_SETTINGS": "Papildu iestatījumi",
"WEBSITE_SUCCESS": "Jūs esat veiksmīgi pabeidzis tīmekļa vietnes kanāla izveidi. Nokopējiet tālāk redzamo kodu un ievietojiet to savā tīmekļa vietnē. Nākamreiz, kad klients izmantos tiešsaistes tērzēšanu, saruna automātiski tiks parādīta Jūsu iesūtnē." "WEBSITE_SUCCESS": "Jūs esat veiksmīgi pabeidzis tīmekļa vietnes kanāla izveidi. Nokopējiet tālāk redzamo kodu un ievietojiet to savā tīmekļa vietnē. Nākamreiz, kad klients izmantos tiešsaistes tērzēšanu, saruna automātiski tiks parādīta Jūsu iesūtnē."

View file

@ -94,14 +94,14 @@
"404": "Šajā kontā vēl nav nokonfigurēta neviena informācijas paneļa lietotne", "404": "Šajā kontā vēl nav nokonfigurēta neviena informācijas paneļa lietotne",
"LOADING": "Notiek informācijas paneļa lietotņu iegūšana...", "LOADING": "Notiek informācijas paneļa lietotņu iegūšana...",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Vārds", "Nosaukums",
"Endpoint" "Endpoint"
], ],
"EDIT_TOOLTIP": "Rediģēt lietotni", "EDIT_TOOLTIP": "Rediģēt lietotni",
"DELETE_TOOLTIP": "Dzēst lietotni" "DELETE_TOOLTIP": "Dzēst lietotni"
}, },
"FORM": { "FORM": {
"TITLE_LABEL": "Vārds", "TITLE_LABEL": "Nosaukums",
"TITLE_PLACEHOLDER": "Ievadiet informācijas paneļa lietotnes nosaukumu", "TITLE_PLACEHOLDER": "Ievadiet informācijas paneļa lietotnes nosaukumu",
"TITLE_ERROR": "Informācijas paneļa lietotnei ir jānorāda nosaukums", "TITLE_ERROR": "Informācijas paneļa lietotnei ir jānorāda nosaukums",
"URL_LABEL": "Endpoint", "URL_LABEL": "Endpoint",

View file

@ -10,7 +10,7 @@
"TITLE": "Pārvaldīt Etiķetes", "TITLE": "Pārvaldīt Etiķetes",
"DESC": "Etiķetes ļauj grupēt sarunas kopā.", "DESC": "Etiķetes ļauj grupēt sarunas kopā.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Vārds", "Nosaukums",
"Apraksts", "Apraksts",
"Krāsa" "Krāsa"
] ]

View file

@ -179,6 +179,7 @@
"CONTACTS": "Kontaktpersonas", "CONTACTS": "Kontaktpersonas",
"HOME": "Sākums", "HOME": "Sākums",
"AGENTS": "Aģenti", "AGENTS": "Aģenti",
"AGENT_BOTS": "Bots",
"INBOXES": "Iesūtnes", "INBOXES": "Iesūtnes",
"NOTIFICATIONS": "Paziņojumi", "NOTIFICATIONS": "Paziņojumi",
"CANNED_RESPONSES": "Sagatavotās Atbildes", "CANNED_RESPONSES": "Sagatavotās Atbildes",
@ -189,6 +190,7 @@
"LABELS": "Etiķetes", "LABELS": "Etiķetes",
"CUSTOM_ATTRIBUTES": "Pielāgotas Īpašības", "CUSTOM_ATTRIBUTES": "Pielāgotas Īpašības",
"AUTOMATION": "Automatizācija", "AUTOMATION": "Automatizācija",
"MACROS": "Macros",
"TEAMS": "Komandas", "TEAMS": "Komandas",
"BILLING": "Norēķini", "BILLING": "Norēķini",
"CUSTOM_VIEWS_FOLDER": "Mapes", "CUSTOM_VIEWS_FOLDER": "Mapes",

View file

@ -69,7 +69,7 @@
}, },
"AGENTS": { "AGENTS": {
"AGENT": "AĢENTS", "AGENT": "AĢENTS",
"EMAIL": "e-pasts", "EMAIL": "Epasts",
"BUTTON_TEXT": "Pievienot aģentus", "BUTTON_TEXT": "Pievienot aģentus",
"ADD_AGENTS": "Notiek aģentu pievienošana Jūsu komandai...", "ADD_AGENTS": "Notiek aģentu pievienošana Jūsu komandai...",
"SELECT": "izvēlēties", "SELECT": "izvēlēties",

View file

@ -2,9 +2,11 @@
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas", "CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas",
"AGENT_SELECT_LABEL": "Selecione Agente", "AGENT_SELECT_LABEL": "Selecione Agente",
"ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que deseja atribuir %{conversationCount} %{conversationLabel} para", "ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer atribuir %{conversationCount} %{conversationLabel} para",
"UNASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer remover a atribuição de %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Voltar atrás", "GO_BACK_LABEL": "Voltar atrás",
"ASSIGN_LABEL": "Atribua", "ASSIGN_LABEL": "Atribua",
"YES": "Sim",
"ASSIGN_AGENT_TOOLTIP": "Atribuir Agente", "ASSIGN_AGENT_TOOLTIP": "Atribuir Agente",
"ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso", "ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso",
"ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente", "ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente",

View file

@ -12,9 +12,9 @@
</woot-modal> </woot-modal>
<woot-button <woot-button
icon="more-vertical" icon="more-vertical"
class="button--delete-message"
color-scheme="secondary" color-scheme="secondary"
variant="link" variant="clear"
size="small"
@click="handleContextMenuClick" @click="handleContextMenuClick"
/> />
<div <div
@ -40,6 +40,7 @@
variant="clear" variant="clear"
size="small" size="small"
icon="clipboard" icon="clipboard"
color-scheme="secondary"
@click="handleCopy" @click="handleCopy"
> >
{{ $t('CONVERSATION.CONTEXT_MENU.COPY') }} {{ $t('CONVERSATION.CONTEXT_MENU.COPY') }}
@ -52,6 +53,7 @@
variant="clear" variant="clear"
size="small" size="small"
icon="comment-add" icon="comment-add"
color-scheme="secondary"
@click="showCannedResponseModal" @click="showCannedResponseModal"
> >
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }} {{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }}
@ -132,12 +134,12 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-pane { .dropdown-pane {
bottom: var(--space-medium); bottom: var(--space-large);
} }
.dropdown-pane--left { .dropdown-pane--left {
right: var(--space-small); right: var(--space-minus-small);
} }
.dropdown-pane--right { .dropdown-pane--right {
left: var(--space-small); left: var(--space-minus-small);
} }
</style> </style>

View file

@ -10,10 +10,11 @@
<woot-button <woot-button
v-if="showCopy" v-if="showCopy"
type="submit" type="submit"
variant="link" variant="clear"
size="tiny"
color-scheme="secondary" color-scheme="secondary"
icon="clipboard" icon="clipboard"
class-names="icon copy-icon" class-names="copy-icon"
@click="onCopy" @click="onCopy"
/> />
</a> </a>

View file

@ -239,7 +239,7 @@ export default {
const hasNextWord = value.includes(' '); const hasNextWord = value.includes(' ');
const isShortCodeActive = this.hasSlashCommand && !hasNextWord; const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
if (isShortCodeActive) { if (isShortCodeActive) {
this.cannedResponseSearchKey = value.substr(1, value.length); this.cannedResponseSearchKey = value.substring(1);
this.showCannedResponseMenu = true; this.showCannedResponseMenu = true;
} else { } else {
this.cannedResponseSearchKey = ''; this.cannedResponseSearchKey = '';

View file

@ -16,7 +16,12 @@
</div> </div>
</td> </td>
<td> <td>
<span class="fs-small">{{ category.name }}</span> <router-link
class="fs-small button clear link secondary"
:to="getCategoryRoute(category.slug)"
>
{{ category.name }}
</router-link>
</td> </td>
<td> <td>
<span class="fs-small"> <span class="fs-small">
@ -43,6 +48,8 @@
<script> <script>
import timeMixin from 'dashboard/mixins/time'; import timeMixin from 'dashboard/mixins/time';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default { export default {
mixins: [timeMixin, portalMixin], mixins: [timeMixin, portalMixin],
@ -97,6 +104,14 @@ export default {
} }
}, },
}, },
methods: {
getCategoryRoute(categorySlug) {
const { portalSlug, locale } = this.$route.params;
return frontendURL(
`accounts/${this.accountId}/portals/${portalSlug}/${locale}/categories/${categorySlug}`
);
},
},
}; };
</script> </script>

View file

@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue'; import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import AddCategory from '../pages/categories/AddCategory'; import AddCategory from '../pages/categories/AddCategory';
@ -72,7 +73,7 @@ export default {
PortalPopover, PortalPopover,
AddCategory, AddCategory,
}, },
mixins: [portalMixin], mixins: [portalMixin, uiSettingsMixin],
data() { data() {
return { return {
isSidebarOpen: false, isSidebarOpen: false,
@ -231,7 +232,13 @@ export default {
}, },
updated() { updated() {
const slug = this.$route.params.portalSlug; const slug = this.$route.params.portalSlug;
if (slug) this.lastActivePortalSlug = slug; if (slug) {
this.lastActivePortalSlug = slug;
this.updateUISettings({
last_active_portal_slug: slug,
last_active_locale_code: this.selectedLocaleInPortal,
});
}
}, },
methods: { methods: {
handleResize() { handleResize() {

View file

@ -188,13 +188,15 @@
<script> <script>
import thumbnail from 'dashboard/components/widgets/Thumbnail'; import thumbnail from 'dashboard/components/widgets/Thumbnail';
import LocaleItemTable from './PortalListItemTable'; import LocaleItemTable from './PortalListItemTable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
components: { components: {
thumbnail, thumbnail,
LocaleItemTable, LocaleItemTable,
}, },
mixins: [alertMixin], mixins: [alertMixin, uiSettingsMixin],
props: { props: {
portal: { portal: {
type: Object, type: Object,
@ -274,6 +276,10 @@ export default {
this.alertMessage = this.$t( this.alertMessage = this.$t(
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS' 'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
); );
this.updateUISettings({
last_active_portal_slug: undefined,
last_active_locale_code: undefined,
});
} catch (error) { } catch (error) {
this.alertMessage = this.alertMessage =
error?.message || error?.message ||

View file

@ -21,12 +21,20 @@ const EditCategory = () => import('./pages/categories/EditCategory');
const ListCategoryArticles = () => const ListCategoryArticles = () =>
import('./pages/articles/ListCategoryArticles'); import('./pages/articles/ListCategoryArticles');
const ListAllArticles = () => import('./pages/articles/ListAllArticles'); const ListAllArticles = () => import('./pages/articles/ListAllArticles');
const DefaultPortalArticles = () =>
import('./pages/articles/DefaultPortalArticles');
const NewArticle = () => import('./pages/articles/NewArticle'); const NewArticle = () => import('./pages/articles/NewArticle');
const EditArticle = () => import('./pages/articles/EditArticle'); const EditArticle = () => import('./pages/articles/EditArticle');
const portalRoutes = [ const portalRoutes = [
{ {
path: getPortalRoute(''), path: getPortalRoute(''),
name: 'default_portal_articles',
roles: ['administrator', 'agent'],
component: DefaultPortalArticles,
},
{
path: getPortalRoute('all'),
name: 'list_all_portals', name: 'list_all_portals',
roles: ['administrator', 'agent'], roles: ['administrator', 'agent'],
component: ListAllPortals, component: ListAllPortals,

View file

@ -0,0 +1,31 @@
<template>
<div>Loading...</div>
</template>
<script>
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
mixins: [uiSettingsMixin],
mounted() {
const {
last_active_portal_slug: lastActivePortalSlug,
last_active_locale_code: lastActiveLocaleCode,
} = this.uiSettings || {};
if (lastActivePortalSlug)
this.$router.push({
name: 'list_all_locale_articles',
params: {
portalSlug: lastActivePortalSlug,
locale: lastActiveLocaleCode,
},
replace: true,
});
else
this.$router.push({
name: 'list_all_portals',
replace: true,
});
},
};
</script>

View file

@ -19,15 +19,11 @@
:script="currentInbox.callback_webhook_url" :script="currentInbox.callback_webhook_url"
/> />
</div> </div>
<div class="medium-6 small-offset-3"> <div v-if="isWhatsAppCloudInbox" class="medium-6 small-offset-3">
<p class="config--label"> <p class="config--label">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }} {{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}
</p> </p>
<woot-code <woot-code lang="html" :script="currentInbox.callback_webhook_url" />
v-if="isWhatsAppCloudInbox"
lang="html"
:script="currentInbox.callback_webhook_url"
/>
<p class="config--label"> <p class="config--label">
{{ {{
$t( $t(
@ -36,7 +32,6 @@
}} }}
</p> </p>
<woot-code <woot-code
v-if="isWhatsAppCloudInbox"
lang="html" lang="html"
:script="currentInbox.provider_config.webhook_verify_token" :script="currentInbox.provider_config.webhook_verify_token"
/> />

View file

@ -243,7 +243,7 @@ export default {
this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', { this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', {
options: JSON.stringify(options), options: JSON.stringify(options),
}) + }) +
script.substring(13, script.length) script.substring(13)
); );
}, },
getWidgetViewOptions() { getWidgetViewOptions() {

View file

@ -10,7 +10,7 @@ import {
getUserCookieName, getUserCookieName,
hasUserKeys, hasUserKeys,
} from '../sdk/cookieHelpers'; } from '../sdk/cookieHelpers';
import { addClass, removeClass } from '../sdk/DOMHelpers'; import { addClasses, removeClasses } from '../sdk/DOMHelpers';
import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents'; import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents';
const runSDK = ({ baseUrl, websiteToken }) => { const runSDK = ({ baseUrl, websiteToken }) => {
if (window.$chatwoot) { if (window.$chatwoot) {
@ -41,12 +41,12 @@ const runSDK = ({ baseUrl, websiteToken }) => {
let widgetElm = document.querySelector('.woot--bubble-holder'); let widgetElm = document.querySelector('.woot--bubble-holder');
let widgetHolder = document.querySelector('.woot-widget-holder'); let widgetHolder = document.querySelector('.woot-widget-holder');
if (visibility === 'hide') { if (visibility === 'hide') {
addClass(widgetHolder, 'woot-widget--without-bubble'); addClasses(widgetHolder, 'woot-widget--without-bubble');
addClass(widgetElm, 'woot-hidden'); addClasses(widgetElm, 'woot-hidden');
window.$chatwoot.hideMessageBubble = true; window.$chatwoot.hideMessageBubble = true;
} else if (visibility === 'show') { } else if (visibility === 'show') {
removeClass(widgetElm, 'woot-hidden'); removeClasses(widgetElm, 'woot-hidden');
removeClass(widgetHolder, 'woot-widget--without-bubble'); removeClasses(widgetHolder, 'woot-widget--without-bubble');
window.$chatwoot.hideMessageBubble = false; window.$chatwoot.hideMessageBubble = false;
} }
IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, { IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, {

View file

@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper';
export const loadCSS = () => { export const loadCSS = () => {
const css = document.createElement('style'); const css = document.createElement('style');
css.type = 'text/css';
css.innerHTML = `${SDK_CSS}`; css.innerHTML = `${SDK_CSS}`;
document.body.appendChild(css); document.body.appendChild(css);
}; };
export const wootOn = (elm, event, fn) => { export const addClasses = (elm, classes) => {
if (document.addEventListener) { elm.classList.add(...classes.split(' '));
elm.addEventListener(event, fn, false);
} else if (document.attachEvent) {
// <= IE 8 loses scope so need to apply, we add this to object so we
// can detach later (can't detach anonymous functions)
// eslint-disable-next-line
elm[event + fn] = function() {
// eslint-disable-next-line
return fn.apply(elm, arguments);
};
elm.attachEvent(`on${event}`, elm[event + fn]);
}
};
export const classHelper = (classes, action, elm) => {
let search;
let replace;
let i;
let has = false;
if (classes) {
// Trim any whitespace
const classarray = classes.split(/\s+/);
for (i = 0; i < classarray.length; i += 1) {
search = new RegExp(`\\b${classarray[i]}\\b`, 'g');
replace = new RegExp(` *${classarray[i]}\\b`, 'g');
if (action === 'remove') {
// eslint-disable-next-line
elm.className = elm.className.replace(replace, '');
} else if (action === 'toggle') {
// eslint-disable-next-line
elm.className = elm.className.match(search)
? elm.className.replace(replace, '')
: `${elm.className} ${classarray[i]}`;
} else if (action === 'has') {
if (elm.className.match(search)) {
has = true;
break;
}
}
}
}
return has;
};
export const addClass = (elm, classes) => {
if (classes) {
elm.className += ` ${classes}`;
}
}; };
export const toggleClass = (elm, classes) => { export const toggleClass = (elm, classes) => {
classHelper(classes, 'toggle', elm); elm.classList.toggle(classes);
}; };
export const removeClass = (elm, classes) => { export const removeClasses = (elm, classes) => {
classHelper(classes, 'remove', elm); elm.classList.remove(...classes.split(' '));
}; };
export const onLocationChange = ({ referrerURL, referrerHost }) => { export const onLocationChange = ({ referrerURL, referrerHost }) => {

View file

@ -1,9 +1,8 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { import {
wootOn, addClasses,
addClass,
loadCSS, loadCSS,
removeClass, removeClasses,
onLocationChangeListener, onLocationChangeListener,
} from './DOMHelpers'; } from './DOMHelpers';
import { import {
@ -68,7 +67,7 @@ export const IFrameHelper = {
holderClassName += ` woot-widget-holder--flat`; holderClassName += ` woot-widget-holder--flat`;
} }
addClass(widgetHolder, holderClassName); addClasses(widgetHolder, holderClassName);
widgetHolder.appendChild(iframe); widgetHolder.appendChild(iframe);
body.appendChild(widgetHolder); body.appendChild(widgetHolder);
IFrameHelper.initPostMessageCommunication(); IFrameHelper.initPostMessageCommunication();
@ -99,7 +98,7 @@ export const IFrameHelper = {
}; };
}, },
initWindowSizeListener: () => { initWindowSizeListener: () => {
wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton()); window.addEventListener('resize', () => IFrameHelper.toggleCloseButton());
}, },
preventDefaultScroll: () => { preventDefaultScroll: () => {
widgetHolder.addEventListener('wheel', event => { widgetHolder.addEventListener('wheel', event => {
@ -241,9 +240,9 @@ export const IFrameHelper = {
event.unreadMessageCount > 0 && event.unreadMessageCount > 0 &&
!bubbleElement.classList.contains('unread-notification') !bubbleElement.classList.contains('unread-notification')
) { ) {
addClass(bubbleElement, 'unread-notification'); addClasses(bubbleElement, 'unread-notification');
} else if (event.unreadMessageCount === 0) { } else if (event.unreadMessageCount === 0) {
removeClass(bubbleElement, 'unread-notification'); removeClasses(bubbleElement, 'unread-notification');
} }
}, },
@ -284,7 +283,7 @@ export const IFrameHelper = {
target: chatBubble, target: chatBubble,
}); });
addClass(closeBubble, closeBtnClassName); addClasses(closeBubble, closeBtnClassName);
chatIcon.style.background = widgetColor; chatIcon.style.background = widgetColor;
closeBubble.style.background = widgetColor; closeBubble.style.background = widgetColor;

View file

@ -1,4 +1,4 @@
import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers'; import { addClasses, removeClasses, toggleClass } from './DOMHelpers';
import { IFrameHelper } from './IFrameHelper'; import { IFrameHelper } from './IFrameHelper';
import { isExpandedView } from './settingsHelper'; import { isExpandedView } from './settingsHelper';
@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => {
export const createBubbleHolder = hideMessageBubble => { export const createBubbleHolder = hideMessageBubble => {
if (hideMessageBubble) { if (hideMessageBubble) {
addClass(bubbleHolder, 'woot-hidden'); addClasses(bubbleHolder, 'woot-hidden');
} }
addClass(bubbleHolder, 'woot--bubble-holder'); addClasses(bubbleHolder, 'woot--bubble-holder');
body.appendChild(bubbleHolder); body.appendChild(bubbleHolder);
}; };
export const createNotificationBubble = () => { export const createNotificationBubble = () => {
addClass(notificationBubble, 'woot--notification'); addClasses(notificationBubble, 'woot--notification');
return notificationBubble; return notificationBubble;
}; };
@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => {
}; };
export const onClickChatBubble = () => { export const onClickChatBubble = () => {
wootOn(bubbleHolder, 'click', onBubbleClick); bubbleHolder.addEventListener('click', onBubbleClick);
}; };
export const addUnreadClass = () => { export const addUnreadClass = () => {
const holderEl = document.querySelector('.woot-widget-holder'); const holderEl = document.querySelector('.woot-widget-holder');
addClass(holderEl, 'has-unread-view'); addClasses(holderEl, 'has-unread-view');
}; };
export const removeUnreadClass = () => { export const removeUnreadClass = () => {
const holderEl = document.querySelector('.woot-widget-holder'); const holderEl = document.querySelector('.woot-widget-holder');
removeClass(holderEl, 'has-unread-view'); removeClasses(holderEl, 'has-unread-view');
}; };

View file

@ -41,9 +41,18 @@
:class="{ 'dropdown-pane--open': showSearchDropdown }" :class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane" class="dropdown-pane"
> >
<h4 class="text-block-title text-truncate"> <div class="dropdown__header">
{{ multiselectorTitle }} <h4 class="text-block-title text-truncate">
</h4> {{ multiselectorTitle }}
</h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<multiselect-dropdown-items <multiselect-dropdown-items
v-if="showSearchDropdown" v-if="showSearchDropdown"
:options="options" :options="options"
@ -172,4 +181,15 @@ export default {
width: 100%; width: 100%;
} }
} }
.dropdown__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-smaller);
.text-block-title {
margin: 0;
}
}
</style> </style>

View file

@ -20,6 +20,7 @@
<woot-button <woot-button
class="multiselect-dropdown--item" class="multiselect-dropdown--item"
variant="clear" variant="clear"
color-scheme="secondary"
:class="{ :class="{
active: option.id === (selectedItem && selectedItem.id), active: option.id === (selectedItem && selectedItem.id),
}" }"
@ -129,7 +130,7 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 16rem; max-height: 20rem;
} }
.search-wrap { .search-wrap {
@ -162,7 +163,7 @@ export default {
.multiselect-dropdown--list { .multiselect-dropdown--list {
width: 100%; width: 100%;
max-height: 12rem; max-height: 16rem;
} }
.multiselect-dropdown--item { .multiselect-dropdown--item {
@ -170,13 +171,13 @@ export default {
width: 100%; width: 100%;
&.active { &.active {
background-color: var(--w-50); background-color: var(--w-25);
color: var(--w-900); color: var(--w-800);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-medium);
} }
&:focus { &:focus {
background-color: var(--color-background); background-color: var(--color-background-light);
} }
} }

View file

@ -29,19 +29,16 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-menu__item { .dropdown-menu__item {
list-style: none; list-style: none;
margin-bottom: var(--space-micro);
::v-deep { ::v-deep {
a, a,
.button { .button {
display: inline-flex;
white-space: nowrap;
width: 100%; width: 100%;
text-align: left; text-align: left;
color: var(--s-700); color: var(--s-700);
white-space: nowrap;
display: inline-flex;
padding: var(--space-small);
padding-top: var(--space-small);
padding-bottom: var(--space-small);
border-radius: var(--border-radius-normal);
&:hover { &:hover {
background: var(--color-background); background: var(--color-background);

View file

@ -1,10 +1,10 @@
import { CAMPAIGN_TYPES } from '../constants/campaign'; import { CAMPAIGN_TYPES } from '../constants/campaign';
export default { export default {
computed: { computed: {
campaignType() { campaignType() {
const pageURL = window.location.href; const pageURL = window.location.href;
const type = pageURL.substr(pageURL.lastIndexOf('/') + 1); return pageURL.substring(pageURL.lastIndexOf('/') + 1);
return type;
}, },
isOngoingType() { isOngoingType() {
return this.campaignType === CAMPAIGN_TYPES.ONGOING; return this.campaignType === CAMPAIGN_TYPES.ONGOING;

View file

@ -93,7 +93,7 @@ export default {
computed: { computed: {
surveyId() { surveyId() {
const pageURL = window.location.href; const pageURL = window.location.href;
return pageURL.substr(pageURL.lastIndexOf('/') + 1); return pageURL.substring(pageURL.lastIndexOf('/') + 1);
}, },
isRatingSubmitted() { isRatingSubmitted() {
return this.surveyDetails && this.surveyDetails.rating; return this.surveyDetails && this.surveyDetails.rating;

View file

@ -3,6 +3,5 @@ import { API } from 'widget/helpers/axios';
export const getAvailableAgents = async websiteToken => { export const getAvailableAgents = async websiteToken => {
const urlData = endPoints.getAvailableAgents(websiteToken); const urlData = endPoints.getAvailableAgents(websiteToken);
const result = await API.get(urlData.url, { params: urlData.params }); return API.get(urlData.url, { params: urlData.params });
return result;
}; };

View file

@ -3,8 +3,7 @@ import { API } from 'widget/helpers/axios';
const getCampaigns = async websiteToken => { const getCampaigns = async websiteToken => {
const urlData = endPoints.getCampaigns(websiteToken); const urlData = endPoints.getCampaigns(websiteToken);
const result = await API.get(urlData.url, { params: urlData.params }); return API.get(urlData.url, { params: urlData.params });
return result;
}; };
const triggerCampaign = async ({ const triggerCampaign = async ({

View file

@ -3,26 +3,22 @@ import { API } from 'widget/helpers/axios';
const createConversationAPI = async content => { const createConversationAPI = async content => {
const urlData = endPoints.createConversation(content); const urlData = endPoints.createConversation(content);
const result = await API.post(urlData.url, urlData.params); return API.post(urlData.url, urlData.params);
return result;
}; };
const sendMessageAPI = async content => { const sendMessageAPI = async content => {
const urlData = endPoints.sendMessage(content); const urlData = endPoints.sendMessage(content);
const result = await API.post(urlData.url, urlData.params); return API.post(urlData.url, urlData.params);
return result;
}; };
const sendAttachmentAPI = async attachment => { const sendAttachmentAPI = async attachment => {
const urlData = endPoints.sendAttachment(attachment); const urlData = endPoints.sendAttachment(attachment);
const result = await API.post(urlData.url, urlData.params); return API.post(urlData.url, urlData.params);
return result;
}; };
const getMessagesAPI = async ({ before }) => { const getMessagesAPI = async ({ before }) => {
const urlData = endPoints.getConversation({ before }); const urlData = endPoints.getConversation({ before });
const result = await API.get(urlData.url, { params: urlData.params }); return API.get(urlData.url, { params: urlData.params });
return result;
}; };
const getConversationAPI = async () => { const getConversationAPI = async () => {

View file

@ -61,10 +61,7 @@ body {
.is-flat-design { .is-flat-design {
.chat-bubble { .chat-bubble {
border-bottom-left-radius: 0 !important; border-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
box-shadow: none; box-shadow: none;
} }

View file

@ -104,8 +104,7 @@ export default {
) { ) {
return false; return false;
} }
if (!this.message.content) return false; return this.message.content;
return true;
}, },
readableTime() { readableTime() {
const { created_at: createdAt = '' } = this.message; const { created_at: createdAt = '' } = this.message;

View file

@ -54,9 +54,9 @@ export default {
}, },
async onFileUpload(file) { async onFileUpload(file) {
if (this.globalConfig.directUploadsEnabled) { if (this.globalConfig.directUploadsEnabled) {
this.onDirectFileUpload(file); await this.onDirectFileUpload(file);
} else { } else {
this.onIndirectFileUpload(file); await this.onIndirectFileUpload(file);
} }
}, },
async onDirectFileUpload(file) { async onDirectFileUpload(file) {

View file

@ -7,7 +7,12 @@
class="flex items-start" class="flex items-start"
:class="[avatarUrl ? 'justify-between' : 'justify-end']" :class="[avatarUrl ? 'justify-between' : 'justify-end']"
> >
<img v-if="avatarUrl" class="h-12 rounded-full" :src="avatarUrl" /> <img
v-if="avatarUrl"
class="h-12 rounded-full"
:src="avatarUrl"
alt="Avatar"
/>
<header-actions :show-popout-button="showPopoutButton" /> <header-actions :show-popout-button="showPopoutButton" />
</div> </div>
<h2 <h2

View file

@ -209,9 +209,7 @@ export default {
min-height: $space-large; min-height: $space-large;
max-height: 2.4 * $space-mega; max-height: 2.4 * $space-mega;
resize: none; resize: none;
padding: 0; padding: $space-smaller 0;
padding-top: $space-smaller;
padding-bottom: $space-smaller;
margin-top: $space-small; margin-top: $space-small;
margin-bottom: $space-small; margin-bottom: $space-small;
} }

View file

@ -49,8 +49,7 @@ export default {
: decodeURI(this.fileName); : decodeURI(this.fileName);
}, },
fileName() { fileName() {
const filename = this.url.substring(this.url.lastIndexOf('/') + 1); return this.url.substring(this.url.lastIndexOf('/') + 1);
return filename;
}, },
}, },
methods: { methods: {

View file

@ -118,13 +118,10 @@ export default {
const isUserEmailAvailable = !!this.currentUser.email; const isUserEmailAvailable = !!this.currentUser.email;
const isUserPhoneNumberAvailable = !!this.currentUser.phone_number; const isUserPhoneNumberAvailable = !!this.currentUser.phone_number;
return this.preChatFields.filter(field => { return this.preChatFields.filter(field => {
if ( return !(
(isUserEmailAvailable && field.name === 'emailAddress') || (isUserEmailAvailable && field.name === 'emailAddress') ||
(isUserPhoneNumberAvailable && field.name === 'phoneNumber') (isUserPhoneNumberAvailable && field.name === 'phoneNumber')
) { );
return false;
}
return true;
}); });
}, },
enabledPreChatFields() { enabledPreChatFields() {

View file

@ -13,7 +13,7 @@
}} }}
</div> </div>
<div class="text-xs leading-4 mt-1"> <div class="text-xs leading-4 mt-1">
{{ replyWaitMeessage }} {{ replyWaitMessage }}
</div> </div>
</div> </div>
<available-agents v-if="isOnline" :agents="availableAgents" /> <available-agents v-if="isOnline" :agents="availableAgents" />
@ -75,7 +75,7 @@ export default {
} }
return anyAgentOnline; return anyAgentOnline;
}, },
replyWaitMeessage() { replyWaitMessage() {
const { workingHoursEnabled } = this.channelConfig; const { workingHoursEnabled } = this.channelConfig;
if (this.isOnline) { if (this.isOnline) {

View file

@ -107,13 +107,12 @@ export default {
.clear-button { .clear-button {
background: transparent; background: transparent;
color: $color-woot; color: $color-woot;
padding: 0;
border: 0; border: 0;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
font-size: $font-size-medium; font-size: $font-size-medium;
transition: all 0.3s var(--ease-in-cubic); transition: all 0.3s var(--ease-in-cubic);
margin-left: $space-smaller; margin-left: $space-smaller;
padding-right: $space-one; padding: 0 $space-one 0 0;
&:hover { &:hover {
transform: translateX($space-smaller); transform: translateX($space-smaller);

View file

@ -10,7 +10,7 @@ export const loadedEventConfig = () => {
export const getExtraSpaceToScroll = () => { export const getExtraSpaceToScroll = () => {
// This function calculates the extra space needed for the view to // This function calculates the extra space needed for the view to
// accomodate the height of close button + height of // accommodate the height of close button + height of
// read messages button. So that scrollbar won't appear // read messages button. So that scrollbar won't appear
const unreadMessageWrap = document.querySelector('.unread-messages'); const unreadMessageWrap = document.querySelector('.unread-messages');
const unreadCloseWrap = document.querySelector('.close-unread-wrap'); const unreadCloseWrap = document.querySelector('.close-unread-wrap');

View file

@ -3,13 +3,6 @@ import { WOOT_PREFIX } from './constants';
export const isEmptyObject = obj => export const isEmptyObject = obj =>
Object.keys(obj).length === 0 && obj.constructor === Object; Object.keys(obj).length === 0 && obj.constructor === Object;
export const arrayToHashById = array =>
array.reduce((map, obj) => {
const newMap = map;
newMap[obj.id] = obj;
return newMap;
}, {});
export const sendMessage = msg => { export const sendMessage = msg => {
window.parent.postMessage( window.parent.postMessage(
`chatwoot-widget:${JSON.stringify({ ...msg })}`, `chatwoot-widget:${JSON.stringify({ ...msg })}`,
@ -22,9 +15,7 @@ export const IFrameHelper = {
sendMessage, sendMessage,
isAValidEvent: e => { isAValidEvent: e => {
const isDataAString = typeof e.data === 'string'; const isDataAString = typeof e.data === 'string';
const isAValidWootEvent = return isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
return isAValidWootEvent;
}, },
getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')), getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')),
}; };

View file

@ -32,7 +32,7 @@ export const getters = {
}, },
getUnreadMessageCount: _state => { getUnreadMessageCount: _state => {
const { userLastSeenAt } = _state.meta; const { userLastSeenAt } = _state.meta;
const count = Object.values(_state.conversations).filter(chat => { return Object.values(_state.conversations).filter(chat => {
const { created_at: createdAt, message_type: messageType } = chat; const { created_at: createdAt, message_type: messageType } = chat;
const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING; const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
const hasNotSeen = userLastSeenAt const hasNotSeen = userLastSeenAt
@ -40,7 +40,6 @@ export const getters = {
: true; : true;
return hasNotSeen && isOutGoing; return hasNotSeen && isOutGoing;
}).length; }).length;
return count;
}, },
getUnreadTextMessages: (_state, _getters) => { getUnreadTextMessages: (_state, _getters) => {
const unreadCount = _getters.getUnreadMessageCount; const unreadCount = _getters.getUnreadMessageCount;
@ -50,7 +49,6 @@ export const getters = {
return messageType === MESSAGE_TYPE.OUTGOING; return messageType === MESSAGE_TYPE.OUTGOING;
}); });
const maxUnreadCount = Math.min(unreadCount, 3); const maxUnreadCount = Math.min(unreadCount, 3);
const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount); return unreadAgentMessages.splice(-maxUnreadCount);
return allUnreadMessages;
}, },
}; };

View file

@ -29,7 +29,7 @@ const shouldShowAvatar = (message, nextMessage) => {
export const groupConversationBySender = conversationsForADate => export const groupConversationBySender = conversationsForADate =>
conversationsForADate.map((message, index) => { conversationsForADate.map((message, index) => {
let showAvatar = false; let showAvatar;
const isLastMessage = index === conversationsForADate.length - 1; const isLastMessage = index === conversationsForADate.length - 1;
if (isASubmittedFormMessage(message)) { if (isASubmittedFormMessage(message)) {
showAvatar = false; showAvatar = false;

View file

@ -88,8 +88,7 @@ export const mutations = {
}, },
toggleAgentTypingStatus($state, { status }) { toggleAgentTypingStatus($state, { status }) {
const isTyping = status === 'on'; $state.uiFlags.isAgentTyping = status === 'on';
$state.uiFlags.isAgentTyping = isTyping;
}, },
setMetaUserLastSeenAt($state, lastSeen) { setMetaUserLastSeenAt($state, lastSeen) {

View file

@ -9,14 +9,14 @@ export const actions = {
try { try {
await conversationLabels.create(label); await conversationLabels.create(label);
} catch (error) { } catch (error) {
// Ingore error // Ignore error
} }
}, },
destroy: async (_, label) => { destroy: async (_, label) => {
try { try {
await conversationLabels.destroy(label); await conversationLabels.destroy(label);
} catch (error) { } catch (error) {
// Ingore error // Ignore error
} }
}, },
}; };

View file

@ -34,7 +34,7 @@ module MailboxHelper
end end
def create_contact def create_contact
@contact_inbox = ::ContactBuilder.new( @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: processed_mail.original_sender, source_id: processed_mail.original_sender,
inbox: @inbox, inbox: @inbox,
contact_attributes: { contact_attributes: {

View file

@ -37,16 +37,11 @@ class Channel::FacebookPage < ApplicationRecord
end end
def create_contact_inbox(instagram_id, name) def create_contact_inbox(instagram_id, name)
ActiveRecord::Base.transaction do @contact_inbox = ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!(name: name) source_id: instagram_id,
::ContactInbox.create!( inbox: inbox,
contact_id: contact.id, contact_attributes: { name: name }
inbox_id: inbox.id, }).perform
source_id: instagram_id
)
rescue StandardError => e
Rails.logger.error e
end
end end
def subscribe def subscribe

View file

@ -32,16 +32,11 @@ class Channel::TwitterProfile < ApplicationRecord
end end
def create_contact_inbox(profile_id, name, additional_attributes) def create_contact_inbox(profile_id, name, additional_attributes)
ActiveRecord::Base.transaction do ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name) source_id: profile_id,
::ContactInbox.create!( inbox: inbox,
contact_id: contact.id, contact_attributes: { name: name, additional_attributes: additional_attributes }
inbox_id: inbox.id, }).perform
source_id: profile_id
)
rescue StandardError => e
Rails.logger.error e
end
end end
def twitter_client def twitter_client

View file

@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord
end end
def create_contact_inbox(additional_attributes = {}) def create_contact_inbox(additional_attributes = {})
ActiveRecord::Base.transaction do ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!( inbox: inbox,
name: ::Haikunator.haikunate(1000), contact_attributes: { additional_attributes: additional_attributes }
additional_attributes: additional_attributes }).perform
)
contact_inbox = ::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: SecureRandom.uuid
)
contact_inbox
rescue StandardError => e
Rails.logger.error e
end
end end
end end

View file

@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord
validate :open_all_day_and_closed_all_day validate :open_all_day_and_closed_all_day
def self.today def self.today
find_by(day_of_week: Date.current.wday) # While getting the day of the week, consider the timezone as well. `first` would
# return the first working hour from the list of working hours available per week.
inbox = first.inbox
find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday)
end end
def open_at?(time) def open_at?(time)

View file

@ -81,7 +81,7 @@ class Line::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: line_contact_info['userId'], source_id: line_contact_info['userId'],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -37,7 +37,7 @@ class Sms::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:from], source_id: params[:from],
inbox: @inbox, inbox: @inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -31,7 +31,7 @@ class Telegram::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:message][:from][:id], source_id: params[:message][:from][:id],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -47,7 +47,7 @@ class Twilio::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:From], source_id: params[:From],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService
set_conversation set_conversation
return if @processed_params[:messages].blank? return if @processed_params[:messages].blank? || unprocessable_message_type?
@message = @conversation.messages.build( @message = @conversation.messages.build(
content: message_content(@processed_params[:messages].first), content: message_content(@processed_params[:messages].first),
@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService
contact_params = @processed_params[:contacts]&.first contact_params = @processed_params[:contacts]&.first
return if contact_params.blank? return if contact_params.blank?
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: contact_params[:wa_id], source_id: contact_params[:wa_id],
inbox: inbox, inbox: inbox,
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" } contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
@ -86,6 +86,10 @@ class Whatsapp::IncomingMessageBaseService
@processed_params[:messages].first[:type] @processed_params[:messages].first[:type]
end end
def unprocessable_message_type?
%w[reaction contacts].include?(message_type)
end
def attach_files def attach_files
return if %w[text button interactive].include?(message_type) return if %w[text button interactive].include?(message_type)

View file

@ -46,8 +46,13 @@ unless Rails.env.production?
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
InboxMember.create!(user: user, inbox: inbox) InboxMember.create!(user: user, inbox: inbox)
contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account) contact = ::ContactInboxWithContactBuilder.new(
contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true) source_id: user.id,
inbox: inbox,
hmac_verified: true,
contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' }
).perform&.contact
conversation = Conversation.create!( conversation = Conversation.create!(
account: account, account: account,
inbox: inbox, inbox: inbox,

View file

@ -12,8 +12,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: contact.phone_number source_id: contact.phone_number
).perform ).perform
@ -23,8 +23,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -33,8 +33,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: '+224213223422' source_id: '+224213223422'
).perform ).perform
@ -44,12 +44,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number) expect(contact_inbox.source_id).to eq(contact.phone_number)
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: twilio_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'twilio whatsapp inbox' do describe 'twilio whatsapp inbox' do
@ -59,8 +70,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: "whatsapp:#{contact.phone_number}" source_id: "whatsapp:#{contact.phone_number}"
).perform ).perform
@ -70,8 +81,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -80,8 +91,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: 'whatsapp:+555555' source_id: 'whatsapp:+555555'
).perform ).perform
@ -91,12 +102,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}") expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: twilio_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'whatsapp inbox' do describe 'whatsapp inbox' do
@ -105,8 +127,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id, inbox: whatsapp_inbox,
source_id: contact.phone_number&.delete('+') source_id: contact.phone_number&.delete('+')
).perform ).perform
@ -116,8 +138,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id inbox: whatsapp_inbox
).perform ).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id) expect(contact_inbox.id).to be(existing_contact_inbox.id)
@ -126,8 +148,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id, inbox: whatsapp_inbox,
source_id: '555555' source_id: '555555'
).perform ).perform
@ -137,12 +159,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id inbox: whatsapp_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+')) expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: whatsapp_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'sms inbox' do describe 'sms inbox' do
@ -152,8 +185,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id, inbox: sms_inbox,
source_id: contact.phone_number source_id: contact.phone_number
).perform ).perform
@ -163,8 +196,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id inbox: sms_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -173,8 +206,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id, inbox: sms_inbox,
source_id: '+224213223422' source_id: '+224213223422'
).perform ).perform
@ -184,12 +217,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id inbox: sms_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number) expect(contact_inbox.source_id).to eq(contact.phone_number)
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: sms_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'email inbox' do describe 'email inbox' do
@ -199,8 +243,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id, inbox: email_inbox,
source_id: contact.email source_id: contact.email
).perform ).perform
@ -210,8 +254,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id inbox: email_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -220,8 +264,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id, inbox: email_inbox,
source_id: 'xyc@xyc.com' source_id: 'xyc@xyc.com'
).perform ).perform
@ -231,12 +275,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id inbox: email_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.email) expect(contact_inbox.source_id).to eq(contact.email)
end end
it 'raises error when contact email is not present and no source id is provided' do
contact.update!(email: nil)
expect do
described_class.new(
contact: contact,
inbox: email_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email')
end
end end
describe 'api inbox' do describe 'api inbox' do
@ -246,8 +301,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test') existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id, inbox: api_inbox,
source_id: 'test' source_id: 'test'
).perform ).perform
@ -257,8 +312,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id, inbox: api_inbox,
source_id: 'test' source_id: 'test'
).perform ).perform
@ -268,61 +323,12 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id inbox: api_inbox
).perform ).perform
expect(contact_inbox.source_id).not_to be_nil expect(contact_inbox.source_id).not_to be_nil
end end
end end
describe 'web widget' do
let!(:website_channel) { create(:channel_widget, account: account) }
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: website_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
describe 'facebook inbox' do
before do
stub_request(:post, /graph.facebook.com/)
end
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: facebook_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
describe 'twitter inbox' do
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twitter_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
end end
end end

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
describe ::ContactBuilder do describe ::ContactInboxWithContactBuilder do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') } let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') }

View file

@ -62,6 +62,59 @@ RSpec.describe Webhooks::WhatsappEventsJob, type: :job do
job.perform_now(wb_params) job.perform_now(wb_params)
end end
it 'Ignore reaction type message and stop raising error' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
messages: [{
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
}],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Message, :count)
end
it 'Ignore contacts type message and stop raising error' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
messages: [{ from: '1111981136571',
contacts: [{ phones: [{ phone: '+1987654' }], name: { first_name: 'contact name' } }],
timestamp: '1664799904',
type: 'contacts' }],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Message, :count)
end
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false) validate_provider_config: false)

View file

@ -88,4 +88,18 @@ RSpec.describe WorkingHour do
'Validation failed: open_all_day and closed_all_day cannot be true at the same time') 'Validation failed: open_all_day and closed_all_day cannot be true at the same time')
end end
end end
context 'when on monday 9am in Sydney timezone' do
let(:inbox) { create(:inbox) }
before do
Time.zone = 'Australia/Sydney'
inbox.update(timezone: 'Australia/Sydney')
travel_to '10.10.2022 9:00 AEDT'
end
it 'is considered working hour' do
expect(described_class.today.open_now?).to be true
end
end
end end

View file

@ -167,6 +167,7 @@ describe ::Contacts::FilterService do
context 'with x_days_before filter' do context 'with x_days_before filter' do
before do before do
Time.zone = 'UTC'
el_contact.update(last_activity_at: (Time.zone.today - 4.days)) el_contact.update(last_activity_at: (Time.zone.today - 4.days))
cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) cs_contact.update(last_activity_at: (Time.zone.today - 5.days))
en_contact.update(last_activity_at: (Time.zone.today - 2.days)) en_contact.update(last_activity_at: (Time.zone.today - 2.days))

View file

@ -309,6 +309,7 @@ describe ::Conversations::FilterService do
context 'with x_days_before filter' do context 'with x_days_before filter' do
before do before do
Time.zone = 'UTC'
en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days)) en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days))
en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days)) en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days))
user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days)) user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days))