feat: Add the ability to receive contact(vCard) on a WhatsApp inbox (#6330)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Jacson Santos
2023-02-08 00:36:38 -03:00
committed by GitHub
parent bc96e5ed22
commit 73d14f204e
10 changed files with 296 additions and 80 deletions

View File

@@ -45,6 +45,11 @@
:longitude="attachment.coordinates_long"
:name="attachment.fallback_title"
/>
<bubble-contact
v-else-if="attachment.file_type === 'contact'"
:name="data.content"
:phone-number="attachment.fallback_title"
/>
<instagram-image-error-placeholder
v-else-if="hasImageError && hasInstagramStory"
/>
@@ -125,6 +130,7 @@ import BubbleLocation from './bubble/Location';
import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text';
import BubbleVideo from './bubble/Video.vue';
import BubbleContact from './bubble/Contact';
import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
@@ -143,6 +149,7 @@ export default {
BubbleMailHead,
BubbleText,
BubbleVideo,
BubbleContact,
ContextMenu,
Spinner,
instagramImageErrorPlaceholder,

View File

@@ -0,0 +1,115 @@
<template>
<div class="contact--group">
<fluent-icon icon="call" class="file--icon" size="18" />
<div class="meta">
<p class="text-truncate margin-bottom-0">
{{ phoneNumber }}
</p>
</div>
<div v-if="formattedPhoneNumber" class="link-wrap">
<woot-button variant="clear" size="small" @click.prevent="addContact">
{{ $t('CONVERSATION.SAVE_CONTACT') }}
</woot-button>
</div>
</div>
</template>
<script>
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
name: {
type: String,
default: '',
},
phoneNumber: {
type: String,
default: '',
},
},
computed: {
formattedPhoneNumber() {
return this.phoneNumber.replace(/\s|-|[A-Za-z]/g, '');
},
rawPhoneNumber() {
return this.phoneNumber.replace(/\D/g, '');
},
},
methods: {
async addContact() {
try {
let contact = await this.filterContactByNumber(this.rawPhoneNumber);
if (!contact) {
contact = await this.$store.dispatch(
'contacts/create',
this.getContactObject()
);
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
}
this.openContactNewTab(contact.id);
} catch (error) {
if (error instanceof DuplicateContactException) {
if (error.data.includes('phone_number')) {
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
}
} else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data);
} else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
getContactObject() {
const contactItem = {
name: this.name,
phone_number: `+${this.rawPhoneNumber}`,
};
return contactItem;
},
async filterContactByNumber(phoneNumber) {
const query = {
attribute_key: 'phone_number',
filter_operator: 'equal_to',
values: [phoneNumber],
attribute_model: 'standard',
custom_attribute_type: '',
};
const queryPayload = { payload: [query] };
const contacts = await this.$store.dispatch('contacts/filter', {
queryPayload,
resetState: false,
});
return contacts.shift();
},
openContactNewTab(contactId) {
const accountId = window.location.pathname.split('/')[3];
const url = `/app/accounts/${accountId}/contacts/${contactId}`;
window.open(url, '_blank');
},
},
};
</script>
<style lang="scss" scoped>
.contact--group {
align-items: center;
display: flex;
margin-top: var(--space-smaller);
.meta {
flex: 1;
margin-left: var(--space-small);
}
.link-wrap {
margin-left: var(--space-small);
}
}
</style>

View File

@@ -0,0 +1,28 @@
import ContactBubble from '../bubble/Contact.vue';
export default {
title: 'Components/Messaging/ContactBubble',
component: ContactBubble,
argTypes: {
name: {
defaultValue: 'Eden Hazard',
control: {
type: 'string',
},
},
phoneNumber: {
defaultValue: '+517554433220',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ContactBubble },
template: '<contact-bubble v-bind="$props" />',
});
export const ContactBubbleView = Template.bind({});

View File

@@ -35,6 +35,7 @@
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
"UNKNOWN_FILE_TYPE": "Unknown File",
"SAVE_CONTACT": "Save",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",

View File

@@ -35,6 +35,16 @@ const buildContactFormData = contactParams => {
return formData;
};
export const raiseContactCreateErrors = error => {
if (error.response?.status === 422) {
throw new DuplicateContactException(error.response.data.attributes);
} else if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else {
throw new Error(error);
}
};
export const actions = {
search: async ({ commit }, { search, page, sortAttr, label }) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
@@ -110,13 +120,10 @@ export const actions = {
AnalyticsHelper.track(CONTACTS_EVENTS.CREATE_CONTACT);
commit(types.SET_CONTACT_ITEM, response.data.payload.contact);
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
return response.data.payload.contact;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else {
throw new Error(error);
}
return raiseContactCreateErrors(error);
}
},
@@ -227,19 +234,26 @@ export const actions = {
}
},
filter: async ({ commit }, { page = 1, sortAttr, queryPayload } = {}) => {
filter: async (
{ commit },
{ page = 1, sortAttr, queryPayload, resetState = true } = {}
) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.filter(page, sortAttr, queryPayload);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
if (resetState) {
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
return payload;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
return [];
},
setContactFilters({ commit }, data) {