feat: add contact card
This commit is contained in:
@@ -21,10 +21,11 @@ import VideoBubble from './bubbles/Video.vue';
|
||||
import InstagramStoryBubble from './bubbles/InstagramStory.vue';
|
||||
import AttachmentsBubble from './bubbles/Attachments.vue';
|
||||
import EmailBubble from './bubbles/Email/Index.vue';
|
||||
import UnsupportedBubble from './bubbles/Unsupported.vue';
|
||||
import ContactBubble from './bubbles/Contact.vue';
|
||||
const LocationBubble = defineAsyncComponent(
|
||||
() => import('./bubbles/Location.vue')
|
||||
);
|
||||
import UnsupportedBubble from './bubbles/Unsupported.vue';
|
||||
|
||||
import MessageError from './MessageError.vue';
|
||||
import MessageMeta from './MessageMeta.vue';
|
||||
@@ -253,14 +254,19 @@ const componentToRender = computed(() => {
|
||||
return InstagramStoryBubble;
|
||||
}
|
||||
|
||||
if (props.attachments.length === 1 && !props.content) {
|
||||
if (props.attachments.length === 1) {
|
||||
const fileType = props.attachments[0].fileType;
|
||||
if (fileType === ATTACHMENT_TYPES.IMAGE) return ImageBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.FILE) return FileBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.AUDIO) return AudioBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.VIDEO) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.IG_REEL) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.LOCATION) return LocationBubble;
|
||||
|
||||
if (!props.content) {
|
||||
if (fileType === ATTACHMENT_TYPES.IMAGE) return ImageBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.FILE) return FileBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.AUDIO) return AudioBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.VIDEO) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.IG_REEL) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.LOCATION) return LocationBubble;
|
||||
}
|
||||
// Attachment content is the name of the contact
|
||||
if (fileType === ATTACHMENT_TYPES.CONTACT) return ContactBubble;
|
||||
}
|
||||
|
||||
if (props.attachments.length > 1 && !props.content) {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BaseBubble from './Base.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import Button from 'next/button/Button.vue';
|
||||
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Attachment
|
||||
* @property {number} id - Unique identifier for the attachment
|
||||
* @property {number} messageId - ID of the associated message
|
||||
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
|
||||
* @property {number} accountId - ID of the associated account
|
||||
* @property {string|null} extension - File extension
|
||||
* @property {string} dataUrl - URL to access the full attachment data
|
||||
* @property {string} thumbUrl - URL to access the thumbnail version
|
||||
* @property {number} fileSize - Size of the file in bytes
|
||||
* @property {number|null} width - Width of the image if applicable
|
||||
* @property {number|null} height - Height of the image if applicable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const $store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const attachment = computed(() => {
|
||||
return props.attachments[0];
|
||||
});
|
||||
|
||||
const phoneNumber = computed(() => {
|
||||
return attachment.value.fallbackTitle;
|
||||
});
|
||||
|
||||
const formattedPhoneNumber = computed(() => {
|
||||
return phoneNumber.value.replace(/\s|-|[A-Za-z]/g, '');
|
||||
});
|
||||
|
||||
const rawPhoneNumber = computed(() => {
|
||||
return phoneNumber.value.replace(/\D/g, '');
|
||||
});
|
||||
|
||||
const name = computed(() => {
|
||||
return props.content;
|
||||
});
|
||||
|
||||
function getContactObject() {
|
||||
const contactItem = {
|
||||
name: name.value,
|
||||
phone_number: `+${rawPhoneNumber.value}`,
|
||||
};
|
||||
return contactItem;
|
||||
}
|
||||
|
||||
async function filterContactByNumber(searchCandidate) {
|
||||
const query = {
|
||||
attribute_key: 'phone_number',
|
||||
filter_operator: 'equal_to',
|
||||
values: [searchCandidate],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
|
||||
const queryPayload = { payload: [query] };
|
||||
const contacts = await $store.dispatch('contacts/filter', {
|
||||
queryPayload,
|
||||
resetState: false,
|
||||
});
|
||||
return contacts.shift();
|
||||
}
|
||||
|
||||
function openContactNewTab(contactId) {
|
||||
const accountId = window.location.pathname.split('/')[3];
|
||||
const url = `/app/accounts/${accountId}/contacts/${contactId}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
async function addContact() {
|
||||
try {
|
||||
let contact = await filterContactByNumber(rawPhoneNumber);
|
||||
if (!contact) {
|
||||
contact = await $store.dispatch('contacts/create', getContactObject());
|
||||
useAlert(t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
}
|
||||
openContactNewTab(contact.id);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('phone_number')) {
|
||||
useAlert(t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="p-3 min-w-64">
|
||||
<div class="grid gap-0.5 mb-2">
|
||||
<span class="truncate">{{ name }}</span>
|
||||
<span class="truncate text-sm text-n-slate-11">
|
||||
{{ phoneNumber }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="formattedPhoneNumber" class="link-wrap">
|
||||
<Button slate solid sm class="w-full" @click.prevent="addContact">
|
||||
{{ t('CONVERSATION.SAVE_CONTACT') }}
|
||||
</Button>
|
||||
</div>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -73,6 +73,49 @@ export default camelcaseKeys(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5296,
|
||||
content: 'Shivam Mishra',
|
||||
inbox_id: 486,
|
||||
conversation_id: 1016,
|
||||
message_type: 0,
|
||||
content_type: 'text',
|
||||
status: 'sent',
|
||||
content_attributes: {
|
||||
in_reply_to: null,
|
||||
},
|
||||
created_at: 1732710571,
|
||||
private: false,
|
||||
source_id: null,
|
||||
sender: {
|
||||
additional_attributes: {
|
||||
created_at_ip: '::1',
|
||||
},
|
||||
custom_attributes: {},
|
||||
email: 'anthony@example.com',
|
||||
id: 599,
|
||||
identifier: null,
|
||||
name: 'anthony',
|
||||
phone_number: null,
|
||||
thumbnail: '',
|
||||
type: 'contact',
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
id: 378,
|
||||
message_id: 5314,
|
||||
file_type: 'contact',
|
||||
account_id: 2,
|
||||
coordinates_lat: 37.7937545,
|
||||
coordinates_long: -122.3997472,
|
||||
fallback_title: '+919999999999',
|
||||
extension: null,
|
||||
file_size: 287949,
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5297,
|
||||
content: 'Give the team a way to reach you.',
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||
"SAVE_CONTACT": "Save",
|
||||
"SAVE_CONTACT": "Save Contact",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
|
||||
|
||||
Reference in New Issue
Block a user