Merge branch 'develop' into upgrade-fb-api
This commit is contained in:
+3
-7
@@ -8,15 +8,11 @@ Layout/LineLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 125
|
||||
Max: 175
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
||||
@contacts = fetch_contacts(resolved_contacts)
|
||||
end
|
||||
|
||||
def search
|
||||
@@ -29,7 +29,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
search: "%#{params[:q].strip}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
@contacts = fetch_contacts(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
@@ -63,7 +63,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
||||
contacts = result[:contacts]
|
||||
@contacts_count = result[:count]
|
||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||
@contacts = fetch_contacts(contacts)
|
||||
end
|
||||
|
||||
def contactable_inboxes
|
||||
@@ -125,17 +125,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
|
||||
contacts_with_conversation_count = filtrate(contacts)
|
||||
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
def fetch_contacts(contacts)
|
||||
contacts_with_avatar = filtrate(contacts)
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
|
||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
|
||||
contacts_with_conversation_count
|
||||
contacts_with_avatar
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
|
||||
@@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
ensure_account_is_active?(account)
|
||||
render_unauthorized('Account is suspended') and return unless account.active?
|
||||
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
@@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
|
||||
def ensure_account_is_active?(account)
|
||||
render_unauthorized('Account is suspended') unless account.active?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,9 +120,18 @@ export const generateConditionOptions = (options, key = 'id') => {
|
||||
});
|
||||
};
|
||||
|
||||
// Add the "None" option to the agent list
|
||||
export const agentList = agents => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
...(agents || []),
|
||||
];
|
||||
|
||||
export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||
const actionsMap = {
|
||||
assign_agent: agents,
|
||||
assign_agent: agentList(agents),
|
||||
assign_team: teams,
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
|
||||
@@ -26,6 +26,7 @@ import pt_BR from './locale/pt_BR';
|
||||
import ro from './locale/ro';
|
||||
import ru from './locale/ru';
|
||||
import sk from './locale/sk';
|
||||
import sr from './locale/sr';
|
||||
import sv from './locale/sv';
|
||||
import ta from './locale/ta';
|
||||
import th from './locale/th';
|
||||
@@ -66,6 +67,7 @@ export default {
|
||||
ro,
|
||||
ru,
|
||||
sk,
|
||||
sr,
|
||||
sv,
|
||||
ta,
|
||||
th,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section
|
||||
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden"
|
||||
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden -mt-1"
|
||||
>
|
||||
<ve-table
|
||||
:fixed-header="true"
|
||||
@@ -256,13 +256,6 @@ export default {
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CREATED_AT'),
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
},
|
||||
{
|
||||
field: 'conversationsCount',
|
||||
key: 'conversationsCount',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.CONVERSATIONS'),
|
||||
width: 150,
|
||||
align: this.isRTLView ? 'right' : 'left',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
|
||||
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
|
||||
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
|
||||
"chat-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z",
|
||||
"checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
|
||||
"chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z",
|
||||
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
|
||||
|
||||
@@ -28,6 +28,7 @@ import pt_BR from './locale/pt_BR.json';
|
||||
import ro from './locale/ro.json';
|
||||
import ru from './locale/ru.json';
|
||||
import sk from './locale/sk.json';
|
||||
import sr from './locale/sr.json';
|
||||
import sv from './locale/sv.json';
|
||||
import ta from './locale/ta.json';
|
||||
import th from './locale/th.json';
|
||||
@@ -68,6 +69,7 @@ export default {
|
||||
ro,
|
||||
ru,
|
||||
sk,
|
||||
sr,
|
||||
sv,
|
||||
ta,
|
||||
th,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50 mb-0">
|
||||
<h3 class="text-sm font-medium text-slate-800 dark:text-slate-50 mb-0">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<article-list :articles="articles" @click="onArticleClick" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<category-card
|
||||
:title="$t('PORTAL.POPULAR_ARTICLES')"
|
||||
:articles="articles.slice(0, 4)"
|
||||
:articles="articles.slice(0, 6)"
|
||||
@view-all="$emit('view-all')"
|
||||
@view="onArticleClick"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<li
|
||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-75 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
|
||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-25 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<button
|
||||
class="underline-offset-2 text-sm leading-6 text-left"
|
||||
@click="onClick"
|
||||
>
|
||||
<button class="underline-offset-2 text-sm leading-6 text-left">
|
||||
{{ title }}
|
||||
</button>
|
||||
<span class="pl-1 arrow">
|
||||
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
return this.agents.slice(0, 5).map(agent => ({
|
||||
return this.agents.slice(0, 4).map(agent => ({
|
||||
id: agent.id,
|
||||
avatar: agent.avatar_url,
|
||||
name: agent.name,
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button v-if="showBackButton" @click="onBackButtonClick">
|
||||
<button
|
||||
v-if="showBackButton"
|
||||
class="-ml-3 px-2"
|
||||
@click="onBackButtonClick"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="24"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<header
|
||||
class="header-expanded py-6 px-5 relative box-border w-full"
|
||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||
class="header-expanded pt-6 pb-4 px-5 relative box-border w-full bg-transparent"
|
||||
>
|
||||
<div
|
||||
class="flex items-start"
|
||||
@@ -13,23 +12,25 @@
|
||||
:src="avatarUrl"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<header-actions :show-popout-button="showPopoutButton" />
|
||||
<header-actions
|
||||
:show-popout-button="showPopoutButton"
|
||||
:show-end-conversation-button="false"
|
||||
/>
|
||||
</div>
|
||||
<h2
|
||||
v-dompurify-html="introHeading"
|
||||
class="mt-5 text-3xl mb-3 leading-8 font-normal"
|
||||
class="mt-4 text-2xl mb-1.5 font-medium"
|
||||
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="introBody"
|
||||
class="text-lg leading-normal"
|
||||
class="text-base leading-normal"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import HeaderActions from './HeaderActions';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
@@ -57,10 +58,5 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full justify-between items-center rounded-md ring-1 ring-inset ring-slate-50 px-2 py-2 text-sm text-slate-700 bg-slate-25 hover:bg-slate-50 dark:text-white dark:bg-slate-800 dark:ring-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-600 group"
|
||||
@click="$emit('continue')"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-md bg-slate-75 dark:bg-slate-700 text-lg flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chat"
|
||||
size="16"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-left flex flex-col justify-start flex-grow max-w-[calc(100%-80px)] mx-2 group-hover:opacity-75"
|
||||
>
|
||||
<h5 class="font-medium text-slate-900 dark:text-white">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<p class="h-4 leading-4 flex items-center gap-1">
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="inline-flex items-center justify-center rounded-full bg-green-200 px-1 min-w-[16px] leading-4 text-xxs font-medium text-green-700 mr-0.5"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
<span
|
||||
v-dompurify-html="content"
|
||||
class="leading-4 h-4 text-ellipsis overflow-hidden whitespace-nowrap dark:text-slate-25"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-8 h-10 flex items-center justify-center">
|
||||
<fluent-icon icon="chevron-right" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Continue your chat',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: 'Chat with us',
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
"
|
||||
>
|
||||
<thumbnail
|
||||
size="40px"
|
||||
size="36px"
|
||||
:username="user.name"
|
||||
:src="user.avatar"
|
||||
has-border
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div v-if="showHeaderActions" class="actions flex items-center">
|
||||
<button
|
||||
v-if="canLeaveConversation && hasEndConversationEnabled"
|
||||
v-if="
|
||||
canLeaveConversation &&
|
||||
hasEndConversationEnabled &&
|
||||
showEndConversationButton
|
||||
"
|
||||
class="button transparent compact"
|
||||
:title="$t('END_CONVERSATION')"
|
||||
@click="resolveConversation"
|
||||
@@ -56,6 +60,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showEndConversationButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<template>
|
||||
<div class="px-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="max-w-xs"
|
||||
:class="$dm('text-black-700', 'dark:text-slate-50')"
|
||||
>
|
||||
<div class="text-base leading-5 font-medium mb-1">
|
||||
<div class="p-4 shadow-sm rounded-md bg-white dark:bg-slate-700">
|
||||
<div class="flex items-center justify-between ">
|
||||
<div class=" ">
|
||||
<div class="text-sm font-medium text-slate-700 dark:text-slate-50">
|
||||
{{
|
||||
isOnline
|
||||
? $t('TEAM_AVAILABILITY.ONLINE')
|
||||
: $t('TEAM_AVAILABILITY.OFFLINE')
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs leading-3 mt-1">
|
||||
<div class="text-sm mt-1 text-slate-500 dark:text-slate-100">
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<available-agents v-if="isOnline" :agents="availableAgents" />
|
||||
</div>
|
||||
<custom-button
|
||||
class="font-medium"
|
||||
block
|
||||
:bg-color="widgetColor"
|
||||
:text-color="textColor"
|
||||
<button
|
||||
class="inline-flex text-sm font-medium rounded-md py-1 mt-2 px-2 -ml-2 leading-6 text-slate-800 dark:text-slate-50 justify-between items-center hover:bg-slate-25 dark:hover:bg-slate-800"
|
||||
:style="{ color: widgetColor }"
|
||||
@click="startConversation"
|
||||
>
|
||||
{{
|
||||
hasConversation ? $t('CONTINUE_CONVERSATION') : $t('START_CONVERSATION')
|
||||
}}
|
||||
</custom-button>
|
||||
<span class="pr-2 text-sm">
|
||||
{{
|
||||
hasConversation
|
||||
? $t('CONTINUE_CONVERSATION')
|
||||
: $t('START_CONVERSATION')
|
||||
}}
|
||||
</span>
|
||||
<fluent-icon icon="arrow-right" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,18 +37,16 @@ import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import CustomButton from 'shared/components/Button';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
export default {
|
||||
name: 'TeamAvailability',
|
||||
components: {
|
||||
AvailableAgents,
|
||||
CustomButton,
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin, darkMixin],
|
||||
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
|
||||
props: {
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
@@ -58,6 +56,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full flex flex-col"
|
||||
:class="$dm('bg-slate-50', 'dark:bg-slate-800')"
|
||||
class="w-full h-full bg-slate-25 dark:bg-slate-800"
|
||||
:class="{ 'overflow-auto': isOnHomeView }"
|
||||
@keydown.esc="closeWindow"
|
||||
>
|
||||
<div
|
||||
class="header-wrap"
|
||||
:class="{
|
||||
expanded: !isHeaderCollapsed,
|
||||
collapsed: isHeaderCollapsed,
|
||||
}"
|
||||
>
|
||||
<transition
|
||||
enter-active-class="transition-all delay-200 duration-300 ease-in"
|
||||
leave-active-class="transition-all duration-200 ease-out"
|
||||
enter-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
<div class="flex flex-col h-full relative">
|
||||
<div
|
||||
class="header-wrap sticky top-0 z-40 transition-all"
|
||||
:class="{
|
||||
expanded: !isHeaderCollapsed,
|
||||
collapsed: isHeaderCollapsed,
|
||||
'custom-header-shadow': isHeaderCollapsed,
|
||||
...opacityClass,
|
||||
}"
|
||||
>
|
||||
<chat-header-expanded
|
||||
v-if="!isHeaderCollapsed"
|
||||
@@ -32,24 +27,14 @@
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="appConfig.showPopoutButton"
|
||||
:available-agents="availableAgents"
|
||||
:show-back-button="showBackButton"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<banner />
|
||||
<transition
|
||||
enter-active-class="transition-all delay-300 duration-300 ease-in"
|
||||
leave-active-class="transition-all duration-200 ease-out"
|
||||
enter-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
</div>
|
||||
<banner />
|
||||
<router-view />
|
||||
</transition>
|
||||
<branding
|
||||
class="bg-slate-25 dark:bg-slate-800"
|
||||
:disable-branding="disableBranding"
|
||||
/>
|
||||
|
||||
<branding v-if="!isOnArticleViewer" :disable-branding="disableBranding" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -58,7 +43,6 @@ import Branding from 'shared/components/Branding.vue';
|
||||
import ChatHeader from '../ChatHeader.vue';
|
||||
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
|
||||
import configMixin from '../../mixins/configMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
|
||||
@@ -69,34 +53,88 @@ export default {
|
||||
ChatHeader,
|
||||
ChatHeaderExpanded,
|
||||
},
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
mixins: [configMixin],
|
||||
data() {
|
||||
return {
|
||||
showPopoutButton: false,
|
||||
scrollPosition: 0,
|
||||
ticking: true,
|
||||
disableBranding: window.chatwootWebChannel.disableBranding || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
availableAgents: 'agent/availableAgents',
|
||||
appConfig: 'appConfig/getAppConfig',
|
||||
availableAgents: 'agent/availableAgents',
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
portal() {
|
||||
return window.chatwootWebChannel.portal;
|
||||
},
|
||||
isHeaderCollapsed() {
|
||||
if (!this.hasIntroText) {
|
||||
return true;
|
||||
}
|
||||
return this.$route.name !== 'home';
|
||||
return !this.isOnHomeView;
|
||||
},
|
||||
hasIntroText() {
|
||||
return (
|
||||
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
|
||||
);
|
||||
},
|
||||
showBackButton() {
|
||||
return ['article-viewer', 'messages', 'prechat-form'].includes(
|
||||
this.$route.name
|
||||
);
|
||||
},
|
||||
isOnArticleViewer() {
|
||||
return ['article-viewer'].includes(this.$route.name);
|
||||
},
|
||||
isOnHomeView() {
|
||||
return ['home'].includes(this.$route.name);
|
||||
},
|
||||
opacityClass() {
|
||||
if (this.isHeaderCollapsed) {
|
||||
return {};
|
||||
}
|
||||
if (this.scrollPosition > 30) {
|
||||
return { 'opacity-30': true };
|
||||
}
|
||||
if (this.scrollPosition > 25) {
|
||||
return { 'opacity-40': true };
|
||||
}
|
||||
if (this.scrollPosition > 20) {
|
||||
return { 'opacity-60': true };
|
||||
}
|
||||
if (this.scrollPosition > 15) {
|
||||
return { 'opacity-80': true };
|
||||
}
|
||||
if (this.scrollPosition > 10) {
|
||||
return { 'opacity-90': true };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$el.addEventListener('scroll', this.updateScrollPosition);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$el.removeEventListener('scroll', this.updateScrollPosition);
|
||||
},
|
||||
methods: {
|
||||
closeWindow() {
|
||||
IFrameHelper.sendMessage({ event: 'closeWindow' });
|
||||
},
|
||||
updateScrollPosition(event) {
|
||||
this.scrollPosition = event.target.scrollTop;
|
||||
if (!this.ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.ticking = false;
|
||||
});
|
||||
|
||||
this.ticking = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -105,11 +143,13 @@ export default {
|
||||
@import '~widget/assets/scss/variables';
|
||||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
.custom-header-shadow {
|
||||
@include shadow-large;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
flex-shrink: 0;
|
||||
transition: max-height 300ms;
|
||||
z-index: 99;
|
||||
@include shadow-large;
|
||||
transition: max-height 100ms;
|
||||
|
||||
&.expanded {
|
||||
max-height: 16rem;
|
||||
|
||||
@@ -28,6 +28,7 @@ import pt_BR from './locale/pt_BR.json';
|
||||
import ro from './locale/ro.json';
|
||||
import ru from './locale/ru.json';
|
||||
import sk from './locale/sk.json';
|
||||
import sr from './locale/sr.json';
|
||||
import sv from './locale/sv.json';
|
||||
import ta from './locale/ta.json';
|
||||
import th from './locale/th.json';
|
||||
@@ -68,6 +69,7 @@ export default {
|
||||
ro,
|
||||
ru,
|
||||
sk,
|
||||
sr,
|
||||
sv,
|
||||
ta,
|
||||
th,
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
"START_CONVERSATION": "Start Conversation",
|
||||
"END_CONVERSATION": "End Conversation",
|
||||
"CONTINUE_CONVERSATION": "Continue conversation",
|
||||
"YOU": "You",
|
||||
"START_NEW_CONVERSATION": "Start a new conversation",
|
||||
"VIEW_UNREAD_MESSAGES": "You have unread messages",
|
||||
"UNREAD_VIEW": {
|
||||
"VIEW_MESSAGES_BUTTON": "See new messages",
|
||||
"CLOSE_MESSAGES_BUTTON": "Close",
|
||||
@@ -108,6 +110,27 @@
|
||||
},
|
||||
"PORTAL": {
|
||||
"POPULAR_ARTICLES": "Popular Articles",
|
||||
"VIEW_ALL_ARTICLES": "View all articles"
|
||||
"VIEW_ALL_ARTICLES": "View all articles",
|
||||
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again."
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
"CONTENT": "Picture message"
|
||||
},
|
||||
"audio": {
|
||||
"CONTENT": "Audio message"
|
||||
},
|
||||
"video": {
|
||||
"CONTENT": "Video message"
|
||||
},
|
||||
"file": {
|
||||
"CONTENT": "File Attachment"
|
||||
},
|
||||
"location": {
|
||||
"CONTENT": "Location"
|
||||
},
|
||||
"fallback": {
|
||||
"CONTENT": "has shared a url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ export default new Router({
|
||||
name: 'messages',
|
||||
component: () => import('./views/Messages.vue'),
|
||||
},
|
||||
{
|
||||
path: '/article',
|
||||
name: 'article-viewer',
|
||||
props: true,
|
||||
component: () => import('./views/ArticleViewer.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,25 +1,53 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col justify-end">
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<!-- Load Conversation List Components Here -->
|
||||
<div
|
||||
class="z-50 rounded-md w-full flex flex-1 flex-col"
|
||||
:class="{ 'pb-2': showArticles, 'justify-end': !showArticles }"
|
||||
>
|
||||
<div class="px-4 pt-4 w-full">
|
||||
<team-availability
|
||||
:available-agents="availableAgents"
|
||||
:has-conversation="!!conversationSize"
|
||||
:unread-count="unreadMessageCount"
|
||||
@start-conversation="startConversation"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showArticles" class="px-4 py-2 w-full">
|
||||
<div class="p-4 rounded-md bg-white dark:bg-slate-700 shadow-sm w-full">
|
||||
<article-hero
|
||||
v-if="
|
||||
!articleUiFlags.isFetching &&
|
||||
!articleUiFlags.isError &&
|
||||
popularArticles.length
|
||||
"
|
||||
:articles="popularArticles"
|
||||
@view="openArticleInArticleViewer"
|
||||
@view-all="viewAllArticles"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="articleUiFlags.isFetching" class="px-4 py-2 w-full">
|
||||
<div class="p-4 rounded-md bg-white dark:bg-slate-700 shadow-sm w-full">
|
||||
<article-card-skeleton-loader />
|
||||
</div>
|
||||
</div>
|
||||
<team-availability
|
||||
:available-agents="availableAgents"
|
||||
:has-conversation="!!conversationSize"
|
||||
@start-conversation="startConversation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import TeamAvailability from 'widget/components/TeamAvailability';
|
||||
import ArticleHero from 'widget/components/ArticleHero';
|
||||
import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoader';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
ArticleHero,
|
||||
TeamAvailability,
|
||||
ArticleCardSkeletonLoader,
|
||||
},
|
||||
mixins: [configMixin, routerMixin],
|
||||
props: {
|
||||
@@ -32,15 +60,33 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
availableAgents: 'agent/availableAgents',
|
||||
activeCampaign: 'campaign/getActiveCampaign',
|
||||
conversationSize: 'conversation/getConversationSize',
|
||||
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
||||
popularArticles: 'article/popularArticles',
|
||||
articleUiFlags: 'article/uiFlags',
|
||||
}),
|
||||
portal() {
|
||||
return window.chatwootWebChannel.portal;
|
||||
},
|
||||
showArticles() {
|
||||
return (
|
||||
this.portal &&
|
||||
!this.articleUiFlags.isFetching &&
|
||||
this.popularArticles.length
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.portal && this.popularArticles.length === 0) {
|
||||
this.$store.dispatch('article/fetch', {
|
||||
slug: this.portal.slug,
|
||||
locale: 'en',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
@@ -49,6 +95,19 @@ export default {
|
||||
}
|
||||
return this.replaceRoute('messages');
|
||||
},
|
||||
openArticleInArticleViewer(link) {
|
||||
this.$router.push({
|
||||
name: 'article-viewer',
|
||||
params: { link: `${link}?show_plain_layout=true` },
|
||||
});
|
||||
},
|
||||
viewAllArticles() {
|
||||
const {
|
||||
portal: { slug },
|
||||
locale = 'en',
|
||||
} = window.chatwootWebChannel;
|
||||
this.openArticleInArticleViewer(`/hc/${slug}/${locale}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-hidden">
|
||||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden rounded-b-lg bg-slate-25 dark:bg-slate-800"
|
||||
>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<conversation-wrap :grouped-messages="groupedMessages" />
|
||||
</div>
|
||||
@@ -10,6 +12,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ChatFooter from '../components/ChatFooter.vue';
|
||||
import ConversationWrap from '../components/ConversationWrap.vue';
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ class ActionService
|
||||
end
|
||||
|
||||
def assign_agent(agent_ids = [])
|
||||
return @conversation.update!(assignee_id: nil) if agent_ids[0] == 'nil'
|
||||
|
||||
return unless agent_belongs_to_inbox?(agent_ids)
|
||||
|
||||
@agent = @account.users.find_by(id: agent_ids)
|
||||
|
||||
@@ -57,7 +57,13 @@ class Sms::IncomingMessageService
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.conversations.last
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations.where
|
||||
.not(status: :resolved).last
|
||||
end
|
||||
return if @conversation
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
|
||||
@@ -47,7 +47,7 @@ class Twilio::IncomingMessageService
|
||||
end
|
||||
|
||||
def message_body
|
||||
params[:Body].delete("\u0000")
|
||||
params[:Body]&.delete("\u0000")
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@@ -72,7 +72,13 @@ class Twilio::IncomingMessageService
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.conversations.first
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations.where
|
||||
.not(status: :resolved).last
|
||||
end
|
||||
return if @conversation
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
|
||||
@@ -91,7 +91,13 @@ class Whatsapp::IncomingMessageBaseService
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.conversations.last
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations
|
||||
.where.not(status: :resolved).last
|
||||
end
|
||||
return if @conversation
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
|
||||
@@ -38,3 +38,5 @@ json.associated_articles do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
json.link "hc/#{article.portal.slug}/articles/#{article.slug}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
json.payload do
|
||||
json.array! @articles, partial: 'public/api/v1/models/article', formats: [:json], as: :article
|
||||
json.array! @articles.includes([:category, :associated_articles, { author: { avatar_attachment: [:blob] } }]),
|
||||
partial: 'public/api/v1/models/article', formats: [:json], as: :article
|
||||
end
|
||||
|
||||
json.meta do
|
||||
|
||||
@@ -19,7 +19,7 @@ window.chatwootSettings = {
|
||||
locale: 'en',
|
||||
useBrowserLanguage: true,
|
||||
type: '<%= @widget_type %>',
|
||||
showPopoutButton: true,
|
||||
// showPopoutButton: true,
|
||||
widgetStyle: '<%= @widget_style %>',
|
||||
darkMode: '<%= @dark_mode %>',
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
|
||||
welcomeTitle: '<%= @web_widget.welcome_title %>',
|
||||
widgetColor: '<%= @web_widget.widget_color %>',
|
||||
portal: <%= @web_widget.inbox.portal.to_json.html_safe %>,
|
||||
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
|
||||
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
|
||||
replyTime: '<%= @web_widget.reply_time %>',
|
||||
|
||||
@@ -40,7 +40,8 @@ LANGUAGES_CONFIG = {
|
||||
35 => { name: 'latviešu valoda (lv)', iso_639_3_code: 'lav', iso_639_1_code: 'lv', enabled: true },
|
||||
36 => { name: 'íslenska (is)', iso_639_3_code: 'isl', iso_639_1_code: 'is', enabled: true },
|
||||
37 => { name: 'עִברִית (he)', iso_639_3_code: 'heb', iso_639_1_code: 'he', enabled: true },
|
||||
38 => { name: 'lietuvių (lt)', iso_639_3_code: 'lit', iso_639_1_code: 'lt', enabled: true }
|
||||
38 => { name: 'lietuvių (lt)', iso_639_3_code: 'lit', iso_639_1_code: 'lt', enabled: true },
|
||||
39 => { name: 'Српски (sr)', iso_639_3_code: 'srp', iso_639_1_code: 'sr', enabled: true }
|
||||
}.filter { |_key, val| val[:enabled] }.freeze
|
||||
|
||||
Rails.configuration.i18n.available_locales = LANGUAGES_CONFIG.map { |_index, lang| lang[:iso_639_1_code].to_sym }
|
||||
|
||||
@@ -67,6 +67,18 @@ RSpec.describe 'API Base', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
# this exception occured in a client instance (DoubleRender error)
|
||||
it 'will not throw exception if user does not have access to suspended account' do
|
||||
user_with_out_access = create(:user)
|
||||
account.update!(status: :suspended)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/canned_responses",
|
||||
headers: { api_access_token: user_with_out_access.access_token.token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,7 +87,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
||||
end
|
||||
|
||||
it 'returns includes conversations count and last seen at' do
|
||||
it 'returns last seen at' do
|
||||
create(:conversation, contact: contact, account: account, inbox: contact_inbox.inbox, contact_last_seen_at: Time.now.utc)
|
||||
get "/api/v1/accounts/#{account.id}/contacts",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -95,7 +95,6 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['conversations_count']).to eq(contact.conversations.count)
|
||||
expect(response_body['payload'].first['last_seen_at']).present?
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ActionService do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#resolve_conversation' do
|
||||
let(:conversation) { create(:conversation) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
@@ -27,5 +29,17 @@ describe ActionService do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assign_agent' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'unassigns the conversation if agent id is nil' do
|
||||
action_service.assign_agent(['nil'])
|
||||
expect(conversation.reload.assignee).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Expand this test suite
|
||||
end
|
||||
|
||||
@@ -37,7 +37,43 @@ describe Sms::IncomingMessageService do
|
||||
# no new conversation should be created
|
||||
expect(sms_channel.inbox.conversations.count).to eq(3)
|
||||
# message appended to the last conversation
|
||||
expect(last_conversation.messages.last.content).to eq('test message')
|
||||
expect(last_conversation.messages.last.content).to eq(params[:text])
|
||||
end
|
||||
|
||||
it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
|
||||
sms_channel.inbox.update(lock_to_single_conversation: true)
|
||||
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
|
||||
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: 'resolved')
|
||||
described_class.new(inbox: sms_channel.inbox, params: params).perform
|
||||
# no new conversation should be created
|
||||
expect(sms_channel.inbox.conversations.count).to eq(1)
|
||||
expect(sms_channel.inbox.conversations.open.last.messages.last.content).to eq(params[:text])
|
||||
expect(sms_channel.inbox.conversations.open.last.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
|
||||
sms_channel.inbox.update(lock_to_single_conversation: false)
|
||||
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
|
||||
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: 'resolved')
|
||||
described_class.new(inbox: sms_channel.inbox, params: params).perform
|
||||
# new conversation should be created
|
||||
expect(sms_channel.inbox.conversations.count).to eq(2)
|
||||
# message appended to the last conversation
|
||||
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:text])
|
||||
end
|
||||
|
||||
it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
|
||||
sms_channel.inbox.update(lock_to_single_conversation: false)
|
||||
contact_inbox = create(:contact_inbox, inbox: sms_channel.inbox, source_id: params[:from])
|
||||
last_conversation = create(:conversation, inbox: sms_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
|
||||
described_class.new(inbox: sms_channel.inbox, params: params).perform
|
||||
# new conversation should be created
|
||||
expect(sms_channel.inbox.conversations.count).to eq(1)
|
||||
# message appended to the last conversation
|
||||
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:text])
|
||||
end
|
||||
|
||||
it 'creates attachment messages and ignores .smil files' do
|
||||
|
||||
@@ -37,7 +37,19 @@ describe Twilio::IncomingMessageService do
|
||||
expect(conversation.reload.messages.last.content).to eq('remove null bytes')
|
||||
end
|
||||
|
||||
it 'creates a new conversation' do
|
||||
it 'wont throw error when the body is empty' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
MessagingServiceSid: twilio_channel.messaging_service_sid
|
||||
}
|
||||
|
||||
described_class.new(params: params).perform
|
||||
expect(conversation.reload.messages.last.content).to be_nil
|
||||
end
|
||||
|
||||
it 'creates a new conversation when payload is from different number' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+123456',
|
||||
@@ -50,6 +62,7 @@ describe Twilio::IncomingMessageService do
|
||||
expect(twilio_channel.inbox.conversations.count).to eq(2)
|
||||
end
|
||||
|
||||
# Since we support the case with phone number as well. the previous case is with accoud_sid and messaging_service_sid
|
||||
context 'with a phone number' do
|
||||
let!(:twilio_channel) do
|
||||
create(:channel_twilio_sms, :with_phone_number, account: account, account_sid: 'ACxxx',
|
||||
@@ -69,7 +82,7 @@ describe Twilio::IncomingMessageService do
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
end
|
||||
|
||||
it 'creates a new conversation' do
|
||||
it 'creates a new conversation when payload is from different number' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+123456',
|
||||
@@ -81,6 +94,55 @@ describe Twilio::IncomingMessageService do
|
||||
described_class.new(params: params).perform
|
||||
expect(twilio_channel.inbox.conversations.count).to eq(2)
|
||||
end
|
||||
|
||||
it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
To: twilio_channel.phone_number,
|
||||
Body: 'testing3'
|
||||
}
|
||||
|
||||
twilio_channel.inbox.update(lock_to_single_conversation: true)
|
||||
conversation.update(status: 'resolved')
|
||||
described_class.new(params: params).perform
|
||||
# message appended to the last conversation
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
To: twilio_channel.phone_number,
|
||||
Body: 'testing3'
|
||||
}
|
||||
|
||||
twilio_channel.inbox.update(lock_to_single_conversation: false)
|
||||
conversation.update(status: 'resolved')
|
||||
described_class.new(params: params).perform
|
||||
expect(twilio_channel.inbox.conversations.count).to eq(2)
|
||||
expect(twilio_channel.inbox.conversations.last.messages.last.content).to eq('testing3')
|
||||
end
|
||||
|
||||
it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
|
||||
params = {
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
To: twilio_channel.phone_number,
|
||||
Body: 'testing3'
|
||||
}
|
||||
|
||||
twilio_channel.inbox.update(lock_to_single_conversation: false)
|
||||
conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
|
||||
described_class.new(params: params).perform
|
||||
expect(twilio_channel.inbox.conversations.count).to eq(1)
|
||||
expect(twilio_channel.inbox.conversations.last.messages.last.content).to eq('testing3')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple channels configured' do
|
||||
|
||||
@@ -35,6 +35,41 @@ describe Whatsapp::IncomingMessageService do
|
||||
expect(last_conversation.messages.last.content).to eq(params[:messages].first[:text][:body])
|
||||
end
|
||||
|
||||
it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
|
||||
whatsapp_channel.inbox.update(lock_to_single_conversation: true)
|
||||
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
|
||||
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: 'resolved')
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||
# no new conversation should be created
|
||||
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
|
||||
# message appended to the last conversation
|
||||
expect(last_conversation.messages.last.content).to eq(params[:messages].first[:text][:body])
|
||||
expect(last_conversation.reload.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
|
||||
whatsapp_channel.inbox.update(lock_to_single_conversation: false)
|
||||
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
|
||||
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: 'resolved')
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||
# new conversation should be created
|
||||
expect(whatsapp_channel.inbox.conversations.count).to eq(2)
|
||||
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:messages].first[:text][:body])
|
||||
end
|
||||
|
||||
it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
|
||||
whatsapp_channel.inbox.update(lock_to_single_conversation: false)
|
||||
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
|
||||
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
|
||||
last_conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||
# new conversation should be created
|
||||
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
|
||||
expect(contact_inbox.conversations.last.messages.last.content).to eq(params[:messages].first[:text][:body])
|
||||
end
|
||||
|
||||
it 'will not create duplicate messages when same message is received' do
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||
expect(whatsapp_channel.inbox.messages.count).to eq(1)
|
||||
|
||||
Reference in New Issue
Block a user