Compare commits
3 commits
develop
...
feat/dyte-
Author | SHA1 | Date | |
---|---|---|---|
|
5ab4906212 | ||
|
14006e8f31 | ||
|
5fca522721 |
28 changed files with 638 additions and 103 deletions
|
@ -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
|
|
@ -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?
|
||||
|
||||
|
|
23
app/javascript/dashboard/api/integrations/dyte.js
Normal file
23
app/javascript/dashboard/api/integrations/dyte.js
Normal 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();
|
|
@ -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: () => {},
|
||||
|
|
|
@ -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>
|
|
@ -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 } = {},
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
97
app/javascript/widget/components/IntegrationCard.vue
Normal file
97
app/javascript/widget/components/IntegrationCard.vue
Normal 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>
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
56
lib/dyte.rb
Normal 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
|
50
lib/integrations/dyte/processor_service.rb
Normal file
50
lib/integrations/dyte/processor_service.rb
Normal 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
|
BIN
public/dashboard/images/integrations/dyte.png
Normal file
BIN
public/dashboard/images/integrations/dyte.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Loading…
Reference in a new issue