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:
Shivam Mishra
2024-12-21 13:36:46 +05:30
committed by GitHub
parent c52282307a
commit c19d70a6a0
10 changed files with 102 additions and 84 deletions

View File

@@ -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)"
> >

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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,
{ {

View File

@@ -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">

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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%',