Merge branch 'develop' into feat/email-forwarding
This commit is contained in:
@@ -36,6 +36,7 @@ import DyteBubble from './bubbles/Dyte.vue';
|
||||
import LocationBubble from './bubbles/Location.vue';
|
||||
import CSATBubble from './bubbles/CSAT.vue';
|
||||
import FormBubble from './bubbles/Form.vue';
|
||||
import VoiceCallBubble from './bubbles/VoiceCall.vue';
|
||||
|
||||
import MessageError from './MessageError.vue';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||
@@ -130,6 +131,8 @@ const props = defineProps({
|
||||
sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties
|
||||
});
|
||||
|
||||
const emit = defineEmits(['retry']);
|
||||
|
||||
const contextMenuPosition = ref({});
|
||||
const showBackgroundHighlight = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
@@ -281,6 +284,10 @@ const componentToRender = computed(() => {
|
||||
return FormBubble;
|
||||
}
|
||||
|
||||
if (props.contentType === CONTENT_TYPES.VOICE_CALL) {
|
||||
return VoiceCallBubble;
|
||||
}
|
||||
|
||||
if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) {
|
||||
return EmailBubble;
|
||||
}
|
||||
@@ -393,7 +400,7 @@ function openContextMenu(e) {
|
||||
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
e.target?.tagName.toLowerCase() === 'a';
|
||||
['a', 'img'].includes(e.target?.tagName.toLowerCase());
|
||||
if (shouldSkipContextMenu || getSelection().toString()) {
|
||||
return;
|
||||
}
|
||||
@@ -543,6 +550,7 @@ provideMessageContext({
|
||||
class="[grid-area:meta]"
|
||||
:class="flexOrientationClass"
|
||||
:error="contentAttributes.externalError"
|
||||
@retry="emit('retry')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessageContext } from './provider.js';
|
||||
import { ORIENTATION } from './constants';
|
||||
import { hasOneDayPassed } from 'shared/helpers/timeHelper';
|
||||
import { ORIENTATION, MESSAGE_STATUS } from './constants';
|
||||
|
||||
defineProps({
|
||||
error: { type: String, required: true },
|
||||
});
|
||||
|
||||
const { orientation } = useMessageContext();
|
||||
const emit = defineEmits(['retry']);
|
||||
|
||||
const { orientation, status, createdAt } = useMessageContext();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const canRetry = computed(() => !hasOneDayPassed(createdAt.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,5 +41,14 @@ const { t } = useI18n();
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="canRetry"
|
||||
type="button"
|
||||
:disabled="status !== MESSAGE_STATUS.FAILED"
|
||||
class="bg-n-alpha-2 rounded-md size-5 grid place-content-center cursor-pointer"
|
||||
@click="emit('retry')"
|
||||
>
|
||||
<Icon icon="i-lucide-refresh-ccw" class="text-n-ruby-11 size-[14px]" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,6 +37,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['retry']);
|
||||
|
||||
const allMessages = computed(() => {
|
||||
return useCamelCase(props.messages, { deep: true });
|
||||
});
|
||||
@@ -128,6 +130,7 @@ const getForwardedMessageAddress = message => {
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:current-user-id="currentUserId"
|
||||
data-clarity-mask="True"
|
||||
@retry="emit('retry', message)"
|
||||
/>
|
||||
</template>
|
||||
<slot name="after" />
|
||||
|
||||
@@ -10,6 +10,10 @@ import { useI18n } from 'vue-i18n';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
|
||||
|
||||
const props = defineProps({
|
||||
hideMeta: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const { variant, orientation, inReplyTo, shouldGroupWithNext } =
|
||||
useMessageContext();
|
||||
const { t } = useI18n();
|
||||
@@ -64,6 +68,13 @@ const scrollToMessage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const shouldShowMeta = computed(
|
||||
() =>
|
||||
!props.hideMeta &&
|
||||
!shouldGroupWithNext.value &&
|
||||
variant.value !== MESSAGE_VARIANTS.ACTIVITY
|
||||
);
|
||||
|
||||
const replyToPreview = computed(() => {
|
||||
if (!inReplyTo) return '';
|
||||
|
||||
@@ -93,16 +104,16 @@ const replyToPreview = computed(() => {
|
||||
>
|
||||
<div
|
||||
v-if="inReplyTo"
|
||||
class="bg-n-alpha-black1 rounded-lg p-2 -mx-1 mb-2 cursor-pointer"
|
||||
class="p-2 -mx-1 mb-2 rounded-lg cursor-pointer bg-n-alpha-black1"
|
||||
@click="scrollToMessage"
|
||||
>
|
||||
<span class="line-clamp-2 break-all">
|
||||
<span class="break-all line-clamp-2">
|
||||
{{ replyToPreview }}
|
||||
</span>
|
||||
</div>
|
||||
<slot />
|
||||
<MessageMeta
|
||||
v-if="!shouldGroupWithNext && variant !== MESSAGE_VARIANTS.ACTIVITY"
|
||||
v-if="shouldShowMeta"
|
||||
:class="[
|
||||
flexOrientationClass,
|
||||
variant === MESSAGE_VARIANTS.EMAIL ? 'px-3 pb-3' : '',
|
||||
|
||||
@@ -74,7 +74,7 @@ const action = computed(() => ({
|
||||
|
||||
<style lang="scss">
|
||||
.join-call-button {
|
||||
margin: var(--space-small) 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.video-call--container {
|
||||
@@ -83,9 +83,9 @@ const action = computed(() => ({
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: var(--z-index-high);
|
||||
padding: var(--space-smaller);
|
||||
background: var(--b-800);
|
||||
z-index: 1000;
|
||||
padding: 0.25rem;
|
||||
@apply bg-n-background;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
@@ -95,7 +95,7 @@ const action = computed(() => ({
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: var(--space-smaller);
|
||||
top: 0.25rem;
|
||||
right: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, useTemplateRef, ref, onMounted, reactive } from 'vue';
|
||||
import { Letter } from 'vue-letter';
|
||||
import { sanitizeTextForRender } from '@chatwoot/utils';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
@@ -49,12 +50,12 @@ const { hasTranslations, translationContent } =
|
||||
const originalEmailText = computed(() => {
|
||||
const text =
|
||||
contentAttributes?.value?.email?.textContent?.full ?? content.value;
|
||||
return text?.replace(/\n/g, '<br>');
|
||||
return sanitizeTextForRender(text);
|
||||
});
|
||||
|
||||
const originalEmailHtml = computed(
|
||||
() =>
|
||||
contentAttributes?.value?.email?.htmlContent?.full ??
|
||||
contentAttributes?.value?.email?.htmlContent?.full ||
|
||||
originalEmailText.value
|
||||
);
|
||||
|
||||
@@ -149,7 +150,13 @@ defineExpose({
|
||||
>
|
||||
<div
|
||||
v-if="isExpandable && !isExpanded"
|
||||
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent"
|
||||
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end"
|
||||
:class="{
|
||||
'bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent':
|
||||
isIncoming,
|
||||
'bg-gradient-to-t from-n-solid-blue via-n-solid-blue via-20% to-transparent':
|
||||
isOutgoing,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { useVoiceCallStatus } from 'dashboard/composables/useVoiceCallStatus';
|
||||
|
||||
const { contentAttributes } = useMessageContext();
|
||||
|
||||
const data = computed(() => contentAttributes.value?.data);
|
||||
|
||||
const status = computed(() => data.value?.status);
|
||||
const direction = computed(() => data.value?.call_direction);
|
||||
|
||||
const { labelKey, subtextKey, bubbleIconBg, bubbleIconName } =
|
||||
useVoiceCallStatus(status, direction);
|
||||
|
||||
const containerRingClass = computed(() => {
|
||||
return status.value === 'ringing' ? 'ring-1 ring-emerald-300' : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="p-0 border-none" hide-meta>
|
||||
<div
|
||||
class="flex overflow-hidden flex-col w-full max-w-xs bg-white rounded-lg border border-slate-100 text-slate-900 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100"
|
||||
:class="containerRingClass"
|
||||
>
|
||||
<div class="flex gap-3 items-center p-3 w-full">
|
||||
<div
|
||||
class="flex justify-center items-center rounded-full size-10 shrink-0"
|
||||
:class="bubbleIconBg"
|
||||
>
|
||||
<span class="text-xl" :class="bubbleIconName" />
|
||||
</div>
|
||||
|
||||
<div class="flex overflow-hidden flex-col flex-grow">
|
||||
<span class="text-base font-medium truncate">{{ $t(labelKey) }}</span>
|
||||
<span class="text-xs text-slate-500">{{ $t(subtextKey) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -64,6 +64,7 @@ export const CONTENT_TYPES = {
|
||||
INPUT_CSAT: 'input_csat',
|
||||
INTEGRATIONS: 'integrations',
|
||||
STICKER: 'sticker',
|
||||
VOICE_CALL: 'voice_call',
|
||||
};
|
||||
|
||||
export const MEDIA_TYPES = [
|
||||
|
||||
Reference in New Issue
Block a user