feat: Add video call option with Dyte in the dashboard (#6207)

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Pranav Raj S
2023-01-09 11:49:27 -08:00
committed by GitHub
parent 0a65a233d7
commit 24cf7af30b
12 changed files with 358 additions and 50 deletions

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

@@ -0,0 +1,35 @@
import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
expect(DyteAPIClient).toBeInstanceOf(ApiClient);
expect(DyteAPIClient).toHaveProperty('createAMeeting');
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
});
describeWithAPIMock('createAMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting',
{
conversation_id: 1,
}
);
});
});
describeWithAPIMock('addParticipantToMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting',
{
message_id: 1,
}
);
});
});
});

View File

@@ -0,0 +1,57 @@
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="
$t('INTEGRATION_SETTINGS.DYTE.START_VIDEO_CALL_HELP_TEXT')
"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
@click="onClick"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: Number,
default: 0,
},
},
data() {
return { isLoading: false };
},
computed: {
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
isVideoIntegrationEnabled() {
return this.appIntegrations.find(
integration => 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) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally {
this.isLoading = false;
}
},
},
};
</script>

View File

@@ -87,6 +87,10 @@
:title="'Whatsapp Templates'" :title="'Whatsapp Templates'"
@click="$emit('selectWhatsappTemplate')" @click="$emit('selectWhatsappTemplate')"
/> />
<video-call-button
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
:conversation-id="conversationId"
/>
<transition name="modal-fade"> <transition name="modal-fade">
<div <div
v-show="$refs.upload && $refs.upload.dropActive" v-show="$refs.upload && $refs.upload.dropActive"
@@ -124,13 +128,13 @@ import {
ALLOWED_FILE_TYPES, ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP, ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages'; } from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton';
import { REPLY_EDITOR_MODES } from './constants'; import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
name: 'ReplyBottomPanel', name: 'ReplyBottomPanel',
components: { FileUpload }, components: { FileUpload, VideoCallButton },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin], mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: { props: {
mode: { mode: {
@@ -209,6 +213,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
conversationId: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@@ -269,7 +277,7 @@ export default {
} }
}, },
showMessageSignatureButton() { showMessageSignatureButton() {
return !this.isPrivate && this.isAnEmailChannel; return !this.isOnPrivateNote && this.isAnEmailChannel;
}, },
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; const { send_with_signature: isEnabled } = this.uiSettings;

View File

@@ -1,8 +1,5 @@
<template> <template>
<li <li v-if="shouldRenderMessage" :class="alignBubble">
v-if="hasAttachments || data.content || isEmailContentType"
:class="alignBubble"
>
<div :class="wrapClass"> <div :class="wrapClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass"> <div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head <bubble-mail-head
@@ -17,6 +14,11 @@
:is-email="isEmailContentType" :is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton" :display-quoted-button="displayQuotedButton"
/> />
<bubble-integration
:message-id="data.id"
:content-attributes="contentAttributes"
:inbox-id="data.inbox_id"
/>
<span <span
v-if="isPending && hasAttachments" v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent" class="chat-bubble has-attachment agent"
@@ -111,14 +113,14 @@
</template> </template>
<script> <script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions';
import BubbleFile from './bubble/File';
import BubbleImage from './bubble/Image';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location';
import BubbleMailHead from './bubble/MailHead'; import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text'; import BubbleText from './bubble/Text';
import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File';
import BubbleVideo from './bubble/Video.vue'; import BubbleVideo from './bubble/Video.vue';
import BubbleActions from './bubble/Actions';
import BubbleLocation from './bubble/Location';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
@@ -130,12 +132,13 @@ import { generateBotMessageContent } from './helpers/botMessageContentHelper';
export default { export default {
components: { components: {
BubbleActions, BubbleActions,
BubbleText,
BubbleImage,
BubbleFile, BubbleFile,
BubbleVideo, BubbleImage,
BubbleMailHead, BubbleIntegration,
BubbleLocation, BubbleLocation,
BubbleMailHead,
BubbleText,
BubbleVideo,
ContextMenu, ContextMenu,
Spinner, Spinner,
}, },
@@ -169,6 +172,14 @@ export default {
}; };
}, },
computed: { computed: {
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isAnIntegrationMessage
);
},
emailMessageContent() { emailMessageContent() {
const { const {
html_content: { full: fullHTMLContent } = {}, html_content: { full: fullHTMLContent } = {},
@@ -274,6 +285,9 @@ export default {
isTemplate() { isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE; return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
}, },
isAnIntegrationMessage() {
return this.contentType === 'integrations';
},
emailHeadAttributes() { emailHeadAttributes() {
return { return {
email: this.contentAttributes.email, email: this.contentAttributes.email,

View File

@@ -96,25 +96,26 @@
</p> </p>
</div> </div>
<reply-bottom-panel <reply-bottom-panel
:mode="replyType" :conversation-id="conversationId"
:inbox="inbox"
:send-button-text="replyButtonLabel"
:on-file-upload="onFileUpload"
:show-file-upload="showFileUpload"
:show-audio-recorder="showAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:show-emoji-picker="showEmojiPicker"
:on-send="onSendReply"
:is-send-disabled="isReplyButtonDisabled"
:recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState"
:is-recording-audio="isRecordingAudio"
:is-on-private-note="isOnPrivateNote"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:enable-multiple-file-upload="enableMultipleFileUpload" :enable-multiple-file-upload="enableMultipleFileUpload"
:has-whatsapp-templates="hasWhatsappTemplates" :has-whatsapp-templates="hasWhatsappTemplates"
:inbox="inbox"
:is-on-private-note="isOnPrivateNote"
:is-recording-audio="isRecordingAudio"
:is-send-disabled="isReplyButtonDisabled"
:mode="replyType"
:on-file-upload="onFileUpload"
:on-send="onSendReply"
:recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState"
:send-button-text="replyButtonLabel"
:show-audio-recorder="showAudioRecorder"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:show-emoji-picker="showEmojiPicker"
:show-file-upload="showFileUpload"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
@selectWhatsappTemplate="openWhatsappTemplateModal" @selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor" @toggle-editor="toggleRichContentEditor"
/> />

View File

@@ -0,0 +1,39 @@
<template>
<dyte-video-call
v-if="showDyteIntegration"
:message-id="messageId"
:meeting-data="contentAttributes.data"
/>
</template>
<script>
import DyteVideoCall from './integrations/Dyte.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
export default {
components: { DyteVideoCall },
mixins: [inboxMixin],
props: {
messageId: {
type: Number,
required: true,
},
contentAttributes: {
type: Object,
default: () => ({}),
},
inboxId: {
type: Number,
required: true,
},
},
computed: {
showDyteIntegration() {
const isEnabledOnTheInbox = this.isAPIInbox || this.isAWebWidgetInbox;
return isEnabledOnTheInbox && this.contentAttributes.type === 'dyte';
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
},
};
</script>

View File

@@ -0,0 +1,104 @@
<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">
<iframe
:src="meetingLink"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
class="join-call-button"
@click="leaveTheRoom"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
</woot-button>
</div>
</div>
</template>
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
const DYTE_MEETING_LINK = 'https://app.dyte.in/meeting/stage/';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
messageId: {
type: Number,
required: true,
},
meetingData: {
type: Object,
default: () => ({}),
},
},
data() {
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
},
computed: {
meetingLink() {
return `${DYTE_MEETING_LINK}${this.meetingData.room_name}?authToken=${this.dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`;
},
},
methods: {
async joinTheCall() {
this.isLoading = true;
try {
const {
data: { authResponse: { authToken } = {} } = {},
} = await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
},
leaveTheRoom() {
this.dyteAuthToken = '';
},
},
};
</script>
<style lang="scss">
.join-call-button {
margin: var(--space-small) 0;
}
.video-call--container {
position: fixed;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: var(--z-index-high);
padding: var(--space-smaller);
background: var(--b-800);
iframe {
width: 100%;
height: 100%;
border: 0;
}
button {
position: absolute;
top: var(--space-smaller);
right: 16rem;
}
}
</style>

View File

@@ -35,10 +35,7 @@
"LIST": { "LIST": {
"404": "There are no webhooks configured for this account.", "404": "There are no webhooks configured for this account.",
"TITLE": "Manage webhooks", "TITLE": "Manage webhooks",
"TABLE_HEADER": [ "TABLE_HEADER": ["Webhook endpoint", "Actions"]
"Webhook endpoint",
"Actions"
]
}, },
"EDIT": { "EDIT": {
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
@@ -76,6 +73,13 @@
"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>" "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",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"API": { "API": {
@@ -93,10 +97,7 @@
"LIST": { "LIST": {
"404": "There are no dashboard apps configured on this account yet", "404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...", "LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": [ "TABLE_HEADER": ["Name", "Endpoint"],
"Name",
"Endpoint"
],
"EDIT_TOOLTIP": "Edit app", "EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app" "DELETE_TOOLTIP": "Delete app"
}, },

View File

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

View File

@@ -5,28 +5,40 @@ describe('#getters', () => {
const state = { const state = {
records: [ records: [
{ {
id: 1, id: 'test1',
name: 'test1', name: 'test1',
logo: 'test', logo: 'test',
enabled: true, enabled: true,
}, },
{ {
id: 2, id: 'test2',
name: 'test2', name: 'test2',
logo: 'test', logo: 'test',
enabled: true, enabled: true,
}, },
{
id: 'dyte',
name: 'dyte',
logo: 'test',
enabled: true,
},
{
id: 'dialogflow',
name: 'dialogflow',
logo: 'test',
enabled: true,
},
], ],
}; };
expect(getters.getIntegrations(state)).toEqual([ expect(getters.getIntegrations(state)).toEqual([
{ {
id: 1, id: 'test1',
name: 'test1', name: 'test1',
logo: 'test', logo: 'test',
enabled: true, enabled: true,
}, },
{ {
id: 2, id: 'test2',
name: 'test2', name: 'test2',
logo: 'test', logo: 'test',
enabled: true, enabled: true,
@@ -38,11 +50,17 @@ describe('#getters', () => {
const state = { const state = {
records: [ records: [
{ {
id: 1, id: 'test1',
name: 'test1', name: 'test1',
logo: 'test', logo: 'test',
enabled: true, enabled: true,
}, },
{
id: 'dyte',
name: 'dyte',
logo: 'test',
enabled: true,
},
{ {
id: 'dialogflow', id: 'dialogflow',
name: 'test2', name: 'test2',
@@ -52,6 +70,12 @@ describe('#getters', () => {
], ],
}; };
expect(getters.getAppIntegrations(state)).toEqual([ expect(getters.getAppIntegrations(state)).toEqual([
{
id: 'dyte',
name: 'dyte',
logo: 'test',
enabled: true,
},
{ {
id: 'dialogflow', id: 'dialogflow',
name: 'test2', name: 'test2',

View File

@@ -145,6 +145,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", "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", "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-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", "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", "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", "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",