Compare commits

...

3 commits

Author SHA1 Message Date
Pranav Raj S
5ab4906212 fix rubocop 2022-12-21 10:01:51 -08:00
Pranav Raj S
14006e8f31 Dyte v2 2022-12-20 16:19:40 -08:00
Pranav Raj S
5fca522721 Dyte Integration V1 2022-12-17 20:55:41 -08:00
28 changed files with 638 additions and 103 deletions

View file

@ -0,0 +1,35 @@
class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:create_a_meeting]
before_action :fetch_message, only: [:add_participant_to_meeting]
def create_a_meeting
dyte_processor_service.create_a_meeting(Current.user)
head :ok
end
def add_participant_to_meeting
return render json: { error: 'Invalid Data' }, status: :unprocessable_entity if @message.content_type == 'integations'
response = dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user)
render json: response
end
private
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: Current.account, conversation: @conversation)
end
def permitted_params
params.permit(:conversation_id, :message_id)
end
def fetch_conversation
@conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id])
end
def fetch_message
@message = Current.account.messages.find(permitted_params[:message_id])
@conversation = @message.conversation
end
end

View file

@ -1,6 +1,6 @@
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
before_action :set_conversation, only: [:create]
before_action :set_message, only: [:update]
before_action :set_message, only: [:update, :add_participant_to_meeting]
def index
@messages = conversation.nil? ? [] : message_finder.perform
@ -26,8 +26,19 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
end
def add_participant_to_meeting
return render json: { error: 'Invalid Data' }, status: :unprocessable_entity if @message.content_type == 'integations'
response = dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], @message.conversation.contact)
render json: response
end
private
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: @web_widget.inbox.account, conversation: @message)
end
def build_attachment
return if params[:message][:attachments].blank?

View file

@ -0,0 +1,23 @@
/* global axios */
import ApiClient from '../ApiClient';
class DyteAPI extends ApiClient {
constructor() {
super('integrations/dyte', { accountScoped: true });
}
createAMeeting(conversationId) {
return axios.post(`${this.url}/create_a_meeting`, {
conversation_id: conversationId,
});
}
addParticipantToMeeting(messageId) {
return axios.post(`${this.url}/add_participant_to_meeting`, {
message_id: messageId,
});
}
}
export default new DyteAPI();

View file

@ -87,6 +87,7 @@
:title="'Whatsapp Templates'"
@click="$emit('selectWhatsappTemplate')"
/>
<video-call :conversation-id="conversationId" />
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
@ -124,13 +125,13 @@ import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages';
import VideoCall from './VideoCall.vue';
import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload },
components: { FileUpload, VideoCall },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
mode: {
@ -169,6 +170,10 @@ export default {
type: Boolean,
default: false,
},
conversationId: {
type: Number,
default: 0,
},
toggleEmojiPicker: {
type: Function,
default: () => {},

View file

@ -0,0 +1,57 @@
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="'Start a new video call with the customer'"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
:title="'Whatsapp Templates'"
@click="onClick"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import DyteAPI from '../../../api/integrations/dyte';
export default {
props: {
conversationId: {
type: Number,
default: 0,
},
},
data() {
return { isLoading: false };
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
isVideoIntegrationEnabled() {
return this.appIntegrations.find(integration => {
return integration.id === 'dyte' && !!integration.hooks.length;
});
},
},
mounted() {
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
},
methods: {
async onClick() {
this.isLoading = true;
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
// Ignore Error
} finally {
this.isLoading = false;
}
},
},
};
</script>

View file

@ -1,8 +1,5 @@
<template>
<li
v-if="hasAttachments || data.content || isEmailContentType"
:class="alignBubble"
>
<li v-if="shouldRenderMessage" :class="alignBubble">
<div :class="wrapClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head
@ -18,6 +15,10 @@
:readable-time="readableTime"
:display-quoted-button="displayQuotedButton"
/>
<integration-message
:message-id="data.id"
:content-attributes="contentAttributes"
/>
<span
v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent"
@ -136,7 +137,7 @@ import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import IntegrationMessage from './messageTypes/IntegrationMessage.vue';
export default {
components: {
BubbleActions,
@ -147,6 +148,7 @@ export default {
BubbleMailHead,
BubbleLocation,
ContextMenu,
IntegrationMessage,
Spinner,
},
mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
@ -183,6 +185,17 @@ export default {
};
},
computed: {
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isAnIntegrationMessage
);
},
isAnIntegrationMessage() {
return this.contentType === 'integrations';
},
emailMessageContent() {
const {
html_content: { full: fullHTMLContent } = {},

View file

@ -114,6 +114,7 @@
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:enable-multiple-file-upload="enableMultipleFileUpload"
:has-whatsapp-templates="hasWhatsappTemplates"
:conversation-id="conversationId"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
/>

View file

@ -0,0 +1,96 @@
<template>
<div>
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
icon="video-add"
class="join-call-button"
:is-loading="isLoading"
@click="joinTheCall"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN') }}
</woot-button>
<div v-if="dyteAuthToken" class="video-call--container" draggable>
<iframe
:src="
`https://app.dyte.in/meeting/stage/${meetingData.room_name}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`
"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
class="join-call-button"
:is-loading="isLoading"
@click="leaveTheRoom"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
</woot-button>
</div>
</div>
</template>
<script>
import DyteAPI from '../../../../api/integrations/dyte';
export default {
props: {
messageId: {
type: Number,
required: true,
},
meetingData: {
type: Object,
default: () => ({}),
},
},
data() {
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
},
mounted() {
window.addEventListener('message', this.receiveMessage, false);
},
beforeDestroy() {
window.removeEventListener('message', this.receiveMessage, false);
},
methods: {
async joinTheCall() {
const {
data: { success, data },
} = await DyteAPI.addParticipantToMeeting(this.messageId);
if (success) {
this.dyteAuthToken = data.authResponse.authToken;
}
},
leaveTheRoom() {
this.dyteAuthToken = '';
},
},
};
</script>
<style lang="scss">
.join-call-button {
margin: var(--space-small) 0;
}
.video-call--container {
position: fixed;
bottom: var(--space-normal);
right: 0;
z-index: 10000;
padding: var(--space-smaller);
background: var(--b-800);
border-radius: var(--border-radius-normal);
iframe {
width: 600px;
height: 600px;
border: 0;
}
button {
position: absolute;
top: var(--space-normal);
right: var(--space-normal);
}
}
</style>

View file

@ -0,0 +1,23 @@
<template>
<dyte-video-call
v-if="contentAttributes.type === 'dyte'"
:message-id="messageId"
:meeting-data="contentAttributes.data"
/>
</template>
<script>
import DyteVideoCall from './DyteVideoCall.vue';
export default {
components: { DyteVideoCall },
props: {
messageId: {
type: Number,
required: true,
},
contentAttributes: {
type: Object,
default: () => ({}),
},
},
};
</script>

View file

@ -35,10 +35,7 @@
"LIST": {
"404": "There are no webhooks configured for this account.",
"TITLE": "Manage webhooks",
"TABLE_HEADER": [
"Webhook endpoint",
"Actions"
]
"TABLE_HEADER": ["Webhook endpoint", "Actions"]
},
"EDIT": {
"BUTTON_TEXT": "Edit",
@ -76,6 +73,10 @@
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room"
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
@ -93,10 +94,7 @@
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": [
"Name",
"Endpoint"
],
"TABLE_HEADER": ["Name", "Endpoint"],
"EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app"
},

View file

@ -16,14 +16,16 @@ const state = {
},
};
const isAValidAppIntegration = integration => {
return ['fullcontact', 'dialogflow', 'dyte'].includes(integration.id);
};
export const getters = {
getIntegrations($state) {
return $state.records.filter(
item => item.id !== 'fullcontact' && item.id !== 'dialogflow'
);
return $state.records.filter(item => !isAValidAppIntegration(item));
},
getAppIntegrations($state) {
return $state.records.filter(item => item.id === 'dialogflow');
return $state.records.filter(item => isAValidAppIntegration(item));
},
getIntegration: $state => integrationId => {
const [integration] = $state.records.filter(

View file

@ -56,7 +56,8 @@ export const IFrameHelper = {
widgetUrl = `${widgetUrl}&cw_conversation=${cwCookie}`;
}
iframe.src = widgetUrl;
iframe.allow =
'camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;';
iframe.id = 'chatwoot_live_chat_widget';
iframe.style.visibility = 'hidden';

View file

@ -144,6 +144,7 @@
"tag-outline": "M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z",
"upload-outline": "M6.087 7.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 3.962 4.552 6.534 6.534 0 0 0-1.597-1.364A2.501 2.501 0 0 0 17.5 9.25h-.756a.75.75 0 0 1-.75-.713 4.25 4.25 0 0 0-8.489 0 .75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h4.4a6.458 6.458 0 0 0-.357 1.5H6a4 4 0 0 1 0-8h.087ZM22 16.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0Zm-6-1.793V19.5a.5.5 0 0 0 1 0v-4.793l1.646 1.647a.5.5 0 0 0 .708-.708l-2.5-2.5a.5.5 0 0 0-.708 0l-2.5 2.5a.5.5 0 0 0 .708.708L16 14.707Z",
"video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z",
"video-add-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-1.063c.154-.478.255-.98.294-1.5h.769a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6h-8.5A1.75 1.75 0 0 0 3.5 7.75v3.982A6.517 6.517 0 0 0 2 12.81V7.75A3.25 3.25 0 0 1 5.25 4.5h8.5Zm6.75 3.073L17 9.674v4.651l3.5 2.1V7.573ZM12 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0ZM7 18l.001 2.503a.5.5 0 1 1-1 0V18H3.496a.5.5 0 0 1 0-1H6v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H7Z",
"warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z",
"wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z",
"whatsapp-outline": "M19.05 4.91A9.816 9.816 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01zm-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.264 8.264 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.183 8.183 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23zm4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07c0 1.22.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28z",

View file

@ -12,6 +12,11 @@
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
"search-outline": "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-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z",
"video-add-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-1.063c.154-.478.255-.98.294-1.5h.769a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6h-8.5A1.75 1.75 0 0 0 3.5 7.75v3.982A6.517 6.517 0 0 0 2 12.81V7.75A3.25 3.25 0 0 1 5.25 4.5h8.5Zm6.75 3.073L17 9.674v4.651l3.5 2.1V7.573ZM12 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0ZM7 18l.001 2.503a.5.5 0 1 1-1 0V18H3.496a.5.5 0 0 1 0-1H6v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H7Z",
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z", "M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z", "M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
"sign-out-outline": [
"M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z",
"M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z",
"M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"
]
}

View file

@ -66,6 +66,10 @@ const updateMessage = id => ({
url: `/api/v1/widget/messages/${id}${window.location.search}`,
});
const addParticipantToMeeting = id => ({
url: `/api/v1/widget/messages/${id}/add_participant_to_meeting${window.location.search}`,
});
const getAvailableAgents = token => ({
url: '/api/v1/widget/inbox_members',
params: {
@ -102,4 +106,5 @@ export default {
getAvailableAgents,
getCampaigns,
triggerCampaign,
addParticipantToMeeting,
};

View file

@ -1,12 +1,17 @@
import authEndPoint from 'widget/api/endPoints';
import endPoints from 'widget/api/endPoints';
import { API } from 'widget/helpers/axios';
export default {
update: ({ messageId, email, values }) => {
const urlData = authEndPoint.updateMessage(messageId);
const urlData = endPoints.updateMessage(messageId);
return API.patch(urlData.url, {
contact: { email },
message: { submitted_values: values },
});
},
addParticipantToMeeting: messageId => {
const urlData = endPoints.addParticipantToMeeting(messageId);
return API.post(urlData.url);
},
};

View file

@ -17,7 +17,14 @@
:message-id="messageId"
:message-content-attributes="messageContentAttributes"
/>
<div v-if="isIntegrations">
<integration-card
:message-id="messageId"
:meeting-data="messageContentAttributes.data"
/>
</div>
</div>
<div v-if="isOptions">
<chat-options
:title="message"
@ -63,6 +70,7 @@ import ChatArticle from './template/Article';
import EmailInput from './template/EmailInput';
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import IntegrationCard from './IntegrationCard';
export default {
name: 'AgentMessageBubble',
@ -73,6 +81,7 @@ export default {
ChatOptions,
EmailInput,
CustomerSatisfaction,
IntegrationCard,
},
mixins: [messageFormatterMixin, darkModeMixin],
props: {
@ -107,6 +116,9 @@ export default {
isCSAT() {
return this.contentType === 'input_csat';
},
isIntegrations() {
return this.contentType === 'integrations';
},
},
methods: {
onResponse(messageResponse) {

View file

@ -0,0 +1,97 @@
<template>
<div>
<button
class="button icon-button join-call-button"
color-scheme="secondary"
:is-loading="isLoading"
@click="joinTheCall"
>
<fluent-icon icon="video-add" class="mr-2" />
{{ $t('INTEGRATIONS.DYTE.CLICK_HERE_TO_JOIN') }}
</button>
<div v-if="dyteAuthToken" class="video-call--container" draggable>
<iframe
:src="
`https://app.dyte.in/meeting/stage/${meetingData.room_name}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`
"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<button
class="button small join-call-button leave-room-button"
:is-loading="isLoading"
@click="leaveTheRoom"
>
{{ $t('INTEGRATIONS.DYTE.LEAVE_THE_ROOM') }}
</button>
</div>
</div>
</template>
<script>
import APIClient from 'widget/api/message';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
export default {
components: {
FluentIcon,
},
props: {
messageId: {
type: Number,
required: true,
},
meetingData: {
type: Object,
default: () => ({}),
},
},
data() {
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
},
methods: {
async joinTheCall() {
const {
data: { success, data },
} = await APIClient.addParticipantToMeeting(this.messageId);
if (success) {
this.dyteAuthToken = data.authResponse.authToken;
}
},
leaveTheRoom() {
this.dyteAuthToken = '';
},
},
};
</script>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
.video-call--container {
position: fixed;
top: 72px;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
iframe {
width: 100%;
height: calc(100% - 72px);
border: 0;
}
}
.join-call-button {
margin: $space-small 0;
border-radius: 4px;
display: flex;
align-items: center;
}
.leave-room-button {
position: absolute;
top: 0;
right: $space-small;
}
</style>

View file

@ -86,5 +86,11 @@
"BUTTON_TEXT": "Request a conversation transcript",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
"SEND_EMAIL_ERROR": "There was an error, please try again"
},
"INTEGRATIONS": {
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the call"
}
}
}

View file

@ -57,7 +57,8 @@ class Message < ApplicationRecord
form: 6,
article: 7,
incoming_email: 8,
input_csat: 9
input_csat: 9,
integrations: 10
}
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
# [:submitted_email, :items, :submitted_values] : Used for bot message types

View file

@ -30,50 +30,68 @@ dialogflow:
action: /dialogflow
hook_type: inbox
allow_multiple_hooks: true
settings_json_schema: {
"type": "object",
"properties": {
"project_id": { "type": "string" },
"credentials": { "type": "object" }
},
"required": ["project_id", "credentials"],
"additionalProperties": false
}
settings_form_schema: [
settings_json_schema:
{
"label": "Dialogflow Project ID",
"type": "text",
"name": "project_id",
"validation": "required",
"validationName": 'Project Id',
'type': 'object',
'properties':
{
'project_id': { 'type': 'string' },
'credentials': { 'type': 'object' },
},
'required': ['project_id', 'credentials'],
'additionalProperties': false,
}
settings_form_schema:
[
{
'label': 'Dialogflow Project ID',
'type': 'text',
'name': 'project_id',
'validation': 'required',
'validationName': 'Project Id',
},
{
"label": "Dialogflow Project Key File",
"type": "textarea",
"name": "credentials",
"validation": "required|JSON",
"validationName": 'Credentials',
"validation-messages": {
"JSON": "Invalid JSON",
"required": "Credentials is required"
}
}
'label': 'Dialogflow Project Key File',
'type': 'textarea',
'name': 'credentials',
'validation': 'required|JSON',
'validationName': 'Credentials',
'validation-messages':
{ 'JSON': 'Invalid JSON', 'required': 'Credentials is required' },
},
]
visible_properties: ['project_id']
fullcontact:
id: fullcontact
logo: fullcontact.png
i18n_key: fullcontact
action: /fullcontact
dyte:
id: dyte
logo: dyte.png
i18n_key: dyte
action: /dyte
hook_type: account
allow_multiple_hooks: false
settings_json_schema:
{
'type': 'object',
'properties': { 'api_key': { 'type': 'string' } },
'required': ['api_key'],
'properties':
{
'api_key': { 'type': 'string' },
'organization_id': { 'type': 'string' },
},
'required': ['api_key', 'organization_id'],
'additionalProperties': false,
}
settings_form_schema:
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
visible_properties: ['api_key']
[
{
'label': 'Organization ID',
'type': 'text',
'name': 'organization_id',
'validation': 'required',
},
{
'label': 'API Key',
'type': 'text',
'name': 'api_key',
'validation': 'required',
},
]
visible_properties: ['organization_id']

View file

@ -30,7 +30,7 @@
# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"
hello: 'Hello world'
messages:
reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
reset_password_failure: Uh ho! We could not find any user with the specified email.
@ -43,7 +43,7 @@ en:
signup:
disposable_email: We do not allow disposable emails
invalid_email: You have entered an invalid email
email_already_exists: "You have already signed up for an account with %{email}"
email_already_exists: 'You have already signed up for an account with %{email}'
failed: Signup failed
data_import:
data_type:
@ -105,66 +105,70 @@ en:
notifications:
notification_title:
conversation_creation: "[New conversation] - #%{display_id} has been created in %{inbox_name}"
conversation_assignment: "[Assigned to you] - #%{display_id} has been assigned to you"
assigned_conversation_new_message: "[New message] - #%{display_id} %{content}"
conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}"
conversation_creation: '[New conversation] - #%{display_id} has been created in %{inbox_name}'
conversation_assignment: '[Assigned to you] - #%{display_id} has been assigned to you'
assigned_conversation_new_message: '[New message] - #%{display_id} %{content}'
conversation_mention: 'You have been mentioned in conversation [ID - %{display_id}] by %{name}'
conversations:
messages:
instagram_story_content: "%{story_sender} mentioned you in the story: "
instagram_story_content: '%{story_sender} mentioned you in the story: '
instagram_deleted_story_content: This story is no longer available.
deleted: This message was deleted
activity:
status:
resolved: "Conversation was marked resolved by %{user_name}"
contact_resolved: "Conversation was resolved by %{contact_name}"
open: "Conversation was reopened by %{user_name}"
pending: "Conversation was marked as pending by %{user_name}"
snoozed: "Conversation was snoozed by %{user_name}"
auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity"
resolved: 'Conversation was marked resolved by %{user_name}'
contact_resolved: 'Conversation was resolved by %{contact_name}'
open: 'Conversation was reopened by %{user_name}'
pending: 'Conversation was marked as pending by %{user_name}'
snoozed: 'Conversation was snoozed by %{user_name}'
auto_resolved: 'Conversation was marked resolved by system due to %{duration} days of inactivity'
assignee:
self_assigned: "%{user_name} self-assigned this conversation"
assigned: "Assigned to %{assignee_name} by %{user_name}"
removed: "Conversation unassigned by %{user_name}"
self_assigned: '%{user_name} self-assigned this conversation'
assigned: 'Assigned to %{assignee_name} by %{user_name}'
removed: 'Conversation unassigned by %{user_name}'
team:
assigned: "Assigned to %{team_name} by %{user_name}"
assigned_with_assignee: "Assigned to %{assignee_name} via %{team_name} by %{user_name}"
removed: "Unassigned from %{team_name} by %{user_name}"
assigned: 'Assigned to %{team_name} by %{user_name}'
assigned_with_assignee: 'Assigned to %{assignee_name} via %{team_name} by %{user_name}'
removed: 'Unassigned from %{team_name} by %{user_name}'
labels:
added: "%{user_name} added %{labels}"
removed: "%{user_name} removed %{labels}"
muted: "%{user_name} has muted the conversation"
unmuted: "%{user_name} has unmuted the conversation"
added: '%{user_name} added %{labels}'
removed: '%{user_name} removed %{labels}'
muted: '%{user_name} has muted the conversation'
unmuted: '%{user_name} has unmuted the conversation'
templates:
greeting_message_body: "%{account_name} typically replies in a few hours."
ways_to_reach_you_message_body: "Give the team a way to reach you."
email_input_box_message_body: "Get notified by email"
csat_input_message_body: "Please rate the conversation"
greeting_message_body: '%{account_name} typically replies in a few hours.'
ways_to_reach_you_message_body: 'Give the team a way to reach you.'
email_input_box_message_body: 'Get notified by email'
csat_input_message_body: 'Please rate the conversation'
reply:
email:
header:
from_with_name: '%{assignee_name} from %{inbox_name} <%{from_email}>'
reply_with_name: '%{assignee_name} from %{inbox_name} <reply+%{reply_email}>'
email_subject: "New messages on this conversation"
transcript_subject: "Conversation Transcript"
email_subject: 'New messages on this conversation'
transcript_subject: 'Conversation Transcript'
survey:
response: "Please rate this conversation, %{link}"
response: 'Please rate this conversation, %{link}'
contacts:
online:
delete: "%{contact_name} is Online, please try again later"
delete: '%{contact_name} is Online, please try again later'
integration_apps:
dyte:
name: 'Dyte'
description: 'Dyte is tool that helps you to add live audio & video to your application with just a few lines of code. This integration allows you to give an option to your agents to have a video or voice call with your customers from without leaving Chatwoot.'
meeting_name: '%{agent_name} has started a meeting:'
slack:
name: "Slack"
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
name: 'Slack'
description: 'Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack.'
webhooks:
name: "Webhooks"
name: 'Webhooks'
description: "Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks."
dialogflow:
name: "Dialogflow"
description: "Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent."
name: 'Dialogflow'
description: 'Build chatbots using Dialogflow and connect them to your inbox quickly. Let the bots handle the queries before handing them off to a customer service agent.'
fullcontact:
name: "Fullcontact"
description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key."
name: 'Fullcontact'
description: 'FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key.'
public_portal:
search:
search_placeholder: Search for article by title or body...

View file

@ -158,6 +158,12 @@ Rails.application.routes.draw do
resources :apps, only: [:index, :show]
resources :hooks, only: [:create, :update, :destroy]
resource :slack, only: [:create, :update, :destroy], controller: 'slack'
resource :dyte, controller: 'dyte' do
collection do
post :create_a_meeting
post :add_participant_to_meeting
end
end
end
resources :working_hours, only: [:update]
@ -194,7 +200,11 @@ Rails.application.routes.draw do
resource :config, only: [:create]
resources :campaigns, only: [:index]
resources :events, only: [:create]
resources :messages, only: [:index, :create, :update]
resources :messages, only: [:index, :create, :update] do
member do
post :add_participant_to_meeting
end
end
resources :conversations, only: [:index, :create] do
collection do
post :update_last_seen

View file

@ -38,7 +38,7 @@ class CsmlEngine
def process_response(response)
return response.parsed_response if response.success?
{ error: response.parsed_response, status: response.code }
{ error: response.parsed_response, status: response.code }.to_h
end
def post(path, payload)

56
lib/dyte.rb Normal file
View file

@ -0,0 +1,56 @@
class Dyte
BASE_URL = 'https://api.cluster.dyte.in/v1'.freeze
API_KEY_HEADER = 'Authorization'.freeze
def initialize(api_key, organization_id)
@api_key = api_key
@organization_id = organization_id
end
def create_a_meeting(title)
payload = {
'title': title,
'presetName': 'AV_Participant',
'authorization': {
'waitingRoom': false,
'closed': false
},
'recordOnStart': false,
'liveStreamOnStart': false
}
path = "organizations/#{@organization_id}/meeting"
response = post(path, payload)
process_response(response)
end
def add_participant_to_meeting(meeting_id, client_id, name, avatar_url)
payload = {
'clientSpecificId': client_id.to_s,
'userDetails': {
'name': name,
'picture': avatar_url
},
'presetName': 'AV_Participant'
}
path = "organizations/#{@organization_id}/meetings/#{meeting_id}/participant"
response = post(path, payload)
process_response(response)
end
private
def process_response(response)
return response.parsed_response if response.success?
{ error: response.parsed_response, status: response.code }
end
def post(path, payload)
HTTParty.post(
"#{BASE_URL}/#{path}", {
headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' },
body: payload.to_json
}
)
end
end

View file

@ -0,0 +1,50 @@
class Integrations::Dyte::ProcessorService
pattr_initialize [:account!, :conversation!]
def create_a_meeting(agent)
title = I18n.t('integration_apps.dyte.meeting_name', agent_name: agent.available_name)
response = dyte_client.create_a_meeting(title)
return if response[:error].present?
meeting = response['data']['meeting']
@conversation.messages.create(
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :outgoing,
content_type: :integrations,
content: title,
content_attributes: {
type: 'dyte',
data: {
meeting_id: meeting['id'],
room_name: meeting['roomName']
}
},
sender: agent
}
)
end
def add_participant_to_meeting(meeting_id, user)
dyte_client.add_participant_to_meeting(meeting_id, user.id, user.name, avatar_url(user))
end
private
def avatar_url(user)
return user.avatar_url if user.avatar_url.present?
"#{ENV.fetch('FRONTEND_URL', nil)}/integrations/slack/user.png"
end
def dyte_hook
@dyte_hook ||= account.hooks.find_by!(app_id: 'dyte')
end
def dyte_client
credentials = dyte_hook.settings
@dyte_client ||= Dyte.new(credentials['api_key'], credentials['organization_id'])
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB