FEAT: Refatorado componentes dos chats, removido mock e adicionado marcador de data
This commit is contained in:
parent
fbdbca7f20
commit
3343a12548
@ -110,7 +110,7 @@ function UnreadBadge({ count }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SavedContactLabel({ contact }) {
|
||||
function SavedContactIcon({ contact }) {
|
||||
const profile = contact.contactProfile;
|
||||
const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note);
|
||||
if (!hasSavedContact) return null;
|
||||
@ -118,12 +118,23 @@ function SavedContactLabel({ contact }) {
|
||||
return (
|
||||
<span
|
||||
title="Contato salvo na agenda"
|
||||
aria-label="Contato salvo na agenda"
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
border: '1px solid rgba(183, 121, 31, 0.28)',
|
||||
background: 'rgba(183, 121, 31, 0.1)',
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b7791f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='10' cy='7' r='4'/%3E%3Cpath d='m17 11 2 2 4-4'/%3E%3C/svg%3E\")",
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 14,
|
||||
color: '#b7791f',
|
||||
flex: '0 0 auto',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 800,
|
||||
lineHeight: 1,
|
||||
display: 'inline-grid',
|
||||
placeItems: 'center',
|
||||
fontSize: 0,
|
||||
}}
|
||||
>
|
||||
•Salvo•
|
||||
@ -213,9 +224,9 @@ export function ChatConversationList({
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
|
||||
<SavedContactIcon contact={contact} />
|
||||
<ChannelBadge channel={contact.channel} />
|
||||
<SpecialtyBadge contact={contact} />
|
||||
<SavedContactLabel contact={contact} />
|
||||
</span>
|
||||
<UnreadBadge count={contact.unread} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
function getMediaUrl(media) {
|
||||
if (!media?.data || !media?.mimetype) return '';
|
||||
@ -29,6 +29,68 @@ function formatMessageTime(timestamp) {
|
||||
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function getMessageDate(timestamp) {
|
||||
if (!timestamp) return null;
|
||||
const numericTimestamp = Number(timestamp);
|
||||
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
function getDateKey(timestamp) {
|
||||
const date = getMessageDate(timestamp);
|
||||
if (!date) return '';
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatDateSeparator(timestamp) {
|
||||
const date = getMessageDate(timestamp);
|
||||
if (!date) return '';
|
||||
|
||||
const today = new Date();
|
||||
const isToday =
|
||||
date.getFullYear() === today.getFullYear() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
|
||||
if (isToday) return 'Hoje';
|
||||
|
||||
return date.toLocaleDateString('pt-BR');
|
||||
}
|
||||
|
||||
function DateSeparator({ label }) {
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto 1fr',
|
||||
gap: '0.75rem',
|
||||
alignItems: 'center',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 800,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
<span style={{ height: 1, background: 'var(--color-border)' }} />
|
||||
<span
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 999,
|
||||
padding: '0.28rem 0.7rem',
|
||||
background: 'rgba(255,255,255,0.88)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ height: 1, background: 'var(--color-border)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
|
||||
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
|
||||
const mimetype = message.media?.mimetype || '';
|
||||
@ -414,91 +476,99 @@ export function ChatWindow({
|
||||
'radial-gradient(circle at top left, rgba(0, 164, 183, 0.06), transparent 22%), linear-gradient(180deg, rgba(245, 248, 251, 0.8), rgba(255, 255, 255, 0.95))',
|
||||
}}
|
||||
>
|
||||
{messages.map((message) => {
|
||||
{messages.map((message, index) => {
|
||||
const isAgent = message.sender === 'agent';
|
||||
const isSystem = message.sender === 'system';
|
||||
const parsedText = parseMessageText(message.text);
|
||||
const messageTime = formatMessageTime(message.timestamp);
|
||||
const dateKey = getDateKey(message.timestamp);
|
||||
const previousDateKey = index > 0 ? getDateKey(messages[index - 1]?.timestamp) : '';
|
||||
const shouldShowDateSeparator = dateKey && dateKey !== previousDateKey;
|
||||
const dateSeparator = formatDateSeparator(message.timestamp);
|
||||
|
||||
if (isSystem) {
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
style={{
|
||||
justifySelf: 'center',
|
||||
padding: '0.7rem 1rem',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(0, 49, 80, 0.08)',
|
||||
color: 'var(--color-primary)',
|
||||
fontSize: '0.88rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
<Fragment key={message.id}>
|
||||
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||
<div
|
||||
style={{
|
||||
justifySelf: 'center',
|
||||
padding: '0.7rem 1rem',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(0, 49, 80, 0.08)',
|
||||
color: 'var(--color-primary)',
|
||||
fontSize: '0.88rem',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
style={{
|
||||
justifySelf: isAgent ? 'end' : 'start',
|
||||
maxWidth: isMobile ? '88%' : '72%',
|
||||
padding: '0.95rem 1rem',
|
||||
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
|
||||
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
|
||||
color: isAgent ? '#fff' : 'var(--color-text)',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
display: 'grid',
|
||||
gap: '0.65rem',
|
||||
}}
|
||||
>
|
||||
<MediaRenderer
|
||||
message={message}
|
||||
contactId={safeContact.id}
|
||||
onLoadMedia={onLoadMedia}
|
||||
isAgent={isAgent}
|
||||
/>
|
||||
{parsedText.senderLabel ? (
|
||||
<strong
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: '0.78rem',
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '0.02em',
|
||||
textTransform: 'uppercase',
|
||||
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
|
||||
}}
|
||||
>
|
||||
{parsedText.senderLabel}
|
||||
</strong>
|
||||
) : null}
|
||||
{parsedText.body ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{parsedText.body}
|
||||
</span>
|
||||
) : null}
|
||||
{messageTime ? (
|
||||
<span
|
||||
style={{
|
||||
justifySelf: 'end',
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1,
|
||||
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
|
||||
}}
|
||||
>
|
||||
{messageTime}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Fragment key={message.id}>
|
||||
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||
<div
|
||||
style={{
|
||||
justifySelf: isAgent ? 'end' : 'start',
|
||||
maxWidth: isMobile ? '88%' : '72%',
|
||||
padding: '0.95rem 1rem',
|
||||
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
|
||||
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
|
||||
color: isAgent ? '#fff' : 'var(--color-text)',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
display: 'grid',
|
||||
gap: '0.65rem',
|
||||
}}
|
||||
>
|
||||
<MediaRenderer
|
||||
message={message}
|
||||
contactId={safeContact.id}
|
||||
onLoadMedia={onLoadMedia}
|
||||
isAgent={isAgent}
|
||||
/>
|
||||
{parsedText.senderLabel ? (
|
||||
<strong
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: '0.78rem',
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '0.02em',
|
||||
textTransform: 'uppercase',
|
||||
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
|
||||
}}
|
||||
>
|
||||
{parsedText.senderLabel}
|
||||
</strong>
|
||||
) : null}
|
||||
{parsedText.body ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{parsedText.body}
|
||||
</span>
|
||||
) : null}
|
||||
{messageTime ? (
|
||||
<span
|
||||
style={{
|
||||
justifySelf: 'end',
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1,
|
||||
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
|
||||
}}
|
||||
>
|
||||
{messageTime}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -606,7 +676,7 @@ export function ChatWindow({
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onSend();
|
||||
onSend?.(draft);
|
||||
}
|
||||
}}
|
||||
disabled={!safeContact.id || !canReply}
|
||||
@ -633,7 +703,7 @@ export function ChatWindow({
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSend}
|
||||
onClick={() => onSend?.(draft)}
|
||||
disabled={!safeContact.id || !canReply}
|
||||
style={{
|
||||
border: 'none',
|
||||
|
||||
@ -3,14 +3,9 @@ import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
||||
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
|
||||
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
||||
import { transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
||||
|
||||
function buildInitialMessages() {
|
||||
return chatContacts.reduce((acc, contact) => {
|
||||
acc[contact.id] = contact.messages;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
|
||||
|
||||
function getLastMessageFromMe(messages = []) {
|
||||
const lastMessage = [...messages].reverse().find(isDisplayableMessage);
|
||||
@ -145,15 +140,6 @@ function fileToBase64(file) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildFallbackContacts() {
|
||||
return chatContacts.map((contact) => ({
|
||||
...contact,
|
||||
assignment: null,
|
||||
areaId: null,
|
||||
lastMessageFromMe: getLastMessageFromMe(contact.messages),
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeComparableContact(contact) {
|
||||
return {
|
||||
id: contact.id,
|
||||
@ -217,14 +203,14 @@ export function useChat() {
|
||||
const currentUserAreas = getUserAreas(currentUser);
|
||||
const isAdminUser = currentUserProfile === 'admin';
|
||||
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
|
||||
const [contacts, setContacts] = useState(buildFallbackContacts);
|
||||
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id);
|
||||
const [messagesByContact, setMessagesByContact] = useState(buildInitialMessages);
|
||||
const [contacts, setContacts] = useState([]);
|
||||
const [activeContactId, setActiveContactId] = useState('');
|
||||
const [messagesByContact, setMessagesByContact] = useState({});
|
||||
const [draft, setDraft] = useState('');
|
||||
const [attachedFile, setAttachedFile] = useState(null);
|
||||
const [areaOptions, setAreaOptions] = useState([]);
|
||||
const [accessUsers, setAccessUsers] = useState([]);
|
||||
const [selectedArea, setSelectedArea] = useState(chatContacts[0].area);
|
||||
const [selectedArea, setSelectedArea] = useState('Sem fila');
|
||||
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
||||
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
|
||||
const [transferAttendant, setTransferAttendant] = useState('');
|
||||
@ -329,11 +315,9 @@ export function useChat() {
|
||||
|
||||
async function loadChats({ showLoading = false } = {}) {
|
||||
if (whatsappStatus !== 'CONNECTED') {
|
||||
const fallbackContacts = buildFallbackContacts();
|
||||
setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts));
|
||||
setActiveContactId((current) =>
|
||||
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
|
||||
);
|
||||
setContacts((current) => (current.length ? [] : current));
|
||||
setActiveContactId('');
|
||||
setMessagesByContact({});
|
||||
setIsLoadingChats(false);
|
||||
return;
|
||||
}
|
||||
@ -502,12 +486,18 @@ export function useChat() {
|
||||
|
||||
async function attachFile(file) {
|
||||
if (!file) return;
|
||||
if (file.size > MAX_ATTACHMENT_SIZE_BYTES) {
|
||||
setApiError('Arquivo muito grande. Envie uma mídia de até 15 MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await fileToBase64(file);
|
||||
setAttachedFile({
|
||||
name: file.name,
|
||||
type: file.type || 'application/octet-stream',
|
||||
data,
|
||||
});
|
||||
setApiError(null);
|
||||
}
|
||||
|
||||
function removeAttachedFile() {
|
||||
@ -597,7 +587,8 @@ export function useChat() {
|
||||
}
|
||||
|
||||
async function sendMessage(messageText = draft, contactId = activeContactId) {
|
||||
const trimmed = String(messageText || '').trim();
|
||||
const rawMessage = typeof messageText === 'string' ? messageText : draft;
|
||||
const trimmed = rawMessage.trim();
|
||||
if (!trimmed && !attachedFile) return;
|
||||
|
||||
const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact;
|
||||
@ -650,7 +641,7 @@ export function useChat() {
|
||||
if (!contactId.includes('@')) return;
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE_URL}/whatsapp/send`, {
|
||||
const response = await fetch(`${API_BASE_URL}/whatsapp/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@ -660,6 +651,15 @@ export function useChat() {
|
||||
media,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
response.status === 413
|
||||
? 'Arquivo muito grande para envio. Tente uma mídia menor.'
|
||||
: 'Não foi possível enviar a mensagem.';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
setApiError(null);
|
||||
updateContact(contactId, (contact) => ({
|
||||
...contact,
|
||||
|
||||
@ -1,74 +1,7 @@
|
||||
export const chatContacts = [
|
||||
{
|
||||
id: 'maria-souza',
|
||||
name: 'Maria Souza',
|
||||
channel: 'WhatsApp',
|
||||
status: 'away',
|
||||
area: 'Suporte',
|
||||
lastSeen: 'Última atividade as 09:42',
|
||||
preview: 'Preciso atualizar o cadastro do meu pedido.',
|
||||
time: '09:42',
|
||||
unread: 2,
|
||||
messages: [
|
||||
{ id: 1, sender: 'customer', text: 'Oi, bom dia! Preciso de ajuda com meu pedido.' },
|
||||
{ id: 2, sender: 'agent', text: 'Bom dia, Maria! Claro, me conta o que aconteceu.' },
|
||||
{ id: 3, sender: 'customer', text: 'Quero confirmar se o endereço foi alterado.' },
|
||||
{ id: 4, sender: 'agent', text: 'Estou verificando aqui e te atualizo em instantes.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'joao-pedro',
|
||||
name: 'João Pedro',
|
||||
channel: 'SMS',
|
||||
status: 'offline',
|
||||
area: 'Financeiro',
|
||||
lastSeen: 'Última atividade as 08:15',
|
||||
preview: 'Pode me ligar em 10 minutos?',
|
||||
time: '08:15',
|
||||
unread: 1,
|
||||
messages: [
|
||||
{ id: 1, sender: 'customer', text: 'Recebi a cobrança em duplicidade.' },
|
||||
{ id: 2, sender: 'agent', text: 'Vou analisar isso agora para você.' },
|
||||
{ id: 3, sender: 'customer', text: 'Pode me ligar em 10 minutos?' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'empresa-alpha',
|
||||
name: 'Empresa Alpha',
|
||||
channel: 'Email',
|
||||
status: 'offline',
|
||||
area: 'Comercial',
|
||||
lastSeen: 'Visto ontem',
|
||||
preview: 'Aguardando retorno sobre a proposta comercial.',
|
||||
time: 'Ontem',
|
||||
unread: 0,
|
||||
messages: [
|
||||
{ id: 1, sender: 'customer', text: 'Precisamos rever os valores da ultima proposta.' },
|
||||
{ id: 2, sender: 'agent', text: 'Perfeito, vou encaminhar para o time comercial.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const transferAreas = ['Suporte', 'Financeiro', 'Comercial'];
|
||||
|
||||
export const attendantsByArea = {
|
||||
Suporte: ['Ana Camolesi', 'Rafael Lopes', 'Romero Britto'],
|
||||
Financeiro: ['Roberto Pêra', 'Monica Limoeira', 'Edson Arantes'],
|
||||
Comercial: ['Natasha Homanoff', 'Helena Pêra', 'Pedro Parque'],
|
||||
};
|
||||
|
||||
export const quickReplies = [
|
||||
'Recebi sua mensagem e já vou verificar.',
|
||||
'Consegue me confirmar o número do protocolo?',
|
||||
'Posso seguir com essa atualização por aqui.',
|
||||
'Recebi sua mensagem e ja vou verificar.',
|
||||
'Consegue me confirmar o numero do protocolo?',
|
||||
'Posso seguir com essa atualizacao por aqui.',
|
||||
];
|
||||
|
||||
export function getMockReply(contactName) {
|
||||
const replies = [
|
||||
`Perfeito, obrigado pelo retorno, ${contactName.split(' ')[0]}.`,
|
||||
'Tudo bem, fico no aguardo dessa confirmação.',
|
||||
'Entendi. Se precisar, posso encaminhar para a especialidade responsável.',
|
||||
];
|
||||
|
||||
return replies[Math.floor(Math.random() * replies.length)];
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
|
||||
import { getCurrentUser } from '../../auth/services/sessionService';
|
||||
@ -123,6 +123,68 @@ function formatMessageTime(timestamp) {
|
||||
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function getMessageDate(timestamp) {
|
||||
if (!timestamp) return null;
|
||||
const numericTimestamp = Number(timestamp);
|
||||
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
function getDateKey(timestamp) {
|
||||
const date = getMessageDate(timestamp);
|
||||
if (!date) return '';
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatDateSeparator(timestamp) {
|
||||
const date = getMessageDate(timestamp);
|
||||
if (!date) return '';
|
||||
|
||||
const today = new Date();
|
||||
const isToday =
|
||||
date.getFullYear() === today.getFullYear() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
|
||||
if (isToday) return 'Hoje';
|
||||
|
||||
return date.toLocaleDateString('pt-BR');
|
||||
}
|
||||
|
||||
function DateSeparator({ label }) {
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto 1fr',
|
||||
gap: '0.7rem',
|
||||
alignItems: 'center',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontSize: '0.74rem',
|
||||
fontWeight: 800,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
<span style={{ height: 1, background: 'var(--color-border)' }} />
|
||||
<span
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 999,
|
||||
padding: '0.24rem 0.62rem',
|
||||
background: 'rgba(255,255,255,0.9)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ height: 1, background: 'var(--color-border)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getUserId(user) {
|
||||
const value = user?.databaseId || user?.id;
|
||||
const numeric = Number(value);
|
||||
@ -408,62 +470,69 @@ export function MessagesWorkspace({
|
||||
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
|
||||
}}
|
||||
>
|
||||
{safeActiveConversation.messages.map((message) => {
|
||||
{safeActiveConversation.messages.map((message, index) => {
|
||||
const isAgent = message.from === 'agent';
|
||||
const parsedText = parseMessageText(message.text);
|
||||
const messageTime = formatMessageTime(message.timestamp);
|
||||
const dateKey = getDateKey(message.timestamp);
|
||||
const previousDateKey =
|
||||
index > 0 ? getDateKey(safeActiveConversation.messages[index - 1]?.timestamp) : '';
|
||||
const shouldShowDateSeparator = dateKey && dateKey !== previousDateKey;
|
||||
const dateSeparator = formatDateSeparator(message.timestamp);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
style={{
|
||||
justifySelf: isAgent ? 'end' : 'start',
|
||||
maxWidth: '72%',
|
||||
padding: '0.95rem 1rem',
|
||||
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
|
||||
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
|
||||
color: isAgent ? '#fff' : 'var(--color-text)',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
display: 'grid',
|
||||
gap: '0.55rem',
|
||||
}}
|
||||
>
|
||||
{parsedText.senderLabel ? (
|
||||
<strong
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: '0.76rem',
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '0.02em',
|
||||
textTransform: 'uppercase',
|
||||
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
|
||||
}}
|
||||
>
|
||||
{parsedText.senderLabel}
|
||||
</strong>
|
||||
) : null}
|
||||
<span
|
||||
<Fragment key={message.id}>
|
||||
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
justifySelf: isAgent ? 'end' : 'start',
|
||||
maxWidth: '72%',
|
||||
padding: '0.95rem 1rem',
|
||||
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
|
||||
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
|
||||
color: isAgent ? '#fff' : 'var(--color-text)',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
display: 'grid',
|
||||
gap: '0.55rem',
|
||||
}}
|
||||
>
|
||||
{parsedText.body}
|
||||
</span>
|
||||
{messageTime ? (
|
||||
{parsedText.senderLabel ? (
|
||||
<strong
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: '0.76rem',
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '0.02em',
|
||||
textTransform: 'uppercase',
|
||||
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
|
||||
}}
|
||||
>
|
||||
{parsedText.senderLabel}
|
||||
</strong>
|
||||
) : null}
|
||||
<span
|
||||
style={{
|
||||
justifySelf: 'end',
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1,
|
||||
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{messageTime}
|
||||
{parsedText.body}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{messageTime ? (
|
||||
<span
|
||||
style={{
|
||||
justifySelf: 'end',
|
||||
fontSize: '0.72rem',
|
||||
lineHeight: 1,
|
||||
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
|
||||
}}
|
||||
>
|
||||
{messageTime}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user