Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
b155f64fe2
54 changed files with 657 additions and 175 deletions
|
@ -60,9 +60,10 @@ MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||||
|
|
||||||
#SMTP domain key is set up for HELO checking
|
#SMTP domain key is set up for HELO checking
|
||||||
SMTP_DOMAIN=chatwoot.com
|
SMTP_DOMAIN=chatwoot.com
|
||||||
# the default value is set "mailhog" and is used by docker-compose for development environments,
|
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||||
# Set the value as "localhost" or your SMTP address in other environments
|
# Set the value as "localhost" or your SMTP address in other environments
|
||||||
SMTP_ADDRESS=mailhog
|
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||||
|
SMTP_ADDRESS=
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
|
@ -16,7 +16,6 @@ Metrics/ClassLength:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||||
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
|
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
- 'app/listeners/action_cable_listener.rb'
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
|
|
40
app/builders/conversation_builder.rb
Normal file
40
app/builders/conversation_builder.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
class ConversationBuilder
|
||||||
|
pattr_initialize [:params!, :contact_inbox!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
look_up_exising_conversation || create_new_conversation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def look_up_exising_conversation
|
||||||
|
return unless @contact_inbox.inbox.lock_to_single_conversation?
|
||||||
|
|
||||||
|
@contact_inbox.conversations.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_new_conversation
|
||||||
|
::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||||
|
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||||
|
status = params[:status].present? ? { status: params[:status] } : {}
|
||||||
|
|
||||||
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = { status: 'pending' } if status[:status] == 'bot'
|
||||||
|
{
|
||||||
|
account_id: @contact_inbox.inbox.account_id,
|
||||||
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
|
contact_id: @contact_inbox.contact_id,
|
||||||
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: additional_attributes,
|
||||||
|
custom_attributes: custom_attributes,
|
||||||
|
snoozed_until: params[:snoozed_until],
|
||||||
|
assignee_id: params[:assignee_id],
|
||||||
|
team_id: params[:team_id]
|
||||||
|
}.merge(status)
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@conversation = ::Conversation.create!(conversation_params)
|
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||||
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -99,8 +99,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation_status
|
def set_conversation_status
|
||||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
@conversation.status = status
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||||
|
@conversation.status = params[:status]
|
||||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -152,26 +154,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
|
||||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
|
||||||
status = params[:status].present? ? { status: params[:status] } : {}
|
|
||||||
|
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
status = { status: 'pending' } if status[:status] == 'bot'
|
|
||||||
{
|
|
||||||
account_id: Current.account.id,
|
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
|
||||||
contact_id: @contact_inbox.contact_id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
additional_attributes: additional_attributes,
|
|
||||||
custom_attributes: custom_attributes,
|
|
||||||
snoozed_until: params[:snoozed_until],
|
|
||||||
assignee_id: params[:assignee_id],
|
|
||||||
team_id: params[:team_id]
|
|
||||||
}.merge(status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_finder
|
||||||
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -113,7 +113,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def inbox_attributes
|
def inbox_attributes
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||||
|
:lock_to_single_conversation]
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
|
|
|
@ -56,7 +56,6 @@ class ConversationFinder
|
||||||
filter_by_team if @team
|
filter_by_team if @team
|
||||||
filter_by_labels if params[:labels]
|
filter_by_labels if params[:labels]
|
||||||
filter_by_query if params[:q]
|
filter_by_query if params[:q]
|
||||||
filter_by_reply_status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_inboxes
|
def set_inboxes
|
||||||
|
@ -76,12 +75,9 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
if params[:conversation_type] == 'mention'
|
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||||
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
filter_by_conversation_type if params[:conversation_type]
|
||||||
@conversations = current_account.conversations.where(id: conversation_ids)
|
@conversations
|
||||||
else
|
|
||||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
|
@ -96,8 +92,15 @@ class ConversationFinder
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_reply_status
|
def filter_by_conversation_type
|
||||||
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
|
case @params[:conversation_type]
|
||||||
|
when 'mention'
|
||||||
|
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
||||||
|
@conversations = @conversations.where(id: conversation_ids)
|
||||||
|
when 'unattended'
|
||||||
|
@conversations = @conversations.where(first_reply_created_at: nil)
|
||||||
|
end
|
||||||
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
|
|
|
@ -335,10 +335,8 @@ export default {
|
||||||
status: this.activeStatus,
|
status: this.activeStatus,
|
||||||
page: this.currentPage + 1,
|
page: this.currentPage + 1,
|
||||||
labels: this.label ? [this.label] : undefined,
|
labels: this.label ? [this.label] : undefined,
|
||||||
teamId: this.teamId ? this.teamId : undefined,
|
teamId: this.teamId || undefined,
|
||||||
conversationType: this.conversationType
|
conversationType: this.conversationType || undefined,
|
||||||
? this.conversationType
|
|
||||||
: undefined,
|
|
||||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -355,6 +353,9 @@ export default {
|
||||||
if (this.conversationType === 'mention') {
|
if (this.conversationType === 'mention') {
|
||||||
return this.$t('CHAT_LIST.MENTION_HEADING');
|
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||||
}
|
}
|
||||||
|
if (this.conversationType === 'unattended') {
|
||||||
|
return this.$t('CHAT_LIST.UNATTENDED_HEADING');
|
||||||
|
}
|
||||||
if (this.hasActiveFolders) {
|
if (this.hasActiveFolders) {
|
||||||
return this.activeFolder.name;
|
return this.activeFolder.name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ const conversations = accountId => ({
|
||||||
'conversation_through_mentions',
|
'conversation_through_mentions',
|
||||||
'folder_conversations',
|
'folder_conversations',
|
||||||
'conversations_through_folders',
|
'conversations_through_folders',
|
||||||
|
'conversation_unattended',
|
||||||
|
'conversation_through_unattended',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
|
@ -33,6 +35,13 @@ const conversations = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
||||||
toStateName: 'conversation_mentions',
|
toStateName: 'conversation_mentions',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'mail-unread',
|
||||||
|
label: 'UNATTENDED_CONVERSATIONS',
|
||||||
|
key: 'conversation_unattended',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
|
||||||
|
toStateName: 'conversation_unattended',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -233,7 +233,9 @@ export default {
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
return this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
insertCannedResponse(cannedItem) {
|
insertCannedResponse(cannedItem) {
|
||||||
|
@ -241,22 +243,26 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.insertText(
|
let from = this.range.from - 1;
|
||||||
cannedItem,
|
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
|
||||||
this.range.from,
|
cannedItem
|
||||||
this.range.to
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (node.childCount === 1) {
|
||||||
|
node = this.editorView.state.schema.text(cannedItem);
|
||||||
|
from = this.range.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = this.editorView.state.tr.replaceWith(
|
||||||
|
from,
|
||||||
|
this.range.to,
|
||||||
|
node
|
||||||
|
);
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
|
||||||
// Hacky fix for #5501
|
tr.scrollIntoView();
|
||||||
this.state = createState(
|
|
||||||
this.contentFromEditor,
|
|
||||||
this.placeholder,
|
|
||||||
this.plugins
|
|
||||||
);
|
|
||||||
this.editorView.updateState(this.state);
|
|
||||||
this.focusEditorInputField();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -59,10 +59,12 @@
|
||||||
:story-sender="storySender"
|
:story-sender="storySender"
|
||||||
:story-id="storyId"
|
:story-id="storyId"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:is-email="isEmailContentType"
|
:is-email="isEmailContentType"
|
||||||
:is-private="data.private"
|
:is-private="data.private"
|
||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
|
:message-status="status"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
:source-id="data.source_id"
|
:source-id="data.source_id"
|
||||||
:inbox-id="data.inbox_id"
|
:inbox-id="data.inbox_id"
|
||||||
|
@ -157,6 +159,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isAWhatsAppChannel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
hasInstagramStory: {
|
hasInstagramStory: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -231,6 +237,9 @@ export default {
|
||||||
sender() {
|
sender() {
|
||||||
return this.data.sender || {};
|
return this.data.sender || {};
|
||||||
},
|
},
|
||||||
|
status() {
|
||||||
|
return this.data.status;
|
||||||
|
},
|
||||||
storySender() {
|
storySender() {
|
||||||
return this.contentAttributes.story_sender || null;
|
return this.contentAttributes.story_sender || null;
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
class="message--read ph-no-capture"
|
class="message--read ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
:has-user-read-message="
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
class="message--unread ph-no-capture"
|
class="message--unread ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
:has-user-read-message="
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||||
|
|
|
@ -3,20 +3,30 @@
|
||||||
<span class="time" :class="{ delivered: messageRead }">{{
|
<span class="time" :class="{ delivered: messageRead }">{{
|
||||||
readableTime
|
readableTime
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="showSentIndicator" class="time">
|
<span v-if="showReadIndicator" class="read-indicator-wrap">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
||||||
icon="checkmark"
|
icon="checkmark-double"
|
||||||
|
class="action--icon read-tick read-indicator"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-if="showDeliveredIndicator" class="read-indicator-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
|
||||||
|
icon="checkmark-double"
|
||||||
|
class="action--icon read-tick"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-if="showSentIndicator" class="read-indicator-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||||
|
icon="checkmark"
|
||||||
|
class="action--icon read-tick"
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<fluent-icon
|
|
||||||
v-if="messageRead"
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
|
||||||
icon="checkmark-double"
|
|
||||||
class="action--icon read-tick"
|
|
||||||
size="12"
|
|
||||||
/>
|
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="isEmail"
|
v-if="isEmail"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||||
|
@ -74,7 +84,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
|
|
||||||
|
@ -117,6 +127,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
messageStatus: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
sourceId: {
|
sourceId: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -144,6 +158,15 @@ export default {
|
||||||
isOutgoing() {
|
isOutgoing() {
|
||||||
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||||
},
|
},
|
||||||
|
isDelivered() {
|
||||||
|
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
|
||||||
|
},
|
||||||
|
isRead() {
|
||||||
|
return MESSAGE_STATUS.READ === this.messageStatus;
|
||||||
|
},
|
||||||
|
isSent() {
|
||||||
|
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||||
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
this.sender || {};
|
this.sender || {};
|
||||||
|
@ -168,7 +191,23 @@ export default {
|
||||||
return (
|
return (
|
||||||
this.isOutgoing &&
|
this.isOutgoing &&
|
||||||
this.sourceId &&
|
this.sourceId &&
|
||||||
(this.isAnEmailChannel || this.isAWhatsAppChannel)
|
(this.isAnEmailChannel || (this.isAWhatsAppChannel && this.isSent))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showDeliveredIndicator() {
|
||||||
|
return (
|
||||||
|
this.isOutgoing &&
|
||||||
|
this.sourceId &&
|
||||||
|
this.isAWhatsAppChannel &&
|
||||||
|
this.isDelivered
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showReadIndicator() {
|
||||||
|
return (
|
||||||
|
this.isOutgoing &&
|
||||||
|
this.sourceId &&
|
||||||
|
this.isAWhatsAppChannel &&
|
||||||
|
this.isRead
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -185,16 +224,20 @@ export default {
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
.message-text--metadata {
|
.message-text--metadata {
|
||||||
|
align-items: center;
|
||||||
.time {
|
.time {
|
||||||
color: var(--w-100);
|
color: var(--w-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action--icon {
|
.action--icon {
|
||||||
|
color: var(--white);
|
||||||
&.read-tick {
|
&.read-tick {
|
||||||
color: var(--v-100);
|
color: var(--v-100);
|
||||||
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
|
|
||||||
}
|
}
|
||||||
color: var(--white);
|
|
||||||
|
&.read-indicator {
|
||||||
|
color: var(--g-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lock--icon--private {
|
.lock--icon--private {
|
||||||
|
@ -296,4 +339,10 @@ export default {
|
||||||
.delivered-icon {
|
.delivered-icon {
|
||||||
margin-left: -var(--space-normal);
|
margin-left: -var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.read-indicator-wrap {
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -56,6 +56,8 @@ export const conversationUrl = ({
|
||||||
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
||||||
} else if (conversationType === 'mention') {
|
} else if (conversationType === 'mention') {
|
||||||
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
||||||
|
} else if (conversationType === 'unattended') {
|
||||||
|
url = `accounts/${accountId}/unattended/conversations/${id}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
},
|
},
|
||||||
"TAB_HEADING": "Conversations",
|
"TAB_HEADING": "Conversations",
|
||||||
"MENTION_HEADING": "Mentions",
|
"MENTION_HEADING": "Mentions",
|
||||||
|
"UNATTENDED_HEADING": "Unattended",
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"INPUT": "Search for People, Chats, Saved Replies .."
|
"INPUT": "Search for People, Chats, Saved Replies .."
|
||||||
},
|
},
|
||||||
|
@ -56,6 +57,8 @@
|
||||||
"REPLY_TO_TWEET": "Reply to this tweet",
|
"REPLY_TO_TWEET": "Reply to this tweet",
|
||||||
"LINK_TO_STORY": "Go to instagram story",
|
"LINK_TO_STORY": "Go to instagram story",
|
||||||
"SENT": "Sent successfully",
|
"SENT": "Sent successfully",
|
||||||
|
"READ": "Read successfully",
|
||||||
|
"DELIVERED": "Delivered successfully",
|
||||||
"NO_MESSAGES": "No Messages",
|
"NO_MESSAGES": "No Messages",
|
||||||
"NO_CONTENT": "No content available",
|
"NO_CONTENT": "No content available",
|
||||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||||
|
|
|
@ -388,6 +388,10 @@
|
||||||
"ENABLED": "Enabled",
|
"ENABLED": "Enabled",
|
||||||
"DISABLED": "Disabled"
|
"DISABLED": "Disabled"
|
||||||
},
|
},
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION": {
|
||||||
|
"ENABLED": "Enabled",
|
||||||
|
"DISABLED": "Disabled"
|
||||||
|
},
|
||||||
"ENABLE_HMAC": {
|
"ENABLE_HMAC": {
|
||||||
"LABEL": "Enable"
|
"LABEL": "Enable"
|
||||||
}
|
}
|
||||||
|
@ -441,6 +445,8 @@
|
||||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
|
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
|
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
|
||||||
"INBOX_UPDATE_TITLE": "Inbox Settings",
|
"INBOX_UPDATE_TITLE": "Inbox Settings",
|
||||||
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
||||||
|
|
|
@ -177,6 +177,7 @@
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"ALL_CONVERSATIONS": "All Conversations",
|
"ALL_CONVERSATIONS": "All Conversations",
|
||||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||||
|
"UNATTENDED_CONVERSATIONS": "Unattended",
|
||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
|
|
|
@ -121,5 +121,25 @@ export default {
|
||||||
conversationType: 'mention',
|
conversationType: 'mention',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/unattended/conversations'),
|
||||||
|
name: 'conversation_unattended',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: ConversationView,
|
||||||
|
props: () => ({ conversationType: 'unattended' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL(
|
||||||
|
'accounts/:accountId/unattended/conversations/:conversationId'
|
||||||
|
),
|
||||||
|
name: 'conversation_through_unattended',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: ConversationView,
|
||||||
|
props: route => ({
|
||||||
|
conversationId: route.params.conversationId,
|
||||||
|
conversationType: 'unattended',
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -258,6 +258,28 @@
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
v-if="canLocktoSingleConversation"
|
||||||
|
class="medium-9 columns settings-item"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX_MGMT.SETTINGS_POPUP.LOCK_TO_SINGLE_CONVERSATION') }}
|
||||||
|
<select v-model="locktoSingleConversation">
|
||||||
|
<option :value="true">
|
||||||
|
{{ $t('INBOX_MGMT.EDIT.LOCK_TO_SINGLE_CONVERSATION.ENABLED') }}
|
||||||
|
</option>
|
||||||
|
<option :value="false">
|
||||||
|
{{ $t('INBOX_MGMT.EDIT.LOCK_TO_SINGLE_CONVERSATION.DISABLED') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="help-text">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'INBOX_MGMT.SETTINGS_POPUP.LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label v-if="isAWebWidgetInbox">
|
<label v-if="isAWebWidgetInbox">
|
||||||
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
|
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -380,6 +402,7 @@ export default {
|
||||||
greetingMessage: '',
|
greetingMessage: '',
|
||||||
emailCollectEnabled: false,
|
emailCollectEnabled: false,
|
||||||
csatSurveyEnabled: false,
|
csatSurveyEnabled: false,
|
||||||
|
locktoSingleConversation: false,
|
||||||
allowMessagesAfterResolved: true,
|
allowMessagesAfterResolved: true,
|
||||||
continuityViaEmail: true,
|
continuityViaEmail: true,
|
||||||
selectedInboxName: '',
|
selectedInboxName: '',
|
||||||
|
@ -496,6 +519,9 @@ export default {
|
||||||
}
|
}
|
||||||
return this.inbox.name;
|
return this.inbox.name;
|
||||||
},
|
},
|
||||||
|
canLocktoSingleConversation() {
|
||||||
|
return this.isASmsInbox || this.isAWhatsAppChannel;
|
||||||
|
},
|
||||||
inboxNameLabel() {
|
inboxNameLabel() {
|
||||||
if (this.isAWebWidgetInbox) {
|
if (this.isAWebWidgetInbox) {
|
||||||
return this.$t('INBOX_MGMT.ADD.WEBSITE_NAME.LABEL');
|
return this.$t('INBOX_MGMT.ADD.WEBSITE_NAME.LABEL');
|
||||||
|
@ -567,6 +593,7 @@ export default {
|
||||||
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
||||||
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
|
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
|
||||||
this.replyTime = this.inbox.reply_time;
|
this.replyTime = this.inbox.reply_time;
|
||||||
|
this.locktoSingleConversation = this.inbox.lock_to_single_conversation;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async updateInbox() {
|
async updateInbox() {
|
||||||
|
@ -579,6 +606,7 @@ export default {
|
||||||
allow_messages_after_resolved: this.allowMessagesAfterResolved,
|
allow_messages_after_resolved: this.allowMessagesAfterResolved,
|
||||||
greeting_enabled: this.greetingEnabled,
|
greeting_enabled: this.greetingEnabled,
|
||||||
greeting_message: this.greetingMessage || '',
|
greeting_message: this.greetingMessage || '',
|
||||||
|
lock_to_single_conversation: this.locktoSingleConversation,
|
||||||
channel: {
|
channel: {
|
||||||
widget_color: this.inbox.widget_color,
|
widget_color: this.inbox.widget_color,
|
||||||
website_url: this.channelWebsiteUrl,
|
website_url: this.channelWebsiteUrl,
|
||||||
|
|
|
@ -89,7 +89,19 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
[types.default.ADD_CONTACT_CONVERSATION]: ($state, { id, data }) => {
|
[types.default.ADD_CONTACT_CONVERSATION]: ($state, { id, data }) => {
|
||||||
const conversations = $state.records[id] || [];
|
const conversations = $state.records[id] || [];
|
||||||
Vue.set($state.records, id, [...conversations, data]);
|
|
||||||
|
const updatedConversations = [...conversations];
|
||||||
|
const index = conversations.findIndex(
|
||||||
|
conversation => conversation.id === data.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
updatedConversations[index] = { ...conversations[index], ...data };
|
||||||
|
} else {
|
||||||
|
updatedConversations.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.set($state.records, id, updatedConversations);
|
||||||
},
|
},
|
||||||
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
||||||
Vue.delete($state.records, id);
|
Vue.delete($state.records, id);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { createPendingMessage } from 'dashboard/helper/commons';
|
||||||
import {
|
import {
|
||||||
buildConversationList,
|
buildConversationList,
|
||||||
isOnMentionsView,
|
isOnMentionsView,
|
||||||
|
isOnUnattendedView,
|
||||||
} from './helpers/actionHelpers';
|
} from './helpers/actionHelpers';
|
||||||
import messageReadActions from './actions/messageReadActions';
|
import messageReadActions from './actions/messageReadActions';
|
||||||
// actions
|
// actions
|
||||||
|
@ -230,6 +231,7 @@ const actions = {
|
||||||
if (
|
if (
|
||||||
!hasAppliedFilters &&
|
!hasAppliedFilters &&
|
||||||
!isOnMentionsView(rootState) &&
|
!isOnMentionsView(rootState) &&
|
||||||
|
!isOnUnattendedView(rootState) &&
|
||||||
isMatchingInboxFilter
|
isMatchingInboxFilter
|
||||||
) {
|
) {
|
||||||
commit(types.ADD_CONVERSATION, conversation);
|
commit(types.ADD_CONVERSATION, conversation);
|
||||||
|
@ -243,6 +245,12 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addUnattended({ dispatch, rootState }, conversation) {
|
||||||
|
if (isOnUnattendedView(rootState)) {
|
||||||
|
dispatch('updateConversation', conversation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateConversation({ commit, dispatch }, conversation) {
|
updateConversation({ commit, dispatch }, conversation) {
|
||||||
const {
|
const {
|
||||||
meta: { sender },
|
meta: { sender },
|
||||||
|
|
|
@ -5,33 +5,54 @@ export const findPendingMessageIndex = (chat, message) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterByStatus = (chatStatus, filterStatus) =>
|
export const filterByStatus = (chatStatus, filterStatus) =>
|
||||||
filterStatus === 'all' ? true : chatStatus === filterStatus;
|
filterStatus === 'all' ? true : chatStatus === filterStatus;
|
||||||
|
|
||||||
|
export const filterByInbox = (shouldFilter, inboxId, chatInboxId) => {
|
||||||
|
const isOnInbox = Number(inboxId) === chatInboxId;
|
||||||
|
return inboxId ? isOnInbox && shouldFilter : shouldFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterByTeam = (shouldFilter, teamId, chatTeamId) => {
|
||||||
|
const isOnTeam = Number(teamId) === chatTeamId;
|
||||||
|
return teamId ? isOnTeam && shouldFilter : shouldFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterByLabel = (shouldFilter, labels, chatLabels) => {
|
||||||
|
const isOnLabel = labels.every(label => chatLabels.includes(label));
|
||||||
|
return labels.length ? isOnLabel && shouldFilter : shouldFilter;
|
||||||
|
};
|
||||||
|
export const filterByUnattended = (
|
||||||
|
shouldFilter,
|
||||||
|
conversationType,
|
||||||
|
firstReplyOn
|
||||||
|
) => {
|
||||||
|
return conversationType === 'unattended'
|
||||||
|
? !firstReplyOn && shouldFilter
|
||||||
|
: shouldFilter;
|
||||||
|
};
|
||||||
|
|
||||||
export const applyPageFilters = (conversation, filters) => {
|
export const applyPageFilters = (conversation, filters) => {
|
||||||
const { inboxId, status, labels = [], teamId } = filters;
|
const { inboxId, status, labels = [], teamId, conversationType } = filters;
|
||||||
const {
|
const {
|
||||||
status: chatStatus,
|
status: chatStatus,
|
||||||
inbox_id: chatInboxId,
|
inbox_id: chatInboxId,
|
||||||
labels: chatLabels = [],
|
labels: chatLabels = [],
|
||||||
meta = {},
|
meta = {},
|
||||||
|
first_reply_created_at: firstReplyOn,
|
||||||
} = conversation;
|
} = conversation;
|
||||||
const team = meta.team || {};
|
const team = meta.team || {};
|
||||||
const { id: chatTeamId } = team;
|
const { id: chatTeamId } = team;
|
||||||
|
|
||||||
let shouldFilter = filterByStatus(chatStatus, status);
|
let shouldFilter = filterByStatus(chatStatus, status);
|
||||||
if (inboxId) {
|
shouldFilter = filterByInbox(shouldFilter, inboxId, chatInboxId);
|
||||||
const filterByInbox = Number(inboxId) === chatInboxId;
|
shouldFilter = filterByTeam(shouldFilter, teamId, chatTeamId);
|
||||||
shouldFilter = shouldFilter && filterByInbox;
|
shouldFilter = filterByLabel(shouldFilter, labels, chatLabels);
|
||||||
}
|
shouldFilter = filterByUnattended(
|
||||||
if (teamId) {
|
shouldFilter,
|
||||||
const filterByTeam = Number(teamId) === chatTeamId;
|
conversationType,
|
||||||
shouldFilter = shouldFilter && filterByTeam;
|
firstReplyOn
|
||||||
}
|
);
|
||||||
if (labels.length) {
|
|
||||||
const filterByLabels = labels.every(label => chatLabels.includes(label));
|
|
||||||
shouldFilter = shouldFilter && filterByLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldFilter;
|
return shouldFilter;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,14 @@ export const isOnMentionsView = ({ route: { name: routeName } }) => {
|
||||||
return MENTION_ROUTES.includes(routeName);
|
return MENTION_ROUTES.includes(routeName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isOnUnattendedView = ({ route: { name: routeName } }) => {
|
||||||
|
const UNATTENDED_ROUTES = [
|
||||||
|
'conversation_unattended',
|
||||||
|
'conversation_through_unattended',
|
||||||
|
];
|
||||||
|
return UNATTENDED_ROUTES.includes(routeName);
|
||||||
|
};
|
||||||
|
|
||||||
export const buildConversationList = (
|
export const buildConversationList = (
|
||||||
context,
|
context,
|
||||||
requestPayload,
|
requestPayload,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import {
|
import {
|
||||||
findPendingMessageIndex,
|
findPendingMessageIndex,
|
||||||
applyPageFilters,
|
applyPageFilters,
|
||||||
|
filterByInbox,
|
||||||
|
filterByTeam,
|
||||||
|
filterByLabel,
|
||||||
|
filterByUnattended,
|
||||||
} from '../../conversations/helpers';
|
} from '../../conversations/helpers';
|
||||||
|
|
||||||
const conversationList = [
|
const conversationList = [
|
||||||
|
@ -119,3 +123,52 @@ describe('#applyPageFilters', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#filterByInbox', () => {
|
||||||
|
it('returns true if conversation has inbox filter active', () => {
|
||||||
|
const inboxId = '1';
|
||||||
|
const chatInboxId = 1;
|
||||||
|
expect(filterByInbox(true, inboxId, chatInboxId)).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns false if inbox filter is not active', () => {
|
||||||
|
const inboxId = '1';
|
||||||
|
const chatInboxId = 13;
|
||||||
|
expect(filterByInbox(true, inboxId, chatInboxId)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#filterByTeam', () => {
|
||||||
|
it('returns true if conversation has team and team filter is active', () => {
|
||||||
|
const [teamId, chatTeamId] = ['1', 1];
|
||||||
|
expect(filterByTeam(true, teamId, chatTeamId)).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns false if team filter is not active', () => {
|
||||||
|
const [teamId, chatTeamId] = ['1', 12];
|
||||||
|
expect(filterByTeam(true, teamId, chatTeamId)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#filterByLabel', () => {
|
||||||
|
it('returns true if conversation has labels and labels filter is active', () => {
|
||||||
|
const labels = ['dev', 'cs'];
|
||||||
|
const chatLabels = ['dev', 'cs', 'sales'];
|
||||||
|
expect(filterByLabel(true, labels, chatLabels)).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns false if conversation has not all labels', () => {
|
||||||
|
const labels = ['dev', 'cs', 'sales'];
|
||||||
|
const chatLabels = ['cs', 'sales'];
|
||||||
|
expect(filterByLabel(true, labels, chatLabels)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#filterByUnattended', () => {
|
||||||
|
it('returns true if conversation type is unattended and has no first reply', () => {
|
||||||
|
expect(filterByUnattended(true, 'unattended', undefined)).toEqual(true);
|
||||||
|
});
|
||||||
|
it('returns false if conversation type is not unattended and has no first reply', () => {
|
||||||
|
expect(filterByUnattended(false, 'mentions', undefined)).toEqual(false);
|
||||||
|
});
|
||||||
|
it('returns true if conversation type is unattended and has first reply', () => {
|
||||||
|
expect(filterByUnattended(true, 'mentions', 123)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
||||||
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
||||||
"mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z",
|
"mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z",
|
||||||
|
"mail-unread-outline": "M16 6.5H5.25a1.75 1.75 0 0 0-1.744 1.606l-.004.1L11 12.153l6.03-3.174a3.489 3.489 0 0 0 2.97.985v6.786a3.25 3.25 0 0 1-3.066 3.245L16.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-8.5a3.25 3.25 0 0 1 3.066-3.245L5.25 5h11.087A3.487 3.487 0 0 0 16 6.5Zm2.5 3.399-7.15 3.765a.75.75 0 0 1-.603.042l-.096-.042L3.5 9.9v6.85a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.899ZM19.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z",
|
||||||
"mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z",
|
"mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z",
|
||||||
"map-outline": "m9.203 4 .047-.002.046.001a.73.73 0 0 1 .067.007l.016.004c.086.014.17.044.252.092l.051.034 5.07 3.565L19.82 4.14a.75.75 0 0 1 1.174.51l.007.104v10.632a.75.75 0 0 1-.238.548l-.08.066-5.5 3.866a.744.744 0 0 1-.828.023L9.25 16.297l-5.07 3.565a.75.75 0 0 1-1.174-.51l-.007-.104V8.616a.75.75 0 0 1 .238-.548l.08-.066 5.5-3.866a.762.762 0 0 1 .2-.101l.122-.028.064-.008Zm10.298 2.197-4 2.812v8.799l4-2.812v-8.8ZM8.5 6.193l-4 2.812v8.8l4-2.813V6.193Zm1.502 0v8.8l4 2.811V9.005l-4-2.812Z",
|
"map-outline": "m9.203 4 .047-.002.046.001a.73.73 0 0 1 .067.007l.016.004c.086.014.17.044.252.092l.051.034 5.07 3.565L19.82 4.14a.75.75 0 0 1 1.174.51l.007.104v10.632a.75.75 0 0 1-.238.548l-.08.066-5.5 3.866a.744.744 0 0 1-.828.023L9.25 16.297l-5.07 3.565a.75.75 0 0 1-1.174-.51l-.007-.104V8.616a.75.75 0 0 1 .238-.548l.08-.066 5.5-3.866a.762.762 0 0 1 .2-.101l.122-.028.064-.008Zm10.298 2.197-4 2.812v8.799l4-2.812v-8.8ZM8.5 6.193l-4 2.812v8.8l4-2.813V6.193Zm1.502 0v8.8l4 2.811V9.005l-4-2.812Z",
|
||||||
"megaphone-outline": "M21.907 5.622c.062.208.093.424.093.641V17.74a2.25 2.25 0 0 1-2.891 2.156l-5.514-1.64a4.002 4.002 0 0 1-7.59-1.556L6 16.5l-.001-.5-2.39-.711A2.25 2.25 0 0 1 2 13.131V10.87a2.25 2.25 0 0 1 1.61-2.156l15.5-4.606a2.25 2.25 0 0 1 2.797 1.515ZM7.499 16.445l.001.054a2.5 2.5 0 0 0 4.624 1.321l-4.625-1.375Zm12.037-10.9-15.5 4.605a.75.75 0 0 0-.536.72v2.261a.75.75 0 0 0 .536.72l15.5 4.607a.75.75 0 0 0 .964-.72V6.264a.75.75 0 0 0-.964-.719Z",
|
"megaphone-outline": "M21.907 5.622c.062.208.093.424.093.641V17.74a2.25 2.25 0 0 1-2.891 2.156l-5.514-1.64a4.002 4.002 0 0 1-7.59-1.556L6 16.5l-.001-.5-2.39-.711A2.25 2.25 0 0 1 2 13.131V10.87a2.25 2.25 0 0 1 1.61-2.156l15.5-4.606a2.25 2.25 0 0 1 2.797 1.515ZM7.499 16.445l.001.054a2.5 2.5 0 0 0 4.624 1.321l-4.625-1.375Zm12.037-10.9-15.5 4.605a.75.75 0 0 0-.536.72v2.261a.75.75 0 0 0 .536.72l15.5 4.607a.75.75 0 0 0 .964-.72V6.264a.75.75 0 0 0-.964-.719Z",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export const MESSAGE_STATUS = {
|
export const MESSAGE_STATUS = {
|
||||||
FAILED: 'failed',
|
FAILED: 'failed',
|
||||||
SENT: 'sent',
|
SENT: 'sent',
|
||||||
|
DELIVERED: 'delivered',
|
||||||
|
READ: 'read',
|
||||||
PROGRESS: 'progress',
|
PROGRESS: 'progress',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
37
app/jobs/agents/destroy_job.rb
Normal file
37
app/jobs/agents/destroy_job.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
class Agents::DestroyJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(account, user)
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
destroy_notification_setting(account, user)
|
||||||
|
remove_user_from_teams(account, user)
|
||||||
|
remove_user_from_inboxes(account, user)
|
||||||
|
unassign_conversations(account, user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_user_from_inboxes(account, user)
|
||||||
|
inboxes = account.inboxes.all
|
||||||
|
inbox_members = user.inbox_members.where(inbox_id: inboxes.pluck(:id))
|
||||||
|
inbox_members.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_user_from_teams(account, user)
|
||||||
|
teams = account.teams.all
|
||||||
|
team_members = user.team_members.where(team_id: teams.pluck(:id))
|
||||||
|
team_members.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_notification_setting(account, user)
|
||||||
|
setting = user.notification_settings.find_by(account_id: account.id)
|
||||||
|
setting&.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
def unassign_conversations(account, user)
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
user.assigned_conversations.where(account: account).in_batches.update_all(assignee_id: nil)
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,9 +36,9 @@ class ReportingEventListener < BaseListener
|
||||||
event_start_time: conversation.created_at,
|
event_start_time: conversation.created_at,
|
||||||
event_end_time: message.created_at
|
event_end_time: message.created_at
|
||||||
)
|
)
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
|
||||||
conversation.update_columns(first_reply_created_at: message.created_at)
|
conversation.update(first_reply_created_at: message.created_at)
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
|
||||||
reporting_event.save!
|
reporting_event.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,7 +46,7 @@ class AccountUser < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_user_from_account
|
def remove_user_from_account
|
||||||
::Agents::DestroyService.new(account: account, user: user).perform
|
::Agents::DestroyJob.perform_later(account, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -73,7 +73,8 @@ module ActivityMessageHandler
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_team_change_activity_key
|
def generate_team_change_activity_key
|
||||||
key = team_id ? 'assigned' : 'removed'
|
team = Team.find_by(id: team_id)
|
||||||
|
key = team.present? ? 'assigned' : 'removed'
|
||||||
key += '_with_assignee' if key == 'assigned' && saved_change_to_assignee_id? && assignee
|
key += '_with_assignee' if key == 'assigned' && saved_change_to_assignee_id? && assignee
|
||||||
key
|
key
|
||||||
end
|
end
|
||||||
|
|
|
@ -219,7 +219,7 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
def notify_conversation_updation
|
def notify_conversation_updation
|
||||||
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
||||||
custom_attributes label_list]).present?
|
custom_attributes label_list first_reply_created_at]).present?
|
||||||
|
|
||||||
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# enable_email_collect :boolean default(TRUE)
|
# enable_email_collect :boolean default(TRUE)
|
||||||
# greeting_enabled :boolean default(FALSE)
|
# greeting_enabled :boolean default(FALSE)
|
||||||
# greeting_message :string
|
# greeting_message :string
|
||||||
|
# lock_to_single_conversation :boolean default(FALSE), not null
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# out_of_office_message :string
|
# out_of_office_message :string
|
||||||
# timezone :string default("UTC")
|
# timezone :string default("UTC")
|
||||||
|
|
|
@ -65,8 +65,9 @@ class Message < ApplicationRecord
|
||||||
# [:in_reply_to] : Used to reply to a particular tweet in threads
|
# [:in_reply_to] : Used to reply to a particular tweet in threads
|
||||||
# [:deleted] : Used to denote whether the message was deleted by the agent
|
# [:deleted] : Used to denote whether the message was deleted by the agent
|
||||||
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
||||||
|
# [:external_error : Can specify if the message creation failed due to an error at external API
|
||||||
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
||||||
:external_created_at, :story_sender, :story_id], coder: JSON
|
:external_created_at, :story_sender, :story_id, :external_error], coder: JSON
|
||||||
|
|
||||||
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
||||||
custom_attributes: custom_attributes,
|
custom_attributes: custom_attributes,
|
||||||
snoozed_until: snoozed_until,
|
snoozed_until: snoozed_until,
|
||||||
unread_count: unread_incoming_messages.count,
|
unread_count: unread_incoming_messages.count,
|
||||||
|
first_reply_created_at: first_reply_created_at,
|
||||||
**push_timestamps
|
**push_timestamps
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
class Agents::DestroyService
|
|
||||||
pattr_initialize [:account!, :user!]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
destroy_notification_setting
|
|
||||||
remove_user_from_teams
|
|
||||||
remove_user_from_inboxes
|
|
||||||
unassign_conversations
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def remove_user_from_inboxes
|
|
||||||
inboxes = account.inboxes.all
|
|
||||||
inbox_members = user.inbox_members.where(inbox_id: inboxes.pluck(:id))
|
|
||||||
inbox_members.destroy_all
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_user_from_teams
|
|
||||||
teams = account.teams.all
|
|
||||||
team_members = user.team_members.where(team_id: teams.pluck(:id))
|
|
||||||
team_members.destroy_all
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy_notification_setting
|
|
||||||
setting = user.notification_settings.find_by(account_id: account.id)
|
|
||||||
setting&.destroy!
|
|
||||||
end
|
|
||||||
|
|
||||||
def unassign_conversations
|
|
||||||
user.assigned_conversations.where(account: account).find_each(&:update_assignee)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -7,11 +7,38 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
def perform
|
def perform
|
||||||
processed_params
|
processed_params
|
||||||
|
|
||||||
|
perform_statuses
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
return unless @contact
|
return unless @contact
|
||||||
|
|
||||||
set_conversation
|
set_conversation
|
||||||
|
|
||||||
|
perform_messages
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def perform_statuses
|
||||||
|
return if @processed_params[:statuses].blank?
|
||||||
|
|
||||||
|
status = @processed_params[:statuses].first
|
||||||
|
@message = Message.find_by(source_id: status[:id])
|
||||||
|
return unless @message
|
||||||
|
|
||||||
|
update_message_with_status(@message, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_message_with_status(message, status)
|
||||||
|
message.status = status[:status]
|
||||||
|
if status[:status] == 'failed' && status[:errors].present?
|
||||||
|
error = status[:errors]&.first
|
||||||
|
message.external_error = "#{error[:code]}: #{error[:title]}"
|
||||||
|
end
|
||||||
|
message.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_messages
|
||||||
return if @processed_params[:messages].blank? || unprocessable_message_type?
|
return if @processed_params[:messages].blank? || unprocessable_message_type?
|
||||||
|
|
||||||
@message = @conversation.messages.build(
|
@message = @conversation.messages.build(
|
||||||
|
@ -27,8 +54,6 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
@message.save!
|
@message.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def processed_params
|
def processed_params
|
||||||
@processed_params ||= params
|
@processed_params ||= params
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,17 +69,18 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
||||||
def send_attachment_message(phone_number, message)
|
def send_attachment_message(phone_number, message)
|
||||||
attachment = message.attachments.first
|
attachment = message.attachments.first
|
||||||
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
||||||
attachment_url = attachment.download_url
|
type_content = {
|
||||||
|
'link': attachment.download_url
|
||||||
|
}
|
||||||
|
type_content['caption'] = message.content unless %w[audio sticker].include?(type)
|
||||||
|
type_content['filename'] = attachment.file.filename if type == 'document'
|
||||||
response = HTTParty.post(
|
response = HTTParty.post(
|
||||||
"#{api_base_path}/messages",
|
"#{api_base_path}/messages",
|
||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
body: {
|
body: {
|
||||||
'to' => phone_number,
|
'to' => phone_number,
|
||||||
'type' => type,
|
'type' => type,
|
||||||
type.to_s => {
|
type.to_s => type_content
|
||||||
'link': attachment_url,
|
|
||||||
'caption': message.content
|
|
||||||
}
|
|
||||||
}.to_json
|
}.to_json
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -69,11 +69,11 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||||
def send_attachment_message(phone_number, message)
|
def send_attachment_message(phone_number, message)
|
||||||
attachment = message.attachments.first
|
attachment = message.attachments.first
|
||||||
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
||||||
attachment_url = attachment.download_url
|
|
||||||
type_content = {
|
type_content = {
|
||||||
'link': attachment_url
|
'link': attachment.download_url
|
||||||
}
|
}
|
||||||
type_content['caption'] = message.content if type != 'audio'
|
type_content['caption'] = message.content unless %w[audio sticker].include?(type)
|
||||||
|
type_content['filename'] = attachment.file.filename if type == 'document'
|
||||||
response = HTTParty.post(
|
response = HTTParty.post(
|
||||||
"#{phone_id_path}/messages",
|
"#{phone_id_path}/messages",
|
||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
|
|
|
@ -38,5 +38,6 @@ json.muted conversation.muted?
|
||||||
json.snoozed_until conversation.snoozed_until
|
json.snoozed_until conversation.snoozed_until
|
||||||
json.status conversation.status
|
json.status conversation.status
|
||||||
json.timestamp conversation.last_activity_at.to_i
|
json.timestamp conversation.last_activity_at.to_i
|
||||||
|
json.first_reply_created_at conversation.first_reply_created_at.to_i
|
||||||
json.unread_count conversation.unread_incoming_messages.count
|
json.unread_count conversation.unread_incoming_messages.count
|
||||||
json.last_non_activity_message conversation.messages.non_activity_messages.first.try(:push_event_data)
|
json.last_non_activity_message conversation.messages.non_activity_messages.first.try(:push_event_data)
|
||||||
|
|
|
@ -15,12 +15,13 @@ json.working_hours resource.weekly_schedule
|
||||||
json.timezone resource.timezone
|
json.timezone resource.timezone
|
||||||
json.callback_webhook_url resource.callback_webhook_url
|
json.callback_webhook_url resource.callback_webhook_url
|
||||||
json.allow_messages_after_resolved resource.allow_messages_after_resolved
|
json.allow_messages_after_resolved resource.allow_messages_after_resolved
|
||||||
|
json.lock_to_single_conversation resource.lock_to_single_conversation
|
||||||
json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter?
|
|
||||||
|
|
||||||
## Channel specific settings
|
## Channel specific settings
|
||||||
## TODO : Clean up and move the attributes into channel sub section
|
## TODO : Clean up and move the attributes into channel sub section
|
||||||
|
|
||||||
|
json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter?
|
||||||
|
|
||||||
## WebWidget Attributes
|
## WebWidget Attributes
|
||||||
json.widget_color resource.channel.try(:widget_color)
|
json.widget_color resource.channel.try(:widget_color)
|
||||||
json.website_url resource.channel.try(:website_url)
|
json.website_url resource.channel.try(:website_url)
|
||||||
|
|
|
@ -5,6 +5,7 @@ json.echo_id message.echo_id if message.echo_id
|
||||||
json.conversation_id message.conversation.display_id
|
json.conversation_id message.conversation.display_id
|
||||||
json.message_type message.message_type_before_type_cast
|
json.message_type message.message_type_before_type_cast
|
||||||
json.content_type message.content_type
|
json.content_type message.content_type
|
||||||
|
json.status message.status
|
||||||
json.content_attributes message.content_attributes
|
json.content_attributes message.content_attributes
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.private message.private
|
json.private message.private
|
||||||
|
|
|
@ -30,6 +30,9 @@ Rails.application.configure do
|
||||||
# You can use letter opener for your local development by setting the environment variable
|
# You can use letter opener for your local development by setting the environment variable
|
||||||
config.action_mailer.delivery_method = :letter_opener if Rails.env.development? && ENV['LETTER_OPENER']
|
config.action_mailer.delivery_method = :letter_opener if Rails.env.development? && ENV['LETTER_OPENER']
|
||||||
|
|
||||||
|
# Use sendmail if using postfix for email
|
||||||
|
config.action_mailer.delivery_method = :sendmail if ENV['SMTP_ADDRESS'].blank?
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# Configuration Related to Action MailBox
|
# Configuration Related to Action MailBox
|
||||||
#########################################
|
#########################################
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddLockConversationToSingleThread < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :inboxes, :lock_to_single_conversation, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -535,6 +535,7 @@ ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
||||||
t.boolean "csat_survey_enabled", default: false
|
t.boolean "csat_survey_enabled", default: false
|
||||||
t.boolean "allow_messages_after_resolved", default: true
|
t.boolean "allow_messages_after_resolved", default: true
|
||||||
t.jsonb "auto_assignment_config", default: {}
|
t.jsonb "auto_assignment_config", default: {}
|
||||||
|
t.boolean "lock_to_single_conversation", default: false, null: false
|
||||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
46
spec/builders/conversation_builder_spec.rb
Normal file
46
spec/builders/conversation_builder_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ::ConversationBuilder do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let!(:sms_channel) { create(:channel_sms, account: account) }
|
||||||
|
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
|
||||||
|
let(:contact) { create(:contact, account: account) }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: sms_inbox) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'creates conversation' do
|
||||||
|
conversation = described_class.new(
|
||||||
|
contact_inbox: contact_inbox,
|
||||||
|
params: {}
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when lock_to_single_conversation is true for inbox' do
|
||||||
|
before do
|
||||||
|
sms_inbox.update!(lock_to_single_conversation: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates conversation when existing conversation is not present' do
|
||||||
|
conversation = described_class.new(
|
||||||
|
contact_inbox: contact_inbox,
|
||||||
|
params: {}
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns last from existing conversations when existing conversation is not present' do
|
||||||
|
create(:conversation, contact_inbox: contact_inbox)
|
||||||
|
existing_conversation = create(:conversation, contact_inbox: contact_inbox)
|
||||||
|
conversation = described_class.new(
|
||||||
|
contact_inbox: contact_inbox,
|
||||||
|
params: {}
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(conversation.id).to eq(existing_conversation.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,7 +26,9 @@ describe ::NotificationBuilder do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not throw error if notification setting is not present' do
|
it 'will not throw error if notification setting is not present' do
|
||||||
user.account_users.destroy_all
|
perform_enqueued_jobs do
|
||||||
|
user.account_users.destroy_all
|
||||||
|
end
|
||||||
expect(
|
expect(
|
||||||
described_class.new(
|
described_class.new(
|
||||||
notification_type: 'conversation_creation',
|
notification_type: 'conversation_creation',
|
||||||
|
|
|
@ -5,10 +5,11 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:agent_1) { create(:user, account: account, role: :agent) }
|
let(:agent_1) { create(:user, account: account, role: :agent) }
|
||||||
let(:agent_2) { create(:user, account: account, role: :agent) }
|
let(:agent_2) { create(:user, account: account, role: :agent) }
|
||||||
|
let(:team_1) { create(:team, account: account) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:conversation, account_id: account.id, status: :open)
|
create(:conversation, account_id: account.id, status: :open, team_id: team_1.id)
|
||||||
create(:conversation, account_id: account.id, status: :open)
|
create(:conversation, account_id: account.id, status: :open, team_id: team_1.id)
|
||||||
create(:conversation, account_id: account.id, status: :open)
|
create(:conversation, account_id: account.id, status: :open)
|
||||||
create(:conversation, account_id: account.id, status: :open)
|
create(:conversation, account_id: account.id, status: :open)
|
||||||
end
|
end
|
||||||
|
@ -55,6 +56,44 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
|
||||||
expect(Conversation.first.assignee_id).to be_nil
|
expect(Conversation.first.assignee_id).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'Bulk update conversation team id to none' do
|
||||||
|
params = { type: 'Conversation', fields: { team_id: 0 }, ids: Conversation.first(1).pluck(:display_id) }
|
||||||
|
expect(Conversation.first.team).not_to be_nil
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
params: params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(Conversation.first.team).to be_nil
|
||||||
|
|
||||||
|
last_activity_message = Conversation.first.messages.activity.last
|
||||||
|
|
||||||
|
expect(last_activity_message.content).to eq("Unassigned from #{team_1.name} by #{agent.name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'Bulk update conversation team id to team' do
|
||||||
|
params = { type: 'Conversation', fields: { team_id: team_1.id }, ids: Conversation.last(2).pluck(:display_id) }
|
||||||
|
expect(Conversation.last.team_id).to be_nil
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
post "/api/v1/accounts/#{account.id}/bulk_actions",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
params: params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(Conversation.last.team).to eq(team_1)
|
||||||
|
|
||||||
|
last_activity_message = Conversation.last.messages.activity.last
|
||||||
|
|
||||||
|
expect(last_activity_message.content).to eq("Assigned to #{team_1.name} by #{agent.name}")
|
||||||
|
end
|
||||||
|
|
||||||
it 'Bulk update conversation assignee id' do
|
it 'Bulk update conversation assignee id' do
|
||||||
params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) }
|
params = { type: 'Conversation', fields: { assignee_id: agent_1.id }, ids: Conversation.first(3).pluck(:display_id) }
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ RSpec.describe 'Conversations API', type: :request do
|
||||||
|
|
||||||
get "/api/v1/accounts/#{account.id}/conversations",
|
get "/api/v1/accounts/#{account.id}/conversations",
|
||||||
headers: agent_1.create_new_auth_token,
|
headers: agent_1.create_new_auth_token,
|
||||||
params: { reply_status: 'unattended' },
|
params: { conversation_type: 'unattended' },
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
|
@ -265,17 +265,18 @@ RSpec.describe 'Conversations API', type: :request do
|
||||||
|
|
||||||
# TODO: remove this spec when we remove the condition check in controller
|
# TODO: remove this spec when we remove the condition check in controller
|
||||||
# Added for backwards compatibility for bot status
|
# Added for backwards compatibility for bot status
|
||||||
it 'creates a conversation as pending if status is specified as bot' do
|
# remove this in subsequent release
|
||||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
# it 'creates a conversation as pending if status is specified as bot' do
|
||||||
post "/api/v1/accounts/#{account.id}/conversations",
|
# allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
headers: agent.create_new_auth_token,
|
# post "/api/v1/accounts/#{account.id}/conversations",
|
||||||
params: { source_id: contact_inbox.source_id, status: 'bot' },
|
# headers: agent.create_new_auth_token,
|
||||||
as: :json
|
# params: { source_id: contact_inbox.source_id, status: 'bot' },
|
||||||
|
# as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
# expect(response).to have_http_status(:success)
|
||||||
response_data = JSON.parse(response.body, symbolize_names: true)
|
# response_data = JSON.parse(response.body, symbolize_names: true)
|
||||||
expect(response_data[:status]).to eq('pending')
|
# expect(response_data[:status]).to eq('pending')
|
||||||
end
|
# end
|
||||||
|
|
||||||
it 'creates a new conversation with message when message is passed' do
|
it 'creates a new conversation with message when message is passed' do
|
||||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
@ -408,17 +409,18 @@ RSpec.describe 'Conversations API', type: :request do
|
||||||
|
|
||||||
# TODO: remove this spec when we remove the condition check in controller
|
# TODO: remove this spec when we remove the condition check in controller
|
||||||
# Added for backwards compatibility for bot status
|
# Added for backwards compatibility for bot status
|
||||||
it 'toggles the conversation status to pending status when parameter bot is passed' do
|
# remove in next release
|
||||||
expect(conversation.status).to eq('open')
|
# it 'toggles the conversation status to pending status when parameter bot is passed' do
|
||||||
|
# expect(conversation.status).to eq('open')
|
||||||
|
|
||||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
# post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
||||||
headers: agent.create_new_auth_token,
|
# headers: agent.create_new_auth_token,
|
||||||
params: { status: 'bot' },
|
# params: { status: 'bot' },
|
||||||
as: :json
|
# as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
# expect(response).to have_http_status(:success)
|
||||||
expect(conversation.reload.status).to eq('pending')
|
# expect(conversation.reload.status).to eq('pending')
|
||||||
end
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -136,5 +136,15 @@ describe ::ConversationFinder do
|
||||||
expect(result[:conversations].length).to be 25
|
expect(result[:conversations].length).to be 25
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with unattended' do
|
||||||
|
let(:params) { { status: 'open', assignee_type: 'me', conversation_type: 'unattended' } }
|
||||||
|
|
||||||
|
it 'returns unattended conversations' do
|
||||||
|
create_list(:conversation, 25, account: account, inbox: inbox, assignee: user_1)
|
||||||
|
result = conversation_finder.perform
|
||||||
|
expect(result[:conversations].length).to be 25
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Agents::DestroyService do
|
RSpec.describe Agents::DestroyJob, type: :job do
|
||||||
let(:account) { create(:account) }
|
subject(:job) { described_class.perform_later(account, user) }
|
||||||
|
|
||||||
|
let!(:account) { create(:account) }
|
||||||
let(:user) { create(:user, account: account) }
|
let(:user) { create(:user, account: account) }
|
||||||
let(:team1) { create(:team, account: account) }
|
let(:team1) { create(:team, account: account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
@ -12,9 +14,16 @@ describe Agents::DestroyService do
|
||||||
create(:conversation, account: account, assignee: user, inbox: inbox)
|
create(:conversation, account: account, assignee: user, inbox: inbox)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'enqueues the job' do
|
||||||
|
expect { job }.to have_enqueued_job(described_class)
|
||||||
|
.with(account, user)
|
||||||
|
.on_queue('default')
|
||||||
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
it 'remove inboxes, teams, and conversations when removed from account' do
|
it 'remove inboxes, teams, and conversations when removed from account' do
|
||||||
described_class.new(account: account, user: user).perform
|
described_class.perform_now(account, user)
|
||||||
|
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.teams.length).to eq 0
|
expect(user.teams.length).to eq 0
|
||||||
expect(user.inboxes.length).to eq 0
|
expect(user.inboxes.length).to eq 0
|
|
@ -3,13 +3,10 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe User do
|
RSpec.describe User do
|
||||||
let!(:account_user) { create(:account_user) }
|
include ActiveJob::TestHelper
|
||||||
let(:agent_destroy_service) { double }
|
|
||||||
|
|
||||||
before do
|
let!(:account_user) { create(:account_user) }
|
||||||
allow(Agents::DestroyService).to receive(:new).and_return(agent_destroy_service)
|
let!(:inbox) { create(:inbox, account: account_user.account) }
|
||||||
allow(agent_destroy_service).to receive(:perform).and_return(agent_destroy_service)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'notification_settings' do
|
describe 'notification_settings' do
|
||||||
it 'gets created with the right default settings' do
|
it 'gets created with the right default settings' do
|
||||||
|
@ -22,12 +19,16 @@ RSpec.describe User do
|
||||||
|
|
||||||
describe 'destroy call agent::destroy service' do
|
describe 'destroy call agent::destroy service' do
|
||||||
it 'gets created with the right default settings' do
|
it 'gets created with the right default settings' do
|
||||||
|
create(:conversation, account: account_user.account, assignee: account_user.user, inbox: inbox)
|
||||||
user = account_user.user
|
user = account_user.user
|
||||||
account = account_user.account
|
|
||||||
account_user.destroy!
|
expect(user.assigned_conversations.count).to eq(1)
|
||||||
expect(Agents::DestroyService).to have_received(:new).with({
|
|
||||||
user: user, account: account
|
perform_enqueued_jobs do
|
||||||
})
|
account_user.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(user.assigned_conversations.count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -455,6 +455,7 @@ RSpec.describe Conversation, type: :model do
|
||||||
channel: 'Channel::WebWidget',
|
channel: 'Channel::WebWidget',
|
||||||
snoozed_until: conversation.snoozed_until,
|
snoozed_until: conversation.snoozed_until,
|
||||||
custom_attributes: conversation.custom_attributes,
|
custom_attributes: conversation.custom_attributes,
|
||||||
|
first_reply_created_at: nil,
|
||||||
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
||||||
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
||||||
unread_count: 0
|
unread_count: 0
|
||||||
|
|
|
@ -27,6 +27,7 @@ RSpec.describe Conversations::EventDataPresenter do
|
||||||
timestamp: conversation.last_activity_at.to_i,
|
timestamp: conversation.last_activity_at.to_i,
|
||||||
snoozed_until: conversation.snoozed_until,
|
snoozed_until: conversation.snoozed_until,
|
||||||
custom_attributes: conversation.custom_attributes,
|
custom_attributes: conversation.custom_attributes,
|
||||||
|
first_reply_created_at: nil,
|
||||||
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
||||||
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
||||||
unread_count: 0
|
unread_count: 0
|
||||||
|
|
|
@ -70,6 +70,46 @@ describe Whatsapp::IncomingMessageService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when valid status params' do
|
||||||
|
let(:from) { '2423423243' }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: from) }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => from }],
|
||||||
|
'messages' => [{ 'from' => from, 'id' => from, 'text' => { 'body' => 'Test' },
|
||||||
|
'timestamp' => '1633034394', 'type' => 'text' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'update status message to read' do
|
||||||
|
status_params = {
|
||||||
|
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'read' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
message = Message.find_by!(source_id: from)
|
||||||
|
expect(message.status).to eq('sent')
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||||
|
expect(message.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'update status message to failed' do
|
||||||
|
status_params = {
|
||||||
|
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'failed',
|
||||||
|
'errors' => [{ 'code': 123, 'title': 'abc' }] }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
|
||||||
|
message = Message.find_by!(source_id: from)
|
||||||
|
expect(message.status).to eq('sent')
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||||
|
expect(message.reload.status).to eq('failed')
|
||||||
|
expect(message.external_error).to eq('123: abc')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when valid interactive message params' do
|
context 'when valid interactive message params' do
|
||||||
it 'creates appropriate conversations, message and contacts' do
|
it 'creates appropriate conversations, message and contacts' do
|
||||||
params = {
|
params = {
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls message endpoints for attachment message messages' do
|
it 'calls message endpoints for image attachment message messages' do
|
||||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||||
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||||
|
|
||||||
|
@ -37,7 +37,27 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||||
body: hash_including({
|
body: hash_including({
|
||||||
messaging_product: 'whatsapp',
|
messaging_product: 'whatsapp',
|
||||||
to: '+123456789',
|
to: '+123456789',
|
||||||
type: 'image'
|
type: 'image',
|
||||||
|
image: WebMock::API.hash_including({ caption: message.content, link: anything })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||||
|
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls message endpoints for document attachment message messages' do
|
||||||
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :file)
|
||||||
|
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/sample.pdf')), filename: 'sample.pdf', content_type: 'application/pdf')
|
||||||
|
|
||||||
|
# ref: https://github.com/bblimke/webmock/issues/900
|
||||||
|
# reason for Webmock::API.hash_including
|
||||||
|
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||||
|
.with(
|
||||||
|
body: hash_including({
|
||||||
|
messaging_product: 'whatsapp',
|
||||||
|
to: '+123456789',
|
||||||
|
type: 'document',
|
||||||
|
document: WebMock::API.hash_including({ filename: 'sample.pdf', caption: message.content, link: anything })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||||
|
|
Loading…
Reference in a new issue