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
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = spaces
indent_style = space
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

View file

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

5
.gitignore vendored
View file

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

View file

@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
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
pattr_initialize [:contact_id!, :inbox_id!, :source_id]
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
def perform
@contact = Contact.find(contact_id)
@inbox = @contact.account.inboxes.find(inbox_id)
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?
@source_id ||= generate_source_id
create_contact_inbox if source_id.present?
end
private
@ -19,23 +18,37 @@ class ContactInboxBuilder
when 'Channel::Whatsapp'
wa_source_id
when 'Channel::Email'
@contact.email
email_source_id
when 'Channel::Sms'
@contact.phone_number
when 'Channel::Api'
phone_source_id
when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid
else
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
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
return unless @contact.phone_number
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
# whatsapp doesn't want the + in e164 format
@contact.phone_number.delete('+').to_s
end
def twilio_source_id
return unless @contact.phone_number
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
case @inbox.channel.medium
when 'sms'
@ -45,11 +58,11 @@ class ContactInboxBuilder
end
end
def create_contact_inbox(source_id)
::ContactInbox.find_or_create_by!(
def create_contact_inbox
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: @contact.id,
inbox_id: @inbox.id,
source_id: source_id
source_id: @source_id
)
end
end

View file

@ -1,25 +1,47 @@
class ContactBuilder
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
# This Builder will create a contact and contact inbox with specified attributes.
# 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
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
return contact_inbox if contact_inbox
find_or_create_contact_and_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
private
def build_contact_with_contact_inbox
@contact = find_contact || create_contact
@contact_inbox = create_contact_inbox
end
def account
@account ||= inbox.account
end
def create_contact_inbox(contact)
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id
)
def create_contact_inbox
ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: @source_id,
hmac_verified: hmac_verified
).perform
end
def update_contact_avatar(contact)
@ -61,16 +83,4 @@ class ContactBuilder
account.contacts.find_by(phone_number: phone_number)
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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include DateRangeHelper
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
result = conversation_finder.perform
@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
authorize @conversation.inbox, :show?
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
@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])
authorize @contact_inbox.inbox, :show?
end
def build_contact_inbox
return if params[:contact_id].blank? || params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id])
authorize inbox, :show?
return if @inbox.blank? || @contact.blank?
ContactInboxBuilder.new(
contact_id: params[:contact_id],
inbox_id: inbox.id,
contact: @contact,
inbox: @inbox,
source_id: params[:source_id]
).perform
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -239,7 +239,9 @@
},
"API_CALLBACK": {
"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",
"API": {
@ -357,7 +359,7 @@
},
"FINISH": {
"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",
"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ē."

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,11 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas",
"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",
"ASSIGN_LABEL": "Atribua",
"YES": "Sim",
"ASSIGN_AGENT_TOOLTIP": "Atribuir Agente",
"ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso",
"ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente",

View file

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

View file

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

View file

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

View file

@ -16,7 +16,12 @@
</div>
</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>
<span class="fs-small">
@ -43,6 +48,8 @@
<script>
import timeMixin from 'dashboard/mixins/time';
import portalMixin from '../mixins/portalMixin';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
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>

View file

@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import portalMixin from '../mixins/portalMixin';
import AddCategory from '../pages/categories/AddCategory';
@ -72,7 +73,7 @@ export default {
PortalPopover,
AddCategory,
},
mixins: [portalMixin],
mixins: [portalMixin, uiSettingsMixin],
data() {
return {
isSidebarOpen: false,
@ -231,7 +232,13 @@ export default {
},
updated() {
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: {
handleResize() {

View file

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

View file

@ -21,12 +21,20 @@ const EditCategory = () => import('./pages/categories/EditCategory');
const ListCategoryArticles = () =>
import('./pages/articles/ListCategoryArticles');
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
const DefaultPortalArticles = () =>
import('./pages/articles/DefaultPortalArticles');
const NewArticle = () => import('./pages/articles/NewArticle');
const EditArticle = () => import('./pages/articles/EditArticle');
const portalRoutes = [
{
path: getPortalRoute(''),
name: 'default_portal_articles',
roles: ['administrator', 'agent'],
component: DefaultPortalArticles,
},
{
path: getPortalRoute('all'),
name: 'list_all_portals',
roles: ['administrator', 'agent'],
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"
/>
</div>
<div class="medium-6 small-offset-3">
<div v-if="isWhatsAppCloudInbox" class="medium-6 small-offset-3">
<p class="config--label">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}
</p>
<woot-code
v-if="isWhatsAppCloudInbox"
lang="html"
:script="currentInbox.callback_webhook_url"
/>
<woot-code lang="html" :script="currentInbox.callback_webhook_url" />
<p class="config--label">
{{
$t(
@ -36,7 +32,6 @@
}}
</p>
<woot-code
v-if="isWhatsAppCloudInbox"
lang="html"
:script="currentInbox.provider_config.webhook_verify_token"
/>

View file

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

View file

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

View file

@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper';
export const loadCSS = () => {
const css = document.createElement('style');
css.type = 'text/css';
css.innerHTML = `${SDK_CSS}`;
document.body.appendChild(css);
};
export const wootOn = (elm, event, fn) => {
if (document.addEventListener) {
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 addClasses = (elm, classes) => {
elm.classList.add(...classes.split(' '));
};
export const toggleClass = (elm, classes) => {
classHelper(classes, 'toggle', elm);
elm.classList.toggle(classes);
};
export const removeClass = (elm, classes) => {
classHelper(classes, 'remove', elm);
export const removeClasses = (elm, classes) => {
elm.classList.remove(...classes.split(' '));
};
export const onLocationChange = ({ referrerURL, referrerHost }) => {

View file

@ -1,9 +1,8 @@
import Cookies from 'js-cookie';
import {
wootOn,
addClass,
addClasses,
loadCSS,
removeClass,
removeClasses,
onLocationChangeListener,
} from './DOMHelpers';
import {
@ -68,7 +67,7 @@ export const IFrameHelper = {
holderClassName += ` woot-widget-holder--flat`;
}
addClass(widgetHolder, holderClassName);
addClasses(widgetHolder, holderClassName);
widgetHolder.appendChild(iframe);
body.appendChild(widgetHolder);
IFrameHelper.initPostMessageCommunication();
@ -99,7 +98,7 @@ export const IFrameHelper = {
};
},
initWindowSizeListener: () => {
wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton());
window.addEventListener('resize', () => IFrameHelper.toggleCloseButton());
},
preventDefaultScroll: () => {
widgetHolder.addEventListener('wheel', event => {
@ -241,9 +240,9 @@ export const IFrameHelper = {
event.unreadMessageCount > 0 &&
!bubbleElement.classList.contains('unread-notification')
) {
addClass(bubbleElement, 'unread-notification');
addClasses(bubbleElement, 'unread-notification');
} else if (event.unreadMessageCount === 0) {
removeClass(bubbleElement, 'unread-notification');
removeClasses(bubbleElement, 'unread-notification');
}
},
@ -284,7 +283,7 @@ export const IFrameHelper = {
target: chatBubble,
});
addClass(closeBubble, closeBtnClassName);
addClasses(closeBubble, closeBtnClassName);
chatIcon.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 { isExpandedView } from './settingsHelper';
@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => {
export const createBubbleHolder = hideMessageBubble => {
if (hideMessageBubble) {
addClass(bubbleHolder, 'woot-hidden');
addClasses(bubbleHolder, 'woot-hidden');
}
addClass(bubbleHolder, 'woot--bubble-holder');
addClasses(bubbleHolder, 'woot--bubble-holder');
body.appendChild(bubbleHolder);
};
export const createNotificationBubble = () => {
addClass(notificationBubble, 'woot--notification');
addClasses(notificationBubble, 'woot--notification');
return notificationBubble;
};
@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => {
};
export const onClickChatBubble = () => {
wootOn(bubbleHolder, 'click', onBubbleClick);
bubbleHolder.addEventListener('click', onBubbleClick);
};
export const addUnreadClass = () => {
const holderEl = document.querySelector('.woot-widget-holder');
addClass(holderEl, 'has-unread-view');
addClasses(holderEl, 'has-unread-view');
};
export const removeUnreadClass = () => {
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"
>
<h4 class="text-block-title text-truncate">
{{ multiselectorTitle }}
</h4>
<div class="dropdown__header">
<h4 class="text-block-title text-truncate">
{{ multiselectorTitle }}
</h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<multiselect-dropdown-items
v-if="showSearchDropdown"
:options="options"
@ -172,4 +181,15 @@ export default {
width: 100%;
}
}
.dropdown__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-smaller);
.text-block-title {
margin: 0;
}
}
</style>

View file

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

View file

@ -29,19 +29,16 @@ export default {
<style lang="scss" scoped>
.dropdown-menu__item {
list-style: none;
margin-bottom: var(--space-micro);
::v-deep {
a,
.button {
display: inline-flex;
white-space: nowrap;
width: 100%;
text-align: left;
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 {
background: var(--color-background);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,12 @@
class="flex items-start"
: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" />
</div>
<h2

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ export const loadedEventConfig = () => {
export const getExtraSpaceToScroll = () => {
// 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
const unreadMessageWrap = document.querySelector('.unread-messages');
const unreadCloseWrap = document.querySelector('.close-unread-wrap');

View file

@ -3,13 +3,6 @@ import { WOOT_PREFIX } from './constants';
export const isEmptyObject = obj =>
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 => {
window.parent.postMessage(
`chatwoot-widget:${JSON.stringify({ ...msg })}`,
@ -22,9 +15,7 @@ export const IFrameHelper = {
sendMessage,
isAValidEvent: e => {
const isDataAString = typeof e.data === 'string';
const isAValidWootEvent =
isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
return isAValidWootEvent;
return isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
},
getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')),
};

View file

@ -32,7 +32,7 @@ export const getters = {
},
getUnreadMessageCount: _state => {
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 isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
const hasNotSeen = userLastSeenAt
@ -40,7 +40,6 @@ export const getters = {
: true;
return hasNotSeen && isOutGoing;
}).length;
return count;
},
getUnreadTextMessages: (_state, _getters) => {
const unreadCount = _getters.getUnreadMessageCount;
@ -50,7 +49,6 @@ export const getters = {
return messageType === MESSAGE_TYPE.OUTGOING;
});
const maxUnreadCount = Math.min(unreadCount, 3);
const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount);
return allUnreadMessages;
return unreadAgentMessages.splice(-maxUnreadCount);
},
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord
end
def create_contact_inbox(additional_attributes = {})
ActiveRecord::Base.transaction do
contact = inbox.account.contacts.create!(
name: ::Haikunator.haikunate(1000),
additional_attributes: additional_attributes
)
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
::ContactInboxWithContactBuilder.new({
inbox: inbox,
contact_attributes: { additional_attributes: additional_attributes }
}).perform
end
end

View file

@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord
validate :open_all_day_and_closed_all_day
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
def open_at?(time)

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService
set_conversation
return if @processed_params[:messages].blank?
return if @processed_params[:messages].blank? || unprocessable_message_type?
@message = @conversation.messages.build(
content: message_content(@processed_params[:messages].first),
@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService
contact_params = @processed_params[:contacts]&.first
return if contact_params.blank?
contact_inbox = ::ContactBuilder.new(
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: contact_params[:wa_id],
inbox: inbox,
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]
end
def unprocessable_message_type?
%w[reaction contacts].include?(message_type)
end
def attach_files
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')
InboxMember.create!(user: user, inbox: inbox)
contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account)
contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true)
contact = ::ContactInboxWithContactBuilder.new(
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!(
account: account,
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
contact: contact,
inbox: twilio_inbox,
source_id: contact.phone_number
).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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
contact: contact,
inbox: twilio_inbox
).perform
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
contact: contact,
inbox: twilio_inbox,
source_id: '+224213223422'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
contact: contact,
inbox: twilio_inbox
).perform
expect(contact_inbox.source_id).to eq(contact.phone_number)
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
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
contact: contact,
inbox: twilio_inbox,
source_id: "whatsapp:#{contact.phone_number}"
).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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
contact: contact,
inbox: twilio_inbox
).perform
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
contact: contact,
inbox: twilio_inbox,
source_id: 'whatsapp:+555555'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
contact: contact,
inbox: twilio_inbox
).perform
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
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
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: whatsapp_inbox.id,
contact: contact,
inbox: whatsapp_inbox,
source_id: contact.phone_number&.delete('+')
).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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: whatsapp_inbox.id
contact: contact,
inbox: whatsapp_inbox
).perform
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: whatsapp_inbox.id,
contact: contact,
inbox: whatsapp_inbox,
source_id: '555555'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: whatsapp_inbox.id
contact: contact,
inbox: whatsapp_inbox
).perform
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
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
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: sms_inbox.id,
contact: contact,
inbox: sms_inbox,
source_id: contact.phone_number
).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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: sms_inbox.id
contact: contact,
inbox: sms_inbox
).perform
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: sms_inbox.id,
contact: contact,
inbox: sms_inbox,
source_id: '+224213223422'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: sms_inbox.id
contact: contact,
inbox: sms_inbox
).perform
expect(contact_inbox.source_id).to eq(contact.phone_number)
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
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id,
contact: contact,
inbox: email_inbox,
source_id: contact.email
).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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id
contact: contact,
inbox: email_inbox
).perform
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id,
contact: contact,
inbox: email_inbox,
source_id: 'xyc@xyc.com'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id
contact: contact,
inbox: email_inbox
).perform
expect(contact_inbox.source_id).to eq(contact.email)
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
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
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id,
contact: contact,
inbox: api_inbox,
source_id: 'test'
).perform
@ -257,8 +312,8 @@ describe ::ContactInboxBuilder 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)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id,
contact: contact,
inbox: api_inbox,
source_id: 'test'
).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
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id
contact: contact,
inbox: api_inbox
).perform
expect(contact_inbox.source_id).not_to be_nil
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

View file

@ -1,6 +1,6 @@
require 'rails_helper'
describe ::ContactBuilder do
describe ::ContactInboxWithContactBuilder do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
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)
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
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: 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')
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

View file

@ -167,6 +167,7 @@ describe ::Contacts::FilterService do
context 'with x_days_before filter' do
before do
Time.zone = 'UTC'
el_contact.update(last_activity_at: (Time.zone.today - 4.days))
cs_contact.update(last_activity_at: (Time.zone.today - 5.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
before do
Time.zone = 'UTC'
en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.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))