Merge branch 'develop' of https://github.com/chatwoot/chatwoot into spike/multiple-conv-widget
This commit is contained in:
commit
430c0b1569
31 changed files with 252 additions and 94 deletions
|
@ -161,3 +161,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
# LETTER_OPENER=true
|
# LETTER_OPENER=true
|
||||||
# meant to be used in github codespaces
|
# meant to be used in github codespaces
|
||||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||||
|
|
||||||
|
# If you want to use official mobile app,
|
||||||
|
# the notifications would be relayed via a Chatwoot server
|
||||||
|
ENABLE_PUSH_RELAY_SERVER=true
|
||||||
|
|
4
app.json
4
app.json
|
@ -28,6 +28,10 @@
|
||||||
"FRONTEND_URL": {
|
"FRONTEND_URL": {
|
||||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||||
"value": "https://CHANGE.herokuapp.com"
|
"value": "https://CHANGE.herokuapp.com"
|
||||||
|
},
|
||||||
|
"INSTALLATION_ENV": {
|
||||||
|
"description": "Installation method used for Chatwoot.",
|
||||||
|
"value": "heroku"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formation": {
|
"formation": {
|
||||||
|
|
|
@ -3,8 +3,9 @@ class ContactIdentifyAction
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
merge_if_existing_identified_contact
|
||||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
merge_if_existing_email_contact
|
||||||
|
merge_if_existing_phone_number_contact
|
||||||
update_contact
|
update_contact
|
||||||
end
|
end
|
||||||
@contact
|
@contact
|
||||||
|
@ -16,6 +17,18 @@ class ContactIdentifyAction
|
||||||
@account ||= @contact.account
|
@account ||= @contact.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_identified_contact
|
||||||
|
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_email_contact
|
||||||
|
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_if_existing_phone_number_contact
|
||||||
|
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
|
||||||
|
end
|
||||||
|
|
||||||
def existing_identified_contact
|
def existing_identified_contact
|
||||||
return if params[:identifier].blank?
|
return if params[:identifier].blank?
|
||||||
|
|
||||||
|
@ -28,6 +41,12 @@ class ContactIdentifyAction
|
||||||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def existing_phone_number_contact
|
||||||
|
return if params[:phone_number].blank?
|
||||||
|
|
||||||
|
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
|
||||||
|
end
|
||||||
|
|
||||||
def merge_contacts?(existing_contact, _contact)
|
def merge_contacts?(existing_contact, _contact)
|
||||||
existing_contact && existing_contact.id != @contact.id
|
existing_contact && existing_contact.id != @contact.id
|
||||||
end
|
end
|
||||||
|
@ -36,7 +55,9 @@ class ContactIdentifyAction
|
||||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||||
# blank identifier or email will throw unique index error
|
# blank identifier or email will throw unique index error
|
||||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||||
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||||
|
v.blank?
|
||||||
|
end.merge({ custom_attributes: custom_attributes }))
|
||||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||||
|
|
||||||
contacts = resolved_contacts.where(
|
contacts = resolved_contacts.where(
|
||||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||||
search: "%#{params[:q]}%"
|
search: "%#{params[:q]}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
|
@ -108,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_params
|
def contact_params
|
||||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_custom_attributes
|
def contact_custom_attributes
|
||||||
|
|
|
@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,22 @@
|
||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-light);
|
||||||
margin-top: var(--space-large);
|
margin-top: var(--space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-subscription--checkbox {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-right: var(--space-one);
|
||||||
|
margin-top: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-box {
|
.alert-box {
|
||||||
|
@ -20,17 +36,4 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-subscription--checkbox {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-right: var(--space-one);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
openLink() {
|
openLink() {
|
||||||
const win = window.open(this.url, '_blank', 'noopener');
|
const win = window.open(this.url, '_blank', 'noopener');
|
||||||
win.focus();
|
if (win) win.focus();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,9 +45,20 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
valueWithLink(attribute) {
|
valueWithLink(attribute) {
|
||||||
const messageFormatter = new MessageFormatter(attribute);
|
const parsedAttribute = this.parseAttributeToString(attribute);
|
||||||
|
const messageFormatter = new MessageFormatter(parsedAttribute);
|
||||||
return messageFormatter.formattedMessage;
|
return messageFormatter.formattedMessage;
|
||||||
},
|
},
|
||||||
|
parseAttributeToString(attribute) {
|
||||||
|
switch (typeof attribute) {
|
||||||
|
case 'string':
|
||||||
|
return attribute;
|
||||||
|
case 'object':
|
||||||
|
return JSON.stringify(attribute);
|
||||||
|
default:
|
||||||
|
return `${attribute}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -97,6 +97,15 @@ export const IFrameHelper = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFrameHeightToFitContent: (extraHeight, isFixedHeight) => {
|
||||||
|
const iframe = IFrameHelper.getAppFrame();
|
||||||
|
const updatedIframeHeight = isFixedHeight ? `${extraHeight}px` : '100%';
|
||||||
|
|
||||||
|
if (iframe)
|
||||||
|
iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`);
|
||||||
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
loaded: message => {
|
loaded: message => {
|
||||||
Cookies.set('cw_conversation', message.config.authToken, {
|
Cookies.set('cw_conversation', message.config.authToken, {
|
||||||
|
@ -169,6 +178,13 @@ export const IFrameHelper = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateIframeHeight: message => {
|
||||||
|
const { extraHeight = 0, isFixedHeight } = message;
|
||||||
|
if (!extraHeight) return;
|
||||||
|
|
||||||
|
IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight);
|
||||||
|
},
|
||||||
|
|
||||||
resetUnreadMode: () => {
|
resetUnreadMode: () => {
|
||||||
IFrameHelper.sendMessage('unset-unread-view');
|
IFrameHelper.sendMessage('unset-unread-view');
|
||||||
IFrameHelper.events.removeUnreadClass();
|
IFrameHelper.events.removeUnreadClass();
|
||||||
|
@ -178,22 +194,6 @@ export const IFrameHelper = {
|
||||||
const holderEl = document.querySelector('.woot-widget-holder');
|
const holderEl = document.querySelector('.woot-widget-holder');
|
||||||
removeClass(holderEl, 'has-unread-view');
|
removeClass(holderEl, 'has-unread-view');
|
||||||
},
|
},
|
||||||
|
|
||||||
updateIframeHeight: message => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const iframe = IFrameHelper.getAppFrame();
|
|
||||||
const scrollableMessageHeight =
|
|
||||||
iframe.contentWindow.document.querySelector('.unread-messages')
|
|
||||||
.scrollHeight + 40;
|
|
||||||
const updatedIframeHeight = message.isFixedHeight
|
|
||||||
? `${scrollableMessageHeight}px`
|
|
||||||
: '100%';
|
|
||||||
iframe.setAttribute(
|
|
||||||
'style',
|
|
||||||
`height: ${updatedIframeHeight} !important`
|
|
||||||
);
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
pushEvent: eventName => {
|
pushEvent: eventName => {
|
||||||
IFrameHelper.sendMessage('push-event', { eventName });
|
IFrameHelper.sendMessage('push-event', { eventName });
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
export const SDK_CSS = `.woot-widget-holder {
|
export const SDK_CSS = `.woot-widget-holder {
|
||||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
|
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translateY(0);
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
transition-duration: 0.5s, 0.5s;
|
transition: opacity 0.2s linear, transform 0.25s linear;
|
||||||
transition-property: opacity, bottom;
|
|
||||||
z-index: 2147483000 !important;
|
z-index: 2147483000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
||||||
}
|
}
|
||||||
|
|
||||||
.woot-widget-bubble img {
|
.woot-widget-bubble img {
|
||||||
|
all: revert;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
@ -110,7 +112,8 @@ export const SDK_CSS = `.woot-widget-holder {
|
||||||
}
|
}
|
||||||
|
|
||||||
.woot--hide {
|
.woot--hide {
|
||||||
bottom: -20000px !important;
|
bottom: -100vh;
|
||||||
|
transform: translateY(40px);
|
||||||
top: unset !important;
|
top: unset !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
|
|
|
@ -25,47 +25,42 @@ export default {
|
||||||
default: 'top',
|
default: 'top',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.focusItem();
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
focusItem() {
|
dropdownMenuButtons() {
|
||||||
this.$refs.dropdownMenu
|
return this.$refs.dropdownMenu.querySelectorAll(
|
||||||
.querySelector('ul.dropdown li.dropdown-menu__item .button')
|
'ul.dropdown li.dropdown-menu__item .button'
|
||||||
.focus();
|
);
|
||||||
|
},
|
||||||
|
activeElementIndex() {
|
||||||
|
const menuButtons = this.dropdownMenuButtons();
|
||||||
|
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||||
|
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||||
|
);
|
||||||
|
const activeIndex = [...menuButtons].indexOf(focusedButton);
|
||||||
|
return activeIndex;
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
if (hasPressedArrowUpKey(e)) {
|
const menuButtons = this.dropdownMenuButtons();
|
||||||
const items = this.$refs.dropdownMenu.querySelectorAll(
|
const lastElementIndex = menuButtons.length - 1;
|
||||||
'ul.dropdown li.dropdown-menu__item .button'
|
|
||||||
);
|
|
||||||
const focusItems = this.$refs.dropdownMenu.querySelector(
|
|
||||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
|
||||||
);
|
|
||||||
const activeElementIndex = [...items].indexOf(focusItems);
|
|
||||||
const lastElementIndex = items.length - 1;
|
|
||||||
|
|
||||||
if (activeElementIndex >= 1) {
|
if (menuButtons.length === 0) return;
|
||||||
items[activeElementIndex - 1].focus();
|
|
||||||
|
if (hasPressedArrowUpKey(e)) {
|
||||||
|
const activeIndex = this.activeElementIndex();
|
||||||
|
|
||||||
|
if (activeIndex >= 1) {
|
||||||
|
menuButtons[activeIndex - 1].focus();
|
||||||
} else {
|
} else {
|
||||||
items[lastElementIndex].focus();
|
menuButtons[lastElementIndex].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasPressedArrowDownKey(e)) {
|
if (hasPressedArrowDownKey(e)) {
|
||||||
const items = this.$refs.dropdownMenu.querySelectorAll(
|
const activeIndex = this.activeElementIndex();
|
||||||
'li.dropdown-menu__item .button'
|
|
||||||
);
|
|
||||||
const focusItems = this.$refs.dropdownMenu.querySelector(
|
|
||||||
'li.dropdown-menu__item .button:focus'
|
|
||||||
);
|
|
||||||
const activeElementIndex = [...items].indexOf(focusItems);
|
|
||||||
const lastElementIndex = items.length - 1;
|
|
||||||
|
|
||||||
if (activeElementIndex === lastElementIndex) {
|
if (activeIndex === lastElementIndex) {
|
||||||
items[0].focus();
|
menuButtons[0].focus();
|
||||||
} else {
|
} else {
|
||||||
items[activeElementIndex + 1].focus();
|
menuButtons[activeIndex + 1].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -59,6 +59,16 @@ export default {
|
||||||
activeCampaign() {
|
activeCampaign() {
|
||||||
this.setCampaignView();
|
this.setCampaignView();
|
||||||
},
|
},
|
||||||
|
showUnreadView(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.setIframeHeight(this.isMobile);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showCampaignView(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.setIframeHeight(this.isMobile);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const { websiteToken, locale } = window.chatwootWebChannel;
|
const { websiteToken, locale } = window.chatwootWebChannel;
|
||||||
|
@ -98,9 +108,13 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setIframeHeight(isFixedHeight) {
|
setIframeHeight(isFixedHeight) {
|
||||||
IFrameHelper.sendMessage({
|
this.$nextTick(() => {
|
||||||
event: 'updateIframeHeight',
|
const extraHeight = this.getExtraSpaceToscroll();
|
||||||
isFixedHeight,
|
IFrameHelper.sendMessage({
|
||||||
|
event: 'updateIframeHeight',
|
||||||
|
isFixedHeight,
|
||||||
|
extraHeight,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setLocale(locale) {
|
setLocale(locale) {
|
||||||
|
@ -256,6 +270,23 @@ export default {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getExtraSpaceToscroll: () => {
|
||||||
|
// This function calculates the extra space needed for the view to
|
||||||
|
// accomodate the height of close button + height of
|
||||||
|
// read messages button. So that scrollbar won't appear
|
||||||
|
const unreadMessageWrap = document.querySelector('.unread-messages');
|
||||||
|
const unreadCloseWrap = document.querySelector('.close-unread-wrap');
|
||||||
|
const readViewWrap = document.querySelector('.open-read-view-wrap');
|
||||||
|
|
||||||
|
if (!unreadMessageWrap) return 0;
|
||||||
|
|
||||||
|
// 24px to compensate the paddings
|
||||||
|
let extraHeight = 24 + unreadMessageWrap.scrollHeight;
|
||||||
|
if (unreadCloseWrap) extraHeight += unreadCloseWrap.scrollHeight;
|
||||||
|
if (readViewWrap) extraHeight += readViewWrap.scrollHeight;
|
||||||
|
|
||||||
|
return extraHeight;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const actions = {
|
||||||
name: userObject.name,
|
name: userObject.name,
|
||||||
avatar_url: userObject.avatar_url,
|
avatar_url: userObject.avatar_url,
|
||||||
identifier_hash: userObject.identifier_hash,
|
identifier_hash: userObject.identifier_hash,
|
||||||
|
phone_number: userObject.phone_number,
|
||||||
};
|
};
|
||||||
const {
|
const {
|
||||||
data: { pubsub_token: pubsubToken },
|
data: { pubsub_token: pubsubToken },
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="open-read-view-wrap">
|
||||||
<button
|
<button
|
||||||
v-if="unreadMessageCount"
|
v-if="unreadMessageCount"
|
||||||
class="button clear-button"
|
class="button clear-button"
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Notification::PushNotificationService
|
||||||
notification_subscriptions.each do |subscription|
|
notification_subscriptions.each do |subscription|
|
||||||
send_browser_push(subscription)
|
send_browser_push(subscription)
|
||||||
send_fcm_push(subscription)
|
send_fcm_push(subscription)
|
||||||
|
send_push_via_chatwoot_hub(subscription)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,6 +75,18 @@ class Notification::PushNotificationService
|
||||||
|
|
||||||
fcm = FCM.new(ENV['FCM_SERVER_KEY'])
|
fcm = FCM.new(ENV['FCM_SERVER_KEY'])
|
||||||
response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
|
response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
|
||||||
|
remove_subscription_if_error(subscription, response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_push_via_chatwoot_hub(subscription)
|
||||||
|
return if ENV['FCM_SERVER_KEY']
|
||||||
|
return unless ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
||||||
|
return unless subscription.fcm?
|
||||||
|
|
||||||
|
ChatwootHub.send_browser_push([subscription.subscription_attributes['push_token']], fcm_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_subscription_if_error(subscription, response)
|
||||||
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,9 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="update-subscription--checkbox">
|
<div class="update-subscription--checkbox">
|
||||||
<%= check_box_tag "subscribe_to_updates", 'true', true %>
|
<%= check_box_tag "subscribe_to_updates", 'true', true %>
|
||||||
<div for="subscribe_to_updates">
|
<label for="subscribe_to_updates">
|
||||||
Subscribe to release notes, newsletters & product feedback surveys.
|
Subscribe to release notes, newsletters & product feedback surveys.
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button nice large expanded">
|
<button type="submit" class="button nice large expanded">
|
||||||
Finish Setup
|
Finish Setup
|
||||||
|
|
|
@ -39,7 +39,8 @@ window.addEventListener('chatwoot:ready', function() {
|
||||||
window.$chatwoot.setUser('<%= user_id %>', {
|
window.$chatwoot.setUser('<%= user_id %>', {
|
||||||
identifier_hash: '<%= user_hash %>',
|
identifier_hash: '<%= user_hash %>',
|
||||||
email: 'jane@example.com',
|
email: 'jane@example.com',
|
||||||
name: 'Jane Doe'
|
name: 'Jane Doe',
|
||||||
|
phone_number: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
shared: &shared
|
shared: &shared
|
||||||
version: '1.18.1'
|
version: '1.19.0'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
|
|
|
@ -63,6 +63,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||||
|
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
||||||
|
|
||||||
RAILS_ENV=production bundle exec rake db:create
|
RAILS_ENV=production bundle exec rake db:create
|
||||||
RAILS_ENV=production bundle exec rake db:reset
|
RAILS_ENV=production bundle exec rake db:reset
|
||||||
|
|
|
@ -70,6 +70,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||||
|
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
||||||
|
|
||||||
RAILS_ENV=production bundle exec rake db:create
|
RAILS_ENV=production bundle exec rake db:create
|
||||||
RAILS_ENV=production bundle exec rake db:reset
|
RAILS_ENV=production bundle exec rake db:reset
|
||||||
|
|
|
@ -15,6 +15,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- RAILS_ENV=production
|
- RAILS_ENV=production
|
||||||
|
- INSTALLATION_ENV=docker
|
||||||
entrypoint: docker/entrypoints/rails.sh
|
entrypoint: docker/entrypoints/rails.sh
|
||||||
command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0']
|
command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0']
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- RAILS_ENV=production
|
- RAILS_ENV=production
|
||||||
|
- INSTALLATION_ENV=docker
|
||||||
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
|
|
|
@ -2,6 +2,7 @@ class ChatwootHub
|
||||||
BASE_URL = ENV['CHATWOOT_HUB_URL'] || 'https://hub.2.chatwoot.com'
|
BASE_URL = ENV['CHATWOOT_HUB_URL'] || 'https://hub.2.chatwoot.com'
|
||||||
PING_URL = "#{BASE_URL}/ping".freeze
|
PING_URL = "#{BASE_URL}/ping".freeze
|
||||||
REGISTRATION_URL = "#{BASE_URL}/instances".freeze
|
REGISTRATION_URL = "#{BASE_URL}/instances".freeze
|
||||||
|
PUSH_NOTIFICATION_URL = "#{BASE_URL}/send_push".freeze
|
||||||
EVENTS_URL = "#{BASE_URL}/events".freeze
|
EVENTS_URL = "#{BASE_URL}/events".freeze
|
||||||
|
|
||||||
def self.installation_identifier
|
def self.installation_identifier
|
||||||
|
@ -14,7 +15,8 @@ class ChatwootHub
|
||||||
{
|
{
|
||||||
installation_identifier: installation_identifier,
|
installation_identifier: installation_identifier,
|
||||||
installation_version: Chatwoot.config[:version],
|
installation_version: Chatwoot.config[:version],
|
||||||
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host
|
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host,
|
||||||
|
installation_env: ENV.fetch('INSTALLATION_ENV', '')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,7 +52,16 @@ class ChatwootHub
|
||||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
Rails.logger.info "Exception: #{e.message}"
|
Rails.logger.info "Exception: #{e.message}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Raven.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.send_browser_push(fcm_token_list, fcm_options)
|
||||||
|
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
|
||||||
|
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||||
|
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||||
|
Rails.logger.info "Exception: #{e.message}"
|
||||||
|
rescue StandardError => e
|
||||||
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.emit_event(event_name, event_data)
|
def self.emit_event(event_name, event_data)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
module ExceptionList
|
module ExceptionList
|
||||||
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
|
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
|
||||||
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
|
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
|
||||||
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout, SocketError].freeze
|
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout,
|
||||||
|
RestClient::MovedPermanently, SocketError].freeze
|
||||||
SMTP_EXCEPTIONS = [
|
SMTP_EXCEPTIONS = [
|
||||||
Net::SMTPSyntaxError
|
Net::SMTPSyntaxError
|
||||||
].freeze
|
].freeze
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@chatwoot/chatwoot",
|
"name": "@chatwoot/chatwoot",
|
||||||
"version": "1.18.1",
|
"version": "1.19.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint app/javascript --fix",
|
"eslint": "eslint app/javascript --fix",
|
||||||
|
|
|
@ -45,6 +45,17 @@ describe ::ContactIdentifyAction do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when contact with same phone_number exists' do
|
||||||
|
it 'merges the current contact to phone_number contact' do
|
||||||
|
existing_phone_number_contact = create(:contact, account: account, phone_number: '+919999888877')
|
||||||
|
params = { phone_number: '+919999888877' }
|
||||||
|
result = described_class.new(contact: contact, params: params).perform
|
||||||
|
expect(result.id).to eq existing_phone_number_contact.id
|
||||||
|
expect(result.name).to eq existing_phone_number_contact.name
|
||||||
|
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
|
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
|
||||||
it 'updates the attributes of contact passed in to identify action' do
|
it 'updates the attributes of contact passed in to identify action' do
|
||||||
create(:contact, account: account, identifier: '')
|
create(:contact, account: account, identifier: '')
|
||||||
|
|
|
@ -188,6 +188,19 @@ RSpec.describe 'Contacts API', type: :request do
|
||||||
expect(response.body).to include(contact2.email)
|
expect(response.body).to include(contact2.email)
|
||||||
expect(response.body).not_to include(contact1.email)
|
expect(response.body).not_to include(contact1.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'matches the contact respecting the identifier character casing' do
|
||||||
|
contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer')
|
||||||
|
contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier')
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
||||||
|
params: { q: 'TestIdentifier' },
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include(contact_special.identifier)
|
||||||
|
expect(response.body).not_to include(contact_normal.identifier)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -284,7 +297,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||||
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates the contact identifier when inbox id is passed' do
|
it 'creates the contact inbox when inbox id is passed' do
|
||||||
expect do
|
expect do
|
||||||
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
||||||
params: valid_params.merge({ inbox_id: inbox.id })
|
params: valid_params.merge({ inbox_id: inbox.id })
|
||||||
|
|
|
@ -20,17 +20,20 @@ describe Notification::PushNotificationService do
|
||||||
described_class.new(notification: notification).perform
|
described_class.new(notification: notification).perform
|
||||||
expect(Webpush).to have_received(:payload_send)
|
expect(Webpush).to have_received(:payload_send)
|
||||||
expect(FCM).not_to have_received(:new)
|
expect(FCM).not_to have_received(:new)
|
||||||
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
|
ENV['VAPID_PUBLIC_KEY'] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends a fcm notification for firebase subscription' do
|
it 'sends a fcm notification for firebase subscription' do
|
||||||
ENV['FCM_SERVER_KEY'] = 'test'
|
ENV['FCM_SERVER_KEY'] = 'test'
|
||||||
|
ENV['ENABLE_PUSH_RELAY_SERVER'] = 'false'
|
||||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||||
|
|
||||||
described_class.new(notification: notification).perform
|
described_class.new(notification: notification).perform
|
||||||
expect(FCM).to have_received(:new)
|
expect(FCM).to have_received(:new)
|
||||||
expect(Webpush).not_to have_received(:payload_send)
|
expect(Webpush).not_to have_received(:payload_send)
|
||||||
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
|
|
||||||
|
ENV['FCM_SERVER_KEY'] = nil
|
||||||
|
ENV['ENABLE_PUSH_RELAY_SERVER'] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,13 @@ properties:
|
||||||
required: true
|
required: true
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
description: name of the contact
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
description: email of the contact
|
||||||
phone_number:
|
phone_number:
|
||||||
type: string
|
type: string
|
||||||
|
description: phone number of the contact
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
description: A unique identifier for the contact in external system
|
||||||
|
|
|
@ -2,7 +2,13 @@ type: object
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
description: name of the contact
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
description: email of the contact
|
||||||
phone_number:
|
phone_number:
|
||||||
type: string
|
type: string
|
||||||
|
description: phone number of the contact
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
description: A unique identifier for the contact in external system
|
||||||
|
|
|
@ -8,6 +8,7 @@ get:
|
||||||
- name: q
|
- name: q
|
||||||
in: query
|
in: query
|
||||||
type: string
|
type: string
|
||||||
|
description: Search using contact `name`, `identifier`, `email` or `phone number`
|
||||||
- $ref: '#/parameters/contact_sort_param'
|
- $ref: '#/parameters/contact_sort_param'
|
||||||
- $ref: '#/parameters/page'
|
- $ref: '#/parameters/page'
|
||||||
responses:
|
responses:
|
||||||
|
|
|
@ -1281,7 +1281,8 @@
|
||||||
{
|
{
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Search using contact `name`, `identifier`, `email` or `phone number`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/parameters/contact_sort_param"
|
"$ref": "#/parameters/contact_sort_param"
|
||||||
|
@ -3376,13 +3377,20 @@
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "name of the contact"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "email of the contact"
|
||||||
},
|
},
|
||||||
"phone_number": {
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "phone number of the contact"
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A unique identifier for the contact in external system"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3390,13 +3398,20 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "name of the contact"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "email of the contact"
|
||||||
},
|
},
|
||||||
"phone_number": {
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "phone number of the contact"
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A unique identifier for the contact in external system"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue