Compare commits
19 commits
develop
...
feat-searc
Author | SHA1 | Date | |
---|---|---|---|
|
228167823c | ||
|
c618064b13 | ||
|
d23b4a45ad | ||
|
4af9d00d6f | ||
|
e8bd066f93 | ||
|
4dc691e29c | ||
|
6c07ab8bb6 | ||
|
a98bffbe2b | ||
|
4b674a167a | ||
|
bd4b051460 | ||
|
963980d13b | ||
|
c498668345 | ||
|
cb388d93e6 | ||
|
7ba0666cb1 | ||
|
75b7f8f0c4 | ||
|
9b8f0e0152 | ||
|
92ea452680 | ||
|
65de030244 | ||
|
54eab87806 |
57 changed files with 1716 additions and 144 deletions
10
Gemfile.lock
10
Gemfile.lock
|
@ -427,14 +427,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.9.0)
|
newrelic_rpm (8.9.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.9)
|
nokogiri (1.13.10)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-arm64-darwin)
|
nokogiri (1.13.10-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-x86_64-darwin)
|
nokogiri (1.13.10-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-x86_64-linux)
|
nokogiri (1.13.10-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.10)
|
oauth (0.5.10)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -459,7 +459,7 @@ GEM
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.1)
|
||||||
rack (2.2.4)
|
rack (2.2.4)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
|
|
@ -46,6 +46,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_attachment_file_type(attachment)
|
def update_attachment_file_type(attachment)
|
||||||
|
return if @message.reload.attachments.blank?
|
||||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||||
|
|
||||||
attachment.file_type = file_type(attachment.file&.content_type)
|
attachment.file_type = file_type(attachment.file&.content_type)
|
||||||
|
@ -62,6 +63,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
story_sender = result['from']['username']
|
story_sender = result['from']['username']
|
||||||
message.content_attributes[:story_sender] = story_sender
|
message.content_attributes[:story_sender] = story_sender
|
||||||
message.content_attributes[:story_id] = story_id
|
message.content_attributes[:story_id] = story_id
|
||||||
|
message.content_attributes[:image_type] = 'story_mention'
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||||
message.save!
|
message.save!
|
||||||
end
|
end
|
||||||
|
@ -74,6 +76,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
||||||
|
@message.attachments.destroy_all
|
||||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -2,8 +2,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
include Events::Types
|
include Events::Types
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
before_action :conversation, except: [:index, :meta, :search, :create, :filter, :text_search]
|
||||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
before_action :inbox, :contact, :contact_inbox, :text_search, only: [:create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
|
@ -11,6 +11,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
@conversations_count = result[:count]
|
@conversations_count = result[:count]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def text_search
|
||||||
|
@result = TextSearch.new(Current.user, params).perform
|
||||||
|
end
|
||||||
|
|
||||||
def meta
|
def meta
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
@conversations_count = result[:count]
|
@conversations_count = result[:count]
|
||||||
|
|
|
@ -24,7 +24,6 @@ class DashboardController < ActionController::Base
|
||||||
'API_CHANNEL_NAME',
|
'API_CHANNEL_NAME',
|
||||||
'API_CHANNEL_THUMBNAIL',
|
'API_CHANNEL_THUMBNAIL',
|
||||||
'ANALYTICS_TOKEN',
|
'ANALYTICS_TOKEN',
|
||||||
'ANALYTICS_HOST',
|
|
||||||
'DIRECT_UPLOADS_ENABLED',
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
'HCAPTCHA_SITE_KEY',
|
'HCAPTCHA_SITE_KEY',
|
||||||
'LOGOUT_REDIRECT_LINK',
|
'LOGOUT_REDIRECT_LINK',
|
||||||
|
|
36
app/finders/text_search.rb
Normal file
36
app/finders/text_search.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
class TextSearch
|
||||||
|
attr_reader :current_user, :current_account, :params
|
||||||
|
|
||||||
|
DEFAULT_STATUS = 'open'.freeze
|
||||||
|
|
||||||
|
def initialize(current_user, params)
|
||||||
|
@current_account = current_user.account || Current.account
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
{
|
||||||
|
messages: filter_messages,
|
||||||
|
conversations: filter_conversations,
|
||||||
|
contacts: filter_contacts
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_conversations
|
||||||
|
conversation_ids = PgSearch.multisearch("#{@params[:q]}%").where(account_id: @current_account,
|
||||||
|
searchable_type: 'Conversation').pluck(:searchable_id)
|
||||||
|
@conversations = Conversation.where(id: conversation_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_messages
|
||||||
|
message_ids = PgSearch.multisearch("#{@params[:q]}%").where(account_id: @current_account, searchable_type: 'Message').pluck(:searchable_id)
|
||||||
|
@messages = Message.where(id: message_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_contacts
|
||||||
|
contact_ids = PgSearch.multisearch("#{@params[:q]}%").where(account_id: @current_account, searchable_type: 'Contact').pluck(:searchable_id)
|
||||||
|
@contacts = Contact.where(id: contact_ids)
|
||||||
|
end
|
||||||
|
end
|
|
@ -45,6 +45,12 @@ class ConversationApi extends ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textSearch(query) {
|
||||||
|
return axios.post(`${this.url}/text_search`, null, {
|
||||||
|
params: { query },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||||
status,
|
status,
|
||||||
|
|
|
@ -47,6 +47,9 @@ import {
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
const createState = (content, placeholder, plugins = []) => {
|
const createState = (content, placeholder, plugins = []) => {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
|
@ -268,6 +271,7 @@ export default {
|
||||||
);
|
);
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -297,6 +301,7 @@ export default {
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
|
||||||
tr.scrollIntoView();
|
tr.scrollIntoView();
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,9 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
|
||||||
import { trimContent, debounce } from '@chatwoot/utils';
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants';
|
import wootConstants from 'dashboard/constants';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
|
@ -698,6 +701,7 @@ export default {
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,19 +54,6 @@
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
|
|
||||||
:href="linkToStory"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
|
|
||||||
icon="open"
|
|
||||||
class="action--icon cursor-pointer"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const EXECUTED_A_MACRO = 'Executed a macro';
|
||||||
|
export const SENT_MESSAGE = 'Sent a message';
|
||||||
|
export const SENT_PRIVATE_NOTE = 'Sent a private note';
|
||||||
|
export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response';
|
||||||
|
export const USED_MENTIONS = 'Used mentions';
|
||||||
|
export const MERGED_CONTACTS = 'Used merge contact option';
|
||||||
|
export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option';
|
||||||
|
export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute';
|
||||||
|
export const ADDED_AN_INBOX = 'Added an inbox';
|
67
app/javascript/dashboard/helper/AnalyticsHelper/index.js
Normal file
67
app/javascript/dashboard/helper/AnalyticsHelper/index.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { AnalyticsBrowser } from '@june-so/analytics-next';
|
||||||
|
|
||||||
|
class AnalyticsHelper {
|
||||||
|
constructor({ token: analyticsToken } = {}) {
|
||||||
|
this.analyticsToken = analyticsToken;
|
||||||
|
this.analytics = null;
|
||||||
|
this.user = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.analyticsToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [analytics] = await AnalyticsBrowser.load({
|
||||||
|
writeKey: this.analyticsToken,
|
||||||
|
});
|
||||||
|
this.analytics = analytics;
|
||||||
|
}
|
||||||
|
|
||||||
|
identify(user) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.user = user;
|
||||||
|
this.analytics.identify(this.user.email, {
|
||||||
|
userId: this.user.id,
|
||||||
|
email: this.user.email,
|
||||||
|
name: this.user.name,
|
||||||
|
avatar: this.user.avatar_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { accounts, account_id: accountId } = this.user;
|
||||||
|
const [currentAccount] = accounts.filter(
|
||||||
|
account => account.id === accountId
|
||||||
|
);
|
||||||
|
if (currentAccount) {
|
||||||
|
this.analytics.group(currentAccount.id, this.user.id, {
|
||||||
|
name: currentAccount.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track(eventName, properties = {}) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analytics.track({
|
||||||
|
userId: this.user.id,
|
||||||
|
event: eventName,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
page(params) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analytics.page(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as ANALYTICS_EVENTS from './events';
|
||||||
|
|
||||||
|
export default new AnalyticsHelper(window.analyticsConfig);
|
|
@ -17,13 +17,22 @@ const formatArray = params => {
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generatePayloadForObject = item => {
|
||||||
|
if (item.action_params.id) {
|
||||||
|
item.action_params = [item.action_params.id];
|
||||||
|
} else {
|
||||||
|
item.action_params = [item.action_params];
|
||||||
|
}
|
||||||
|
return item.action_params;
|
||||||
|
};
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
const actions = JSON.parse(JSON.stringify(data));
|
const actions = JSON.parse(JSON.stringify(data));
|
||||||
let payload = actions.map(item => {
|
let payload = actions.map(item => {
|
||||||
if (Array.isArray(item.action_params)) {
|
if (Array.isArray(item.action_params)) {
|
||||||
item.action_params = formatArray(item.action_params);
|
item.action_params = formatArray(item.action_params);
|
||||||
} else if (typeof item.action_params === 'object') {
|
} else if (typeof item.action_params === 'object') {
|
||||||
item.action_params = [item.action_params.id];
|
item.action_params = generatePayloadForObject(item);
|
||||||
} else if (!item.action_params) {
|
} else if (!item.action_params) {
|
||||||
item.action_params = [];
|
item.action_params = [];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import posthog from 'posthog-js';
|
import AnalyticsHelper from './AnalyticsHelper';
|
||||||
|
|
||||||
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
||||||
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
||||||
|
@ -8,16 +8,9 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
|
||||||
|
|
||||||
export const initializeAnalyticsEvents = () => {
|
export const initializeAnalyticsEvents = () => {
|
||||||
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||||
if (window.analyticsConfig) {
|
AnalyticsHelper.identify(user);
|
||||||
posthog.identify(user.id, { name: user.name, email: user.email });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.bus.$on(ANALYTICS_RESET, () => {
|
|
||||||
if (window.analyticsConfig) {
|
|
||||||
posthog.reset();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
window.bus.$on(ANALYTICS_RESET, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initializeChatwootEvents = () => {
|
export const initializeChatwootEvents = () => {
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
"SEARCH_PLACEHOLDER": "Search or jump to",
|
"SEARCH_PLACEHOLDER": "Search or jump to",
|
||||||
"SECTIONS": {
|
"SECTIONS": {
|
||||||
"GENERAL": "General",
|
"GENERAL": "General",
|
||||||
|
"CHATWOOT": "Search Chatwoot",
|
||||||
"REPORTS": "Reports",
|
"REPORTS": "Reports",
|
||||||
"CONVERSATION": "Conversation",
|
"CONVERSATION": "Conversation",
|
||||||
"CHANGE_ASSIGNEE": "Change Assignee",
|
"CHANGE_ASSIGNEE": "Change Assignee",
|
||||||
|
@ -107,6 +108,7 @@
|
||||||
},
|
},
|
||||||
"COMMANDS": {
|
"COMMANDS": {
|
||||||
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||||
|
"SEARCH_EVEYTHING": "Search everything",
|
||||||
"GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard",
|
"GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard",
|
||||||
"GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview",
|
"GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview",
|
||||||
"GO_TO_CONVERSATION_REPORTS": "Go to Conversation Reports",
|
"GO_TO_CONVERSATION_REPORTS": "Go to Conversation Reports",
|
||||||
|
|
|
@ -24,6 +24,9 @@ import MergeContact from 'dashboard/modules/contact/components/MergeContact';
|
||||||
import ContactAPI from 'dashboard/api/contacts';
|
import ContactAPI from 'dashboard/api/contacts';
|
||||||
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { MergeContact },
|
components: { MergeContact },
|
||||||
|
@ -72,6 +75,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onMergeContacts(childContactId) {
|
async onMergeContacts(childContactId) {
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.MERGED_CONTACTS);
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('contacts/merge', {
|
await this.$store.dispatch('contacts/merge', {
|
||||||
childId: childContactId,
|
childId: childContactId,
|
||||||
|
|
|
@ -72,6 +72,9 @@ import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -127,6 +130,7 @@ export default {
|
||||||
this.$emit('toggle', false);
|
this.$emit('toggle', false);
|
||||||
},
|
},
|
||||||
showCannedResponseModal() {
|
showCannedResponseModal() {
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.ADDED_TO_CANNED_RESPONSE);
|
||||||
this.isCannedResponseModalOpen = true;
|
this.isCannedResponseModalOpen = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,3 +26,4 @@ export const ICON_LABELS = `<svg role="img" class="ninja-icon ninja-icon--fluent
|
||||||
export const ICON_ACCOUNT_SETTINGS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 3h6.5a.75.75 0 0 1 .743.648L16 3.75V7h1.75A3.25 3.25 0 0 1 21 10.25v6.5A3.25 3.25 0 0 1 17.75 20H6.25A3.25 3.25 0 0 1 3 16.75v-6.5A3.25 3.25 0 0 1 6.25 7H8V3.75a.75.75 0 0 1 .648-.743L8.75 3h6.5-6.5Zm9 5.5H6.25a1.75 1.75 0 0 0-1.75 1.75v6.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-6.5a1.75 1.75 0 0 0-1.75-1.75Zm-3.25-4h-5V7h5V4.5Z" fill="currentColor"/></svg>`;
|
export const ICON_ACCOUNT_SETTINGS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 3h6.5a.75.75 0 0 1 .743.648L16 3.75V7h1.75A3.25 3.25 0 0 1 21 10.25v6.5A3.25 3.25 0 0 1 17.75 20H6.25A3.25 3.25 0 0 1 3 16.75v-6.5A3.25 3.25 0 0 1 6.25 7H8V3.75a.75.75 0 0 1 .648-.743L8.75 3h6.5-6.5Zm9 5.5H6.25a1.75 1.75 0 0 0-1.75 1.75v6.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-6.5a1.75 1.75 0 0 0-1.75-1.75Zm-3.25-4h-5V7h5V4.5Z" fill="currentColor"/></svg>`;
|
||||||
export const ICON_INBOXES = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="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 3h11.5-11.5ZM4.5 14.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-.188H4.5v3.25-3.25Zm13.25-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.5Z" fill="currentColor"/></svg>`;
|
export const ICON_INBOXES = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="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 3h11.5-11.5ZM4.5 14.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-.188H4.5v3.25-3.25Zm13.25-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.5Z" fill="currentColor"/></svg>`;
|
||||||
export const ICON_APPS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18.492 2.33 3.179 3.179a2.25 2.25 0 0 1 0 3.182l-2.584 2.584A2.25 2.25 0 0 1 21 13.5v5.25A2.25 2.25 0 0 1 18.75 21H5.25A2.25 2.25 0 0 1 3 18.75V5.25A2.25 2.25 0 0 1 5.25 3h5.25a2.25 2.25 0 0 1 2.225 1.915L15.31 2.33a2.25 2.25 0 0 1 3.182 0ZM4.5 18.75c0 .414.336.75.75.75l5.999-.001.001-6.75H4.5v6Zm8.249.749h6.001a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75h-6.001v6.75Zm-2.249-15H5.25a.75.75 0 0 0-.75.75v6h6.75v-6a.75.75 0 0 0-.75-.75Zm2.25 4.81v1.94h1.94l-1.94-1.94Zm3.62-5.918-3.178 3.178a.75.75 0 0 0 0 1.061l3.179 3.179a.75.75 0 0 0 1.06 0l3.18-3.179a.75.75 0 0 0 0-1.06l-3.18-3.18a.75.75 0 0 0-1.06 0Z" fill="currentColor"/></svg>`;
|
export const ICON_APPS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18.492 2.33 3.179 3.179a2.25 2.25 0 0 1 0 3.182l-2.584 2.584A2.25 2.25 0 0 1 21 13.5v5.25A2.25 2.25 0 0 1 18.75 21H5.25A2.25 2.25 0 0 1 3 18.75V5.25A2.25 2.25 0 0 1 5.25 3h5.25a2.25 2.25 0 0 1 2.225 1.915L15.31 2.33a2.25 2.25 0 0 1 3.182 0ZM4.5 18.75c0 .414.336.75.75.75l5.999-.001.001-6.75H4.5v6Zm8.249.749h6.001a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75h-6.001v6.75Zm-2.249-15H5.25a.75.75 0 0 0-.75.75v6h6.75v-6a.75.75 0 0 0-.75-.75Zm2.25 4.81v1.94h1.94l-1.94-1.94Zm3.62-5.918-3.178 3.178a.75.75 0 0 0 0 1.061l3.179 3.179a.75.75 0 0 0 1.06 0l3.18-3.179a.75.75 0 0 0 0-1.06l-3.18-3.18a.75.75 0 0 0-1.06 0Z" fill="currentColor"/></svg>`;
|
||||||
|
export const SEARCH = `<svg class="ninja-icon ninja-icon--fluent" width="18" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073l-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5a5.75 5.75 0 0 0 0-11.5Z"/></svg>`;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
hideBreadcrumbs
|
hideBreadcrumbs
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@selected="setCommandbarData"
|
@selected="setCommandbarData"
|
||||||
|
@change="onChange"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ import agentMixin from 'dashboard/mixins/agentMixin';
|
||||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||||
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||||
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [
|
mixins: [
|
||||||
|
@ -53,8 +55,28 @@ export default {
|
||||||
this.setCommandbarData();
|
this.setCommandbarData();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setCommandbarData() {
|
setCommandbarData(e) {
|
||||||
this.$refs.ninjakeys.data = this.hotKeys;
|
if (e && e.detail.action.id && e.detail.action.title) {
|
||||||
|
const action = e.detail.action;
|
||||||
|
if (action.type === 'contact') {
|
||||||
|
this.$refs.ninjakeys.close();
|
||||||
|
this.$router.push(
|
||||||
|
frontendURL(`accounts/${this.accountId}/contacts/${action.key}`)
|
||||||
|
);
|
||||||
|
} else if (action.type === 'message') {
|
||||||
|
this.$refs.ninjakeys.close();
|
||||||
|
this.$router.push(
|
||||||
|
frontendURL(
|
||||||
|
`accounts/${this.accountId}/conversations/${action.key}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$refs.ninjakeys.data = this.hotKeys;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange(ninjaKeyInstance) {
|
||||||
|
// console.log(ninjaKeyInstance);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,41 @@ import { mapGetters } from 'vuex';
|
||||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||||
|
|
||||||
const GO_TO_COMMANDS = [
|
const GO_TO_COMMANDS = [
|
||||||
|
{
|
||||||
|
id: 'search_everything',
|
||||||
|
title: 'COMMAND_BAR.COMMANDS.SEARCH_EVEYTHING',
|
||||||
|
section: 'COMMAND_BAR.SECTIONS.CHATWOOT',
|
||||||
|
icon: ICON_CONVERSATION_DASHBOARD,
|
||||||
|
builder: searchKey => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
fetch(`/api/v1/accounts/1/conversations/text_search?q=${searchKey}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
api_access_token: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const contacts = data.payload.contacts.map(result => ({
|
||||||
|
id: result.id,
|
||||||
|
title: result.name,
|
||||||
|
type: 'contact',
|
||||||
|
}));
|
||||||
|
const messages = data.payload.messages.map(result => ({
|
||||||
|
id: result.conversation_id,
|
||||||
|
title: result.content,
|
||||||
|
type: 'message',
|
||||||
|
}));
|
||||||
|
const flattened = [...contacts, ...messages];
|
||||||
|
return resolve(flattened);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
role: ['agent'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'goto_conversation_dashboard',
|
id: 'goto_conversation_dashboard',
|
||||||
title: 'COMMAND_BAR.COMMANDS.GO_TO_CONVERSATION_DASHBOARD',
|
title: 'COMMAND_BAR.COMMANDS.GO_TO_CONVERSATION_DASHBOARD',
|
||||||
|
@ -180,18 +215,25 @@ export default {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.isAdmin) {
|
if (!this.isAdmin) {
|
||||||
commands = commands.filter(command => command.role.includes('agent'));
|
commands = commands.filter(command => command.role.includes('agent'));
|
||||||
}
|
}
|
||||||
|
return commands.map(command => {
|
||||||
return commands.map(command => ({
|
const hotKey = {
|
||||||
id: command.id,
|
id: command.id,
|
||||||
section: this.$t(command.section),
|
section: this.$t(command.section),
|
||||||
title: this.$t(command.title),
|
title: this.$t(command.title),
|
||||||
icon: command.icon,
|
icon: command.icon,
|
||||||
handler: () => this.openRoute(command.path(this.accountId)),
|
};
|
||||||
}));
|
if (command.builder) {
|
||||||
|
hotKey.builder = command.builder;
|
||||||
|
} else {
|
||||||
|
hotKey.handler = () => {
|
||||||
|
this.openRoute(command.path(this.accountId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return hotKey;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import MacroPreview from './MacroPreview';
|
import MacroPreview from './MacroPreview';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../../helper/AnalyticsHelper';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MacroPreview,
|
MacroPreview,
|
||||||
|
@ -64,6 +67,7 @@ export default {
|
||||||
macroId: macro.id,
|
macroId: macro.id,
|
||||||
conversationIds: [this.conversationId],
|
conversationIds: [this.conversationId],
|
||||||
});
|
});
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.EXECUTED_A_MACRO);
|
||||||
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('MACROS.ERROR'));
|
this.showAlert(this.$t('MACROS.ERROR'));
|
||||||
|
|
|
@ -171,11 +171,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||||
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
|
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
|
||||||
|
import { URLPattern } from 'urlpattern-polyfill';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -221,8 +222,23 @@ export default {
|
||||||
},
|
},
|
||||||
endPoint: {
|
endPoint: {
|
||||||
required,
|
required,
|
||||||
minLength: minLength(7),
|
shouldBeAValidURLPattern(value) {
|
||||||
url,
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
new URLPattern(value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldStartWithHTTP(value) {
|
||||||
|
if (value) {
|
||||||
|
return (
|
||||||
|
value.startsWith('https://') || value.startsWith('http://')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
timeOnPage: {
|
timeOnPage: {
|
||||||
required,
|
required,
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<label :class="{ error: $v.selectedInbox.$error }">
|
<label :class="{ error: $v.selectedInbox.$error }">
|
||||||
{{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }}
|
{{ $t('CAMPAIGN.ADD.FORM.INBOX.LABEL') }}
|
||||||
<select v-model="selectedInbox" @change="onChangeInbox($event)">
|
<select v-model="selectedInbox" @change="onChangeInbox($event)">
|
||||||
<option v-for="item in inboxes" :key="item.name" :value="item.id">
|
<option v-for="item in inboxes" :key="item.id" :value="item.id">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -111,10 +111,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||||
|
import { URLPattern } from 'urlpattern-polyfill';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
|
@ -152,8 +154,21 @@ export default {
|
||||||
},
|
},
|
||||||
endPoint: {
|
endPoint: {
|
||||||
required,
|
required,
|
||||||
minLength: minLength(7),
|
shouldBeAValidURLPattern(value) {
|
||||||
url,
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
new URLPattern(value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldStartWithHTTP(value) {
|
||||||
|
if (value) {
|
||||||
|
return value.startsWith('https://') || value.startsWith('http://');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
timeOnPage: {
|
timeOnPage: {
|
||||||
required,
|
required,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import dashboard from './dashboard/dashboard.routes';
|
||||||
import login from './login/login.routes';
|
import login from './login/login.routes';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
||||||
|
import AnalyticsHelper from '../helper/AnalyticsHelper';
|
||||||
|
|
||||||
const routes = [...login.routes, ...dashboard.routes, ...authRoute.routes];
|
const routes = [...login.routes, ...dashboard.routes, ...authRoute.routes];
|
||||||
|
|
||||||
|
@ -117,6 +118,11 @@ export const validateRouteAccess = (to, from, next, { getters }) => {
|
||||||
export const initalizeRouter = () => {
|
export const initalizeRouter = () => {
|
||||||
const userAuthentication = store.dispatch('setUser');
|
const userAuthentication = store.dispatch('setUser');
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
AnalyticsHelper.page(to.name || '', {
|
||||||
|
path: to.path,
|
||||||
|
name: to.name,
|
||||||
|
});
|
||||||
|
|
||||||
if (validateSSOLoginParams(to)) {
|
if (validateSSOLoginParams(to)) {
|
||||||
clearBrowserSessionCookies();
|
clearBrowserSessionCookies();
|
||||||
next();
|
next();
|
||||||
|
|
|
@ -10,6 +10,9 @@ import {
|
||||||
isOnUnattendedView,
|
isOnUnattendedView,
|
||||||
} from './helpers/actionHelpers';
|
} from './helpers/actionHelpers';
|
||||||
import messageReadActions from './actions/messageReadActions';
|
import messageReadActions from './actions/messageReadActions';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
// actions
|
// actions
|
||||||
const actions = {
|
const actions = {
|
||||||
getConversation: async ({ commit }, conversationId) => {
|
getConversation: async ({ commit }, conversationId) => {
|
||||||
|
@ -171,6 +174,11 @@ const actions = {
|
||||||
status: MESSAGE_STATUS.PROGRESS,
|
status: MESSAGE_STATUS.PROGRESS,
|
||||||
});
|
});
|
||||||
const response = await MessageApi.create(pendingMessage);
|
const response = await MessageApi.create(pendingMessage);
|
||||||
|
AnalyticsHelper.track(
|
||||||
|
pendingMessage.private
|
||||||
|
? ANALYTICS_EVENTS.SENT_PRIVATE_NOTE
|
||||||
|
: ANALYTICS_EVENTS.SENT_MESSAGE
|
||||||
|
);
|
||||||
commit(types.ADD_MESSAGE, {
|
commit(types.ADD_MESSAGE, {
|
||||||
...response.data,
|
...response.data,
|
||||||
status: MESSAGE_STATUS.SENT,
|
status: MESSAGE_STATUS.SENT,
|
||||||
|
|
|
@ -6,6 +6,9 @@ import WebChannel from '../../api/channel/webChannel';
|
||||||
import FBChannel from '../../api/channel/fbChannel';
|
import FBChannel from '../../api/channel/fbChannel';
|
||||||
import TwilioChannel from '../../api/channel/twilioChannel';
|
import TwilioChannel from '../../api/channel/twilioChannel';
|
||||||
import { throwErrorMessage } from '../utils/api';
|
import { throwErrorMessage } from '../utils/api';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
const buildInboxData = inboxParams => {
|
const buildInboxData = inboxParams => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
@ -117,6 +120,12 @@ export const getters = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendAnalyticsEvent = channelType => {
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.ADDED_AN_INBOX, {
|
||||||
|
channelType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
get: async ({ commit }) => {
|
get: async ({ commit }) => {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: true });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: true });
|
||||||
|
@ -134,6 +143,8 @@ export const actions = {
|
||||||
const response = await WebChannel.create(params);
|
const response = await WebChannel.create(params);
|
||||||
commit(types.default.ADD_INBOXES, response.data);
|
commit(types.default.ADD_INBOXES, response.data);
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
const { channel = {} } = params;
|
||||||
|
sendAnalyticsEvent(channel.type);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
@ -146,6 +157,7 @@ export const actions = {
|
||||||
const response = await WebChannel.create(buildInboxData(params));
|
const response = await WebChannel.create(buildInboxData(params));
|
||||||
commit(types.default.ADD_INBOXES, response.data);
|
commit(types.default.ADD_INBOXES, response.data);
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
sendAnalyticsEvent('website');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
@ -158,6 +170,7 @@ export const actions = {
|
||||||
const response = await TwilioChannel.create(params);
|
const response = await TwilioChannel.create(params);
|
||||||
commit(types.default.ADD_INBOXES, response.data);
|
commit(types.default.ADD_INBOXES, response.data);
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
sendAnalyticsEvent('twilio');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
@ -170,6 +183,7 @@ export const actions = {
|
||||||
const response = await FBChannel.create(params);
|
const response = await FBChannel.create(params);
|
||||||
commit(types.default.ADD_INBOXES, response.data);
|
commit(types.default.ADD_INBOXES, response.data);
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
sendAnalyticsEvent('facebook');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/* eslint no-console: 0 */
|
|
||||||
/* eslint-env browser */
|
|
||||||
/* eslint-disable no-new */
|
|
||||||
/* Vue Core */
|
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
|
@ -32,7 +27,6 @@ import constants from '../dashboard/constants';
|
||||||
import * as Sentry from '@sentry/vue';
|
import * as Sentry from '@sentry/vue';
|
||||||
import 'vue-easytable/libs/theme-default/index.css';
|
import 'vue-easytable/libs/theme-default/index.css';
|
||||||
import { Integrations } from '@sentry/tracing';
|
import { Integrations } from '@sentry/tracing';
|
||||||
import posthog from 'posthog-js';
|
|
||||||
import {
|
import {
|
||||||
initializeAnalyticsEvents,
|
initializeAnalyticsEvents,
|
||||||
initializeChatwootEvents,
|
initializeChatwootEvents,
|
||||||
|
@ -40,6 +34,7 @@ import {
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||||
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
||||||
|
import AnalyticsHelper from '../dashboard/helper/AnalyticsHelper';
|
||||||
|
|
||||||
Vue.config.env = process.env;
|
Vue.config.env = process.env;
|
||||||
|
|
||||||
|
@ -51,12 +46,6 @@ if (window.errorLoggingConfig) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.analyticsConfig) {
|
|
||||||
posthog.init(window.analyticsConfig.token, {
|
|
||||||
api_host: window.analyticsConfig.host,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
Vue.use(VueI18n);
|
Vue.use(VueI18n);
|
||||||
|
@ -90,6 +79,7 @@ window.WootConstants = constants;
|
||||||
window.axios = createAxios(axios);
|
window.axios = createAxios(axios);
|
||||||
window.bus = new Vue();
|
window.bus = new Vue();
|
||||||
initializeChatwootEvents();
|
initializeChatwootEvents();
|
||||||
|
AnalyticsHelper.init();
|
||||||
initializeAnalyticsEvents();
|
initializeAnalyticsEvents();
|
||||||
initalizeRouter();
|
initalizeRouter();
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
|
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!error && helpText"
|
||||||
|
class="text-red-400 mt-2 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{{ helpText }}
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
@ -41,6 +47,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
helpText: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labelClass() {
|
labelClass() {
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
export const stripTrailingSlash = ({ URL }) => {
|
import { URLPattern } from 'urlpattern-polyfill';
|
||||||
return URL.replace(/\/$/, '');
|
|
||||||
|
export const isPatternMatchingWithURL = (urlPattern, url) => {
|
||||||
|
let updatedUrlPattern = urlPattern;
|
||||||
|
const locationObj = new URL(url);
|
||||||
|
|
||||||
|
if (updatedUrlPattern.endsWith('/')) {
|
||||||
|
updatedUrlPattern = updatedUrlPattern.slice(0, -1) + '*\\?*\\#*';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locationObj.pathname.endsWith('/')) {
|
||||||
|
locationObj.pathname = locationObj.pathname.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new URLPattern(updatedUrlPattern);
|
||||||
|
return pattern.test(locationObj.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format all campaigns
|
// Format all campaigns
|
||||||
|
@ -22,10 +36,7 @@ export const filterCampaigns = ({
|
||||||
isInBusinessHours,
|
isInBusinessHours,
|
||||||
}) => {
|
}) => {
|
||||||
return campaigns.filter(campaign => {
|
return campaigns.filter(campaign => {
|
||||||
const hasMatchingURL =
|
if (!isPatternMatchingWithURL(campaign.url, currentURL)) {
|
||||||
stripTrailingSlash({ URL: campaign.url }) ===
|
|
||||||
stripTrailingSlash({ URL: currentURL });
|
|
||||||
if (!hasMatchingURL) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (campaign.triggerOnlyDuringBusinessHours) {
|
if (campaign.triggerOnlyDuringBusinessHours) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
stripTrailingSlash,
|
|
||||||
formatCampaigns,
|
formatCampaigns,
|
||||||
filterCampaigns,
|
filterCampaigns,
|
||||||
|
isPatternMatchingWithURL,
|
||||||
} from '../campaignHelper';
|
} from '../campaignHelper';
|
||||||
import campaigns from './campaignFixtures';
|
import campaigns from './campaignFixtures';
|
||||||
|
|
||||||
|
@ -9,11 +9,35 @@ global.chatwootWebChannel = {
|
||||||
workingHoursEnabled: false,
|
workingHoursEnabled: false,
|
||||||
};
|
};
|
||||||
describe('#Campaigns Helper', () => {
|
describe('#Campaigns Helper', () => {
|
||||||
describe('stripTrailingSlash', () => {
|
describe('#isPatternMatchingWithURL', () => {
|
||||||
it('should return striped trailing slash if url with trailing slash is passed', () => {
|
it('returns correct value if a valid URL is passed', () => {
|
||||||
expect(
|
expect(
|
||||||
stripTrailingSlash({ URL: 'https://www.chatwoot.com/pricing/' })
|
isPatternMatchingWithURL(
|
||||||
).toBe('https://www.chatwoot.com/pricing');
|
'https://chatwoot.com/pricing*',
|
||||||
|
'https://chatwoot.com/pricing/'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isPatternMatchingWithURL(
|
||||||
|
'https://*.chatwoot.com/pricing/',
|
||||||
|
'https://app.chatwoot.com/pricing/'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isPatternMatchingWithURL(
|
||||||
|
'https://{*.}?chatwoot.com/pricing?test=true',
|
||||||
|
'https://app.chatwoot.com/pricing/?test=true'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isPatternMatchingWithURL(
|
||||||
|
'https://{*.}?chatwoot.com/pricing*\\?*',
|
||||||
|
'https://chatwoot.com/pricing/?test=true'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -70,12 +70,15 @@ class Attachment < ApplicationRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def file_metadata
|
def file_metadata
|
||||||
{
|
metadata = {
|
||||||
extension: extension,
|
extension: extension,
|
||||||
data_url: file_url,
|
data_url: file_url,
|
||||||
thumb_url: thumb_url,
|
thumb_url: thumb_url,
|
||||||
file_size: file.byte_size
|
file_size: file.byte_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata[:data_url] = metadata[:thumb_url] = external_url if message.instagram_story_mention?
|
||||||
|
metadata
|
||||||
end
|
end
|
||||||
|
|
||||||
def location_metadata
|
def location_metadata
|
||||||
|
|
|
@ -86,7 +86,8 @@ class Campaign < ApplicationRecord
|
||||||
def validate_url
|
def validate_url
|
||||||
return unless trigger_rules['url']
|
return unless trigger_rules['url']
|
||||||
|
|
||||||
errors.add(:url, 'invalid') if inbox.inbox_type == 'Website' && !url_valid?(trigger_rules['url'])
|
use_http_protocol = trigger_rules['url'].starts_with?('http://') || trigger_rules['url'].starts_with?('https://')
|
||||||
|
errors.add(:url, 'invalid') if inbox.inbox_type == 'Website' && !use_http_protocol
|
||||||
end
|
end
|
||||||
|
|
||||||
def prevent_completed_campaign_from_update
|
def prevent_completed_campaign_from_update
|
||||||
|
|
|
@ -63,4 +63,21 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: We will be removing this code after instagram_manage_insights is implemented
|
||||||
|
def fetch_instagram_story_link(message)
|
||||||
|
k = Koala::Facebook::API.new(page_access_token)
|
||||||
|
result = k.get_object(message.source_id, fields: %w[story]) || {}
|
||||||
|
story_link = result['story']['mention']['link']
|
||||||
|
# If the story is expired then it raises the ClientError and if the story is deleted with valid story-id it responses with nil
|
||||||
|
delete_instagram_story(message) if story_link.blank?
|
||||||
|
story_link
|
||||||
|
rescue Koala::Facebook::ClientError => e
|
||||||
|
delete_instagram_story(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_instagram_story(message)
|
||||||
|
message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'), content_attributes: {})
|
||||||
|
message.attachments.destroy_all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,4 +16,8 @@ module MessageFilterHelpers
|
||||||
def email_reply_summarizable?
|
def email_reply_summarizable?
|
||||||
incoming? || outgoing? || input_csat?
|
incoming? || outgoing? || input_csat?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def instagram_story_mention?
|
||||||
|
inbox.instagram? && try(:content_attributes)[:image_type] == 'story_mention'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
48
app/models/concerns/multi_searchable_helpers.rb
Normal file
48
app/models/concerns/multi_searchable_helpers.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
module MultiSearchableHelpers
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
PgSearch.multisearch_options = {
|
||||||
|
using: {
|
||||||
|
tsearch: {
|
||||||
|
prefix: true,
|
||||||
|
any_word: true,
|
||||||
|
normalization: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_contact_search_document
|
||||||
|
return if contact_pg_search_record.present?
|
||||||
|
|
||||||
|
initialize_contact_pg_search_record.update!(
|
||||||
|
content: "#{contact.id} #{contact.email} #{contact.name} #{contact.phone_number}",
|
||||||
|
conversation_id: id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_pg_search_record
|
||||||
|
contacts_pg_search_records.find_by(conversation_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_contact_pg_search_record
|
||||||
|
record = contacts_pg_search_records.find_by(conversation_id: nil)
|
||||||
|
|
||||||
|
return record if record.present?
|
||||||
|
|
||||||
|
PgSearch::Document.new(
|
||||||
|
searchable_type: 'Contact',
|
||||||
|
searchable_id: contact_id,
|
||||||
|
account_id: account_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def contacts_pg_search_records
|
||||||
|
PgSearch::Document.where(
|
||||||
|
searchable_type: 'Contact',
|
||||||
|
searchable_id: contact_id,
|
||||||
|
account_id: account_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,6 +26,13 @@ class Contact < ApplicationRecord
|
||||||
include Avatarable
|
include Avatarable
|
||||||
include AvailabilityStatusable
|
include AvailabilityStatusable
|
||||||
include Labelable
|
include Labelable
|
||||||
|
include PgSearch::Model
|
||||||
|
include MultiSearchableHelpers
|
||||||
|
|
||||||
|
multisearchable(
|
||||||
|
against: [:id, :email, :name, :phone_number],
|
||||||
|
additional_attributes: ->(contact) { { conversation_id: nil, account_id: contact.account_id } }
|
||||||
|
)
|
||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :email, allow_blank: true, uniqueness: { scope: [:account_id], case_sensitive: false },
|
validates :email, allow_blank: true, uniqueness: { scope: [:account_id], case_sensitive: false },
|
||||||
|
@ -140,6 +147,24 @@ class Contact < ApplicationRecord
|
||||||
email_format
|
email_format
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.rebuild_pg_search_documents
|
||||||
|
return super unless name == 'Contact'
|
||||||
|
|
||||||
|
connection.execute <<~SQL.squish
|
||||||
|
INSERT INTO pg_search_documents (searchable_type, searchable_id, content, account_id, conversation_id, created_at, updated_at)
|
||||||
|
SELECT 'Contact' AS searchable_type,
|
||||||
|
contacts.id AS searchable_id,
|
||||||
|
CONCAT_WS(' ', contacts.email, contacts.name, contacts.phone_number, contacts.id, contacts.account_id) AS content,
|
||||||
|
contacts.account_id::int AS account_id,
|
||||||
|
conversations.id AS conversation_id,
|
||||||
|
now() AS created_at,
|
||||||
|
now() AS updated_at
|
||||||
|
FROM contacts
|
||||||
|
LEFT OUTER JOIN conversations
|
||||||
|
ON conversations.contact_id = contacts.id
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ip_lookup
|
def ip_lookup
|
||||||
|
|
|
@ -48,7 +48,13 @@ class Conversation < ApplicationRecord
|
||||||
include ActivityMessageHandler
|
include ActivityMessageHandler
|
||||||
include UrlHelper
|
include UrlHelper
|
||||||
include SortHandler
|
include SortHandler
|
||||||
|
include PgSearch::Model
|
||||||
|
include MultiSearchableHelpers
|
||||||
|
|
||||||
|
multisearchable(
|
||||||
|
against: [:display_id],
|
||||||
|
additional_attributes: ->(conversation) { { conversation_id: conversation.id, account_id: conversation.account_id } }
|
||||||
|
)
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
before_validation :validate_additional_attributes
|
before_validation :validate_additional_attributes
|
||||||
|
@ -93,6 +99,7 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
after_update_commit :execute_after_update_commit_callbacks
|
after_update_commit :execute_after_update_commit_callbacks
|
||||||
after_create_commit :notify_conversation_creation
|
after_create_commit :notify_conversation_creation
|
||||||
|
after_create_commit :update_contact_search_document, if: :contact_id?
|
||||||
after_commit :set_display_id, unless: :display_id?
|
after_commit :set_display_id, unless: :display_id?
|
||||||
|
|
||||||
delegate :auto_resolve_duration, to: :account
|
delegate :auto_resolve_duration, to: :account
|
||||||
|
|
|
@ -78,6 +78,10 @@ class Inbox < ApplicationRecord
|
||||||
channel_type == 'Channel::FacebookPage'
|
channel_type == 'Channel::FacebookPage'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def instagram?
|
||||||
|
facebook? && channel.instagram_id.present?
|
||||||
|
end
|
||||||
|
|
||||||
def web_widget?
|
def web_widget?
|
||||||
channel_type == 'Channel::WebWidget'
|
channel_type == 'Channel::WebWidget'
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,6 +33,14 @@
|
||||||
class Message < ApplicationRecord
|
class Message < ApplicationRecord
|
||||||
include MessageFilterHelpers
|
include MessageFilterHelpers
|
||||||
NUMBER_OF_PERMITTED_ATTACHMENTS = 15
|
NUMBER_OF_PERMITTED_ATTACHMENTS = 15
|
||||||
|
include PgSearch::Model
|
||||||
|
include MultiSearchableHelpers
|
||||||
|
|
||||||
|
multisearchable(
|
||||||
|
against: [:content],
|
||||||
|
if: :allowed_message_types?,
|
||||||
|
additional_attributes: ->(message) { { conversation_id: message.conversation_id, account_id: message.account_id } }
|
||||||
|
)
|
||||||
|
|
||||||
before_validation :ensure_content_type
|
before_validation :ensure_content_type
|
||||||
|
|
||||||
|
@ -107,10 +115,20 @@ class Message < ApplicationRecord
|
||||||
conversation: { assignee_id: conversation.assignee_id }
|
conversation: { assignee_id: conversation.assignee_id }
|
||||||
)
|
)
|
||||||
data.merge!(echo_id: echo_id) if echo_id.present?
|
data.merge!(echo_id: echo_id) if echo_id.present?
|
||||||
|
validate_instagram_story if instagram_story_mention?
|
||||||
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
||||||
merge_sender_attributes(data)
|
merge_sender_attributes(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: We will be removing this code after instagram_manage_insights is implemented
|
||||||
|
# Better logic is to listen to webhook and remove stories proactively rather than trying
|
||||||
|
# a fetch every time a message is returned
|
||||||
|
def validate_instagram_story
|
||||||
|
inbox.channel.fetch_instagram_story_link(self)
|
||||||
|
# we want to reload the message in case the story has expired and data got removed
|
||||||
|
reload
|
||||||
|
end
|
||||||
|
|
||||||
def merge_sender_attributes(data)
|
def merge_sender_attributes(data)
|
||||||
data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
|
data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
|
||||||
data.merge!(sender: sender.push_event_data(inbox)) if sender.is_a?(AgentBot)
|
data.merge!(sender: sender.push_event_data(inbox)) if sender.is_a?(AgentBot)
|
||||||
|
@ -264,4 +282,8 @@ class Message < ApplicationRecord
|
||||||
conversation.update_columns(last_activity_at: created_at)
|
conversation.update_columns(last_activity_at: created_at)
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allowed_message_types?
|
||||||
|
incoming? || outgoing?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
# display_name :string
|
# display_name :string
|
||||||
# email :string
|
# email :string
|
||||||
# encrypted_password :string default(""), not null
|
# encrypted_password :string default(""), not null
|
||||||
|
# failed_attempts :integer
|
||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
# last_sign_in_ip :string
|
# last_sign_in_ip :string
|
||||||
|
# locked_at :datetime
|
||||||
# message_signature :text
|
# message_signature :text
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# provider :string default("email"), not null
|
# provider :string default("email"), not null
|
||||||
|
@ -28,6 +30,7 @@
|
||||||
# ui_settings :jsonb
|
# ui_settings :jsonb
|
||||||
# uid :string default(""), not null
|
# uid :string default(""), not null
|
||||||
# unconfirmed_email :string
|
# unconfirmed_email :string
|
||||||
|
# unlock_token :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
# display_name :string
|
# display_name :string
|
||||||
# email :string
|
# email :string
|
||||||
# encrypted_password :string default(""), not null
|
# encrypted_password :string default(""), not null
|
||||||
|
# failed_attempts :integer
|
||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
# last_sign_in_ip :string
|
# last_sign_in_ip :string
|
||||||
|
# locked_at :datetime
|
||||||
# message_signature :text
|
# message_signature :text
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# provider :string default("email"), not null
|
# provider :string default("email"), not null
|
||||||
|
@ -28,6 +30,7 @@
|
||||||
# ui_settings :jsonb
|
# ui_settings :jsonb
|
||||||
# uid :string default(""), not null
|
# uid :string default(""), not null
|
||||||
# unconfirmed_email :string
|
# unconfirmed_email :string
|
||||||
|
# unlock_token :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
json.payload do
|
||||||
|
json.conversations do
|
||||||
|
json.array! @result[:conversations] do |conversation|
|
||||||
|
json.partial! 'api/v1/models/conversation', formats: [:json], conversation: conversation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.contacts do
|
||||||
|
json.array! @result[:contacts] do |contact|
|
||||||
|
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.messages do
|
||||||
|
json.array! @result[:messages] do |message|
|
||||||
|
json.partial! 'api/v1/models/message', formats: [:json], message: message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@ json.id message.id
|
||||||
json.content message.content
|
json.content message.content
|
||||||
json.inbox_id message.inbox_id
|
json.inbox_id message.inbox_id
|
||||||
json.echo_id message.echo_id if message.echo_id
|
json.echo_id message.echo_id if message.echo_id
|
||||||
json.conversation_id message.conversation.display_id
|
json.conversation_id message.conversation.try(: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.status message.status
|
||||||
|
|
|
@ -51,11 +51,10 @@
|
||||||
}
|
}
|
||||||
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_DSN', '')%>'
|
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_DSN', '')%>'
|
||||||
</script>
|
</script>
|
||||||
<% if @global_config['ANALYTICS_TOKEN'].present? && @global_config['ANALYTICS_HOST'].present? %>
|
<% if @global_config['ANALYTICS_TOKEN'].present? %>
|
||||||
<script>
|
<script>
|
||||||
window.analyticsConfig = {
|
window.analyticsConfig = {
|
||||||
token: '<%= @global_config['ANALYTICS_TOKEN'] %>',
|
token: '<%= @global_config['ANALYTICS_TOKEN'] %>',
|
||||||
host: '<%= @global_config['ANALYTICS_HOST'] %>',
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -42,8 +42,6 @@
|
||||||
value:
|
value:
|
||||||
- name: ANALYTICS_TOKEN
|
- name: ANALYTICS_TOKEN
|
||||||
value:
|
value:
|
||||||
- name: ANALYTICS_HOST
|
|
||||||
value:
|
|
||||||
- name: DIRECT_UPLOADS_ENABLED
|
- name: DIRECT_UPLOADS_ENABLED
|
||||||
value: false
|
value: false
|
||||||
locked: false
|
locked: false
|
||||||
|
|
|
@ -71,6 +71,7 @@ Rails.application.routes.draw do
|
||||||
get :meta
|
get :meta
|
||||||
get :search
|
get :search
|
||||||
post :filter
|
post :filter
|
||||||
|
get :text_search
|
||||||
end
|
end
|
||||||
scope module: :conversations do
|
scope module: :conversations do
|
||||||
resources :messages, only: [:index, :create, :destroy]
|
resources :messages, only: [:index, :create, :destroy]
|
||||||
|
|
21
db/migrate/20221205081737_create_pg_search_documents.rb
Normal file
21
db/migrate/20221205081737_create_pg_search_documents.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
class CreatePgSearchDocuments < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
say_with_time('Creating table for pg_search multisearch') do
|
||||||
|
create_table :pg_search_documents do |t|
|
||||||
|
t.text :content
|
||||||
|
t.bigint 'conversation_id'
|
||||||
|
t.bigint 'account_id'
|
||||||
|
t.belongs_to :searchable, polymorphic: true, index: true
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :pg_search_documents, :account_id
|
||||||
|
add_index :pg_search_documents, :conversation_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
say_with_time('Dropping table for pg_search multisearch') do
|
||||||
|
drop_table :pg_search_documents
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
db/migrate/20221212061802_enable_multi_searchable.rb
Normal file
11
db/migrate/20221212061802_enable_multi_searchable.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class EnableMultiSearchable < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
Contact.rebuild_pg_search_documents
|
||||||
|
PgSearch::Multisearch.rebuild(Conversation)
|
||||||
|
PgSearch::Multisearch.rebuild(Message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
PgSearch::Document.delete_all
|
||||||
|
end
|
||||||
|
end
|
20
db/schema.rb
20
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
ActiveRecord::Schema.define(version: 2022_12_12_061802) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -399,7 +399,7 @@ ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
||||||
t.datetime "agent_last_seen_at"
|
t.datetime "agent_last_seen_at"
|
||||||
t.jsonb "additional_attributes", default: {}
|
t.jsonb "additional_attributes", default: {}
|
||||||
t.bigint "contact_inbox_id"
|
t.bigint "contact_inbox_id"
|
||||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
t.uuid "uuid", default: -> { "public.gen_random_uuid()" }, null: false
|
||||||
t.string "identifier"
|
t.string "identifier"
|
||||||
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||||
t.bigint "team_id"
|
t.bigint "team_id"
|
||||||
|
@ -674,6 +674,19 @@ ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
||||||
t.index ["user_id"], name: "index_notifications_on_user_id"
|
t.index ["user_id"], name: "index_notifications_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "pg_search_documents", force: :cascade do |t|
|
||||||
|
t.text "content"
|
||||||
|
t.bigint "conversation_id"
|
||||||
|
t.bigint "account_id"
|
||||||
|
t.string "searchable_type"
|
||||||
|
t.bigint "searchable_id"
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["account_id"], name: "index_pg_search_documents_on_account_id"
|
||||||
|
t.index ["conversation_id"], name: "index_pg_search_documents_on_conversation_id"
|
||||||
|
t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "platform_app_permissibles", force: :cascade do |t|
|
create_table "platform_app_permissibles", force: :cascade do |t|
|
||||||
t.bigint "platform_app_id", null: false
|
t.bigint "platform_app_id", null: false
|
||||||
t.string "permissible_type", null: false
|
t.string "permissible_type", null: false
|
||||||
|
@ -836,6 +849,9 @@ ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
||||||
t.jsonb "custom_attributes", default: {}
|
t.jsonb "custom_attributes", default: {}
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.text "message_signature"
|
t.text "message_signature"
|
||||||
|
t.datetime "locked_at"
|
||||||
|
t.integer "failed_attempts"
|
||||||
|
t.string "unlock_token"
|
||||||
t.index ["email"], name: "index_users_on_email"
|
t.index ["email"], name: "index_users_on_email"
|
||||||
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
|
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
||||||
"@chatwoot/utils": "^0.0.10",
|
"@chatwoot/utils": "^0.0.10",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
|
"@june-so/analytics-next": "^1.36.5",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
"@rails/ujs": "^7.0.3-1",
|
"@rails/ujs": "^7.0.3-1",
|
||||||
"@rails/webpacker": "5.3.0",
|
"@rails/webpacker": "5.3.0",
|
||||||
|
@ -44,9 +45,8 @@
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"marked": "4.0.10",
|
"marked": "4.0.10",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"ninja-keys": "^1.1.9",
|
"ninja-keys": "github:fayazara/ninja-keys",
|
||||||
"opus-recorder": "^8.0.5",
|
"opus-recorder": "^8.0.5",
|
||||||
"posthog-js": "^1.13.7",
|
|
||||||
"prosemirror-markdown": "1.5.1",
|
"prosemirror-markdown": "1.5.1",
|
||||||
"prosemirror-state": "1.3.4",
|
"prosemirror-state": "1.3.4",
|
||||||
"prosemirror-view": "1.18.4",
|
"prosemirror-view": "1.18.4",
|
||||||
|
@ -55,6 +55,7 @@
|
||||||
"tailwindcss": "^1.9.6",
|
"tailwindcss": "^1.9.6",
|
||||||
"turbolinks": "^5.2.0",
|
"turbolinks": "^5.2.0",
|
||||||
"url-loader": "^2.0.0",
|
"url-loader": "^2.0.0",
|
||||||
|
"urlpattern-polyfill": "^6.0.2",
|
||||||
"v-tooltip": "~2.1.3",
|
"v-tooltip": "~2.1.3",
|
||||||
"videojs-record": "^4.5.0",
|
"videojs-record": "^4.5.0",
|
||||||
"vue": "2.6.12",
|
"vue": "2.6.12",
|
||||||
|
|
|
@ -68,7 +68,7 @@ describe ::Messages::Instagram::MessageBuilder do
|
||||||
|
|
||||||
expect(contact.name).to eq('Jane Dae')
|
expect(contact.name).to eq('Jane Dae')
|
||||||
expect(message.content).to eq('This story is no longer available.')
|
expect(message.content).to eq('This story is no longer available.')
|
||||||
expect(message.attachments.count).to eq(1)
|
expect(message.attachments.count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create message for unsupported file type' do
|
it 'does not create message for unsupported file type' do
|
||||||
|
|
|
@ -6,5 +6,9 @@ FactoryBot.define do
|
||||||
user_access_token { SecureRandom.uuid }
|
user_access_token { SecureRandom.uuid }
|
||||||
page_id { SecureRandom.uuid }
|
page_id { SecureRandom.uuid }
|
||||||
account
|
account
|
||||||
|
|
||||||
|
before :create do |_channel|
|
||||||
|
WebMock::API.stub_request(:post, 'https://graph.facebook.com/v3.2/me/subscribed_apps')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -8,6 +8,18 @@ FactoryBot.define do
|
||||||
content_type { 'text' }
|
content_type { 'text' }
|
||||||
account { create(:account) }
|
account { create(:account) }
|
||||||
|
|
||||||
|
trait :instagram_story_mention do
|
||||||
|
content_attributes { { image_type: 'story_mention' } }
|
||||||
|
after(:build) do |message|
|
||||||
|
unless message.inbox.instagram?
|
||||||
|
message.inbox = create(:inbox, account: message.account,
|
||||||
|
channel: create(:channel_instagram_fb_page, account: message.account, instagram_id: 'instagram-123'))
|
||||||
|
end
|
||||||
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image, external_url: 'https://www.example.com/test.jpeg')
|
||||||
|
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
after(:build) do |message|
|
after(:build) do |message|
|
||||||
message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
|
message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
|
||||||
message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account)
|
message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account)
|
||||||
|
|
51
spec/finders/text_search_spec.rb
Normal file
51
spec/finders/text_search_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ::TextSearch do
|
||||||
|
subject(:text_search) { described_class.new(user_1, params) }
|
||||||
|
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let!(:user_1) { create(:user, account: account) }
|
||||||
|
let!(:user_2) { create(:user, account: account) }
|
||||||
|
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:inbox_member, user: user_1, inbox: inbox)
|
||||||
|
create(:inbox_member, user: user_2, inbox: inbox)
|
||||||
|
|
||||||
|
create(:contact, name: '1223', account_id: account.id)
|
||||||
|
create(:contact, name: 'Potter', account_id: account.id)
|
||||||
|
contact_2 = create(:contact, name: 'Harry Potter', account_id: account.id)
|
||||||
|
conversation_1 = create(:conversation, account: account, inbox: inbox, assignee: user_1, display_id: 121)
|
||||||
|
conversation_2 = create(:conversation, account: account, inbox: inbox, assignee: user_1, display_id: 122)
|
||||||
|
create(:conversation, account: account, inbox: inbox, assignee: user_1, status: 'resolved', display_id: 13, contact_id: contact_2.id)
|
||||||
|
create(:conversation, account: account, inbox: inbox, assignee: user_2, display_id: 14)
|
||||||
|
create(:conversation, account: account, inbox: inbox, display_id: 15)
|
||||||
|
Current.account = account
|
||||||
|
|
||||||
|
create(:message, conversation_id: conversation_1.id, account_id: account.id, content: 'Ask Lisa')
|
||||||
|
create(:message, conversation_id: conversation_1.id, account_id: account.id, content: 'message_12')
|
||||||
|
create(:message, conversation_id: conversation_1.id, account_id: account.id, content: 'message_13')
|
||||||
|
|
||||||
|
create(:message, conversation_id: conversation_2.id, account_id: account.id, content: 'Pottery Barn order')
|
||||||
|
create(:message, conversation_id: conversation_2.id, account_id: account.id, content: 'message_22')
|
||||||
|
create(:message, conversation_id: conversation_2.id, account_id: account.id, content: 'message_23')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'with text search' do
|
||||||
|
it 'filter conversations by number' do
|
||||||
|
params = { q: '12' }
|
||||||
|
result = described_class.new(user_1, params).perform
|
||||||
|
expect(result[:conversations].length).to be 2
|
||||||
|
expect(result[:contacts].length).to be 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filter conversations by string' do
|
||||||
|
params = { q: 'pot' }
|
||||||
|
result = described_class.new(user_1, params).perform
|
||||||
|
expect(result[:messages].length).to be 1
|
||||||
|
expect(result[:contacts].length).to be 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -113,6 +113,9 @@ describe Webhooks::InstagramEventsJob do
|
||||||
|
|
||||||
expect(instagram_inbox.messages.count).to be 1
|
expect(instagram_inbox.messages.count).to be 1
|
||||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||||
|
|
||||||
|
attachment = instagram_inbox.messages.last.attachments.last
|
||||||
|
expect(attachment.push_event_data[:data_url]).to eq(attachment.external_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates does not create contact or messages' do
|
it 'creates does not create contact or messages' do
|
||||||
|
|
|
@ -9,4 +9,23 @@ RSpec.describe Attachment, type: :model do
|
||||||
expect(attachment.download_url).not_to be_nil
|
expect(attachment.download_url).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'push_event_data for instagram story mentions' do
|
||||||
|
let(:instagram_message) { create(:message, :instagram_story_mention) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# stubbing the request to facebook api during the message creation
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
|
||||||
|
from: { username: 'Sender-id-1', id: 'Sender-id-1' },
|
||||||
|
id: 'instagram-message-id-1234'
|
||||||
|
}.to_json, headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns external url as data and thumb urls' do
|
||||||
|
external_url = instagram_message.attachments.first.external_url
|
||||||
|
expect(instagram_message.attachments.first.push_event_data[:data_url]).to eq external_url
|
||||||
|
expect(instagram_message.attachments.first.push_event_data[:thumb_url]).to eq external_url
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -180,4 +180,34 @@ RSpec.describe Message, type: :model do
|
||||||
expect(message.email_notifiable_message?).to be true
|
expect(message.email_notifiable_message?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when facebook channel with unavailable story link' do
|
||||||
|
let(:instagram_message) { create(:message, :instagram_story_mention) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# stubbing the request to facebook api during the message creation
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
|
||||||
|
from: { username: 'Sender-id-1', id: 'Sender-id-1' },
|
||||||
|
id: 'instagram-message-id-1234'
|
||||||
|
}.to_json, headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the attachment for deleted stories' do
|
||||||
|
expect(instagram_message.attachments.count).to eq 1
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 404)
|
||||||
|
instagram_message.push_event_data
|
||||||
|
expect(instagram_message.reload.attachments.count).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the attachment for expired stories' do
|
||||||
|
expect(instagram_message.attachments.count).to eq 1
|
||||||
|
# for expired stories, the link will be empty
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: '', id: '17920786367196703' } }
|
||||||
|
}.to_json, headers: {})
|
||||||
|
instagram_message.push_event_data
|
||||||
|
expect(instagram_message.reload.attachments.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue