chore: Remove older UI (#11720)

This commit is contained in:
Sivin Varghese
2025-07-01 09:43:44 +05:30
committed by GitHub
parent 58da92a252
commit 24ea968b00
369 changed files with 974 additions and 9363 deletions

View File

@@ -19,10 +19,10 @@ const onClick = () => {
/>
<div
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500"
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-n-brand"
/>
<div
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500 opacity-50"
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-n-brand opacity-50"
/>
</div>
</template>

View File

@@ -70,11 +70,11 @@ export default {
@submit.prevent="applyText"
>
<div v-if="draftMessage" class="w-full">
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
<h4 class="mt-1 text-base text-n-slate-12">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }}
</h4>
<p v-dompurify-html="formatMessage(draftMessage, false)" />
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
<h4 class="mt-1 text-base text-n-slate-12">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE')
}}

View File

@@ -22,23 +22,15 @@
gap: 4px;
.ai-typing--icon {
color: var(--v-500);
@apply text-n-iris-11;
}
}
label {
display: inline-block;
margin-right: var(--space-smaller);
color: var(--v-400);
@apply text-n-iris-11 ltr:mr-1 rtl:ml-1 inline-block;
}
.loader {
display: inline-block;
width: 6px;
height: 6px;
margin-right: var(--space-smaller);
margin-top: var(--space-slab);
background-color: var(--v-300);
border-radius: 50%;
animation: bubble-scale 1.2s infinite;
@apply bg-n-iris-11 inline-block size-1.5 ltr:mr-1 rtl:ml-1 mt-3 rounded-full;
}
.loader:nth-child(2) {

View File

@@ -50,7 +50,7 @@ const fileName = file => {
<div
v-for="(attachment, index) in nonRecordedAudioAttachments"
:key="attachment.id"
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
class="flex items-center p-1 bg-n-slate-3 gap-1 rounded-md w-[15rem] mb-1"
>
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
<img

View File

@@ -266,11 +266,11 @@ export default {
@apply flex items-center justify-center relative my-2.5 mx-0;
.operator__line {
@apply absolute w-full border-b border-solid border-slate-75 dark:border-slate-600;
@apply absolute w-full border-b border-solid border-n-weak;
}
.operator__select {
margin-bottom: var(--space-zero) !important;
margin-bottom: 0 !important;
@apply relative w-auto;
}
}

View File

@@ -56,9 +56,9 @@ export default {
<style scoped>
.multiselect {
margin: var(--space-smaller) var(--space-zero);
margin: 0.25rem 0;
}
textarea {
margin-bottom: var(--space-zero);
margin-bottom: 0;
}
</style>

View File

@@ -79,13 +79,13 @@ input[type='file'] {
@apply hidden;
}
.input-wrapper {
@apply flex h-9 bg-white dark:bg-slate-900 py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-woot-100 dark:border-woot-500;
@apply flex h-9 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-n-strong;
}
.success-icon {
@apply text-green-500 dark:text-green-600 mr-2;
@apply text-n-teal-9 mr-2;
}
.error-icon {
@apply text-red-500 dark:text-red-600 mr-2;
@apply text-n-ruby-9 mr-2;
}
.processing {

View File

@@ -50,6 +50,6 @@ export default {
}
}
.avatar-container {
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-woot-600 dark:text-woot-200;
@apply flex leading-[100%] font-medium items-center justify-center text-center cursor-default avatar-color dark:dark-avatar-color text-n-blue-text;
}
</style>

View File

@@ -48,26 +48,17 @@ useKeyboardEvents(keyboardEvents);
<template>
<woot-tabs
:index="activeTabIndex"
class="w-full px-3 -mt-1 py-0 tab--chat-type"
class="w-full px-3 -mt-1 py-0 [&_ul]:p-0"
@change="onTabChange"
>
<woot-tabs-item
v-for="(item, index) in items"
:key="item.key"
class="text-sm"
class="text-sm [&_a]:font-medium"
:index="index"
:name="item.name"
:count="item.count"
is-compact
/>
</woot-tabs>
</template>
<style scoped lang="scss">
.tab--chat-type {
::v-deep {
.tabs {
@apply p-0;
}
}
}
</style>

View File

@@ -52,19 +52,16 @@ export default {
</template>
<style scoped lang="scss">
@import 'dashboard/assets/scss/variables';
@import 'dashboard/assets/scss/mixins';
.colorpicker {
position: relative;
}
.colorpicker--selected {
@apply border border-solid border-slate-50 dark:border-slate-600 rounded cursor-pointer h-8 w-8 mb-4;
@apply border border-solid border-n-weak rounded cursor-pointer h-8 w-8 mb-4;
}
.colorpicker--chrome.vc-chrome {
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-slate-75 dark:border-slate-600 rounded;
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-n-weak rounded;
::v-deep {
input {

View File

@@ -17,7 +17,7 @@ export default {
</h3>
<p
v-if="message"
class="block text-center text-n-slate-11 dark:text-slate-400 my-4 mx-auto w-[90%]"
class="block text-center text-n-slate-11 my-4 mx-auto w-[90%]"
>
{{ message }}
</p>

View File

@@ -264,12 +264,10 @@ export default {
v-if="showQueryOperator"
class="flex items-center justify-center relative my-2.5 mx-0"
>
<hr
class="absolute w-full border-b border-solid border-slate-75 dark:border-slate-800"
/>
<hr class="absolute w-full border-b border-solid border-n-weak" />
<select
v-model="query_operator"
class="relative w-auto mb-0 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
class="relative w-auto mb-0 bg-n-background text-n-slate-12 border-n-weak"
>
<option value="and">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
@@ -285,7 +283,7 @@ export default {
<style lang="scss" scoped>
.filter__answer--wrap {
input {
@apply bg-white dark:bg-slate-900 mb-0 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600;
@apply bg-n-background mb-0 text-n-slate-12 border-n-weak;
}
}

View File

@@ -1,20 +1,20 @@
<script>
export default {
props: {
message: { type: String, default: '' },
},
};
<script setup>
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
defineProps({
message: { type: String, default: '' },
});
</script>
<template>
<div class="flex items-center justify-center p-8">
<h6
class="flex items-center gap-2 text-base text-center w-100 text-slate-800 dark:text-slate-300"
class="flex items-center gap-3 text-base text-center w-100 text-n-slate-11"
>
<span class="text-base font-medium text-slate-800 dark:text-slate-100">
<span class="text-base font-medium text-n-slate-12">
{{ message }}
</span>
<span class="spinner" />
<Spinner class="text-n-brand" />
</h6>
</div>
</template>

View File

@@ -16,13 +16,10 @@ export default {
<template>
<div class="border-b border-solid border-n-weak/60">
<div class="max-w-6xl w-full mx-auto pt-4 pb-0 px-8">
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
<h2 class="text-2xl text-n-slate-12 mb-1 font-medium">
{{ headerTitle }}
</h2>
<p
v-if="headerContent"
class="w-full text-slate-600 dark:text-slate-300 text-sm mb-2"
>
<p v-if="headerContent" class="w-full text-n-slate-11 text-sm mb-2">
{{ headerContent }}
</p>
<slot />

View File

@@ -37,7 +37,7 @@ const toggleShowMore = () => {
{{ textToBeDisplayed }}
<button
v-if="text.length > limit"
class="text-woot-500 !p-0 !border-0 align-top"
class="text-n-brand !p-0 !border-0 align-top"
@click="toggleShowMore"
>
{{ buttonLabel }}

View File

@@ -16,7 +16,7 @@ defineProps({
</script>
<template>
<span class="text-sm text-slate-700 dark:text-slate-200 font-medium">
<span class="text-sm text-n-slate-11 font-medium">
{{
$t('GENERAL.SHOWING_RESULTS', {
firstIndex,

View File

@@ -93,7 +93,7 @@ export default {
},
thumbnailClass() {
const className = this.hasBorder
? 'border border-solid border-white dark:border-slate-700/50'
? 'border border-solid border-white dark:border-n-weak'
: '';
const variant =
this.variant === 'circle' ? 'thumbnail-rounded' : 'thumbnail-square';
@@ -183,7 +183,7 @@ export default {
.user-thumbnail {
border-radius: 50%;
&.thumbnail-square {
border-radius: var(--border-radius-large);
border-radius: 0.5625rem;
}
height: 100%;
width: 100%;
@@ -193,20 +193,16 @@ export default {
}
.source-badge {
border-radius: var(--border-radius-small);
bottom: var(--space-minus-micro);
box-shadow: var(--shadow-small);
height: var(--space-slab);
padding: var(--space-micro);
border-radius: 0.1875rem;
bottom: -0.125rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
position: absolute;
right: 0;
width: var(--space-slab);
@apply bg-white dark:bg-slate-900;
@apply bg-n-background p-0.5 size-3;
}
.user-online-status {
border-radius: 50%;
bottom: var(--space-micro);
@apply bottom-0.5 rounded-full;
&:after {
content: ' ';
@@ -214,15 +210,15 @@ export default {
}
.user-online-status--online {
@apply bg-green-400 dark:bg-green-400;
@apply bg-n-teal-10;
}
.user-online-status--busy {
@apply bg-yellow-500 dark:bg-yellow-500;
@apply bg-n-amber-10;
}
.user-online-status--offline {
@apply bg-slate-500 dark:bg-slate-500;
@apply bg-n-slate-10;
}
}
</style>

View File

@@ -60,31 +60,19 @@ export default {
.overlapping-thumbnail {
position: relative;
box-shadow: var(--shadow-small);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
&:not(:first-child) {
margin-left: var(--space-minus-smaller);
margin-left: -0.25rem;
}
.gap-tight {
margin-left: var(--space-minus-small);
margin-left: -0.5rem;
}
}
.thumbnail-more-text {
display: inline-flex;
align-items: center;
position: relative;
margin-left: var(--space-minus-small);
padding: 0 var(--space-small);
box-shadow: var(--shadow-small);
background: var(--color-background);
border-radius: var(--space-giga);
border: 1px solid var(--white);
color: var(--s-600);
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
@apply text-n-slate-11 bg-n-slate-4 border border-n-weak text-xs font-medium rounded-full px-2 ltr:-ml-2 rtl:-mr-2 inline-flex items-center relative;
}
</style>

View File

@@ -709,7 +709,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
<div ref="editor" />
<div
v-show="isImageNodeSelected && showImageResizeToolbar"
class="absolute shadow-md rounded-[4px] flex gap-1 py-1 px-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50"
class="absolute shadow-md rounded-[6px] flex gap-1 py-1 px-1 bg-n-solid-3 outline outline-1 outline-n-weak text-n-slate-12"
:style="{
top: toolbarPosition.top,
left: toolbarPosition.left,
@@ -718,7 +718,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
<button
v-for="size in sizes"
:key="size.name"
class="text-xs font-medium rounded-[4px] border border-solid border-slate-200 dark:border-slate-600 px-1.5 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-800"
class="text-xs font-medium rounded-[4px] outline outline-1 outline-n-strong px-1.5 py-0.5 hover:bg-n-slate-5"
@click="setURLWithQueryAndImageSize(size)"
>
{{ size.name }}
@@ -735,16 +735,16 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
@apply flex flex-col;
.ProseMirror-menubar {
min-height: var(--space-two) !important;
min-height: 1.25rem !important;
@apply -ml-2.5 pb-0 bg-transparent text-n-slate-11;
.ProseMirror-menu-active {
@apply bg-slate-75 dark:bg-slate-800;
@apply bg-n-slate-5 dark:bg-n-solid-3;
}
}
> .ProseMirror {
@apply p-0 break-words text-slate-800 dark:text-slate-100;
@apply p-0 break-words text-n-slate-12;
h1,
h2,
@@ -753,14 +753,14 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
h5,
h6,
p {
@apply text-slate-800 dark:text-slate-100;
@apply text-n-slate-12;
}
blockquote {
@apply border-slate-400 dark:border-slate-500;
@apply border-n-slate-7;
p {
@apply text-slate-600 dark:text-slate-400;
@apply text-n-slate-11;
}
}
@@ -825,6 +825,6 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
}
.editor-warning__message {
@apply text-red-400 dark:text-red-400 font-normal text-sm pt-1 pb-0 px-0;
@apply text-n-ruby-9 dark:text-n-ruby-9 font-normal text-sm pt-1 pb-0 px-0;
}
</style>

View File

@@ -355,10 +355,10 @@ export default {
<transition name="modal-fade">
<div
v-show="uploadRef && uploadRef.dropActive"
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-n-slate-12 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
>
<fluent-icon icon="cloud-backup" size="40" />
<h4 class="text-2xl break-words text-slate-900 dark:text-slate-50">
<h4 class="text-2xl break-words text-n-slate-12">
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
</h4>
</div>
@@ -402,7 +402,7 @@ export default {
}
&:hover button {
@apply dark:bg-slate-800 bg-slate-100;
@apply enabled:bg-n-slate-9/20;
}
}
</style>

View File

@@ -73,7 +73,7 @@ export default {
};
},
charLengthClass() {
return this.charactersRemaining < 0 ? 'text-red-600' : 'text-slate-600';
return this.charactersRemaining < 0 ? 'text-n-ruby-9' : 'text-n-slate-11';
},
characterLengthWarning() {
return this.charactersRemaining < 0

View File

@@ -52,13 +52,13 @@ onMounted(() => {
>
<template #default="{ item, selected }">
<span
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 truncate"
class="max-w-full inline-flex items-center gap-0.5 min-w-0 mb-0 text-sm font-medium text-n-slate-12 group-hover:text-n-brand truncate"
>
{{ item.emoji }}
<p
class="relative mb-0 truncate bottom-px"
:class="{
'text-woot-500 dark:text-woot-500': selected,
'text-n-brand': selected,
'font-normal': !selected,
}"
>

View File

@@ -1,27 +0,0 @@
<script>
export default {
props: {
status: { type: String, default: '' },
},
};
</script>
<template>
<div
:class="`status-badge status-badge__${status} rounded-full w-2.5 h-2.5 mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-flex`"
/>
</template>
<style lang="scss" scoped>
.status-badge {
&__online {
@apply bg-green-400;
}
&__offline {
@apply bg-slate-500;
}
&__busy {
@apply bg-yellow-500;
}
}
</style>

View File

@@ -1 +0,0 @@
<!-- // not using this component -->

View File

@@ -91,7 +91,7 @@ export default {
<template>
<div
class="conversation-details-wrap bg-n-background relative"
class="conversation-details-wrap flex flex-col min-w-0 w-full bg-n-background relative"
:class="{
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
}"
@@ -104,7 +104,7 @@ export default {
<woot-tabs
v-if="dashboardApps.length && currentChat.id"
:index="activeIndex"
class="-mt-px dashboard-app--tabs border-t border-t-n-background"
class="-mt-px border-t border-t-n-background"
@change="onDashboardAppTabChange"
>
<woot-tabs-item
@@ -113,6 +113,8 @@ export default {
:index="tab.index"
:name="tab.name"
:show-badge="false"
is-compact
class="[&_a]:pt-1"
/>
</woot-tabs>
<div v-show="!activeIndex" class="flex h-full min-h-0 m-0">
@@ -138,19 +140,3 @@ export default {
/>
</div>
</template>
<style lang="scss" scoped>
.conversation-details-wrap {
@apply flex flex-col min-w-0 w-full;
}
.dashboard-app--tabs {
::v-deep {
.tabs-title {
a {
@apply pb-2 pt-1;
}
}
}
}
</style>

View File

@@ -328,9 +328,7 @@ export default {
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
</span>
</p>
<div
class="absolute flex flex-col conversation--meta ltr:right-4 rtl:left-4 top-4"
>
<div class="absolute flex flex-col mt-4 ltr:right-4 rtl:left-4 top-4">
<span class="ml-auto font-normal leading-4 text-xxs">
<TimeAgo
:last-activity-timestamp="chat.timestamp"
@@ -338,7 +336,7 @@ export default {
/>
</span>
<span
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ml-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
class="unread shadow-lg rounded-full hidden text-xxs font-semibold h-4 leading-4 ltr:ml-auto rtl:mr-auto mt-1 min-w-[1rem] px-1 py-0 text-center text-white bg-n-teal-9"
>
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>

View File

@@ -23,9 +23,7 @@ export default {
src="dashboard/assets/images/no-chat.svg"
alt="No Chat"
/>
<span
class="text-sm text-slate-800 dark:text-slate-200 font-medium text-center"
>
<span class="text-sm text-n-slate-12 font-medium text-center">
{{ message }}
<br />
</span>

View File

@@ -1,800 +0,0 @@
<script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import BubbleText from './bubble/Text.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import { CONTENT_TYPES } from 'shared/constants/contentType';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/vue';
import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
BubbleActions,
BubbleContact,
BubbleFile,
BubbleImageAudioVideo,
BubbleIntegration,
BubbleLocation,
BubbleMailHead,
BubbleReplyTo,
BubbleText,
ContextMenu,
InstagramStory,
InstagramStoryReply,
Spinner,
NextButton,
},
props: {
data: {
type: Object,
required: true,
},
isATweet: {
type: Boolean,
default: false,
},
isAFacebookInbox: {
type: Boolean,
default: false,
},
isInstagram: {
type: Boolean,
default: false,
},
isAWhatsAppChannel: {
type: Boolean,
default: false,
},
isAnEmailInbox: {
type: Boolean,
default: false,
},
inboxSupportsReplyTo: {
type: Object,
default: () => ({}),
},
inReplyTo: {
type: Object,
default: () => ({}),
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return {
showContextMenu: false,
hasMediaLoadError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
};
},
computed: {
attachments() {
// Here it is used to get sender and created_at for each attachment
return this.data?.attachments.map(attachment => ({
...attachment,
sender: this.data.sender || {},
created_at: this.data.created_at || '',
}));
},
hasOneDayPassed() {
// Disable retry button if the message is failed and the message is older than 24 hours
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
},
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isUnsupported ||
this.isAnIntegrationMessage
);
},
emailMessageContent() {
const {
html_content: { full: fullHTMLContent } = {},
text_content: { full: fullTextContent } = {},
} = this.contentAttributes.email || {};
if (fullHTMLContent) {
return fullHTMLContent;
}
if (fullTextContent) {
return fullTextContent.replace(/\n/g, '<br>');
}
return '';
},
displayQuotedButton() {
if (this.emailMessageContent.includes('<blockquote')) {
return true;
}
if (!this.isIncoming) {
return false;
}
return false;
},
message() {
// If the message is an email, emailMessageContent would be present
// In that case, we would use letter package to render the email
if (this.emailMessageContent && this.isIncoming) {
return this.emailMessageContent;
}
const botMessageContent = generateBotMessageContent(
this.contentType,
this.contentAttributes,
{
noResponseText: this.$t('CONVERSATION.NO_RESPONSE'),
csat: {
ratingTitle: this.$t('CONVERSATION.RATING_TITLE'),
feedbackTitle: this.$t('CONVERSATION.FEEDBACK_TITLE'),
},
}
);
if (this.contentType === 'input_csat') {
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE') + botMessageContent;
}
return (
this.formatMessage(
this.data.content,
this.isATweet,
this.data.private
) + botMessageContent
);
},
inReplyToMessageId() {
// Why not use the inReplyTo object directly?
// Glad you asked! The inReplyTo object may or may not be available
// depending on the current scroll position of the message list
// since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to;
},
isAnInstagramStory() {
return this.contentAttributes.image_type === 'story_mention';
},
contextMenuEnabledOptions() {
return {
copy: this.hasText,
delete:
(this.hasText || this.hasAttachments) &&
!this.isMessageDeleted &&
!this.isFailed,
cannedResponse:
this.isOutgoing && this.hasText && !this.isMessageDeleted,
copyLink: !this.isFailed || !this.isProcessing,
translate:
(!this.isFailed || !this.isProcessing) &&
!this.isMessageDeleted &&
this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
};
},
contentAttributes() {
return this.data.content_attributes || {};
},
externalError() {
return this.contentAttributes.external_error || '';
},
sender() {
return this.data.sender || {};
},
status() {
return this.data.status;
},
storySender() {
return this.contentAttributes.story_sender || null;
},
storyId() {
return this.contentAttributes.story_id || null;
},
storyUrl() {
return this.contentAttributes.story_url || null;
},
contentType() {
const {
data: { content_type: contentType },
} = this;
return contentType;
},
twitterProfileLink() {
const additionalAttributes = this.sender.additional_attributes || {};
const { screen_name: screenName } = additionalAttributes;
return `https://twitter.com/${screenName}`;
},
alignBubble() {
const { message_type: messageType } = this.data;
const isCentered = messageType === MESSAGE_TYPE.ACTIVITY;
const isLeftAligned = messageType === MESSAGE_TYPE.INCOMING;
const isRightAligned =
messageType === MESSAGE_TYPE.OUTGOING ||
messageType === MESSAGE_TYPE.TEMPLATE;
return {
center: isCentered,
left: isLeftAligned,
right: isRightAligned,
'has-context-menu': this.showContextMenu,
// this handles the offset required to align the context menu button
// extra alignment is required since a tweet message has a the user name and avatar below it
'has-tweet-menu': this.isATweet,
'has-bg': this.showBackgroundHighlight,
};
},
createdAt() {
return this.contentAttributes.external_created_at || this.data.created_at;
},
isBubble() {
return [0, 1, 3].includes(this.data.message_type);
},
isIncoming() {
return this.data.message_type === MESSAGE_TYPE.INCOMING;
},
isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
},
isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
},
isAnIntegrationMessage() {
return this.contentType === 'integrations';
},
emailHeadAttributes() {
return {
email: this.contentAttributes.email,
cc: this.contentAttributes.cc_emails,
bcc: this.contentAttributes.bcc_emails,
};
},
hasAttachments() {
return !!(this.data.attachments && this.data.attachments.length > 0);
},
isMessageDeleted() {
return this.contentAttributes.deleted;
},
hasText() {
return !!this.data.content;
},
tooltipForSender() {
const name = this.senderNameForAvatar;
const { message_type: messageType } = this.data;
const showTooltip =
messageType === MESSAGE_TYPE.OUTGOING ||
messageType === MESSAGE_TYPE.TEMPLATE;
return showTooltip
? {
content: `${this.$t('CONVERSATION.SENT_BY')} ${name}`,
}
: false;
},
errorMessageTooltip() {
if (this.isFailed) {
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
}
return '';
},
wrapClass() {
return {
wrap: this.isBubble,
'activity-wrap': !this.isBubble,
'is-pending': this.isPending,
'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
};
},
bubbleClass() {
return {
bubble: this.isBubble,
'is-private': this.data.private,
'is-unsupported': this.isUnsupported,
'is-image': this.hasMediaAttachment('image'),
'is-video': this.hasMediaAttachment('video'),
'is-text': this.hasText,
'is-from-bot': this.isSentByBot,
'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
};
},
isUnsupported() {
return this.contentAttributes.is_unsupported ?? false;
},
isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS;
},
isFailed() {
return this.data.status === MESSAGE_STATUS.FAILED;
},
isSentByBot() {
if (this.isPending || this.isFailed) return false;
return !this.sender.type || this.sender.type === 'agent_bot';
},
shouldShowContextMenu() {
return !this.isUnsupported;
},
showAvatar() {
if (this.isOutgoing || this.isTemplate) {
return true;
}
return this.isATweet && this.isIncoming && this.sender;
},
senderNameForAvatar() {
if (this.isOutgoing || this.isTemplate) {
const { name = this.$t('CONVERSATION.BOT') } = this.sender || {};
return name;
}
return '';
},
isEmailContentType() {
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
},
},
watch: {
data() {
this.hasMediaLoadError = false;
},
},
mounted() {
this.hasMediaLoadError = false;
emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
unmounted() {
emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
clearTimeout(this.higlightTimeout);
},
methods: {
isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video', 'story_mention', 'ig_reel'].includes(
fileType
);
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
return this.compareMessageFileType(this.data, type);
}
return false;
},
compareMessageFileType(messageData, type) {
try {
const { attachments = [{}] } = messageData;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
} catch (err) {
Sentry.setContext('attachment-parsing-error', {
messageData,
type,
hasMediaLoadError: this.hasMediaLoadError,
});
Sentry.captureException(err);
return false;
}
},
handleContextMenuClick() {
this.showContextMenu = !this.showContextMenu;
},
async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data);
},
onMediaLoadError() {
this.hasMediaLoadError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
e.target?.tagName.toLowerCase() === 'a';
if (shouldSkipContextMenu || getSelection().toString()) {
return;
}
e.preventDefault();
if (e.type === 'contextmenu') {
useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
}
this.contextMenuPosition = {
x: e.pageX || e.clientX,
y: e.pageY || e.clientY,
};
this.showContextMenu = true;
},
closeContextMenu() {
this.showContextMenu = false;
this.contextMenuPosition = { x: null, y: null };
},
handleReplyTo() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
const { conversation_id: conversationId, id: replyTo } = this.data;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
return;
}
this.showBackgroundHighlight = true;
const HIGHLIGHT_TIMER = 1000;
this.higlightTimeout = setTimeout(() => {
this.showBackgroundHighlight = false;
}, HIGHLIGHT_TIMER);
},
},
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<li
v-if="shouldRenderMessage"
:id="`message${data.id}`"
class="group/context-menu"
:class="[alignBubble]"
>
<div :class="wrapClass">
<div
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
class="message-failed--alert"
>
<NextButton
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
ghost
xs
ruby
icon="i-lucide-refresh-ccw"
@click="retrySendMessage"
/>
</div>
<div :class="bubbleClass" @contextmenu="openContextMenu($event)">
<BubbleMailHead
:email-attributes="contentAttributes.email"
:cc="emailHeadAttributes.cc"
:bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming"
/>
<InstagramStoryReply v-if="storyUrl" :story-url="storyUrl" />
<BubbleReplyTo
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
:message="inReplyTo"
:message-type="data.message_type"
:parent-has-attachments="hasAttachments"
/>
<div v-if="isUnsupported">
<template v-if="isAFacebookInbox && isInstagram">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
</template>
<template v-else-if="isAFacebookInbox">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
</template>
<template v-else>
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
</template>
</div>
<BubbleText
v-else-if="data.content"
:message="message"
:is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton"
/>
<BubbleIntegration
:message-id="data.id"
:content-attributes="contentAttributes"
:inbox-id="data.inbox_id"
/>
<span
v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent"
>
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
</span>
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in attachments" :key="attachment.id">
<InstagramStory
v-if="isAnInstagramStory"
:story-url="attachment.data_url"
@error="onMediaLoadError"
/>
<BubbleImageAudioVideo
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment"
@error="onMediaLoadError"
/>
<BubbleLocation
v-else-if="attachment.file_type === 'location'"
:latitude="attachment.coordinates_lat"
:longitude="attachment.coordinates_long"
:name="attachment.fallback_title"
/>
<BubbleContact
v-else-if="attachment.file_type === 'contact'"
:name="data.content"
:phone-number="attachment.fallback_title"
/>
<BubbleFile v-else :url="attachment.data_url" />
</div>
</div>
<BubbleActions
:id="data.id"
:sender="data.sender"
:story-sender="storySender"
:external-error="errorMessageTooltip"
:story-id="`${storyId}`"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:is-email="isEmailContentType"
:is-private="data.private"
:message-type="data.message_type"
:message-status="status"
:source-id="data.source_id"
:inbox-id="data.inbox_id"
:created-at="createdAt"
/>
</div>
<Spinner v-if="isPending" size="tiny" />
<div
v-if="showAvatar"
v-tooltip.left="tooltipForSender"
class="sender--info"
>
<woot-thumbnail
:src="sender.thumbnail"
:username="senderNameForAvatar"
size="16px"
/>
<a
v-if="isATweet && isIncoming"
class="sender--available-name"
:href="twitterProfileLink"
target="_blank"
rel="noopener noreferrer nofollow"
>
{{ sender.name }}
</a>
</div>
</div>
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
<ContextMenu
v-if="isBubble && !isMessageDeleted"
:context-menu-position="contextMenuPosition"
:is-open="showContextMenu"
:enabled-options="contextMenuEnabledOptions"
:message="data"
@open="openContextMenu"
@close="closeContextMenu"
@reply-to="handleReplyTo"
/>
</div>
</li>
</template>
<style lang="scss">
.wrap {
> .bubble {
@apply min-w-[128px];
&.is-unsupported {
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
.message-text--metadata .time {
@apply text-slate-400 dark:text-slate-300;
}
}
&.is-image,
&.is-video {
@apply p-0 overflow-hidden;
.image,
.video {
@apply max-w-[20rem] p-0.5;
> img,
> video {
/** ensure that the bubble radius and image radius match*/
@apply rounded-[0.4rem];
}
> video {
@apply h-full w-full object-cover;
}
}
.video {
@apply h-[11.25rem];
}
}
&.is-image.is-text > .message-text__wrap,
&.is-video.is-text > .message-text__wrap {
@apply max-w-[20rem] py-2 px-4;
}
&.is-private .file.message-text__wrap {
.file--icon {
@apply text-woot-400 dark:text-woot-400;
}
.attachment-name {
@apply text-slate-700 dark:text-slate-200;
}
.download.button {
@apply text-woot-400 dark:text-woot-400;
}
}
&.is-private.is-text > .message-text__wrap .link {
@apply text-woot-600 dark:text-woot-200;
}
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
}
&.is-from-bot {
@apply bg-violet-400 dark:bg-violet-400;
.message-text--metadata .time {
@apply text-violet-50 dark:text-violet-50;
}
&.is-private .message-text--metadata .time {
@apply text-slate-400 dark:text-slate-400;
}
}
&.is-failed {
@apply bg-n-ruby-4 dark:bg-n-ruby-4 text-n-slate-12;
.message-text--metadata .time {
@apply text-n-ruby-12 dark:text-n-ruby-12;
}
}
}
&.is-pending {
@apply relative opacity-80;
.spinner {
@apply absolute bottom-1 right-1;
}
> .is-image.is-text.bubble > .message-text__wrap {
@apply p-0;
}
}
}
.wrap.is-email {
--bubble-max-width: 84% !important;
}
.sender--info {
@apply items-center text-black-700 dark:text-black-100 inline-flex py-1 px-0;
.sender--available-name {
@apply text-xs ml-1;
}
}
.message-failed--alert {
@apply text-red-900 dark:text-red-900 flex-grow text-right mt-1 mr-1 mb-0 ml-0;
}
li.left,
li.right {
@apply flex items-end;
}
li.left.has-tweet-menu .context-menu {
// this handles the offset required to align the context menu button
// extra alignment is required since a tweet message has a the user name and avatar below it
@apply mb-6;
}
li.has-bg {
@apply bg-woot-75 dark:bg-woot-600;
}
li.right .context-menu-wrap {
@apply ml-auto;
}
li.right {
@apply flex-row-reverse justify-end;
.wrap.is-pending {
@apply ml-auto;
}
.wrap.is-failed {
@apply flex items-end ltr:ml-auto rtl:mr-auto;
}
}
.has-context-menu {
@apply bg-slate-50 dark:bg-slate-700;
}
.context-menu {
@apply relative;
}
/* Markdown styling */
.bubble .text-content {
p code {
@apply bg-slate-75 dark:bg-slate-700 inline-block leading-none rounded-sm p-1;
}
ol li {
@apply list-item list-decimal;
}
pre {
@apply bg-slate-75 dark:bg-slate-700 block border-slate-75 dark:border-slate-700 text-slate-800 dark:text-slate-100 rounded-md p-2 mt-1 mb-2 leading-relaxed whitespace-pre-wrap;
code {
@apply bg-transparent text-slate-800 dark:text-slate-100 p-0;
}
}
blockquote {
@apply border-l-4 mx-0 my-1 pt-2 pr-2 pb-0 pl-4 border-slate-75 border-solid dark:border-slate-600 text-slate-800 dark:text-slate-100;
p {
@apply text-slate-800 dark:text-slate-300;
}
}
}
.right .bubble .text-content {
p code {
@apply bg-woot-600 dark:bg-woot-600 text-white dark:text-white;
}
pre {
@apply bg-woot-800 dark:bg-woot-800 border-woot-700 dark:border-woot-700 text-white dark:text-white;
code {
@apply bg-transparent text-white dark:text-white;
}
}
blockquote {
@apply border-l-4 border-solid border-woot-400 dark:border-woot-400 text-white dark:text-white;
p {
@apply text-woot-75 dark:text-woot-75;
}
}
}
</style>

View File

@@ -66,26 +66,26 @@ export default {
<fluent-icon
v-if="isMessagePrivate"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
icon="lock-closed"
/>
<fluent-icon
v-else-if="messageByAgent"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
icon="arrow-reply"
/>
<fluent-icon
v-else-if="isMessageAnActivity"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
class="-mt-0.5 align-middle text-n-slate-11 inline-block"
icon="info"
/>
</template>
<span v-if="message.content && isMessageSticker">
<fluent-icon
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
class="-mt-0.5 align-middle inline-block text-n-slate-11"
icon="image"
/>
{{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }}
@@ -97,7 +97,7 @@ export default {
<fluent-icon
v-if="attachmentIcon && showMessageType"
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
class="-mt-0.5 align-middle inline-block text-n-slate-11"
:icon="attachmentIcon"
/>
{{ $t(`${attachmentMessageContent}`) }}

View File

@@ -11,7 +11,7 @@ const openProfileSettings = () => {
<template>
<div
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-slate-25 dark:hover:bg-slate-800 border border-dashed border-slate-100 dark:border-slate-700 rounded-sm overflow-auto"
class="my-0 mx-4 px-1 flex max-h-[8vh] items-baseline justify-between hover:bg-n-slate-1 border border-dashed border-n-weak rounded-sm overflow-auto"
>
<p class="w-fit !m-0">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}

View File

@@ -4,14 +4,13 @@ import { ref, provide } from 'vue';
import { useConfig } from 'dashboard/composables/useConfig';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useAI } from 'dashboard/composables/useAI';
import { useMapGetter } from 'dashboard/composables/store';
// components
import ReplyBox from './ReplyBox.vue';
import Message from './Message.vue';
import NextMessageList from 'next/message/MessageList.vue';
import MessageList from 'next/message/MessageList.vue';
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
// stores and apis
import { mapGetters } from 'vuex';
@@ -35,16 +34,15 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
export default {
components: {
Message,
NextMessageList,
MessageList,
ReplyBox,
Banner,
ConversationLabelSuggestion,
Spinner,
},
mixins: [inboxMixin],
setup() {
@@ -52,17 +50,11 @@ export default {
const conversationPanelRef = ref(null);
const { isEnterprise } = useConfig();
const closePopOutReplyBox = () => {
isPopOutReplyBox.value = false;
};
const showPopOutReplyBox = () => {
isPopOutReplyBox.value = !isPopOutReplyBox.value;
};
const keyboardEvents = {
Escape: {
action: closePopOutReplyBox,
action: () => {
isPopOutReplyBox.value = false;
},
},
};
@@ -75,28 +67,15 @@ export default {
fetchLabelSuggestions,
} = useAI();
const currentAccountId = useMapGetter('getCurrentAccountId');
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const showNextBubbles = isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.CHATWOOT_V4
);
provide('contextMenuElementTarget', conversationPanelRef);
return {
isEnterprise,
isPopOutReplyBox,
closePopOutReplyBox,
showPopOutReplyBox,
isAIIntegrationEnabled,
isLabelSuggestionFeatureEnabled,
fetchIntegrationsIfRequired,
fetchLabelSuggestions,
showNextBubbles,
conversationPanelRef,
};
},
@@ -180,20 +159,6 @@ export default {
(!this.listLoadingStatus && this.isLoadingPrevious)
);
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
return type || '';
},
isATweet() {
return this.conversationType === 'tweet';
},
getLastSeenAt() {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt;
},
// Check there is a instagram inbox exists with the same instagram_id
hasDuplicateInstagramInbox() {
const instagramId = this.inbox.instagram_id;
@@ -269,9 +234,6 @@ export default {
: 'CONVERSATION.UNREAD_MESSAGE';
return `${count} ${this.$t(label)}`;
},
isInstagramDM() {
return this.conversationType === 'instagram_direct_message';
},
inboxSupportsReplyTo() {
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
const outgoing =
@@ -475,18 +437,6 @@ export default {
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},
getInReplyToMessage(parentMessage) {
if (!parentMessage) return {};
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return {};
return this.currentChat?.messages.find(message => {
if (message.id === inReplyToMessageId) {
return true;
}
return false;
});
},
},
};
</script>
@@ -507,10 +457,9 @@ export default {
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
<NextMessageList
v-if="showNextBubbles"
<MessageList
ref="conversationPanelRef"
class="conversation-panel"
class="conversation-panel flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4"
:current-user-id="currentUserId"
:first-unread-id="unReadMessages[0]?.id"
:is-an-email-channel="isAnEmailChannel"
@@ -520,14 +469,18 @@ export default {
<template #beforeAll>
<transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]">
<span v-if="shouldShowSpinner" class="spinner message" />
<li
class="min-h-[4rem] flex flex-shrink-0 flex-grow-0 items-center flex-auto justify-center max-w-full mt-0 mr-0 mb-1 ml-0 relative first:mt-auto last:mb-0"
>
<Spinner v-if="shouldShowSpinner" class="text-n-brand" />
</li>
</transition>
</template>
<template #unreadBadge>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
<li v-show="unreadMessageCount != 0">
<span
class="shadow-lg rounded-full bg-n-brand text-white text-xs font-medium my-2.5 mx-auto px-2.5 py-1.5"
>
{{ unreadMessageLabel }}
</span>
</li>
@@ -540,65 +493,12 @@ export default {
:conversation-id="currentChat.id"
/>
</template>
</NextMessageList>
<ul v-else ref="conversationPanelRef" class="conversation-panel">
<transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]">
<span v-if="shouldShowSpinner" class="spinner message" />
</li>
</transition>
<Message
v-for="message in readMessages"
:key="message.id"
class="message--read ph-no-capture"
data-clarity-mask="True"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-an-email-inbox="isAnEmailChannel"
:is-instagram="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
{{
unreadMessageCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES')
: $t('CONVERSATION.UNREAD_MESSAGE')
}}
</span>
</li>
<Message
v-for="message in unReadMessages"
:key="message.id"
class="message--unread ph-no-capture"
data-clarity-mask="True"
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-instagram-dm="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
<ConversationLabelSuggestion
v-if="shouldShowLabelSuggestions"
:suggested-labels="labelSuggestions"
:chat-labels="currentChat.labels"
:conversation-id="currentChat.id"
/>
</ul>
</MessageList>
<div
class="conversation-footer"
class="flex relative flex-col"
:class="{
'modal-mask': isPopOutReplyBox,
'bg-n-background': showNextBubbles && !isPopOutReplyBox,
'bg-n-background': !isPopOutReplyBox,
}"
>
<div
@@ -606,7 +506,7 @@ export default {
class="absolute flex items-center w-full h-0 -top-7"
>
<div
class="flex py-2 pr-4 pl-5 shadow-md rounded-full bg-white dark:bg-slate-700 text-n-slate-11 text-xs font-semibold my-2.5 mx-auto"
class="flex py-2 pr-4 pl-5 shadow-md rounded-full bg-white dark:bg-n-solid-3 text-n-slate-11 text-xs font-semibold my-2.5 mx-auto"
>
{{ typingUserNames }}
<img
@@ -617,8 +517,8 @@ export default {
</div>
</div>
<ReplyBox
v-model:popout-reply-box="isPopOutReplyBox"
@toggle-popout="showPopOutReplyBox"
:pop-out-reply-box="isPopOutReplyBox"
@update:pop-out-reply-box="isPopOutReplyBox = $event"
/>
</div>
</div>
@@ -626,7 +526,7 @@ export default {
<style scoped lang="scss">
.modal-mask {
@apply absolute;
@apply fixed;
&::v-deep {
.ProseMirror-woot-style {
@@ -650,7 +550,7 @@ export default {
}
.emoji-dialog {
@apply absolute left-auto bottom-1;
@apply absolute ltr:left-auto rtl:right-auto bottom-1;
}
}
}

View File

@@ -29,24 +29,24 @@ defineProps({
<template>
<div
class="h-full w-full bg-white dark:bg-slate-900 border border-slate-100 dark:border-white/10 rounded-lg p-4 flex flex-col"
class="h-full w-full bg-n-background border border-n-weak rounded-lg p-4 flex flex-col"
>
<div class="flex-1 flex items-center justify-center">
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
</div>
<div class="mt-auto">
<p
class="text-base text-slate-800 dark:text-slate-100 font-interDisplay font-semibold tracking-[0.3px]"
class="text-base text-n-slate-12 font-interDisplay font-semibold tracking-[0.3px]"
>
{{ title }}
</p>
<p class="text-slate-600 dark:text-slate-400 text-sm">
<p class="text-n-slate-11 text-sm">
{{ description }}
</p>
<router-link
v-if="to"
:to="{ name: to }"
class="no-underline text-woot-500 text-sm font-medium"
class="no-underline text-n-brand text-sm font-medium"
>
<span>{{ linkText }}</span>
<span class="ml-2">{{ `` }}</span>

View File

@@ -32,11 +32,11 @@ const greetingMessage = computed(() => {
>
<div class="col-span-full self-start">
<p
class="text-xl font-semibold text-slate-900 dark:text-white font-interDisplay tracking-[0.3px]"
class="text-xl font-semibold text-n-slate-12 font-interDisplay tracking-[0.3px]"
>
{{ greetingMessage }}
</p>
<p class="text-slate-600 dark:text-slate-400 max-w-2xl text-base">
<p class="text-n-slate-11 max-w-2xl text-base">
{{
$t('ONBOARDING.DESCRIPTION', {
installationName: globalConfig.installationName,

View File

@@ -1,6 +1,5 @@
<script>
// [TODO] The popout events are needlessly complex and should be simplified
import { defineAsyncComponent, defineModel, useTemplateRef } from 'vue';
import { defineAsyncComponent, useTemplateRef } from 'vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
@@ -66,7 +65,13 @@ export default {
WootMessageEditor,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
emits: ['update:popoutReplyBox', 'togglePopout'],
props: {
popOutReplyBox: {
type: Boolean,
default: false,
},
},
emits: ['update:popOutReplyBox'],
setup() {
const {
uiSettings,
@@ -75,16 +80,10 @@ export default {
fetchSignatureFlagFromUISettings,
} = useUISettings();
const popoutReplyBox = defineModel('popoutReplyBox', {
type: Boolean,
default: false,
});
const replyEditor = useTemplateRef('replyEditor');
return {
uiSettings,
popoutReplyBox,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
@@ -123,7 +122,6 @@ export default {
},
computed: {
...mapGetters({
isRTL: 'accounts/isRTL',
currentChat: 'getSelectedChat',
messageSignature: 'getMessageSignature',
currentUser: 'getCurrentUser',
@@ -316,15 +314,6 @@ export default {
this.uiSettings;
return conversationDisplayType !== CONDENSED;
},
emojiDialogClassOnExpandedLayoutAndRTLView() {
if (this.isOnExpandedLayout || this.popoutReplyBox) {
return 'emoji-dialog--expanded';
}
if (this.isRTL) {
return 'emoji-dialog--rtl';
}
return '';
},
isMessageEmpty() {
if (!this.message) {
return true;
@@ -719,7 +708,7 @@ export default {
this.clearMessage();
this.hideEmojiPicker();
this.$emit('update:popoutReplyBox', false);
this.$emit('update:popOutReplyBox', false);
}
},
sendMessageAsMultipleMessages(message) {
@@ -1098,6 +1087,9 @@ export default {
file => !file?.isRecordedAudio
);
},
togglePopout() {
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
},
},
};
</script>
@@ -1118,9 +1110,9 @@ export default {
:mode="replyType"
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:popout-reply-box="popoutReplyBox"
:popout-reply-box="popOutReplyBox"
@set-reply-mode="setReplyMode"
@toggle-popout="$emit('togglePopout')"
@toggle-popout="togglePopout"
/>
<ArticleSearchPopover
v-if="showArticleSearchPopover && connectedPortalSlug"
@@ -1144,7 +1136,9 @@ export default {
<EmojiInput
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
:class="{
'emoji-dialog--expanded': isOnExpandedLayout || popOutReplyBox,
}"
:on-click="addIntoEditor"
/>
<ReplyEmailHead
@@ -1297,21 +1291,11 @@ export default {
}
.emoji-dialog {
@apply top-[unset] -bottom-10 -left-80 right-[unset];
@apply top-[unset] -bottom-10 ltr:-left-80 ltr:right-[unset] rtl:left-[unset] rtl:-right-80;
&::before {
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
}
}
.emoji-dialog--rtl {
@apply left-[unset] -right-80;
&::before {
transform: rotate(90deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
@apply ltr:-right-4 bottom-2 rtl:-left-4 ltr:rotate-[270deg] rtl:rotate-[90deg];
}
}
@@ -1320,12 +1304,12 @@ export default {
&::before {
transform: rotate(0deg);
@apply left-1 -bottom-2;
@apply ltr:left-1 rtl:right-1 -bottom-2;
}
}
.normal-editor__canned-box {
width: calc(100% - 2 * var(--space-normal));
left: var(--space-normal);
width: calc(100% - 2 * 1rem);
left: 1rem;
}
</style>

View File

@@ -27,7 +27,7 @@ const getStatusClass = status => {
const classes = {
paid: 'bg-n-teal-5 text-n-teal-12',
};
return classes[status] || 'bg-slate-50 text-slate-700';
return classes[status] || 'bg-n-solid-3 text-n-slate-12';
};
const getStatusI18nKey = (type, status = '') => {
@@ -52,11 +52,11 @@ const financialStatus = computed(() => {
const getFulfillmentClass = status => {
const classes = {
fulfilled: 'text-green-600',
partial: 'text-yellow-600',
unfulfilled: 'text-red-600',
fulfilled: 'text-n-teal-9',
partial: 'text-n-amber-9',
unfulfilled: 'text-n-ruby-9',
};
return classes[status] || 'text-slate-600';
return classes[status] || 'text-n-slate-11';
};
</script>

View File

@@ -82,7 +82,7 @@ const onAgentSelect = index => {
@click="onAgentSelect(index)"
@mouseover="onHover(index)"
>
<div class="mr-2">
<div class="ltr:mr-2 rtl:ml-2">
<Avatar :src="agent.thumbnail" :name="agent.name" rounded-full />
</div>
<div

View File

@@ -69,6 +69,6 @@ export default {
<style scoped>
.variable--list-label {
font-weight: var(--font-weight-bold);
font-weight: 600;
}
</style>

View File

@@ -1,357 +0,0 @@
<script>
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import inboxMixin from 'shared/mixins/inboxMixin';
import { messageTimestamp } from 'shared/helpers/timeHelper';
export default {
mixins: [inboxMixin],
props: {
sender: {
type: Object,
default: () => ({}),
},
createdAt: {
type: Number,
default: 0,
},
storySender: {
type: String,
default: '',
},
externalError: {
type: String,
default: '',
},
storyId: {
type: String,
default: '',
},
isEmail: {
type: Boolean,
default: true,
},
isPrivate: {
type: Boolean,
default: true,
},
isATweet: {
type: Boolean,
default: true,
},
messageType: {
type: Number,
default: 1,
},
messageStatus: {
type: String,
default: '',
},
sourceId: {
type: String,
default: '',
},
inboxId: {
type: [String, Number],
default: 0,
},
},
computed: {
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
isIncoming() {
return MESSAGE_TYPE.INCOMING === this.messageType;
},
isOutgoing() {
return MESSAGE_TYPE.OUTGOING === this.messageType;
},
isTemplate() {
return MESSAGE_TYPE.TEMPLATE === this.messageType;
},
isDelivered() {
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
},
isRead() {
return MESSAGE_STATUS.READ === this.messageStatus;
},
isSent() {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
this.sender || {};
return additionalAttributes?.screen_name || '';
},
linkToTweet() {
if (!this.sourceId || !this.inbox.name) {
return '';
}
const { screenName, sourceId } = this;
return `https://twitter.com/${
screenName || this.inbox.name
}/status/${sourceId}`;
},
linkToStory() {
if (!this.storyId || !this.storySender) {
return '';
}
const { storySender, storyId } = this;
return `https://www.instagram.com/stories/direct/${storySender}_${storyId}`;
},
showStatusIndicators() {
if ((this.isOutgoing || this.isTemplate) && !this.isPrivate) {
return true;
}
return false;
},
showSentIndicator() {
if (!this.showStatusIndicators) {
return false;
}
// Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox ||
this.isASmsInbox ||
this.isATelegramChannel
) {
return this.sourceId && this.isSent;
}
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
return false;
},
showDeliveredIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isASmsInbox ||
this.isAFacebookInbox
) {
return this.sourceId && this.isDelivered;
}
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent;
}
if (this.isALineChannel) {
return this.isDelivered;
}
return false;
},
showReadIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox
) {
return this.sourceId && this.isRead;
}
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isRead;
}
return false;
},
},
};
</script>
<template>
<div class="message-text--metadata">
<span
class="time"
:class="{
'has-status-icon':
showSentIndicator || showDeliveredIndicator || showReadIndicator,
}"
>
{{ readableTime }}
</span>
<span v-if="externalError" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="externalError"
icon="error-circle"
class="action--icon"
size="14"
/>
</span>
<span v-if="showReadIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick read-indicator"
size="14"
/>
</span>
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
icon="checkmark-double"
class="action--icon read-tick"
size="14"
/>
</span>
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
icon="checkmark"
class="action--icon read-tick"
size="14"
/>
</span>
<fluent-icon
v-if="isEmail"
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
icon="mail"
class="action--icon"
size="16"
/>
<fluent-icon
v-if="isPrivate"
v-tooltip.top-start="$t('CONVERSATION.VISIBLE_TO_AGENTS')"
icon="lock-closed"
class="action--icon lock--icon--private"
size="16"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
/>
<a
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
:href="linkToTweet"
target="_blank"
rel="noopener noreferrer nofollow"
>
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
icon="open"
class="cursor-pointer action--icon"
size="16"
/>
</a>
</div>
</template>
<style lang="scss" scoped>
.right {
.message-text--metadata {
@apply items-center;
.time {
@apply text-woot-100 dark:text-woot-100;
}
.action--icon {
@apply text-white dark:text-white;
&.read-tick {
@apply text-violet-100 dark:text-violet-100;
}
&.read-indicator {
@apply text-green-200 dark:text-green-200;
}
}
.lock--icon--private {
@apply text-slate-400 dark:text-slate-400;
}
}
}
.left {
.message-text--metadata {
.time {
@apply text-slate-400 dark:text-slate-200;
}
}
}
.message-text--metadata {
@apply items-start flex;
.time {
@apply mr-2 block text-xxs leading-[1.8];
}
.action--icon {
@apply mr-2 ml-2 text-slate-900 dark:text-slate-100;
}
a {
@apply text-slate-900 dark:text-slate-100;
}
}
.activity-wrap {
.message-text--metadata {
.time {
@apply ml-2 rtl:mr-2 rtl:ml-0 flex text-center text-xxs text-slate-300 dark:text-slate-200;
}
}
}
.is-image,
.is-video {
.message-text--metadata {
.time {
@apply bottom-1 text-white dark:text-slate-50 absolute right-2 whitespace-nowrap;
&.has-status-icon {
@apply right-8 leading-loose;
}
}
.read-tick {
@apply absolute bottom-2 right-2;
}
}
}
.is-private {
.message-text--metadata {
@apply items-center;
.time {
@apply text-slate-400 dark:text-slate-400;
}
.icon {
@apply text-slate-400 dark:text-slate-400;
}
}
&.is-image,
&.is-video {
.time {
position: inherit;
@apply pl-2.5;
}
}
}
.delivered-icon {
@apply ml-4;
}
.read-indicator-wrap {
@apply leading-none flex items-center;
}
</style>

View File

@@ -1,123 +0,0 @@
<script>
import { useAlert } from 'dashboard/composables';
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
name: {
type: String,
default: '',
},
phoneNumber: {
type: String,
default: '',
},
},
computed: {
formattedPhoneNumber() {
return this.phoneNumber.replace(/\s|-|[A-Za-z]/g, '');
},
rawPhoneNumber() {
return this.phoneNumber.replace(/\D/g, '');
},
},
methods: {
async addContact() {
try {
let contact = await this.filterContactByNumber(this.rawPhoneNumber);
if (!contact) {
contact = await this.$store.dispatch(
'contacts/create',
this.getContactObject()
);
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
}
this.openContactNewTab(contact.id);
} catch (error) {
if (error instanceof DuplicateContactException) {
if (error.data.includes('phone_number')) {
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
}
} else if (error instanceof ExceptionWithMessage) {
useAlert(error.data);
} else {
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
getContactObject() {
const contactItem = {
name: this.name,
phone_number: `+${this.rawPhoneNumber}`,
};
return contactItem;
},
async filterContactByNumber(phoneNumber) {
const query = {
attribute_key: 'phone_number',
filter_operator: 'equal_to',
values: [phoneNumber],
attribute_model: 'standard',
custom_attribute_type: '',
};
const queryPayload = { payload: [query] };
const contacts = await this.$store.dispatch('contacts/filter', {
queryPayload,
resetState: false,
});
return contacts.shift();
},
openContactNewTab(contactId) {
const accountId = window.location.pathname.split('/')[3];
const url = `/app/accounts/${accountId}/contacts/${contactId}`;
window.open(url, '_blank');
},
},
};
</script>
<template>
<div class="contact--group">
<fluent-icon icon="call" class="file--icon" size="18" />
<div class="meta">
<p
class="overflow-hidden whitespace-nowrap text-ellipsis margin-bottom-0"
>
{{ phoneNumber }}
</p>
</div>
<div v-if="formattedPhoneNumber" class="link-wrap">
<NextButton
ghost
xs
:label="$t('CONVERSATION.SAVE_CONTACT')"
@click.prevent="addContact"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.contact--group {
align-items: center;
display: flex;
margin-top: var(--space-smaller);
.meta {
flex: 1;
margin-left: var(--space-small);
}
.link-wrap {
margin-left: var(--space-small);
}
}
</style>

View File

@@ -1,86 +0,0 @@
<script>
export default {
props: {
url: {
type: String,
required: true,
},
},
computed: {
fileName() {
if (this.url) {
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
return filename || this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
}
return this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
},
},
methods: {
openLink() {
const win = window.open(this.url, '_blank', 'noopener');
if (win) win.focus();
},
},
};
</script>
<template>
<div class="file message-text__wrap">
<div class="icon-wrap">
<fluent-icon icon="document" class="file--icon" size="32" />
</div>
<div class="meta">
<h5 class="attachment-name text-slate-700 dark:text-slate-400">
{{ decodeURI(fileName) }}
</h5>
<a
class="download clear link button small"
rel="noreferrer noopener nofollow"
target="_blank"
:href="url"
>
{{ $t('CONVERSATION.DOWNLOAD') }}
</a>
</div>
</div>
</template>
<style lang="scss" scoped>
@import 'dashboard/assets/scss/variables';
.file {
display: flex;
flex-direction: row;
padding: $space-smaller 0;
cursor: pointer;
.icon-wrap {
font-size: $font-size-giga;
color: $color-white;
line-height: 1;
margin-left: $space-smaller;
margin-right: $space-slab;
}
.attachment-name {
margin: 0;
color: $color-white;
font-weight: $font-weight-bold;
word-break: break-word;
}
.button {
padding: 0;
margin: 0;
color: $color-primary-light;
}
.meta {
padding-right: $space-two;
}
.time {
min-width: $space-larger;
}
}
</style>

View File

@@ -1,34 +0,0 @@
<script>
export default {
components: {},
props: {
url: {
type: String,
required: true,
},
},
emits: ['error'],
data() {
return {
show: false,
};
},
methods: {
onClose() {
this.show = false;
},
onClick() {
this.show = true;
},
},
};
</script>
<template>
<div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="$emit('error')" />
<woot-modal v-model:show="show" full-width :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" />
</woot-modal>
</div>
</template>

View File

@@ -1,127 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import { hasPressedCommand } from 'shared/helpers/KeyboardHelpers';
import GalleryView from '../components/GalleryView.vue';
import { timeStampAppendedURL } from 'dashboard/helper/URLHelper';
const ALLOWED_FILE_TYPES = {
IMAGE: 'image',
VIDEO: 'video',
AUDIO: 'audio',
IG_REEL: 'ig_reel',
};
export default {
components: {
GalleryView,
},
props: {
attachment: {
type: Object,
required: true,
},
},
emits: ['error'],
data() {
return {
show: false,
isImageError: false,
};
},
computed: {
...mapGetters({
currentChatAttachments: 'getSelectedChatAttachments',
}),
isImage() {
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
},
isVideo() {
return (
this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO ||
this.attachment.file_type === ALLOWED_FILE_TYPES.IG_REEL
);
},
isAudio() {
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
},
timeStampURL() {
return timeStampAppendedURL(this.dataUrl);
},
attachmentTypeClasses() {
return {
image: this.isImage,
video: this.isVideo,
};
},
filteredCurrentChatAttachments() {
const attachments = this.currentChatAttachments.filter(attachment =>
['image', 'video', 'audio'].includes(attachment.file_type)
);
return attachments;
},
dataUrl() {
return this.attachment.data_url;
},
imageWidth() {
return this.attachment.width ? `${this.attachment.width}px` : 'auto';
},
imageHeight() {
return this.attachment.height ? `${this.attachment.height}px` : 'auto';
},
},
watch: {
attachment() {
this.isImageError = false;
},
},
methods: {
onClose() {
this.show = false;
},
onClick(e) {
if (hasPressedCommand(e)) {
window.open(this.attachment.data_url, '_blank');
return;
}
this.show = true;
},
onImgError() {
this.isImageError = true;
this.$emit('error');
},
},
};
</script>
<template>
<div class="message-text__wrap" :class="attachmentTypeClasses">
<img
v-if="isImage && !isImageError"
class="bg-woot-200 dark:bg-woot-900"
:src="dataUrl"
:width="imageWidth"
:height="imageHeight"
@click="onClick"
@error="onImgError"
/>
<video
v-if="isVideo"
:src="dataUrl"
muted
playsInline
@error="onImgError"
@click="onClick"
/>
<audio v-else-if="isAudio" controls class="skip-context-menu mb-0.5">
<source :src="timeStampURL" />
</audio>
<GalleryView
v-if="show"
v-model:show="show"
:attachment="attachment"
:all-attachments="filteredCurrentChatAttachments"
@error="onImgError"
@close="onClose"
/>
</div>
</template>

View File

@@ -1,53 +0,0 @@
<script>
import BubbleImage from './Image.vue';
import BubbleVideo from './Video.vue';
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
export default {
components: {
BubbleImage,
BubbleVideo,
InstagramStoryErrorPlaceHolder,
},
props: {
storyUrl: {
type: String,
default: '',
},
},
emits: ['error'],
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
this.emitError();
},
onVideoLoadError() {
this.hasVideoStoryError = true;
this.emitError();
},
emitError() {
this.$emit('error');
},
},
};
</script>
<template>
<BubbleImage
v-if="!hasImgStoryError"
:url="storyUrl"
@error="onImageLoadError"
/>
<BubbleVideo
v-else-if="!hasVideoStoryError"
:url="storyUrl"
@error="onVideoLoadError"
/>
<InstagramStoryErrorPlaceHolder v-else />
</template>

View File

@@ -1,14 +0,0 @@
<script>
export default {};
</script>
<template>
<div
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
>
<fluent-icon icon="document-error" size="32" />
<p class="mb-0 text-slate-700 dark:text-slate-75">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
</template>

View File

@@ -1,36 +0,0 @@
<script>
import InstagramStory from './InstagramStory.vue';
export default {
components: { InstagramStory },
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
},
onVideoLoadError() {
this.hasVideoStoryError = true;
},
},
};
</script>
<template>
<blockquote
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
>
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<InstagramStory :story-url="storyUrl" class="mt-3 rounded-md" />
</blockquote>
</template>

View File

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

View File

@@ -1,52 +0,0 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
latitude: {
type: Number,
default: undefined,
},
longitude: {
type: Number,
default: undefined,
},
name: {
type: String,
default: '',
},
});
const mapUrl = computed(
() => `https://maps.google.com/?q=${props.latitude},${props.longitude}`
);
</script>
<template>
<div
class="flex flex-row items-center justify-start gap-1 w-full py-1 px-0 cursor-pointer overflow-hidden"
>
<fluent-icon
icon="location"
class="text-slate-600 dark:text-slate-200 leading-none my-0 flex items-center flex-shrink-0"
size="32"
/>
<div class="flex flex-col items-start flex-1 min-w-0">
<h5
class="text-sm text-slate-800 dark:text-slate-100 truncate m-0 w-full"
:title="name"
>
{{ name }}
</h5>
<div class="flex items-center">
<a
class="text-woot-600 dark:text-woot-600 text-xs underline"
rel="noreferrer noopener nofollow"
target="_blank"
:href="mapUrl"
>
{{ $t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP') }}
</a>
</div>
</div>
</div>
</template>

View File

@@ -1,100 +0,0 @@
<script>
export default {
props: {
emailAttributes: {
type: Object,
default: () => ({}),
},
isIncoming: {
type: Boolean,
default: true,
},
cc: {
type: Array,
default: () => [],
},
bcc: {
type: Array,
default: () => [],
},
},
computed: {
fromMail() {
const from = this.emailAttributes.from || [];
return from.join(', ');
},
toMails() {
const to = this.emailAttributes.to || [];
return to.join(', ');
},
ccMails() {
const cc = this.emailAttributes.cc || this.cc || [];
return cc.join(', ');
},
bccMails() {
const bcc = this.emailAttributes.bcc || this.bcc || [];
return bcc.join(', ');
},
subject() {
return this.emailAttributes.subject || '';
},
showHead() {
return this.toMails || this.ccMails || this.bccMails || this.fromMail;
},
},
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div
v-if="showHead"
class="message__mail-head"
:class="{ 'is-incoming': isIncoming }"
>
<div v-if="fromMail" class="meta-wrap">
<span class="message__content--type">{{ $t('EMAIL_HEADER.FROM') }}:</span>
<span>{{ fromMail }}</span>
</div>
<div v-if="toMails" class="meta-wrap">
<span class="message__content--type">{{ $t('EMAIL_HEADER.TO') }}:</span>
<span>{{ toMails }}</span>
</div>
<div v-if="ccMails" class="meta-wrap">
<span class="message__content--type">{{ $t('EMAIL_HEADER.CC') }}:</span>
<span>{{ ccMails }}</span>
</div>
<div v-if="bccMails" class="meta-wrap">
<span class="message__content--type">{{ $t('EMAIL_HEADER.BCC') }}:</span>
<span>{{ bccMails }}</span>
</div>
<div v-if="subject" class="meta-wrap">
<span class="message__content--type">
{{ $t('EMAIL_HEADER.SUBJECT') }}:
</span>
<span>{{ subject }}</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.message__mail-head {
padding-bottom: var(--space-small);
margin-bottom: var(--space-small);
border-bottom: 1px solid var(--w-300);
&.is-incoming {
border-bottom: 1px solid var(--color-border-light);
}
}
.meta-wrap {
.message__content--type {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-mini);
}
span {
font-size: var(--font-size-mini);
}
}
</style>

View File

@@ -1,57 +0,0 @@
<script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
name: 'ReplyTo',
components: {
MessagePreview,
},
props: {
message: {
type: Object,
required: true,
},
messageType: {
type: Number,
required: true,
},
parentHasAttachments: {
type: Boolean,
required: true,
},
},
data() {
return { MESSAGE_TYPE };
},
methods: {
scrollToMessage() {
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
messageId: this.message.id,
});
},
},
};
</script>
<template>
<div
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
:class="{
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
'-mx-2': !parentHasAttachments,
}"
@click="scrollToMessage"
>
<MessagePreview
class="cursor-pointer"
:message="message"
:show-message-type="false"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
/>
</div>
</template>

View File

@@ -1,162 +0,0 @@
<script>
import { Letter } from 'vue-letter';
import GalleryView from '../components/GalleryView.vue';
export default {
components: { Letter, GalleryView },
props: {
message: {
type: String,
default: '',
},
isEmail: {
type: Boolean,
default: true,
},
displayQuotedButton: {
type: Boolean,
default: false,
},
},
data() {
return {
showQuotedContent: false,
showGalleryViewer: false,
attachment: {},
availableAttachments: [],
};
},
computed: {
isQuotedContentPresent() {
if (!this.isEmail) {
return this.message.includes('<blockquote');
}
return this.showQuotedContent;
},
showQuoteToggle() {
if (!this.isEmail) {
return false;
}
return this.displayQuotedButton;
},
},
methods: {
toggleQuotedContent() {
this.showQuotedContent = !this.showQuotedContent;
},
handleClickOnContent(event) {
// if event target is IMG and not close in A tag
// then open image preview
const isImageElement = event.target.tagName === 'IMG';
const isWrappedInLink = event.target.closest('A');
if (isImageElement && !isWrappedInLink) {
this.openImagePreview(event.target.src);
}
},
openImagePreview(src) {
this.showGalleryViewer = true;
this.attachment = {
file_type: 'image',
data_url: src,
message_id: Math.floor(Math.random() * 100),
};
this.availableAttachments = [{ ...this.attachment }];
},
onClose() {
this.showGalleryViewer = false;
this.resetAttachmentData();
},
resetAttachmentData() {
this.attachment = {};
this.availableAttachments = [];
},
},
};
</script>
<template>
<div
class="message-text__wrap"
:class="{
'show--quoted': isQuotedContentPresent,
'hide--quoted': !isQuotedContentPresent,
}"
>
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<div v-else @click="handleClickOnContent">
<Letter
class="text-content bg-white dark:bg-white text-slate-900 dark:text-slate-900 p-2 rounded-[4px]"
:html="message"
/>
</div>
<button
v-if="showQuoteToggle"
class="py-1 text-xs cursor-pointer text-slate-300 dark:text-slate-300"
@click="toggleQuotedContent"
>
<span v-if="showQuotedContent" class="flex items-center gap-0.5">
<fluent-icon icon="chevron-up" size="16" />
{{ $t('CHAT_LIST.HIDE_QUOTED_TEXT') }}
</span>
<span v-else class="flex items-center gap-0.5">
<fluent-icon icon="chevron-down" size="16" />
{{ $t('CHAT_LIST.SHOW_QUOTED_TEXT') }}
</span>
</button>
<GalleryView
v-if="showGalleryViewer"
v-model:show="showGalleryViewer"
:attachment="attachment"
:all-attachments="availableAttachments"
@error="onClose"
@close="onClose"
/>
</div>
</template>
<style lang="scss">
.text-content {
overflow: auto;
ul,
ol {
padding-left: var(--space-two);
}
table {
margin: 0;
border: 0;
td {
margin: 0;
border: 0;
}
tr {
border-bottom: 0 !important;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: var(--font-size-normal);
}
}
.show--quoted {
blockquote {
@apply block;
}
}
.hide--quoted {
blockquote {
@apply hidden;
}
}
</style>

View File

@@ -1,43 +0,0 @@
<script>
export default {
props: {
url: {
type: String,
required: true,
},
},
emits: ['error'],
data() {
return {
show: false,
};
},
mounted() {
this.$refs.videoElement.onerror = () => {
this.$emit('error');
};
},
methods: {
onClose() {
this.show = false;
},
onClick() {
this.show = true;
},
},
};
</script>
<template>
<div class="video message-text__wrap">
<video ref="videoElement" :src="url" muted playsInline @click="onClick" />
<woot-modal v-model:show="show" :on-close="onClose">
<video
:src="url"
controls
playsInline
class="modal-video skip-context-menu mx-auto"
/>
</woot-modal>
</div>
</template>

View File

@@ -1,98 +0,0 @@
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
messageId: {
type: Number,
required: true,
},
},
data() {
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
},
computed: {
meetingLink() {
return buildDyteURL(this.dyteAuthToken);
},
},
methods: {
async joinTheCall() {
this.isLoading = true;
try {
const { data: { token } = {} } = await DyteAPI.addParticipantToMeeting(
this.messageId
);
this.dyteAuthToken = token;
} catch (err) {
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
},
leaveTheRoom() {
this.dyteAuthToken = '';
},
},
};
</script>
<template>
<div>
<NextButton
blue
sm
icon="i-lucide-video"
:label="$t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN')"
:is-loading="isLoading"
@click="joinTheCall"
/>
<div v-if="dyteAuthToken" class="video-call--container">
<iframe
:src="meetingLink"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<NextButton
sm
class="mt-2"
:label="$t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM')"
@click="leaveTheRoom"
/>
</div>
</div>
</template>
<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: 10rem;
}
}
</style>

View File

@@ -21,11 +21,11 @@ export default {
align-items: center;
flex-direction: column;
justify-content: center;
padding: var(--space-normal) 0;
min-width: calc(var(--space-mega) * 2);
padding: 1rem 0;
min-width: calc(6.25rem * 2);
p {
margin: var(--space-small) 0 0 0;
margin: 0.5rem 0 0 0;
}
}
</style>

View File

@@ -18,10 +18,7 @@ export default {
</script>
<template>
<div
class="menu text-slate-800 dark:text-slate-100 min-h-7 min-w-0"
role="button"
>
<div class="menu text-n-slate-12 min-h-7 min-w-0" role="button">
<fluent-icon
v-if="variant === 'icon' && option.icon"
:icon="option.icon"
@@ -49,7 +46,7 @@ export default {
<style scoped lang="scss">
.menu {
width: calc(var(--space-mega) * 2);
width: calc(6.25rem * 2);
@apply flex items-center flex-nowrap p-1 rounded-md overflow-hidden cursor-pointer;
.menu-label {
@@ -57,7 +54,7 @@ export default {
}
&:hover {
@apply bg-n-brand text-white dark:text-slate-50;
@apply bg-n-brand text-white;
}
}

View File

@@ -40,7 +40,7 @@ const submenuPosition = computed(() => [
<template>
<div
ref="menuRef"
class="text-slate-800 dark:text-slate-100 menu-with-submenu min-width-calc w-full p-1 flex items-center h-7 rounded-md relative bg-n-alpha-3/50 backdrop-blur-[100px] justify-between hover:bg-n-brand/10 cursor-pointer dark:hover:bg-n-solid-3"
class="text-n-slate-12 menu-with-submenu min-width-calc w-full p-1 flex items-center h-7 rounded-md relative bg-n-alpha-3/50 backdrop-blur-[100px] justify-between hover:bg-n-brand/10 cursor-pointer dark:hover:bg-n-solid-3"
:class="!subMenuAvailable ? 'opacity-50 cursor-not-allowed' : ''"
>
<div class="flex items-center h-4">

View File

@@ -154,7 +154,7 @@ export default {
<template>
<li
v-if="shouldShowSuggestions"
class="label-suggestion right"
class="label-suggestion right list-none"
@mouseover="isHovered = true"
@mouseleave="isHovered = false"
>
@@ -180,9 +180,7 @@ export default {
<woot-label
variant="dashed"
v-bind="label"
:bg-color="
selectedLabels.includes(label.title) ? 'var(--w-100)' : ''
"
:bg-color="selectedLabels.includes(label.title) ? '#2781F6' : ''"
/>
</button>
<NextButton
@@ -253,17 +251,14 @@ export default {
.label-suggestion {
flex-direction: row;
justify-content: flex-end;
margin-top: var(--space-normal);
margin-top: 1rem;
.label-suggestion--container {
max-width: 300px;
}
.label-suggestion--options {
text-align: right;
display: flex;
align-items: center;
gap: var(--space-micro);
@apply gap-0.5 text-end flex items-center;
button.label-suggestion--option {
.label {
@@ -274,14 +269,12 @@ export default {
}
.chatwoot-ai-icon {
height: var(--font-size-mini);
width: var(--font-size-mini);
height: 0.75rem;
width: 0.75rem;
}
.label-suggestion--title {
color: var(--b-600);
margin-top: var(--space-micro);
font-size: var(--font-size-micro);
@apply text-n-slate-11 mt-0.5 text-xxs;
}
}
</style>

View File

@@ -129,7 +129,7 @@ export default {
:username="agent.name"
size="22px"
/>
<span class="my-0 text-slate-800 dark:text-slate-75">
<span class="my-0 text-n-slate-12">
{{ agent.name }}
</span>
</div>
@@ -181,7 +181,7 @@ export default {
<style scoped lang="scss">
.bulk-action__agents {
@apply max-w-[75%] absolute right-2 top-12 origin-top-right w-auto z-20 min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
@apply max-w-[75%] absolute ltr:right-2 rtl:left-2 top-12 origin-top-right w-auto z-20 min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
.header {
@apply p-2.5;
@@ -198,7 +198,7 @@ export default {
.agent-list-search {
@apply py-0 px-2.5 bg-n-alpha-black2 border border-solid border-n-strong rounded-md;
.search-icon {
@apply text-slate-400 dark:text-slate-200;
@apply text-n-slate-10;
}
.agent--search_input {
@@ -207,8 +207,7 @@ export default {
}
}
.triangle {
right: var(--triangle-position);
@apply block z-10 absolute -top-3 text-left;
@apply block z-10 absolute -top-3 text-left ltr:right-[--triangle-position] rtl:left-[--triangle-position];
svg path {
@apply fill-n-alpha-3 backdrop-blur-[100px] stroke-n-weak;
@@ -248,6 +247,6 @@ ul {
}
.agent__list-loading {
@apply m-2.5 rounded-md bg-slate-25 dark:bg-slate-900 flex items-center justify-center flex-col p-5 h-[calc(95%-6.25rem)];
@apply m-2.5 rounded-md dark:bg-n-solid-3 bg-n-slate-2 flex items-center justify-center flex-col p-5 h-[calc(95%-6.25rem)];
}
</style>

View File

@@ -262,15 +262,6 @@ export default {
</template>
<style scoped lang="scss">
// For RTL direction view
.app-rtl--wrapper {
.bulk-action__actions {
::v-deep .button--only-icon:last-child {
margin-right: var(--space-smaller);
}
}
}
.bulk-action__container {
@apply p-4 relative border-b border-solid border-n-strong dark:border-n-weak;
}
@@ -288,7 +279,7 @@ export default {
}
.bulk-action__alert {
@apply bg-yellow-50 text-yellow-700 rounded text-xs mt-2 py-1 px-2 border border-solid border-yellow-300 dark:border-yellow-300/10 dark:bg-yellow-200/20 dark:text-yellow-400;
@apply bg-n-amber-3 text-n-amber-12 rounded text-xs mt-2 py-1 px-2 border border-solid border-n-amber-5;
}
.popover-animation-enter-active,

View File

@@ -130,7 +130,7 @@ export default {
@apply bg-n-alpha-black2 py-0 px-2.5 border border-solid border-n-strong rounded-md;
.search-icon {
@apply text-slate-400 dark:text-slate-200;
@apply text-n-slate-10;
}
.label--search_input {
@@ -139,7 +139,7 @@ export default {
}
.labels-container {
@apply absolute right-2 top-12 origin-top-right w-auto z-20 max-w-[15rem] min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
@apply absolute ltr:right-2 rtl:left-2 top-12 origin-top-right w-auto z-20 max-w-[15rem] min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
.header {
@apply p-2.5;
@@ -158,8 +158,7 @@ export default {
}
.triangle {
right: var(--triangle-position);
@apply block z-10 absolute text-left -top-3;
@apply block z-10 absolute text-left -top-3 ltr:right-[--triangle-position] rtl:left-[--triangle-position];
svg path {
@apply fill-n-alpha-3 backdrop-blur-[100px] stroke-n-weak;
@@ -182,7 +181,7 @@ ul {
@apply items-center rounded-md cursor-pointer flex py-1 px-2.5 hover:bg-n-slate-3 dark:hover:bg-n-solid-3;
&.label-selected {
@apply bg-slate-50 dark:bg-slate-900;
@apply bg-n-slate-2;
}
span {
@@ -190,7 +189,7 @@ ul {
}
.label-checkbox {
@apply my-0 mr-2.5 ml-0;
@apply my-0 ltr:mr-2.5 rtl:ml-2.5;
}
.label-title {
@@ -198,7 +197,7 @@ ul {
}
.label-pill {
@apply bg-slate-50 rounded-md h-3 w-3 flex-shrink-0 border border-solid border-n-weak;
@apply rounded-md h-3 w-3 flex-shrink-0 border border-solid border-n-weak;
}
}
}
@@ -206,12 +205,4 @@ ul {
.search-container {
@apply bg-n-alpha-3 backdrop-blur-[100px] py-0 px-2.5 sticky top-0 z-20;
}
.actions-container {
@apply bg-white dark:bg-slate-900 bottom-0 p-2 sticky z-20;
button {
@apply w-full;
}
}
</style>

View File

@@ -66,9 +66,7 @@ export default {
<template v-if="filteredTeams.length">
<li v-for="team in filteredTeams" :key="team.id">
<div class="team__list-item" @click="assignTeam(team)">
<span
class="my-0 ltr:ml-2 rtl:mr-2 text-slate-800 dark:text-slate-75"
>
<span class="my-0 ltr:ml-2 rtl:mr-2 text-n-slate-12">
{{ team.name }}
</span>
</div>
@@ -76,9 +74,7 @@ export default {
</template>
<li v-else>
<div class="team__list-item">
<span
class="my-0 ltr:ml-2 rtl:mr-2 text-slate-800 dark:text-slate-75"
>
<span class="my-0 ltr:ml-2 rtl:mr-2 text-n-slate-12">
{{ $t('BULK_ACTION.TEAMS.NO_TEAMS_AVAILABLE') }}
</span>
</div>
@@ -91,7 +87,7 @@ export default {
<style scoped lang="scss">
.bulk-action__teams {
@apply max-w-[75%] absolute right-2 top-12 origin-top-right w-auto z-20 min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
@apply max-w-[75%] absolute ltr:right-2 rtl:left-2 top-12 origin-top-right w-auto z-20 min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
.header {
@apply p-2.5;
@@ -108,7 +104,7 @@ export default {
.agent-list-search {
@apply py-0 px-2.5 bg-n-alpha-black2 border border-solid border-n-strong rounded-md;
.search-icon {
@apply text-slate-400 dark:text-slate-200;
@apply text-n-slate-10;
}
.agent--search_input {
@@ -117,8 +113,7 @@ export default {
}
}
.triangle {
right: var(--triangle-position);
@apply block z-10 absolute text-left -top-3;
@apply block z-10 absolute text-left -top-3 ltr:right-[--triangle-position] rtl:left-[--triangle-position];
svg path {
@apply fill-n-alpha-3 backdrop-blur-[100px] stroke-n-weak;

View File

@@ -68,7 +68,7 @@ const actionLabel = key => {
<template>
<div
v-on-clickaway="onClose"
class="absolute z-20 w-auto origin-top-right border border-solid rounded-lg shadow-md right-2 top-12 bg-n-alpha-3 backdrop-blur-[100px] border-n-weak"
class="absolute z-20 w-auto origin-top-right border border-solid rounded-lg shadow-md ltr:right-2 rtl:left-2 top-12 bg-n-alpha-3 backdrop-blur-[100px] border-n-weak"
>
<div
class="right-[var(--triangle-position)] block z-10 absolute text-left -top-3"
@@ -83,7 +83,7 @@ const actionLabel = key => {
</svg>
</div>
<div class="p-2.5 flex gap-1 items-center justify-between">
<span class="text-sm font-medium text-slate-600 dark:text-slate-100">
<span class="text-sm font-medium text-n-slate-12">
{{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }}
</span>
<Button ghost xs slate icon="i-lucide-x" @click="onClose" />
@@ -96,7 +96,7 @@ const actionLabel = key => {
ghost
sm
slate
class="!w-full ltr:!justify-start rtl:!justify-end"
class="!w-full !justify-start"
:icon="action.icon"
:label="actionLabel(action.key)"
@click="updateConversations(action.key)"

View File

@@ -86,7 +86,7 @@ const onShowLabels = e => {
? $t('CONVERSATION.CARD.HIDE_LABELS')
: $t('CONVERSATION.CARD.SHOW_LABELS')
"
class="h-5 py-0 px-1 flex-shrink-0 mr-6 ml-0 rtl:ml-6 rtl:mr-0 rtl:rotate-180 text-slate-700 dark:text-slate-200 border-n-strong dark:border-n-strong"
class="h-5 py-0 px-1 flex-shrink-0 mr-6 ml-0 rtl:ml-6 rtl:mr-0 rtl:rotate-180 text-n-slate-11 border-n-strong dark:border-n-strong"
@click="onShowLabels"
>
<fluent-icon

View File

@@ -1,87 +0,0 @@
import { CSAT_RATINGS } from '../../../../../shared/constants/messages';
const generateInputSelectContent = contentAttributes => {
const { submitted_values: submittedValues = [] } = contentAttributes;
const [selectedOption] = submittedValues;
if (selectedOption && selectedOption.title) {
return `<strong>${selectedOption.title}</strong>`;
}
return '';
};
const generateInputEmailContent = contentAttributes => {
const { submitted_email: submittedEmail = '' } = contentAttributes;
if (submittedEmail) {
return `<strong>${submittedEmail}</strong>`;
}
return '';
};
const generateFormContent = (contentAttributes, { noResponseText }) => {
const { items, submitted_values: submittedValues = [] } = contentAttributes;
if (submittedValues.length) {
const submittedObject = submittedValues.reduce((acc, keyValuePair) => {
acc[keyValuePair.name] = keyValuePair.value;
return acc;
}, {});
let formMessageContent = '';
items.forEach(item => {
formMessageContent += `<div>${item.label}</div>`;
const response = submittedObject[item.name] || noResponseText;
formMessageContent += `<strong>${response}</strong><br/><br/>`;
});
return formMessageContent;
}
return '';
};
const generateCSATContent = (
contentAttributes,
{ ratingTitle, feedbackTitle }
) => {
const {
submitted_values: { csat_survey_response: surveyResponse = {} } = {},
} = contentAttributes;
const { rating, feedback_message } = surveyResponse || {};
let messageContent = '';
if (rating) {
const [ratingObject = {}] = CSAT_RATINGS.filter(
csatRating => csatRating.value === rating
);
messageContent += `<div><strong>${ratingTitle}</strong></div>`;
messageContent += `<p>${ratingObject.emoji}</p>`;
}
if (feedback_message) {
messageContent += `<div><strong>${feedbackTitle}</strong></div>`;
messageContent += `<p>${feedback_message}</p>`;
}
return messageContent;
};
export const generateBotMessageContent = (
contentType,
contentAttributes,
{
noResponseText = 'No response',
csat: { ratingTitle = 'Rating', feedbackTitle = 'Feedback' } = {},
} = {}
) => {
const contentTypeMethods = {
input_select: generateInputSelectContent,
input_email: generateInputEmailContent,
form: generateFormContent,
input_csat: generateCSATContent,
};
const contentTypeMethod = contentTypeMethods[contentType];
if (contentTypeMethod && typeof contentTypeMethod === 'function') {
return contentTypeMethod(contentAttributes, {
noResponseText,
ratingTitle,
feedbackTitle,
});
}
return '';
};

View File

@@ -1,66 +0,0 @@
import { generateBotMessageContent } from '../botMessageContentHelper';
describe('#generateBotMessageContent', () => {
it('return correct input_select content', () => {
expect(
generateBotMessageContent('input_select', {
submitted_values: [{ value: 'salad', title: 'Salad' }],
})
).toEqual('<strong>Salad</strong>');
});
it('return correct input_email content', () => {
expect(
generateBotMessageContent('input_email', {
submitted_email: 'hello@chatwoot.com',
})
).toEqual('<strong>hello@chatwoot.com</strong>');
});
it('return correct input_csat content', () => {
expect(
generateBotMessageContent('input_csat', {
submitted_values: {
csat_survey_response: {
rating: 5,
feedback_message: 'Great Service',
},
},
})
).toEqual(
'<div><strong>Rating</strong></div><p>😍</p><div><strong>Feedback</strong></div><p>Great Service</p>'
);
expect(
generateBotMessageContent(
'input_csat',
{
submitted_values: {
csat_survey_response: { rating: 1, feedback_message: '' },
},
},
{ csat: { ratingTitle: 'റേറ്റിംഗ്', feedbackTitle: 'പ്രതികരണം' } }
)
).toEqual('<div><strong>റേറ്റിംഗ്</strong></div><p>😞</p>');
});
it('return correct form content', () => {
expect(
generateBotMessageContent('form', {
items: [
{
name: 'large_text',
label: 'This a large text',
},
{
name: 'email',
label: 'Email',
},
],
submitted_values: [{ name: 'large_text', value: 'Large Text Content' }],
})
).toEqual(
'<div>This a large text</div><strong>Large Text Content</strong><br/><br/><div>Email</div><strong>No response</strong><br/><br/>'
);
});
});

View File

@@ -58,7 +58,7 @@ const onClickTabChange = index => {
/>
<div class="flex flex-col h-auto overflow-auto">
<div class="flex flex-col px-8 pb-4">
<div class="flex flex-col px-8 pb-4 mt-1">
<woot-tabs
class="ltr:[&>ul]:pl-0 rtl:[&>ul]:pr-0"
:index="selectedTabIndex"
@@ -70,6 +70,7 @@ const onClickTabChange = index => {
:index="index"
:name="tab.name"
:show-badge="false"
is-compact
/>
</woot-tabs>
</div>

View File

@@ -53,7 +53,7 @@ export default {
:src="src"
:username="usernameAvatar"
/>
<div v-if="src && deleteAvatar" class="avatar-delete-btn">
<div v-if="src && deleteAvatar" class="my-1">
<NextButton
outline
xs
@@ -74,10 +74,3 @@ export default {
</label>
</div>
</template>
<style lang="scss" scoped>
.avatar-delete-btn {
margin-top: var(--space-smaller);
margin-bottom: var(--space-smaller);
}
</style>

View File

@@ -194,7 +194,7 @@ export default {
</div>
<span
v-if="activeDialCode"
class="flex py-2 pl-2 pr-0 text-base font-normal leading-normal text-n-slate-12"
class="flex py-2 ltr:pl-2 rtl:pr-2 text-base font-normal leading-normal text-n-slate-12"
>
{{ activeDialCode }}
</span>

View File

@@ -92,7 +92,7 @@ const variableKey = (item = {}) => {
>
<slot :item="item" :index="index" :selected="index === selectedIndex">
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-n-slate-11 group-hover:text-n-slate-11 text-ellipsis whitespace-nowrap"
class="max-w-full min-w-0 mb-0 overflow-hidden text-sm font-medium text-n-slate-11 group-hover:text-n-slate-12 text-ellipsis whitespace-nowrap"
:class="{
'text-n-slate-12': index === selectedIndex,
}"
@@ -100,7 +100,7 @@ const variableKey = (item = {}) => {
{{ item.description }}
</p>
<p
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-slate-500 dark:text-slate-300 group-hover:text-n-slate-11 text-ellipsis whitespace-nowrap"
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-n-slate-11 group-hover:text-n-slate-12 text-ellipsis whitespace-nowrap"
:class="{
'text-n-slate-12': index === selectedIndex,
}"

View File

@@ -43,7 +43,7 @@ onMounted(async () => {
/>
<div class="grid grid-cols-2 px-8 pt-0 pb-4 mt-6 gap-x-5 gap-y-3">
<div class="flex justify-between items-center min-w-[25rem]">
<h5 class="text-sm text-slate-800 dark:text-slate-100">
<h5 class="text-sm text-n-slate-12">
{{ $t('KEYBOARD_SHORTCUTS.TOGGLE_MODAL') }}
</h5>
<div class="flex items-center gap-2 mb-1 ml-2">
@@ -63,7 +63,7 @@ onMounted(async () => {
:key="shortcut.id"
class="flex justify-between items-center min-w-[25rem]"
>
<h5 class="text-sm text-slate-800 min-w-[36px] dark:text-slate-100">
<h5 class="text-sm text-n-slate-12 min-w-[36px]">
{{ title(shortcut) }}
</h5>
<div class="flex items-center gap-2 mb-1 ml-2">
@@ -83,7 +83,7 @@ onMounted(async () => {
</template>
<span
v-else
class="flex items-center text-sm font-semibold text-slate-800 dark:text-slate-100"
class="flex items-center text-sm font-semibold text-n-slate-12"
>
{{ key }}
</span>
@@ -97,6 +97,6 @@ onMounted(async () => {
<style scoped>
.key {
@apply py-2 px-2.5 font-semibold text-xs text-slate-700 dark:text-slate-100 bg-slate-75 dark:bg-slate-900 shadow border-b-2 rtl:border-l-2 ltr:border-r-2 border-slate-200 dark:border-slate-700;
@apply py-2 px-2.5 font-semibold text-xs text-n-slate-12 bg-n-slate-4 dark:bg-n-slate-2 shadow border-b-2 rtl:border-l-2 ltr:border-r-2 border-n-strong;
}
</style>