fix: bubble UI issues (#10608)
This PR has fixes for the following issues - Inconsistent spacing between meta and text in text bubble - Activity bubble overflows for longer text (for now I have truncated it, I'll work with @absurdiya on a better solution) - Ugly lookinh gradient for expand button on email bubble - Email bubble overflow issues and text rendering issues - Alignment for error message - Minute-wise grouping not working - Link color should not be blue - Use `gray-3` for bubble background instead of `gray-4`
This commit is contained in:
@@ -218,9 +218,11 @@ const gridTemplate = computed(() => {
|
|||||||
const map = {
|
const map = {
|
||||||
[ORIENTATION.LEFT]: `
|
[ORIENTATION.LEFT]: `
|
||||||
"avatar bubble"
|
"avatar bubble"
|
||||||
|
"spacer meta"
|
||||||
`,
|
`,
|
||||||
[ORIENTATION.RIGHT]: `
|
[ORIENTATION.RIGHT]: `
|
||||||
"bubble"
|
"bubble"
|
||||||
|
"meta"
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -399,6 +401,7 @@ provideMessageContext({
|
|||||||
class="[grid-area:bubble] flex"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'pl-9 justify-end': orientation === ORIENTATION.RIGHT,
|
'pl-9 justify-end': orientation === ORIENTATION.RIGHT,
|
||||||
|
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
}"
|
}"
|
||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed } from 'vue';
|
import { defineProps, computed } from 'vue';
|
||||||
import Message from './Message.vue';
|
import Message from './Message.vue';
|
||||||
|
import { MESSAGE_TYPES } from './constants.js';
|
||||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +12,7 @@ import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
|||||||
* @property {Number} currentUserId - ID of the current user
|
* @property {Number} currentUserId - ID of the current user
|
||||||
* @property {Boolean} isAnEmailChannel - Whether this is an email channel
|
* @property {Boolean} isAnEmailChannel - Whether this is an email channel
|
||||||
* @property {Object} inboxSupportsReplyTo - Inbox reply support configuration
|
* @property {Object} inboxSupportsReplyTo - Inbox reply support configuration
|
||||||
* @property {Array} messages - Array of all messages
|
* @property {Array} messages - Array of all messages [These are not in camelcase]
|
||||||
*/
|
*/
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
readMessages: {
|
readMessages: {
|
||||||
@@ -51,20 +52,29 @@ const read = computed(() => {
|
|||||||
/**
|
/**
|
||||||
* Determines if a message should be grouped with the next message
|
* Determines if a message should be grouped with the next message
|
||||||
* @param {Number} index - Index of the current message
|
* @param {Number} index - Index of the current message
|
||||||
* @param {Array} messages - Array of messages to check
|
* @param {Array} searchList - Array of messages to check
|
||||||
* @returns {Boolean} - Whether the message should be grouped with next
|
* @returns {Boolean} - Whether the message should be grouped with next
|
||||||
*/
|
*/
|
||||||
const shouldGroupWithNext = (index, messages) => {
|
const shouldGroupWithNext = (index, searchList) => {
|
||||||
if (index === messages.length - 1) return false;
|
if (index === searchList.length - 1) return false;
|
||||||
|
|
||||||
const current = messages[index];
|
const current = searchList[index];
|
||||||
const next = messages[index + 1];
|
const next = searchList[index + 1];
|
||||||
|
|
||||||
if (next.status === 'failed') return false;
|
if (next.status === 'failed') return false;
|
||||||
|
|
||||||
const nextSenderId = next.senderId ?? next.sender?.id;
|
const nextSenderId = next.senderId ?? next.sender?.id;
|
||||||
const currentSenderId = current.senderId ?? current.sender?.id;
|
const currentSenderId = current.senderId ?? current.sender?.id;
|
||||||
if (currentSenderId !== nextSenderId) return false;
|
const hasSameSender = nextSenderId === currentSenderId;
|
||||||
|
|
||||||
|
const nextMessageType = next.messageType;
|
||||||
|
const currentMessageType = current.messageType;
|
||||||
|
|
||||||
|
const areBothTemplates =
|
||||||
|
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
|
||||||
|
currentMessageType === MESSAGE_TYPES.TEMPLATE;
|
||||||
|
|
||||||
|
if (!hasSameSender || !areBothTemplates) return false;
|
||||||
|
|
||||||
// Check if messages are in the same minute by rounding down to nearest minute
|
// Check if messages are in the same minute by rounding down to nearest minute
|
||||||
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
|
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
|
||||||
@@ -101,7 +111,7 @@ const getInReplyToMessage = parentMessage => {
|
|||||||
v-bind="message"
|
v-bind="message"
|
||||||
:is-email-inbox="isAnEmailChannel"
|
:is-email-inbox="isAnEmailChannel"
|
||||||
:in-reply-to="getInReplyToMessage(message)"
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
:group-with-next="shouldGroupWithNext(index, readMessages)"
|
:group-with-next="shouldGroupWithNext(index, read)"
|
||||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
data-clarity-mask="True"
|
data-clarity-mask="True"
|
||||||
@@ -112,7 +122,7 @@ const getInReplyToMessage = parentMessage => {
|
|||||||
<Message
|
<Message
|
||||||
v-bind="message"
|
v-bind="message"
|
||||||
:in-reply-to="getInReplyToMessage(message)"
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
:group-with-next="shouldGroupWithNext(index, unReadMessages)"
|
:group-with-next="shouldGroupWithNext(index, unread)"
|
||||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:is-email-inbox="isAnEmailChannel"
|
:is-email-inbox="isAnEmailChannel"
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const statusToShow = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-xs flex items-center gap-1.5">
|
<div class="text-xs flex items-center gap-1.5">
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<span class="inline">{{ readableTime }}</span>
|
<time class="inline">{{ readableTime }}</time>
|
||||||
</div>
|
</div>
|
||||||
<Icon v-if="isPrivate" icon="i-lucide-lock-keyhole" class="size-3" />
|
<Icon v-if="isPrivate" icon="i-lucide-lock-keyhole" class="size-3" />
|
||||||
<MessageStatus v-if="showStatusIndicator" :status="statusToShow" />
|
<MessageStatus v-if="showStatusIndicator" :status="statusToShow" />
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ const readableTime = computed(() =>
|
|||||||
class="px-2 py-0.5 !rounded-full flex items-center gap-2"
|
class="px-2 py-0.5 !rounded-full flex items-center gap-2"
|
||||||
data-bubble-name="activity"
|
data-bubble-name="activity"
|
||||||
>
|
>
|
||||||
<span v-dompurify-html="content" />
|
<span v-dompurify-html="content" :title="content" class="truncate" />
|
||||||
<div v-if="readableTime" class="w-px h-3 rounded-full bg-n-slate-7" />
|
<div v-if="readableTime" class="w-px h-3 rounded-full bg-n-slate-7" />
|
||||||
<span class="text-n-slate-10">
|
<time class="text-n-slate-10 truncate flex-shrink" :title="readableTime">
|
||||||
{{ readableTime }}
|
{{ readableTime }}
|
||||||
</span>
|
</time>
|
||||||
</BaseBubble>
|
</BaseBubble>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ const varaintBaseMap = {
|
|||||||
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
|
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.PRIVATE]:
|
[MESSAGE_VARIANTS.PRIVATE]:
|
||||||
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
|
||||||
[MESSAGE_VARIANTS.USER]: 'bg-n-gray-4 text-n-slate-12',
|
[MESSAGE_VARIANTS.USER]: 'bg-n-gray-3 text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
|
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
|
||||||
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
|
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
|
||||||
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
|
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
|
||||||
[MESSAGE_VARIANTS.EMAIL]: 'bg-n-alpha-2 w-full',
|
[MESSAGE_VARIANTS.EMAIL]: 'bg-n-gray-3 w-full',
|
||||||
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
[MESSAGE_VARIANTS.UNSUPPORTED]:
|
||||||
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
|
||||||
};
|
};
|
||||||
@@ -81,7 +81,7 @@ const previewMessage = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="text-sm min-w-32 break-words"
|
class="text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
messageClass,
|
messageClass,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const showMeta = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
v-show="showMeta"
|
v-show="showMeta"
|
||||||
class="p-4 space-y-1 pr-9 border-b border-n-strong"
|
class="space-y-1 pr-9 border-b border-n-strong text-sm"
|
||||||
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
||||||
>
|
>
|
||||||
<template v-if="showMeta">
|
<template v-if="showMeta">
|
||||||
|
|||||||
@@ -48,59 +48,62 @@ const textToShow = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseBubble class="w-full overflow-hidden" data-bubble-name="email">
|
<BaseBubble class="w-full" data-bubble-name="email">
|
||||||
<EmailMeta />
|
<EmailMeta class="p-3" />
|
||||||
<section
|
<section ref="contentContainer" class="p-3">
|
||||||
ref="contentContainer"
|
|
||||||
class="p-4"
|
|
||||||
:class="{
|
|
||||||
'max-h-[400px] overflow-hidden relative': !isExpanded && isExpandable,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-if="isExpandable && !isExpanded"
|
:class="{
|
||||||
class="absolute left-0 right-0 bottom-0 h-40 p-8 flex items-end bg-gradient-to-t dark:from-[#24252b] from-[#F5F5F6] dark:via-[rgba(36,37,43,0.5)] via-[rgba(245,245,246,0.50)] dark:to-transparent to-[rgba(245,245,246,0.00)]"
|
'max-h-[400px] overflow-hidden relative': !isExpanded && isExpandable,
|
||||||
|
'overflow-y-scroll relative': isExpanded,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
v-if="isExpandable && !isExpanded"
|
||||||
@click="isExpanded = true"
|
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-gray-3 via-n-gray-3 via-20% to-transparent"
|
||||||
>
|
>
|
||||||
<Icon icon="i-lucide-maximize-2" />
|
<button
|
||||||
{{ $t('EMAIL_HEADER.EXPAND') }}
|
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
|
||||||
|
@click="isExpanded = true"
|
||||||
|
>
|
||||||
|
<Icon icon="i-lucide-maximize-2" />
|
||||||
|
{{ $t('EMAIL_HEADER.EXPAND') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FormattedContent v-if="isOutgoing && content" :content="content" />
|
||||||
|
<template v-else>
|
||||||
|
<Letter
|
||||||
|
v-if="showQuotedMessage"
|
||||||
|
class-name="prose prose-email !max-w-none"
|
||||||
|
:html="fullHTML"
|
||||||
|
:text="textToShow"
|
||||||
|
/>
|
||||||
|
<Letter
|
||||||
|
v-else
|
||||||
|
class-name="prose prose-email !max-w-none"
|
||||||
|
:html="unquotedHTML"
|
||||||
|
:text="textToShow"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
v-if="hasQuotedMessage"
|
||||||
|
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
||||||
|
@click="showQuotedMessage = !showQuotedMessage"
|
||||||
|
>
|
||||||
|
<template v-if="showQuotedMessage">
|
||||||
|
{{ $t('CHAT_LIST.HIDE_QUOTED_TEXT') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('CHAT_LIST.SHOW_QUOTED_TEXT') }}
|
||||||
|
</template>
|
||||||
|
<Icon
|
||||||
|
:icon="
|
||||||
|
showQuotedMessage
|
||||||
|
? 'i-lucide-chevron-up'
|
||||||
|
: 'i-lucide-chevron-down'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<FormattedContent v-if="isOutgoing && content" :content="content" />
|
|
||||||
<template v-else>
|
|
||||||
<Letter
|
|
||||||
v-if="showQuotedMessage"
|
|
||||||
class-name="prose prose-email !max-w-none"
|
|
||||||
:html="fullHTML"
|
|
||||||
:text="textToShow"
|
|
||||||
/>
|
|
||||||
<Letter
|
|
||||||
v-else
|
|
||||||
class-name="prose prose-email !max-w-none"
|
|
||||||
:html="unquotedHTML"
|
|
||||||
:text="textToShow"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<button
|
|
||||||
v-if="hasQuotedMessage"
|
|
||||||
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
|
|
||||||
@click="showQuotedMessage = !showQuotedMessage"
|
|
||||||
>
|
|
||||||
<template v-if="showQuotedMessage">
|
|
||||||
{{ $t('CHAT_LIST.HIDE_QUOTED_TEXT') }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ $t('CHAT_LIST.SHOW_QUOTED_TEXT') }}
|
|
||||||
</template>
|
|
||||||
<Icon
|
|
||||||
:icon="
|
|
||||||
showQuotedMessage ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</section>
|
</section>
|
||||||
<section v-if="attachments.length" class="px-4 pb-4 space-y-2">
|
<section v-if="attachments.length" class="px-4 pb-4 space-y-2">
|
||||||
<AttachmentChips :attachments="attachments" class="gap-1" />
|
<AttachmentChips :attachments="attachments" class="gap-1" />
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ const formattedContent = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-dompurify-html="formattedContent"
|
v-dompurify-html="formattedContent"
|
||||||
class="[&>p:last-child]:mb-0 [&>ul]:list-inside"
|
class="[&_.link]:text-n-slate-11 [&_.link]:underline [&>p:last-child]:mb-0 [&>ul]:list-inside"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,20 +19,22 @@ const isEmpty = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseBubble class="flex flex-col gap-3 px-4 py-3" data-bubble-name="text">
|
<BaseBubble class="px-4 py-3" data-bubble-name="text">
|
||||||
<span v-if="isEmpty" class="text-n-slate-11">
|
<div class="gap-3 flex flex-col">
|
||||||
{{ $t('CONVERSATION.NO_CONTENT') }}
|
<span v-if="isEmpty" class="text-n-slate-11">
|
||||||
</span>
|
{{ $t('CONVERSATION.NO_CONTENT') }}
|
||||||
<FormattedContent v-if="content" :content="content" />
|
</span>
|
||||||
<AttachmentChips :attachments="attachments" class="gap-2" />
|
<FormattedContent v-if="content" :content="content" />
|
||||||
<template v-if="isTemplate">
|
<AttachmentChips :attachments="attachments" class="gap-2" />
|
||||||
<div
|
<template v-if="isTemplate">
|
||||||
v-if="contentAttributes.submittedEmail"
|
<div
|
||||||
class="px-2 py-1 rounded-lg bg-n-alpha-3"
|
v-if="contentAttributes.submittedEmail"
|
||||||
>
|
class="px-2 py-1 rounded-lg bg-n-alpha-3"
|
||||||
{{ contentAttributes.submittedEmail }}
|
>
|
||||||
</div>
|
{{ contentAttributes.submittedEmail }}
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</BaseBubble>
|
</BaseBubble>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const tailwindConfig = {
|
|||||||
typography: {
|
typography: {
|
||||||
email: {
|
email: {
|
||||||
css: {
|
css: {
|
||||||
color: 'rgb(var(--slate-11))',
|
color: 'rgb(var(--slate-12))',
|
||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
'*': {
|
'*': {
|
||||||
@@ -129,16 +129,16 @@ const tailwindConfig = {
|
|||||||
th: {
|
th: {
|
||||||
padding: '0.75em',
|
padding: '0.75em',
|
||||||
color: 'rgb(var(--slate-12))',
|
color: 'rgb(var(--slate-12))',
|
||||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
border: `none`,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
tr: {
|
tr: {
|
||||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
border: `none`,
|
||||||
},
|
},
|
||||||
td: {
|
td: {
|
||||||
padding: '0.75em',
|
padding: '0.75em',
|
||||||
borderBottom: `1px solid rgb(var(--border-strong))`,
|
border: `none`,
|
||||||
},
|
},
|
||||||
img: {
|
img: {
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
|||||||
Reference in New Issue
Block a user